├── .gitignore ├── LICENSE ├── README.md ├── ast2json.py ├── code-analyzer.js ├── code-generator.js ├── codegen-utils.js ├── main.js ├── menus └── menu.json ├── package.json ├── preferences └── preference.json ├── python-objects.js └── unittest-files ├── generate └── CodeGenTestModel.mdj └── reverse ├── classes.py ├── namespace ├── module.py ├── subnamespace1 │ └── module.py └── subnamespace2 │ └── module.py └── package ├── __init__.py ├── subpackage1 └── __init__.py └── subpackage2 └── __init__.py /.gitignore: -------------------------------------------------------------------------------- 1 | Thumbs.db 2 | 3 | # ignore node_modules created by grunt, but not more deeply-nested node_modules 4 | /node_modules 5 | /npm-debug.log 6 | 7 | #OSX .DS_Store files 8 | .DS_Store 9 | 10 | dev.txt 11 | .vscode 12 | *.code-workspace 13 | #IDE files 14 | .idea 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Minkyu Lee 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > # LOOKING FOR A NEW MAINTAINER 2 | > If you are interested to be a new maintainer, please create an issue with the title "Become a new maintainer". 3 | 4 | Python Extension for StarUML 5 | ============================ 6 | 7 | This extension for StarUML(http://staruml.io) support to generate Python code from UML model. Install this extension from Extension Manager of StarUML. 8 | 9 | Python Code Generation 10 | ---------------------- 11 | 12 | 1. Click the menu (`Tools > Python > Generate Code...`) 13 | 2. Select a base model (or package) that will be generated to Python. 14 | 3. Select a folder where generated Python source files (.py) will be placed. 15 | 16 | Belows are the rules to convert from UML model elements to Python source codes. 17 | 18 | ### UMLPackage 19 | 20 | * converted to a python _Package_ (as a folder with `__init__.py`). 21 | 22 | ### UMLClass, UMLInterface 23 | 24 | * converted to a python _Class_ definition as a separated module (`.py`). 25 | * Default constructor is generated (`def __init__(self):`) 26 | * `documentation` property to docstring 27 | 28 | ### UMLEnumeration 29 | 30 | * converted to a python class inherited from _Enum_ as a separated module (`.py`). 31 | * literals converted to class variables 32 | 33 | ### UMLAttribute, UMLAssociationEnd 34 | 35 | * converted to an instance variable if `isStatic` property is false, or a class variable if `isStatic` property is true 36 | * `name` property to identifier 37 | * `documentation` property to docstring 38 | * If `multiplicity` is one of `0..*`, `1..*`, `*`, then the variable will be initialized with `[]`. 39 | 40 | ### UMLOperation 41 | 42 | * converted to an instance method if `isStatic` property is false, or a class method (`@classmethod`) if `isStatic` property is true 43 | * `name` property to identifier 44 | * `documentation` property to docstring 45 | * _UMLParameter_ to method parameter 46 | 47 | ### UMLGeneralization, UMLInterfaceRealization 48 | 49 | * converted to inheritance 50 | 51 | -------------------------------------------------------------------------------- /ast2json.py: -------------------------------------------------------------------------------- 1 | import ast 2 | import sys 3 | import json 4 | 5 | def classname(cls): 6 | return cls.__class__.__name__ 7 | 8 | def jsonify_ast(node, level=0): 9 | """ 10 | https://github.com/maligree/python-ast-explorer/blob/master/parse.py 11 | """ 12 | fields = {} 13 | for k in node._fields: 14 | fields[k] = '...' 15 | v = getattr(node, k) 16 | if isinstance(v, ast.AST): 17 | if v._fields: 18 | fields[k] = jsonify_ast(v) 19 | else: 20 | fields[k] = classname(v) 21 | 22 | elif isinstance(v, list): 23 | fields[k] = [] 24 | for e in v: 25 | fields[k].append(jsonify_ast(e)) 26 | 27 | elif isinstance(v, str): 28 | fields[k] = v 29 | 30 | elif isinstance(v, int) or isinstance(v, float): 31 | fields[k] = v 32 | 33 | elif v is None: 34 | fields[k] = None 35 | 36 | else: 37 | fields[k] = 'unrecognized' 38 | 39 | ret = { classname(node): fields } 40 | return ret 41 | 42 | 43 | with open(sys.argv[1]) as py_file: 44 | code = py_file.read() 45 | tree = ast.parse(code) 46 | print(json.dumps(jsonify_ast(tree))) -------------------------------------------------------------------------------- /code-analyzer.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014-2018 MKLab. All rights reserved. 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a 5 | * copy of this software and associated documentation files (the "Software"), 6 | * to deal in the Software without restriction, including without limitation 7 | * the rights to use, copy, modify, merge, publish, distribute, sublicense, 8 | * and/or sell copies of the Software, and to permit persons to whom the 9 | * Software is furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in 12 | * all copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 19 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 20 | * DEALINGS IN THE SOFTWARE. 21 | */ 22 | 23 | const { 24 | PyObject, 25 | PyClass, 26 | PyPackage, 27 | PyFunction, 28 | PyModule, 29 | PyParameter, 30 | PyProperty, 31 | isPythonNamespace, 32 | isPythonPackage, 33 | } = require("./python-objects"); 34 | 35 | /** 36 | * Python Code Analyzer 37 | */ 38 | class PythonCodeAnalyzer { 39 | /** 40 | * @constructor 41 | * @param {Object} options 42 | */ 43 | constructor(options) { 44 | /** @member {type.UMLModel} */ 45 | var project = app.repository.select("@Project")[0]; 46 | this._root = app.factory.createModel({ 47 | id: "UMLModel", 48 | parent: project, 49 | modelInitializer: function (model) { 50 | model.name = "PythonReverse"; 51 | }, 52 | }); 53 | 54 | this._options = options; 55 | this._classes = []; 56 | } 57 | 58 | analyze(pth) { 59 | // Make sure the path is a Python package or namespace 60 | console.log(pth); 61 | 62 | if (isPythonPackage(pth) || isPythonNamespace(pth)) { 63 | var pyPackage = new PyPackage(pth, this._options); 64 | console.log(pyPackage); 65 | this.translatePyPackage(this._root, pyPackage); 66 | } 67 | 68 | // Load To Project 69 | var writer = new app.repository.Writer(); 70 | writer.writeObj("data", this._root); 71 | var json = writer.current.data; 72 | app.project.importFromJson(app.project.getProject(), json); 73 | 74 | // Generate Diagrams 75 | this.generateDiagrams(); 76 | console.log("[Python] done."); 77 | } 78 | 79 | /** 80 | * Generate Diagrams (Type Hierarchy, Package Structure, Package Overview) 81 | */ 82 | generateDiagrams() { 83 | var baseModel = app.repository.get(this._root._id); 84 | if (this._options.packageStructure) { 85 | app.commands.execute( 86 | "diagram-generator:package-structure", 87 | baseModel, 88 | true, 89 | ); 90 | } 91 | if (this._options.typeHierarchy) { 92 | app.commands.execute("diagram-generator:type-hierarchy", baseModel, true); 93 | } 94 | if (this._options.packageOverview) { 95 | baseModel.traverse((elem) => { 96 | if (elem instanceof type.UMLPackage) { 97 | app.commands.execute("diagram-generator:overview", elem, true); 98 | } 99 | }); 100 | } 101 | } 102 | 103 | /** 104 | * Translate Python package. 105 | * @param {type.Model} namespace 106 | * @param {PyPackage} pyPackage 107 | */ 108 | translatePyPackage(namespace, pyPackage) { 109 | var _package = app.factory.createModel({ 110 | id: "UMLPackage", 111 | parent: namespace, 112 | modelInitializer: function (model) { 113 | model.name = pyPackage.name; 114 | }, 115 | }); 116 | 117 | pyPackage.modules.forEach((module) => { 118 | this.translatePyModule(_package, module); 119 | }); 120 | pyPackage.packages.forEach((pkg) => { 121 | this.translatePyPackage(_package, pkg); 122 | }); 123 | } 124 | 125 | /** 126 | * Translate Python module. 127 | * @param {type.Model} namespace 128 | * @param {PyModule} pyModule 129 | */ 130 | translatePyModule(namespace, pyModule) { 131 | pyModule.classes.forEach((cls) => { 132 | this.translatePyClass(namespace, cls); 133 | }); 134 | } 135 | 136 | /** 137 | * Translate Python class. 138 | * @param {type.Model} namespace 139 | * @param {PyClass} pyClass 140 | */ 141 | translatePyClass(namespace, pyClass) { 142 | var _class = this._classes.find((value) => { 143 | return value.name == pyClass.getNameWithoutVisibility(); 144 | }); 145 | 146 | // If before the class was created earlier as a temporary parent - update it 147 | if (_class) { 148 | _class.parent = namespace; 149 | _class.name = pyClass.getNameWithoutVisibility(); 150 | _class.visibility = pyClass.getVisibility(); 151 | } else { 152 | // else - create 153 | _class = app.factory.createModel({ 154 | id: "UMLClass", 155 | parent: namespace, 156 | modelInitializer: function (model) { 157 | model.name = pyClass.getNameWithoutVisibility(); 158 | model.visibility = pyClass.getVisibility(); 159 | }, 160 | }); 161 | this._classes.push(_class); 162 | } 163 | 164 | // Create Generalizations 165 | pyClass.bases.forEach((base) => { 166 | var baseClassName = PyObject.prototype.getNameWithoutVisibility(base); 167 | 168 | var baseClass = this._classes.find((value) => { 169 | return value.name == baseClassName; 170 | }); 171 | 172 | if (!baseClass) { 173 | // create temp base class 174 | baseClass = app.factory.createModel({ 175 | id: "UMLClass", 176 | parent: namespace, 177 | modelInitializer: function (model) { 178 | model.name = baseClassName; 179 | model.documentation = "Class not found"; 180 | }, 181 | }); 182 | this._classes.push(baseClass); 183 | } 184 | 185 | var generalization = new type.UMLGeneralization(); 186 | generalization._parent = _class; 187 | generalization.source = _class; 188 | generalization.target = baseClass; 189 | _class.ownedElements.push(generalization); 190 | }); 191 | 192 | // Translate Members 193 | pyClass.methods.forEach((method) => { 194 | this.translateMethod(_class, method); 195 | }); 196 | 197 | pyClass.properties.forEach((property) => { 198 | this.translateClassProperty(_class, property); 199 | }); 200 | } 201 | 202 | /** 203 | * Translate Python Class property. 204 | * @param {type.Model} namespace 205 | * @param {PyProperty} pyProperty 206 | */ 207 | translateClassProperty(namespace, pyProperty) { 208 | // Create Attribute 209 | app.factory.createModel({ 210 | id: "UMLAttribute", 211 | parent: namespace, 212 | field: "attributes", 213 | modelInitializer: function (attribute) { 214 | attribute.name = pyProperty.getNameWithoutVisibility(); 215 | attribute.visibility = pyProperty.getVisibility(); 216 | if (pyProperty.default) attribute.defaultValue = pyProperty.default; 217 | if (pyProperty.type) attribute.type = pyProperty.type; 218 | attribute.isStatic = pyProperty.isStatic; 219 | }, 220 | }); 221 | } 222 | 223 | /** 224 | * Translate Python function 225 | * @param {type.Model} namespace 226 | * @param {PyFunction} pyFunction 227 | */ 228 | translateMethod(namespace, pyFunction) { 229 | if ( 230 | this._options.skipMagicMethods && 231 | pyFunction.isMagic && 232 | !pyFunction.isConstructor 233 | ) 234 | return; 235 | 236 | var _operation = app.factory.createModel({ 237 | id: "UMLOperation", 238 | parent: namespace, 239 | field: "operations", 240 | modelInitializer: function (operation) { 241 | operation.name = pyFunction.getNameWithoutVisibility(); 242 | 243 | // Modifiers 244 | operation.visibility = pyFunction.getVisibility(); 245 | operation.stereotype = pyFunction.stereotype; 246 | operation.isStatic = pyFunction.isStatic; 247 | }, 248 | }); 249 | 250 | // Return Type 251 | if (pyFunction.return) { 252 | app.factory.createModel({ 253 | id: "UMLParameter", 254 | parent: _operation, 255 | field: "parameters", 256 | modelInitializer: function (parameter) { 257 | parameter.name = "return"; 258 | parameter.type = pyFunction.return; 259 | parameter.direction = type.UMLParameter.DK_RETURN; 260 | }, 261 | }); 262 | } 263 | 264 | // Parameters 265 | pyFunction.parameters.forEach((parameter) => { 266 | this.translateParameter(_operation, parameter); 267 | }); 268 | } 269 | 270 | /** 271 | * Translate Function Parameters 272 | * @param {type.Model} namespace 273 | * @param {PyParameter} pyParameter 274 | */ 275 | translateParameter(namespace, pyParameter) { 276 | if (this._options.skipSelfParam && pyParameter.name == "self") return; 277 | 278 | app.factory.createModel({ 279 | id: "UMLParameter", 280 | parent: namespace, 281 | field: "parameters", 282 | modelInitializer: function (parameter) { 283 | parameter.name = pyParameter.name; 284 | parameter.type = pyParameter.type; 285 | if (pyParameter.default) parameter.defaultValue = pyParameter.default; 286 | }, 287 | }); 288 | } 289 | } 290 | 291 | exports.PythonCodeAnalyzer = PythonCodeAnalyzer; 292 | -------------------------------------------------------------------------------- /code-generator.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014-2018 MKLab. All rights reserved. 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a 5 | * copy of this software and associated documentation files (the "Software"), 6 | * to deal in the Software without restriction, including without limitation 7 | * the rights to use, copy, modify, merge, publish, distribute, sublicense, 8 | * and/or sell copies of the Software, and to permit persons to whom the 9 | * Software is furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in 12 | * all copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 19 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 20 | * DEALINGS IN THE SOFTWARE. 21 | * 22 | */ 23 | 24 | const fs = require("fs"); 25 | const path = require("path"); 26 | const codegen = require("./codegen-utils"); 27 | 28 | /** 29 | * Python Code Generator 30 | */ 31 | class PythonCodeGenerator { 32 | /** 33 | * @constructor 34 | * 35 | * @param {type.UMLPackage} baseModel 36 | * @param {string} basePath generated files and directories to be placed 37 | */ 38 | constructor(baseModel, basePath) { 39 | /** @member {type.Model} */ 40 | this.baseModel = baseModel; 41 | 42 | /** @member {string} */ 43 | this.basePath = basePath; 44 | } 45 | 46 | /** 47 | * Return Indent String based on options 48 | * @param {Object} options 49 | * @return {string} 50 | */ 51 | getIndentString(options) { 52 | if (options.useTab) { 53 | return "\t"; 54 | } else { 55 | var i, len; 56 | var indent = []; 57 | for (i = 0, len = options.indentSpaces; i < len; i++) { 58 | indent.push(" "); 59 | } 60 | return indent.join(""); 61 | } 62 | } 63 | 64 | /** 65 | * Collect inheritances (super classes or interfaces) of a given element 66 | * @param {type.Model} elem 67 | * @return {Array.} 68 | */ 69 | getInherits(elem) { 70 | var inherits = app.repository.getRelationshipsOf(elem, function (rel) { 71 | return ( 72 | rel.source === elem && 73 | (rel instanceof type.UMLGeneralization || 74 | rel instanceof type.UMLInterfaceRealization) 75 | ); 76 | }); 77 | return inherits.map(function (gen) { 78 | return gen.target; 79 | }); 80 | } 81 | 82 | /** 83 | * Write Doc 84 | * @param {StringWriter} codeWriter 85 | * @param {string} text 86 | * @param {Object} options 87 | */ 88 | writeDoc(codeWriter, text, options) { 89 | var i, len, lines; 90 | if (options.docString && text.trim().length > 0) { 91 | lines = text.trim().split("\n"); 92 | if (lines.length > 1) { 93 | codeWriter.writeLine('"""'); 94 | for (i = 0, len = lines.length; i < len; i++) { 95 | codeWriter.writeLine(lines[i]); 96 | } 97 | codeWriter.writeLine('"""'); 98 | } else { 99 | codeWriter.writeLine('"""' + lines[0] + '"""'); 100 | } 101 | } 102 | } 103 | 104 | /** 105 | * Write Variable 106 | * @param {StringWriter} codeWriter 107 | * @param {type.Model} elem 108 | * @param {Object} options 109 | */ 110 | writeVariable(codeWriter, elem, options, isClassVar) { 111 | if (elem.name.length > 0) { 112 | var line; 113 | if (isClassVar) { 114 | line = elem.name; 115 | } else { 116 | line = "self." + elem.name; 117 | } 118 | if ( 119 | elem.multiplicity && 120 | ["0..*", "1..*", "*"].includes(elem.multiplicity.trim()) 121 | ) { 122 | line += " = []"; 123 | } else if (elem.defaultValue && elem.defaultValue.length > 0) { 124 | line += " = " + elem.defaultValue; 125 | } else { 126 | line += " = None"; 127 | } 128 | codeWriter.writeLine(line); 129 | } 130 | } 131 | 132 | /** 133 | * Write Constructor 134 | * @param {StringWriter} codeWriter 135 | * @param {type.Model} elem 136 | * @param {Object} options 137 | */ 138 | writeConstructor(codeWriter, elem, options) { 139 | var self = this; 140 | var hasBody = false; 141 | codeWriter.writeLine("def __init__(self):"); 142 | codeWriter.indent(); 143 | 144 | // from attributes 145 | if (elem.attributes.length > 0) { 146 | elem.attributes.forEach(function (attr) { 147 | if (attr.isStatic === false) { 148 | self.writeVariable(codeWriter, attr, options, false); 149 | hasBody = true; 150 | } 151 | }); 152 | } 153 | 154 | // from associations 155 | var associations = app.repository.getRelationshipsOf(elem, function (rel) { 156 | return rel instanceof type.UMLAssociation; 157 | }); 158 | for (var i = 0, len = associations.length; i < len; i++) { 159 | var asso = associations[i]; 160 | if (asso.end1.reference === elem && asso.end2.navigable === true) { 161 | self.writeVariable(codeWriter, asso.end2, options); 162 | hasBody = true; 163 | } 164 | if (asso.end2.reference === elem && asso.end1.navigable === true) { 165 | self.writeVariable(codeWriter, asso.end1, options); 166 | hasBody = true; 167 | } 168 | } 169 | 170 | if (!hasBody) { 171 | codeWriter.writeLine("pass"); 172 | } 173 | 174 | codeWriter.outdent(); 175 | codeWriter.writeLine(); 176 | } 177 | 178 | /** 179 | * Write Method 180 | * @param {StringWriter} codeWriter 181 | * @param {type.Model} elem 182 | * @param {Object} options 183 | * @param {boolean} skipBody 184 | * @param {boolean} skipParams 185 | */ 186 | writeMethod(codeWriter, elem, options) { 187 | if (elem.name.length > 0) { 188 | // name 189 | var line = "def " + elem.name; 190 | 191 | // params 192 | var params = elem.getNonReturnParameters(); 193 | var paramStr = params 194 | .map(function (p) { 195 | return p.name; 196 | }) 197 | .join(", "); 198 | 199 | if (elem.isStatic) { 200 | codeWriter.writeLine("@classmethod"); 201 | codeWriter.writeLine(line + "(cls, " + paramStr + "):"); 202 | } else { 203 | codeWriter.writeLine(line + "(self, " + paramStr + "):"); 204 | } 205 | codeWriter.indent(); 206 | this.writeDoc(codeWriter, elem.documentation, options); 207 | codeWriter.writeLine("pass"); 208 | codeWriter.outdent(); 209 | codeWriter.writeLine(); 210 | } 211 | } 212 | 213 | /** 214 | * Write Enum 215 | * @param {StringWriter} codeWriter 216 | * @param {type.Model} elem 217 | * @param {Object} options 218 | */ 219 | writeEnum(codeWriter, elem, options) { 220 | var line = ""; 221 | 222 | codeWriter.writeLine("from enum import Enum"); 223 | codeWriter.writeLine(); 224 | 225 | // Enum 226 | line = "class " + elem.name + "(Enum):"; 227 | codeWriter.writeLine(line); 228 | codeWriter.indent(); 229 | 230 | // Docstring 231 | this.writeDoc(codeWriter, elem.documentation, options); 232 | 233 | if (elem.literals.length === 0) { 234 | codeWriter.writeLine("pass"); 235 | } else { 236 | for (var i = 0, len = elem.literals.length; i < len; i++) { 237 | codeWriter.writeLine(elem.literals[i].name + " = " + (i + 1)); 238 | } 239 | } 240 | codeWriter.outdent(); 241 | codeWriter.writeLine(); 242 | } 243 | 244 | /** 245 | * Write Class 246 | * @param {StringWriter} codeWriter 247 | * @param {type.Model} elem 248 | * @param {Object} options 249 | */ 250 | writeClass(codeWriter, elem, options) { 251 | var self = this; 252 | var line = ""; 253 | var _inherits = this.getInherits(elem); 254 | 255 | // Import 256 | if (_inherits.length > 0) { 257 | _inherits.forEach(function (e) { 258 | var _path = e 259 | .getPath(self.baseModel) 260 | .map(function (item) { 261 | return options.lowerCase ? item.name.toLowerCase() : item.name; 262 | }) 263 | .join("."); 264 | codeWriter.writeLine("from " + _path + " import " + e.name); 265 | }); 266 | codeWriter.writeLine(); 267 | codeWriter.writeLine(); 268 | } 269 | 270 | // Class 271 | line = "class " + elem.name; 272 | 273 | // Inherits 274 | if (_inherits.length > 0) { 275 | line += 276 | "(" + 277 | _inherits 278 | .map(function (e) { 279 | return e.name; 280 | }) 281 | .join(", ") + 282 | ")"; 283 | } 284 | 285 | codeWriter.writeLine(line + ":"); 286 | codeWriter.indent(); 287 | 288 | // Docstring 289 | this.writeDoc(codeWriter, elem.documentation, options); 290 | 291 | if (elem.attributes.length === 0 && elem.operations.length === 0) { 292 | codeWriter.writeLine("pass"); 293 | } else { 294 | // Class Variable 295 | var hasClassVar = false; 296 | elem.attributes.forEach(function (attr) { 297 | if (attr.isStatic) { 298 | self.writeVariable(codeWriter, attr, options, true); 299 | hasClassVar = true; 300 | } 301 | }); 302 | if (hasClassVar) { 303 | codeWriter.writeLine(); 304 | } 305 | 306 | // Constructor 307 | this.writeConstructor(codeWriter, elem, options); 308 | 309 | // Methods 310 | if (elem.operations.length > 0) { 311 | elem.operations.forEach(function (op) { 312 | self.writeMethod(codeWriter, op, options); 313 | }); 314 | } 315 | } 316 | codeWriter.outdent(); 317 | } 318 | 319 | /** 320 | * Generate codes from a given element 321 | * @param {type.Model} elem 322 | * @param {string} path 323 | * @param {Object} options 324 | */ 325 | generate(elem, basePath, options) { 326 | var result = new $.Deferred(); 327 | var fullPath, codeWriter, file; 328 | 329 | // Package (a directory with __init__.py) 330 | if (elem instanceof type.UMLPackage) { 331 | fullPath = path.join(basePath, elem.name); 332 | fs.mkdirSync(fullPath); 333 | file = path.join(fullPath, "__init__.py"); 334 | fs.writeFileSync(file, ""); 335 | elem.ownedElements.forEach((child) => { 336 | this.generate(child, fullPath, options); 337 | }); 338 | 339 | // Class 340 | } else if ( 341 | elem instanceof type.UMLClass || 342 | elem instanceof type.UMLInterface 343 | ) { 344 | fullPath = 345 | basePath + 346 | "/" + 347 | (options.lowerCase 348 | ? codegen.toCamelCase(elem.name.toLowerCase()) 349 | : codegen.toCamelCase(elem.name)) + 350 | ".py"; 351 | codeWriter = new codegen.CodeWriter(this.getIndentString(options)); 352 | codeWriter.writeLine(options.installPath); 353 | codeWriter.writeLine("# -*- coding: utf-8 -*-"); 354 | codeWriter.writeLine(); 355 | this.writeClass(codeWriter, elem, options); 356 | fs.writeFileSync(fullPath, codeWriter.getData()); 357 | 358 | // Enum 359 | } else if (elem instanceof type.UMLEnumeration) { 360 | fullPath = 361 | basePath + 362 | "/" + 363 | (options.lowerCase ? elem.name.toLowerCase() : elem.name) + 364 | ".py"; 365 | codeWriter = new codegen.CodeWriter(this.getIndentString(options)); 366 | codeWriter.writeLine(options.installPath); 367 | codeWriter.writeLine("# -*- coding: utf-8 -*-"); 368 | codeWriter.writeLine(); 369 | this.writeEnum(codeWriter, elem, options); 370 | fs.writeFileSync(fullPath, codeWriter.getData()); 371 | 372 | // Others (Nothing generated.) 373 | } else { 374 | result.resolve(); 375 | } 376 | return result.promise(); 377 | } 378 | } 379 | 380 | /** 381 | * Generate 382 | * @param {type.Model} baseModel 383 | * @param {string} basePath 384 | * @param {Object} options 385 | */ 386 | function generate(baseModel, basePath, options) { 387 | var fullPath; 388 | var pythonCodeGenerator = new PythonCodeGenerator(baseModel, basePath); 389 | fullPath = basePath + "/" + baseModel.name; 390 | fs.mkdirSync(fullPath); 391 | baseModel.ownedElements.forEach((child) => { 392 | pythonCodeGenerator.generate(child, fullPath, options); 393 | }); 394 | } 395 | 396 | exports.generate = generate; 397 | -------------------------------------------------------------------------------- /codegen-utils.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014-2018 MKLab. All rights reserved. 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a 5 | * copy of this software and associated documentation files (the "Software"), 6 | * to deal in the Software without restriction, including without limitation 7 | * the rights to use, copy, modify, merge, publish, distribute, sublicense, 8 | * and/or sell copies of the Software, and to permit persons to whom the 9 | * Software is furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in 12 | * all copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 19 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 20 | * DEALINGS IN THE SOFTWARE. 21 | * 22 | */ 23 | 24 | /** 25 | * CodeWriter 26 | */ 27 | class CodeWriter { 28 | /** 29 | * @constructor 30 | */ 31 | constructor(indentString) { 32 | /** @member {Array.} lines */ 33 | this.lines = []; 34 | 35 | /** @member {string} indentString */ 36 | this.indentString = indentString || " "; // default 4 spaces 37 | 38 | /** @member {Array.} indentations */ 39 | this.indentations = []; 40 | } 41 | 42 | /** 43 | * Indent 44 | */ 45 | indent() { 46 | this.indentations.push(this.indentString); 47 | } 48 | 49 | /** 50 | * Outdent 51 | */ 52 | outdent() { 53 | this.indentations.splice(this.indentations.length - 1, 1); 54 | } 55 | 56 | /** 57 | * Write a line 58 | * @param {string} line 59 | */ 60 | writeLine(line) { 61 | if (line) { 62 | this.lines.push(this.indentations.join("") + line); 63 | } else { 64 | this.lines.push(""); 65 | } 66 | } 67 | 68 | /** 69 | * Return as all string data 70 | * @return {string} 71 | */ 72 | getData() { 73 | return this.lines.join("\n"); 74 | } 75 | } 76 | 77 | exports.CodeWriter = CodeWriter; 78 | 79 | /** 80 | * Return as camelCase the given string 81 | * @param {string} name 82 | * @return {string} camelCase name 83 | */ 84 | function toCamelCase(name) { 85 | return name.replace(/(?:^\w|[A-Z]|\b\w|\s+)/g, function (match, index) { 86 | if (+match === 0) return ""; 87 | return index === 0 ? match.toLowerCase() : match.toUpperCase(); 88 | }); 89 | } 90 | 91 | exports.toCamelCase = toCamelCase; 92 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 MKLab. All rights reserved. 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a 5 | * copy of this software and associated documentation files (the "Software"), 6 | * to deal in the Software without restriction, including without limitation 7 | * the rights to use, copy, modify, merge, publish, distribute, sublicense, 8 | * and/or sell copies of the Software, and to permit persons to whom the 9 | * Software is furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in 12 | * all copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 19 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 20 | * DEALINGS IN THE SOFTWARE. 21 | * 22 | */ 23 | 24 | const codeGenerator = require("./code-generator"); 25 | const { PythonCodeAnalyzer } = require("./code-analyzer"); 26 | 27 | function getGenOptions() { 28 | return { 29 | installPath: app.preferences.get("python.gen.installPath"), 30 | useTab: app.preferences.get("python.gen.useTab"), 31 | indentSpaces: app.preferences.get("python.gen.indentSpaces"), 32 | docString: app.preferences.get("python.gen.docString"), 33 | lowerCase: app.preferences.get("python.gen.lowerCase"), 34 | }; 35 | } 36 | 37 | function getRevOptions() { 38 | return { 39 | skipSelfParam: app.preferences.get("python.rev.skipSelfParam"), 40 | skipMagicMethods: app.preferences.get("python.rev.skipMagicMethods"), 41 | pythonPath: app.preferences.get("python.rev.PythonPath"), 42 | typeHierarchy: app.preferences.get("java.rev.typeHierarchy"), 43 | packageOverview: app.preferences.get("java.rev.packageOverview"), 44 | packageStructure: app.preferences.get("java.rev.packageStructure"), 45 | }; 46 | } 47 | 48 | /** 49 | * Command Handler for Python Code Generation 50 | * 51 | * @param {Element} base 52 | * @param {string} path 53 | * @param {Object} options 54 | */ 55 | async function _handleGenerate(base, path, options) { 56 | // If options is not passed, get from preference 57 | options = options || getGenOptions(); 58 | // If base is not assigned, popup ElementPicker 59 | if (!base) { 60 | app.elementPickerDialog 61 | .showDialog( 62 | "Select a base model to generate codes", 63 | null, 64 | type.UMLPackage, 65 | ) 66 | .then(async function ({ buttonId, returnValue }) { 67 | if (buttonId === "ok") { 68 | base = returnValue; 69 | // If path is not assigned, popup Open Dialog to select a folder 70 | if (!path) { 71 | var files = await app.dialogs.showOpenDialogAsync( 72 | "Select a folder where generated codes to be located", 73 | null, 74 | null, 75 | { properties: ["openDirectory"] }, 76 | ); 77 | if (files && files.length > 0) { 78 | path = files[0]; 79 | codeGenerator.generate(base, path, options); 80 | } 81 | } else { 82 | codeGenerator.generate(base, path, options); 83 | } 84 | } 85 | }); 86 | } else { 87 | // If path is not assigned, popup Open Dialog to select a folder 88 | if (!path) { 89 | var files = await app.dialogs.showOpenDialogAsync( 90 | "Select a folder where generated codes to be located", 91 | null, 92 | null, 93 | { properties: ["openDirectory"] }, 94 | ); 95 | if (files && files.length > 0) { 96 | path = files[0]; 97 | codeGenerator.generate(base, path, options); 98 | } 99 | } else { 100 | codeGenerator.generate(base, path, options); 101 | } 102 | } 103 | } 104 | 105 | /** 106 | * Popup PreferenceDialog with Python Preference Schema 107 | */ 108 | function _handleConfigure() { 109 | app.commands.execute("application:preferences", "python"); 110 | } 111 | 112 | /** 113 | * Command Handler for Python Reverse 114 | * 115 | * @param {string} basePath 116 | * @param {Object} options 117 | */ 118 | async function _handleReverse(basePath, options) { 119 | // If options is not passed, get from preference 120 | options = options || getRevOptions(); 121 | 122 | // If basePath is not assigned, popup Open Dialog to select a folder 123 | if (!basePath) { 124 | var files = await app.dialogs.showOpenDialogAsync( 125 | "Select Folder", 126 | null, 127 | null, 128 | { 129 | properties: ["openDirectory"], 130 | }, 131 | ); 132 | if (files && files.length > 0) { 133 | basePath = files[0]; 134 | } 135 | } 136 | 137 | var pythonCodeAnalyzer = new PythonCodeAnalyzer(options); 138 | pythonCodeAnalyzer.analyze(basePath); 139 | } 140 | 141 | function init() { 142 | app.commands.register("python:generate", _handleGenerate); 143 | app.commands.register("python:reverse", _handleReverse); 144 | app.commands.register("python:configure", _handleConfigure); 145 | } 146 | 147 | exports.init = init; 148 | -------------------------------------------------------------------------------- /menus/menu.json: -------------------------------------------------------------------------------- 1 | { 2 | "menu": [ 3 | { 4 | "id": "tools", 5 | "submenu": [ 6 | { 7 | "label": "Python", 8 | "id": "tools.python", 9 | "submenu": [ 10 | { "label": "Generate Code...", "id": "tools.python.generate", "command": "python:generate" }, 11 | { "label": "Reverse Code...", "id": "tools.python.reverse", "command": "python:reverse" }, 12 | { "type": "separator" }, 13 | { "label": "Configure...", "id": "tools.python.configure", "command": "python:configure" } 14 | ] 15 | } 16 | ] 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "niklauslee.staruml-python", 3 | "title": "Python", 4 | "description": "Python code generator and reverse engineering.", 5 | "homepage": "https://github.com/niklauslee/staruml-python", 6 | "issues": "https://github.com/niklauslee/staruml-python/issues", 7 | "keywords": [ 8 | "python" 9 | ], 10 | "version": "0.9.2", 11 | "author": { 12 | "name": "Minkyu Lee", 13 | "email": "niklaus.lee@gmail.com", 14 | "url": "https://github.com/niklauslee" 15 | }, 16 | "license": "MIT", 17 | "engines": { 18 | "staruml": "^6.0.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /preferences/preference.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "python", 3 | "name": "Python", 4 | "schema": { 5 | "python.gen": { 6 | "text": "Python Code Generation", 7 | "type": "section" 8 | }, 9 | "python.gen.installPath": { 10 | "text": "Installation Path", 11 | "description": "Installation path of python", 12 | "type": "string", 13 | "default": "#!/usr/bin/python" 14 | }, 15 | "python.gen.useTab": { 16 | "text": "Use Tab", 17 | "description": "Use Tab for indentation instead of spaces.", 18 | "type": "check", 19 | "default": false 20 | }, 21 | "python.gen.indentSpaces": { 22 | "text": "Indent Spaces", 23 | "description": "Number of spaces for indentation.", 24 | "type": "number", 25 | "default": 4 26 | }, 27 | "python.gen.docString": { 28 | "text": "Docstring", 29 | "description": "Generate docstrings.", 30 | "type": "check", 31 | "default": true 32 | }, 33 | "python.rev": { 34 | "text": "Python Reverse Engineering", 35 | "type": "section" 36 | }, 37 | "python.rev.PythonPath": { 38 | "text": "Pyton Installation Path", 39 | "description": "Installation path of python", 40 | "type": "string", 41 | "default": "/usr/bin/python" 42 | }, 43 | "python.rev.skipSelfParam": { 44 | "text": "Skip self parameter", 45 | "description": "Don't show self parameter in methods.", 46 | "type": "check", 47 | "default": true 48 | }, 49 | "python.rev.skipMagicMethods": { 50 | "text": "Skip Magic Methods", 51 | "description": "Don't show Python's magic methods. Like __repr__, __hash__, etc.", 52 | "type": "check", 53 | "default": true 54 | }, 55 | "python.rev.typeHierarchy": { 56 | "text": "Type Hierarchy Diagram", 57 | "description": "Create a type hierarchy diagram for all classes and interfaces", 58 | "type": "check", 59 | "default": true 60 | }, 61 | "python.rev.packageOverview": { 62 | "text": "Package Overview Diagram", 63 | "description": "Create overview diagram for each package", 64 | "type": "check", 65 | "default": true 66 | }, 67 | "python.rev.packageStructure": { 68 | "text": "Package Structure Diagram", 69 | "description": "Create a package structure diagram for all packages", 70 | "type": "check", 71 | "default": true 72 | }, 73 | "python.gen.lowerCase": { 74 | "text": "Lower Case Filenames", 75 | "description": "Make filenames lower case, according to PEP8 recommendation.", 76 | "type": "check", 77 | "default": false 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /python-objects.js: -------------------------------------------------------------------------------- 1 | const { execSync } = require("child_process"); 2 | const fs = require("fs"); 3 | const path = require("path"); 4 | 5 | class PyObject { 6 | constructor(ctx) { 7 | this._ctx = ctx; 8 | this.name = this._getName(); 9 | } 10 | 11 | _getName() { 12 | return this._ctx.name; 13 | } 14 | 15 | /** 16 | * Return visiblity from node name 17 | * 18 | * @return {string} Visibility constants for UML Elements 19 | */ 20 | getVisibility() { 21 | if (this.name.startsWith("__")) { 22 | return type.UMLModelElement.VK_PRIVATE; 23 | } else if (this.name.startsWith("_")) { 24 | return type.UMLModelElement.VK_PROTECTED; 25 | } 26 | 27 | return type.UMLModelElement.VK_PUBLIC; 28 | } 29 | 30 | /** 31 | * Return name without visibility from object node 32 | * 33 | * @return {string} Object name 34 | */ 35 | getNameWithoutVisibility(name = this.name) { 36 | if (name.startsWith("__") && name.endsWith("__")) { 37 | return name; 38 | } else if (name.startsWith("__")) { 39 | name = name.substring(2); 40 | } else if (name.startsWith("_")) { 41 | name = name.substring(1); 42 | } 43 | 44 | return name; 45 | } 46 | } 47 | exports.PyObject = PyObject; 48 | 49 | class PyParameter extends PyObject { 50 | constructor(ctx) { 51 | super(ctx); 52 | this.type = this._getType(); 53 | this.default = this._getDefault(); 54 | } 55 | 56 | _getDefault() {} 57 | 58 | _getName() { 59 | return this._ctx.arg.arg; 60 | } 61 | 62 | _getType() { 63 | if (this._ctx.arg.annotation) return this._ctx.arg.annotation.Name.id; 64 | } 65 | } 66 | exports.PyParameter = PyParameter; 67 | 68 | class PyProperty extends PyParameter { 69 | constructor(ctx) { 70 | super(ctx); 71 | this.isStatic = false; 72 | } 73 | _getDefault() { 74 | if (this._ctx.value) { 75 | if (this._ctx.value.Name) { 76 | return this._ctx.value.Name.id.toString(); 77 | } else { 78 | if (this._ctx.value.Constant && this._ctx.value.Constant.value) 79 | return this._ctx.value.Constant.value.toString(); 80 | } 81 | } 82 | } 83 | 84 | _getName() { 85 | if (this._ctx.target) { 86 | if (Object.keys(this._ctx.target) == "Attribute") { 87 | return this._ctx.target.Attribute.attr; 88 | } else { 89 | return this._ctx.target.Name.id; 90 | } 91 | } else if (this._ctx.targets) { 92 | if (Object.keys(this._ctx.targets[0]) == "Attribute") { 93 | return this._ctx.targets[0].Attribute.attr; 94 | } else { 95 | return this._ctx.targets[0].Name.id; 96 | } 97 | } 98 | } 99 | 100 | _getType() { 101 | if (this._ctx.annotation) return this._ctx.annotation.Name.id; 102 | } 103 | } 104 | exports.PyProperty = PyProperty; 105 | 106 | class PyCallable extends PyObject { 107 | constructor(ctx) { 108 | super(ctx); 109 | this.parameters = this._getParameters(); 110 | this.return = this._getReturn(); 111 | } 112 | 113 | _getReturn() { 114 | if (this._ctx.returns) { 115 | if (this._ctx.returns.Name) { 116 | return this._ctx.returns.Name.id.toString(); 117 | } else { 118 | if (this._ctx.returns.Constant && this._ctx.returns.Constant.value) 119 | return this._ctx.returns.Constant.value.toString(); 120 | } 121 | } 122 | 123 | return null; 124 | } 125 | 126 | _getParameters() { 127 | var parameters = []; 128 | 129 | if (this._ctx.args) { 130 | var count = 131 | this._ctx.args.arguments.args.length - 132 | this._ctx.args.arguments.defaults.length; 133 | 134 | this._ctx.args.arguments.args.forEach((element, index) => { 135 | var parameter = new PyParameter(element); 136 | if (index - count >= 0) { 137 | parameter.default = 138 | this._ctx.args.arguments.defaults[index - count].Constant.value; 139 | } 140 | parameters.push(parameter); 141 | }); 142 | } 143 | 144 | return parameters; 145 | } 146 | } 147 | 148 | class PyFunction extends PyCallable { 149 | constructor(ctx) { 150 | super(ctx); 151 | this.objectProperties = this._getObjectProperties(); 152 | this.stereotype = this._getStereotype(); 153 | this.isStatic = this._isStatic(); 154 | this.isConstructor = this._isConstructor(); 155 | this.isMagic = this._isMagic(); 156 | } 157 | 158 | _isStatic() { 159 | return Boolean( 160 | this._ctx?.decorator_list?.find( 161 | (decorator) => decorator?.Name?.id == "staticmethod", 162 | ), 163 | ); 164 | } 165 | 166 | _getStereotype() { 167 | var stereotype = null; 168 | 169 | if (this.name == "__init__") return "constructor"; 170 | 171 | this._ctx.decorator_list?.every((decorator) => { 172 | if (decorator?.Name?.id == "property") { 173 | stereotype = "property"; 174 | return false; 175 | } 176 | if (decorator?.Name?.id == "classmethod") { 177 | stereotype = "constructor"; 178 | return false; 179 | } 180 | 181 | return true; 182 | }); 183 | 184 | return stereotype; 185 | } 186 | 187 | _getObjectProperties() { 188 | var objectProperties = []; 189 | 190 | this._ctx.body.forEach((element) => { 191 | if (Object.keys(element) == "Assign") { 192 | var property = new PyProperty(element.Assign); 193 | objectProperties.push(property); 194 | } else if (Object.keys(element) == "AnnAssign") { 195 | var property = new PyProperty(element.AnnAssign); 196 | objectProperties.push(property); 197 | } 198 | }); 199 | 200 | return objectProperties; 201 | } 202 | 203 | _isConstructor() { 204 | return this.stereotype == "constructor"; 205 | } 206 | 207 | _isMagic() { 208 | return this.name.startsWith("__") && this.name.endsWith("__"); 209 | } 210 | 211 | getVisibility() { 212 | return this.isConstructor 213 | ? type.UMLModelElement.VK_PUBLIC 214 | : PyCallable.prototype.getVisibility.call(this); 215 | } 216 | } 217 | exports.PyFunction = PyFunction; 218 | 219 | class PyClass extends PyObject { 220 | constructor(ctx) { 221 | super(ctx); 222 | console.log(ctx); 223 | this.properties = this._getProperties(); 224 | this.methods = this._getMethods(); 225 | this.bases = this._getBases(); 226 | } 227 | 228 | _getBases() { 229 | var bases = []; 230 | 231 | this._ctx.bases.forEach((element) => { 232 | bases.push(element.Name.id.toString()); 233 | }); 234 | 235 | return bases; 236 | } 237 | 238 | _getMethods() { 239 | var functions = []; 240 | 241 | this._ctx.body.forEach((element) => { 242 | if (Object.keys(element) == "FunctionDef") { 243 | var pyFunction = new PyFunction(element.FunctionDef); 244 | functions.push(pyFunction); 245 | if (pyFunction.isConstructor) { 246 | this.properties = this.properties.concat(pyFunction.objectProperties); 247 | } 248 | } 249 | }); 250 | 251 | return functions; 252 | } 253 | 254 | _getProperties(ctx) { 255 | var properties = []; 256 | 257 | this._ctx.body.forEach((element) => { 258 | if (Object.keys(element) == "Assign") { 259 | var property = new PyProperty(element.Assign); 260 | property.isStatic = true; 261 | properties.push(property); 262 | } else if (Object.keys(element) == "AnnAssign") { 263 | var property = new PyProperty(element.AnnAssign); 264 | property.isStatic = true; 265 | properties.push(property); 266 | } 267 | }); 268 | 269 | return properties; 270 | } 271 | } 272 | exports.PyClass = PyClass; 273 | 274 | class PyModule extends PyObject { 275 | constructor(ctx) { 276 | super(ctx); 277 | this.classes = this._getClasses(ctx.Module.body); 278 | } 279 | 280 | _getClasses(ctx) { 281 | var classes = []; 282 | 283 | ctx.forEach((element) => { 284 | if (Object.keys(element) == "ClassDef") { 285 | classes.push(new PyClass(element.ClassDef)); 286 | } 287 | }); 288 | 289 | return classes; 290 | } 291 | } 292 | exports.PyModule = PyModule; 293 | 294 | class PyPackage { 295 | constructor(path, options) { 296 | this.name = this._getName(path); 297 | this.modules = []; 298 | this.packages = []; 299 | this._options = options; 300 | 301 | var files = fs.readdirSync(path); 302 | if (files && files.length > 0) { 303 | files.forEach((entry) => { 304 | var fullPath = path + "/" + entry; 305 | this._translatePackagesAndModules(fullPath); 306 | }); 307 | } 308 | } 309 | 310 | _getName(path) { 311 | var packagePath = path.split("/"); 312 | var name = packagePath[packagePath.length - 1]; 313 | 314 | return name; 315 | } 316 | 317 | _translatePackagesAndModules(pth) { 318 | var stat = fs.lstatSync(pth); 319 | if (stat.isFile()) { 320 | var ext = path.extname(pth).toLowerCase(); 321 | if (ext === ".py") { 322 | try { 323 | var result = execSync( 324 | `${this._options.pythonPath} "${path.join( 325 | __dirname, 326 | "ast2json.py", 327 | )}" "${pth}"`, 328 | ); 329 | 330 | var astJson = JSON.parse(result.toString()); 331 | 332 | var pyModule = new PyModule(astJson); 333 | 334 | this.modules.push(pyModule); 335 | } catch (ex) { 336 | console.error("[Python] Failed to parse - " + pth); 337 | console.error(ex); 338 | } 339 | } 340 | } else if (stat.isDirectory()) { 341 | if (isPythonPackage(pth) || isPythonNamespace(pth)) { 342 | var pyPackage = new PyPackage(pth, this._options); 343 | this.packages.push(pyPackage); 344 | } 345 | } 346 | } 347 | } 348 | exports.PyPackage = PyPackage; 349 | 350 | /** 351 | * Checking if the path is python package (if contains __init__.py) 352 | * @param {string} folderPath 353 | */ 354 | function isPythonPackage(folderPath) { 355 | folderPath = path.join(folderPath, "__init__.py"); 356 | return fs.existsSync(folderPath); 357 | } 358 | exports.isPythonPackage = isPythonPackage; 359 | 360 | /** 361 | * Checking if the path is python namespace (if the folder contains any *.py) 362 | * @param {string} folderPath 363 | */ 364 | function isPythonNamespace(folderPath) { 365 | var containModules = false; 366 | var files = fs.readdirSync(folderPath); 367 | 368 | if (files && files.length > 0) { 369 | files.every((file) => { 370 | folderPath = path.join(folderPath, file); 371 | 372 | if (path.extname(folderPath).toLowerCase() === ".py") { 373 | containModules = true; 374 | return false; 375 | } 376 | 377 | return true; 378 | }); 379 | } 380 | 381 | return containModules; 382 | } 383 | exports.isPythonNamespace = isPythonNamespace; 384 | -------------------------------------------------------------------------------- /unittest-files/reverse/classes.py: -------------------------------------------------------------------------------- 1 | class Foo: 2 | static_var:bool = False 3 | _static_var:int = 123 4 | __static_var = 123.456 5 | 6 | def __init__(self, x: int = 6): 7 | self.__x = x 8 | self._y: str = 'Hello' 9 | self.z = 123.456 10 | 11 | def foo(self, x: int = 3, y: str = 'Yoohu') -> int: 12 | pass 13 | 14 | class _Bar: 15 | static_bar_var:str = 'Hello' 16 | static_bar_var_int: int = 123 17 | 18 | def _bar(self): 19 | pass 20 | 21 | 22 | class __Baz(Foo, _Bar): 23 | def __baz(self): 24 | pass 25 | -------------------------------------------------------------------------------- /unittest-files/reverse/namespace/module.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niklauslee/staruml-python/f530922e09df2b43add43a0405f36122f96e33d0/unittest-files/reverse/namespace/module.py -------------------------------------------------------------------------------- /unittest-files/reverse/namespace/subnamespace1/module.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niklauslee/staruml-python/f530922e09df2b43add43a0405f36122f96e33d0/unittest-files/reverse/namespace/subnamespace1/module.py -------------------------------------------------------------------------------- /unittest-files/reverse/namespace/subnamespace2/module.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niklauslee/staruml-python/f530922e09df2b43add43a0405f36122f96e33d0/unittest-files/reverse/namespace/subnamespace2/module.py -------------------------------------------------------------------------------- /unittest-files/reverse/package/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niklauslee/staruml-python/f530922e09df2b43add43a0405f36122f96e33d0/unittest-files/reverse/package/__init__.py -------------------------------------------------------------------------------- /unittest-files/reverse/package/subpackage1/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niklauslee/staruml-python/f530922e09df2b43add43a0405f36122f96e33d0/unittest-files/reverse/package/subpackage1/__init__.py -------------------------------------------------------------------------------- /unittest-files/reverse/package/subpackage2/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niklauslee/staruml-python/f530922e09df2b43add43a0405f36122f96e33d0/unittest-files/reverse/package/subpackage2/__init__.py --------------------------------------------------------------------------------