Skip to content

Commit

Permalink
Add command to update robotpy dependency
Browse files Browse the repository at this point in the history
- Fixes #89
- 'robotpy project update'
- Also checks for updates on sync
  • Loading branch information
virtuald committed Feb 17, 2024
1 parent 2a11a2d commit baa9f83
Show file tree
Hide file tree
Showing 6 changed files with 156 additions and 10 deletions.
59 changes: 59 additions & 0 deletions robotpy_installer/cli_project.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import argparse
import json
import logging
import pathlib
import typing


from .installer import RobotpyInstaller
from . import pyproject

logger = logging.getLogger("project")


class UpdateRobotpy:
"""
Update the version of RobotPy your project depends on
"""

def __init__(self, parser: argparse.ArgumentParser) -> None:
parser.add_argument(
"--use-certifi",
action="store_true",
default=False,
help="Use SSL certificates from certifi",
)

def run(self, project_path: pathlib.Path, use_certifi: bool) -> bool:
try:
project = pyproject.load(project_path)
except FileNotFoundError:
logger.error("Could not load pyproject.toml")
return False

print("Project robotpy version is", project.robotpy_version)

installer = RobotpyInstaller(log_startup=False)

# Determine what the latest version is
v = installer.get_pypi_version("robotpy", use_certifi)
print("Latest version of robotpy is", v)

if project.robotpy_version > v:
print("ERROR: refusing to update pyproject.toml!")
return False

# Update it in pyproject.toml
pyproject.set_robotpy_version(project_path, v)

return True


class Project:
"""
Manage your robot project
"""

subcommands = [
("update-robotpy", UpdateRobotpy),
]
21 changes: 21 additions & 0 deletions robotpy_installer/cli_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,12 +67,20 @@ def __init__(self, parser: argparse.ArgumentParser):
help="Do not install any packages",
)

parser.add_argument(
"--no-upgrade-project",
action="store_true",
default=False,
help="Do not check to see if the project can be upgraded",
)

@handle_cli_error
def run(
self,
project_path: pathlib.Path,
main_file: pathlib.Path,
no_install: bool,
no_upgrade_project: bool,
user: bool,
use_certifi: bool,
):
Expand All @@ -87,6 +95,19 @@ def run(

# parse pyproject.toml to determine the requirements
project = pyproject.load(project_path, write_if_missing=True)
logger.info(
"RobotPy version in `pyproject.toml` is '%s'", project.robotpy_version
)

# Check for upgrade
if not no_upgrade_project:
latest_robotpy_version = installer.get_pypi_version("robotpy", use_certifi)
logger.info("Latest version of RobotPy is '%s'", latest_robotpy_version)
if project.robotpy_version < latest_robotpy_version:
msg = f"Update robotpy_version in `pyproject.toml` to {latest_robotpy_version}?"
if yesno(msg):
pyproject.set_robotpy_version(project_path, latest_robotpy_version)
project.robotpy_version = latest_robotpy_version

# Get the local version and don't accidentally downgrade them
try:
Expand Down
29 changes: 29 additions & 0 deletions robotpy_installer/installer.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import contextlib
import inspect
import io
import json
import logging
import pathlib
import re
Expand All @@ -12,6 +13,8 @@

from os.path import basename, exists

from packaging.version import Version

from .version import version as __version__
from . import roborio_utils
from .cacheserver import CacheServer
Expand Down Expand Up @@ -632,6 +635,32 @@ def pip_uninstall(
with catch_ssh_error("uninstalling packages"):
self.ssh.exec_cmd(shlex.join(pip_args), check=True, print_output=True)

def get_pypi_version(self, package: str, use_certifi: bool) -> Version:
"""
Retrieves the latest version of a package on pypi that corresponds to the current year
"""
fname = self.cache_root / f"pypi-{package}.json"
_urlretrieve(
f"https://pypi.org/simple/{package}",
fname,
True,
_make_ssl_context(use_certifi),
False,
{"Accept": "application/vnd.pypi.simple.v1+json"},
)
with open(fname, "r") as fp:
data = json.load(fp)

versions = [Version(v) for v in data["versions"]]

# Sort the versions
maxv = Version(str(int(_WPILIB_YEAR) + 1))
versions = sorted(v for v in versions if v < maxv)
if not versions:
raise InstallerException(f"could not find {package} version on pypi")

return versions[-1]


def _make_ssl_context(use_certifi: bool):
if not use_certifi:
Expand Down
17 changes: 17 additions & 0 deletions robotpy_installer/pyproject.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from packaging.requirements import Requirement
from packaging.version import Version, InvalidVersion
import tomli
import tomlkit

from . import pypackages
from .pypackages import Packages, Env
Expand Down Expand Up @@ -245,3 +246,19 @@ def _load(
robotpy_extras=robotpy_extras,
requires=requires,
)


def set_robotpy_version(project_path: pathlib.Path, version: Version):
pyproject_path = toml_path(project_path)
with open(pyproject_path) as fp:
data = tomlkit.parse(fp.read())

try:
data["tool"]["robotpy"]["robotpy_version"] = str(version) # type: ignore
except Exception as e:
raise ValueError("`pyproject.toml` is not valid") from e

rawdata = tomlkit.dumps(data)

with open(pyproject_path, "w") as fp:
fp.write(rawdata)
38 changes: 28 additions & 10 deletions robotpy_installer/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import pathlib
import socket
import sys
import typing
import urllib.request

from robotpy_installer import __version__
Expand All @@ -26,9 +27,17 @@ def md5sum(fname):
return md5.hexdigest()


def _urlretrieve(url, fname: pathlib.Path, cache: bool, ssl_context):
# Get it
print("Downloading", url)
def _urlretrieve(
url,
fname: pathlib.Path,
cache: bool,
ssl_context,
show_status: bool = True,
reqheaders: typing.Optional[typing.Dict[str, str]] = None,
):
if show_status:
# Get it
print("Downloading", url)

# Save bandwidth! Use stored metadata to prevent re-downloading
# stuff we already have
Expand Down Expand Up @@ -59,14 +68,19 @@ def _reporthook(read, totalsize):
sys.stdout.flush()

try:
if reqheaders:
reqheaders = reqheaders.copy()
else:
reqheaders = {}

# adapted from urlretrieve source
headers = {"User-Agent": _useragent}
reqheaders["User-Agent"] = _useragent
if last_modified:
headers["If-Modified-Since"] = last_modified
reqheaders["If-Modified-Since"] = last_modified
if etag:
headers["If-None-Match"] = etag
reqheaders["If-None-Match"] = etag

req = urllib.request.Request(url, headers=headers)
req = urllib.request.Request(url, headers=reqheaders)

with contextlib.closing(
urllib.request.urlopen(req, context=ssl_context)
Expand All @@ -86,7 +100,9 @@ def _reporthook(read, totalsize):
break
read += len(block)
dfp.write(block)
_reporthook(read, size)

if show_status:
_reporthook(read, size)

if size >= 0 and read < size:
raise ValueError("Only retrieved %s of %s bytes" % (read, size))
Expand All @@ -104,7 +120,8 @@ def _reporthook(read, totalsize):
json.dump(md, fp)
except urllib.error.HTTPError as e:
if e.code == 304:
sys.stdout.write("Not modified")
if show_status:
sys.stdout.write("Not modified")
else:
raise
except Exception as e:
Expand All @@ -117,7 +134,8 @@ def _reporthook(read, totalsize):
raise Exception(msg) from e
else:
raise e
sys.stdout.write("\n")
if show_status:
sys.stdout.write("\n")


def _resolve_addr(hostname):
Expand Down
2 changes: 2 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ install_requires =
pynetconsole~=2.0.2
robotpy-cli~=2024.0
tomli
tomlkit
setup_requires =
setuptools_scm > 6
python_requires = >=3.8
Expand All @@ -41,5 +42,6 @@ robotpy =
deploy-info = robotpy_installer.cli_deploy_info:DeployInfo
init = robotpy_installer.cli_init:Init
installer = robotpy_installer.cli_installer:Installer
project = robotpy_installer.cli_project:Project
sync = robotpy_installer.cli_sync:Sync
undeploy = robotpy_installer.cli_undeploy:Undeploy

0 comments on commit baa9f83

Please sign in to comment.