Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

* Added `compas_rhino.install_with_pip` with corresponding command line utility `install_in_rhino`.
* Added support for `.stp` file extension in addition to `.step` for `RhinoBrep.from_step()` and `RhinoBrep.to_step()` methods.

### Changed
Expand Down
46 changes: 46 additions & 0 deletions docs/userguide/cad.rhino8.rst
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,52 @@ To create an editable install, you should update `pip` itself, first.
$ ~/.rhinocode/py39-rh8/python3.9 -m pip install -e .


Experimental Method
===================

COMPAS now makes the `install_in_rhino` command line utility available to simplify the installation of Python packages in Rhino 8 CPython.
This utility is available after installing the main `compas` package:

Install any Python package from PyPI

.. code-block:: bash

install_in_rhino requests numpy

Install from the current directory

.. code-block:: bash

install_in_rhino .

Install from a local path

.. code-block:: bash

install_in_rhino path/to/package

Install from a requirements file

.. code-block:: bash

install_in_rhino -r requirements.txt

Install in a specific site environment.

.. code-block:: bash

install_in_rhino compas --env compas-dev

Clear the site environment before installing

.. code-block:: bash

install_in_rhino compas --env compas-dev --clear


For more information, see :func:`compas_rhino.install_with_pip.install_in_rhino_with_pip`.


Verification
============

Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ Forum = "https://forum.compas-framework.org/"

[project.scripts]
compas_rpc = "compas.rpc.__main__:main"
install_in_rhino = "compas_rhino.install_with_pip:main"

# ============================================================================
# setuptools config
Expand Down
167 changes: 158 additions & 9 deletions src/compas_rhino/install_with_pip.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,169 @@
import argparse
import pathlib
import random
import shutil
import string
import subprocess
from typing import Optional

import compas_rhino
import compas

executable = "python" if compas.WINDOWS else "python3.9"
rhinocode = pathlib.Path().home() / ".rhinocode"
rhinopython = rhinocode / "py39-rh8" / executable
site_envs = rhinocode / "py39-rh8" / "site-envs"
Comment thread
tomvanmele marked this conversation as resolved.

def install(args):
subprocess.check_call([compas_rhino._get_default_rhino_cpython_path("8.0"), "-m", "pip", "install"] + args)

def random_string(length=8) -> str:
return "".join(random.choices(string.ascii_letters + string.digits, k=length))
Comment thread
tomvanmele marked this conversation as resolved.

if __name__ == "__main__":
import argparse

parser = argparse.ArgumentParser()
def find_full_env_name(name: str) -> str:
for filepath in site_envs.iterdir():
if filepath.stem.startswith(name):
return filepath.stem
raise ValueError(f"No environment with this name exists: {name}")


def ensure_site_env(name: str) -> str:
try:
fullname = find_full_env_name(name)
except ValueError:
fullname = f"{name}-{random_string()}"
return fullname


def install_in_rhino_with_pip(
packages: list[str],
requirements: Optional[str] = None,
env: str = "default",
upgrade: bool = False,
deps: bool = True,
clear: bool = False,
):
"""Install a package with Rhino's CPython pip.

Parameters
----------
packages : list of str
The package(s) to install (PyPI names or local package paths).
requirements : str, optional
Path to a requirements file.
env : str, optional
Name of the site env, without the random suffix.
upgrade : bool, optional
Attempt to upgrade packages already installed.
deps : bool, optional
If False, do not install dependencies.
clear : bool, optional
If True, clear the environment before installing.

Returns
-------
int
The return code from the pip install command.

Raises
------
ValueError
If both packages and requirements are provided, or if neither are provided.

Examples
--------
>>> install_in_rhino_with_pip(packages=["requests"], env="myenv", upgrade=True)
>>> install_in_rhino_with_pip(requirements="requirements.txt", env="myenv")
>>> install_in_rhino_with_pip(packages=["."], env="myenv")
>>> install_in_rhino_with_pip(packages=[".."], env="myenv")

Notes
-----
This function is made available as a command line script under the name `install_in_rhino`.
On the command line you can use the following syntax

.. code-block:: bash

install_in_rhino requests numpy
install_in_rhino -r requirements.txt --env myenv --upgrade
install_in_rhino . --env myenv
install_in_rhino .. --env myenv
install_in_rhino -r requirements.txt --env myenv --no-deps
install_in_rhino requests --env myenv --clear

"""

parser.add_argument("pipargs", help="Arguments to be passed on to pip as a string")
if requirements and packages:
raise ValueError("You must provide either packages or a requirements file, not both.")

if requirements:
params = ["-r", str(pathlib.Path(requirements).resolve())]
elif packages:
params = []
for p in packages:
if p == ".":
p = str(pathlib.Path().cwd())
elif p == "..":
p = str(pathlib.Path().cwd().parent)
params.append(p)
else:
raise ValueError("You must provide either packages or a requirements file.")

env = env or "default"

if clear:
try:
fullname = find_full_env_name(env)
except ValueError:
pass
else:
target = site_envs / fullname
shutil.rmtree(target, ignore_errors=True)

target = site_envs / ensure_site_env(env)
target.mkdir(exist_ok=True)

args = [
str(rhinopython),
"-m",
"pip",
"install",
*params,
"--target",
str(target),
"--no-warn-script-location",
]

if upgrade:
args.append("--upgrade")
if not deps:
args.append("--no-deps")

return subprocess.check_call(args)


def main():
parser = argparse.ArgumentParser(description="Install a package with Rhino's CPython pip.")
parser.add_argument("packages", help="The package(s) to install (PyPI names or local package paths)", nargs="*")
parser.add_argument("-r", "--requirements", help="Path to a requirements file")
parser.add_argument("--env", default="default", help="Name of the site env, without the random suffix")
parser.add_argument("--upgrade", action="store_true", help="Attempt to upgrade packages already installed")
parser.add_argument("--no-deps", dest="deps", action="store_false", help="Do not install dependencies")
parser.add_argument("--clear", action="store_true", help="Clear the environment before installing")
parser.set_defaults(deps=True)
args = parser.parse_args()
pipargs = args.pipargs.split()

install(pipargs)
install_in_rhino_with_pip(
packages=args.packages,
requirements=args.requirements,
env=args.env,
upgrade=args.upgrade,
deps=args.deps,
clear=args.clear,
)


# =============================================================================
# Main
# =============================================================================

if __name__ == "__main__":
main()
Loading