Metadata-Version: 2.5
Name: ghpython_componentizer
Version: 0.1.0
Summary: Build Grasshopper .ghuser components out of plain Python source bundles
Project-URL: Homepage, https://github.com/compas-dev/ghpython_componentizer
Project-URL: Repository, https://github.com/compas-dev/ghpython_componentizer
Project-URL: Issues, https://github.com/compas-dev/ghpython_componentizer/issues
Author: Gonzalo Casas, Andrea Settimi, Chen Kasirer, Rafael Pastrana, Sam Wilcock
Maintainer-email: compas-dev <info@compas.dev>
License: MIT License
        
        Copyright (c) 2026 Gramazio Kohler Research, ETH Zurich
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
License-File: LICENSE
Keywords: compas,ghpython,ghuser,grasshopper,rhino
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: MacOS
Classifier: Operating System :: Microsoft :: Windows
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Multimedia :: Graphics :: 3D Modeling
Requires-Python: >=3.9
Requires-Dist: pythonnet>=3.0.3
Provides-Extra: dev
Requires-Dist: build>=1.0; extra == 'dev'
Requires-Dist: bump-my-version>=0.20; extra == 'dev'
Requires-Dist: pytest>=7.0; extra == 'dev'
Requires-Dist: ruff>=0.5; extra == 'dev'
Description-Content-Type: text/markdown

# ghpython_componentizer

> Build Grasshopper `.ghuser` components out of plain Python source bundles. 🐵

Write your Grasshopper components as real Python files, in a real editor, under version control,
and compile them into `.ghuser` components for the CPython (Python 3) interpreter of Rhino 8.

This package is the standalone version of the componentizer behind the
[compas-actions.ghpython_components](https://github.com/compas-dev/compas-actions.ghpython_components)
GitHub action: same tool, usable from your own scripts, build systems or CI, without going through the action.

## Requirements

* Python 3.9 or newer
* [pythonnet](https://pythonnet.github.io/) (installed automatically) and a .NET runtime, i.e. Windows
* The `GH_IO.dll` assembly, which is downloaded automatically from NuGet unless you point at a local copy
* On macOS, [Mono](https://www.mono-project.com/) and `libgdiplus`: `brew install mono mono-libgdiplus`

## Installation

```bash
pip install ghpython_componentizer
```

## Usage

### From the command line

```bash
ghpython-componentizer <source> <target>
```

For example, to build all component bundles under `components` into a `build` folder:

```bash
ghpython-componentizer components build
```

Optionally, tag the components with a version, which replaces the `{{version}}` template variable in the code:

```bash
ghpython-componentizer components build --version 0.1.2
```

An optional name prefix can help tell components apart from other similarly named ones:

```bash
ghpython-componentizer components build --prefix "(PACKAGE-NAME)"
```

If you already have a copy of `GH_IO.dll` (e.g. from a NuGet restore), point the tool at the folder
containing it to skip the download:

```bash
ghpython-componentizer components build --ghio ./lib
```

The tool is also runnable as a module, which is handy when the scripts folder is not on `PATH`:

```bash
python -m ghpython_componentizer components build
```

### From Python

```python
from ghpython_componentizer import build_components

build_components("components", "build", version="0.1.2", prefix="(COMPAS)")
```

Or one component at a time:

```python
from ghpython_componentizer import create_ghuser_component

create_ghuser_component("components/MyComponent", "build/MyComponent.ghuser")
```

Both functions download `GH_IO.dll` from NuGet on first use, unless the `ghio_dir` argument points
at a folder that contains it.

### From a build script, on any platform

```python
from ghpython_componentizer import run_componentizer

run_componentizer("components", "build", version="0.1.2", prefix="(COMPAS)")
```

This does the same as `build_components`, but in a subprocess started with the environment the
platform needs. On macOS that is the only thing that works from a process that is already running,
see below. Everywhere else the two are interchangeable.

## macOS

Only Mono can load the `net48` assemblies this tool needs, and Mono draws the component icons
through the native `libgdiplus` library. Homebrew installs `libgdiplus` in a prefix the dynamic
loader does not search by default, so without help the build fails with a
`System.TypeInitializationException` while embedding the icon.

`DYLD_LIBRARY_PATH` is only read when a process starts, so it cannot be fixed from inside a running
process. The command line therefore relaunches itself once, with the right environment, and
everything works out of the box:

```bash
brew install mono mono-libgdiplus
ghpython-componentizer components build
```

From Python, `build_components` and `create_ghuser_component` cannot fix the environment of the
process calling them: they raise a `RuntimeError` explaining what to do. Use `run_componentizer`
instead, or start your own subprocess with the environment returned by `componentizer_env`:

```python
import subprocess
import sys

from ghpython_componentizer import componentizer_env

# Note: no shell in between, macOS strips DYLD_* variables when it starts one.
subprocess.run(
    [sys.executable, "-m", "ghpython_componentizer", "components", "build"],
    env=componentizer_env(),
    check=True,
)
```

The same caveat applies to the interpreter itself: macOS strips `DYLD_*` variables when starting a
protected binary, so build with a Python from python.org, Homebrew or uv rather than
`/usr/bin/python3`.

## How to create components

1. Create a folder to contain your components.
1. Each component goes into its own folder, called a *source bundle*.
1. The name of the folder determines the name of the `.ghuser` file created.
1. Inside the component folder:
   1. Create a `metadata.json` file containing all required details of the component.
   1. Add a lovely icon named `icon.png` (24x24).
   1. Add a `code.py` file with the Python script of the component.

```
components/
└── My_Component/
    ├── code.py
    ├── icon.png
    └── metadata.json
```

## Specification

### Icon

* Icon name should be `icon.png`
* Icon dimensions should be `24x24`

### Python code

Supports a small set of templated variables that can be used in code:

* `{{version}}`: Gets replaced with the version, if specified.
* `{{name}}`: Gets replaced with the name of the component as defined in the metadata file.
* `{{ghuser_name}}`: Gets replaced with the name of the `.ghuser` file being generated.

### Metadata

* `name`: Name of the component. Keep it short, single words are best.
* `nickname`: Abbreviation of the component. Keep it short, 1~5 character words are best.
* `category`: Category of the component. The category controls in which tab the component will end up.
* `subcategory`: Subcategory for this component. The subcategory controls in which panel the component will end up.
* `description`: **(optional)** Description of the component. Be succinct but clear.
* `exposure`: **(optional)** Controls where the component will be exposed. Defaults to `2` (primary). Accepts one of the following integer values:
  * `-1`: Hidden. Do not expose the object anywhere.
  * `2`: Primary. Expose the object in the first section on the toolbar.
  * `4`: Secondary. Expose the object in the second section on the toolbar.
  * `8`, `16`, `32`, `64`, `128`: Expose the object in the third to seventh section on the toolbar.
* `instanceGuid`: **(optional)** Statically define a GUID for this instance. Defaults to a new Guid.
* `ghpython`
  * `marshalGuids`: **(optional)** Defines whether input Guids will be looked up or not. Defaults to `True`. Change to `False` to preserve input Guids.
  * `iconDisplay`: **(optional)** Defines whether to display the icon or not. Defaults to `0`.
    * `0`: Application setting
    * `1`: Text display
    * `2`: Icon display
  * `inputParameters`: List of input parameters.
    * `name`: Name of the input parameter.
    * `nickname`: **(optional)** Abbreviation of the input parameter. Defaults to the same as `name`.
    * `description`: **(optional)** Description of the input parameter.
    * `optional`: **(optional)** Defines whether the input parameter is optional or not. Defaults to `True`.
    * `allowTreeAccess`: **(optional)** Defines whether to allow tree access for this input parameter. Defaults to `True`.
    * `showTypeHints`: **(optional)** Defines whether to show type hints for this input parameter. Defaults to `True`.
    * `scriptParamAccess`: **(optional)** Defines access type of the parameter. Defaults to `item`. Accepts either integer value or string value.
      * `0` / `item`: item access
      * `1` / `list`: list access
      * `2` / `tree`: tree access
    * `wireDisplay`: **(optional)** Defines wire display type. Accepts either integer value or string value.
      * `0` / `default`: Wire display is controlled by the application settings.
      * `1` / `faint`: Wires are displayed faintly while the parameter is not selected.
      * `2` / `hidden`: Wires are not displayed at all while the parameter is not selected.
    * `typeHintID`: **(optional)** Defines the type hint of the input parameter. Defaults to `ghdoc`.
      Accepts either a Guid value or one of the following string values:
      `none`, `ghdoc`, `float`, `bool`, `int`, `complex`, `str`, `datetime`, `guid`,
      `color`, `point`, `vector`, `plane`, `interval`, `uvinterval`, `box`, `transform`,
      `line`, `circle`, `arc`, `polyline`, `rectangle`, `curve`, `mesh`, `surface`, `subd`, `brep`,
      `pointcloud`, `geometrybase`.
    * `reverse`: **(optional)** Defines whether data inside the parameter is reversed. Defaults to `False`.
    * `simplify`: **(optional)** Defines whether data inside the parameter is simplified. Defaults to `False`.
    * `flatten`: **(optional)** Defines whether data inside the parameter is flattened. Mutually exclusive with `graft`. Defaults to `False`.
    * `graft`: **(optional)** Defines whether data inside the parameter is grafted. Mutually exclusive with `flatten`. Defaults to `False`.
  * `outputParameters`: List of output parameters.
    * `name`: Name of the output parameter.
    * `nickname`: **(optional)** Abbreviation of the output parameter. Defaults to the same as `name`.
    * `description`: **(optional)** Description of the output parameter.
    * `optional`: **(optional)** Defines whether the output parameter is optional or not. Defaults to `False`.
    * `reverse`: **(optional)** Defines whether data inside the parameter is reversed. Defaults to `False`.
    * `simplify`: **(optional)** Defines whether data inside the parameter is simplified. Defaults to `False`.
    * `flatten`: **(optional)** Defines whether data inside the parameter is flattened. Mutually exclusive with `graft`. Defaults to `False`.
    * `graft`: **(optional)** Defines whether data inside the parameter is grafted. Mutually exclusive with `flatten`. Defaults to `False`.

## Caveats

GHUser components have one important limitation: once used in a document, they forget who they are.
They don't know they were created out of a `ghuser` component, they will be simple script components.
This has an important consequence: **if you update the `ghuser` components,
those already in use will NOT be automatically updated**.

## IronPython components

This package builds components for the CPython (Python 3) interpreter of Rhino 8 only.
Components for the IronPython interpreters of Rhino 7 and Rhino 8 are still built with the
scripts of the [GitHub action](https://github.com/compas-dev/compas-actions.ghpython_components).

## Development

```bash
pip install -e ".[dev]"
pytest
ruff check .
ruff format .
```

The test suite covers the parts that do not require a .NET runtime, so it runs on any platform.
Linting and formatting follow the same [ruff](https://docs.astral.sh/ruff/) rules as the other
COMPAS packages, configured in `pyproject.toml`.

## Releasing

Releases are prepared and published by pull request, with the shared
[compas-actions](https://github.com/compas-dev/compas-actions).

1. Describe the changes under `## Unreleased` in [CHANGELOG.md](CHANGELOG.md), as they are merged.
1. Run the *prepare release* workflow from the Actions tab, choosing `patch`, `minor` or `major`.
   It bumps the version, closes the changelog section and opens a `release/vX.Y.Z` pull request.
1. Review and merge that pull request.

Merging it runs the *release* workflow: it builds the distributions, publishes them to PyPI through
[trusted publishing](https://docs.pypi.org/trusted-publishers/) and creates the tagged GitHub
release, with the changelog section as release notes. No API token is involved: PyPI needs a
publisher configured for this repository, the `release.yml` workflow and the `pypi` environment.

## License

This package is maintained by Gramazio Kohler Research [@gramaziokohler](https://github.com/gramaziokohler)
and it is published under an [MIT License](LICENSE).
