diff --git a/src/blocks/mrc_call_python_function.ts b/src/blocks/mrc_call_python_function.ts index 24a53679..29049a1b 100644 --- a/src/blocks/mrc_call_python_function.ts +++ b/src/blocks/mrc_call_python_function.ts @@ -865,7 +865,7 @@ export function pythonFromBlock( generator: ExtendedPythonGenerator, ) { if (block.mrcImportModule) { - generator.addImport(block.mrcImportModule); + generator.importModule(block.mrcImportModule); } let code = ''; let needOpenParen = true; diff --git a/src/blocks/mrc_component.ts b/src/blocks/mrc_component.ts index 689c5324..f310f28a 100644 --- a/src/blocks/mrc_component.ts +++ b/src/blocks/mrc_component.ts @@ -246,7 +246,7 @@ export const pythonFromBlock = function ( generator: ExtendedPythonGenerator, ) { if (block.mrcImportModule) { - generator.addImport(block.mrcImportModule); + generator.importModule(block.mrcImportModule); } let code = 'self.' + block.getFieldValue(FIELD_NAME) + ' = ' + block.getFieldValue(FIELD_TYPE) + "("; diff --git a/src/blocks/mrc_get_python_enum_value.ts b/src/blocks/mrc_get_python_enum_value.ts index d1b8cf87..1fd2c775 100644 --- a/src/blocks/mrc_get_python_enum_value.ts +++ b/src/blocks/mrc_get_python_enum_value.ts @@ -150,7 +150,7 @@ export const pythonFromBlock = function( const enumClassName = block.getFieldValue(FIELD_ENUM_CLASS_NAME); const enumValue = block.getFieldValue(FIELD_ENUM_VALUE); if (getPythonEnumValueBlock.mrcImportModule) { - generator.addImport(getPythonEnumValueBlock.mrcImportModule); + generator.importModule(getPythonEnumValueBlock.mrcImportModule); } const code = enumClassName + '.' + enumValue; return [code, Order.MEMBER]; diff --git a/src/blocks/mrc_get_python_variable.ts b/src/blocks/mrc_get_python_variable.ts index 2d7b5ac7..ce992b10 100644 --- a/src/blocks/mrc_get_python_variable.ts +++ b/src/blocks/mrc_get_python_variable.ts @@ -277,7 +277,7 @@ export const pythonFromBlock = function( case VariableKind.MODULE: { const moduleName = block.getFieldValue(FIELD_MODULE_OR_CLASS_NAME); if (getPythonVariableBlock.mrcImportModule) { - generator.addImport(getPythonVariableBlock.mrcImportModule); + generator.importModule(getPythonVariableBlock.mrcImportModule); } const code = moduleName + '.' + varName; return [code, Order.MEMBER]; @@ -285,7 +285,7 @@ export const pythonFromBlock = function( case VariableKind.CLASS: { const className = block.getFieldValue(FIELD_MODULE_OR_CLASS_NAME); if (getPythonVariableBlock.mrcImportModule) { - generator.addImport(getPythonVariableBlock.mrcImportModule); + generator.importModule(getPythonVariableBlock.mrcImportModule); } const code = className + '.' + varName; return [code, Order.MEMBER]; diff --git a/src/blocks/mrc_mechanism.ts b/src/blocks/mrc_mechanism.ts index c2dfa34d..de1362b4 100644 --- a/src/blocks/mrc_mechanism.ts +++ b/src/blocks/mrc_mechanism.ts @@ -328,7 +328,7 @@ export const pythonFromBlock = function ( generator: ExtendedPythonGenerator, ) { if (block.mrcImportModule) { - generator.addImport(block.mrcImportModule); + generator.importModule(block.mrcImportModule); } let code = 'self.' + block.getFieldValue(FIELD_NAME) + ' = ' + block.mrcImportModule + '.' + block.getFieldValue(FIELD_TYPE) + '('; diff --git a/src/blocks/mrc_port.ts b/src/blocks/mrc_port.ts index e48f267a..a9600637 100644 --- a/src/blocks/mrc_port.ts +++ b/src/blocks/mrc_port.ts @@ -115,7 +115,7 @@ const PORT = { appendFields(this.appendDummyInput(), PORT_TYPE_EXPANSION_HUB_SERVO_PORT, iField++); break; default: - throw new Error('Unexpected portType: ' + state.portType) + throw new Error('Unexpected portType: ' + state.portType); } this.mrcPortType = state.portType; this.mrcPortCount = iField; @@ -129,17 +129,20 @@ export const setup = function () { export const pythonFromBlock = function ( block: PortBlock, generator: ExtendedPythonGenerator) { - generator.addImport('port'); - + const ports: string[] = []; for (let i = 0; i < block.mrcPortCount; i++) { ports.push(block.getFieldValue(FIELD_PREFIX_PORT_NUM + i)); } - let code = 'port.'; + const portType = generator.importModuleName('port', 'PortType'); + const simplePort = generator.importModuleName('port', 'SimplePort'); + const compoundPort = (ports.length === 2) ? generator.importModuleName('port', 'CompoundPort') : ''; + + let code = ''; if (ports.length === 1) { - code += `SimplePort(port_type = port.PortType.${block.mrcPortType}, location = ${ports[0]})`; + code += `${simplePort}(port_type = ${portType}.${block.mrcPortType}, location = ${ports[0]})`; } else if (ports.length === 2) { let port1Type = 'UNKNOWN'; @@ -159,9 +162,9 @@ export const pythonFromBlock = function ( port2Type = PORT_TYPE_EXPANSION_HUB_SERVO_PORT; break; } - code += `CompoundPort(port_type = port.PortType.${block.mrcPortType},\n`; - code += `${generator.INDENT}port1 = port.SimplePort(port_type = port.PortType.${port1Type}, location = ${ports[0]}),\n`; - code += `${generator.INDENT}port2 = port.SimplePort(port_type = port.PortType.${port2Type}, location = ${ports[1]}))`; + code += `${compoundPort}(port_type = ${portType}.${block.mrcPortType},\n`; + code += `${generator.INDENT}port1 = ${simplePort}(port_type = ${portType}.${port1Type}, location = ${ports[0]}),\n`; + code += `${generator.INDENT}port2 = ${simplePort}(port_type = ${portType}.${port2Type}, location = ${ports[1]}))`; } return [code, Order.FUNCTION_CALL]; diff --git a/src/blocks/mrc_set_python_variable.ts b/src/blocks/mrc_set_python_variable.ts index 1cab8ca1..60905a73 100644 --- a/src/blocks/mrc_set_python_variable.ts +++ b/src/blocks/mrc_set_python_variable.ts @@ -268,7 +268,7 @@ export const pythonFromBlock = function( const moduleName = block.getFieldValue(FIELD_MODULE_OR_CLASS_NAME); const value = generator.valueToCode(block, 'VALUE', Order.NONE); if (setPythonVariableBlock.mrcImportModule) { - generator.addImport(setPythonVariableBlock.mrcImportModule); + generator.importModule(setPythonVariableBlock.mrcImportModule); } const code = moduleName + '.' + varName + ' = ' + value + '\n'; return code; @@ -277,7 +277,7 @@ export const pythonFromBlock = function( const className = block.getFieldValue(FIELD_MODULE_OR_CLASS_NAME); const value = generator.valueToCode(block, 'VALUE', Order.NONE); if (setPythonVariableBlock.mrcImportModule) { - generator.addImport(setPythonVariableBlock.mrcImportModule); + generator.importModule(setPythonVariableBlock.mrcImportModule); } const code = className + '.' + varName + ' = ' + value + '\n'; return code; diff --git a/src/blocks/utils/python.ts b/src/blocks/utils/python.ts index 1f26fd25..d4d26226 100644 --- a/src/blocks/utils/python.ts +++ b/src/blocks/utils/python.ts @@ -287,3 +287,15 @@ export function getLegalName(proposedName: string, existingNames: string[]){ } return newName; } + +export function isExistingPythonModule(moduleName: string): boolean { + for (const pythonData of allPythonData) { + // Process modules. + for (const moduleData of pythonData.modules) { + if (moduleData.moduleName === moduleName) { + return true; + } + } + } + return false; +} diff --git a/src/editor/extended_python_generator.ts b/src/editor/extended_python_generator.ts index d4a207aa..9e294cd3 100644 --- a/src/editor/extended_python_generator.ts +++ b/src/editor/extended_python_generator.ts @@ -32,31 +32,21 @@ import { import * as storageModule from '../storage/module'; export class OpModeDetails { - constructor(private name: string, private group : string, private enabled : boolean, private type : string) {} - decorations(className : string) : string{ + constructor(private name: string, private group: string, private enabled: boolean, private type: string) {} + generateDecorators(generator: ExtendedPythonGenerator, className: string): string { let code = ''; - if (this.enabled){ - code += '@' + this.type + "\n"; + if (this.enabled) { + const typeDecorator = generator.importModuleName(MODULE_NAME_BLOCKS_BASE_CLASSES, this.type); + code += `@${typeDecorator}\n`; - if (this.name){ - code += '@Name(' + className + ', "' + this.name + '")\n'; + if (this.name) { + const nameDecorator = generator.importModuleName(MODULE_NAME_BLOCKS_BASE_CLASSES, 'Name'); + code += `@${nameDecorator}(${className}, "${this.name}")\n`; } - if (this.group){ - code += '@Group(' + className + ', "' + this.group + '")\n'; - } - } - return code; - } - imports() : string{ - let code = ''; - if (this.enabled){ - code += 'from ' + MODULE_NAME_BLOCKS_BASE_CLASSES + ' import ' + this.type; - if (this.name){ - code += ', Name'; - } - if (this.group){ - code += ', Group'; + if (this.group) { + const groupDecorator = generator.importModuleName(MODULE_NAME_BLOCKS_BASE_CLASSES, 'Group'); + code += `@${groupDecorator}(${className}, "${this.group}")\n`; } } @@ -68,8 +58,8 @@ export class OpModeDetails { // variables that have been defined so they can be used in other modules. export class ExtendedPythonGenerator extends PythonGenerator { - private workspace: Blockly.Workspace | null = null; private readonly context: GeneratorContext; + private workspace: Blockly.Workspace | null = null; // Fields related to generating the __init__ for a mechanism. private hasAnyComponents = false; @@ -80,15 +70,19 @@ export class ExtendedPythonGenerator extends PythonGenerator { private classMethods: {[key: string]: string} = Object.create(null); private registerEventHandlerStatements: string[] = []; + + private importedNames: {[name: string]: string} = Object.create(null); // value is an import statement + private fromModuleImportNames: {[module: string]: string[]} = Object.create(null); // value is an array of names being imported from the module. + // Opmode details - private details : OpModeDetails | null = null; + private opModeDetails: OpModeDetails | null = null; constructor() { super('Python'); this.context = createGeneratorContext(); } - init(workspace: Blockly.Workspace){ + init(workspace: Blockly.Workspace) { super.init(workspace); // super.init will have put all variables in this.definitions_['variables'] but we need to make @@ -109,7 +103,7 @@ export class ExtendedPythonGenerator extends PythonGenerator { * This is called from the python generator for the mrc_class_method_def for the * init method */ - generateInitStatements() : string { + generateInitStatements(): string { let initStatements = ''; if (this.getModuleType() === storageModule.ModuleType.MECHANISM && this.hasAnyComponents) { @@ -135,8 +129,6 @@ export class ExtendedPythonGenerator extends PythonGenerator { this.context.setModule(module); this.init(workspace); - this.hasAnyComponents = false; - this.componentPorts = Object.create(null); if (this.getModuleType() === storageModule.ModuleType.MECHANISM) { this.hasAnyComponents = mechanismContainerHolder.hasAnyComponents(workspace); mechanismContainerHolder.getComponentPorts(workspace, this.componentPorts); @@ -145,8 +137,18 @@ export class ExtendedPythonGenerator extends PythonGenerator { const code = super.workspaceToCode(workspace); + // Clean up. this.context.setModule(null); this.workspace = null; + this.hasAnyComponents = false; + this.componentPorts = Object.create(null); + this.hasAnyEventHandlers = false; + this.classMethods = Object.create(null); + this.registerEventHandlerStatements = []; + this.importedNames = Object.create(null); + this.fromModuleImportNames = Object.create(null); + this.opModeDetails = null; + return code; } @@ -158,21 +160,53 @@ export class ExtendedPythonGenerator extends PythonGenerator { } /** - * Add an import statement for a python module. - * If the given moduleOrClass is in the blocks_base_classes package, the simple name is returned. + * Import a python module. */ - addImport(moduleOrClass: string): string { - const key = 'import_' + moduleOrClass; - - if (moduleOrClass.startsWith(MODULE_NAME_BLOCKS_BASE_CLASSES + '.') && - moduleOrClass.lastIndexOf('.') == MODULE_NAME_BLOCKS_BASE_CLASSES.length) { - const simpleName = moduleOrClass.substring(MODULE_NAME_BLOCKS_BASE_CLASSES.length + 1); - this.definitions_[key] = 'from ' + MODULE_NAME_BLOCKS_BASE_CLASSES + ' import ' + simpleName; - return simpleName; + importModule(module: string): void { + const key = 'import_' + module; + const importStatement = 'import ' + module; + // Note(lizlooney): We can't really handle name collisions for "import " statements. + // For the user's code, we could try to do "import as ", but that would be + // more difficulat to do for robotpy modules. + this.definitions_[key] = importStatement; + this.importedNames[module] = importStatement; + } + + /** + * Add an import statement of the form "from import ". + * Returns true if successful. + */ + private fromModuleImportName(module: string, name: string): boolean { + const importStatement = 'from ' + module + ' import ' + name; + if (name in this.importedNames) { + // This name is already in the importedNames map. + // If the previously stored value is the same import statement, we don't need to do anything; return true. + // Otherwise, we can't import this because the name will collide; return false. + return (this.importedNames[name] === importStatement); + } + this.importedNames[name] = importStatement; + + if (!(module in this.fromModuleImportNames)) { + this.fromModuleImportNames[module] = []; } + this.fromModuleImportNames[module].push(name); - this.definitions_[key] = 'import ' + moduleOrClass; - return ''; + return true; + } + + /** + * Import a name from a python module. + * If possible, a "from import " import will be used, otherwise a "import " + * import will be used. + * Returns the name, which may be qualified with the module, that can be used in generated python + * code. + */ + importModuleName(module: string, name: string): string { + if (this.fromModuleImportName(module, name)) { + return name; + } + this.importModule(module); + return module + '.' + name; } /** @@ -197,57 +231,71 @@ export class ExtendedPythonGenerator extends PythonGenerator { finish(code: string): string { if (this.workspace) { const className = this.context.getClassName(); - const baseClassName = this.context.getBaseClassName(); - const decorations = this.details?.decorations(className); - const import_decorations = this.details?.imports(); - - if (import_decorations) { - this.definitions_['import_decorations'] = import_decorations; + const decorators = this.opModeDetails + ? this.opModeDetails.generateDecorators(this, className) + : ''; + + let baseClassName = this.context.getBaseClassName(); + if (baseClassName.startsWith(MODULE_NAME_BLOCKS_BASE_CLASSES + '.') && + baseClassName.lastIndexOf('.') == MODULE_NAME_BLOCKS_BASE_CLASSES.length) { + const simpleName = baseClassName.substring(MODULE_NAME_BLOCKS_BASE_CLASSES.length + 1); + if (this.fromModuleImportName(MODULE_NAME_BLOCKS_BASE_CLASSES, simpleName)) { + baseClassName = simpleName; + } else { + this.importModule(MODULE_NAME_BLOCKS_BASE_CLASSES); + } } - const simpleBaseClassName = this.addImport(baseClassName); - if (!simpleBaseClassName) { - throw new Error('addImport for ' + baseClassName + ' did not return a valid simple name') - } + code = decorators + 'class ' + className + '(' + baseClassName + '):\n'; - const classDef = 'class ' + className + '(' + simpleBaseClassName + '):\n'; const classMethods = []; - if (this.registerEventHandlerStatements && this.registerEventHandlerStatements.length > 0) { - let code = 'def register_event_handlers(self):\n'; - for (const registerEventHandlerStatement of this.registerEventHandlerStatements) { - code += this.INDENT + registerEventHandlerStatement; - } - classMethods.push(code); - } // Generate the __init__ method first. if ('__init__' in this.classMethods) { classMethods.push(this.classMethods['__init__']) } + + // Generate the define_hardware method next. + if ('define_hardware' in this.classMethods) { + classMethods.push(this.classMethods['define_hardware']) + } + + // Generate the register_event_handlers method next. + if (this.registerEventHandlerStatements && this.registerEventHandlerStatements.length > 0) { + let registerEventHandlers = 'def register_event_handlers(self):\n'; + for (const registerEventHandlerStatement of this.registerEventHandlerStatements) { + registerEventHandlers += this.INDENT + registerEventHandlerStatement; + } + classMethods.push(registerEventHandlers); + } + + // Generate the remaining methods. for (const name in this.classMethods) { - if (name === '__init__') { + if (name === '__init__' || name === 'define_hardware') { continue; } classMethods.push(this.classMethods[name]) } - this.classMethods = Object.create(null); - this.registerEventHandlerStatements = []; - this.componentPorts = Object.create(null); - code = classDef + this.prefixLines(classMethods.join('\n\n'), this.INDENT); - if (decorations){ - code = decorations + code; - } - this.details = null; + + code += this.prefixLines(classMethods.join('\n\n'), this.INDENT); + } + + // Process the fromModuleImportNames to generate "from import , , ..." statements. + for (const module in this.fromModuleImportNames) { + const names = this.fromModuleImportNames[module]; + const key = 'import_from_' + module; + const importStatement = 'from ' + module + ' import ' + names.sort().join(', '); + this.definitions_[key] = importStatement; } return super.finish(code); } - setOpModeDetails(details : OpModeDetails) { - this.details = details; + setOpModeDetails(opModeDetails: OpModeDetails) { + this.opModeDetails = opModeDetails; } - getClassSpecificForInit() : string{ + getClassSpecificForInit(): string { if (this.context?.getBaseClassName() == CLASS_NAME_OPMODE) { return 'robot' } @@ -259,7 +307,7 @@ export class ExtendedPythonGenerator extends PythonGenerator { * knows whether to call super() or not. * @returns list of method names */ - getBaseClassMethods() : string[] { + getBaseClassMethods(): string[] { const methodNames: string[] = []; const baseClassName = this.context?.getBaseClassName(); diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json index 57ba9688..ad4bb68c 100644 --- a/src/i18n/locales/en/translation.json +++ b/src/i18n/locales/en/translation.json @@ -95,8 +95,9 @@ "ERROR_LOADING_DEPENDENCIES": "Error loading dependencies.", "COPYRIGHT": "© 2025 FIRST. All rights reserved." }, - "INVALID_CLASS_NAME": "{{name}} is not a valid name. Please enter a different name.", + "INVALID_CLASS_NAME": "{{className}} is not a valid name. Please enter a different name.", "CLASS_NAME_ALREADY_EXISTS": "Another Mechanism or OpMode is already named {{name}}. Please enter a different name.", + "PYTHON_MODULE_NAME_ALREADY_EXISTS": "{{name}} is not a valid name because there is a python module named {{moduleName}}.", "THEME_MODAL": { "LIGHT": "Light Theme", "LIGHT_DESCRIPTION": "Clean and bright interface for daytime use", diff --git a/src/i18n/locales/es/translation.json b/src/i18n/locales/es/translation.json index c3739b1c..cafcc207 100644 --- a/src/i18n/locales/es/translation.json +++ b/src/i18n/locales/es/translation.json @@ -98,6 +98,7 @@ }, "INVALID_CLASS_NAME": "{{name}} no es un nombre válido. Por favor ingrese un nombre diferente.", "CLASS_NAME_ALREADY_EXISTS": "Otro Mecanismo u OpMode ya se llama {{name}}. Por favor ingrese un nombre diferente.", + "PYTHON_MODULE_NAME_ALREADY_EXISTS": "{{name}} no es un nombre válido porque existe un módulo de Python llamado {{moduleName}}.", "THEME_MODAL": { "LIGHT": "Tema Claro", "LIGHT_DESCRIPTION": "Interfaz limpia y brillante para uso diurno", diff --git a/src/i18n/locales/he/translation.json b/src/i18n/locales/he/translation.json index 7b9d5bfc..22f3ee3c 100644 --- a/src/i18n/locales/he/translation.json +++ b/src/i18n/locales/he/translation.json @@ -97,6 +97,7 @@ }, "INVALID_CLASS_NAME": "{{name}} אינו שם תקין. אנא הזן שם אחר.", "CLASS_NAME_ALREADY_EXISTS": "מנגנון או אופמוד אחר כבר נקרא {{name}}. אנא הזן שם אחר.", + "PYTHON_MODULE_NAME_ALREADY_EXISTS": "{{name}} אינו שם חוקי מכיוון שקיים מודול פייתון בשם {{moduleName}}.", "THEME_MODAL": { "LIGHT": "ערכת נושא בהירה", "LIGHT_DESCRIPTION": "ממשק נקי ובהיר לשימוש ביום", diff --git a/src/reactComponents/ClassNameComponent.tsx b/src/reactComponents/ClassNameComponent.tsx index f13a2a97..38d9b4c8 100644 --- a/src/reactComponents/ClassNameComponent.tsx +++ b/src/reactComponents/ClassNameComponent.tsx @@ -26,6 +26,7 @@ import * as React from 'react'; import * as commonStorage from '../storage/common_storage'; import * as storageProject from '../storage/project'; import * as storageNames from '../storage/names'; +import { isExistingPythonModule } from '../blocks/utils/python'; /** Props for the ClassNameComponent. */ interface ClassNameComponentProps { @@ -75,6 +76,11 @@ export default function ClassNameComponent(props: ClassNameComponentProps): Reac error = t('CLASS_NAME_ALREADY_EXISTS', { name: newClassName }); } + const moduleName = storageNames.pascalCaseToSnakeCase(newClassName); + if (isExistingPythonModule(moduleName)) { + error = t('PYTHON_MODULE_NAME_ALREADY_EXISTS', { name: newClassName, moduleName: moduleName }); + } + if (!error) { clearError(); props.onAddNewItem();