From 67c2e126e56562b455d45890efb3b2e4f1b543cc Mon Sep 17 00:00:00 2001 From: Dustin Spicuzza Date: Tue, 2 Jan 2024 01:09:00 -0500 Subject: [PATCH] Remove wpilib.run - Corresponding logic will be moved to robotpy-meta - __main__ will no longer be the user's robot code, but I don't think anything uses that? - Fixes #51 --- .../robotpy-halsim-ds-socket/README.md | 4 +- subprojects/robotpy-halsim-gui/README.md | 4 +- subprojects/robotpy-halsim-ws/README.md | 8 +- .../robotpy-wpilib/wpilib/_impl/logconfig.py | 65 ----- .../robotpy-wpilib/wpilib/_impl/main.py | 225 +----------------- .../robotpy-wpilib/wpilib/_impl/start.py | 96 +++++++- .../wpilib/src/rpy/Filesystem.inc | 19 +- 7 files changed, 120 insertions(+), 301 deletions(-) delete mode 100644 subprojects/robotpy-wpilib/wpilib/_impl/logconfig.py diff --git a/subprojects/robotpy-halsim-ds-socket/README.md b/subprojects/robotpy-halsim-ds-socket/README.md index 1f1b70d8..87e0e164 100644 --- a/subprojects/robotpy-halsim-ds-socket/README.md +++ b/subprojects/robotpy-halsim-ds-socket/README.md @@ -10,9 +10,9 @@ Usage First, install pyfrc. Then run your robot with the 'sim' argument and --ds-socket flag: # Windows - py -3 ./robot.py sim --ds-socket + py -3 -m robotpy sim --ds-socket # Linux/OSX - python3 robot.py sim --ds-socket + python3 -m robotpy sim --ds-socket WPILib's documentation for using the simulator can be found at http://docs.wpilib.org/en/latest/docs/software/wpilib-tools/robot-simulation/ diff --git a/subprojects/robotpy-halsim-gui/README.md b/subprojects/robotpy-halsim-gui/README.md index 154189bf..efc66da4 100644 --- a/subprojects/robotpy-halsim-gui/README.md +++ b/subprojects/robotpy-halsim-gui/README.md @@ -10,9 +10,9 @@ Usage First, install pyfrc. Then run your robot with the 'sim' argument: # Windows - py -3 ./robot.py sim + py -3 -m robotpy sim # Linux/OSX - python3 robot.py sim + python3 -m robotpy sim WPILib's documentation for using the simulator can be found at http://docs.wpilib.org/en/latest/docs/software/wpilib-tools/robot-simulation/simulation-gui.html diff --git a/subprojects/robotpy-halsim-ws/README.md b/subprojects/robotpy-halsim-ws/README.md index e1e6e621..a1d43401 100644 --- a/subprojects/robotpy-halsim-ws/README.md +++ b/subprojects/robotpy-halsim-ws/README.md @@ -9,11 +9,11 @@ Usage First, install pyfrc. Then run your robot with the 'sim' argument and --ws-server or --ws-client flag: # Windows - py -3 ./robot.py sim --ws-server - py -3 ./robot.py sim --ws-client + py -3 -m robotpy sim --ws-server + py -3 -m robotpy sim --ws-client # Linux/OSX - python3 robot.py sim --ws-server - python3 robot.py sim --ws-client + python3 -m robotpy sim --ws-server + python3 -m robotpy sim --ws-client WPILib's documentation for using the simulator can be found at http://docs.wpilib.org/en/latest/docs/software/wpilib-tools/robot-simulation/ diff --git a/subprojects/robotpy-wpilib/wpilib/_impl/logconfig.py b/subprojects/robotpy-wpilib/wpilib/_impl/logconfig.py deleted file mode 100644 index e83bc68b..00000000 --- a/subprojects/robotpy-wpilib/wpilib/_impl/logconfig.py +++ /dev/null @@ -1,65 +0,0 @@ -# novalidate - -import logging -import pprint - -# TODO: Make these configurable -log_datefmt = "%H:%M:%S" -log_format = "%(asctime)s:%(msecs)03d %(levelname)-8s: %(name)-20s: %(message)s" - - -def configure_logging(verbose): - formatter = VerboseExceptionFormatter(fmt=log_format, datefmt=log_datefmt) - - # console logging - handler = logging.StreamHandler() - handler.setFormatter(formatter) - - logging.root.addHandler(handler) - logging.root.setLevel(logging.DEBUG if verbose else logging.INFO) - - -MAX_VARS_LINES = 30 -MAX_LINE_LENGTH = 100 - - -class VerboseExceptionFormatter(logging.Formatter): - """ - Taken from http://word.bitly.com/post/69080588278/logging-locals - """ - - def __init__(self, log_locals_on_exception=True, *args, **kwargs): - self._log_locals = log_locals_on_exception - super(VerboseExceptionFormatter, self).__init__(*args, **kwargs) - - def formatException(self, exc_info): - # First get the original formatted exception. - exc_text = super(VerboseExceptionFormatter, self).formatException(exc_info) - if not self._log_locals: - return exc_text - # Now we're going to format and add the locals information. - output_lines = [exc_text, "\n"] - - # Retrieve locals from the innermost exception - exc = exc_info[1] - while exc.__cause__: - exc = exc.__cause__ - - tb = exc.__traceback__ # This is the outermost frame of the traceback. - if tb: # this should always be true, but sometimes someone messes up - while tb.tb_next: - tb = tb.tb_next # Zoom to the innermost frame. - output_lines.append("Locals at innermost frame:\n") - locals_text = pprint.pformat(tb.tb_frame.f_locals, indent=2) - locals_lines = locals_text.split("\n") - if len(locals_lines) > MAX_VARS_LINES: - locals_lines = locals_lines[:MAX_VARS_LINES] - locals_lines[-1] = "..." - output_lines.extend( - line[: MAX_LINE_LENGTH - 3] + "..." - if len(line) > MAX_LINE_LENGTH - else line - for line in locals_lines - ) - output_lines.append("\n") - return "\n".join(output_lines) diff --git a/subprojects/robotpy-wpilib/wpilib/_impl/main.py b/subprojects/robotpy-wpilib/wpilib/_impl/main.py index 0702fd8e..80de3ee2 100644 --- a/subprojects/robotpy-wpilib/wpilib/_impl/main.py +++ b/subprojects/robotpy-wpilib/wpilib/_impl/main.py @@ -1,227 +1,18 @@ -# novalidate - -import argparse import inspect -import os import sys -from os.path import exists -import importlib.metadata - -from .logconfig import configure_logging - -if sys.version_info < (3, 10): - - def entry_points(group): - eps = importlib.metadata.entry_points() - return eps.get(group) - -else: - entry_points = importlib.metadata.entry_points - - -def _log_versions(): - import wpilib - import wpilib.deployinfo - - import logging - - data = wpilib.deployinfo.getDeployData() - if data: - logger = logging.getLogger("deploy-info") - logger.info( - "%s@%s at %s", - data.get("deploy-user", ""), - data.get("deploy-host", ""), - data.get("deploy-date", ""), - ) - if "git-hash" in data: - logger.info( - "- git info: %s (branch=%s)", - data.get("git-desc", ""), - data.get("git-branch", ""), - ) - - logger = logging.getLogger("wpilib") - - try: - import robotpy.version - except ImportError: - robotpy_version = None - else: - logger.info("RobotPy version %s", robotpy.version.__version__) - - logger.info("WPILib version %s", wpilib.__version__) - - if wpilib.RobotBase.isSimulation(): - logger.info("Running with simulated HAL.") - - # check to see if we're on a RoboRIO - # NOTE: may have false positives, but it should work well enough - if exists("/etc/natinst/share/scs_imagemetadata.ini"): - logger.warning( - "Running simulation HAL on actual roboRIO! This probably isn't what you want, and will probably cause difficult-to-debug issues!" - ) - - if logger.isEnabledFor(logging.DEBUG): - versions = {} - - # Log third party versions - for group in ("robotpylib", "robotpybuild"): - for entry_point in entry_points(group=group): - # Don't actually load the entry points -- just print the - # packages unless we need to load them - dist = entry_point.dist - versions[dist.name] = dist.version - - for k, v in versions.items(): - if k != "wpilib": - logger.debug("%s version %s", k, v) - - -def _enable_faulthandler(): - # - # In the event of a segfault, faulthandler will dump the currently - # active stack so you can figure out what went wrong. - # - # Additionally, on non-Windows platforms we register a SIGUSR2 - # handler -- if you send the robot process a SIGUSR2, then - # faulthandler will dump all of your current stacks. This can - # be really useful for figuring out things like deadlocks. - # - - import logging - - logger = logging.getLogger("faulthandler") - - try: - # These should work on all platforms - import faulthandler - - faulthandler.enable() - except Exception as e: - logger.warn("Could not enable faulthandler: %s", e) - return - - try: - import signal - - faulthandler.register(signal.SIGUSR2) - logger.info("registered SIGUSR2 for PID %s", os.getpid()) - except Exception: - return - - -class _CustomHelpAction(argparse.Action): - def __init__( - self, - option_strings, - dest=argparse.SUPPRESS, - default=argparse.SUPPRESS, - help=None, - ): - super(_CustomHelpAction, self).__init__( - option_strings=option_strings, - dest=dest, - default=default, - nargs=0, - help=help, - ) - - def __call__(self, parser, namespace, values, option_string=None): - parser.print_help() - parser.exit(1) # argparse uses an exit code of zero by default - - -argparse._HelpAction = _CustomHelpAction - def run(robot_class, **kwargs): """ - This function gets called in robot.py like so:: - - if __name__ == '__main__': - wpilib.run(MyRobot) + ``wpilib.run`` is no longer used. You should run your robot code via one of + the following methods instead: - This function loads available entry points, parses arguments, and - sets things up specific to RobotPy so that the robot can run. This - function is used whether the code is running on the roboRIO or - a simulation. + * Windows: ``py -m robotpy [arguments]`` + * Linux/macOS: ``python -m robotpy [arguments]`` - :param robot_class: A class that inherits from :class:`.RobotBase` - :param kwargs: Keyword arguments that will be passed to the executed entry points - :returns: This function should never return + In a virtualenv the ``robotpy`` command can be used directly. """ - # sanity check - if not hasattr(robot_class, "main"): - print( - "ERROR: run() must be passed a robot class that inherits from RobotBase (or IterativeBase/SampleBase)" - ) - exit(1) - - parser = argparse.ArgumentParser() - subparser = parser.add_subparsers(dest="command", help="commands") - subparser.required = True - - parser.add_argument( - "-v", - "--verbose", - action="store_true", - default=False, - help="Enable debug logging", - ) - - parser.add_argument( - "--ignore-plugin-errors", - action="store_true", - default=False, - help="Ignore errors caused by RobotPy plugins (probably should fix or replace instead!)", - ) - - has_cmd = False - - for entry_point in entry_points(group="robotpy"): - try: - cmd_class = entry_point.load() - except ImportError: - if "--ignore-plugin-errors" in sys.argv: - print("WARNING: Ignoring error in '%s'" % entry_point) - continue - else: - print( - "Plugin error detected in '%s' (use --ignore-plugin-errors to ignore this)" - % entry_point - ) - raise - - cmdparser = subparser.add_parser( - entry_point.name, help=inspect.getdoc(cmd_class) - ) - obj = cmd_class(cmdparser) - cmdparser.set_defaults(cmdobj=obj) - has_cmd = True - - if not has_cmd: - parser.error( - "No entry points defined -- robot code can't do anything. Install packages to add entry points (see README)" - ) - exit(1) - - options = parser.parse_args() - - configure_logging(options.verbose) - - _log_versions() - _enable_faulthandler() - - retval = options.cmdobj.run(options, robot_class, **kwargs) - - if retval is None: - retval = 0 - elif retval is True: - retval = 0 - elif retval is False: - retval = 1 - - exit(retval) + msg = inspect.cleandoc(inspect.getdoc(run) or "`wpilib.run` is no longer used") + print(msg, file=sys.stderr) + sys.exit(1) diff --git a/subprojects/robotpy-wpilib/wpilib/_impl/start.py b/subprojects/robotpy-wpilib/wpilib/_impl/start.py index 357c4608..76ad8667 100644 --- a/subprojects/robotpy-wpilib/wpilib/_impl/start.py +++ b/subprojects/robotpy-wpilib/wpilib/_impl/start.py @@ -1,12 +1,82 @@ import hal import wpilib import logging +import os.path +import sys import threading import time +import typing + +import importlib.metadata + +if sys.version_info < (3, 10): + + def entry_points(group): + eps = importlib.metadata.entry_points() + return eps.get(group, []) + +else: + entry_points = importlib.metadata.entry_points + from .report_error import reportError, reportErrorInternal +def _log_versions(robotpy_version: typing.Optional[str]): + import wpilib + import wpilib.deployinfo + + import logging + + data = wpilib.deployinfo.getDeployData() + if data: + logger = logging.getLogger("deploy-info") + logger.info( + "%s@%s at %s", + data.get("deploy-user", ""), + data.get("deploy-host", ""), + data.get("deploy-date", ""), + ) + if "git-hash" in data: + logger.info( + "- git info: %s (branch=%s)", + data.get("git-desc", ""), + data.get("git-branch", ""), + ) + + logger = logging.getLogger("wpilib") + + if robotpy_version: + logger.info("RobotPy version %s", robotpy_version) + + logger.info("WPILib version %s", wpilib.__version__) + + if wpilib.RobotBase.isSimulation(): + logger.info("Running with simulated HAL.") + + # check to see if we're on a RoboRIO + # NOTE: may have false positives, but it should work well enough + if os.path.exists("/etc/natinst/share/scs_imagemetadata.ini"): + logger.warning( + "Running simulation HAL on actual roboRIO! This probably isn't what you want, and will probably cause difficult-to-debug issues!" + ) + + if logger.isEnabledFor(logging.DEBUG): + versions = {} + + # Log third party versions + for group in ("robotpylib", "robotpybuild"): + for entry_point in entry_points(group=group): + # Don't actually load the entry points -- just print the + # packages unless we need to load them + dist = entry_point.dist + versions[dist.name] = dist.version + + for k, v in versions.items(): + if k != "wpilib": + logger.debug("%s version %s", k, v) + + class Main: """ Executes the robot code using the currently installed HAL (this is probably not what you want unless you're on the roboRIO) @@ -24,8 +94,23 @@ def __init__(self): self.logger = logging.getLogger("robotpy") self.robot = None self.suppressExitWarning = False + self._robotpy_version = None + + @property + def robotpy_version(self) -> typing.Optional[str]: + if not self._robotpy_version: + try: + pkg = importlib.metadata.metadata("robotpy") + except importlib.metadata.PackageNotFoundError: + pass + else: + self._robotpy_version = pkg.get("Version", None) + + return self._robotpy_version def run(self, robot_cls: wpilib.RobotBase) -> bool: + _log_versions(self.robotpy_version) + retval = False if hal.hasMain(): rval = [False] @@ -52,11 +137,11 @@ def _start(): try: robot.endCompetition() except: - self.logger.warn("endCompetition raised an exception") + self.logger.warning("endCompetition raised an exception") th.join(1) if th.is_alive(): - self.logger.warn("robot thread didn't die, crash may occur next!") + self.logger.warning("robot thread didn't die, crash may occur next!") retval = rval[0] else: retval = self.start(robot_cls) @@ -146,11 +231,10 @@ def _start(self, robot_cls: wpilib.RobotBase) -> bool: # return False if not isSimulation: - try: - from robotpy.version import __version__ as robotpy_version - + robotpy_version = self.robotpy_version + if robotpy_version: version_string = f"RobotPy {robotpy_version}" - except ImportError: + else: version_string = f"robotpy-wpilib {wpilib.__version__}" try: diff --git a/subprojects/robotpy-wpilib/wpilib/src/rpy/Filesystem.inc b/subprojects/robotpy-wpilib/wpilib/src/rpy/Filesystem.inc index ae5d9ac1..72d8bfe6 100644 --- a/subprojects/robotpy-wpilib/wpilib/src/rpy/Filesystem.inc +++ b/subprojects/robotpy-wpilib/wpilib/src/rpy/Filesystem.inc @@ -10,11 +10,20 @@ static fs::path getMainPath() { py::gil_scoped_acquire gil; py::dict locals; py::exec(R"( - import sys, os.path - main = sys.modules['__main__']; - if hasattr(main, '__file__'): - main_path = os.path.abspath(os.path.dirname(main.__file__)) - + found = False + try: + from robotpy.main import robot_py_path + if robot_py_path: + main_path = str(robot_py_path.parent.absolute()) + found = True + except ImportError: + pass + + if not found: + import sys, os.path + main = sys.modules['__main__']; + if hasattr(main, '__file__'): + main_path = os.path.abspath(os.path.dirname(main.__file__)) )", py::globals(), locals);