diff --git a/.gitignore b/.gitignore index d2f50d20..0bd1f211 100644 --- a/.gitignore +++ b/.gitignore @@ -12,7 +12,8 @@ /build # python stuff -__pycache__ +__pycache__/ +venv/ *.egg-info # misc diff --git a/external_samples/__init__.py b/external_samples/__init__.py new file mode 100644 index 00000000..3581552f --- /dev/null +++ b/external_samples/__init__.py @@ -0,0 +1 @@ +# external samples diff --git a/external_samples/spark_mini.py b/external_samples/spark_mini.py index 8922f29f..610dcc9c 100644 --- a/external_samples/spark_mini.py +++ b/external_samples/spark_mini.py @@ -31,7 +31,8 @@ def __init__(self, ports : list[tuple[PortType, int]]): portType, port = ports[0] if portType != PortType.SMART_MOTOR_PORT: raise InvalidPortException - self.spark_mini = wpilib.SparkMini(port) + # TODO(lizlooney): When we upgrade to 2027 robotpy, change PWMSparkMax to SparkMini. + self.spark_mini = wpilib.PWMSparkMax(port) # wpilib.SparkMini(port) def get_manufacturer(self) -> str: return "REV Robotics" diff --git a/python_tools/README.md b/python_tools/README.md new file mode 100644 index 00000000..f7636004 --- /dev/null +++ b/python_tools/README.md @@ -0,0 +1,19 @@ +# Python Tools + +## To generate JSON for the robotpy modules and classes: + +The following instructions work on macOS Sonoma 14.6.1. + +### Setup + 1. cd /python_tools + 1. python3.12 -m venv ./venv + 1. source ./venv/bin/activate + 1. python3.12 -m pip install -r requirements.txt + 1. deactivate + +### To Regenerate robotpy_data.json + 1. cd /python_tools + 1. python3.12 -m venv ./venv + 1. source ./venv/bin/activate + 1. python3.12 generate_json.py --output_directory=../src/blocks/utils + 1. deactivate diff --git a/python_tools/generate_json.py b/python_tools/generate_json.py new file mode 100644 index 00000000..bde00211 --- /dev/null +++ b/python_tools/generate_json.py @@ -0,0 +1,145 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__author__ = "lizlooney@google.com (Liz Looney)" + +# Python Standard Library +import pathlib +import sys + +# absl +from absl import app +from absl import flags +from absl import logging + +# robotpy +import hal +import hal.simulation +import ntcore +import pyfrc +import wpilib +import wpilib.counter +import wpilib.drive +import wpilib.event +import wpilib.interfaces +import wpilib.shuffleboard +import wpilib.simulation +import wpimath +import wpimath.controller +import wpimath.estimator +import wpimath.filter +import wpimath.geometry +import wpimath.interpolation +import wpimath.kinematics +import wpimath.optimization +import wpimath.path +import wpimath.spline +import wpimath.system +import wpimath.system.plant +import wpimath.trajectory +import wpimath.trajectory.constraint +import wpimath.units +import wpinet +import wpiutil + +# Server python scripts +sys.path.append("../server_python_scripts") +import blocks_base_classes + +# External samples +sys.path.append("../external_samples") +import color_range_sensor +import component +import rev_touch_sensor +import servo +import smart_motor +import spark_mini +import sparkfun_led_stick + +# Local modules +import json_util +import python_util + + +FLAGS = flags.FLAGS + +flags.DEFINE_string('output_directory', None, 'The directory where output should be written.') + + +def main(argv): + del argv # Unused. + + if not FLAGS.output_directory: + logging.error(f'You must specify the --output_directory argument') + return + + pathlib.Path(f'{FLAGS.output_directory}/generated/').mkdir(parents=True, exist_ok=True) + + robotpy_modules = [ + hal, + hal.simulation, + ntcore, + wpilib, + wpilib.counter, + wpilib.drive, + wpilib.event, + wpilib.interfaces, + wpilib.shuffleboard, + wpilib.simulation, + python_util.getModule('wpilib.sysid'), + wpimath, + wpimath.controller, + wpimath.estimator, + wpimath.filter, + wpimath.geometry, + wpimath.interpolation, + wpimath.kinematics, + wpimath.optimization, + wpimath.path, + wpimath.spline, + wpimath.system, + wpimath.system.plant, + wpimath.trajectory, + wpimath.trajectory.constraint, + wpimath.units, + wpinet, + wpiutil, + ] + json_generator = json_util.JsonGenerator(robotpy_modules) + file_path = f'{FLAGS.output_directory}/generated/robotpy_data.json' + json_generator.writeJsonFile(file_path) + + server_python_scripts = [ + blocks_base_classes, + ] + json_generator = json_util.JsonGenerator(server_python_scripts) + file_path = f'{FLAGS.output_directory}/generated/server_python_scripts.json' + json_generator.writeJsonFile(file_path) + + external_samples_modules = [ + color_range_sensor, + component, + rev_touch_sensor, + servo, + smart_motor, + spark_mini, + sparkfun_led_stick, + ] + json_generator = json_util.JsonGenerator(external_samples_modules) + file_path = f'{FLAGS.output_directory}/generated/external_samples_data.json' + json_generator.writeJsonFile(file_path) + + +if __name__ == '__main__': + app.run(main) diff --git a/python_tools/json_util.py b/python_tools/json_util.py new file mode 100644 index 00000000..129279a5 --- /dev/null +++ b/python_tools/json_util.py @@ -0,0 +1,513 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__author__ = "lizlooney@google.com (Liz Looney)" + +# Python Standard Library +import inspect +import json +import sys +import types +import typing + +# Local modules +import python_util + + +_KEY_MODULES = 'modules' +_KEY_CLASSES = 'classes' +_KEY_MODULE_NAME = 'moduleName' +_KEY_MODULE_VARIABLES = 'moduleVariables' +_KEY_TOOLTIP = 'tooltip' +_KEY_CLASS_NAME = 'className' +_KEY_CLASS_VARIABLES = 'classVariables' +_KEY_INSTANCE_VARIABLES = 'instanceVariables' +_KEY_ENUMS = 'enums' +_KEY_ENUM_CLASS_NAME = 'enumClassName' +_KEY_ENUM_VALUES = 'enumValues' +_KEY_VARIABLE_NAME = 'name' +_KEY_VARIABLE_TYPE = 'type' +_KEY_VARIABLE_WRITABLE = 'writable' +_KEY_FUNCTIONS = 'functions' +_KEY_CONSTRUCTORS = 'constructors' +_KEY_INSTANCE_METHODS = 'instanceMethods' +_KEY_STATIC_METHODS = 'staticMethods' +_KEY_FUNCTION_NAME = 'functionName' +_KEY_FUNCTION_RETURN_TYPE = 'returnType' +_KEY_FUNCTION_ARGS = 'args' +_KEY_FUNCTION_DECLARING_CLASS_NAME = 'declaringClassName' +_KEY_ARGUMENT_NAME = 'name' +_KEY_ARGUMENT_TYPE = 'type' +_KEY_ARGUMENT_DEFAULT_VALUE = 'defaultValue' +_KEY_ALIASES = 'aliases' +_KEY_SUBCLASSES = 'subclasses' + + +_DICT_FULL_MODULE_NAME_TO_MODULE_NAME = { + 'hal._wpiHal': 'hal', + 'hal.simulation._simulation': 'hal.simulation', + + 'ntcore._ntcore': 'ntcore', + 'ntcore._ntcore.meta': 'ntcore.meta', + + 'wpilib._wpilib': 'wpilib', + 'wpilib._wpilib.sysid': 'wpilib.sysid', + 'wpilib.counter._counter': 'wpilib.counter', + 'wpilib.drive._drive': 'wpilib.drive', + 'wpilib.event._event': 'wpilib.event', + 'wpilib.interfaces._interfaces': 'wpilib.interfaces', + 'wpilib.shuffleboard._shuffleboard': 'wpilib.shuffleboard', + 'wpilib.simulation._simulation': 'wpilib.simulation', + + 'wpimath._controls._controls.constraint': 'wpimath.trajectory.constraint', + 'wpimath._controls._controls.controller': 'wpimath.controller', + 'wpimath._controls._controls.estimator': 'wpimath.estimator', + 'wpimath._controls._controls.optimization': 'wpimath.optimization', + 'wpimath._controls._controls.path': 'wpimath.path', + 'wpimath._controls._controls.plant': 'wpimath.system.plant', + 'wpimath._controls._controls.system': 'wpimath.system', + 'wpimath._controls._controls.trajectory': 'wpimath.trajectory', + 'wpimath.filter._filter': 'wpimath.filter', + 'wpimath.geometry._geometry': 'wpimath.geometry', + 'wpimath.interpolation._interpolation': 'wpimath.interpolation', + 'wpimath.kinematics._kinematics': 'wpimath.kinematics', + 'wpimath.spline._spline': 'wpimath.spline', + + 'wpinet._wpinet': 'wpinet', + + 'wpiutil._wpiutil': 'wpiutil', + 'wpiutil._wpiutil.log': 'wpiutil.log', + 'wpiutil._wpiutil.sync': 'wpiutil.sync', + 'wpiutil._wpiutil.wpistruct': 'wpiutil.wpistruct', + } + + +def getModuleName(m) -> str: + if inspect.ismodule(m): + module_name = m.__name__ + elif isinstance(m, str): + module_name = m + else: + raise Exception(f'Argument m must be a module or a module name.') + return _DICT_FULL_MODULE_NAME_TO_MODULE_NAME.get(module_name, module_name) + + +def getClassName(c, containing_class_name: str = None) -> str: + if inspect.isclass(c): + full_class_name = python_util.getFullClassName(c) + elif isinstance(c, str): + if c == 'typing.Self' and containing_class_name: + full_class_name = containing_class_name + else: + full_class_name = c + else: + raise Exception(f'Argument c must be a class or a class name.') + for full_module_name, module_name in _DICT_FULL_MODULE_NAME_TO_MODULE_NAME.items(): + full_class_name = full_class_name.replace(full_module_name + '.', module_name + '.') + return full_class_name + + +class JsonGenerator: + def __init__(self, root_modules: list[types.ModuleType]): + self._root_modules = root_modules + (self._packages, self._modules, self._classes, self._dict_full_class_name_to_alias) = python_util.collectModulesAndClasses(self._root_modules) + self._dict_full_class_name_to_subclass_names = python_util.collectSubclasses(self._classes) + + def _getPublicModules(self) -> list[types.ModuleType]: + public_modules = [] + for m in self._modules: + if '._' in python_util.getFullModuleName(m): + continue + public_modules.append(m) + public_modules.sort(key=lambda m: python_util.getFullModuleName(m)) + return public_modules + + + def _createFunctionIsEnumValue( + self, enum_cls: type) -> typing.Callable[[object], bool]: + return lambda value: type(value) == enum_cls + + + def _processModule(self, module) -> dict: + module_data = {} + module_name = getModuleName(module) + module_data[_KEY_MODULE_NAME] = module_name + + # Module variables. + module_variables = [] + for key, value in inspect.getmembers(module, python_util.isNothing): + if not python_util.isModuleVariableReadable(module, key, value): + continue + var_data = {} + var_data[_KEY_VARIABLE_NAME] = key + var_data[_KEY_VARIABLE_TYPE] = getClassName(type(value)) + var_data[_KEY_VARIABLE_WRITABLE] = python_util.isModuleVariableWritable(module, key, value) + var_data[_KEY_TOOLTIP] = '' + module_variables.append(var_data) + module_data[_KEY_MODULE_VARIABLES] = sorted(module_variables, key=lambda var_data: var_data[_KEY_VARIABLE_NAME]) + + # Module functions. + functions = [] + for key, value in inspect.getmembers(module, inspect.isroutine): + if not python_util.isFunction(module, key, value): + continue + # Check whether value is a function imported from another module. + if value.__module__: + declaring_module = python_util.getModule(value.__module__) + if python_util.isBuiltInModule(declaring_module): + # Ignore the imported function from a built-in module. + continue; + if getModuleName(declaring_module) != getModuleName(module): + # Check whether the imported function is exported in __all__. + if not (key in module.__all__): + # Ignore the imported function. + continue + + # Look at each function signature. For overloaded functions, there will be more than one. + (signatures, comments) = python_util.processFunction(value) + if len(signatures) == 0: + print(f'ERROR: failed to determine function signature for {module_name}.{key}', + file=sys.stderr) + continue + for iSignature in range(len(signatures)): + signature = signatures[iSignature] + if '**kwargs' in signature: + continue + # Determine the argument names and types. + try: + (function_name, arg_names, arg_types, arg_default_values, return_type) = python_util.processSignature(signature) + except: + print(f'ERROR: function signature for {module_name}.{key} is not parseable. "{signature}"', + file=sys.stderr) + continue + if function_name != key: + print(f'ERROR: signature has different function name. {module_name}.{key}', + file=sys.stderr) + continue + args = [] + for i in range(len(arg_names)): + arg_data = {} + arg_data[_KEY_ARGUMENT_NAME] = arg_names[i] + arg_data[_KEY_ARGUMENT_TYPE] = getClassName(arg_types[i]) + if arg_default_values[i] is not None: + arg_data[_KEY_ARGUMENT_DEFAULT_VALUE] = arg_default_values[i] + else: + arg_data[_KEY_ARGUMENT_DEFAULT_VALUE] = '' + args.append(arg_data) + function_data = {} + function_data[_KEY_FUNCTION_NAME] = function_name + function_data[_KEY_FUNCTION_RETURN_TYPE] = getClassName(return_type) + function_data[_KEY_FUNCTION_ARGS] = args + if comments[iSignature] is not None: + function_data[_KEY_TOOLTIP] = comments[iSignature] + else: + function_data[_KEY_TOOLTIP] = '' + functions.append(function_data) + module_data[_KEY_FUNCTIONS] = sorted(functions, key=lambda function_data: function_data[_KEY_FUNCTION_NAME]) + + # Enums + enums = [] + for key, value in inspect.getmembers(module, python_util.isEnum): + enum_class_name = getClassName(value) + if getModuleName(value.__module__) != module_name: + continue + fnIsEnumValue = self._createFunctionIsEnumValue(value) + enum_values = [] + enum_tooltip = '' + for keyEnum, valueEnum in inspect.getmembers(value, fnIsEnumValue): + enum_values.append(keyEnum) + if not enum_tooltip: + enum_tooltip = value.__doc__ + enum_values.sort() + enum_data = {} + enum_data[_KEY_ENUM_CLASS_NAME] = enum_class_name + enum_data[_KEY_MODULE_NAME] = getModuleName(value.__module__) + enum_data[_KEY_ENUM_VALUES] = enum_values + if enum_tooltip is not None: + enum_data[_KEY_TOOLTIP] = enum_tooltip + else: + enum_data[_KEY_TOOLTIP] = '' + enums.append(enum_data) + module_data[_KEY_ENUMS] = sorted(enums, key=lambda enum_data: enum_data[_KEY_ENUM_CLASS_NAME]) + return module_data + + + def _processModules(self): + module_data_list = [] + set_of_modules = set() + for module in self._getPublicModules(): + set_of_modules.add(module) + for module in set_of_modules: + module_name = getModuleName(module) + if module_name.startswith('ntcore'): + continue + if module_name.startswith('wpinet'): + continue + if module_name.startswith('wpiutil'): + continue + #if not hasattr(module, '__all__'): + # print(f'Skipping module {getModuleName(module)}') + # continue + module_data = self._processModule(module) + module_data_list.append(module_data) + return sorted(module_data_list, key=lambda module_data: module_data[_KEY_MODULE_NAME]) + + + def _getPublicClasses(self) -> list[type]: + public_classes = [] + for c in self._classes: + class_name = getClassName(c) + if '._' in class_name: + continue + public_classes.append(c) + public_classes.sort(key=lambda c: python_util.getFullClassName(c)) + return public_classes + + + def _processClass(self, cls): + class_data = {} + class_name = getClassName(cls) + full_class_name = python_util.getFullClassName(cls) + class_data[_KEY_CLASS_NAME] = class_name + class_data[_KEY_MODULE_NAME] = getModuleName(cls.__module__) + + # Class variables. + class_variables = [] + for key, value in inspect.getmembers(cls, python_util.isNothing): + if not python_util.isClassVariableReadable(cls, key, value): + continue + var_data = {} + var_data[_KEY_VARIABLE_NAME] = key + var_data[_KEY_VARIABLE_TYPE] = getClassName(type(value), class_name) + var_data[_KEY_VARIABLE_WRITABLE] = python_util.isClassVariableWritable(cls, key, value) + var_data[_KEY_TOOLTIP] = '' + class_variables.append(var_data) + class_data[_KEY_CLASS_VARIABLES] = sorted(class_variables, key=lambda var_data: var_data[_KEY_VARIABLE_NAME]) + + # Instance variables + instance_variables = [] + for key, value in inspect.getmembers(cls, inspect.isdatadescriptor): + if not python_util.isInstanceVariableReadable(cls, key, value): + continue + var_type = python_util.getVarTypeFromGetter(value.fget) + var_data = {} + var_data[_KEY_VARIABLE_NAME] = key + var_data[_KEY_VARIABLE_TYPE] = getClassName(var_type, class_name) + var_data[_KEY_VARIABLE_WRITABLE] = python_util.isInstanceVariableWritable(cls, key, value) + if value.__doc__ is not None: + var_data[_KEY_TOOLTIP] = value.__doc__ + else: + var_data[_KEY_TOOLTIP] = '' + instance_variables.append(var_data) + class_data[_KEY_INSTANCE_VARIABLES] = sorted(instance_variables, key=lambda var_data: var_data[_KEY_VARIABLE_NAME]) + + # Constructors + constructors = [] + for key, value in inspect.getmembers(cls):#, python_util.mightBeConstructor): + if not python_util.isConstructor(cls, key, value): + continue + # Look at each function signature. For overloaded functions, there will be more than one. + (signatures, comments) = python_util.processFunction(value, cls) + if len(signatures) == 0: + print(f'ERROR: failed to determine function signature for {class_name}.{key}', + file=sys.stderr) + continue + for iSignature in range(len(signatures)): + signature = signatures[iSignature] + # Determine the argument names and types. + try: + (function_name, arg_names, arg_types, arg_default_values, return_type) = python_util.processSignature(signature) + except: + print(f'ERROR: function signature for {class_name}.{key} is not parseable. "{signature}"', + file=sys.stderr) + continue + if function_name != key: + print(f'ERROR: signature has different function name. {class_name}.{key}', + file=sys.stderr) + continue + declaring_class_name = class_name + constructor_data = {} + constructor_data[_KEY_FUNCTION_NAME] = function_name + if comments[iSignature] is not None: + constructor_data[_KEY_TOOLTIP] = comments[iSignature] + else: + constructor_data[_KEY_TOOLTIP] = '' + args = [] + for i in range(len(arg_names)): + arg_name = arg_names[i] + arg_type = arg_types[i] + if i == 0 and arg_name == 'self': + if arg_type != full_class_name: + declaring_class_name = getClassName(arg_type, class_name) + # Don't append the self argument to the args array. + continue; + arg_data = {} + arg_data[_KEY_ARGUMENT_NAME] = arg_name + arg_data[_KEY_ARGUMENT_TYPE] = getClassName(arg_type, class_name) + if arg_default_values[i] is not None: + arg_data[_KEY_ARGUMENT_DEFAULT_VALUE] = arg_default_values[i] + else: + arg_data[_KEY_ARGUMENT_DEFAULT_VALUE] = '' + args.append(arg_data) + constructor_data[_KEY_FUNCTION_ARGS] = args + constructor_data[_KEY_FUNCTION_DECLARING_CLASS_NAME] = declaring_class_name + constructor_data[_KEY_FUNCTION_RETURN_TYPE] = declaring_class_name + constructors.append(constructor_data) + class_data[_KEY_CONSTRUCTORS] = constructors + + # Functions + instance_methods = [] + static_methods = [] + for key, value in inspect.getmembers(cls, inspect.isroutine): + if not python_util.isFunction(cls, key, value): + continue + # Look at each function signature. For overloaded functions, there will be more than one. + (signatures, comments) = python_util.processFunction(value, cls) + if len(signatures) == 0: + print(f'ERROR: failed to determine function signature for {class_name}.{key}', + file=sys.stderr) + continue + for iSignature in range(len(signatures)): + signature = signatures[iSignature] + # Determine the argument names and types. + try: + (function_name, arg_names, arg_types, arg_default_values, return_type) = python_util.processSignature(signature) + except: + print(f'ERROR: function signature for {class_name}.{key} is not parseable. "{signature}"', + file=sys.stderr) + continue + if function_name != key: + print(f'ERROR: signature has different function name. {class_name}.{key}', + file=sys.stderr) + continue + declaring_class_name = class_name + args = [] + found_self_arg = False + for i in range(len(arg_names)): + arg_name = arg_names[i] + arg_type = arg_types[i] + if i == 0 and arg_name == 'self': + found_self_arg = True + if arg_type != full_class_name: + declaring_class_name = getClassName(arg_type, class_name) + arg_data = {} + arg_data[_KEY_ARGUMENT_NAME] = arg_name + arg_data[_KEY_ARGUMENT_TYPE] = getClassName(arg_type, class_name) + if arg_default_values[i] is not None: + arg_data[_KEY_ARGUMENT_DEFAULT_VALUE] = arg_default_values[i] + else: + arg_data[_KEY_ARGUMENT_DEFAULT_VALUE] = '' + args.append(arg_data) + function_data = {} + function_data[_KEY_FUNCTION_NAME] = function_name + function_data[_KEY_FUNCTION_RETURN_TYPE] = getClassName(return_type, class_name) + function_data[_KEY_FUNCTION_ARGS] = args + function_data[_KEY_FUNCTION_DECLARING_CLASS_NAME] = declaring_class_name + if comments[iSignature] is not None: + function_data[_KEY_TOOLTIP] = comments[iSignature] + else: + function_data[_KEY_TOOLTIP] = '' + if found_self_arg: + instance_methods.append(function_data) + else: + static_methods.append(function_data) + class_data[_KEY_INSTANCE_METHODS] = sorted(instance_methods, key=lambda function_data: function_data[_KEY_FUNCTION_NAME]) + class_data[_KEY_STATIC_METHODS] = sorted(static_methods, key=lambda function_data: function_data[_KEY_FUNCTION_NAME]) + + # Enums + enums = [] + for key, value in inspect.getmembers(cls, python_util.isEnum): + if not getClassName(value).startswith(class_name): + continue + enum_class_name = getClassName(value) + fnIsEnumValue = self._createFunctionIsEnumValue(value) + enum_values = [] + enum_tooltip = '' + for keyEnum, valueEnum in inspect.getmembers(value, fnIsEnumValue): + enum_values.append(keyEnum) + if not enum_tooltip: + enum_tooltip = value.__doc__ + enum_values.sort() + enum_data = {} + enum_data[_KEY_ENUM_CLASS_NAME] = enum_class_name + enum_data[_KEY_MODULE_NAME] = getModuleName(value.__module__) + enum_data[_KEY_ENUM_VALUES] = enum_values + if enum_tooltip is not None: + enum_data[_KEY_TOOLTIP] = enum_tooltip + else: + enum_data[_KEY_TOOLTIP] = '' + enums.append(enum_data) + class_data[_KEY_ENUMS] = sorted(enums, key=lambda enum_data: enum_data[_KEY_ENUM_CLASS_NAME]) + return class_data + + + def _processClasses(self): + class_data_list = [] + set_of_classes = set() + for cls in self._getPublicClasses(): + for c in inspect.getmro(cls): + if python_util.isBuiltInClass(c): + break + set_of_classes.add(c) + for cls in set_of_classes: + if python_util.isEnum(cls): + continue + module_name = getModuleName(cls.__module__) + if module_name.startswith('ntcore'): + continue + if module_name.startswith('wpinet'): + continue + if module_name.startswith('wpiutil'): + continue + #module = python_util.getModule(module_name) + #if not hasattr(module, '__all__'): + # print(f'Skipping class {getClassName(cls)} because module {module_name} has no __all__') + # continue + #elif cls.__name__ not in module.__all__: + # print(f'Skipping class {getClassName(cls)} because module {module_name}.__all__ does not include {cls.__name__}') + # continue + class_data = self._processClass(cls) + class_data_list.append(class_data) + return sorted(class_data_list, key=lambda class_data: class_data[_KEY_CLASS_NAME]) + + + def _processAliases(self): + aliases = {} + for full_class_name, alias in self._dict_full_class_name_to_alias.items(): + aliases[getClassName(full_class_name)] = getClassName(alias) + return aliases + + + def _processSubclasses(self): + subclasses = {} + for full_class_name, full_subclass_names in self._dict_full_class_name_to_subclass_names.items(): + list = [] + for full_subclass_name in full_subclass_names: + list.append(getClassName(full_subclass_name)) + subclasses[getClassName(full_class_name)] = list + return subclasses + + def _getJsonData(self): + json_data = {} + json_data[_KEY_MODULES] = self._processModules() + json_data[_KEY_CLASSES] = self._processClasses() + json_data[_KEY_ALIASES] = self._processAliases() + json_data[_KEY_SUBCLASSES] = self._processSubclasses() + return json_data + + def writeJsonFile(self, file_path: str): + json_data = self._getJsonData() + json_file = open(file_path, 'w', encoding='utf-8') + json.dump(json_data, json_file, sort_keys=True, indent=4) + json_file.close() diff --git a/python_tools/python_util.py b/python_tools/python_util.py new file mode 100644 index 00000000..26565b08 --- /dev/null +++ b/python_tools/python_util.py @@ -0,0 +1,552 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +__author__ = "lizlooney@google.com (Liz Looney)" + +# Python Standard Library +from enum import Enum +import importlib +import inspect +import logging +import re +import sys +import types +import typing + + + +def getModule(module_name: str) -> types.ModuleType: + return importlib.import_module(module_name) + + +def getFullModuleName(module: types.ModuleType) -> str: + return module.__name__ + + +def getClass(full_class_name: str) -> type: + parts = full_class_name.split(".") + for i in range(len(parts)): + if i == 0: + object = getModule(parts[i]) + else: + object = getattr(object, parts[i]) + if not inspect.isclass(object): + raise Exception(f"Failed to find the class for {full_class_name}") + return object + + +def getFullClassName(cls: type) -> str: + match = re.fullmatch(r"\", str(cls)) + if match: + return match.group(1) + # The following doesn't work for nested classes. + return f"{cls.__module__}.{cls.__name__}" + + +def _isSignature(line: str) -> bool: + match = re.fullmatch(r"(\w+)\((.*)\) \-\> (.+)", line) + return True if match else False + + +def _findEndOfToken(text: str, i: int, delimiters: list[str]): + stack = [] + while i < len(text): + ch = text[i] + if ch == "(": + stack.append(")") + elif ch == "[": + stack.append("]") + else: + if len(stack) > 0: + # We have some parentheses or brackets that we need to find. + if ch == stack[-1]: + stack.pop() + else: + if ch in delimiters: + break + i += 1 + return i + + +def processSignature(signature_line: str) -> tuple[str, list[str], list[str], list[str], str]: + match = re.fullmatch(r"(\w+)\((.*)\) \-\> (.+)", signature_line) + if not match: + raise Exception(f"Failed to parse signature line {signature_line}") + function_name = match.group(1) + args = match.group(2) + return_type = match.group(3) + arg_names = [] + arg_types = [] + arg_default_values = [] + i = 0 + while i < len(args): + # Get the argument name. + iStartOfArgName = i + # Look for ": ", which is right after the argument name. + i = args.find(": ", iStartOfArgName) + if i == -1: + star_star_kwargs_ = "**kwargs" + star_args = "*args" + if args[iStartOfArgName:] == "*args, **kwargs": + arg_names.append("args") + arg_types.append("tuple") + arg_default_values.append(None) + arg_names.append("kwargs") + arg_types.append("dict") + arg_default_values.append(None) + break + elif args[iStartOfArgName:] == "*args": + arg_names.append("args") + arg_types.append("tuple") + arg_default_values.append(None) + break + elif args[iStartOfArgName:] == "**kwargs": + arg_names.append("kwargs") + arg_types.append("dict") + arg_default_values.append(None) + break + else: + raise Exception(f"Failed to parse signature line {signature_line}") + arg_name = args[iStartOfArgName:i] + arg_names.append(arg_name) + # Get the argument type. + iStartOfArgType = i + 2 # Skip over ": " + i = _findEndOfToken(args, iStartOfArgType, [",", " "]) + arg_type = args[iStartOfArgType:i] + arg_types.append(arg_type) + if i + 2 < len(args) and args[i:i + 3] == " = ": + # Get the default value. + iStartOfDefaultValue = i + 3 # Skip over the space + i = _findEndOfToken(args, iStartOfDefaultValue, [","]) + arg_default_value = args[iStartOfDefaultValue:i] + arg_default_values.append(arg_default_value) + else: + arg_default_values.append(None) + if i + 1 < len(args) and args[i:i + 2] == ", ": + i += 2 # Skip over ", " + return (function_name, arg_names, arg_types, arg_default_values, return_type) + + +def ignoreMember(parent, key: str, member): + """ + if inspect.ismodule(parent): + if hasattr(parent, '__all__'): + if key not in parent.__all__: + return True + else: + return True + """ + + if inspect.ismodule(member): + # Member is a module. + if not inspect.ismodule(parent): + return True + if not member.__name__.startswith(parent.__name__): + return True + if (key == "_impl" or key == "deployinfo" or key == "version"): + return True + return False + + # Member is not a module. + if key.startswith("_"): + # In general we ignore members whose names start with _, but there are some exceptions. + # __init__ is for constructors and we don't ignore them. + # _ is used as a prefix for members whose name would normally begin with a number and we don't ignore them. + if key != "__init__" and not startsWithUnderscoreDigit(key): + return True + return False + + +def startsWithUnderscoreDigit(s: str): + return ( + s.startswith("_") and + len(s) > 1 and s[1].isdigit()) + + +def startsWithkUpper(s: str): + return ( + s.startswith("k") and + len(s) > 1 and s[1].isupper()) + + +def isEnum(object): + if not inspect.isclass(object): + return False + if issubclass(object, Enum): + return True + return ( + object.__doc__ and + (object.__doc__.startswith("Members:\n\n") or object.__doc__.find("\n\nMembers:\n\n") != -1) and + hasattr(object, "__init__") and + inspect.isroutine(object.__init__) and + inspect.ismethoddescriptor(object.__init__) and + hasattr(object.__init__, "__doc__") and + object.__init__.__doc__ == f"__init__(self: {getFullClassName(object)}, value: int) -> None\n" and + hasattr(object, "name") and + inspect.isdatadescriptor(object.name) and + object.name.__doc__ == 'name(self: object) -> str\n' and + hasattr(object, "value") and + inspect.isdatadescriptor(object.value)) + + +def mightBeConstructor(object): + return ( + inspect.isroutine(object) and + (inspect.ismethoddescriptor(object) or inspect.isfunction(object)) and + object.__name__ == "__init__") + + +def isConstructor(parent, key: str, object): + return ( + mightBeConstructor(object) and + inspect.isclass(parent) and + not isEnum(parent)) + + +def isFunction(parent, key: str, object): + return ( + inspect.isroutine(object) and + object.__qualname__ != "BaseException.add_note" and + object.__qualname__ != "BaseException.with_traceback" and + not key.startswith("_")) + + +def isNothing(object): + return ( + not inspect.ismodule(object) and + not inspect.isclass(object) and + not inspect.isfunction(object) and + not inspect.isgeneratorfunction(object) and + not inspect.isgenerator(object) and + not inspect.iscoroutinefunction(object) and + not inspect.iscoroutine(object) and + not inspect.isawaitable(object) and + not inspect.isasyncgenfunction(object) and + not inspect.isasyncgen(object) and + not inspect.istraceback(object) and + not inspect.isframe(object) and + not inspect.iscode(object) and + not inspect.isbuiltin(object) and + not inspect.ismethodwrapper(object) and + not inspect.isroutine(object) and + not inspect.isabstract(object) and + not inspect.ismethoddescriptor(object) and + not inspect.isdatadescriptor(object) and + not inspect.isgetsetdescriptor(object) and + not inspect.ismemberdescriptor(object)) + + +def isModuleVariableReadable(parent, key: str, object): + return ( + inspect.ismodule(parent) and + not (key.startswith("_") and not startsWithUnderscoreDigit(key)) and + isNothing(object) and + not (type(object) == logging.Logger) and + not (type(object).__module__ == "typing") and + not (type(object).__module__ == "__future__")) + + +def isModuleVariableWritable(parent, key: str, object): + return ( + isModuleVariableReadable(parent, key, object) and + not key.isupper() and + not startsWithkUpper(key)) + + +def isClassVariableReadable(parent, key: str, object): + return ( + inspect.isclass(parent) and + not (key.startswith("_") and not startsWithUnderscoreDigit(key)) and + isNothing(object) and + not (isEnum(parent) and type(object) == parent) and + not (type(object) == logging.Logger) and + not (key == "WPIStruct" and type(object).__name__ == "PyCapsule")) + + +def isClassVariableWritable(parent, key: str, object): + return ( + isClassVariableReadable(parent, key, object) and + not key.isupper() and + not startsWithkUpper(key)) + + +def isInstanceVariableReadable(parent, key: str, object): + return ( + inspect.isclass(parent) and not isEnum(parent) and + not key.startswith("_") and + not key.startswith("m_") and + inspect.isdatadescriptor(object) and + type(object) == property and + object.fget is not None and inspect.isroutine(object.fget)) + + +def isInstanceVariableWritable(parent, key: str, object): + return ( + isInstanceVariableReadable(parent, key, object) and + object.fset is not None and inspect.isroutine(object.fset)) + + +def isTypeAlias(parent, key: str, object): + return ( + inspect.isclass(object) and + key and + parent and + object.__name__ != key and + (inspect.ismodule(parent) or inspect.isclass(parent))) + + +def isOverloaded(object): + return ( + inspect.isroutine(object) and + object.__doc__ and object.__doc__.startswith(f"{object.__name__}(*args, **kwargs)\nOverloaded function.\n\n")) + + +def _annotationToType(annotation) -> str: + if inspect.isclass(annotation): + return getFullClassName(annotation) + return annotation + + +def inspectSignature(object, cls=None) -> str: + try: + sig = inspect.signature(object) + s = f"{object.__name__}(" + delimiter = "" + for param in sig.parameters.values(): + param_name_prefix = "" + param_type = "" + if param.annotation != inspect.Parameter.empty: + param_type = _annotationToType(param.annotation) + else: + if param.name == "self" and cls: + param_type = getFullClassName(cls) + elif param.name == "args": + param_name_prefix = "*" + elif param.name == "kwargs": + param_name_prefix = "**" + if param_type: + s = f"{s}{delimiter}{param_name_prefix}{param.name}: {param_type}" + else: + s = f"{s}{delimiter}{param_name_prefix}{param.name}" + delimiter = ", " + s = f"{s})" + + if sig.return_annotation != inspect.Signature.empty: + s = f"{s} -> {_annotationToType(sig.return_annotation)}" + else: + if object.__name__ == "__init__": + s = f"{s} -> None" + except: + s = "" + return s + + +def processFunction(object, cls=None) -> tuple[list[str], list[str]]: + if not inspect.isroutine(object): + raise Exception(f"Argument object must be a function. inspect.isroutine returned False.") + signatures = [] + comments = [] + if not object.__doc__: + signature = inspectSignature(object, cls) + if signature: + signatures.append(signature) + comments.append("") + return (signatures, comments) + + doc = re.sub(r" object at 0x[0-9a-fA-F]{9}", "", object.__doc__) + + if not isOverloaded(object): + eolIndex = doc.find("\n") + line = doc[:eolIndex] + if _isSignature(line): + signatures.append(line) + comments.append(doc[eolIndex + 1:].strip()) + else: + signature = inspectSignature(object, cls) + if signature: + signatures.append(signature) + comments.append(doc) + return (signatures, comments) + + signatureIndices = [] + commentEndIndices = [] + + # Find the indices of the start of signatures + expected_number = 1 + index = 0 + while True: + s = f"\n\n{expected_number}. " + index = doc.find(s, index) + if index == -1: + commentEndIndices.append(len(doc)) + break + if expected_number > 1: + commentEndIndices.append(index) + index += len(s) + signatureIndices.append(index) + expected_number += 1 + + for i in range(len(signatureIndices)): + index = signatureIndices[i] + eolIndex = doc.find("\n", index) + signatures.append(doc[index:eolIndex]) + comments.append(doc[eolIndex + 1 : commentEndIndices[i]].strip()) + return (signatures, comments) + + +def getClassesFromSignatureLine(signature_line: str): + classes = [] + try: + (function_name, arg_names, arg_types, arg_default_values, return_type) = processSignature(signature_line) + for arg_type in arg_types: + if arg_type == "bool" or arg_type == "str" or arg_type == "float" or arg_type == "int": + continue + classes.append(getClass(arg_type)) + if return_type != "None" and return_type != "bool" and return_type != "str" and return_type != "float" and return_type != "int": + classes.append(getClass(return_type)) + except: + pass + return classes + + +def _processGetter(fget: types.FunctionType) -> tuple[str, str, str, str]: + signature_line = fget.__doc__.split("\n")[0] + match = re.fullmatch(r"(\w*)\((\w+)\: (.+)\) \-\> (.+)", signature_line) + if not match: + raise Exception(f"Failed to parse signature line {signature_line}") + var_name = match.group(1) + self_name = match.group(2) + self_type = match.group(3) + var_type = match.group(4) + return (var_name, self_name, self_type, var_type) + + +def getVarTypeFromGetter(fget: types.FunctionType) -> str: + try: + (var_name, self_name, self_type, var_type) = _processGetter(fget) + return var_type + except: + return None + + +def _isBuiltInModuleName(first_module_name: str): + if first_module_name in sys.stdlib_module_names: + return True + if first_module_name == "pybind11_builtins": + return True + return False + + +def isBuiltInModule(module: types.ModuleType): + return _isBuiltInModuleName(getFullModuleName(module).split(".")[0]) + + +def isBuiltInClass(cls: type): + return _isBuiltInModuleName(cls.__module__.split(".")[0]) + + +def _collectModulesAndClasses( + object, packages: list[str], modules: list[types.ModuleType], classes: list[type], + dict_class_name_to_alias: dict[str, str], ids: list[int]): + if id(object) in ids: + return + ids.append(id(object)) + + if inspect.ismodule(object): + if isBuiltInModule(object): + return + if object not in modules: + modules.append(object) + if object.__package__: + if object.__package__ not in packages: + packages.append(object.__package__) + if inspect.isclass(object): + if isBuiltInClass(object): + return + if object not in classes: + classes.append(object) + + for key, member in inspect.getmembers(object): + if key == "_": + continue + if ignoreMember(object, key, member): + continue + + if isTypeAlias(object, key, member): + alias = member + if inspect.ismodule(object): + dict_class_name_to_alias.update({f"{getFullModuleName(object)}.{key}": getFullClassName(alias)}) + elif inspect.isclass(object): + dict_class_name_to_alias.update({f"{getFullClassName(object)}.{key}": getFullClassName(alias)}) + + if inspect.ismodule(member): + _collectModulesAndClasses(member, packages, modules, classes, dict_class_name_to_alias, ids) + if inspect.isclass(member): + # Collect the classes in the base classes (including this class). + for cls in inspect.getmro(member): + if isBuiltInClass(cls): + break + _collectModulesAndClasses(cls, packages, modules, classes, dict_class_name_to_alias, ids) + if inspect.isroutine(member) and member.__doc__: + # Collect the classes for the function arguments and return types. + signature_line = member.__doc__.split("\n")[0] + for cls in getClassesFromSignatureLine(signature_line): + if isBuiltInClass(cls): + continue + _collectModulesAndClasses(cls, packages, modules, classes, dict_class_name_to_alias, ids) + if isNothing(member): + # Collect the class of this class variable. + cls = type(member) + if not isBuiltInClass(cls): + _collectModulesAndClasses(cls, packages, modules, classes, dict_class_name_to_alias, ids) + if inspect.isdatadescriptor(member): + if hasattr(member, "fget"): + # Collect the class of this instance variable. + var_type = getVarTypeFromGetter(member.fget) + if var_type and var_type.find(".") != -1: + try: + cls = getClass(var_type) + except: + cls = None + if cls and not isBuiltInClass(cls): + _collectModulesAndClasses(cls, packages, modules, classes, dict_class_name_to_alias, ids) + + +def collectModulesAndClasses(root_modules: list[types.ModuleType]) -> tuple[list[types.ModuleType], list[type], dict[str, list[str]]]: + packages = [] + modules = [] + classes = [] + dict_class_name_to_alias = {} + ids = [] + for module in root_modules: + _collectModulesAndClasses(module, packages, modules, classes, dict_class_name_to_alias, ids) + classes.sort(key=lambda c: getFullClassName(c)) + return (packages, modules, classes, dict_class_name_to_alias) + + +def collectSubclasses(classes: list[type]) -> dict[str, list[str]]: + dict_class_name_to_subclass_names = {} + for subclass in classes: + for base_class in subclass.__bases__: + if isBuiltInClass(base_class): + continue + base_class_name = getFullClassName(base_class) + subclass_names = dict_class_name_to_subclass_names.get(base_class_name) + if not subclass_names: + subclass_names = [] + dict_class_name_to_subclass_names.update({base_class_name: subclass_names}) + subclass_name = getFullClassName(subclass) + if subclass_name not in subclass_names: + subclass_names.append(subclass_name) + return dict_class_name_to_subclass_names diff --git a/python_tools/requirements.txt b/python_tools/requirements.txt new file mode 100644 index 00000000..9c07de7c --- /dev/null +++ b/python_tools/requirements.txt @@ -0,0 +1,32 @@ +absl-py==2.1.0 +bcrypt==4.2.1 +cffi==1.17.1 +cryptography==44.0.0 +flexcache==0.3 +flexparser==0.4 +iniconfig==2.0.0 +packaging==23.2 +paramiko==3.5.0 +Pint==0.24.4 +platformdirs==4.3.6 +pluggy==1.5.0 +pycparser==2.22 +pyfrc==2025.0.0 +PyNaCl==1.5.0 +pynetconsole==2.0.4 +pyntcore==2025.1.1.0 +pytest==8.3.4 +pytest-reraise==2.1.2 +robotpy==2025.1.1.1 +robotpy-cli==2024.0.0 +robotpy-hal==2025.1.1.0 +robotpy-halsim-gui==2025.1.1.0 +robotpy-installer==2025.0.0 +robotpy-wpilib-utilities==2025.0.0 +robotpy-wpimath==2025.1.1.0 +robotpy-wpinet==2025.1.1.0 +robotpy-wpiutil==2025.1.1.0 +tomli==2.2.1 +tomlkit==0.13.2 +typing_extensions==4.12.2 +wpilib==2025.1.1.0 diff --git a/server_python_scripts/blocks_base_classes/mechanism.py b/server_python_scripts/blocks_base_classes/mechanism.py index dbf89a1d..e9d7ab62 100644 --- a/server_python_scripts/blocks_base_classes/mechanism.py +++ b/server_python_scripts/blocks_base_classes/mechanism.py @@ -3,12 +3,12 @@ class Mechanism: def __init__(self): self.hardware = [] - def start(self): + def start(self) -> None: for hardware in self.hardware: hardware.start() - def update(self): + def update(self) -> None: for hardware in self.hardware: hardware.update() - def stop(self): + def stop(self) -> None: for hardware in self.hardware: - hardware.stop() \ No newline at end of file + hardware.stop() diff --git a/server_python_scripts/blocks_base_classes/opmode.py b/server_python_scripts/blocks_base_classes/opmode.py index c4f123f8..de597e53 100644 --- a/server_python_scripts/blocks_base_classes/opmode.py +++ b/server_python_scripts/blocks_base_classes/opmode.py @@ -1,12 +1,14 @@ +from .robot_base import RobotBase + # This is the base class that all OpModes derive from class OpMode: - def __init__(self, robot): + def __init__(self, robot: RobotBase): self.robot = robot - def start(self): + def start(self) -> None: self.robot.start() - def loop(self): + def loop(self) -> None: self.robot.update() - def stop(self): + def stop(self) -> None: self.robot.stop() # For now this does nothing but it lets the decorator work @@ -23,4 +25,4 @@ def Name(OpMode, str): return OpMode def Group(OpMode, str): - return OpMode \ No newline at end of file + return OpMode diff --git a/server_python_scripts/blocks_base_classes/robot_base.py b/server_python_scripts/blocks_base_classes/robot_base.py index e122d85e..c63129ce 100644 --- a/server_python_scripts/blocks_base_classes/robot_base.py +++ b/server_python_scripts/blocks_base_classes/robot_base.py @@ -3,12 +3,12 @@ class RobotBase: def __init__(self): self.hardware = [] - def start(self): + def start(self) -> None: for hardware in self.hardware: hardware.start() - def update(self): + def update(self) -> None: for hardware in self.hardware: hardware.update() - def stop(self): + def stop(self) -> None: for hardware in self.hardware: - hardware.stop() \ No newline at end of file + hardware.stop() diff --git a/src/blocks/mrc_call_python_function.ts b/src/blocks/mrc_call_python_function.ts index 3ec39254..ab1ea5b2 100644 --- a/src/blocks/mrc_call_python_function.ts +++ b/src/blocks/mrc_call_python_function.ts @@ -25,7 +25,7 @@ import { Order } from 'blockly/python'; import { ClassMethodDefExtraState } from './mrc_class_method_def' import { getClassData, getAllowedTypesForSetCheck, getOutputCheck } from './utils/python'; -import { FunctionData, findSuperFunctionData } from './utils/python_json_types'; +import { ArgData, FunctionData, findSuperFunctionData } from './utils/python_json_types'; import * as Value from './utils/value'; import * as Variable from './utils/variable'; import { Editor } from '../editor/editor'; @@ -42,6 +42,7 @@ import * as CommonStorage from '../storage/common_storage'; export const BLOCK_NAME = 'mrc_call_python_function'; export enum FunctionKind { + BUILT_IN = 'built-in', MODULE = 'module', STATIC = 'static', CONSTRUCTOR = 'constructor', @@ -67,6 +68,66 @@ const WARNING_ID_FUNCTION_CHANGED = 'function changed'; // Functions used for creating blocks for the toolbox. +export function addBuiltInFunctionBlocks( + functions: FunctionData[], + contents: ToolboxItems.ContentsType[]) { + for (const functionData of functions) { + const block = createBuiltInMethodBlock(functionData); + contents.push(block); + } +} + +function createBuiltInMethodBlock( + functionData: FunctionData): ToolboxItems.Block { + const extraState: CallPythonFunctionExtraState = { + functionKind: FunctionKind.BUILT_IN, + returnType: functionData.returnType, + args: [], + tooltip: functionData.tooltip, + }; + const fields: {[key: string]: any} = {}; + fields[FIELD_FUNCTION_NAME] = functionData.functionName; + const inputs: {[key: string]: any} = {}; + processArgs(functionData.args, extraState, inputs, functionData.declaringClassName); + return createBlock(extraState, fields, inputs); +} + +function processArgs( + args: ArgData[], + extraState: CallPythonFunctionExtraState, + inputs: {[key: string]: any}, + declaringClassName?: string) { + for (let i = 0; i < args.length; i++) { + let argName = args[i].name; + if (i === 0 && argName === 'self' && declaringClassName) { + argName = Variable.getSelfArgName(declaringClassName); + } + extraState.args.push({ + 'name': argName, + 'type': args[i].type, + }); + // Check if we should plug a variable getter block into the argument input socket. + const input = Value.valueForFunctionArgInput(args[i].type, args[i].defaultValue); + if (input) { + inputs['ARG' + i] = input; + } + } +} + +function createBlock( + extraState: CallPythonFunctionExtraState, + fields: {[key: string]: any}, + inputs: {[key: string]: any}): ToolboxItems.Block { + let block = new ToolboxItems.Block(BLOCK_NAME, extraState, fields, Object.keys(inputs).length ? inputs : null); + if (extraState.returnType && extraState.returnType != 'None') { + const varName = Variable.varNameForType(extraState.returnType); + if (varName) { + block = Variable.createVariableSetterBlock(varName, block); + } + } + return block; +} + export function addModuleFunctionBlocks( moduleName: string, functions: FunctionData[], @@ -107,26 +168,8 @@ function createModuleFunctionOrStaticMethodBlock( fields[FIELD_MODULE_OR_CLASS_NAME] = moduleOrClassName; fields[FIELD_FUNCTION_NAME] = functionData.functionName; const inputs: {[key: string]: any} = {}; - for (let i = 0; i < functionData.args.length; i++) { - const argData = functionData.args[i]; - extraState.args.push({ - 'name': argData.name, - 'type': argData.type, - }); - // Check if we should plug a variable getter block into the argument input socket. - const input = Value.valueForFunctionArgInput(argData.type, argData.defaultValue); - if (input) { - inputs['ARG' + i] = input; - } - } - let block = new ToolboxItems.Block(BLOCK_NAME, extraState, fields, Object.keys(inputs).length ? inputs : null); - if (functionData.returnType && functionData.returnType != 'None') { - const varName = Variable.varNameForType(functionData.returnType); - if (varName) { - block = Variable.createVariableSetterBlock(varName, block); - } - } - return block; + processArgs(functionData.args, extraState, inputs, functionData.declaringClassName); + return createBlock(extraState, fields, inputs); } export function addConstructorBlocks( @@ -152,26 +195,8 @@ function createConstructorBlock( const fields: {[key: string]: any} = {}; fields[FIELD_MODULE_OR_CLASS_NAME] = functionData.declaringClassName; const inputs: {[key: string]: any} = {}; - for (let i = 0; i < functionData.args.length; i++) { - const argData = functionData.args[i]; - extraState.args.push({ - 'name': argData.name, - 'type': argData.type, - }); - // Check if we should plug a variable getter block into the argument input socket. - const input = Value.valueForFunctionArgInput(argData.type, argData.defaultValue); - if (input) { - inputs['ARG' + i] = input; - } - } - let block = new ToolboxItems.Block(BLOCK_NAME, extraState, fields, Object.keys(inputs).length ? inputs : null); - if (functionData.returnType && functionData.returnType != 'None') { - const varName = Variable.varNameForType(functionData.returnType); - if (varName) { - block = Variable.createVariableSetterBlock(varName, block); - } - } - return block; + processArgs(functionData.args, extraState, inputs, functionData.declaringClassName); + return createBlock(extraState, fields, inputs); } export function addInstanceMethodBlocks( @@ -195,30 +220,8 @@ function createInstanceMethodBlock( fields[FIELD_MODULE_OR_CLASS_NAME] = functionData.declaringClassName; fields[FIELD_FUNCTION_NAME] = functionData.functionName; const inputs: {[key: string]: any} = {}; - for (let i = 0; i < functionData.args.length; i++) { - const argData = functionData.args[i]; - let argName = argData.name; - if (i === 0 && argName === 'self' && functionData.declaringClassName) { - argName = Variable.getSelfArgName(functionData.declaringClassName); - } - extraState.args.push({ - 'name': argName, - 'type': argData.type, - }); - // Check if we should plug a variable getter block into the argument input socket. - const input = Value.valueForFunctionArgInput(argData.type, argData.defaultValue); - if (input) { - inputs['ARG' + i] = input; - } - } - let block = new ToolboxItems.Block(BLOCK_NAME, extraState, fields, Object.keys(inputs).length ? inputs : null); - if (functionData.returnType && functionData.returnType != 'None') { - const varName = Variable.varNameForType(functionData.returnType); - if (varName) { - block = Variable.createVariableSetterBlock(varName, block); - } - } - return block; + processArgs(functionData.args, extraState, inputs, functionData.declaringClassName); + return createBlock(extraState, fields, inputs); } export function getInstanceComponentBlocks( @@ -267,29 +270,10 @@ function createInstanceComponentBlock( const inputs: {[key: string]: any} = {}; // For INSTANCE_COMPONENT functions, the 0 argument is 'self', but // self is represented by the FIELD_COMPONENT_NAME field. - // We don't include the arg or input for self. - for (let i = 1; i < functionData.args.length; i++) { - const argData = functionData.args[i]; - const argName = argData.name; - extraState.args.push({ - 'name': argName, - 'type': argData.type, - }); - // Check if we should plug a variable getter block into the argument input socket. - const input = Value.valueForFunctionArgInput(argData.type, argData.defaultValue); - if (input) { - // Because we skipped the self argument, use i - 1 when filling the inputs array. - inputs['ARG' + (i - 1)] = input; - } - } - let block = new ToolboxItems.Block(BLOCK_NAME, extraState, fields, Object.keys(inputs).length ? inputs : null); - if (functionData.returnType && functionData.returnType != 'None') { - const varName = Variable.varNameForType(functionData.returnType); - if (varName) { - block = Variable.createVariableSetterBlock(varName, block); - } - } - return block; + // We don't include the arg for self. + const argsWithoutSelf = functionData.args.slice(1); + processArgs(argsWithoutSelf, extraState, inputs); + return createBlock(extraState, fields, inputs); } export function getInstanceRobotBlocks(methods: CommonStorage.Method[]): ToolboxItems.ContentsType[] { @@ -314,28 +298,18 @@ function createInstanceRobotBlock(method: CommonStorage.Method): ToolboxItems.Bl const fields: {[key: string]: any} = {}; fields[FIELD_FUNCTION_NAME] = method.visibleName; const inputs: {[key: string]: any} = {}; - // We don't include the arg or input for the self argument. + // Convert method.args from CommonStorage.MethodArg[] to ArgData[]. + const args: ArgData[] = []; + // We don't include the arg for the self argument. for (let i = 1; i < method.args.length; i++) { - const arg = method.args[i]; - extraState.args.push({ - 'name': arg.name, - 'type': arg.type, + args.push({ + name: method.args[i].name, + type: method.args[i].type, + defaultValue: '', }); - // Check if we should plug a variable getter block into the argument input socket. - const input = Value.valueForFunctionArgInput(arg.type, ''); - if (input) { - // Because we skipped the self argument, use i - 1 when filling the inputs array. - inputs['ARG' + (i - 1)] = input; - } - } - let block = new ToolboxItems.Block(BLOCK_NAME, extraState, fields, Object.keys(inputs).length ? inputs : null); - if (method.returnType && method.returnType != 'None') { - const varName = Variable.varNameForType(method.returnType); - if (varName) { - block = Variable.createVariableSetterBlock(varName, block); - } } - return block; + processArgs(args, extraState, inputs); + return createBlock(extraState, fields, inputs); } //.............................................................................. @@ -416,6 +390,11 @@ const CALL_PYTHON_FUNCTION = { this.setTooltip(() => { let tooltip: string; switch (this.mrcFunctionKind) { + case FunctionKind.BUILT_IN: { + const functionName = this.getFieldValue(FIELD_FUNCTION_NAME); + tooltip = 'Calls the builtin function ' + functionName + '.'; + break; + } case FunctionKind.MODULE: { const moduleName = this.getFieldValue(FIELD_MODULE_OR_CLASS_NAME); const functionName = this.getFieldValue(FIELD_FUNCTION_NAME); @@ -581,6 +560,11 @@ const CALL_PYTHON_FUNCTION = { if (!this.getInput('TITLE')) { // Add the dummy input. switch (this.mrcFunctionKind) { + case FunctionKind.BUILT_IN: + this.appendDummyInput('TITLE') + .appendField('call') + .appendField(createFieldNonEditableText(''), FIELD_FUNCTION_NAME); + break; case FunctionKind.MODULE: this.appendDummyInput('TITLE') .appendField('call') @@ -822,6 +806,13 @@ export const pythonFromBlock = function( let code; let argStartIndex = 0; switch (callPythonFunctionBlock.mrcFunctionKind) { + case FunctionKind.BUILT_IN: { + const functionName = (callPythonFunctionBlock.mrcActualFunctionName) + ? callPythonFunctionBlock.mrcActualFunctionName + : block.getFieldValue(FIELD_FUNCTION_NAME); + code = functionName; + break; + } case FunctionKind.MODULE: { const moduleName = block.getFieldValue(FIELD_MODULE_OR_CLASS_NAME); const functionName = (callPythonFunctionBlock.mrcActualFunctionName) diff --git a/src/blocks/utils/external_samples_data.ts b/src/blocks/utils/external_samples_data.ts deleted file mode 100644 index 66f7cfd7..00000000 --- a/src/blocks/utils/external_samples_data.ts +++ /dev/null @@ -1,25 +0,0 @@ -/** - * @license - * Copyright 2025 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * @author lizlooney@google.com (Liz Looney) - */ - -import { PythonData } from './python_json_types'; -import generatedExternalSamplesData from './generated/external_samples_data.json'; - -export const externalSamplesData = generatedExternalSamplesData as PythonData; diff --git a/src/blocks/utils/generated/server_python_scripts.json b/src/blocks/utils/generated/server_python_scripts.json new file mode 100644 index 00000000..1b76672b --- /dev/null +++ b/src/blocks/utils/generated/server_python_scripts.json @@ -0,0 +1,212 @@ +{ + "aliases": {}, + "classes": [ + { + "className": "blocks_base_classes.mechanism.Mechanism", + "classVariables": [], + "constructors": [ + { + "args": [], + "declaringClassName": "blocks_base_classes.mechanism.Mechanism", + "functionName": "__init__", + "returnType": "blocks_base_classes.mechanism.Mechanism", + "tooltip": "" + } + ], + "enums": [], + "instanceMethods": [ + { + "args": [ + { + "defaultValue": "", + "name": "self", + "type": "blocks_base_classes.mechanism.Mechanism" + } + ], + "declaringClassName": "blocks_base_classes.mechanism.Mechanism", + "functionName": "start", + "returnType": "None", + "tooltip": "" + }, + { + "args": [ + { + "defaultValue": "", + "name": "self", + "type": "blocks_base_classes.mechanism.Mechanism" + } + ], + "declaringClassName": "blocks_base_classes.mechanism.Mechanism", + "functionName": "stop", + "returnType": "None", + "tooltip": "" + }, + { + "args": [ + { + "defaultValue": "", + "name": "self", + "type": "blocks_base_classes.mechanism.Mechanism" + } + ], + "declaringClassName": "blocks_base_classes.mechanism.Mechanism", + "functionName": "update", + "returnType": "None", + "tooltip": "" + } + ], + "instanceVariables": [], + "moduleName": "blocks_base_classes.mechanism", + "staticMethods": [] + }, + { + "className": "blocks_base_classes.opmode.OpMode", + "classVariables": [], + "constructors": [ + { + "args": [ + { + "defaultValue": "", + "name": "robot", + "type": "blocks_base_classes.robot_base.RobotBase" + } + ], + "declaringClassName": "blocks_base_classes.opmode.OpMode", + "functionName": "__init__", + "returnType": "blocks_base_classes.opmode.OpMode", + "tooltip": "" + } + ], + "enums": [], + "instanceMethods": [ + { + "args": [ + { + "defaultValue": "", + "name": "self", + "type": "blocks_base_classes.opmode.OpMode" + } + ], + "declaringClassName": "blocks_base_classes.opmode.OpMode", + "functionName": "loop", + "returnType": "None", + "tooltip": "" + }, + { + "args": [ + { + "defaultValue": "", + "name": "self", + "type": "blocks_base_classes.opmode.OpMode" + } + ], + "declaringClassName": "blocks_base_classes.opmode.OpMode", + "functionName": "start", + "returnType": "None", + "tooltip": "" + }, + { + "args": [ + { + "defaultValue": "", + "name": "self", + "type": "blocks_base_classes.opmode.OpMode" + } + ], + "declaringClassName": "blocks_base_classes.opmode.OpMode", + "functionName": "stop", + "returnType": "None", + "tooltip": "" + } + ], + "instanceVariables": [], + "moduleName": "blocks_base_classes.opmode", + "staticMethods": [] + }, + { + "className": "blocks_base_classes.robot_base.RobotBase", + "classVariables": [], + "constructors": [ + { + "args": [], + "declaringClassName": "blocks_base_classes.robot_base.RobotBase", + "functionName": "__init__", + "returnType": "blocks_base_classes.robot_base.RobotBase", + "tooltip": "" + } + ], + "enums": [], + "instanceMethods": [ + { + "args": [ + { + "defaultValue": "", + "name": "self", + "type": "blocks_base_classes.robot_base.RobotBase" + } + ], + "declaringClassName": "blocks_base_classes.robot_base.RobotBase", + "functionName": "start", + "returnType": "None", + "tooltip": "" + }, + { + "args": [ + { + "defaultValue": "", + "name": "self", + "type": "blocks_base_classes.robot_base.RobotBase" + } + ], + "declaringClassName": "blocks_base_classes.robot_base.RobotBase", + "functionName": "stop", + "returnType": "None", + "tooltip": "" + }, + { + "args": [ + { + "defaultValue": "", + "name": "self", + "type": "blocks_base_classes.robot_base.RobotBase" + } + ], + "declaringClassName": "blocks_base_classes.robot_base.RobotBase", + "functionName": "update", + "returnType": "None", + "tooltip": "" + } + ], + "instanceVariables": [], + "moduleName": "blocks_base_classes.robot_base", + "staticMethods": [] + } + ], + "modules": [ + { + "enums": [], + "functions": [], + "moduleName": "blocks_base_classes", + "moduleVariables": [] + }, + { + "enums": [], + "functions": [], + "moduleName": "blocks_base_classes.mechanism", + "moduleVariables": [] + }, + { + "enums": [], + "functions": [], + "moduleName": "blocks_base_classes.opmode", + "moduleVariables": [] + }, + { + "enums": [], + "functions": [], + "moduleName": "blocks_base_classes.robot_base", + "moduleVariables": [] + } + ], + "subclasses": {} +} \ No newline at end of file diff --git a/src/blocks/utils/python.ts b/src/blocks/utils/python.ts index fa5c6227..45304b93 100644 --- a/src/blocks/utils/python.ts +++ b/src/blocks/utils/python.ts @@ -20,18 +20,26 @@ */ import { ClassData, PythonData, ModuleData, organizeVarDataByType, VariableGettersAndSetters } from './python_json_types'; -import { robotPyData } from './robotpy_data'; -import { externalSamplesData } from './external_samples_data'; +import generatedExternalSamplesData from './generated/external_samples_data.json'; +import generatedRobotPyData from './generated/robotpy_data.json'; +import generatedServerPythonScripts from './generated/server_python_scripts.json'; import * as PythonEnum from "../mrc_get_python_enum_value"; import * as GetPythonVariable from "../mrc_get_python_variable"; import * as SetPythonVariable from "../mrc_set_python_variable"; + // Utilities related to blocks for python modules and classes, including those from RobotPy, external samples, etc. +export const robotPyData = generatedRobotPyData as PythonData; +const externalSamplesData = generatedExternalSamplesData as PythonData +const serverPythonScripts = generatedServerPythonScripts as PythonData; + + const allPythonData: PythonData[] = []; allPythonData.push(robotPyData); allPythonData.push(externalSamplesData); +allPythonData.push(serverPythonScripts); // Initializes enum and variable blocks for python modules and classes. diff --git a/src/blocks/utils/robotpy_data.ts b/src/blocks/utils/robotpy_data.ts deleted file mode 100644 index aba9d152..00000000 --- a/src/blocks/utils/robotpy_data.ts +++ /dev/null @@ -1,25 +0,0 @@ -/** - * @license - * Copyright 2025 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * @author lizlooney@google.com (Liz Looney) - */ - -import { PythonData } from './python_json_types'; -import generatedRobotPyData from './generated/robotpy_data.json'; - -export const robotPyData = generatedRobotPyData as PythonData; diff --git a/src/blocks/utils/value.ts b/src/blocks/utils/value.ts index 55219ff2..0a4283e5 100644 --- a/src/blocks/utils/value.ts +++ b/src/blocks/utils/value.ts @@ -54,8 +54,9 @@ export function valueForFunctionArgInput(argType: string, argDefaultValue: strin // In RobotPy function hal.report(), the arg named feature has a default value of None. return null; } - // If argDefaultValue has single quotes, it's a literal string. - if (argDefaultValue.startsWith("'") && argDefaultValue.endsWith("'")) { + // If argDefaultValue is surrounded by single or double quotes, it's a literal string. + if (argDefaultValue.startsWith("'") && argDefaultValue.endsWith("'") || + argDefaultValue.startsWith('"') && argDefaultValue.endsWith('"')) { const textValue = argDefaultValue.substring(1, argDefaultValue.length-1); return createTextShadowValue(textValue); } diff --git a/src/toolbox/items.ts b/src/toolbox/items.ts index 4a83c714..76d35a75 100644 --- a/src/toolbox/items.ts +++ b/src/toolbox/items.ts @@ -77,7 +77,7 @@ export class Category extends Item { custom?: string; /** The blocks for this category. */ - contents?: ContentsType[]; + contents: ContentsType[] = []; constructor(name: string, contents: ContentsType[], categorystyle?: string, custom?: string) { super('category'); diff --git a/src/toolbox/robotpy_toolbox.ts b/src/toolbox/robotpy_toolbox.ts index 0213257a..03be1e37 100644 --- a/src/toolbox/robotpy_toolbox.ts +++ b/src/toolbox/robotpy_toolbox.ts @@ -29,7 +29,7 @@ import { addInstanceMethodBlocks, addModuleFunctionBlocks, addStaticMethodBlocks } from '../blocks/mrc_call_python_function'; -import { robotPyData } from '../blocks/utils/robotpy_data'; +import { robotPyData } from '../blocks/utils/python'; import { ClassData, ModuleData, @@ -55,8 +55,8 @@ export function getToolboxCategories(shownPythonToolboxCategories: Set | kind: 'category', name: name, moduleName: moduleData.moduleName, + contents: [], }; - moduleCategory.contents = []; addModuleBlocks(moduleData, moduleCategory.contents); allCategories[path] = moduleCategory; moduleCategories[path] = moduleCategory; @@ -72,8 +72,8 @@ export function getToolboxCategories(shownPythonToolboxCategories: Set | kind: 'category', name: name, className: classData.className, + contents: [], }; - classCategory.contents = []; addClassBlocks(classData, classCategory.contents); allCategories[path] = classCategory; classCategories[path] = classCategory; diff --git a/src/toolbox/test_category.ts b/src/toolbox/test_category.ts index 7a88cbd1..f46179a3 100644 --- a/src/toolbox/test_category.ts +++ b/src/toolbox/test_category.ts @@ -1,11 +1,28 @@ + import * as Blockly from 'blockly/core'; -export const getCategory = () => ({ +import * as toolboxItems from './items'; +import { FunctionData } from '../blocks/utils/python_json_types'; +import { addBuiltInFunctionBlocks } from '../blocks/mrc_call_python_function'; + +export function getCategory(): toolboxItems.Category { + const contents: toolboxItems.ContentsType[] = []; + + const printFunction: FunctionData = { + functionName: 'print', + tooltip: 'Print the given message', + returnType: 'None', + args: [{ + name: '', + type: 'str', + defaultValue: '""', + }], + }; + + addBuiltInFunctionBlocks([printFunction], contents); + + return { kind: 'category', name: Blockly.Msg['MRC_CATEGORY_TEST'], - contents: [ - { - kind: 'block', - type: 'mrc_print', - }, - ], -}); \ No newline at end of file + contents, + }; +}