Skip to content

Commit 8fc7828

Browse files
authored
Merge pull request #1478 from compas-dev/install-with-pip
Install with pip
2 parents 43cb6b6 + c7e8434 commit 8fc7828

File tree

4 files changed

+206
-9
lines changed

4 files changed

+206
-9
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Added
1111

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

1415
### Changed

docs/userguide/cad.rhino8.rst

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,52 @@ To create an editable install, you should update `pip` itself, first.
6868
$ ~/.rhinocode/py39-rh8/python3.9 -m pip install -e .
6969
7070
71+
Experimental Method
72+
===================
73+
74+
COMPAS now makes the `install_in_rhino` command line utility available to simplify the installation of Python packages in Rhino 8 CPython.
75+
This utility is available after installing the main `compas` package:
76+
77+
Install any Python package from PyPI
78+
79+
.. code-block:: bash
80+
81+
install_in_rhino requests numpy
82+
83+
Install from the current directory
84+
85+
.. code-block:: bash
86+
87+
install_in_rhino .
88+
89+
Install from a local path
90+
91+
.. code-block:: bash
92+
93+
install_in_rhino path/to/package
94+
95+
Install from a requirements file
96+
97+
.. code-block:: bash
98+
99+
install_in_rhino -r requirements.txt
100+
101+
Install in a specific site environment.
102+
103+
.. code-block:: bash
104+
105+
install_in_rhino compas --env compas-dev
106+
107+
Clear the site environment before installing
108+
109+
.. code-block:: bash
110+
111+
install_in_rhino compas --env compas-dev --clear
112+
113+
114+
For more information, see :func:`compas_rhino.install_with_pip.install_in_rhino_with_pip`.
115+
116+
71117
Verification
72118
============
73119

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ Forum = "https://forum.compas-framework.org/"
3939

4040
[project.scripts]
4141
compas_rpc = "compas.rpc.__main__:main"
42+
install_in_rhino = "compas_rhino.install_with_pip:main"
4243

4344
# ============================================================================
4445
# setuptools config
Lines changed: 158 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,169 @@
1+
import argparse
2+
import pathlib
3+
import random
4+
import shutil
5+
import string
16
import subprocess
7+
from typing import Optional
28

3-
import compas_rhino
9+
import compas
410

11+
executable = "python" if compas.WINDOWS else "python3.9"
12+
rhinocode = pathlib.Path().home() / ".rhinocode"
13+
rhinopython = rhinocode / "py39-rh8" / executable
14+
site_envs = rhinocode / "py39-rh8" / "site-envs"
515

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

17+
def random_string(length=8) -> str:
18+
return "".join(random.choices(string.ascii_letters + string.digits, k=length))
919

10-
if __name__ == "__main__":
11-
import argparse
1220

13-
parser = argparse.ArgumentParser()
21+
def find_full_env_name(name: str) -> str:
22+
for filepath in site_envs.iterdir():
23+
if filepath.stem.startswith(name):
24+
return filepath.stem
25+
raise ValueError(f"No environment with this name exists: {name}")
26+
27+
28+
def ensure_site_env(name: str) -> str:
29+
try:
30+
fullname = find_full_env_name(name)
31+
except ValueError:
32+
fullname = f"{name}-{random_string()}"
33+
return fullname
34+
35+
36+
def install_in_rhino_with_pip(
37+
packages: list[str],
38+
requirements: Optional[str] = None,
39+
env: str = "default",
40+
upgrade: bool = False,
41+
deps: bool = True,
42+
clear: bool = False,
43+
):
44+
"""Install a package with Rhino's CPython pip.
45+
46+
Parameters
47+
----------
48+
packages : list of str
49+
The package(s) to install (PyPI names or local package paths).
50+
requirements : str, optional
51+
Path to a requirements file.
52+
env : str, optional
53+
Name of the site env, without the random suffix.
54+
upgrade : bool, optional
55+
Attempt to upgrade packages already installed.
56+
deps : bool, optional
57+
If False, do not install dependencies.
58+
clear : bool, optional
59+
If True, clear the environment before installing.
60+
61+
Returns
62+
-------
63+
int
64+
The return code from the pip install command.
65+
66+
Raises
67+
------
68+
ValueError
69+
If both packages and requirements are provided, or if neither are provided.
70+
71+
Examples
72+
--------
73+
>>> install_in_rhino_with_pip(packages=["requests"], env="myenv", upgrade=True)
74+
>>> install_in_rhino_with_pip(requirements="requirements.txt", env="myenv")
75+
>>> install_in_rhino_with_pip(packages=["."], env="myenv")
76+
>>> install_in_rhino_with_pip(packages=[".."], env="myenv")
77+
78+
Notes
79+
-----
80+
This function is made available as a command line script under the name `install_in_rhino`.
81+
On the command line you can use the following syntax
82+
83+
.. code-block:: bash
84+
85+
install_in_rhino requests numpy
86+
install_in_rhino -r requirements.txt --env myenv --upgrade
87+
install_in_rhino . --env myenv
88+
install_in_rhino .. --env myenv
89+
install_in_rhino -r requirements.txt --env myenv --no-deps
90+
install_in_rhino requests --env myenv --clear
91+
92+
"""
1493

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

97+
if requirements:
98+
params = ["-r", str(pathlib.Path(requirements).resolve())]
99+
elif packages:
100+
params = []
101+
for p in packages:
102+
if p == ".":
103+
p = str(pathlib.Path().cwd())
104+
elif p == "..":
105+
p = str(pathlib.Path().cwd().parent)
106+
params.append(p)
107+
else:
108+
raise ValueError("You must provide either packages or a requirements file.")
109+
110+
env = env or "default"
111+
112+
if clear:
113+
try:
114+
fullname = find_full_env_name(env)
115+
except ValueError:
116+
pass
117+
else:
118+
target = site_envs / fullname
119+
shutil.rmtree(target, ignore_errors=True)
120+
121+
target = site_envs / ensure_site_env(env)
122+
target.mkdir(exist_ok=True)
123+
124+
args = [
125+
str(rhinopython),
126+
"-m",
127+
"pip",
128+
"install",
129+
*params,
130+
"--target",
131+
str(target),
132+
"--no-warn-script-location",
133+
]
134+
135+
if upgrade:
136+
args.append("--upgrade")
137+
if not deps:
138+
args.append("--no-deps")
139+
140+
return subprocess.check_call(args)
141+
142+
143+
def main():
144+
parser = argparse.ArgumentParser(description="Install a package with Rhino's CPython pip.")
145+
parser.add_argument("packages", help="The package(s) to install (PyPI names or local package paths)", nargs="*")
146+
parser.add_argument("-r", "--requirements", help="Path to a requirements file")
147+
parser.add_argument("--env", default="default", help="Name of the site env, without the random suffix")
148+
parser.add_argument("--upgrade", action="store_true", help="Attempt to upgrade packages already installed")
149+
parser.add_argument("--no-deps", dest="deps", action="store_false", help="Do not install dependencies")
150+
parser.add_argument("--clear", action="store_true", help="Clear the environment before installing")
151+
parser.set_defaults(deps=True)
17152
args = parser.parse_args()
18-
pipargs = args.pipargs.split()
19153

20-
install(pipargs)
154+
install_in_rhino_with_pip(
155+
packages=args.packages,
156+
requirements=args.requirements,
157+
env=args.env,
158+
upgrade=args.upgrade,
159+
deps=args.deps,
160+
clear=args.clear,
161+
)
162+
163+
164+
# =============================================================================
165+
# Main
166+
# =============================================================================
167+
168+
if __name__ == "__main__":
169+
main()

0 commit comments

Comments
 (0)