-
Notifications
You must be signed in to change notification settings - Fork 13
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add
robotpy sync
command to download project requirements
- First part of fix for #70
- Loading branch information
Showing
3 changed files
with
285 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,119 @@ | ||
import argparse | ||
import logging | ||
import os | ||
import pathlib | ||
import sys | ||
|
||
from .utils import handle_cli_error | ||
|
||
|
||
from .installer import RobotpyInstaller | ||
from . import pyproject | ||
|
||
logger = logging.getLogger("sync") | ||
|
||
|
||
class Sync: | ||
""" | ||
Downloads RoboRIO requirements and installs requirements locally | ||
The project requirements are determined by reading pyproject.toml. An | ||
example pyproject.toml is: | ||
[tool.robotpy] | ||
# Version of robotpy this project depends on | ||
robotpy_version = "{robotpy_version}" | ||
# Which extras should be installed | ||
# -> equivalent to `pip install robotpy[extra1, ...] | ||
robotpy_extras = [] | ||
# Other pip packages to install (each element is equivalent to | ||
# a line in requirements.txt) | ||
requires = [] | ||
If no pyproject.toml exists, a default is created using the current robotpy | ||
package version. | ||
You must be connected to the internet for this to work. | ||
""" | ||
|
||
def __init__(self, parser: argparse.ArgumentParser): | ||
parser.add_argument( | ||
"--user", | ||
"-u", | ||
default=False, | ||
action="store_true", | ||
help="Use `pip install --user` to install packages", | ||
) | ||
|
||
parser.add_argument( | ||
"--use-certifi", | ||
action="store_true", | ||
default=False, | ||
help="Use SSL certificates from certifi", | ||
) | ||
|
||
@handle_cli_error | ||
def run( | ||
self, | ||
project_path: pathlib.Path, | ||
main_file: pathlib.Path, | ||
user: bool, | ||
use_certifi: bool, | ||
): | ||
if not main_file.exists(): | ||
print( | ||
f"ERROR: is this a robot project? {main_file} does not exist", | ||
file=sys.stderr, | ||
) | ||
return 1 | ||
|
||
installer = RobotpyInstaller() | ||
|
||
# parse pyproject.toml to determine the requirements | ||
project = pyproject.load(project_path, write_if_missing=True) | ||
|
||
packages = [str(project.robotpy_requires)] | ||
packages.extend([str(req) for req in project.requires]) | ||
|
||
logger.info("Robot project requirements:") | ||
for package in packages: | ||
logger.info("- %s", package) | ||
|
||
# | ||
# First, download requirements for RoboRIO | ||
# | ||
|
||
logger.info("Downloading Python for RoboRIO") | ||
installer.download_python(use_certifi) | ||
|
||
logger.info("Downloading RoboRIO python packages") | ||
installer.pip_download( | ||
no_deps=False, | ||
pre=False, | ||
requirements=[], | ||
packages=packages, | ||
) | ||
|
||
# | ||
# Local requirement installation | ||
# - On windows we can experience sharing violations if this package | ||
# is upgraded, so we exit with the pip installation | ||
# | ||
|
||
logger.info("Installing requirements in local python interpreter") | ||
|
||
pip_args = [ | ||
sys.executable, | ||
"-m", | ||
"pip", | ||
"--disable-pip-version-check", | ||
"install", | ||
] | ||
if user: | ||
pip_args.append("--user") | ||
pip_args.extend(packages) | ||
|
||
os.execv(sys.executable, pip_args) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,163 @@ | ||
import dataclasses | ||
from importlib.metadata import metadata, PackageNotFoundError | ||
import inspect | ||
import pathlib | ||
import typing | ||
|
||
from packaging.requirements import Requirement | ||
from packaging.version import Version, InvalidVersion | ||
import tomli | ||
|
||
from .errors import Error | ||
|
||
|
||
class PyprojectError(Error): | ||
pass | ||
|
||
|
||
def _pyproject_toml_path(project_path: pathlib.Path): | ||
return project_path / "pyproject.toml" | ||
|
||
|
||
@dataclasses.dataclass | ||
class RobotPyProjectToml: | ||
""" | ||
The results of parsing a ``pyproject.toml`` file in a RobotPy project. This | ||
only parses fields that matter for deploy/install (TODO: this probably | ||
should live in a single unified place?) | ||
.. code-block:: toml | ||
[tool.robotpy] | ||
# equivalent to `robotpy==2024.0.0b4` | ||
robotpy_version = "2024.0.0b4" | ||
# equivalent to `robotpy[cscore, ...]` | ||
robotpy_extras = ["cscore"] | ||
# Other pip installable requirement lines | ||
requires = [ | ||
"numpy" | ||
] | ||
""" | ||
|
||
#: Requirement for the robotpy meta package -- all RobotPy projects must | ||
#: depend on it | ||
robotpy_requires: Requirement | ||
|
||
#: Requirements for | ||
requires: typing.List[Requirement] = dataclasses.field(default_factory=list) | ||
|
||
|
||
def write_default_pyproject( | ||
project_path: pathlib.Path, | ||
): | ||
""" | ||
Using the current environment, write a minimal pyproject.toml | ||
:param project_path: Path to robot project | ||
""" | ||
|
||
# this is a bit weird because this project doesn't depend on robotpy, it's | ||
# the other way around.. but oh well? | ||
try: | ||
robotpy_version = metadata("robotpy")["Version"] | ||
except PackageNotFoundError: | ||
raise PyprojectError( | ||
"cannot infer default robotpy package version: robotpy package not installed " | ||
"(do `pip install robotpy`)" | ||
) from None | ||
|
||
with open(_pyproject_toml_path(project_path), "w") as fp: | ||
fp.write( | ||
inspect.cleandoc( | ||
f""" | ||
# | ||
# Use this configuration file to control what RobotPy packages are installed | ||
# on your RoboRIO | ||
# | ||
[tool.robotpy] | ||
# Version of robotpy this project depends on | ||
robotpy_version = "{robotpy_version}" | ||
# Which extras should be installed | ||
# -> equivalent to `pip install robotpy[extra1, ...] | ||
robotpy_extras = [] | ||
# Other pip packages to install | ||
requires = [] | ||
""" | ||
) | ||
+ "\n" | ||
) | ||
|
||
|
||
def load( | ||
project_path: pathlib.Path, *, write_if_missing: bool = False | ||
) -> RobotPyProjectToml: | ||
""" | ||
Reads a pyproject.toml file for a RobotPy project. Raises FileNotFoundError | ||
if the file isn't present | ||
:param project_path: Path to robot project | ||
""" | ||
|
||
pyproject_path = _pyproject_toml_path(project_path) | ||
if write_if_missing and not pyproject_path.exists(): | ||
write_default_pyproject(project_path) | ||
|
||
with open(pyproject_path, "rb") as fp: | ||
data = tomli.load(fp) | ||
|
||
try: | ||
robotpy_data = data["tool"]["robotpy"] | ||
if not isinstance(robotpy_data, dict): | ||
raise KeyError() | ||
except KeyError: | ||
raise PyprojectError( | ||
f"{pyproject_path} must have [tool.robotpy] section" | ||
) from None | ||
|
||
try: | ||
robotpy_version = Version(robotpy_data["robotpy_version"]) | ||
except KeyError: | ||
raise PyprojectError( | ||
f"{pyproject_path} missing required tools.robotpy.robotpy_version" | ||
) from None | ||
except InvalidVersion: | ||
raise PyprojectError( | ||
f"{pyproject_path}: tools.robotpy.robotpy_version is not a valid version" | ||
) from None | ||
|
||
robotpy_extras_any = robotpy_data.get("robotpy_extras") | ||
if isinstance(robotpy_extras_any, list): | ||
robotpy_extras = list(map(str, robotpy_extras_any)) | ||
elif not robotpy_extras_any: | ||
robotpy_extras = [] | ||
else: | ||
robotpy_extras = [str(robotpy_extras_any)] | ||
|
||
# Construct the full requirement | ||
robotpy_pkg = "robotpy" | ||
if robotpy_extras: | ||
extras_s = ",".join(robotpy_extras) | ||
robotpy_pkg = f"robotpy[{extras_s}]" | ||
robotpy_requires = Requirement(f"{robotpy_pkg}=={robotpy_version}") | ||
|
||
requires_any = robotpy_data.get("requires") | ||
if isinstance(requires_any, list): | ||
requires = [] | ||
for req in requires_any: | ||
requires.append(Requirement(req)) | ||
elif requires_any: | ||
requires = [Requirement(str(requires_any))] | ||
else: | ||
requires = [] | ||
|
||
return RobotPyProjectToml(robotpy_requires=robotpy_requires, requires=requires) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters