Skip to content

Commit d0164f3

Browse files
authored
Merge pull request #79 from lizlooney/pr_extended_python_generator
Modified ExtendedPythonGenerator so it can safely be a singleton
2 parents ee263e7 + 0937da9 commit d0164f3

File tree

6 files changed

+213
-121
lines changed

6 files changed

+213
-121
lines changed

src/App.tsx

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ import { initialize as initializeGeneratedBlocks } from './blocks/utils/generate
4949

5050
import * as editor from './editor/editor';
5151
import { extendedPythonGenerator } from './editor/extended_python_generator';
52+
import { createGeneratorContext, GeneratorContext } from './editor/generator_context';
5253

5354
import * as toolboxItems from './toolbox/items';
5455
import * as toolbox from './toolbox/toolbox';
@@ -252,6 +253,7 @@ const App: React.FC = () => {
252253
const [deleteTooltip, setDeleteTooltip] = useState('Delete');
253254
const blocklyComponent = useRef<BlocklyComponentType | null>(null);
254255
const [triggerPythonRegeneration, setTriggerPythonRegeneration] = useState(0);
256+
const generatorContext = useRef<GeneratorContext | null>(null);
255257
const blocksEditor = useRef<editor.Editor | null>(null);
256258
const [generatedCode, setGeneratedCode] = useState('');
257259
const [newProjectNameModalPurpose, setNewProjectNameModalPurpose] = useState('');
@@ -461,6 +463,9 @@ const App: React.FC = () => {
461463
? commonStorage.findModule(modules, currentModulePath)
462464
: null;
463465
setCurrentModule(module);
466+
if (generatorContext.current) {
467+
generatorContext.current.setModule(module);
468+
}
464469

465470
if (module != null) {
466471
if (module.moduleType == commonStorage.MODULE_TYPE_PROJECT) {
@@ -498,10 +503,12 @@ const App: React.FC = () => {
498503
if (ignoreEffect()) {
499504
return;
500505
}
501-
if (blocklyComponent.current) {
506+
if (currentModule && blocklyComponent.current && generatorContext.current) {
502507
const blocklyWorkspace = blocklyComponent.current.getBlocklyWorkspace();
503-
extendedPythonGenerator.setCurrentModule(currentModule);
504-
setGeneratedCode(extendedPythonGenerator.workspaceToCode(blocklyWorkspace));
508+
setGeneratedCode(extendedPythonGenerator.workspaceToCode(
509+
blocklyWorkspace, generatorContext.current));
510+
} else {
511+
setGeneratedCode('');
505512
}
506513
}, [currentModule, triggerPythonRegeneration, blocklyComponent]);
507514

@@ -527,7 +534,8 @@ const App: React.FC = () => {
527534
blocklyWorkspace.addChangeListener(mutatorOpenListener);
528535
blocklyWorkspace.addChangeListener(handleBlocksChanged);
529536
}
530-
blocksEditor.current = new editor.Editor(blocklyWorkspace, storage);
537+
generatorContext.current = createGeneratorContext();
538+
blocksEditor.current = new editor.Editor(blocklyWorkspace, generatorContext.current, storage);
531539
}, [blocklyComponent, storage]);
532540

533541
const handleBlocksChanged = (event: Blockly.Events.Abstract) => {

src/blocks/mrc_class_method_def.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -389,7 +389,7 @@ export const pythonFromBlock = function (
389389
}
390390

391391
let params = block.mrcParameters;
392-
let paramString = "self"
392+
let paramString = "self";
393393
if (params.length != 0) {
394394
block.mrcParameters.forEach((param) => {
395395
paramString += ', ' + param.name;
@@ -409,7 +409,7 @@ export const pythonFromBlock = function (
409409
xfix2 +
410410
returnValue;
411411
code = generator.scrub_(block, code);
412-
generator.addMethod(funcName, code);
412+
generator.addClassMethodDefinition(block.getFieldValue('NAME'), funcName, code);
413413

414-
return code;
414+
return '';
415415
}

src/editor/editor.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import * as Blockly from 'blockly/core';
2323

2424
import { extendedPythonGenerator } from './extended_python_generator';
25+
import { GeneratorContext } from './generator_context';
2526
import * as commonStorage from '../storage/common_storage';
2627
import { getToolboxJSON } from '../toolbox/toolbox';
2728

@@ -33,6 +34,7 @@ const EMPTY_TOOLBOX: Blockly.utils.toolbox.ToolboxDefinition = {
3334

3435
export class Editor {
3536
private blocklyWorkspace: Blockly.WorkspaceSvg;
37+
private generatorContext: GeneratorContext;
3638
private storage: commonStorage.Storage;
3739
private currentModule: commonStorage.Module | null = null;
3840
private modulePath: string = '';
@@ -42,8 +44,9 @@ export class Editor {
4244
private bindedOnChange: any = null;
4345
private toolbox: Blockly.utils.toolbox.ToolboxDefinition = EMPTY_TOOLBOX;
4446

45-
constructor(blocklyWorkspace: Blockly.WorkspaceSvg, storage: commonStorage.Storage) {
47+
constructor(blocklyWorkspace: Blockly.WorkspaceSvg, generatorContext: GeneratorContext, storage: commonStorage.Storage) {
4648
this.blocklyWorkspace = blocklyWorkspace;
49+
this.generatorContext = generatorContext;
4750
this.storage = storage;
4851
}
4952

@@ -104,6 +107,7 @@ export class Editor {
104107
}
105108

106109
public async loadModuleBlocks(currentModule: commonStorage.Module | null) {
110+
this.generatorContext.setModule(currentModule);
107111
this.currentModule = currentModule;
108112
if (currentModule) {
109113
this.modulePath = currentModule.modulePath;
@@ -203,9 +207,8 @@ export class Editor {
203207
}
204208

205209
private getModuleContent(): string {
206-
extendedPythonGenerator.setCurrentModule(this.currentModule);
207-
const pythonCode = extendedPythonGenerator.workspaceToCode(this.blocklyWorkspace);
208-
const exportedBlocks = JSON.stringify(extendedPythonGenerator.getExportedBlocks(this.blocklyWorkspace));
210+
const pythonCode = extendedPythonGenerator.workspaceToCode(this.blocklyWorkspace, this.generatorContext);
211+
const exportedBlocks = JSON.stringify(this.generatorContext.getExportedBlocks());
209212
const blocksContent = JSON.stringify(Blockly.serialization.workspaces.save(this.blocklyWorkspace));
210213
return commonStorage.makeModuleContent(this.currentModule, pythonCode, exportedBlocks, blocksContent);
211214
}

src/editor/extended_python_generator.ts

Lines changed: 52 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -21,30 +21,71 @@
2121

2222
import * as Blockly from 'blockly/core';
2323
import { PythonGenerator } from 'blockly/python';
24-
import { Block } from "../toolbox/items";
24+
import { GeneratorContext } from './generator_context';
25+
import { Block } from '../toolbox/items';
2526
import { FunctionArg } from '../blocks/mrc_call_python_function';
2627
import * as commonStorage from '../storage/common_storage';
2728

2829
// Extends the python generator to collect some information about functions and
2930
// variables that have been defined so they can be used in other modules.
3031

3132
export class ExtendedPythonGenerator extends PythonGenerator {
32-
private currentModule: commonStorage.Module | null = null;
33-
private mapWorkspaceIdToExportedBlocks: { [key: string]: Block[] } = Object.create(null);
34-
protected methods_: {[key: string]: string} = Object.create(null);
33+
private workspace: Blockly.Workspace | null = null;
34+
private context: GeneratorContext | null = null;
3535

36+
private classMethods: {[key: string]: string} = Object.create(null);
3637

3738
constructor() {
3839
super('Python');
3940
}
4041

41-
setCurrentModule(module: commonStorage.Module | null) {
42-
this.currentModule = module;
42+
workspaceToCode(workspace: Blockly.Workspace, context: GeneratorContext): string {
43+
this.workspace = workspace;
44+
this.context = context;
45+
this.context.clear();
46+
47+
const code = super.workspaceToCode(workspace);
48+
49+
this.workspace = workspace;
50+
this.context = null;
51+
return code;
4352
}
4453

45-
init(workspace: Blockly.Workspace) {
46-
super.init(workspace);
54+
/**
55+
* Add an import statement for a python module.
56+
*/
57+
addImport(importModule: string): void {
58+
this.definitions_['import_' + importModule] = 'import ' + importModule;
59+
}
60+
61+
/**
62+
* Add a class method definition.
63+
*/
64+
addClassMethodDefinition(nameFieldValue: string, methodName: string, code: string): void {
65+
this.context.addClassMethodName(nameFieldValue, methodName);
66+
this.classMethods[methodName] = code;
67+
}
68+
69+
finish(code: string): string {
70+
if (this.context) {
71+
const className = this.context.getClassName();
72+
const classParent = this.context.getClassParent();
73+
this.addImport(classParent);
74+
const classDef = 'class ' + className + '(' + classParent + '):\n';
75+
const classMethods = [];
76+
for (let name in this.classMethods) {
77+
classMethods.push(this.classMethods[name])
78+
}
79+
this.classMethods = Object.create(null);
80+
code = classDef + this.prefixLines(classMethods.join('\n\n'), this.INDENT);
81+
82+
this.context.setExportedBlocks(this.produceExportedBlocks(this.workspace));
83+
}
84+
85+
return super.finish(code);
86+
}
4787

88+
private produceExportedBlocks(workspace: Blockly.Workspace): Block[] {
4889
// The exported blocks produced here have the extraState.importModule and fields.MODULE values
4990
// set to the MODULE_NAME_PLACEHOLDER. This is so blocks modules can be renamed and copied
5091
// without having to change the contents of the modules.
@@ -54,6 +95,8 @@ export class ExtendedPythonGenerator extends PythonGenerator {
5495
const exportedBlocks = [];
5596

5697
// All functions are exported.
98+
// TODO(lizlooney): instead of looking at procedure blocks, this code needs
99+
// to look at mrc_class_method_def blocks.
57100
const allProcedures = Blockly.Procedures.allProcedures(workspace);
58101
const procedureTuples = allProcedures[0].concat(allProcedures[1]);
59102
for (const procedureTuple of procedureTuples) {
@@ -149,74 +192,7 @@ export class ExtendedPythonGenerator extends PythonGenerator {
149192
exportedBlocks.push(setPythonModuleVariableBlock);
150193
}
151194
}
152-
this.mapWorkspaceIdToExportedBlocks[workspace.id] = exportedBlocks;
153-
}
154-
155-
getExportedBlocks(workspace: Blockly.Workspace): Block[] {
156-
return this.mapWorkspaceIdToExportedBlocks[workspace.id];
157-
}
158-
159-
// Functions used in python code generation for multiple python blocks.
160-
addImport(importModule: string): void {
161-
this.definitions_['import_' + importModule] = 'import ' + importModule;
162-
}
163-
addMethod(methodName: string, code : string): void {
164-
this.methods_[methodName] = code;
165-
}
166-
167-
classParentFromModuleType(moduleType : string) : string{
168-
if(moduleType == commonStorage.MODULE_TYPE_PROJECT){
169-
return "RobotBase";
170-
}
171-
if(moduleType == commonStorage.MODULE_TYPE_OPMODE){
172-
return "OpMode";
173-
}
174-
if(moduleType == commonStorage.MODULE_TYPE_MECHANISM){
175-
return "Mechanism";
176-
}
177-
return "";
178-
}
179-
180-
finish(code: string): string {
181-
if (!this.currentModule) {
182-
return super.finish(code);
183-
}
184-
let className = 'Robot'; // Default for Workspace
185-
if (this.currentModule.moduleType != commonStorage.MODULE_TYPE_WORKSPACE){
186-
className = this.currentModule.moduleName;
187-
}
188-
let classParent = this.classParentFromModuleType(this.currentModule.moduleType);
189-
this.addImport(classParent);
190-
191-
// Convert the definitions dictionary into a list.
192-
const imports = [];
193-
const definitions = [];
194-
195-
for (let name in this.definitions_) {
196-
const def = this.definitions_[name];
197-
if (def.match(/^(from\s+\S+\s+)?import\s+\S+/)) {
198-
imports.push(def);
199-
} else{
200-
definitions.push(def);
201-
}
202-
}
203-
const methods = [];
204-
for (let name in this.methods_){
205-
methods.push(this.methods_[name])
206-
}
207-
208-
this.definitions_ = Object.create(null);
209-
this.functionNames_ = Object.create(null);
210-
this.methods_ = Object.create(null);
211-
212-
this.isInitialized = false;
213-
214-
let class_def = "class " + className + "(" + classParent + "):\n";
215-
216-
this.nameDB_!.reset();
217-
const allDefs = imports.join('\n') + '\n\n' + definitions.join('\n\n');
218-
return allDefs.replace(/\n\n+/g, '\n\n').replace(/\n*$/, '\n\n\n') + class_def +
219-
this.prefixLines(methods.join('\n\n'), this.INDENT);
195+
return exportedBlocks;
220196
}
221197
}
222198

src/editor/generator_context.ts

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
/**
2+
* @license
3+
* Copyright 2025 Google LLC
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* https://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
/**
19+
* @author [email protected] (Liz Looney)
20+
*/
21+
22+
import { Block } from "../toolbox/items";
23+
import * as commonStorage from '../storage/common_storage';
24+
25+
26+
export function createGeneratorContext(): GeneratorContext {
27+
return new GeneratorContext();
28+
}
29+
30+
export class GeneratorContext {
31+
private module: commonStorage.Module | null = null;
32+
33+
// The exported blocks for the current module.
34+
private exportedBlocks: Block[] = [];
35+
36+
// Key is the mrc_class_method_def block's NAME field, value is the python method name.
37+
private classMethodNames: {[key: string]: string} = Object.create(null);
38+
39+
setModule(module: commonStorage.Module | null) {
40+
this.module = module;
41+
this.clear();
42+
}
43+
44+
clear(): void {
45+
this.clearExportedBlocks();
46+
this.clearClassMethodNames();
47+
}
48+
49+
getClassName(): string {
50+
if (this.module.moduleType === commonStorage.MODULE_TYPE_PROJECT) {
51+
return 'Robot';
52+
}
53+
54+
// TODO(lizlooney): className should be a field in commonStorage.Module.
55+
// Until that happens, we'll figure it out now from the module name.
56+
57+
let className = '';
58+
let nextCharUpper = true;
59+
for (let i = 0; i < this.module.moduleName.length; i++) {
60+
const char = this.module.moduleName.charAt(i);
61+
if (char !== '_') {
62+
className += nextCharUpper ? char.toUpperCase() : char;
63+
}
64+
nextCharUpper = (char === '_');
65+
}
66+
return className;
67+
}
68+
69+
getClassParent(): string {
70+
if (this.module.moduleType === commonStorage.MODULE_TYPE_PROJECT) {
71+
return 'RobotBase';
72+
}
73+
if (this.module.moduleType === commonStorage.MODULE_TYPE_OPMODE) {
74+
return 'OpMode';
75+
}
76+
if (this.module.moduleType === commonStorage.MODULE_TYPE_MECHANISM) {
77+
return 'Mechanism';
78+
}
79+
return '';
80+
}
81+
82+
clearExportedBlocks() {
83+
this.exportedBlocks.length = 0;
84+
}
85+
86+
setExportedBlocks(exportedBlocks: Blocks[]) {
87+
this.exportedBlocks.length = 0;
88+
this.exportedBlocks.push(...exportedBlocks);
89+
}
90+
91+
getExportedBlocks(): Block[] {
92+
return this.exportedBlocks;
93+
}
94+
95+
clearClassMethodNames() {
96+
this.classMethodNames = Object.create(null);
97+
}
98+
99+
addClassMethodName(nameFieldValue: string, methodName: string) {
100+
if (nameFieldValue !== methodName) {
101+
this.classMethodNames[nameFieldValue] = methodName;
102+
}
103+
}
104+
105+
getClassMethodName(nameFieldValue: string): string | null {
106+
if (this.classMethodNames[nameFieldValue]) {
107+
return this.classMethodNames[nameFieldValue];
108+
}
109+
return nameFieldValue;
110+
}
111+
}

0 commit comments

Comments
 (0)