Build Grasshopper
.ghusercomponents 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 GitHub action: same tool, usable from your own scripts, build systems or CI, without going through the action.
- Python 3.9 or newer
- pythonnet (installed automatically) and a .NET runtime, i.e. Windows
- The
GH_IO.dllassembly, which is downloaded automatically from NuGet unless you point at a local copy - On macOS, Mono and
libgdiplus:brew install mono mono-libgdiplus
pip install ghpython_componentizerghpython-componentizer <source> <target>For example, to build all component bundles under components into a build folder:
ghpython-componentizer components buildOptionally, tag the components with a version, which replaces the {{version}} template variable in the code:
ghpython-componentizer components build --version 0.1.2An optional name prefix can help tell components apart from other similarly named ones:
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:
ghpython-componentizer components build --ghio ./libThe tool is also runnable as a module, which is handy when the scripts folder is not on PATH:
python -m ghpython_componentizer components buildfrom ghpython_componentizer import build_components
build_components("components", "build", version="0.1.2", prefix="(COMPAS)")Or one component at a time:
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 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.
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:
brew install mono mono-libgdiplus
ghpython-componentizer components buildFrom 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:
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.
- Create a folder to contain your components.
- Each component goes into its own folder, called a source bundle.
- The name of the folder determines the name of the
.ghuserfile created. - Inside the component folder:
- Create a
metadata.jsonfile containing all required details of the component. - Add a lovely icon named
icon.png(24x24). - Add a
code.pyfile with the Python script of the component.
- Create a
components/
βββ My_Component/
βββ code.py
βββ icon.png
βββ metadata.json
- Icon name should be
icon.png - Icon dimensions should be
24x24
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.ghuserfile being generated.
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 to2(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.ghpythonmarshalGuids: (optional) Defines whether input Guids will be looked up or not. Defaults toTrue. Change toFalseto preserve input Guids.iconDisplay: (optional) Defines whether to display the icon or not. Defaults to0.0: Application setting1: Text display2: Icon display
inputParameters: List of input parameters.name: Name of the input parameter.nickname: (optional) Abbreviation of the input parameter. Defaults to the same asname.description: (optional) Description of the input parameter.optional: (optional) Defines whether the input parameter is optional or not. Defaults toTrue.allowTreeAccess: (optional) Defines whether to allow tree access for this input parameter. Defaults toTrue.showTypeHints: (optional) Defines whether to show type hints for this input parameter. Defaults toTrue.scriptParamAccess: (optional) Defines access type of the parameter. Defaults toitem. Accepts either integer value or string value.0/item: item access1/list: list access2/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 toghdoc. 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 toFalse.simplify: (optional) Defines whether data inside the parameter is simplified. Defaults toFalse.flatten: (optional) Defines whether data inside the parameter is flattened. Mutually exclusive withgraft. Defaults toFalse.graft: (optional) Defines whether data inside the parameter is grafted. Mutually exclusive withflatten. Defaults toFalse.
outputParameters: List of output parameters.name: Name of the output parameter.nickname: (optional) Abbreviation of the output parameter. Defaults to the same asname.description: (optional) Description of the output parameter.optional: (optional) Defines whether the output parameter is optional or not. Defaults toFalse.reverse: (optional) Defines whether data inside the parameter is reversed. Defaults toFalse.simplify: (optional) Defines whether data inside the parameter is simplified. Defaults toFalse.flatten: (optional) Defines whether data inside the parameter is flattened. Mutually exclusive withgraft. Defaults toFalse.graft: (optional) Defines whether data inside the parameter is grafted. Mutually exclusive withflatten. Defaults toFalse.
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.
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.
pip install -e ".[dev]"
pytest
ruff check .
ruff format .Most of the test suite covers the parts that do not require a .NET runtime, so it runs on any
platform. The tests marked dotnet build the examples into actual components and
read them back, and skip themselves when there is no runtime to do it with:
pytest -m "not dotnet" # unit tests only
pytest -m dotnet # build the example componentsLinting and formatting follow the same ruff rules as the other
COMPAS packages, configured in pyproject.toml.
Releases are prepared and published by pull request, with the shared compas-actions.
- Describe the changes under
## Unreleasedin CHANGELOG.md, as they are merged. - Run the prepare release workflow from the Actions tab, choosing
patch,minorormajor. It bumps the version, closes the changelog section and opens arelease/vX.Y.Zpull request. - Review and merge that pull request.
Merging it runs the release workflow: it builds the distributions, publishes them to PyPI through
trusted publishing 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.
This package is maintained by Gramazio Kohler Research @gramaziokohler and it is published under an MIT License.