├── .clang-format ├── .devcontainer ├── Dockerfile └── devcontainer.json ├── .github ├── dependabot.yml └── workflows │ ├── build_wheels.yml │ ├── bumpr.yml │ ├── codeql-analysis.yml │ ├── test.yml │ ├── update-starlark.yml │ └── valgrind.yml ├── .gitignore ├── .readthedocs.yaml ├── LICENSE ├── MANIFEST.in ├── README.md ├── development.txt ├── docs ├── Makefile ├── make.bat ├── requirements.txt └── source │ ├── conf.py │ ├── index.md │ ├── reference.md │ └── usage.md ├── go.mod ├── go.sum ├── pyproject.toml ├── python_eval.go ├── python_exceptions.go ├── python_globals.go ├── python_object.go ├── python_print.go ├── python_to_starlark.go ├── scripts ├── install-go.sh ├── pytest-valgrind.sh ├── pytest-valgrind.supp └── update-starlark.sh ├── setup.cfg ├── setup.py ├── src └── starlark_go │ ├── __init__.py │ ├── errors.py │ ├── py.typed │ └── starlark_go.pyi ├── starlark.c ├── starlark.h ├── starlark_to_python.go └── tests ├── __init__.py ├── fibonacci.star ├── test_configure.py ├── test_conversion_to_python_failed.py ├── test_conversion_to_starlark_failed.py ├── test_evalerror.py ├── test_fibonacci.py ├── test_get_globals.py ├── test_import_exceptions.py ├── test_multi_exec.py ├── test_pop_global.py ├── test_print.py ├── test_resolveerror.py ├── test_set_globals.py ├── test_syntaxerror.py └── test_values.py /.clang-format: -------------------------------------------------------------------------------- 1 | # Try to bend clang-format into formatting C like black formats Python 2 | --- 3 | BasedOnStyle: LLVM 4 | AllowShortIfStatementsOnASingleLine: true 5 | AllowShortBlocksOnASingleLine: true 6 | AllowShortFunctionsOnASingleLine: false 7 | BreakBeforeBraces: Linux 8 | ReflowComments: true 9 | BinPackArguments: false 10 | BinPackParameters: false 11 | AlignAfterOpenBracket: BlockIndent 12 | AllowAllArgumentsOnNextLine: true 13 | Cpp11BracedListStyle: true 14 | ColumnLimit: 88 15 | PenaltyReturnTypeOnItsOwnLine: 1000 16 | -------------------------------------------------------------------------------- /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG BASE_IMAGE=quay.io/pypa/manylinux_2_24_x86_64 2 | ARG GO_URL=https://go.dev/dl/go1.20.2.linux-amd64.tar.gz 3 | ARG CLANG_URL=https://github.com/llvm/llvm-project/releases/download/llvmorg-14.0.0/clang+llvm-14.0.0-x86_64-linux-gnu-ubuntu-18.04.tar.xz 4 | ARG SHELLCHECK_URL=https://github.com/koalaman/shellcheck/releases/download/v0.8.0/shellcheck-v0.8.0.linux.x86_64.tar.xz 5 | ARG MCFLY_URL=https://github.com/cantino/mcfly/releases/download/v0.6.0/mcfly-v0.6.0-x86_64-unknown-linux-musl.tar.gz 6 | 7 | ARG USERNAME=builder 8 | ARG USER_UID=501 9 | ARG USER_GID=$USER_UID 10 | 11 | 12 | FROM ${BASE_IMAGE} AS packages 13 | 14 | ENV DEBIAN_FRONTEND=noninteractive 15 | 16 | RUN apt-get update && apt-get upgrade -y && \ 17 | apt-get install -y \ 18 | build-essential gdb less libffi-dev valgrind \ 19 | curl ca-certificates gnupg2 tar g++ gcc libc6-dev make pkg-config 20 | 21 | RUN curl -sS https://starship.rs/install.sh | sh -s -- -y 22 | 23 | 24 | FROM packages AS go_install 25 | 26 | ARG GO_URL 27 | 28 | ADD ${GO_URL} /usr/src/go.tar.gz 29 | 30 | RUN tar -C /opt -xvf /usr/src/go.tar.gz 31 | 32 | 33 | FROM packages AS builder 34 | 35 | COPY --from=go_install /opt/go/ /opt/go/ 36 | 37 | ENV PATH=/opt/go/bin:/opt/valgrind/bin:$PATH 38 | 39 | ARG USERNAME USER_UID USER_GID 40 | 41 | RUN groupadd -g ${USER_GID} ${USERNAME} && useradd -m -u ${USER_UID} -g ${USERNAME} -s /bin/bash ${USERNAME} 42 | 43 | USER ${USERNAME} 44 | WORKDIR /home/${USERNAME} 45 | ENV USER=${USERNAME} SHELL=/bin/bash GOPATH=/home/${USERNAME}/go 46 | 47 | 48 | FROM builder AS go_tools 49 | 50 | RUN go install golang.org/x/tools/gopls@latest 51 | RUN go install github.com/go-delve/delve/cmd/dlv@latest 52 | RUN go install github.com/ramya-rao-a/go-outline@latest 53 | RUN go install github.com/josharian/impl@latest 54 | RUN go install github.com/fatih/gomodifytags@latest 55 | RUN go install github.com/haya14busa/goplay/cmd/goplay@latest 56 | RUN go install github.com/cweill/gotests/...@latest 57 | RUN go install honnef.co/go/tools/cmd/staticcheck@latest 58 | RUN go install mvdan.cc/gofumpt@latest 59 | RUN go install mvdan.cc/sh/v3/cmd/shfmt@latest 60 | RUN curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.45.2 61 | 62 | 63 | FROM builder AS misc 64 | 65 | ARG SHELLCHECK_URL MCFLY_URL CLANG_URL USERNAME 66 | 67 | ADD --chown=${USERNAME} ${SHELLCHECK_URL} /usr/src/shellcheck.tar.xz 68 | 69 | RUN mkdir /tmp/shellcheck && tar -C /tmp/shellcheck --strip-components=1 -xvf /usr/src/shellcheck.tar.xz 70 | 71 | ADD --chown=${USERNAME} ${MCFLY_URL} /usr/src/mcfly.tar.gz 72 | 73 | RUN mkdir /tmp/mcfly && tar -C /tmp/mcfly -xvf /usr/src/mcfly.tar.gz 74 | 75 | ADD --chown=${USERNAME} ${CLANG_URL} /usr/src/clang.tar.xz 76 | 77 | RUN mkdir /tmp/clang && tar -C /tmp/clang --strip-components=1 -xvf /usr/src/clang.tar.xz 78 | 79 | 80 | FROM builder AS devcontainer 81 | 82 | ARG USERNAME 83 | 84 | RUN echo "set auto-load safe-path /" > /home/${USERNAME}/.gdbinit 85 | 86 | COPY --from=go_tools --chown=${USERNAME} /home/${USERNAME}/go/bin/ /home/${USERNAME}/go/bin 87 | 88 | RUN python3.10 -m venv /home/${USERNAME}/venv 89 | 90 | ENV PATH=/home/${USERNAME}/venv/bin:/opt/python/cp310-cp310/bin:${PATH} 91 | 92 | RUN python3.10 -m pip install black ipython isort memray pytest pytest-memray pytest-valgrind tox 93 | 94 | COPY --from=misc --chown=${USERNAME} /tmp/shellcheck/shellcheck /home/${USERNAME}/.local/bin/shellcheck 95 | COPY --from=misc --chown=${USERNAME} /tmp/mcfly/mcfly /home/${USERNAME}/.local/bin/mcfly 96 | COPY --from=misc --chown=${USERNAME} /tmp/clang/bin/clang-format /home/${USERNAME}/.local/bin/clang-format 97 | 98 | RUN echo 'eval "$(mcfly init bash)"' >> ~/.bashrc && touch ~/.bash_history 99 | RUN echo 'eval "$(starship init bash)"' >> ~/.bashrc 100 | 101 | ENV PATH=/home/${USERNAME}/go/bin:/home/${USERNAME}/.local/bin:$PATH 102 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "manylinux + go", 3 | "build": { 4 | "dockerfile": "Dockerfile", 5 | }, 6 | "settings": { 7 | "python.defaultInterpreterPath": "/home/builder/venv/bin/python3", 8 | "go.toolsEnvVars": { 9 | "CGO_CFLAGS": "-I/opt/python/cp310-cp310/include/python3.10", 10 | "CGO_LDFLAGS": "-Wl,--unresolved-symbols=ignore-all" 11 | }, 12 | "C_Cpp.default.includePath": [ 13 | "/opt/python/cp310-cp310/include/python3.10" 14 | ], 15 | "C_Cpp.formatting": "clangFormat", 16 | "C_Cpp.clang_format_style": "file", 17 | "C_Cpp.clang_format_fallbackStyle": "LLVM", 18 | "C_Cpp.clang_format_path": "/home/builder/.local/bin/clang-format", 19 | "[c]": { 20 | "editor.defaultFormatter": "ms-vscode.cpptools" 21 | }, 22 | "editor.formatOnSave": true, 23 | "go.lintTool": "golangci-lint", 24 | "gopls": { 25 | "formatting.gofumpt": true 26 | }, 27 | }, 28 | "extensions": [ 29 | "bierner.github-markdown-preview", 30 | "eamodio.gitlens", 31 | "foxundermoon.shell-format", 32 | "golang.go", 33 | "ms-azuretools.vscode-docker", 34 | "ms-python.python", 35 | "ms-python.vscode-pylance", 36 | "ms-vscode.cpptools", 37 | "oderwat.indent-rainbow", 38 | "tamasfe.even-better-toml", 39 | "timonwong.shellcheck" 40 | ], 41 | } 42 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "github-actions" 9 | directory: "/" 10 | schedule: 11 | interval: "daily" 12 | 13 | - package-ecosystem: "gomod" # See documentation for possible values 14 | directory: "/" # Location of package manifests 15 | schedule: 16 | interval: "daily" 17 | 18 | - package-ecosystem: "pip" # See documentation for possible values 19 | directory: "/" # Location of package manifests 20 | schedule: 21 | interval: "daily" 22 | -------------------------------------------------------------------------------- /.github/workflows/build_wheels.yml: -------------------------------------------------------------------------------- 1 | name: Build wheels 2 | 3 | on: 4 | pull_request: 5 | push: 6 | tags: 7 | - "v*" 8 | 9 | jobs: 10 | # Build the source distribution for PyPI 11 | build_sdist: 12 | name: Build sdist 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v3 17 | with: 18 | fetch-depth: 0 19 | 20 | - name: Set up Python 21 | uses: actions/setup-python@v4 22 | with: 23 | python-version: "3.12" 24 | 25 | - name: Build sdist 26 | run: | 27 | python3.12 -m pip install --upgrade setuptools wheel 28 | python3.12 setup.py sdist 29 | 30 | - uses: actions/upload-artifact@v3 31 | with: 32 | path: dist/*.tar.gz 33 | 34 | # Build binary distributions for PyPI 35 | build_wheels: 36 | name: Build on ${{ matrix.os }} for ${{matrix.cibw_python}} ${{matrix.cibw_arch}} 37 | runs-on: ${{ matrix.os }} 38 | strategy: 39 | fail-fast: false 40 | matrix: 41 | # Windows isn't working right now: https://github.com/caketop/python-starlark-go/issues/4 42 | os: [ubuntu-latest, macos-latest] 43 | cibw_python: ["cp38-*", "cp39-*", "cp310-*", "cp311-*", "cp312-*", "cp313-*"] 44 | cibw_arch: ["i686", "x86_64", "aarch64", "arm64"] 45 | include: 46 | - cibw_arch: arm64 47 | goarch: arm64 48 | - cibw_arch: aarch64 49 | goarch: arm64 50 | - cibw_arch: x86_64 51 | goarch: amd64 52 | exclude: 53 | - os: ubuntu-latest 54 | cibw_arch: arm64 55 | - os: macos-latest 56 | cibw_arch: i686 57 | - os: macos-latest 58 | cibw_arch: aarch64 59 | - os: macos-latest 60 | cibw_python: "cp37-*" 61 | cibw_arch: arm64 62 | 63 | steps: 64 | - uses: actions/checkout@v3 65 | with: 66 | fetch-depth: 0 67 | 68 | - uses: actions/setup-go@v5 69 | with: 70 | go-version: 'stable' 71 | if: runner.os != 'Linux' 72 | 73 | - name: Set up QEMU 74 | if: runner.os == 'Linux' 75 | uses: docker/setup-qemu-action@v3.0.0 76 | 77 | - name: Build wheels 78 | uses: pypa/cibuildwheel@v2.20.0 79 | env: 80 | CIBW_BUILD_VERBOSITY: 1 81 | CIBW_BUILD: ${{ matrix.cibw_python }} 82 | CIBW_ARCHS: ${{ matrix.cibw_arch }} 83 | GOARCH: ${{ matrix.goarch }} 84 | CGO_ENABLED: 1 85 | 86 | - uses: actions/upload-artifact@v3 87 | with: 88 | path: wheelhouse/starlark_go-*.whl 89 | 90 | # Create a GitHub release 91 | github_release: 92 | name: Create GitHub release 93 | if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') 94 | needs: [build_wheels, build_sdist] 95 | runs-on: ubuntu-latest 96 | permissions: 97 | contents: write 98 | 99 | steps: 100 | - uses: actions/checkout@v3 101 | with: 102 | fetch-depth: 0 103 | 104 | - uses: actions/download-artifact@v3 105 | with: 106 | name: artifact 107 | path: dist 108 | 109 | - name: "✏️ Generate release changelog" 110 | id: changelog 111 | uses: heinrichreimer/github-changelog-generator-action@v2.4 112 | with: 113 | filterByMilestone: false 114 | onlyLastTag: true 115 | pullRequests: true 116 | prWoLabels: true 117 | token: ${{ secrets.GITHUB_TOKEN }} 118 | verbose: true 119 | 120 | - name: Create GitHub release 121 | uses: softprops/action-gh-release@v2 122 | with: 123 | body: ${{ steps.changelog.outputs.changelog }} 124 | files: dist/**/* 125 | 126 | # Test PyPI 127 | test_pypi_publish: 128 | name: Test publishing to PyPI 129 | needs: [build_wheels, build_sdist] 130 | runs-on: ubuntu-latest 131 | 132 | steps: 133 | - uses: actions/download-artifact@v3 134 | with: 135 | name: artifact 136 | path: dist 137 | 138 | - uses: pypa/gh-action-pypi-publish@v1.8.7 139 | with: 140 | user: __token__ 141 | password: ${{ secrets.TEST_PYPI_TOKEN }} 142 | repository_url: https://test.pypi.org/legacy/ 143 | skip_existing: true 144 | 145 | # Publish to PyPI 146 | pypi_publish: 147 | name: Publish to PyPI 148 | if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') 149 | needs: [build_wheels, build_sdist] 150 | runs-on: ubuntu-latest 151 | 152 | steps: 153 | - uses: actions/download-artifact@v3 154 | with: 155 | name: artifact 156 | path: dist 157 | 158 | - uses: pypa/gh-action-pypi-publish@v1.8.7 159 | with: 160 | user: __token__ 161 | password: ${{ secrets.PYPI_TOKEN }} 162 | -------------------------------------------------------------------------------- /.github/workflows/bumpr.yml: -------------------------------------------------------------------------------- 1 | name: Bump version 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | types: 8 | - labeled 9 | 10 | jobs: 11 | release: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | with: 16 | fetch-depth: 0 17 | 18 | # Bump version on merging Pull Requests with specific labels. 19 | # (bump:major,bump:minor,bump:patch) 20 | - uses: haya14busa/action-bumpr@v1 21 | with: 22 | github_token: ${{ secrets.BUMPR_TOKEN }} 23 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ main ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ main ] 20 | schedule: 21 | - cron: '29 19 * * 4' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'cpp', 'go', 'python' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v3 42 | with: 43 | fetch-depth: 0 44 | 45 | - name: Set up Python 46 | uses: actions/setup-python@v4 47 | with: 48 | python-version: "3.10" 49 | 50 | # Initializes the CodeQL tools for scanning. 51 | - name: Initialize CodeQL 52 | uses: github/codeql-action/init@v3 53 | with: 54 | languages: ${{ matrix.language }} 55 | # If you wish to specify custom queries, you can do so here or in a config file. 56 | # By default, queries listed here will override any specified in a config file. 57 | # Prefix the list here with "+" to use these queries and those in the config file. 58 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 59 | 60 | - name: Build 61 | run: | 62 | python3.10 -m pip install --upgrade pip wheel 63 | python3.10 setup.py build 64 | 65 | - name: Perform CodeQL Analysis 66 | uses: github/codeql-action/analyze@v3 67 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | pull_request: 5 | 6 | jobs: 7 | build: 8 | 9 | runs-on: ubuntu-latest 10 | 11 | strategy: 12 | matrix: 13 | python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] 14 | experimental: [false] 15 | include: 16 | - python-version: '3.13-dev' 17 | # sets continue-on-error automatically 18 | experimental: true 19 | 20 | name: Python ${{ matrix.python-version }} 21 | steps: 22 | - uses: actions/checkout@v3 23 | 24 | - uses: actions/setup-go@v5 25 | 26 | - name: Setup python 27 | uses: actions/setup-python@v4 28 | with: 29 | python-version: ${{ matrix.python-version }} 30 | architecture: x64 31 | 32 | # memray doesn't ship 3.12-dev binaries yet 33 | # https://github.com/bloomberg/memray#building-from-source 34 | - name: Install memray build dependencies for Python -dev versions 35 | if: endsWith(matrix.python-version, '-dev') 36 | env: 37 | DEBIAN_FRONTEND: noninteractive 38 | run: sudo apt-get install --yes --no-install-recommends libunwind-dev liblz4-dev 39 | 40 | - name: Install dependencies 41 | run: | 42 | python -m pip install --upgrade pip wheel 43 | python -m pip install tox tox-gh-actions 44 | 45 | - name: Test with tox 46 | run: | 47 | export PYTHONFAULTHANDLER=1 48 | tox 49 | -------------------------------------------------------------------------------- /.github/workflows/update-starlark.yml: -------------------------------------------------------------------------------- 1 | name: Update starlark-go 2 | on: 3 | schedule: 4 | - cron: '0 * * * *' 5 | workflow_dispatch: 6 | 7 | jobs: 8 | update-starlark-go: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v3 13 | with: 14 | ref: main 15 | fetch-depth: 0 16 | 17 | - uses: actions/setup-go@v5 18 | 19 | - run: ./scripts/update-starlark.sh 20 | 21 | - name: Create Pull Request 22 | uses: peter-evans/create-pull-request@v6 23 | if: ${{ env.NEW_STARLARK_VERSION != null && env.NEW_STARLARK_VERSION != '' }} 24 | with: 25 | title: Update starlark-go to ${{ env.NEW_STARLARK_VERSION }} 26 | commit-message: Update starlark-go to ${{ env.NEW_STARLARK_VERSION }} 27 | branch: update-starlark-go 28 | token: ${{ secrets.PR_TOKEN }} 29 | -------------------------------------------------------------------------------- /.github/workflows/valgrind.yml: -------------------------------------------------------------------------------- 1 | name: Valgrind 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | build: 8 | 9 | runs-on: ubuntu-latest 10 | 11 | name: Valgrind 12 | steps: 13 | - uses: actions/checkout@v3 14 | with: 15 | fetch-depth: 0 16 | 17 | - uses: actions/setup-go@v5 18 | 19 | - name: Setup python 20 | uses: actions/setup-python@v4 21 | with: 22 | python-version: "3.10" 23 | architecture: x64 24 | 25 | - name: Install valgrind 26 | run: | 27 | sudo apt-get update 28 | DEBIAN_FRONTEND=noninteractive sudo apt-get install valgrind 29 | 30 | - name: Run valgrind 31 | run: ./scripts/pytest-valgrind.sh 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | build/ 3 | dist/ 4 | *.egg-info/ 5 | .DS_Store 6 | *.o 7 | *.so 8 | *.whl 9 | .eggs 10 | .tox 11 | core 12 | wheelhouse 13 | src/starlark_go/scmversion.py 14 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yaml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Set the version of Python and other tools you might need 9 | build: 10 | os: ubuntu-20.04 11 | tools: 12 | python: "3.10" 13 | golang: "1.19" 14 | 15 | # Build documentation in the docs/source directory with Sphinx 16 | sphinx: 17 | configuration: docs/source/conf.py 18 | 19 | # If using Sphinx, optionally build your docs in additional formats such as PDF 20 | # formats: 21 | # - pdf 22 | 23 | # Optionally declare the Python requirements required to build your docs 24 | python: 25 | install: 26 | - method: pip 27 | path: . 28 | - requirements: docs/requirements.txt 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2020 Kevin Chung 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include go.mod 2 | include go.sum 3 | include *.go 4 | include starlark.c 5 | include starlark.h 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # python-starlark-go 2 | 3 | [![PyPI](https://img.shields.io/pypi/v/starlark-go)](https://pypi.org/project/starlark-go/) 4 | ![Tests](https://github.com/caketop/python-starlark-go/actions/workflows/test.yml/badge.svg) 5 | [![Documentation Status](https://readthedocs.org/projects/python-starlark-go/badge/?version=latest)](https://python-starlark-go.readthedocs.io/en/latest/?badge=latest) 6 | [![action-bumpr supported](https://img.shields.io/badge/bumpr-supported-ff69b4?logo=github&link=https://github.com/haya14busa/action-bumpr)](https://github.com/haya14busa/action-bumpr) 7 | 8 | ## Introduction 9 | 10 | This module provides Python bindings for the Starlark programming language. 11 | 12 | Starlark is a dialect of Python designed for hermetic execution and deterministic evaluation. That means you can run Starlark code you don't trust without worrying about it being able access any data you did not explicitly supply to it, and that you can count on the same code to always produce the same value when used with the same input data. Starlark was originally created for the [Bazel build system](https://bazel.build/). There are now several implementations of Starlark; this module is built on [starlark-go](https://github.com/google/starlark-go). 13 | 14 | This module was originally forked from Kevin Chung's [pystarlark](https://github.com/ColdHeat/pystarlark). 15 | 16 | The version of starlark-go that is currently embedded in this module is [v0.0.0-20230302034142-4b1e35fe2254](https://pkg.go.dev/go.starlark.net@v0.0.0-20230302034142-4b1e35fe2254). 17 | 18 | ## Features 19 | 20 | - Evalutes Starlark code from Python 21 | - Translates Starlark exceptions to Python exceptions 22 | - Converts Starlark values to Python values 23 | - Converts Python values to Starlark values 24 | - No runtime dependencies - cgo is used to bundle [starlark-go](https://github.com/google/starlark-go) into a Python extension 25 | 26 | ## Installation 27 | 28 | ``` 29 | pip install starlark-go 30 | ``` 31 | 32 | ## Usage 33 | 34 | ```python 35 | from starlark_go import Starlark 36 | 37 | s = Starlark() 38 | fibonacci = """ 39 | def fibonacci(n=10): 40 | res = list(range(n)) 41 | for i in res[2:]: 42 | res[i] = res[i-2] + res[i-1] 43 | return res 44 | """ 45 | s.exec(fibonacci) 46 | s.eval("fibonacci(5)") # [0, 1, 1, 2, 3] 47 | 48 | s.set(x=5) 49 | s.eval("x") # 5 50 | s.eval("fibonacci(x)") # [0, 1, 1, 2, 3] 51 | ``` 52 | -------------------------------------------------------------------------------- /development.txt: -------------------------------------------------------------------------------- 1 | setuptools-golang==2.9.0 2 | wheel==0.40.0 3 | pytest==7.4.0 4 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | 13 | %SPHINXBUILD% >NUL 2>NUL 14 | if errorlevel 9009 ( 15 | echo. 16 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 17 | echo.installed, then set the SPHINXBUILD environment variable to point 18 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 19 | echo.may add the Sphinx directory to PATH. 20 | echo. 21 | echo.If you don't have Sphinx installed, grab it from 22 | echo.https://www.sphinx-doc.org/ 23 | exit /b 1 24 | ) 25 | 26 | if "%1" == "" goto help 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | Sphinx==6.1.3 2 | importlib-metadata==7.0.2 3 | myst-parser==2.0.0 4 | readthedocs-sphinx-search==0.3.2 5 | sphinx-rtd-theme==2.0.0 6 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | # import os 14 | # import sys 15 | # sys.path.insert(0, os.path.abspath('.')) 16 | 17 | from importlib_metadata import version # type: ignore 18 | 19 | release = version("starlark_go") 20 | version = ".".join(release.split(".")[:2]) 21 | 22 | # -- Project information ----------------------------------------------------- 23 | 24 | project = "python-starlark-go" 25 | copyright = "2023, Jordan Webb" 26 | author = "Jordan Webb" 27 | 28 | 29 | # -- General configuration --------------------------------------------------- 30 | 31 | # Add any Sphinx extension module names here, as strings. They can be 32 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 33 | # ones. 34 | extensions = [ 35 | "sphinx.ext.autodoc", 36 | "sphinx.ext.coverage", 37 | "myst_parser", 38 | "sphinx_rtd_theme", 39 | "sphinx.ext.intersphinx", 40 | "sphinx_search.extension", 41 | ] 42 | 43 | # Add any paths that contain templates here, relative to this directory. 44 | templates_path = ["_templates"] 45 | 46 | # List of patterns, relative to source directory, that match files and 47 | # directories to ignore when looking for source files. 48 | # This pattern also affects html_static_path and html_extra_path. 49 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store", "README.md"] 50 | 51 | # Allow linking to Python docs 52 | intersphinx_mapping = {"python": ("https://docs.python.org/3", None)} 53 | 54 | # -- Options for HTML output ------------------------------------------------- 55 | 56 | # The theme to use for HTML and HTML Help pages. See the documentation for 57 | # a list of builtin themes. 58 | # 59 | html_theme = "sphinx_rtd_theme" 60 | 61 | html_theme_options = { 62 | "home_page_in_toc": True, 63 | "repository_url": "https://github.com/caketop/python-starlark-go", 64 | "repository_branch": "main", 65 | "use_repository_button": True, 66 | "use_issues_button": True, 67 | } 68 | 69 | # Add any paths that contain custom static files (such as style sheets) here, 70 | # relative to this directory. They are copied after the builtin static files, 71 | # so a file named "default.css" will overwrite the builtin "default.css". 72 | html_static_path = ["_static"] 73 | 74 | manpages_url = "https://manpages.debian.org/{path}" 75 | 76 | autodoc_default_options = {"member-order": "groupwise"} 77 | autodoc_preserve_defaults = True 78 | -------------------------------------------------------------------------------- /docs/source/index.md: -------------------------------------------------------------------------------- 1 | ```{include} ../../README.md 2 | ``` 3 | 4 | ## Documentation 5 | 6 | ```{eval-rst} 7 | .. toctree:: 8 | :maxdepth: 3 9 | 10 | usage.md 11 | reference.md 12 | 13 | * :ref:`genindex` 14 | * :ref:`search` 15 | ``` 16 | -------------------------------------------------------------------------------- /docs/source/reference.md: -------------------------------------------------------------------------------- 1 | # API reference 2 | 3 | ```{eval-rst} 4 | .. automodule:: starlark_go 5 | :show-inheritance: 6 | :members: 7 | ``` 8 | -------------------------------------------------------------------------------- /docs/source/usage.md: -------------------------------------------------------------------------------- 1 | # Usage 2 | 3 | ## Create a context 4 | 5 | A {py:obj}`starlark_go.Starlark` object is needed to provide a context (mainly, a set of global variables) for executing Starlark code. Creating an empty one is simple: 6 | 7 | ```python 8 | from starlark_go import Starlark 9 | 10 | s = Starlark() 11 | ``` 12 | 13 | The `globals` keyword argument to the A {py:obj}`starlark_go.Starlark` constructor can be used to pass a dictionary containing some initial global variables: 14 | 15 | ```python 16 | from starlark_go import Starlark 17 | 18 | s = Starlark(globals={"a": 1, "b": 3}) 19 | ``` 20 | 21 | ## Evaluating code 22 | 23 | {py:meth}`starlark_go.Starlark.eval` can be used to evaluate a Starlark expression: 24 | 25 | ```python 26 | from starlark_go import Starlark 27 | 28 | s = Starlark() 29 | 30 | s.eval("2 + 2") # 4 31 | ``` 32 | 33 | Starlark syntax is more-or-less identical to Python. Expressions can reference variables, just like you might in Python: 34 | 35 | ```python 36 | from starlark_go import Starlark 37 | 38 | s = Starlark(globals={"a": 1, "b": 3}) 39 | 40 | s.eval("a + b") # 4 41 | ``` 42 | 43 | ## Defining variables and functions 44 | 45 | {py:meth}`starlark_go.Starlark.eval` is only for evaluating expressions; if you want to define things in Starlark, you'll need to use {py:meth}`starlark_go.Starlark.exec`. 46 | 47 | ```python 48 | from starlark_go import Starlark 49 | 50 | s = Starlark() 51 | 52 | s.exec("a = 1") 53 | s.exec("b = 3") 54 | s.eval("a + b") # 4 55 | ``` 56 | 57 | There is no distinction between variables set by `globals` versus variables set by `exec`; it is simply another way to set a variable. 58 | 59 | {py:meth}`starlark_go.Starlark.exec` can also be used to define functions in Starlark. Remember, Starlark's syntax is more-or-less identical to Python: 60 | 61 | ```python 62 | from starlark_go import Starlark 63 | 64 | s = Starlark() 65 | 66 | s.exec(""" 67 | def add_one(x): 68 | return x + 1 69 | """) 70 | 71 | s.eval("add_one(3)") # 4 72 | ``` 73 | 74 | ## Defining variables from Python 75 | 76 | {py:meth}`starlark_go.Starlark.set` can be used to define one or more Starlark global variables: 77 | 78 | ```python 79 | from starlark_go import Starlark 80 | 81 | s = Starlark() 82 | 83 | s.set(a=1, b=3) 84 | 85 | s.eval("a + b") # 4 86 | ``` 87 | 88 | There is no distinction between variables set by `set` versus other variables; it is simply another way to set a variable. 89 | 90 | ## Retrieving variables 91 | 92 | {py:meth}`starlark_go.Starlark.get` can be used to retrieve a Starlark global variable: 93 | 94 | ```python 95 | from starlark_go import Starlark 96 | 97 | s = Starlark(globals={"b": 3, "c": True}) 98 | 99 | s.exec("a = 1") 100 | s.set(d=[1, 2, 3]) 101 | 102 | s.get("a") # 1 103 | s.get("b") # 3 104 | s.get("c") # True 105 | s.get("d") # [1, 2, 3] 106 | ``` 107 | 108 | A default value can be provided to {py:meth}`starlark_go.Starlark.get`; if one is not provided and the variable you are attempting to retrieve does not exist, a KeyError will be raised: 109 | 110 | ```python 111 | from starlark_go import Starlark 112 | 113 | s = Starlark() 114 | 115 | s.get("e") # !!! raises KeyError !!! 116 | s.get("e", 72) # 72 117 | ``` 118 | 119 | ## Removing variables 120 | 121 | {py:meth}`starlark_go.Starlark.pop` functions identically to {py:meth}`starlark_go.Starlark.get`, except that it removes the variable before returning its value: 122 | 123 | ```python 124 | from starlark_go import Starlark 125 | 126 | s = Starlark(globals={"a": 1, "b": 2}) 127 | 128 | s.eval("a + b") # 4 129 | s.pop("a") # 1 130 | s.eval("a + b") # !!! raises ResolveError !!! 131 | ``` 132 | 133 | ## Overriding the `print()` function 134 | 135 | By default, Starlark's `print()` function is routed to Python's built-in {py:func}`python:print`, but you can provide a different function to override it. 136 | 137 | This can be done when you create the context: 138 | 139 | ```python 140 | import logging 141 | 142 | from starlark_go import Starlark 143 | 144 | s = Starlark(print=logging.warning) 145 | ``` 146 | 147 | ...or after it is created: 148 | 149 | ```python 150 | import logging 151 | 152 | from starlark_go import Starlark 153 | 154 | s = Starlark() 155 | s.print = logging.warning 156 | ``` 157 | 158 | ...or for individual calls to {py:meth}`starlark_go.Starlark.eval` and {py:meth}`starlark_go.Starlark.exec`: 159 | 160 | 161 | ```python 162 | import logging 163 | 164 | from starlark_go import Starlark 165 | 166 | s = Starlark() 167 | s.exec('print("hello!")', print=logging.warning) 168 | ``` 169 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/caketop/python-starlark-go 2 | 3 | go 1.20 4 | 5 | require go.starlark.net v0.0.0-20230302034142-4b1e35fe2254 6 | 7 | require golang.org/x/sys v0.6.0 // indirect 8 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 3 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 4 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 5 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 6 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 7 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 8 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 9 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 10 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 11 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 12 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 13 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 14 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 15 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 16 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 17 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 18 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 19 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 20 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 21 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 22 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 23 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 24 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 25 | github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 26 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 27 | go.starlark.net v0.0.0-20220328144851-d1966c6b9fcd h1:Uo/x0Ir5vQJ+683GXB9Ug+4fcjsbp7z7Ul8UaZbhsRM= 28 | go.starlark.net v0.0.0-20220328144851-d1966c6b9fcd/go.mod h1:t3mmBBPzAVvK0L0n1drDmrQsJ8FoIx4INCqVMTr/Zo0= 29 | go.starlark.net v0.0.0-20230122040757-066229b0515d h1:CTI+tbxvlfu7QlBj+4QjF8YPHoDh71h0/l2tXOM2k0o= 30 | go.starlark.net v0.0.0-20230122040757-066229b0515d/go.mod h1:1NtVfE+l6AHFaY4GmUPGHeLIW8/THkXnym5iweVgCwU= 31 | go.starlark.net v0.0.0-20230128213706-3f75dec8e403 h1:jPeC7Exc+m8OBJUlWbBLh0O5UZPM7yU5W4adnhhbG4U= 32 | go.starlark.net v0.0.0-20230128213706-3f75dec8e403/go.mod h1:jxU+3+j+71eXOW14274+SmmuW82qJzl6iZSeqEtTGds= 33 | go.starlark.net v0.0.0-20230302034142-4b1e35fe2254 h1:Ss6D3hLXTM0KobyBYEAygXzFfGcjnmfEJOBgSbemCtg= 34 | go.starlark.net v0.0.0-20230302034142-4b1e35fe2254/go.mod h1:jxU+3+j+71eXOW14274+SmmuW82qJzl6iZSeqEtTGds= 35 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 36 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 37 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 38 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 39 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 40 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 41 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 42 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 43 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 44 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 45 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 46 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 47 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 48 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 49 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 50 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f h1:+Nyd8tzPX9R7BWHguqsrbFdRx3WQ/1ib8I44HXV5yTA= 51 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 52 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 53 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ= 54 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 55 | golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18= 56 | golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 57 | golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= 58 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 59 | golang.org/x/term v0.0.0-20220526004731-065cf7ba2467/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 60 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 61 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 62 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 63 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 64 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 65 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 66 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 67 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 68 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 69 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 70 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 71 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 72 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 73 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 74 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 75 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 76 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 77 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 78 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 79 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 80 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 81 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 82 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 83 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 84 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 85 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = [ 3 | "setuptools >= 40.6.0", 4 | "wheel", 5 | "setuptools_scm[toml]>=3.4", 6 | "setuptools-golang>=2.7", 7 | ] 8 | build-backend = "setuptools.build_meta" 9 | 10 | [tool.setuptools_scm] 11 | local_scheme = "no-local-version" 12 | write_to = "src/starlark_go/scmversion.py" 13 | 14 | [tool.isort] 15 | profile = "black" 16 | lines_between_types = 1 17 | 18 | [tool.cibuildwheel] 19 | build = "cp37-* cp38-* cp39-* cp310-* cp311-*" 20 | skip = "*-musllinux_*" 21 | test-requires = "pytest" 22 | test-command = "pytest {project}/tests" 23 | 24 | [tool.cibuildwheel.linux] 25 | before-all = "sh ./scripts/install-go.sh" 26 | archs = ["x86_64", "i686", "aarch64"] 27 | 28 | [tool.cibuildwheel.macos] 29 | archs = ["x86_64", "universal2"] 30 | test-skip = ["*_arm64", "*_universal2:arm64"] 31 | -------------------------------------------------------------------------------- /python_eval.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | /* 4 | #include "starlark.h" 5 | */ 6 | import "C" 7 | 8 | import ( 9 | "unsafe" 10 | 11 | "go.starlark.net/starlark" 12 | ) 13 | 14 | // Manage the Python global interpreter lock (GIL) 15 | type PythonEnv interface { 16 | // Detach the GIL and save the thread state 17 | DetachGIL() 18 | 19 | // Re-attach the GIL with the saved thread state 20 | ReattachGIL() 21 | } 22 | 23 | //export Starlark_eval 24 | func Starlark_eval(self *C.Starlark, args *C.PyObject, kwargs *C.PyObject) *C.PyObject { 25 | var ( 26 | expr *C.char 27 | filename *C.char = nil 28 | convert C.uint = 1 29 | print *C.PyObject = nil 30 | goFilename string = "" 31 | ) 32 | 33 | if C.parseEvalArgs(args, kwargs, &expr, &filename, &convert, &print) == 0 { 34 | return nil 35 | } 36 | 37 | print = pythonPrint(self, print) 38 | if print == nil { 39 | return nil 40 | } 41 | 42 | goExpr := C.GoString(expr) 43 | if filename != nil { 44 | goFilename = C.GoString(filename) 45 | } 46 | 47 | state := rlockSelf(self) 48 | if state == nil { 49 | return nil 50 | } 51 | defer state.Mutex.RUnlock() 52 | 53 | state.DetachGIL() 54 | starlarkPrint := func(_ *starlark.Thread, msg string) { 55 | state.ReattachGIL() 56 | defer state.DetachGIL() 57 | 58 | callPythonPrint(print, msg) 59 | } 60 | 61 | thread := &starlark.Thread{Print: starlarkPrint} 62 | result, err := starlark.Eval(thread, goFilename, goExpr, state.Globals) 63 | state.ReattachGIL() 64 | 65 | if err != nil { 66 | raisePythonException(err) 67 | return nil 68 | } 69 | 70 | if convert == 0 { 71 | cstr := C.CString(result.String()) 72 | defer C.free(unsafe.Pointer(cstr)) 73 | return C.cgoPy_BuildString(cstr) 74 | } else { 75 | retval, err := starlarkValueToPython(result) 76 | if err != nil { 77 | return nil 78 | } 79 | 80 | return retval 81 | } 82 | } 83 | 84 | //export Starlark_exec 85 | func Starlark_exec(self *C.Starlark, args *C.PyObject, kwargs *C.PyObject) *C.PyObject { 86 | var ( 87 | defs *C.char 88 | filename *C.char = nil 89 | print *C.PyObject = nil 90 | goFilename string = "" 91 | ) 92 | 93 | if C.parseExecArgs(args, kwargs, &defs, &filename, &print) == 0 { 94 | return nil 95 | } 96 | 97 | print = pythonPrint(self, print) 98 | if print == nil { 99 | return nil 100 | } 101 | 102 | goDefs := C.GoString(defs) 103 | 104 | if filename != nil { 105 | goFilename = C.GoString(filename) 106 | } 107 | 108 | state := lockSelf(self) 109 | if state == nil { 110 | return nil 111 | } 112 | defer state.Mutex.Unlock() 113 | 114 | state.DetachGIL() 115 | starlarkPrint := func(_ *starlark.Thread, msg string) { 116 | state.ReattachGIL() 117 | defer state.DetachGIL() 118 | 119 | callPythonPrint(print, msg) 120 | } 121 | 122 | _, program, err := starlark.SourceProgram(goFilename, goDefs, state.Globals.Has) 123 | if err != nil { 124 | state.ReattachGIL() 125 | raisePythonException(err) 126 | return nil 127 | } 128 | 129 | thread := &starlark.Thread{Print: starlarkPrint} 130 | newGlobals, err := program.Init(thread, state.Globals) 131 | state.ReattachGIL() 132 | 133 | if err != nil { 134 | raisePythonException(err) 135 | return nil 136 | } 137 | 138 | for k, v := range newGlobals { 139 | v.Freeze() 140 | state.Globals[k] = v 141 | } 142 | 143 | return C.cgoPy_NewRef(C.Py_None) 144 | } 145 | -------------------------------------------------------------------------------- /python_exceptions.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | /* 4 | #include "starlark.h" 5 | 6 | extern PyObject *StarlarkError; 7 | extern PyObject *SyntaxError; 8 | extern PyObject *EvalError; 9 | extern PyObject *ResolveError; 10 | */ 11 | import "C" 12 | 13 | import ( 14 | "errors" 15 | "reflect" 16 | "unsafe" 17 | 18 | "go.starlark.net/resolve" 19 | "go.starlark.net/starlark" 20 | "go.starlark.net/syntax" 21 | ) 22 | 23 | func getCurrentPythonException() (*C.PyObject, *C.PyObject, *C.PyObject) { 24 | var ptype *C.PyObject = nil 25 | var pvalue *C.PyObject = nil 26 | var ptraceback *C.PyObject = nil 27 | 28 | C.PyErr_Fetch(&ptype, &pvalue, &ptraceback) 29 | C.PyErr_NormalizeException(&ptype, &pvalue, &ptraceback) 30 | if ptraceback != nil { 31 | C.PyException_SetTraceback(pvalue, ptraceback) 32 | C.Py_DecRef(ptraceback) 33 | } 34 | 35 | return ptype, pvalue, ptraceback 36 | } 37 | 38 | func setPythonExceptionCause(cause *C.PyObject) { 39 | ptype, pvalue, ptraceback := getCurrentPythonException() 40 | C.PyException_SetCause(pvalue, cause) 41 | C.PyErr_Restore(ptype, pvalue, ptraceback) 42 | } 43 | 44 | func handleConversionError(err error, pytype *C.PyObject) { 45 | _, pvalue, _ := getCurrentPythonException() 46 | 47 | if pvalue != nil { 48 | pvalue = C.cgoPy_NewRef(pvalue) 49 | defer setPythonExceptionCause(pvalue) 50 | } 51 | errmsg := C.CString(err.Error()) 52 | defer C.free(unsafe.Pointer(errmsg)) 53 | C.PyErr_SetString(pytype, errmsg) 54 | } 55 | 56 | func raisePythonException(err error) { 57 | var ( 58 | exc_args *C.PyObject 59 | exc_type *C.PyObject 60 | syntaxErr syntax.Error 61 | evalErr *starlark.EvalError 62 | resolveErr resolve.ErrorList 63 | ) 64 | 65 | error_msg := C.CString(err.Error()) 66 | defer C.free(unsafe.Pointer(error_msg)) 67 | 68 | error_type := C.CString(reflect.TypeOf(err).String()) 69 | defer C.free(unsafe.Pointer(error_type)) 70 | 71 | switch { 72 | case errors.As(err, &syntaxErr): 73 | msg := C.CString(syntaxErr.Msg) 74 | defer C.free(unsafe.Pointer(msg)) 75 | 76 | filename := C.CString(syntaxErr.Pos.Filename()) 77 | defer C.free(unsafe.Pointer(filename)) 78 | 79 | line := C.uint(syntaxErr.Pos.Line) 80 | column := C.uint(syntaxErr.Pos.Col) 81 | 82 | exc_args = C.makeSyntaxErrorArgs(error_msg, error_type, msg, filename, line, column) 83 | exc_type = C.SyntaxError 84 | case errors.As(err, &evalErr): 85 | backtrace := C.CString(evalErr.Backtrace()) 86 | defer C.free(unsafe.Pointer(backtrace)) 87 | 88 | var ( 89 | function_name *C.char 90 | filename *C.char 91 | line C.uint 92 | column C.uint 93 | ) 94 | 95 | if len(evalErr.CallStack) > 0 { 96 | frame := evalErr.CallStack[len(evalErr.CallStack)-1] 97 | 98 | filename = C.CString(frame.Pos.Filename()) 99 | defer C.free(unsafe.Pointer((filename))) 100 | 101 | line = C.uint(frame.Pos.Line) 102 | column = C.uint(frame.Pos.Col) 103 | 104 | function_name = C.CString(frame.Name) 105 | defer C.free(unsafe.Pointer(function_name)) 106 | } else { 107 | filename = C.CString("unknown") 108 | defer C.free(unsafe.Pointer(filename)) 109 | 110 | line = 0 111 | column = 0 112 | function_name = filename 113 | } 114 | 115 | exc_args = C.makeEvalErrorArgs(error_msg, error_type, filename, line, column, function_name, backtrace) 116 | exc_type = C.EvalError 117 | case errors.As(err, &resolveErr): 118 | items := C.PyTuple_New(C.Py_ssize_t(len(resolveErr))) 119 | defer C.Py_DecRef(items) 120 | 121 | for i, err := range resolveErr { 122 | msg := C.CString(err.Msg) 123 | defer C.free(unsafe.Pointer(msg)) 124 | 125 | C.PyTuple_SetItem(items, C.Py_ssize_t(i), C.makeResolveErrorItem(msg, C.uint(err.Pos.Line), C.uint(err.Pos.Col))) 126 | } 127 | 128 | exc_args = C.makeResolveErrorArgs(error_msg, error_type, items) 129 | exc_type = C.ResolveError 130 | default: 131 | exc_args = C.makeStarlarkErrorArgs(error_msg, error_type) 132 | exc_type = C.StarlarkError 133 | } 134 | 135 | C.PyErr_SetObject(exc_type, exc_args) 136 | C.Py_DecRef(exc_args) 137 | } 138 | 139 | func raiseRuntimeError(msg string) { 140 | cmsg := C.CString(msg) 141 | defer C.free(unsafe.Pointer(cmsg)) 142 | C.PyErr_SetString(C.PyExc_RuntimeError, cmsg) 143 | } 144 | -------------------------------------------------------------------------------- /python_globals.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | /* 4 | #include "starlark.h" 5 | */ 6 | import "C" 7 | 8 | import ( 9 | "unsafe" 10 | ) 11 | 12 | //export Starlark_global_names 13 | func Starlark_global_names(self *C.Starlark, _ *C.PyObject) *C.PyObject { 14 | state := rlockSelf(self) 15 | if state == nil { 16 | return nil 17 | } 18 | defer state.Mutex.RUnlock() 19 | 20 | list := C.PyList_New(0) 21 | for _, key := range state.Globals.Keys() { 22 | ckey := C.CString(key) 23 | defer C.free(unsafe.Pointer(ckey)) 24 | 25 | pykey := C.cgoPy_BuildString(ckey) 26 | if pykey == nil { 27 | C.Py_DecRef(list) 28 | return nil 29 | } 30 | 31 | if C.PyList_Append(list, pykey) != 0 { 32 | C.Py_DecRef(pykey) 33 | C.Py_DecRef(list) 34 | return nil 35 | } 36 | } 37 | 38 | return list 39 | } 40 | 41 | //export Starlark_get_global 42 | func Starlark_get_global(self *C.Starlark, args *C.PyObject, kwargs *C.PyObject) *C.PyObject { 43 | var name *C.char = nil 44 | var default_value *C.PyObject = nil 45 | 46 | if C.parseGetGlobalArgs(args, kwargs, &name, &default_value) == 0 { 47 | return nil 48 | } 49 | 50 | goName := C.GoString(name) 51 | state := rlockSelf(self) 52 | if state == nil { 53 | return nil 54 | } 55 | defer state.Mutex.RUnlock() 56 | 57 | value, ok := state.Globals[goName] 58 | if !ok { 59 | if default_value != nil { 60 | return default_value 61 | } 62 | 63 | C.PyErr_SetString(C.PyExc_KeyError, name) 64 | return nil 65 | } 66 | 67 | retval, err := starlarkValueToPython(value) 68 | if err != nil { 69 | return nil 70 | } 71 | 72 | return retval 73 | } 74 | 75 | //export Starlark_set_globals 76 | func Starlark_set_globals(self *C.Starlark, args *C.PyObject, kwargs *C.PyObject) *C.PyObject { 77 | posargs := C.PyObject_Length(args) 78 | 79 | if posargs > 0 { 80 | errmsg := C.CString("set_globals takes no positional arguments") 81 | defer C.free(unsafe.Pointer(errmsg)) 82 | C.PyErr_SetString(C.PyExc_TypeError, errmsg) 83 | return nil 84 | } 85 | 86 | if kwargs == nil { 87 | return C.cgoPy_NewRef(C.Py_None) 88 | } 89 | 90 | pyiter := C.PyObject_GetIter(kwargs) 91 | if pyiter == nil { 92 | return nil 93 | } 94 | defer C.Py_DecRef(pyiter) 95 | 96 | state := rlockSelf(self) 97 | if state == nil { 98 | return nil 99 | } 100 | defer state.Mutex.RUnlock() 101 | 102 | for pykey := C.PyIter_Next(pyiter); pykey != nil; pykey = C.PyIter_Next(pyiter) { 103 | defer C.Py_DecRef(pykey) 104 | 105 | var size C.Py_ssize_t 106 | ckey := C.PyUnicode_AsUTF8AndSize(pykey, &size) 107 | if ckey == nil { 108 | return nil 109 | } 110 | key := C.GoString(ckey) 111 | 112 | pyvalue := C.PyObject_GetItem(kwargs, pykey) 113 | if pyvalue == nil { 114 | return nil 115 | } 116 | defer C.Py_DecRef(pyvalue) 117 | 118 | value, err := pythonToStarlarkValue(pyvalue, state) 119 | if err != nil { 120 | return nil 121 | } 122 | 123 | value.Freeze() 124 | state.Globals[key] = value 125 | } 126 | 127 | return C.cgoPy_NewRef(C.Py_None) 128 | } 129 | 130 | //export Starlark_pop_global 131 | func Starlark_pop_global(self *C.Starlark, args *C.PyObject, kwargs *C.PyObject) *C.PyObject { 132 | var name *C.char = nil 133 | var default_value *C.PyObject = nil 134 | 135 | if C.parsePopGlobalArgs(args, kwargs, &name, &default_value) == 0 { 136 | return nil 137 | } 138 | 139 | goName := C.GoString(name) 140 | state := rlockSelf(self) 141 | if state == nil { 142 | return nil 143 | } 144 | defer state.Mutex.RUnlock() 145 | 146 | value, ok := state.Globals[goName] 147 | if !ok { 148 | if default_value != nil { 149 | return default_value 150 | } 151 | 152 | C.PyErr_SetString(C.PyExc_KeyError, name) 153 | return nil 154 | } 155 | 156 | delete(state.Globals, goName) 157 | retval, err := starlarkValueToPython(value) 158 | if err != nil { 159 | return nil 160 | } 161 | 162 | return retval 163 | } 164 | 165 | //export Starlark_tp_iter 166 | func Starlark_tp_iter(self *C.Starlark) *C.PyObject { 167 | keys := Starlark_global_names(self, nil) 168 | if keys == nil { 169 | return nil 170 | } 171 | return C.PyObject_GetIter(keys) 172 | } 173 | -------------------------------------------------------------------------------- /python_object.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | /* 4 | #include "starlark.h" 5 | 6 | extern PyObject *StarlarkError; 7 | extern PyObject *SyntaxError; 8 | extern PyObject *EvalError; 9 | extern PyObject *ResolveError; 10 | */ 11 | import "C" 12 | 13 | import ( 14 | "fmt" 15 | "runtime/cgo" 16 | "sync" 17 | "unsafe" 18 | 19 | "go.starlark.net/resolve" 20 | "go.starlark.net/starlark" 21 | ) 22 | 23 | type StarlarkState struct { 24 | Globals starlark.StringDict 25 | Mutex sync.RWMutex 26 | Print *C.PyObject 27 | threadState *C.PyThreadState 28 | } 29 | 30 | //export ConfigureStarlark 31 | func ConfigureStarlark(allowSet C.int, allowGlobalReassign C.int, allowRecursion C.int) { 32 | // Ignore input values other than 0 or 1 and leave current value in place 33 | switch allowSet { 34 | case 0: 35 | resolve.AllowSet = false 36 | case 1: 37 | resolve.AllowSet = true 38 | } 39 | 40 | switch allowGlobalReassign { 41 | case 0: 42 | resolve.AllowGlobalReassign = false 43 | case 1: 44 | resolve.AllowGlobalReassign = true 45 | } 46 | 47 | switch allowRecursion { 48 | case 0: 49 | resolve.AllowRecursion = false 50 | case 1: 51 | resolve.AllowRecursion = true 52 | } 53 | } 54 | 55 | func rlockSelf(self *C.Starlark) *StarlarkState { 56 | state := cgo.Handle(self.handle).Value().(*StarlarkState) 57 | state.Mutex.RLock() 58 | return state 59 | } 60 | 61 | func lockSelf(self *C.Starlark) *StarlarkState { 62 | state := cgo.Handle(self.handle).Value().(*StarlarkState) 63 | state.Mutex.Lock() 64 | return state 65 | } 66 | 67 | func (state *StarlarkState) DetachGIL() { 68 | state.threadState = C.PyEval_SaveThread() 69 | } 70 | 71 | func (state *StarlarkState) ReattachGIL() { 72 | if state.threadState == nil { 73 | return 74 | } 75 | 76 | C.PyEval_RestoreThread(state.threadState) 77 | state.threadState = nil 78 | } 79 | 80 | //export Starlark_new 81 | func Starlark_new(pytype *C.PyTypeObject, args *C.PyObject, kwargs *C.PyObject) *C.Starlark { 82 | self := C.starlarkAlloc(pytype) 83 | if self == nil { 84 | return nil 85 | } 86 | 87 | state := &StarlarkState{ 88 | Globals: starlark.StringDict{}, 89 | Mutex: sync.RWMutex{}, 90 | Print: nil, 91 | threadState: nil, 92 | } 93 | self.handle = C.uintptr_t(cgo.NewHandle(state)) 94 | 95 | return self 96 | } 97 | 98 | //export Starlark_init 99 | func Starlark_init(self *C.Starlark, args *C.PyObject, kwargs *C.PyObject) C.int { 100 | var globals *C.PyObject = nil 101 | var print *C.PyObject = nil 102 | 103 | if C.parseInitArgs(args, kwargs, &globals, &print) == 0 { 104 | return -1 105 | } 106 | 107 | if print != nil { 108 | if Starlark_set_print(self, print, nil) != 0 { 109 | return -1 110 | } 111 | } 112 | 113 | if globals != nil { 114 | if C.PyMapping_Check(globals) != 1 { 115 | errmsg := C.CString(fmt.Sprintf("Can't initialize globals from %s", C.GoString(globals.ob_type.tp_name))) 116 | defer C.free(unsafe.Pointer(errmsg)) 117 | C.PyErr_SetString(C.PyExc_TypeError, errmsg) 118 | return -1 119 | } 120 | 121 | retval := Starlark_set_globals(self, args, globals) 122 | if retval == nil { 123 | return -1 124 | } 125 | } 126 | 127 | return 0 128 | } 129 | 130 | //export Starlark_dealloc 131 | func Starlark_dealloc(self *C.Starlark) { 132 | handle := cgo.Handle(self.handle) 133 | state := handle.Value().(*StarlarkState) 134 | 135 | handle.Delete() 136 | 137 | state.Mutex.Lock() 138 | defer state.Mutex.Unlock() 139 | 140 | if state.Print != nil { 141 | C.Py_DecRef(state.Print) 142 | } 143 | 144 | C.starlarkFree(self) 145 | } 146 | 147 | func main() {} 148 | -------------------------------------------------------------------------------- /python_print.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | /* 4 | #include "starlark.h" 5 | */ 6 | import "C" 7 | 8 | import ( 9 | "fmt" 10 | "unsafe" 11 | ) 12 | 13 | //export Starlark_get_print 14 | func Starlark_get_print(self *C.Starlark, closure unsafe.Pointer) *C.PyObject { 15 | state := rlockSelf(self) 16 | if state == nil { 17 | return nil 18 | } 19 | defer state.Mutex.RUnlock() 20 | 21 | if state.Print == nil { 22 | return C.cgoPy_NewRef(C.Py_None) 23 | } 24 | 25 | return C.cgoPy_NewRef(state.Print) 26 | } 27 | 28 | //export Starlark_set_print 29 | func Starlark_set_print(self *C.Starlark, value *C.PyObject, closure unsafe.Pointer) C.int { 30 | if value == C.Py_None { 31 | value = nil 32 | } 33 | 34 | if value != nil { 35 | if C.PyCallable_Check(value) != 1 { 36 | errmsg := C.CString(fmt.Sprintf("%s is not callable", C.GoString(value.ob_type.tp_name))) 37 | defer C.free(unsafe.Pointer(errmsg)) 38 | C.PyErr_SetString(C.PyExc_TypeError, errmsg) 39 | return -1 40 | } 41 | } 42 | 43 | state := lockSelf(self) 44 | if state == nil { 45 | return -1 46 | } 47 | defer state.Mutex.Unlock() 48 | 49 | state.Print = C.cgoPy_NewRef(value) 50 | return 0 51 | } 52 | 53 | func pythonPrint(self *C.Starlark, print *C.PyObject) *C.PyObject { 54 | if print == nil { 55 | state := rlockSelf(self) 56 | if state == nil { 57 | return nil 58 | } 59 | defer state.Mutex.RUnlock() 60 | print = state.Print 61 | } 62 | 63 | if print == nil { 64 | print = pythonBuiltinPrint() 65 | } 66 | 67 | if print == nil { 68 | errmsg := C.CString("Couldn't find print()?") 69 | defer C.free(unsafe.Pointer(errmsg)) 70 | C.PyErr_SetString(C.PyExc_TypeError, errmsg) 71 | return nil 72 | } 73 | 74 | if C.PyCallable_Check(print) != 1 { 75 | errmsg := C.CString(fmt.Sprintf("%s is not callable", C.GoString(print.ob_type.tp_name))) 76 | defer C.free(unsafe.Pointer(errmsg)) 77 | C.PyErr_SetString(C.PyExc_TypeError, errmsg) 78 | return nil 79 | } 80 | 81 | return print 82 | } 83 | 84 | func pythonBuiltinPrint() *C.PyObject { 85 | builtins := C.PyEval_GetBuiltins() 86 | if builtins == nil { 87 | return nil 88 | } 89 | 90 | cstr := C.CString("print") 91 | defer C.free(unsafe.Pointer(cstr)) 92 | return C.PyDict_GetItemString(builtins, cstr) 93 | } 94 | 95 | func callPythonPrint(print *C.PyObject, msg string) { 96 | cmsg := C.CString(msg) 97 | defer C.free(unsafe.Pointer(cmsg)) 98 | 99 | pymsg := C.cgoPy_BuildString(cmsg) 100 | args := C.PyTuple_New(1) 101 | defer C.Py_DecRef(args) 102 | 103 | if C.PyTuple_SetItem(args, 0, pymsg) == 0 { 104 | C.PyObject_CallObject(print, args) 105 | } else { 106 | C.Py_DecRef(pymsg) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /python_to_starlark.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | /* 4 | #include "starlark.h" 5 | 6 | extern PyObject *ConversionToStarlarkFailed; 7 | */ 8 | import "C" 9 | 10 | import ( 11 | "fmt" 12 | "math/big" 13 | "unsafe" 14 | 15 | "go.starlark.net/starlark" 16 | ) 17 | 18 | func pythonToStarlarkTuple(obj *C.PyObject, env PythonEnv) (starlark.Tuple, error) { 19 | var elems []starlark.Value 20 | pyiter := C.PyObject_GetIter(obj) 21 | if pyiter == nil { 22 | return starlark.Tuple{}, fmt.Errorf("Couldn't get iterator for Python tuple") 23 | } 24 | defer C.Py_DecRef(pyiter) 25 | 26 | index := 0 27 | for pyvalue := C.PyIter_Next(pyiter); pyvalue != nil; pyvalue = C.PyIter_Next(pyiter) { 28 | defer C.Py_DecRef(pyvalue) 29 | 30 | value, err := innerPythonToStarlarkValue(pyvalue, env) 31 | if err != nil { 32 | return starlark.Tuple{}, fmt.Errorf("While converting value at index %v in Python tuple: %v", index, err) 33 | } 34 | 35 | elems = append(elems, value) 36 | index += 1 37 | } 38 | 39 | if C.PyErr_Occurred() != nil { 40 | return starlark.Tuple{}, fmt.Errorf("Python exception while converting value at index %v in Python tuple", index) 41 | } 42 | 43 | return starlark.Tuple(elems), nil 44 | } 45 | 46 | func pythonToStarlarkBytes(obj *C.PyObject) (starlark.Bytes, error) { 47 | cbytes := C.PyBytes_AsString(obj) 48 | if cbytes == nil { 49 | return starlark.Bytes(""), fmt.Errorf("Couldn't get pointer to Python bytes") 50 | } 51 | 52 | return starlark.Bytes(C.GoString(cbytes)), nil 53 | } 54 | 55 | func pythonToStarlarkList(obj *C.PyObject, env PythonEnv) (*starlark.List, error) { 56 | len := C.PyObject_Length(obj) 57 | if len < 0 { 58 | return &starlark.List{}, fmt.Errorf("Couldn't get size of Python list") 59 | } 60 | 61 | var elems []starlark.Value 62 | pyiter := C.PyObject_GetIter(obj) 63 | if pyiter == nil { 64 | return &starlark.List{}, fmt.Errorf("Couldn't get iterator for Python list") 65 | } 66 | defer C.Py_DecRef(pyiter) 67 | 68 | index := 0 69 | for pyvalue := C.PyIter_Next(pyiter); pyvalue != nil; pyvalue = C.PyIter_Next(pyiter) { 70 | defer C.Py_DecRef(pyvalue) 71 | value, err := innerPythonToStarlarkValue(pyvalue, env) 72 | if err != nil { 73 | return &starlark.List{}, fmt.Errorf("While converting value at index %v in Python list: %v", index, err) 74 | } 75 | 76 | elems = append(elems, value) 77 | index += 1 78 | } 79 | 80 | if C.PyErr_Occurred() != nil { 81 | return &starlark.List{}, fmt.Errorf("Python exception while converting value at index %v in Python list", index) 82 | } 83 | 84 | return starlark.NewList(elems), nil 85 | } 86 | 87 | func pythonToStarlarkDict(obj *C.PyObject, env PythonEnv) (*starlark.Dict, error) { 88 | size := C.PyObject_Length(obj) 89 | if size < 0 { 90 | return &starlark.Dict{}, fmt.Errorf("Couldn't get size of Python dict") 91 | } 92 | 93 | dict := starlark.NewDict(int(size)) 94 | pyiter := C.PyObject_GetIter(obj) 95 | if pyiter == nil { 96 | return &starlark.Dict{}, fmt.Errorf("Couldn't get iterator for Python dict") 97 | } 98 | defer C.Py_DecRef(pyiter) 99 | 100 | for pykey := C.PyIter_Next(pyiter); pykey != nil; pykey = C.PyIter_Next(pyiter) { 101 | defer C.Py_DecRef(pykey) 102 | 103 | key, err := innerPythonToStarlarkValue(pykey, env) 104 | if err != nil { 105 | return &starlark.Dict{}, fmt.Errorf("While converting key in Python dict: %v", err) 106 | } 107 | 108 | pyvalue := C.PyObject_GetItem(obj, pykey) 109 | if pyvalue == nil { 110 | return &starlark.Dict{}, fmt.Errorf("Couldn't get value of key %v in Python dict", key) 111 | } 112 | defer C.Py_DecRef(pyvalue) 113 | 114 | value, err := innerPythonToStarlarkValue(pyvalue, env) 115 | if err != nil { 116 | return &starlark.Dict{}, fmt.Errorf("While converting value of key %v in Python dict: %v", key, err) 117 | } 118 | 119 | err = dict.SetKey(key, value) 120 | if err != nil { 121 | return &starlark.Dict{}, fmt.Errorf("While setting %v to %v in Starlark dict: %v", key, value, err) 122 | } 123 | } 124 | 125 | if C.PyErr_Occurred() != nil { 126 | return &starlark.Dict{}, fmt.Errorf("Python exception while iterating through Python dict") 127 | } 128 | 129 | return dict, nil 130 | } 131 | 132 | func pythonToStarlarkSet(obj *C.PyObject, env PythonEnv) (*starlark.Set, error) { 133 | size := C.PyObject_Length(obj) 134 | if size < 0 { 135 | return &starlark.Set{}, fmt.Errorf("Couldn't get size of Python set") 136 | } 137 | 138 | set := starlark.NewSet(int(size)) 139 | pyiter := C.PyObject_GetIter(obj) 140 | if pyiter == nil { 141 | return &starlark.Set{}, fmt.Errorf("Couldn't get iterator for Python set") 142 | } 143 | defer C.Py_DecRef(pyiter) 144 | 145 | for pyvalue := C.PyIter_Next(pyiter); pyvalue != nil; pyvalue = C.PyIter_Next(pyiter) { 146 | defer C.Py_DecRef(pyvalue) 147 | 148 | value, err := innerPythonToStarlarkValue(pyvalue, env) 149 | if err != nil { 150 | return &starlark.Set{}, fmt.Errorf("While converting value in Python set: %v", err) 151 | } 152 | 153 | err = set.Insert(value) 154 | if err != nil { 155 | raisePythonException(err) 156 | return &starlark.Set{}, fmt.Errorf("While inserting %v into Starlark set: %v", value, err) 157 | } 158 | } 159 | 160 | if C.PyErr_Occurred() != nil { 161 | return &starlark.Set{}, fmt.Errorf("Python exception while converting value in Python set to Starlark") 162 | } 163 | 164 | return set, nil 165 | } 166 | 167 | func pythonToStarlarkString(obj *C.PyObject) (starlark.String, error) { 168 | var size C.Py_ssize_t 169 | cstr := C.PyUnicode_AsUTF8AndSize(obj, &size) 170 | if cstr == nil { 171 | return starlark.String(""), fmt.Errorf("Couldn't convert Python string to C string") 172 | } 173 | 174 | return starlark.String(C.GoString(cstr)), nil 175 | } 176 | 177 | func pythonToStarlarkInt(obj *C.PyObject) (starlark.Int, error) { 178 | overflow := C.int(0) 179 | longlong := int64(C.PyLong_AsLongLongAndOverflow(obj, &overflow)) // https://youtu.be/6-1Ue0FFrHY 180 | if C.PyErr_Occurred() != nil { 181 | return starlark.Int{}, fmt.Errorf("Couldn't convert Python int to long") 182 | } 183 | 184 | if overflow == 0 { 185 | return starlark.MakeInt64(longlong), nil 186 | } 187 | 188 | pystr := C.PyObject_Str(obj) 189 | if pystr == nil { 190 | return starlark.Int{}, fmt.Errorf("Couldn't convert Python int to string") 191 | } 192 | defer C.Py_DecRef(pystr) 193 | 194 | var size C.Py_ssize_t 195 | cstr := C.PyUnicode_AsUTF8AndSize(pystr, &size) 196 | if cstr == nil { 197 | return starlark.Int{}, fmt.Errorf("Couldn't convert Python int to C string") 198 | } 199 | 200 | i := new(big.Int) 201 | i.SetString(C.GoString(cstr), 10) 202 | return starlark.MakeBigInt(i), nil 203 | } 204 | 205 | func pythonToStarlarkFloat(obj *C.PyObject) (starlark.Float, error) { 206 | cvalue := C.PyFloat_AsDouble(obj) 207 | if C.PyErr_Occurred() != nil { 208 | return starlark.Float(0), fmt.Errorf("Couldn't convert Python float to double") 209 | } 210 | 211 | return starlark.Float(cvalue), nil 212 | } 213 | 214 | func getFuncName(obj *C.PyObject) (string, error) { 215 | nameAttr := C.CString("__name__") 216 | defer C.free(unsafe.Pointer(nameAttr)) 217 | funcName, err := pythonToStarlarkString(C.PyObject_GetAttrString(obj, nameAttr)) 218 | if err != nil { 219 | return "", err 220 | } 221 | return funcName.GoString(), nil 222 | } 223 | 224 | func getPyError() error { 225 | // TODO: replace with PyErr_GetRaisedException when requiring Python >= 3.12 226 | var ( 227 | errType *C.PyObject 228 | errValue *C.PyObject 229 | errTraceback *C.PyObject 230 | ) 231 | C.PyErr_Fetch(&errType, &errValue, &errTraceback) 232 | defer C.Py_DecRef(errType) 233 | defer C.Py_DecRef(errValue) 234 | defer C.Py_DecRef(errTraceback) 235 | 236 | errStr, err := pythonToStarlarkString(C.PyObject_Str(errValue)) 237 | if err != nil { 238 | return err 239 | } 240 | 241 | return fmt.Errorf(errStr.GoString()) 242 | } 243 | 244 | func pythonToStarlarkFunc(obj *C.PyObject, env PythonEnv) (starlark.Value, error) { 245 | funcName, err := getFuncName(obj) 246 | if err != nil { 247 | return starlark.None, err 248 | } 249 | 250 | return starlark.NewBuiltin(funcName, func( 251 | _ *starlark.Thread, 252 | _ *starlark.Builtin, 253 | args starlark.Tuple, 254 | kwargs []starlark.Tuple, 255 | ) (starlark.Value, error) { 256 | env.ReattachGIL() 257 | defer env.DetachGIL() 258 | 259 | cargs, err := starlarkTupleToPython(args) 260 | if err != nil { 261 | return starlark.None, err 262 | } 263 | defer C.Py_DecRef(cargs) 264 | 265 | ckwargs, err := starlarkDictItemsToPython(kwargs) 266 | if err != nil { 267 | return starlark.None, err 268 | } 269 | defer C.Py_DecRef(ckwargs) 270 | 271 | res := C.PyObject_Call(obj, cargs, ckwargs) 272 | if C.PyErr_Occurred() != nil { 273 | return starlark.None, getPyError() 274 | } 275 | 276 | defer C.Py_DecRef(res) 277 | return innerPythonToStarlarkValue(res, env) 278 | }), nil 279 | } 280 | 281 | func pythonToStarlarkMethod(obj *C.PyObject, env PythonEnv) (starlark.Value, error) { 282 | self := C.PyMethod_Self(obj) 283 | f := C.PyMethod_Function(obj) 284 | 285 | funcName, err := getFuncName(f) 286 | if err != nil { 287 | return starlark.None, err 288 | } 289 | 290 | return starlark.NewBuiltin(funcName, func( 291 | _ *starlark.Thread, 292 | _ *starlark.Builtin, 293 | args starlark.Tuple, 294 | kwargs []starlark.Tuple, 295 | ) (starlark.Value, error) { 296 | env.ReattachGIL() 297 | defer env.DetachGIL() 298 | 299 | // create args list with self at the front 300 | cargsList, err := starlarkTupleToPythonList(args) 301 | if err != nil { 302 | return starlark.None, err 303 | } 304 | cargs := C.PyTuple_New(C.Py_ssize_t(len(cargsList) + 1)) 305 | if cargs == nil { 306 | return starlark.None, fmt.Errorf("Could not initialize argument list") 307 | } 308 | C.Py_IncRef(self) 309 | C.PyTuple_SetItem(cargs, 0, self) 310 | for i, arg := range cargsList { 311 | C.PyTuple_SetItem(cargs, C.Py_ssize_t(i + 1), arg) 312 | } 313 | defer C.Py_DecRef(cargs) 314 | 315 | ckwargs, err := starlarkDictItemsToPython(kwargs) 316 | if err != nil { 317 | return starlark.None, err 318 | } 319 | defer C.Py_DecRef(ckwargs) 320 | 321 | res := C.PyObject_Call(f, cargs, ckwargs) 322 | if C.PyErr_Occurred() != nil { 323 | return starlark.None, getPyError() 324 | } 325 | 326 | defer C.Py_DecRef(res) 327 | return innerPythonToStarlarkValue(res, env) 328 | }), nil 329 | } 330 | 331 | func innerPythonToStarlarkValue(obj *C.PyObject, env PythonEnv) (starlark.Value, error) { 332 | var value starlark.Value = nil 333 | var err error = nil 334 | 335 | switch { 336 | case obj == C.Py_None: 337 | value = starlark.None 338 | case obj == C.Py_True: 339 | value = starlark.True 340 | case obj == C.Py_False: 341 | value = starlark.False 342 | case C.cgoPyFloat_Check(obj) == 1: 343 | value, err = pythonToStarlarkFloat(obj) 344 | case C.cgoPyLong_Check(obj) == 1: 345 | value, err = pythonToStarlarkInt(obj) 346 | case C.cgoPyUnicode_Check(obj) == 1: 347 | value, err = pythonToStarlarkString(obj) 348 | case C.cgoPyBytes_Check(obj) == 1: 349 | value, err = pythonToStarlarkBytes(obj) 350 | case C.cgoPySet_Check(obj) == 1: 351 | value, err = pythonToStarlarkSet(obj, env) 352 | case C.cgoPyDict_Check(obj) == 1: 353 | value, err = pythonToStarlarkDict(obj, env) 354 | case C.cgoPyList_Check(obj) == 1: 355 | value, err = pythonToStarlarkList(obj, env) 356 | case C.cgoPyTuple_Check(obj) == 1: 357 | value, err = pythonToStarlarkTuple(obj, env) 358 | case C.PySequence_Check(obj) == 1: 359 | value, err = pythonToStarlarkList(obj, env) 360 | case C.PyMapping_Check(obj) == 1: 361 | value, err = pythonToStarlarkDict(obj, env) 362 | case C.cgoPyFunc_Check(obj) == 1: 363 | value, err = pythonToStarlarkFunc(obj, env) 364 | case C.cgoPyMethod_Check(obj) == 1: 365 | value, err = pythonToStarlarkMethod(obj, env) 366 | default: 367 | err = fmt.Errorf("Don't know how to convert Python %s to Starlark", C.GoString(obj.ob_type.tp_name)) 368 | } 369 | 370 | if err == nil { 371 | if C.PyErr_Occurred() != nil { 372 | err = fmt.Errorf("Python exception while converting to Starlark") 373 | } 374 | } 375 | 376 | return value, err 377 | } 378 | 379 | func pythonToStarlarkValue(obj *C.PyObject, env PythonEnv) (starlark.Value, error) { 380 | value, err := innerPythonToStarlarkValue(obj, env) 381 | if err != nil { 382 | handleConversionError(err, C.ConversionToStarlarkFailed) 383 | return starlark.None, err 384 | } 385 | 386 | return value, nil 387 | } 388 | -------------------------------------------------------------------------------- /scripts/install-go.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -ex 4 | 5 | GO_VERSION=1.22.5 6 | 7 | if [ -e /etc/alpine-release ] && [ -z "$BASH_VERSION" ]; then 8 | apk add bash curl git go 9 | exit 0 10 | fi 11 | 12 | if [ -z "$BASH_VERSION" ]; then 13 | exec bash "$0" 14 | fi 15 | 16 | set -eou pipefail 17 | 18 | if [ -e /etc/debian_version ]; then 19 | apt-get update 20 | DEBIAN_FRONTEND=noninteractive apt-get install -y curl git 21 | fi 22 | 23 | install_go() { 24 | git clone https://github.com/asdf-vm/asdf.git ~/.asdf --branch v0.14.0 25 | 26 | # shellcheck disable=SC1090 27 | . ~/.asdf/asdf.sh 28 | 29 | asdf plugin add golang 30 | asdf install golang "$GO_VERSION" 31 | 32 | ln -s ~/.asdf/installs/golang/${GO_VERSION}/go/bin/go /usr/local/bin/go 33 | ln -s ~/.asdf/installs/golang/${GO_VERSION}/go/bin/gofmt /usr/local/bin/gofmt 34 | } 35 | 36 | (install_go) 37 | 38 | go version 39 | 40 | env | sort 41 | -------------------------------------------------------------------------------- /scripts/pytest-valgrind.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | VALGRIND_ROOT=/tmp/valgrind-python 6 | VALGRIND_PYTHON="$VALGRIND_ROOT/bin/python3" 7 | VALGRIND_LOG="$VALGRIND_ROOT/valgrind.log" 8 | 9 | set -x 10 | 11 | # Set up a venv 12 | if [ ! -d "$VALGRIND_ROOT" ]; then 13 | python3.10 -m venv "$VALGRIND_ROOT" 14 | "$VALGRIND_PYTHON" -m pip install pytest pytest-valgrind 15 | fi 16 | 17 | # Install the module 18 | "$VALGRIND_PYTHON" -m pip install . 19 | 20 | # Nasty hack - rebuild with all the debugging symbols 21 | MY_SO="$VALGRIND_ROOT/lib/python3.10/site-packages/starlark_go/starlark_go.cpython-310-x86_64-linux-gnu.so" 22 | rm "$MY_SO" 23 | env CGO_CFLAGS="-g -O0 $(python3-config --includes)" \ 24 | CGO_LDFLAGS=-Wl,--unresolved-symbols=ignore-all \ 25 | go build -buildmode=c-shared -o "$MY_SO" 26 | 27 | # Remove old log and then run valgrind 28 | rm -f "$VALGRIND_LOG" 29 | if ! valgrind --gen-suppressions=all --suppressions=scripts/pytest-valgrind.supp \ 30 | --show-leak-kinds=definite --errors-for-leak-kinds=definite --log-file="$VALGRIND_LOG" \ 31 | "$VALGRIND_PYTHON" -m pytest -vv --valgrind --valgrind-log="$VALGRIND_LOG"; then 32 | set +x 33 | echo 34 | echo "*** VALGRIND FAILED, FULL LOG FOLLOWS ***" 35 | echo 36 | cat "$VALGRIND_LOG" 37 | exit 1 38 | fi 39 | -------------------------------------------------------------------------------- /scripts/pytest-valgrind.supp: -------------------------------------------------------------------------------- 1 | { 2 | go-runtime-adjustframe 3 | Memcheck:Cond 4 | fun:runtime.adjustframe 5 | obj:* 6 | } 7 | 8 | { 9 | go-runtime-adjustpointer 10 | Memcheck:Cond 11 | fun:runtime.adjustpointer* 12 | obj:* 13 | } 14 | 15 | { 16 | go-runtime-adjustpointer-Addr8 17 | Memcheck:Addr8 18 | fun:runtime.adjustpointer* 19 | obj:* 20 | } 21 | 22 | { 23 | go-runtime-asyncPreempt-Addr8 24 | Memcheck:Addr8 25 | fun:runtime.asyncPreempt.abi0 26 | } 27 | 28 | { 29 | go-runtime-asyncPreempt-Addr16 30 | Memcheck:Addr16 31 | fun:runtime.asyncPreempt.abi0 32 | } 33 | 34 | { 35 | starlark-addr4 36 | Memcheck:Addr4 37 | fun:go.starlark.net/* 38 | } 39 | 40 | { 41 | starlark-addr8 42 | Memcheck:Addr8 43 | fun:go.starlark.net/* 44 | } 45 | 46 | { 47 | starlark-addr32 48 | Memcheck:Addr32 49 | fun:go.starlark.net/* 50 | } 51 | -------------------------------------------------------------------------------- /scripts/update-starlark.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | OLD_STARLARK_VERSION=$(grep 'require go.starlark.net' go.mod | cut -d' ' -f3) 6 | 7 | go get -u go.starlark.net 8 | 9 | NEW_STARLARK_VERSION=$(grep 'require go.starlark.net' go.mod | cut -d' ' -f3) 10 | 11 | if [ "$NEW_STARLARK_VERSION" = "$OLD_STARLARK_VERSION" ]; then 12 | echo "starlark-go is unchanged (still $NEW_STARLARK_VERSION)" 13 | exit 0 14 | fi 15 | 16 | perl -i -pe "s/\Q$OLD_STARLARK_VERSION\E/$NEW_STARLARK_VERSION/g" README.md 17 | 18 | if [ -n "${GITHUB_ENV:-}" ]; then 19 | echo "NEW_STARLARK_VERSION=$NEW_STARLARK_VERSION" >> "$GITHUB_ENV" 20 | fi 21 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = starlark-go 3 | description = Python bindings for the Go implementation of Starlark 4 | long_description = file:README.md 5 | long_description_content_type = text/markdown 6 | author = Jordan Webb 7 | author_email = jordan@caketop.app 8 | url = https://github.com/caketop/python-starlark-go 9 | keywords = starlark 10 | license = Apache License 2.0 11 | license_file = LICENSE 12 | project_urls = 13 | Documentation = https://python-starlark-go.readthedocs.io/en/latest/ 14 | Bug Tracker = https://github.com/caketop/python-starlark-go/issues 15 | Source Code = https://github.com/caketop/python-starlark-go 16 | classifiers = 17 | Development Status :: 4 - Beta 18 | Intended Audience :: Developers 19 | License :: OSI Approved :: Apache Software License 20 | Programming Language :: Python :: 3 :: Only 21 | Programming Language :: Python :: 3.8 22 | Programming Language :: Python :: 3.9 23 | Programming Language :: Python :: 3.10 24 | Programming Language :: Python :: 3.11 25 | Programming Language :: Python :: 3.12 26 | Programming Language :: Python :: 3.13 27 | 28 | [options] 29 | packages = find: 30 | package_dir = 31 | = src 32 | include_package_data = True 33 | python_requires = >= 3.8 34 | setup_requires = 35 | setuptools_scm[toml] >= 3.4 36 | setuptools-golang >= 2.7 37 | 38 | [options.packages.find] 39 | where=src 40 | 41 | [options.package_data] 42 | starlark_go = 43 | *.pyi 44 | py.typed 45 | 46 | [tox:tox] 47 | envlist = py38, py39, py310, py311, py312, py313 48 | 49 | [gh-actions] 50 | python = 51 | 3.8: py38 52 | 3.9: py39 53 | 3.10: py310 54 | 3.11: py311 55 | 3.12: py312 56 | 3.13: py313 57 | 58 | [testenv] 59 | deps = 60 | -r development.txt 61 | pytest-memray 62 | commands = pytest -v --memray {posargs} 63 | 64 | [testenv:py313] 65 | deps = -r development.txt 66 | commands = pytest -v {posargs} 67 | 68 | [flake8] 69 | max-line-length = 88 70 | extend-ignore = E203, W503 71 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import Extension, setup 2 | 3 | # I would only use setup.cfg but it can't compile extensions, so here we are. 4 | 5 | setup( 6 | build_golang={"root": "github.com/caketop/python-starlark-go"}, 7 | ext_modules=[Extension("starlark_go/starlark_go", ["python_object.go"])], 8 | ) 9 | -------------------------------------------------------------------------------- /src/starlark_go/__init__.py: -------------------------------------------------------------------------------- 1 | from starlark_go.errors import ( 2 | ConversionError, 3 | ConversionToPythonFailed, 4 | ConversionToStarlarkFailed, 5 | EvalError, 6 | ResolveError, 7 | ResolveErrorItem, 8 | StarlarkError, 9 | SyntaxError, 10 | ) 11 | from starlark_go.starlark_go import ( # pyright: reportMissingModuleSource=false 12 | Starlark, 13 | configure_starlark, 14 | ) 15 | 16 | __all__ = [ 17 | "configure_starlark", 18 | "Starlark", 19 | "StarlarkError", 20 | "ConversionError", 21 | "ConversionToPythonFailed", 22 | "ConversionToStarlarkFailed", 23 | "EvalError", 24 | "ResolveError", 25 | "ResolveErrorItem", 26 | "SyntaxError", 27 | ] 28 | -------------------------------------------------------------------------------- /src/starlark_go/errors.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Optional, Tuple 2 | 3 | __all__ = ["StarlarkError", "SyntaxError", "EvalError"] 4 | 5 | 6 | class StarlarkError(Exception): 7 | """ 8 | Base class for Starlark errors. 9 | 10 | All Starlark-specific errors that are raised by :py:class:`starlark_go.Starlark` 11 | are derived from this class. 12 | """ 13 | 14 | def __init__(self, error: str, error_type: Optional[str] = None, *extra_args: Any): 15 | super().__init__(error, error_type, *extra_args) 16 | self.error = error 17 | """ 18 | A description of the error. 19 | 20 | :type: str 21 | """ 22 | self.error_type = self.__class__.__name__ if error_type is None else error_type 23 | """ 24 | The name of the Go type of the error. 25 | 26 | :type: typing.Optional[str] 27 | """ 28 | 29 | def __str__(self) -> str: 30 | return self.error 31 | 32 | 33 | class SyntaxError(StarlarkError): 34 | """ 35 | A Starlark syntax error. 36 | 37 | This exception is raised when syntatically invalid code is passed to 38 | :py:meth:`starlark_go.Starlark.eval` or :py:meth:`starlark_go.Starlark.exec`. 39 | """ 40 | 41 | def __init__( 42 | self, 43 | error: str, 44 | error_type: str, 45 | msg: str, 46 | filename: str, 47 | line: int, 48 | column: int, 49 | ): 50 | super().__init__(error, error_type, msg, filename, line, column) 51 | self.msg = msg 52 | """ 53 | A description of the syntax error 54 | 55 | :type: str 56 | """ 57 | self.filename = filename 58 | """ 59 | The name of the file that the error occurred in (taken from the ``filename`` 60 | parameter to :py:meth:`starlark_go.Starlark.eval` 61 | or :py:meth:`starlark_go.Starlark.exec`.) 62 | 63 | :type: str 64 | """ 65 | self.line = line 66 | """ 67 | The line number that the error occurred on (1-based) 68 | 69 | :type: int 70 | """ 71 | self.column = column 72 | """ 73 | The column that the error occurred on (1-based) 74 | 75 | :type: int 76 | """ 77 | 78 | 79 | class EvalError(StarlarkError): 80 | """ 81 | A Starlark evaluation error. 82 | 83 | This exception is raised when otherwise valid code attempts an illegal operation, 84 | such as adding a string to an integer. 85 | """ 86 | 87 | def __init__( 88 | self, 89 | error: str, 90 | error_type: str, 91 | filename: str, 92 | line: int, 93 | column: int, 94 | function_name: str, 95 | backtrace: str, 96 | ): 97 | super().__init__( 98 | error, error_type, filename, line, column, function_name, backtrace 99 | ) 100 | self.filename = filename 101 | """ 102 | The name of the file that the error occurred in. 103 | 104 | :type: str 105 | """ 106 | self.line = line 107 | """ 108 | The line number that the error occurred on (1-based) 109 | 110 | :type: int 111 | """ 112 | self.column = column 113 | """ 114 | The column that the error occurred on (1-based) 115 | 116 | :type: int 117 | """ 118 | self.function_name = function_name 119 | """ 120 | The name of the function that the error occurred in 121 | 122 | :type: str 123 | """ 124 | self.backtrace = backtrace 125 | """ 126 | A backtrace through Starlark's stack leading up to the error. 127 | 128 | :type: str 129 | """ 130 | 131 | context = self.filename 132 | if self.function_name != "": 133 | context += " in " + self.function_name 134 | 135 | self.error = f"{context}:{self.line}:{self.column}: {self.error}" 136 | 137 | 138 | class ResolveErrorItem: 139 | """ 140 | A location associated with a :py:class:`ResolveError`. 141 | """ 142 | 143 | def __init__(self, msg: str, line: int, column: int): 144 | self.msg = msg 145 | """ 146 | A description of the problem at the location. 147 | 148 | :type: str 149 | """ 150 | self.line = line 151 | """ 152 | The line where the problem occurred (1-based) 153 | 154 | :type: int 155 | """ 156 | self.column = column 157 | """ 158 | The column where the problem occurred (1-based) 159 | 160 | :type: int 161 | """ 162 | 163 | 164 | class ResolveError(StarlarkError): 165 | """ 166 | A Starlark resolution error. 167 | 168 | This exception is raised by 169 | :py:meth:`starlark_go.Starlark.eval` or :py:meth:`starlark_go.Starlark.exec` 170 | when an undefined name is referenced. 171 | """ 172 | 173 | def __init__( 174 | self, error: str, error_type: str, errors: Tuple[ResolveErrorItem, ...] 175 | ): 176 | super().__init__(error, error_type, errors) 177 | self.errors = list(errors) 178 | """ 179 | A list of locations where resolution errors occurred. A ResolveError may 180 | contain one or more locations. 181 | 182 | :type: typing.List[ResolveErrorItem] 183 | """ 184 | 185 | 186 | class ConversionError(StarlarkError): 187 | """ 188 | A base class for conversion errors. 189 | """ 190 | 191 | 192 | class ConversionToPythonFailed(ConversionError): 193 | """ 194 | An error when converting a Starlark value to a Python value. 195 | 196 | This exception is raied by :py:meth:`starlark_go.Starlark.eval` 197 | and :py:meth:`starlark_go.Starlark.get` when a Starlark value can 198 | not be converted to a Python value. 199 | """ 200 | 201 | 202 | class ConversionToStarlarkFailed(ConversionError): 203 | """ 204 | An error when converting a Python value to a Starlark value. 205 | 206 | This exception is raied by :py:meth:`starlark_go.Starlark.set` 207 | when a Python value can not be converted to a Starlark value. 208 | """ 209 | -------------------------------------------------------------------------------- /src/starlark_go/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caketop/python-starlark-go/a7a53178df0fea93f39d3e05a2f11c751c392a6e/src/starlark_go/py.typed -------------------------------------------------------------------------------- /src/starlark_go/starlark_go.pyi: -------------------------------------------------------------------------------- 1 | from typing import Any, Callable, List, Mapping, Optional 2 | 3 | def configure_starlark( 4 | *, 5 | allow_set: Optional[bool] = ..., 6 | allow_global_reassign: Optional[bool] = ..., 7 | allow_recursion: Optional[bool] = ..., 8 | ) -> None: ... 9 | 10 | class Starlark: 11 | def __init__( 12 | self, 13 | *, 14 | globals: Optional[Mapping[str, Any]] = ..., 15 | print: Callable[[str], Any] = ..., 16 | ) -> None: ... 17 | def eval( 18 | self, 19 | expr: str, 20 | *, 21 | filename: Optional[str] = ..., 22 | convert: Optional[bool] = ..., 23 | print: Callable[[str], Any] = ..., 24 | ) -> Any: ... 25 | def exec( 26 | self, 27 | defs: str, 28 | *, 29 | filename: Optional[str] = ..., 30 | print: Callable[[str], Any] = ..., 31 | ) -> None: ... 32 | def globals(self) -> List[str]: ... 33 | def get(self, name: str, default_value: Optional[Any] = ...) -> None: ... 34 | def set(self, **kwargs: Any) -> None: ... 35 | def pop(self, name: str, default_value: Optional[Any] = ...) -> Any: ... 36 | @property 37 | def print(self) -> Optional[Callable[[str], Any]]: ... 38 | @print.setter 39 | def print( 40 | self, value: Optional[Callable[[str], Any]] 41 | ) -> Optional[Callable[[str], Any]]: ... 42 | -------------------------------------------------------------------------------- /starlark.c: -------------------------------------------------------------------------------- 1 | #include "starlark.h" 2 | 3 | /* Declarations for object methods written in Go */ 4 | void ConfigureStarlark(int allowSet, int allowGlobalReassign, int allowRecursion); 5 | 6 | int Starlark_init(Starlark *self, PyObject *args, PyObject *kwds); 7 | Starlark *Starlark_new(PyTypeObject *type, PyObject *args, PyObject *kwds); 8 | void Starlark_dealloc(Starlark *self); 9 | PyObject *Starlark_eval(Starlark *self, PyObject *args); 10 | PyObject *Starlark_exec(Starlark *self, PyObject *args); 11 | PyObject *Starlark_global_names(Starlark *self, PyObject *_); 12 | PyObject *Starlark_get_global(Starlark *self, PyObject *args, PyObject **kwargs); 13 | PyObject *Starlark_set_globals(Starlark *self, PyObject *args, PyObject **kwargs); 14 | PyObject *Starlark_pop_global(Starlark *self, PyObject *args, PyObject **kwargs); 15 | PyObject *Starlark_get_print(Starlark *self, void *closure); 16 | int Starlark_set_print(Starlark *self, PyObject *value, void *closure); 17 | PyObject *Starlark_tp_iter(Starlark *self); 18 | 19 | /* Exceptions - the module init function will fill these in */ 20 | PyObject *StarlarkError; 21 | PyObject *SyntaxError; 22 | PyObject *EvalError; 23 | PyObject *ResolveError; 24 | PyObject *ResolveErrorItem; 25 | PyObject *ConversionToPythonFailed; 26 | PyObject *ConversionToStarlarkFailed; 27 | 28 | /* Wrapper for setting Starlark configuration options */ 29 | static char *configure_keywords[] = { 30 | "allow_set", "allow_global_reassign", "allow_recursion", NULL /* Sentinel */ 31 | }; 32 | 33 | PyObject *configure_starlark(PyObject *self, PyObject *args, PyObject *kwargs) 34 | { 35 | /* ConfigureStarlark interprets -1 as "unspecified" */ 36 | int allow_set = -1, allow_global_reassign = -1, allow_recursion = -1; 37 | 38 | if (PyArg_ParseTupleAndKeywords( 39 | args, 40 | kwargs, 41 | "|$ppp:configure_starlark", 42 | configure_keywords, 43 | &allow_set, 44 | &allow_global_reassign, 45 | &allow_recursion 46 | ) == 0) { 47 | return NULL; 48 | } 49 | 50 | ConfigureStarlark(allow_set, allow_global_reassign, allow_recursion); 51 | Py_RETURN_NONE; 52 | } 53 | 54 | PyDoc_STRVAR( 55 | configure_starlark_doc, 56 | "configure_starlark(*, allow_set=None, allow_global_reassign=None, " 57 | "allow_recursion=None)\n--\n\n" 58 | "Change what features the Starlark interpreter allows. Unfortunately, " 59 | "this manipulates global variables, and affects all Starlark interpreters " 60 | "in your application. It is not possible to have one Starlark " 61 | "interpreter with ``allow_set=True`` and another with ``allow_set=False`` " 62 | "simultaneously.\n\n" 63 | "All feature flags are initially ``False``.\n\n" 64 | "See the `starlark-go documentation " 65 | "`_ for " 66 | "more information.\n\n" 67 | ":param allow_set: If ``True``, allow the creation of `set " 68 | "`_ objects " 69 | "in Starlark.\n" 70 | ":type allow_set: typing.Optional[bool]\n" 71 | ":param allow_global_reassign: If ``True``, allow reassignment to top-level names; " 72 | "also, allow if/for/while at top-level.\n" 73 | ":type allow_global_reassign: typing.Optional[bool]\n" 74 | ":param allow_recursion: If ``True``, allow while statements and recursive " 75 | "functions.\n" 76 | ":type allow_recursion: typing.Optional[bool]\n" 77 | ); 78 | 79 | /* Argument names and documentation for our methods */ 80 | static char *init_keywords[] = {"globals", "print", NULL}; 81 | 82 | PyDoc_STRVAR( 83 | Starlark_init_doc, 84 | "Starlark(*, globals=None, print=None)\n--\n\n" 85 | "Create a Starlark object. A Starlark object contains a set of global variables, " 86 | "which can be manipulated by executing Starlark code.\n\n" 87 | ":param globals: Initial set of global variables. Keys must be strings. Values can " 88 | "be any type supported by :func:`set`.\n" 89 | ":type globals: typing.Mapping[str, typing.Any]\n" 90 | ":param print: A function to call in place of Starlark's ``print()`` function. If " 91 | "unspecified, Starlark's ``print()`` function will be forwarded to Python's " 92 | "built-in :py:func:`python:print`.\n" 93 | ":type print: typing.Callable[[str], typing.Any]\n" 94 | ); 95 | 96 | static char *eval_keywords[] = {"expr", "filename", "convert", "print", NULL}; 97 | 98 | PyDoc_STRVAR( 99 | Starlark_eval_doc, 100 | "eval(self, expr, *, filename=None, convert=True, print=None)\n--\n\n" 101 | "Evaluate a Starlark expression. The expression passed to ``eval`` must evaluate " 102 | "to a value. Function definitions, variable assignments, and control structures " 103 | "are not allowed by ``eval``. To use those, please use :meth:`exec`.\n\n" 104 | ":param expr: A string containing a Starlark expression to evaluate\n" 105 | ":type expr: str\n" 106 | ":param filename: An optional filename to use in exceptions, if evaluting the " 107 | "expression fails.\n" 108 | ":type filename: typing.Optional[str]\n" 109 | ":param convert: If True, convert the result of the expression into a Python " 110 | "value. If False, return a string containing the representation of the expression " 111 | "in Starlark. Defaults to True.\n" 112 | ":type convert: bool\n" 113 | ":param print: A function to call in place of Starlark's ``print()`` function. If " 114 | "unspecified, Starlark's ``print()`` function will be forwarded to Python's " 115 | "built-in :py:func:`python:print`.\n" 116 | ":type print: typing.Callable[[str], typing.Any]\n" 117 | ":raises ConversionToPythonFailed: if the value is of an unsupported type for " 118 | "conversion.\n" 119 | ":raises EvalError: if there is a Starlark evaluation error\n" 120 | ":raises ResolveError: if there is a Starlark resolution error\n" 121 | ":raises SyntaxError: if there is a Starlark syntax error\n" 122 | ":raises StarlarkError: if there is an unexpected error\n" 123 | ":rtype: typing.Any\n" 124 | ); 125 | 126 | static char *exec_keywords[] = {"defs", "filename", "print", NULL}; 127 | 128 | PyDoc_STRVAR( 129 | Starlark_exec_doc, 130 | "exec(self, defs, *, filename=None, print=None)\n--\n\n" 131 | "Execute Starlark code. All legal Starlark constructs may be used with " 132 | "``exec``.\n\n" 133 | "``exec`` does not return a value. To evaluate the value of a Starlark expression, " 134 | "please use func:`eval`.\n\n" 135 | ":param defs: A string containing Starlark code to execute\n" 136 | ":type defs: str\n" 137 | ":param filename: An optional filename to use in exceptions, if evaluting the " 138 | "expression fails.\n" 139 | ":type filename: Optional[str]\n" 140 | ":param print: A function to call in place of Starlark's ``print()`` function. If " 141 | "unspecified, Starlark's ``print()`` function will be forwarded to Python's " 142 | "built-in :py:func:`python:print`.\n" 143 | ":type print: typing.Callable[[str], typing.Any]\n" 144 | ":raises EvalError: if there is a Starlark evaluation error\n" 145 | ":raises ResolveError: if there is a Starlark resolution error\n" 146 | ":raises SyntaxError: if there is a Starlark syntax error\n" 147 | ":raises StarlarkError: if there is an unexpected error\n" 148 | ); 149 | 150 | static char *get_global_keywords[] = {"name", "default", NULL}; 151 | 152 | PyDoc_STRVAR( 153 | Starlark_get_doc, 154 | "get(self, name, default_value = ...)\n--\n\n" 155 | "Get the value of a Starlark global variable.\n\n" 156 | "Conversion from most Starlark data types is supported:\n\n" 157 | "* Starlark `None `_ to " 158 | "Python :py:obj:`python:None`\n" 159 | "* Starlark `bool `_ to " 160 | "Python :py:obj:`python:bool`\n" 161 | "* Starlark `bytes `_ to " 162 | "Python :py:obj:`python:bytes`\n" 163 | "* Starlark `float `_ to " 164 | "Python :py:obj:`python:float`\n" 165 | "* Starlark `int `_ to " 166 | "Python :py:obj:`python:int`\n" 167 | "* Starlark `string `_ to " 168 | "Python :py:obj:`python:str`\n" 169 | "* Starlark `dict `_ (and " 170 | "`IterableMapping `_) " 171 | "to Python :py:obj:`python:dict`\n" 172 | "* Starlark `list `_ (and " 173 | "`Iterable `_) to " 174 | "Python :py:obj:`python:list`\n" 175 | "* Starlark `set `_ to " 176 | "Python :py:obj:`python:set`\n" 177 | "* Starlark `tuple `_ to " 178 | "Python :py:obj:`python:tuple`\n\n" 179 | "For the aggregate types (``dict``, ``list``, ``set``, and ``tuple``,) all keys " 180 | "and/or values must also be one of the supported types.\n\n" 181 | "Attempting to get the value of any other Starlark type will raise a " 182 | ":py:class:`ConversionToPythonFailed`.\n\n" 183 | ":param name: The name of the global variable.\n" 184 | ":type name: str\n" 185 | ":param default_value: A default value to return, if no global variable named " 186 | "``name`` is defined.\n" 187 | ":type default_value: typing.Any\n" 188 | ":raises KeyError: if there is no global value named ``name`` defined.\n" 189 | ":raises ConversionToPythonFailed: if the value is of an unsupported type for " 190 | "conversion.\n" 191 | ":rtype: typing.Any\n" 192 | ); 193 | 194 | PyDoc_STRVAR( 195 | Starlark_globals_doc, 196 | "globals(self)\n--\n\n" 197 | "Get the names of the currently defined global variables.\n\n" 198 | ":rtype: typing.List[str]\n" 199 | ); 200 | 201 | PyDoc_STRVAR( 202 | Starlark_set_doc, 203 | "set(self, **kwargs)\n--\n\n" 204 | "Set the value of one or more Starlark global variables.\n\n" 205 | "For each keyword parameter specified, one global variable is set.\n\n" 206 | "Conversion from most basic Python types is supported:\n\n" 207 | "* Python :py:obj:`python:None` to Starlark `None " 208 | "`_\n" 209 | "* Python :py:obj:`python:bool` to Starlark `bool " 210 | "`_\n" 211 | "* Python :py:obj:`python:bytes` to Starlark `bytes " 212 | "`_\n" 213 | "* Python :py:obj:`python:float` to Starlark `float " 214 | "`_\n" 215 | "* Python :py:obj:`python:int` to Starlark `int " 216 | "`_\n" 217 | "* Python :py:obj:`python:str` to Starlark `string " 218 | "`_\n" 219 | "* Python :py:obj:`python:dict` (and other objects that implement the mapping " 220 | "protocol) to Starlark " 221 | "`dict `_\n" 222 | "* Python :py:obj:`python:list` (and other objects that implement the sequence " 223 | "protocol) to Starlark " 224 | "`list `_\n" 225 | "* Python :py:obj:`python:set` to Starlark `set " 226 | "`_\n" 227 | "* Python :py:obj:`python:tuple` to Starlark `tuple " 228 | "`_\n" 229 | "* Python functions can be registered as a Starlark function directly. " 230 | "Any exceptions raised will be rethrown as :py:class:`EvalError`.\n" 231 | "\n" 232 | "For the aggregate types (``dict``, ``list``, ``set``, and ``tuple``,) all keys " 233 | "and/or values must also be one of the supported types.\n\n" 234 | "Attempting to set a value of any other Python type will raise a " 235 | ":py:class:`ConversionToStarlarkFailed`.\n\n" 236 | ":raises ConversionToStarlarkFailed: if a value is of an unsupported type for " 237 | "conversion.\n" 238 | ); 239 | 240 | PyDoc_STRVAR( 241 | Starlark_pop_doc, 242 | "pop(self, name, default_value = ...)\n--\n\n" 243 | "Remove a Starlark global variable, and return its value.\n\n" 244 | "If a value of ``name`` does not exist, and no ``default_value`` has been " 245 | "specified, raise :py:obj:`python:KeyError`. Otherwise, return " 246 | "``default_value``.\n\n" 247 | ":param name: The name of the global variable.\n" 248 | ":type name: str\n" 249 | ":param default_value: A default value to return, if no global variable named " 250 | "``name`` is defined.\n" 251 | ":type default_value: typing.Any\n" 252 | ":raises KeyError: if there is no global value named ``name`` defined.\n" 253 | ":raises ConversionToPythonFailed: if the value is of an unsupported type for " 254 | "conversion.\n" 255 | ":rtype: typing.Any\n" 256 | ); 257 | 258 | PyDoc_STRVAR( 259 | Starlark_print_doc, 260 | "A function to call in place of Starlark's ``print()`` function. If " 261 | "unspecified, Starlark's ``print()`` function will be forwarded to Python's " 262 | "built-in :py:func:`python:print`.\n\n" 263 | ":type: typing.Callable[[str], typing.Any]\n" 264 | ); 265 | 266 | /* Container for module methods */ 267 | static PyMethodDef module_methods[] = { 268 | {"configure_starlark", 269 | (PyCFunction)configure_starlark, 270 | METH_VARARGS | METH_KEYWORDS, 271 | configure_starlark_doc}, 272 | {NULL} /* Sentinel */ 273 | }; 274 | 275 | /* Container for object methods */ 276 | static PyMethodDef StarlarkGo_methods[] = { 277 | {"eval", 278 | (PyCFunction)Starlark_eval, 279 | METH_VARARGS | METH_KEYWORDS, 280 | Starlark_eval_doc}, 281 | {"exec", 282 | (PyCFunction)Starlark_exec, 283 | METH_VARARGS | METH_KEYWORDS, 284 | Starlark_exec_doc}, 285 | {"globals", (PyCFunction)Starlark_global_names, METH_NOARGS, Starlark_globals_doc}, 286 | {"get", 287 | (PyCFunction)Starlark_get_global, 288 | METH_VARARGS | METH_KEYWORDS, 289 | Starlark_get_doc}, 290 | {"set", 291 | (PyCFunction)Starlark_set_globals, 292 | METH_VARARGS | METH_KEYWORDS, 293 | Starlark_set_doc}, 294 | {"pop", 295 | (PyCFunction)Starlark_pop_global, 296 | METH_VARARGS | METH_KEYWORDS, 297 | Starlark_pop_doc}, 298 | {NULL} /* Sentinel */ 299 | }; 300 | 301 | static PyGetSetDef Starlark_getset[] = { 302 | {"print", 303 | (getter)Starlark_get_print, 304 | (setter)Starlark_set_print, 305 | Starlark_print_doc, 306 | NULL}, 307 | {NULL}, 308 | }; 309 | 310 | /* Python type for object */ 311 | static PyTypeObject StarlarkType = { 312 | // clang-format off 313 | PyVarObject_HEAD_INIT(NULL, 0) 314 | .tp_name = "starlark_go.starlark_go.Starlark", 315 | // clang-format on 316 | .tp_doc = Starlark_init_doc, 317 | .tp_basicsize = sizeof(Starlark), 318 | .tp_itemsize = 0, 319 | .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, 320 | .tp_new = (newfunc)Starlark_new, 321 | .tp_init = (initproc)Starlark_init, 322 | .tp_dealloc = (destructor)Starlark_dealloc, 323 | .tp_methods = StarlarkGo_methods, 324 | .tp_iter = (getiterfunc)Starlark_tp_iter, 325 | .tp_getset = Starlark_getset, 326 | }; 327 | 328 | /* Module */ 329 | static PyModuleDef starlark_go = { 330 | PyModuleDef_HEAD_INIT, 331 | .m_name = "starlark_go.starlark_go", 332 | .m_doc = "Interface to starlark-go", 333 | .m_size = -1, 334 | .m_methods = module_methods, 335 | }; 336 | 337 | /* Helpers to allocate and free our object */ 338 | Starlark *starlarkAlloc(PyTypeObject *type) 339 | { 340 | /* Necessary because Cgo can't do function pointers */ 341 | return (Starlark *)type->tp_alloc(type, 0); 342 | } 343 | 344 | void starlarkFree(Starlark *self) 345 | { 346 | /* Necessary because Cgo can't do function pointers */ 347 | Py_TYPE(self)->tp_free((PyObject *)self); 348 | } 349 | 350 | /* Helpers to parse method arguments */ 351 | int parseInitArgs( 352 | PyObject *args, PyObject *kwargs, PyObject **globals, PyObject **print 353 | ) 354 | { 355 | /* Necessary because Cgo can't do varargs */ 356 | /* One optional object */ 357 | return PyArg_ParseTupleAndKeywords( 358 | args, kwargs, "|$OO:Starlark", init_keywords, globals, print 359 | ); 360 | } 361 | 362 | int parseEvalArgs( 363 | PyObject *args, 364 | PyObject *kwargs, 365 | char **expr, 366 | char **filename, 367 | unsigned int *convert, 368 | PyObject **print 369 | ) 370 | { 371 | /* Necessary because Cgo can't do varargs */ 372 | /* One required string, folloed by an optional string and an optional bool */ 373 | return PyArg_ParseTupleAndKeywords( 374 | args, kwargs, "s|$spO:eval", eval_keywords, expr, filename, convert, print 375 | ); 376 | } 377 | 378 | int parseExecArgs( 379 | PyObject *args, PyObject *kwargs, char **defs, char **filename, PyObject **print 380 | ) 381 | { 382 | /* Necessary because Cgo can't do varargs */ 383 | /* One required string, folloed by an optional string */ 384 | return PyArg_ParseTupleAndKeywords( 385 | args, kwargs, "s|$sO:exec", exec_keywords, defs, filename, print 386 | ); 387 | } 388 | 389 | int parseGetGlobalArgs( 390 | PyObject *args, PyObject *kwargs, char **name, PyObject **default_value 391 | ) 392 | { 393 | /* Necessary because Cgo can't do varargs */ 394 | /* One required string, full stop */ 395 | return PyArg_ParseTupleAndKeywords( 396 | args, kwargs, "s|O:get", get_global_keywords, name, default_value 397 | ); 398 | } 399 | 400 | int parsePopGlobalArgs( 401 | PyObject *args, PyObject *kwargs, char **name, PyObject **default_value 402 | ) 403 | { 404 | /* Necessary because Cgo can't do varargs */ 405 | /* One required string, full stop */ 406 | return PyArg_ParseTupleAndKeywords( 407 | args, kwargs, "s|O:pop", get_global_keywords, name, default_value 408 | ); 409 | } 410 | 411 | /* Helpers for Cgo to build exception arguments */ 412 | PyObject *makeStarlarkErrorArgs(const char *error_msg, const char *error_type) 413 | { 414 | /* Necessary because Cgo can't do varargs */ 415 | return Py_BuildValue("ss", error_msg, error_type); 416 | } 417 | 418 | PyObject *makeSyntaxErrorArgs( 419 | const char *error_msg, 420 | const char *error_type, 421 | const char *msg, 422 | const char *filename, 423 | const unsigned int line, 424 | const unsigned int column 425 | ) 426 | { 427 | /* Necessary because Cgo can't do varargs */ 428 | /* Four strings and two unsigned integers */ 429 | return Py_BuildValue("ssssII", error_msg, error_type, msg, filename, line, column); 430 | } 431 | 432 | PyObject *makeEvalErrorArgs( 433 | const char *error_msg, 434 | const char *error_type, 435 | const char *filename, 436 | const unsigned int line, 437 | const unsigned int column, 438 | const char *function_name, 439 | const char *backtrace 440 | ) 441 | { 442 | /* Necessary because Cgo can't do varargs */ 443 | /* Three strings */ 444 | return Py_BuildValue( 445 | "sssIIss", error_msg, error_type, filename, line, column, function_name, backtrace 446 | ); 447 | } 448 | 449 | PyObject *makeResolveErrorItem( 450 | const char *msg, const unsigned int line, const unsigned int column 451 | ) 452 | { 453 | /* Necessary because Cgo can't do varargs */ 454 | /* A string and two unsigned integers */ 455 | PyObject *args = Py_BuildValue("sII", msg, line, column); 456 | PyObject *obj = PyObject_CallObject(ResolveErrorItem, args); 457 | Py_DECREF(args); 458 | return obj; 459 | } 460 | 461 | PyObject *makeResolveErrorArgs( 462 | const char *error_msg, const char *error_type, PyObject *errors 463 | ) 464 | { 465 | /* Necessary because Cgo can't do varargs */ 466 | /* Two strings and a Python object */ 467 | return Py_BuildValue("ssO", error_msg, error_type, errors); 468 | } 469 | 470 | /* Other assorted helpers for Cgo */ 471 | PyObject *cgoPy_BuildString(const char *src) 472 | { 473 | /* Necessary because Cgo can't do varargs */ 474 | return Py_BuildValue("s", src); 475 | } 476 | 477 | PyObject *cgoPy_NewRef(PyObject *obj) 478 | { 479 | /* Necessary because Cgo can't do macros and Py_NewRef is part of 480 | * Python's "stable API" but only since 3.10 481 | */ 482 | Py_INCREF(obj); 483 | return obj; 484 | } 485 | 486 | int cgoPyFloat_Check(PyObject *obj) 487 | { 488 | /* Necessary because Cgo can't do macros */ 489 | return PyFloat_Check(obj); 490 | } 491 | 492 | int cgoPyLong_Check(PyObject *obj) 493 | { 494 | /* Necessary because Cgo can't do macros */ 495 | return PyLong_Check(obj); 496 | } 497 | 498 | int cgoPyUnicode_Check(PyObject *obj) 499 | { 500 | /* Necessary because Cgo can't do macros */ 501 | return PyUnicode_Check(obj); 502 | } 503 | 504 | int cgoPyBytes_Check(PyObject *obj) 505 | { 506 | /* Necessary because Cgo can't do macros */ 507 | return PyBytes_Check(obj); 508 | } 509 | 510 | int cgoPySet_Check(PyObject *obj) 511 | { 512 | /* Necessary because Cgo can't do macros */ 513 | return PySet_Check(obj); 514 | } 515 | 516 | int cgoPyTuple_Check(PyObject *obj) 517 | { 518 | /* Necessary because Cgo can't do macros */ 519 | return PyTuple_Check(obj); 520 | } 521 | 522 | int cgoPyDict_Check(PyObject *obj) 523 | { 524 | /* Necessary because Cgo can't do macros */ 525 | return PyDict_Check(obj); 526 | } 527 | 528 | int cgoPyList_Check(PyObject *obj) 529 | { 530 | /* Necessary because Cgo can't do macros */ 531 | return PyList_Check(obj); 532 | } 533 | 534 | int cgoPyFunc_Check(PyObject *obj) 535 | { 536 | /* Necessary because Cgo can't do macros */ 537 | return PyFunction_Check(obj); 538 | } 539 | 540 | int cgoPyMethod_Check(PyObject *obj) 541 | { 542 | /* Necessary because Cgo can't do macros */ 543 | return PyMethod_Check(obj); 544 | } 545 | 546 | /* Helper to fetch exception classes */ 547 | static PyObject *get_exception_class(PyObject *errors, const char *name) 548 | { 549 | PyObject *retval = PyObject_GetAttrString(errors, name); 550 | 551 | if (retval == NULL) 552 | PyErr_Format(PyExc_RuntimeError, "starlark_go.errors.%s is not defined", name); 553 | 554 | return retval; 555 | } 556 | 557 | /* Module initialization */ 558 | PyMODINIT_FUNC PyInit_starlark_go(void) 559 | { 560 | PyObject *errors = PyImport_ImportModule("starlark_go.errors"); 561 | if (errors == NULL) return NULL; 562 | 563 | StarlarkError = get_exception_class(errors, "StarlarkError"); 564 | if (StarlarkError == NULL) return NULL; 565 | 566 | SyntaxError = get_exception_class(errors, "SyntaxError"); 567 | if (SyntaxError == NULL) return NULL; 568 | 569 | EvalError = get_exception_class(errors, "EvalError"); 570 | if (EvalError == NULL) return NULL; 571 | 572 | ResolveError = get_exception_class(errors, "ResolveError"); 573 | if (ResolveError == NULL) return NULL; 574 | 575 | ResolveErrorItem = get_exception_class(errors, "ResolveErrorItem"); 576 | if (ResolveErrorItem == NULL) return NULL; 577 | 578 | ConversionToPythonFailed = get_exception_class(errors, "ConversionToPythonFailed"); 579 | if (ConversionToPythonFailed == NULL) return NULL; 580 | 581 | ConversionToStarlarkFailed = 582 | get_exception_class(errors, "ConversionToStarlarkFailed"); 583 | if (ConversionToStarlarkFailed == NULL) return NULL; 584 | 585 | PyObject *m; 586 | if (PyType_Ready(&StarlarkType) < 0) return NULL; 587 | 588 | m = PyModule_Create(&starlark_go); 589 | if (m == NULL) return NULL; 590 | 591 | Py_INCREF(&StarlarkType); 592 | if (PyModule_AddObject(m, "Starlark", (PyObject *)&StarlarkType) < 0) { 593 | Py_DECREF(&StarlarkType); 594 | Py_DECREF(m); 595 | 596 | return NULL; 597 | } 598 | 599 | return m; 600 | } 601 | -------------------------------------------------------------------------------- /starlark.h: -------------------------------------------------------------------------------- 1 | #ifndef PYTHON_STARLARK_GO_H 2 | #define PYTHON_STARLARK_GO_H 3 | 4 | #include 5 | #include 6 | #define PY_SSIZE_T_CLEAN 7 | #undef Py_LIMITED_API 8 | #include 9 | 10 | /* Starlark object */ 11 | typedef struct Starlark { 12 | PyObject_HEAD uintptr_t handle; 13 | } Starlark; 14 | 15 | /* Helpers for Cgo, which can't handle varargs or macros */ 16 | Starlark *starlarkAlloc(PyTypeObject *type); 17 | 18 | void starlarkFree(Starlark *self); 19 | 20 | int parseInitArgs( 21 | PyObject *args, PyObject *kwargs, PyObject **globals, PyObject **print 22 | ); 23 | 24 | int parseEvalArgs( 25 | PyObject *args, 26 | PyObject *kwargs, 27 | char **expr, 28 | char **filename, 29 | unsigned int *convert, 30 | PyObject **print 31 | ); 32 | 33 | int parseExecArgs( 34 | PyObject *args, PyObject *kwargs, char **defs, char **filename, PyObject **print 35 | ); 36 | 37 | int parseGetGlobalArgs( 38 | PyObject *args, PyObject *kwargs, char **name, PyObject **default_value 39 | ); 40 | 41 | int parsePopGlobalArgs( 42 | PyObject *args, PyObject *kwargs, char **name, PyObject **default_value 43 | ); 44 | 45 | PyObject *makeStarlarkErrorArgs(const char *error_msg, const char *error_type); 46 | 47 | PyObject *makeSyntaxErrorArgs( 48 | const char *error_msg, 49 | const char *error_type, 50 | const char *msg, 51 | const char *filename, 52 | const unsigned int line, 53 | const unsigned int column 54 | ); 55 | 56 | PyObject *makeEvalErrorArgs( 57 | const char *error_msg, 58 | const char *error_type, 59 | const char *filename, 60 | const unsigned int line, 61 | const unsigned int column, 62 | const char *function_name, 63 | const char *backtrace 64 | ); 65 | 66 | PyObject *makeResolveErrorItem( 67 | const char *msg, const unsigned int line, const unsigned int column 68 | ); 69 | 70 | PyObject *makeResolveErrorArgs( 71 | const char *error_msg, const char *error_type, PyObject *errors 72 | ); 73 | 74 | PyObject *cgoPy_BuildString(const char *src); 75 | 76 | PyObject *cgoPy_NewRef(PyObject *obj); 77 | 78 | int cgoPyFloat_Check(PyObject *obj); 79 | 80 | int cgoPyLong_Check(PyObject *obj); 81 | 82 | int cgoPyUnicode_Check(PyObject *obj); 83 | 84 | int cgoPyBytes_Check(PyObject *obj); 85 | 86 | int cgoPySet_Check(PyObject *obj); 87 | 88 | int cgoPyTuple_Check(PyObject *obj); 89 | 90 | int cgoPyMapping_Check(PyObject *obj); 91 | 92 | int cgoPyDict_Check(PyObject *obj); 93 | 94 | int cgoPyList_Check(PyObject *obj); 95 | 96 | int cgoPyFunc_Check(PyObject *obj); 97 | 98 | int cgoPyMethod_Check(PyObject *obj); 99 | 100 | #endif /* PYTHON_STARLARK_GO_H */ 101 | -------------------------------------------------------------------------------- /starlark_to_python.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | /* 4 | #include "starlark.h" 5 | 6 | extern PyObject *ConversionToPythonFailed; 7 | */ 8 | import "C" 9 | 10 | import ( 11 | "fmt" 12 | "reflect" 13 | "unsafe" 14 | 15 | "go.starlark.net/starlark" 16 | ) 17 | 18 | func starlarkIntToPython(x starlark.Int) (*C.PyObject, error) { 19 | /* Try to do it quickly */ 20 | xint, ok := x.Int64() 21 | if ok { 22 | return C.PyLong_FromLongLong(C.longlong(xint)), nil 23 | } 24 | 25 | /* Fall back to converting from string */ 26 | cstr := C.CString(x.String()) 27 | defer C.free(unsafe.Pointer(cstr)) 28 | return C.PyLong_FromString(cstr, nil, 10), nil 29 | } 30 | 31 | func starlarkStringToPython(x starlark.String) (*C.PyObject, error) { 32 | cstr := C.CString(string(x)) 33 | defer C.free(unsafe.Pointer(cstr)) 34 | return C.cgoPy_BuildString(cstr), nil 35 | } 36 | 37 | func starlarkDictToPython(x starlark.IterableMapping) (*C.PyObject, error) { 38 | items := x.Items() 39 | return starlarkDictItemsToPython(items) 40 | } 41 | 42 | func starlarkDictItemsToPython(items []starlark.Tuple) (*C.PyObject, error) { 43 | dict := C.PyDict_New() 44 | 45 | for _, item := range items { 46 | key, err := innerStarlarkValueToPython(item[0]) 47 | if key != nil { 48 | defer C.Py_DecRef(key) 49 | } 50 | 51 | if err != nil { 52 | C.Py_DecRef(dict) 53 | return nil, fmt.Errorf("While converting key %v in Starlark dict: %v", item[0], err) 54 | } 55 | 56 | value, err := innerStarlarkValueToPython((item[1])) 57 | if value != nil { 58 | defer C.Py_DecRef(value) 59 | } 60 | 61 | if err != nil { 62 | C.Py_DecRef(dict) 63 | return nil, fmt.Errorf("While converting value %v of key %v in Starlark dict: %v", item[1], item[0], err) 64 | } 65 | 66 | // This does not steal references 67 | C.PyDict_SetItem(dict, key, value) 68 | } 69 | 70 | return dict, nil 71 | } 72 | 73 | func starlarkTupleToPython(x starlark.Tuple) (*C.PyObject, error) { 74 | objs, err := starlarkTupleToPythonList(x) 75 | if err != nil { 76 | return nil, err 77 | } 78 | 79 | tuple := C.PyTuple_New(C.Py_ssize_t(len(objs))) 80 | for i, value := range objs { 81 | // This "steals" the ref to value so we don't need to DecRef after 82 | if C.PyTuple_SetItem(tuple, C.Py_ssize_t(i), value) != 0 { 83 | C.Py_DecRef(value) 84 | C.Py_DecRef(tuple) 85 | return nil, fmt.Errorf("Couldn't store converted value of %v at index %v in Python tuple: %v", x[i], i, err) 86 | } 87 | } 88 | 89 | return tuple, nil 90 | } 91 | 92 | func starlarkTupleToPythonList(x starlark.Tuple) ([]*C.PyObject, error) { 93 | result := make([]*C.PyObject, x.Len()) 94 | iter := x.Iterate() 95 | defer iter.Done() 96 | 97 | var elem starlark.Value 98 | for i := 0; iter.Next(&elem); i++ { 99 | value, err := innerStarlarkValueToPython(elem) 100 | if err != nil { 101 | if value != nil { 102 | C.Py_DecRef(value) 103 | } 104 | return nil, fmt.Errorf("While converting value %v at index %v in Starlark tuple: %v", elem, i, err) 105 | } 106 | 107 | result[i] = value 108 | } 109 | 110 | return result, nil 111 | } 112 | 113 | func starlarkListToPython(x starlark.Iterable) (*C.PyObject, error) { 114 | list := C.PyList_New(0) 115 | iter := x.Iterate() 116 | defer iter.Done() 117 | 118 | var elem starlark.Value 119 | for i := 0; iter.Next(&elem); i++ { 120 | value, err := innerStarlarkValueToPython(elem) 121 | if err != nil { 122 | C.Py_DecRef(list) 123 | return nil, fmt.Errorf("While converting value %v at index %v in Starlark list: %v", elem, i, err) 124 | } 125 | 126 | // This "steals" the ref to value so we don't need to DecRef after 127 | if C.PyList_Append(list, value) != 0 { 128 | C.Py_DecRef(value) 129 | C.Py_DecRef(list) 130 | return nil, fmt.Errorf("Couldn't store converted value of %v at index %v in Python list: %v", elem, i, err) 131 | } 132 | } 133 | 134 | return list, nil 135 | } 136 | 137 | func starlarkSetToPython(x starlark.Set) (*C.PyObject, error) { 138 | set := C.PySet_New(nil) 139 | iter := x.Iterate() 140 | defer iter.Done() 141 | 142 | var elem starlark.Value 143 | for i := 0; iter.Next(&elem); i++ { 144 | value, err := innerStarlarkValueToPython(elem) 145 | if value != nil { 146 | defer C.Py_DecRef(value) 147 | } 148 | 149 | if err != nil { 150 | C.Py_DecRef(set) 151 | return nil, fmt.Errorf("While converting value %v in Starlark set: %v", elem, err) 152 | } 153 | 154 | // This does not steal references 155 | C.PySet_Add(set, value) 156 | } 157 | 158 | return set, nil 159 | } 160 | 161 | func starlarkBytesToPython(x starlark.Bytes) (*C.PyObject, error) { 162 | cstr := C.CString(string(x)) 163 | defer C.free(unsafe.Pointer(cstr)) 164 | return C.PyBytes_FromStringAndSize(cstr, C.Py_ssize_t(x.Len())), nil 165 | } 166 | 167 | func innerStarlarkValueToPython(x starlark.Value) (*C.PyObject, error) { 168 | var value *C.PyObject = nil 169 | var err error = nil 170 | 171 | switch x := x.(type) { 172 | case starlark.NoneType: 173 | value = C.cgoPy_NewRef(C.Py_None) 174 | case starlark.Bool: 175 | if x { 176 | value = C.cgoPy_NewRef(C.Py_True) 177 | } else { 178 | value = C.cgoPy_NewRef(C.Py_False) 179 | } 180 | case starlark.Int: 181 | value, err = starlarkIntToPython(x) 182 | case starlark.Float: 183 | value = C.PyFloat_FromDouble(C.double(float64(x))) 184 | case starlark.String: 185 | value, err = starlarkStringToPython(x) 186 | case starlark.Bytes: 187 | value, err = starlarkBytesToPython(x) 188 | case *starlark.Set: 189 | value, err = starlarkSetToPython(*x) 190 | case starlark.IterableMapping: 191 | value, err = starlarkDictToPython(x) 192 | case starlark.Tuple: 193 | value, err = starlarkTupleToPython(x) 194 | case starlark.Iterable: 195 | value, err = starlarkListToPython(x) 196 | default: 197 | err = fmt.Errorf("Don't know how to convert Starlark %s to Python", reflect.TypeOf(x).String()) 198 | } 199 | 200 | if err == nil { 201 | if C.PyErr_Occurred() != nil { 202 | err = fmt.Errorf("Python exception while converting from Starlark") 203 | } 204 | } 205 | 206 | return value, err 207 | } 208 | 209 | func starlarkValueToPython(x starlark.Value) (*C.PyObject, error) { 210 | value, err := innerStarlarkValueToPython(x) 211 | if err != nil { 212 | handleConversionError(err, C.ConversionToPythonFailed) 213 | return nil, err 214 | } 215 | 216 | return value, nil 217 | } 218 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/caketop/python-starlark-go/a7a53178df0fea93f39d3e05a2f11c751c392a6e/tests/__init__.py -------------------------------------------------------------------------------- /tests/fibonacci.star: -------------------------------------------------------------------------------- 1 | def fibonacci(n=10): 2 | res = list(range(n)) 3 | for i in res[2:]: 4 | res[i] = res[i - 2] + res[i - 1] 5 | return res 6 | -------------------------------------------------------------------------------- /tests/test_configure.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from starlark_go import EvalError, Starlark, configure_starlark 4 | 5 | RFIB = """ 6 | def rfib(n): 7 | if n <= 1: 8 | return n 9 | else: 10 | return(rfib(n-1) + rfib(n-2)) 11 | 12 | def fibonacci(n): 13 | r = [] 14 | for i in range(n): 15 | r.append(rfib(i)) 16 | return r 17 | """ 18 | 19 | 20 | def test_recursion(): 21 | s = Starlark() 22 | s.exec(RFIB) 23 | 24 | configure_starlark(allow_recursion=False) 25 | with pytest.raises(EvalError): 26 | s.eval("fibonacci(5)") 27 | 28 | configure_starlark(allow_recursion=True) 29 | assert s.eval("fibonacci(5)") == [0, 1, 1, 2, 3] 30 | assert s.eval("fibonacci(10)") == [0, 1, 1, 2, 3, 5, 8, 13, 21, 34] 31 | 32 | 33 | def test_retention(): 34 | s = Starlark() 35 | 36 | configure_starlark(allow_set=True, allow_recursion=False) 37 | assert s.eval("set((1, 2, 3))") == set((1, 2, 3)) 38 | 39 | # test that allow_set is untouched after setting a different value 40 | configure_starlark(allow_recursion=True) 41 | assert s.eval("set((1, 2, 3))") == set((1, 2, 3)) 42 | -------------------------------------------------------------------------------- /tests/test_conversion_to_python_failed.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from starlark_go import ConversionToPythonFailed, Starlark 4 | 5 | STARLARK_SRC = """ 6 | foo = print 7 | bar = [1, print, 2] 8 | baz = {"c": print} 9 | """ 10 | 11 | 12 | DONT_KNOW = "Don't know how to convert Starlark *starlark.Builtin to Python" 13 | LIST_INDEX_1 = ( 14 | "While converting value at index 1 in Starlark list: " 15 | ) 16 | DICT_KEY_C = ( 17 | 'While converting value of key "c" in Starlark dict: ' 18 | ) 19 | 20 | 21 | @pytest.fixture 22 | def s() -> Starlark: 23 | starlark = Starlark() 24 | starlark.exec(STARLARK_SRC, filename="fake.star") 25 | return starlark 26 | 27 | 28 | def test_ConversionToPythonFailed(s: Starlark): 29 | with pytest.raises(ConversionToPythonFailed) as e: 30 | s.eval("print") 31 | assert str(e.value) == DONT_KNOW 32 | 33 | s.exec("foo = print") 34 | 35 | with pytest.raises(ConversionToPythonFailed) as e: 36 | s.get("foo") 37 | assert str(e.value) == DONT_KNOW 38 | 39 | 40 | def test_ConversionToPythonFailed_bar(s: Starlark): 41 | with pytest.raises(ConversionToPythonFailed) as e: 42 | s.eval("bar") 43 | assert str(e.value) == LIST_INDEX_1 + DONT_KNOW 44 | 45 | 46 | def test_ConversionToPythonFailed_baz(s: Starlark): 47 | with pytest.raises(ConversionToPythonFailed) as e: 48 | s.eval("baz") 49 | assert str(e.value) == DICT_KEY_C + DONT_KNOW 50 | -------------------------------------------------------------------------------- /tests/test_conversion_to_starlark_failed.py: -------------------------------------------------------------------------------- 1 | from typing import Sequence 2 | 3 | import pytest 4 | 5 | from starlark_go import ConversionToStarlarkFailed, Starlark 6 | 7 | foo = print 8 | bar = [1, print, 2] 9 | baz = {"c": print} 10 | 11 | 12 | DONT_KNOW = "Don't know how to convert Python builtin_function_or_method to Starlark" 13 | LIST_INDEX_1 = "While converting value at index 1 in Python list: " 14 | DICT_KEY_C = 'While converting value of key "c" in Python dict: ' 15 | 16 | 17 | class ExplodingException(Exception): 18 | pass 19 | 20 | 21 | class ExplodingSequence(Sequence[str]): 22 | def __len__(self): 23 | return 3 24 | 25 | def __getitem__(self, key: int): 26 | if key == 1: 27 | raise ExplodingException("Surprise!") 28 | return key * 100 29 | 30 | 31 | @pytest.fixture 32 | def s() -> Starlark: 33 | starlark = Starlark() 34 | return starlark 35 | 36 | 37 | def test_ConversionToStarlarkFailed(s: Starlark): 38 | with pytest.raises(ConversionToStarlarkFailed) as e: 39 | s.set(foo=foo) 40 | assert str(e.value) == DONT_KNOW 41 | assert getattr(e.value, "__cause__", None) is None 42 | 43 | 44 | def test_ConversionToStarlarkFailed_bar(s: Starlark): 45 | with pytest.raises(ConversionToStarlarkFailed) as e: 46 | s.set(bar=bar) 47 | assert str(e.value) == LIST_INDEX_1 + DONT_KNOW 48 | assert getattr(e.value, "__cause__", None) is None 49 | 50 | 51 | def test_ConversionToStarlarkFailed_baz(s: Starlark): 52 | with pytest.raises(ConversionToStarlarkFailed) as e: 53 | s.set(baz=baz) 54 | assert str(e.value) == DICT_KEY_C + DONT_KNOW 55 | assert getattr(e.value, "__cause__", None) is None 56 | 57 | 58 | def test_exploding_sequence(s: Starlark): 59 | with pytest.raises(ConversionToStarlarkFailed) as e: 60 | s.set(surprise=ExplodingSequence()) 61 | print(str(e.value)) 62 | assert isinstance(e.value, ConversionToStarlarkFailed) 63 | assert hasattr(e.value, "__cause__") 64 | assert isinstance(getattr(e.value, "__cause__"), ExplodingException) 65 | -------------------------------------------------------------------------------- /tests/test_evalerror.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from starlark_go import EvalError, Starlark 4 | 5 | STARLARK_SRC = """ 6 | def wrong(): 7 | return 1 + "2" 8 | """ 9 | 10 | 11 | def test_raises_evalerror(): 12 | s = Starlark() 13 | 14 | with pytest.raises(EvalError): 15 | s.eval('1 + "2"') 16 | 17 | with pytest.raises(EvalError): 18 | s.exec('1 + "2"') 19 | 20 | 21 | def test_eval_attrs(): 22 | s = Starlark() 23 | raised = False 24 | 25 | s.exec(STARLARK_SRC, filename="fake.star") 26 | 27 | try: 28 | s.eval("wrong()") 29 | except EvalError as e: 30 | assert hasattr(e, "error") 31 | assert isinstance(e.error, str) 32 | assert hasattr(e, "error_type") 33 | assert isinstance(e.error_type, str) 34 | assert e.error_type == "*starlark.EvalError" 35 | assert hasattr(e, "filename") 36 | assert isinstance(e.filename, str) 37 | assert e.filename == "fake.star" 38 | assert hasattr(e, "line") 39 | assert isinstance(e.line, int) 40 | assert e.line == 3 41 | assert hasattr(e, "column") 42 | assert isinstance(e.column, int) 43 | assert e.column == 12 44 | assert hasattr(e, "function_name") 45 | assert isinstance(e.function_name, str) 46 | assert e.function_name == "wrong" 47 | assert hasattr(e, "backtrace") 48 | assert isinstance(e.backtrace, str) 49 | 50 | strerror = str(e) 51 | assert strerror.startswith( 52 | f"{e.filename} in {e.function_name}:{e.line}:{e.column}: " 53 | ) 54 | raised = True 55 | 56 | assert raised 57 | -------------------------------------------------------------------------------- /tests/test_fibonacci.py: -------------------------------------------------------------------------------- 1 | from starlark_go import Starlark 2 | 3 | 4 | def test_fibonacci(): 5 | s = Starlark() 6 | fibonacci = """ 7 | def fibonacci(n=10): 8 | res = list(range(n)) 9 | for i in res[2:]: 10 | res[i] = res[i-2] + res[i-1] 11 | return res 12 | """ 13 | s.exec(fibonacci) 14 | assert s.eval("fibonacci(5)") == [0, 1, 1, 2, 3] 15 | assert s.eval("fibonacci(10)") == [0, 1, 1, 2, 3, 5, 8, 13, 21, 34] 16 | -------------------------------------------------------------------------------- /tests/test_get_globals.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from starlark_go import Starlark, configure_starlark 4 | from starlark_go.errors import ResolveError 5 | 6 | NESTED = [{"one": (1, 1, 1), "two": [2, {"two": 2222.22}]}, ("a", "b", "c")] 7 | NESTED_STR = '[{"one": (1, 1, 1), "two": [2, {"two": 2222.22}]}, ("a", "b", "c")]' 8 | 9 | 10 | def test_int(): 11 | s = Starlark() 12 | 13 | s.exec("x = 7") 14 | assert len(s.globals()) == 1 15 | assert s.globals() == ["x"] 16 | assert isinstance(s.get("x"), int) 17 | assert s.get("x") == 7 18 | 19 | # too big to fit in 64 bits 20 | s.exec("y = 10000000000000000000") 21 | assert len(s.globals()) == 2 22 | assert sorted(s.globals()) == ["x", "y"] 23 | assert isinstance(s.get("x"), int) 24 | assert isinstance(s.get("y"), int) 25 | assert s.get("x") == 7 26 | assert s.get("y") == 10000000000000000000 27 | 28 | 29 | def test_float(): 30 | s = Starlark() 31 | 32 | s.exec("x = 7.7") 33 | assert len(s.globals()) == 1 34 | assert s.globals() == ["x"] 35 | assert isinstance(s.get("x"), float) 36 | assert s.get("x") == 7.7 37 | 38 | 39 | def test_bool(): 40 | s = Starlark() 41 | 42 | s.exec("x = True") 43 | assert len(s.globals()) == 1 44 | assert s.globals() == ["x"] 45 | assert isinstance(s.get("x"), bool) 46 | assert s.get("x") is True 47 | 48 | 49 | def test_none(): 50 | s = Starlark() 51 | 52 | s.exec("x = None") 53 | assert len(s.globals()) == 1 54 | assert s.globals() == ["x"] 55 | assert s.get("x") is None 56 | 57 | 58 | def test_str(): 59 | s = Starlark() 60 | 61 | s.exec('x = "True"') 62 | assert len(s.globals()) == 1 63 | assert s.globals() == ["x"] 64 | assert isinstance(s.get("x"), str) 65 | assert s.get("x") == "True" 66 | 67 | 68 | def test_list(): 69 | s = Starlark() 70 | 71 | s.exec('x = [4, 2, 0, "go"]') 72 | assert len(s.globals()) == 1 73 | assert s.globals() == ["x"] 74 | assert isinstance(s.get("x"), list) 75 | assert s.get("x") == [4, 2, 0, "go"] 76 | 77 | 78 | def test_dict(): 79 | s = Starlark() 80 | 81 | s.exec('x = {"lamb": "little", "pickles": 3}') 82 | assert len(s.globals()) == 1 83 | assert s.globals() == ["x"] 84 | assert isinstance(s.get("x"), dict) 85 | assert s.get("x") == {"lamb": "little", "pickles": 3} 86 | 87 | 88 | def test_set(): 89 | s = Starlark() 90 | 91 | configure_starlark(allow_set=False) 92 | with pytest.raises(ResolveError): 93 | s.exec("x = set((1, 2, 3))") 94 | 95 | configure_starlark(allow_set=True) 96 | s.exec("x = set((1, 2, 3))") 97 | assert len(s.globals()) == 1 98 | assert s.globals() == ["x"] 99 | assert isinstance(s.get("x"), set) 100 | assert s.get("x") == set((1, 2, 3)) 101 | 102 | 103 | def test_bytes(): 104 | s = Starlark() 105 | 106 | s.exec("x = b'dead0000beef'") 107 | assert len(s.globals()) == 1 108 | assert s.globals() == ["x"] 109 | assert isinstance(s.get("x"), bytes) 110 | assert s.get("x") == b"dead0000beef" 111 | 112 | 113 | def test_tuple(): 114 | s = Starlark() 115 | 116 | s.exec("x = (13, 37)") 117 | assert len(s.globals()) == 1 118 | assert s.globals() == ["x"] 119 | assert isinstance(s.get("x"), tuple) 120 | assert s.get("x") == (13, 37) 121 | 122 | 123 | def test_nested(): 124 | s = Starlark() 125 | 126 | s.exec(f"x = {NESTED_STR}") 127 | assert s.globals() == ["x"] 128 | assert len(s.globals()) == 1 129 | assert s.get("x") == NESTED 130 | -------------------------------------------------------------------------------- /tests/test_import_exceptions.py: -------------------------------------------------------------------------------- 1 | def test_import_starlarkerror(): 2 | from starlark_go import StarlarkError 3 | 4 | assert issubclass(StarlarkError, BaseException) 5 | 6 | 7 | def test_import_syntaxerror(): 8 | from starlark_go import StarlarkError, SyntaxError 9 | 10 | assert issubclass(SyntaxError, StarlarkError) 11 | 12 | 13 | def test_import_evalerror(): 14 | from starlark_go import EvalError, StarlarkError 15 | 16 | assert issubclass(EvalError, StarlarkError) 17 | -------------------------------------------------------------------------------- /tests/test_multi_exec.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from starlark_go import ResolveError, Starlark 4 | 5 | ADD_ONE = """ 6 | def add_one(x): 7 | return x + 1 8 | """ 9 | 10 | ADD_TWO = """ 11 | def add_two(x): 12 | return add_one(add_one(x)) 13 | """ 14 | 15 | 16 | def test_multi_exec(): 17 | s = Starlark() 18 | 19 | s.exec(ADD_ONE) 20 | 21 | assert s.eval("add_one(1)") == 2 22 | 23 | with pytest.raises(ResolveError): 24 | s.eval("add_two(1)") 25 | 26 | s.exec(ADD_TWO) 27 | 28 | assert s.eval("add_two(1)") == 3 29 | -------------------------------------------------------------------------------- /tests/test_pop_global.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from starlark_go import Starlark 4 | 5 | 6 | def test_pop_global(): 7 | s = Starlark(globals={"a": 1, "b": 2, "c": 3}) 8 | 9 | assert sorted(s.globals()) == ["a", "b", "c"] 10 | 11 | b = s.pop("b") 12 | 13 | assert b == 2 14 | assert sorted(s.globals()) == ["a", "c"] 15 | 16 | with pytest.raises(KeyError): 17 | s.pop("b") 18 | 19 | assert s.pop("b", None) is None 20 | 21 | assert sorted(s.globals()) == ["a", "c"] 22 | -------------------------------------------------------------------------------- /tests/test_print.py: -------------------------------------------------------------------------------- 1 | from io import StringIO 2 | 3 | import pytest 4 | 5 | from starlark_go import Starlark 6 | 7 | 8 | def test_print_eval(capsys: pytest.CaptureFixture[str]): 9 | s = Starlark() 10 | s.eval('print("hello")') 11 | 12 | captured = capsys.readouterr() 13 | assert captured.out == "hello\n" 14 | 15 | 16 | def test_print_exec(capsys: pytest.CaptureFixture[str]): 17 | s = Starlark() 18 | s.exec('print("hello")') 19 | 20 | captured = capsys.readouterr() 21 | assert captured.out == "hello\n" 22 | 23 | 24 | def test_more_eval(): 25 | a = StringIO() 26 | b = StringIO() 27 | 28 | s = Starlark(print=lambda x: a.write(x + "\n")) 29 | s.eval('print("hello")') 30 | 31 | assert a.getvalue() == "hello\n" 32 | 33 | s.eval('print("hello")', print=lambda x: b.write(x + "\n")) 34 | 35 | assert a.getvalue() == "hello\n" 36 | assert b.getvalue() == "hello\n" 37 | 38 | s.print = lambda x: b.write(x + "\n") 39 | 40 | s.eval('print("goodbye")') 41 | 42 | assert a.getvalue() == "hello\n" 43 | assert b.getvalue() == "hello\ngoodbye\n" 44 | 45 | 46 | def test_more_exec(): 47 | a = StringIO() 48 | b = StringIO() 49 | 50 | s = Starlark(print=lambda x: a.write(x + "\n")) 51 | s.exec('print("hello")') 52 | 53 | assert a.getvalue() == "hello\n" 54 | 55 | s.exec('print("hello")', print=lambda x: b.write(x + "\n")) 56 | 57 | assert a.getvalue() == "hello\n" 58 | assert b.getvalue() == "hello\n" 59 | 60 | s.print = lambda x: b.write(x + "\n") 61 | 62 | s.exec('print("goodbye")') 63 | 64 | assert a.getvalue() == "hello\n" 65 | assert b.getvalue() == "hello\ngoodbye\n" 66 | -------------------------------------------------------------------------------- /tests/test_resolveerror.py: -------------------------------------------------------------------------------- 1 | from starlark_go import ResolveError, Starlark 2 | 3 | 4 | def test_eval_resolveerror(): 5 | s = Starlark() 6 | raised = False 7 | 8 | try: 9 | s.eval("add_one(1)") 10 | except ResolveError as e: 11 | assert isinstance(e.errors, list) 12 | assert len(e.errors) == 1 13 | assert e.errors[0].line == 1 14 | assert e.errors[0].column == 1 15 | assert e.errors[0].msg == "undefined: add_one" 16 | raised = True 17 | 18 | try: 19 | s.eval("from_bad(True) + to_worse(True)") 20 | except ResolveError as e: 21 | assert isinstance(e.errors, list) 22 | assert len(e.errors) == 2 23 | assert e.errors[0].line == 1 24 | assert e.errors[0].column == 1 25 | assert e.errors[0].msg == "undefined: from_bad" 26 | assert e.errors[1].line == 1 27 | assert e.errors[1].column == 18 28 | assert e.errors[1].msg == "undefined: to_worse" 29 | raised = True 30 | 31 | assert raised 32 | 33 | 34 | def test_exec_resolveerror(): 35 | s = Starlark() 36 | raised = False 37 | 38 | try: 39 | s.exec("add_one(1)") 40 | except ResolveError as e: 41 | assert isinstance(e.errors, list) 42 | assert len(e.errors) == 1 43 | assert e.errors[0].line == 1 44 | assert e.errors[0].column == 1 45 | assert e.errors[0].msg == "undefined: add_one" 46 | raised = True 47 | 48 | try: 49 | s.exec("from_bad(True) + to_worse(True)") 50 | except ResolveError as e: 51 | assert isinstance(e.errors, list) 52 | assert len(e.errors) == 2 53 | assert e.errors[0].line == 1 54 | assert e.errors[0].column == 1 55 | assert e.errors[0].msg == "undefined: from_bad" 56 | assert e.errors[1].line == 1 57 | assert e.errors[1].column == 18 58 | assert e.errors[1].msg == "undefined: to_worse" 59 | raised = True 60 | 61 | assert raised 62 | -------------------------------------------------------------------------------- /tests/test_set_globals.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | import pytest 4 | 5 | from starlark_go import EvalError, Starlark, StarlarkError 6 | 7 | NESTED = [{"one": (1, 1, 1), "two": [2, {"two": 2222.22}]}, ("a", "b", "c")] 8 | 9 | 10 | def test_set_globals(): 11 | s = Starlark() 12 | 13 | s.set() 14 | assert len(s.globals()) == 0 15 | 16 | s.set(x=1) 17 | assert len(s.globals()) == 1 18 | assert s.globals() == ["x"] 19 | 20 | s.set(x=1, y=[2], z={3: 3}) 21 | assert len(s.globals()) == 3 22 | assert sorted(s.globals()) == ["x", "y", "z"] 23 | 24 | s2 = Starlark(globals={"x": 1, "y": 2, "z": 3}) 25 | assert len(s2.globals()) == 3 26 | assert sorted(s2.globals()) == ["x", "y", "z"] 27 | 28 | s3 = Starlark(globals={}) 29 | assert len(s3.globals()) == 0 30 | 31 | with pytest.raises(TypeError): 32 | Starlark(globals=True) # type: ignore 33 | 34 | with pytest.raises(TypeError): 35 | Starlark(globals=[1, 2, 3]) # type: ignore 36 | 37 | with pytest.raises(TypeError): 38 | Starlark(globals="nope") # type: ignore 39 | 40 | with pytest.raises(TypeError): 41 | Starlark(globals=b"dead") # type: ignore 42 | 43 | with pytest.raises(TypeError): 44 | Starlark(globals=set((1, 2, 3))) # type: ignore 45 | 46 | 47 | def test_int(): 48 | s = Starlark() 49 | 50 | s.set(x=7) 51 | assert len(s.globals()) == 1 52 | assert s.globals() == ["x"] 53 | assert isinstance(s.get("x"), int) 54 | assert s.get("x") == 7 55 | assert s.eval("x + 1") == 8 56 | 57 | # too big to fit in 64 bits 58 | s.set(y=10000000000000000000) 59 | assert len(s.globals()) == 2 60 | assert sorted(s.globals()) == ["x", "y"] 61 | assert isinstance(s.get("x"), int) 62 | assert isinstance(s.get("y"), int) 63 | assert s.get("x") == 7 64 | assert s.get("y") == 10000000000000000000 65 | assert s.eval("y + 1") == 10000000000000000001 66 | 67 | 68 | def test_float(): 69 | s = Starlark() 70 | 71 | s.set(x=7.7) 72 | assert len(s.globals()) == 1 73 | assert s.globals() == ["x"] 74 | assert isinstance(s.get("x"), float) 75 | assert s.get("x") == 7.7 76 | assert s.eval("int(x)") == 7 77 | assert s.eval("int(x + 1)") == 8 78 | 79 | 80 | def test_bool(): 81 | s = Starlark() 82 | 83 | s.set(x=True) 84 | assert len(s.globals()) == 1 85 | assert s.globals() == ["x"] 86 | assert isinstance(s.get("x"), bool) 87 | assert s.get("x") is True 88 | assert s.eval("not x") is False 89 | 90 | 91 | def test_none(): 92 | s = Starlark() 93 | 94 | s.set(x=None) 95 | assert len(s.globals()) == 1 96 | assert s.globals() == ["x"] 97 | assert s.get("x") is None 98 | 99 | 100 | def test_str(): 101 | s = Starlark() 102 | 103 | s.set(x="True") 104 | assert len(s.globals()) == 1 105 | assert s.globals() == ["x"] 106 | assert isinstance(s.get("x"), str) 107 | assert s.get("x") == "True" 108 | assert s.eval("x + 'True'") == "TrueTrue" 109 | 110 | 111 | def test_list(): 112 | s = Starlark() 113 | 114 | s.set(x=[4, 2, 0, "go"]) 115 | assert len(s.globals()) == 1 116 | assert s.globals() == ["x"] 117 | assert isinstance(s.get("x"), list) 118 | assert s.get("x") == [4, 2, 0, "go"] 119 | 120 | 121 | def test_dict(): 122 | s = Starlark() 123 | 124 | s.set(x={"lamb": "little", "pickles": 3}) 125 | assert len(s.globals()) == 1 126 | assert s.globals() == ["x"] 127 | assert isinstance(s.get("x"), dict) 128 | assert s.get("x") == {"lamb": "little", "pickles": 3} 129 | 130 | 131 | def test_set(): 132 | s = Starlark() 133 | 134 | s.set(x=set((1, 2, 3))) 135 | assert len(s.globals()) == 1 136 | assert s.globals() == ["x"] 137 | assert isinstance(s.get("x"), set) 138 | assert s.get("x") == set((1, 2, 3)) 139 | 140 | 141 | def test_bytes(): 142 | s = Starlark() 143 | 144 | s.set(x=b"dead0000beef") 145 | assert len(s.globals()) == 1 146 | assert s.globals() == ["x"] 147 | assert isinstance(s.get("x"), bytes) 148 | assert s.get("x") == b"dead0000beef" 149 | 150 | 151 | def test_tuple(): 152 | s = Starlark() 153 | 154 | s.set(x=(13, 37)) 155 | assert len(s.globals()) == 1 156 | assert s.globals() == ["x"] 157 | assert isinstance(s.get("x"), tuple) 158 | assert s.get("x") == (13, 37) 159 | 160 | 161 | def test_nested(): 162 | s = Starlark() 163 | 164 | s.set(x=NESTED) 165 | assert s.globals() == ["x"] 166 | assert len(s.globals()) == 1 167 | assert s.get("x") == NESTED 168 | 169 | 170 | def test_func(): 171 | s = Starlark() 172 | 173 | def func_impl(x): 174 | if x == 0: 175 | raise ValueError("got zero") 176 | return x * 2 177 | 178 | s.set(func=func_impl) 179 | assert s.globals() == ["func"] 180 | 181 | with pytest.raises( 182 | StarlarkError, 183 | match=r"Don't know how to convert Starlark \*starlark.Builtin to Python", 184 | ): 185 | s.get("func") 186 | 187 | assert s.eval("func(10)") == 20 188 | assert s.eval("func(x = 10)") == 20 189 | 190 | with pytest.raises(EvalError, match=r" in func_impl:0:0: got zero"): 191 | s.eval("func(0)") 192 | 193 | name = "func_impl" if sys.version_info < (3, 10) else "test_func..func_impl" 194 | 195 | with pytest.raises(EvalError, match=r" in func_impl:0:0: " + name + r"\(\) missing 1 required positional argument: 'x'"): 196 | s.eval("func()") 197 | 198 | with pytest.raises(EvalError, match=r" in func_impl:0:0: " + name + r"\(\) got an unexpected keyword argument 'unknown'"): 199 | s.eval("func(unknown=0)") 200 | 201 | 202 | def test_func_references(): 203 | s = Starlark() 204 | 205 | def func_new_ref(): 206 | return {"a": 1} 207 | 208 | def func_arg_ref(x): 209 | return {"a": 1, "b": x} 210 | 211 | s.set( 212 | func_new_ref=func_new_ref, 213 | func_arg_ref=func_arg_ref, 214 | ) 215 | 216 | assert s.eval("func_new_ref()") == {"a": 1} 217 | assert s.eval("func_arg_ref([1, 2, 3])") == {"a": 1, "b": [1, 2, 3]} 218 | 219 | def test_method(): 220 | s = Starlark() 221 | 222 | class Test: 223 | def __init__(self): 224 | self.result = None 225 | 226 | def func_impl(self, x): 227 | if x == 0: 228 | raise ValueError("got zero") 229 | self.result = x * 2 230 | 231 | test = Test() 232 | s.set(func=test.func_impl) 233 | assert s.globals() == ["func"] 234 | 235 | with pytest.raises( 236 | StarlarkError, 237 | match=r"Don't know how to convert Starlark \*starlark.Builtin to Python", 238 | ): 239 | s.get("func") 240 | 241 | s.exec("func(10)") 242 | assert test.result == 20 243 | 244 | with pytest.raises(EvalError, match=r" in func_impl:0:0: got zero"): 245 | s.exec("func(0)") 246 | 247 | name = "func_impl" if sys.version_info < (3, 10) else "test_method..Test.func_impl" 248 | 249 | with pytest.raises(EvalError, match=r" in func_impl:0:0: " + name + r"\(\) missing 1 required positional argument: 'x'"): 250 | s.eval("func()") 251 | 252 | with pytest.raises(EvalError, match=r" in func_impl:0:0: " + name + r"\(\) got an unexpected keyword argument 'unknown'"): 253 | s.eval("func(unknown=0)") 254 | 255 | 256 | def test_method_references(): 257 | s = Starlark() 258 | 259 | class Test: 260 | def __init__(self): 261 | self.result = None 262 | 263 | def func_new_ref(self): 264 | self.result = {"a": 1} 265 | 266 | def func_arg_ref(self, x): 267 | self.result = {"a": 1, "b": x} 268 | 269 | test = Test() 270 | s.set( 271 | func_new_ref=test.func_new_ref, 272 | func_arg_ref=test.func_arg_ref, 273 | ) 274 | 275 | s.exec("func_new_ref()") 276 | assert test.result == {"a": 1} 277 | s.exec("func_arg_ref([1, 2, 3])") 278 | assert test.result == {"a": 1, "b": [1, 2, 3]} 279 | -------------------------------------------------------------------------------- /tests/test_syntaxerror.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from starlark_go import Starlark, SyntaxError 4 | 5 | 6 | def test_raises_syntaxerror(): 7 | s = Starlark() 8 | 9 | assert s.eval("7") == 7 10 | 11 | with pytest.raises(SyntaxError): 12 | s.eval(" 7 ") 13 | 14 | with pytest.raises(SyntaxError): 15 | s.exec(" 7 ") 16 | 17 | 18 | def test_syntaxerror_attrs(): 19 | s = Starlark() 20 | raised = False 21 | 22 | try: 23 | s.eval(" 7 ") 24 | except SyntaxError as e: 25 | assert hasattr(e, "error") 26 | assert isinstance(e.error, str) 27 | assert hasattr(e, "error_type") 28 | assert isinstance(e.error_type, str) 29 | assert e.error_type == "syntax.Error" 30 | assert hasattr(e, "msg") 31 | assert isinstance(e.msg, str) 32 | assert hasattr(e, "filename") 33 | assert isinstance(e.filename, str) 34 | assert e.filename == "" 35 | assert hasattr(e, "line") 36 | assert isinstance(e.line, int) 37 | assert e.line == 1 38 | assert hasattr(e, "column") 39 | assert isinstance(e.column, int) 40 | assert e.column == 2 41 | raised = True 42 | 43 | assert raised 44 | 45 | 46 | def test_syntaxerror_eval_filename(): 47 | s = Starlark() 48 | raised = False 49 | 50 | try: 51 | s.eval(" 7 ", filename="whyd_you_park_your_car_so_far_from.star") 52 | except SyntaxError as e: 53 | assert hasattr(e, "filename") 54 | assert isinstance(e.filename, str) 55 | assert e.filename == "whyd_you_park_your_car_so_far_from.star" 56 | raised = True 57 | 58 | assert raised 59 | 60 | 61 | def test_syntaxerror_exec_filename(): 62 | s = Starlark() 63 | raised = False 64 | 65 | try: 66 | s.exec(" 7 ", filename="whyd_you_park_your_car_so_far_from.star") 67 | except SyntaxError as e: 68 | assert hasattr(e, "filename") 69 | assert isinstance(e.filename, str) 70 | assert e.filename == "whyd_you_park_your_car_so_far_from.star" 71 | raised = True 72 | 73 | assert raised 74 | -------------------------------------------------------------------------------- /tests/test_values.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from starlark_go import Starlark, configure_starlark 4 | from starlark_go.errors import ResolveError 5 | 6 | NESTED = [{"one": (1, 1, 1), "two": [2, {"two": 2222.22}]}, ("a", "b", "c")] 7 | NESTED_STR = '[{"one": (1, 1, 1), "two": [2, {"two": 2222.22}]}, ("a", "b", "c")]' 8 | 9 | 10 | def test_int(): 11 | s = Starlark() 12 | 13 | x = s.eval("7") 14 | assert isinstance(x, int) 15 | assert x == 7 16 | 17 | x = s.eval("7", convert=True) 18 | assert isinstance(x, int) 19 | assert x == 7 20 | 21 | x = s.eval("7", convert=False) 22 | assert isinstance(x, str) 23 | assert x == "7" 24 | 25 | # too big to fit in 64 bits 26 | x = s.eval("10000000000000000000") 27 | assert isinstance(x, int) 28 | assert x == 10000000000000000000 29 | 30 | 31 | def test_float(): 32 | s = Starlark() 33 | 34 | x = s.eval("7.7") 35 | assert isinstance(x, float) 36 | assert x == 7.7 37 | 38 | x = s.eval("7.7", convert=True) 39 | assert isinstance(x, float) 40 | assert x == 7.7 41 | 42 | x = s.eval("7.7", convert=False) 43 | assert isinstance(x, str) 44 | assert x == "7.7" 45 | 46 | 47 | def test_bool(): 48 | s = Starlark() 49 | 50 | x = s.eval("True") 51 | assert isinstance(x, bool) 52 | assert x is True 53 | 54 | x = s.eval("True", convert=True) 55 | assert isinstance(x, bool) 56 | assert x is True 57 | 58 | x = s.eval("True", convert=False) 59 | assert isinstance(x, str) 60 | assert x == "True" 61 | 62 | 63 | def test_none(): 64 | s = Starlark() 65 | 66 | x = s.eval("None") 67 | assert x is None 68 | 69 | x = s.eval("None", convert=True) 70 | assert x is None 71 | 72 | x = s.eval("None", convert=False) 73 | assert isinstance(x, str) 74 | assert x == "None" 75 | 76 | 77 | def test_str(): 78 | s = Starlark() 79 | 80 | x = s.eval('"True"') 81 | assert isinstance(x, str) 82 | assert x == "True" 83 | 84 | x = s.eval('"True"', convert=True) 85 | assert isinstance(x, str) 86 | assert x == "True" 87 | 88 | x = s.eval('"True"', convert=False) 89 | assert isinstance(x, str) 90 | assert x == '"True"' 91 | 92 | 93 | def test_list(): 94 | s = Starlark() 95 | 96 | x = s.eval('[4, 2, 0, "go"]') 97 | assert isinstance(x, list) 98 | assert x == [4, 2, 0, "go"] 99 | 100 | x = s.eval('[4, 2, 0, "go"]', convert=True) 101 | assert isinstance(x, list) 102 | assert x == [4, 2, 0, "go"] 103 | 104 | x = s.eval('[4, 2, 0, "go"]', convert=False) 105 | assert isinstance(x, str) 106 | assert x == '[4, 2, 0, "go"]' 107 | 108 | 109 | def test_dict(): 110 | s = Starlark() 111 | 112 | x = s.eval('{"lamb": "little", "pickles": 3}') 113 | assert isinstance(x, dict) 114 | assert x == {"lamb": "little", "pickles": 3} 115 | 116 | x = s.eval('{"lamb": "little", "pickles": 3}', convert=True) 117 | assert isinstance(x, dict) 118 | assert x == {"lamb": "little", "pickles": 3} 119 | 120 | x = s.eval('{"lamb": "little", "pickles": 3}', convert=False) 121 | assert isinstance(x, str) 122 | assert x.startswith("{") 123 | assert x.endswith("}") 124 | 125 | 126 | def test_set(): 127 | s = Starlark() 128 | 129 | configure_starlark(allow_set=False) 130 | with pytest.raises(ResolveError): 131 | s.eval("set((1, 2, 3))") 132 | 133 | configure_starlark(allow_set=True) 134 | x = s.eval("set((1, 2, 3))") 135 | assert isinstance(x, set) 136 | assert x == set((1, 2, 3)) 137 | 138 | x = s.eval("set((1, 2, 3))", convert=True) 139 | assert isinstance(x, set) 140 | assert x == set((1, 2, 3)) 141 | 142 | x = s.eval("set((1, 2, 3))", convert=False) 143 | assert isinstance(x, str) 144 | assert x.startswith("set(") 145 | assert x.endswith(")") 146 | 147 | 148 | def test_bytes(): 149 | s = Starlark() 150 | 151 | x = s.eval("b'dead0000beef'") 152 | assert isinstance(x, bytes) 153 | assert x == b"dead0000beef" 154 | 155 | x = s.eval("b'dead0000beef'", convert=True) 156 | assert isinstance(x, bytes) 157 | assert x == b"dead0000beef" 158 | 159 | x = s.eval("b'dead0000beef'", convert=False) 160 | assert isinstance(x, str) 161 | assert x == 'b"dead0000beef"' 162 | 163 | 164 | def test_tuple(): 165 | s = Starlark() 166 | 167 | x = s.eval("(13, 37)") 168 | assert isinstance(x, tuple) 169 | assert x == (13, 37) 170 | 171 | x = s.eval("(13, 37)", convert=True) 172 | assert isinstance(x, tuple) 173 | assert x == (13, 37) 174 | 175 | x = s.eval("(13, 37)", convert=False) 176 | assert isinstance(x, str) 177 | assert x == "(13, 37)" 178 | 179 | 180 | def test_nested(): 181 | s = Starlark() 182 | 183 | x = s.eval(NESTED_STR) 184 | assert x == NESTED 185 | --------------------------------------------------------------------------------