//################################################################################################## // // Custom Visualization Core library // Copyright (C) Ceetron Solutions AS // // This library may be used under the terms of either the GNU General Public License or // the GNU Lesser General Public License as follows: // // GNU General Public License Usage // This library is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This library is distributed in the hope that it will be useful, but WITHOUT ANY // WARRANTY; without even the implied warranty of MERCHANTABILITY or // FITNESS FOR A PARTICULAR PURPOSE. // // See the GNU General Public License at <> // for more details. // // GNU Lesser General Public License Usage // This library is free software; you can redistribute it and/or modify // it under the terms of the GNU Lesser General Public License as published by // the Free Software Foundation; either version 2.1 of the License, or // (at your option) any later version. // // This library is distributed in the hope that it will be useful, but WITHOUT ANY // WARRANTY; without even the implied warranty of MERCHANTABILITY or // FITNESS FOR A PARTICULAR PURPOSE. // // See the GNU Lesser General Public License at <> // for more details. // //################################################################################################## #include "cafPdmPythonGenerator.h" #include "cafAssert.h" #include "cafPdmAbstractFieldScriptingCapability.h" #include "cafPdmChildArrayField.h" #include "cafPdmChildField.h" #include "cafPdmLogging.h" #include "cafPdmObject.h" #include "cafPdmObjectFactory.h" #include "cafPdmObjectMethod.h" #include "cafPdmObjectScriptingCapabilityRegister.h" #include "cafPdmProxyValueField.h" #include "cafPdmScriptEnumNameRegistry.h" #include "cafPdmXmlFieldHandle.h" #ifndef CAF_EXCLUDE_CVF #include "cafPdmFieldScriptingCapabilityCvfColor3.h" #include "cafPdmFieldScriptingCapabilityCvfVec3d.h" #endif #include #include #include #include #include #include #include using namespace caf; CAF_PDM_CODE_GENERATOR_SOURCE_INIT( PdmPythonGenerator, "py" ); namespace internal { void appendCodeForMethod( std::map>& classMethods, const std::list& classInheritanceStack, const QString& classKeyword, const QString& methodName, const QString& sourceCode ) { // Check if method already exists in any super class. This is not supported, and causes the generated // Python code to silently ignore the second version of the source code. Show an error message and assert will be // issued. It is difficult to trap this error earlier. The method names are registered with the macro // CAF_PDM_OBJECT_METHOD_SOURCE_INIT, and this macro does not have access to the class inheritance structure. // See "cafPdmObjectMethod.h" // https://github.com/OPM/ResInsight/issues/12972 for ( auto it = classInheritanceStack.begin(); it != classInheritanceStack.end(); ++it ) { const auto& superClassKeyword = *it; if ( superClassKeyword == classKeyword ) continue; if ( classMethods[superClassKeyword].contains( methodName ) ) { QString txt = QString( "Method '%1' already exists in super class '%2'. Cannot add method '%3' to class '%4'." ) .arg( methodName ) .arg( superClassKeyword ) .arg( methodName ) .arg( classKeyword ); caf::PdmLogging::error( txt ); // Show both message and assert, as this is a serious error that must be fixed by the developer. Not // possible to have dynamic error message in the assert message. CAF_ASSERT( false && "Detected duplicated super class method name when creating a Python class method" ); return; } } classMethods[classKeyword][methodName] = sourceCode; } } //namespace internal //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- QString caf::PdmPythonGenerator::generate( PdmObjectFactory* factory, std::vector& errorMessages ) const { QString generatedCode; QTextStream out( &generatedCode ); std::vector classKeywords = factory->classKeywords(); std::vector> dummyObjects; for ( QString classKeyword : classKeywords ) { auto objectHandle = factory->create( classKeyword ); PdmObject* object = dynamic_cast( objectHandle ); CAF_ASSERT( object ); std::shared_ptr sharedObject( object ); if ( PdmObjectScriptingCapabilityRegister::isScriptable( sharedObject.get() ) ) { dummyObjects.push_back( sharedObject ); } } // Sort to make sure super classes get created before sub classes std::sort( dummyObjects.begin(), dummyObjects.end(), []( std::shared_ptr lhs, std::shared_ptr rhs ) { if ( lhs->inheritsClassWithKeyword( rhs->classKeyword() ) ) { return false; } return lhs->classKeyword() < rhs->classKeyword(); } ); std::map>> classAttributesGenerated; std::map> classMethodsGenerated; std::map classCommentsGenerated; std::set dataTypesInChildFields; // Bookkeeping for AppEnum-typed fields. Each unique C++ AppEnum type (keyed by its xml // dataTypeName, which is typeid().name() and therefore unique per type) gets one Python // StrEnum class. Names are derived from the script field name to stay readable. struct EnumClassDef { QString className; std::vector> members; // (memberName, originalText), in declaration order std::map memberNameByText; }; std::map enumDefByDataTypeName; std::map enumClassNameByDataTypeName; std::map> inferredEnumNameAndFieldByDataTypeName; std::map> enumTypeAndFieldByBaseName; std::set ambiguousEnumDataTypesReported; std::set duplicateEnumClassNamesReported; bool enumClassNameCollisionDetected = false; // Seed with names that are already bound at module scope in the generated file (imports // and module-level functions) so a generated enum cannot shadow them. std::set usedEnumClassNames = { "PdmObjectBase", "PdmObject_pb2", "grpc", "StrEnum", "Optional", "Dict", "List", "Tuple", "Type", "class_dict", "class_from_keyword" }; for ( std::shared_ptr object : dummyObjects ) { QString scriptClassName = PdmObjectScriptingCapabilityRegister::scriptClassNameFromClassKeyword( object->classKeyword() ); if ( scriptClassName.isEmpty() ) scriptClassName = object->classKeyword(); usedEnumClassNames.insert( scriptClassName ); } auto sanitizeEnumMemberName = []( const QString& text ) -> QString { // Python identifiers must start with a letter or underscore and may only contain // letters, digits, and underscores. Some AppEnum texts (e.g. "R-G", "None") violate // these rules — coerce them into valid identifiers without changing the wire value. static const std::set pythonKeywords = { "False", "None", "True", "and", "as", "assert", "async", "await", "break", "class", "continue", "def", "del", "elif", "else", "except", "finally", "for", "from", "global", "if", "import", "in", "is", "lambda", "nonlocal", "not", "or", "pass", "raise", "return", "try", "while", "with", "yield", "match", "case" }; QString s; for ( QChar ch : text ) { if ( ch.isLetterOrNumber() || ch == '_' ) s += ch; else s += '_'; } if ( s.isEmpty() ) s = "_"; if ( s[0].isDigit() ) s = "_" + s; if ( pythonKeywords.count( s ) ) s += "_"; return s; }; auto enumClassNameForField = [&]( PdmFieldHandle* field, PdmAbstractFieldScriptingCapability* scriptability ) -> QString { QStringList enumTexts = scriptability->enumScriptTexts(); if ( enumTexts.empty() ) return QString(); QString dataTypeName = field->capability()->dataTypeName(); // Prefer an explicit override registered by application code so the Python enum class name // is stable independent of which field the generator visits first. QString baseName = PdmScriptEnumNameRegistry::lookup( dataTypeName ); QString inferredName = snakeToCamelCase( camelToSnakeCase( scriptability->scriptFieldName() ) ); if ( inferredName.isEmpty() ) inferredName = "Enum"; if ( baseName.isEmpty() ) { QString fieldName = QString( "%1.%2" ).arg( field->ownerClass() ).arg( scriptability->scriptFieldName() ); auto [it, inserted] = inferredEnumNameAndFieldByDataTypeName.emplace( dataTypeName, std::make_pair( inferredName, fieldName ) ); if ( !inserted && it->second.first != inferredName && !ambiguousEnumDataTypesReported.count( dataTypeName ) ) { errorMessages.push_back( QString( "Ambiguous Python enum class names '%1' and '%2' inferred from fields '%3' and '%4'. " "Register a canonical name with caf::PdmScriptEnumNameRegistry." ) .arg( it->second.first ) .arg( inferredName ) .arg( it->second.second ) .arg( fieldName ) ); ambiguousEnumDataTypesReported.insert( dataTypeName ); } baseName = it->second.first; } QString fieldName = QString( "%1.%2" ).arg( field->ownerClass() ).arg( scriptability->scriptFieldName() ); auto [enumNameIt, inserted] = enumTypeAndFieldByBaseName.emplace( baseName, std::make_pair( dataTypeName, fieldName ) ); if ( !inserted && enumNameIt->second.first != dataTypeName ) { enumClassNameCollisionDetected = true; if ( !duplicateEnumClassNamesReported.count( baseName ) ) { errorMessages.push_back( QString( "Duplicate Python enum class name '%1' is inferred for distinct enum " "types used by fields '%2' and '%3'. " "Register distinct names with caf::PdmScriptEnumNameRegistry, or " "move the enum to a shared header if both fields " "represent the same enum." ) .arg( baseName ) .arg( enumNameIt->second.second ) .arg( fieldName ) ); duplicateEnumClassNamesReported.insert( baseName ); } return QString(); } auto cached = enumClassNameByDataTypeName.find( dataTypeName ); if ( cached != enumClassNameByDataTypeName.end() ) return cached->second; if ( usedEnumClassNames.count( baseName ) ) { enumClassNameCollisionDetected = true; if ( !duplicateEnumClassNamesReported.count( baseName ) ) { errorMessages.push_back( QString( "Python enum class name '%1' inferred for field '%2' conflicts with an existing module " "name. Register a distinct name with caf::PdmScriptEnumNameRegistry." ) .arg( baseName ) .arg( fieldName ) ); duplicateEnumClassNamesReported.insert( baseName ); } return QString(); } EnumClassDef def; def.className = baseName; std::set usedMemberNames; for ( const QString& text : enumTexts ) { QString memberName = sanitizeEnumMemberName( text ); int memberSuffix = 1; QString unique = memberName; while ( usedMemberNames.count( unique ) ) { unique = memberName + QString( "_%1" ).arg( ++memberSuffix ); } usedMemberNames.insert( unique ); def.members.push_back( { unique, text } ); def.memberNameByText[text] = unique; } enumClassNameByDataTypeName[dataTypeName] = baseName; enumDefByDataTypeName[dataTypeName] = def; usedEnumClassNames.insert( baseName ); return baseName; }; auto enumClassNameForMethodReturn = [&]( const QString& scriptName, const QStringList& enumTexts ) -> QString { if ( scriptName.isEmpty() || enumTexts.empty() ) return QString(); // Key by the script name so methods that declare the same return enum share one class. // Prefixed to avoid colliding with AppEnum field keys (which are typeid().name()). QString dataTypeKey = QString( "__method_return__:" ) + scriptName; auto cached = enumClassNameByDataTypeName.find( dataTypeKey ); if ( cached != enumClassNameByDataTypeName.end() ) return cached->second; EnumClassDef def; def.className = scriptName; std::set usedMemberNames; for ( const QString& text : enumTexts ) { QString memberName = sanitizeEnumMemberName( text ); int memberSuffix = 1; QString unique = memberName; while ( usedMemberNames.count( unique ) ) { unique = memberName + QString( "_%1" ).arg( ++memberSuffix ); } usedMemberNames.insert( unique ); def.members.push_back( { unique, text } ); def.memberNameByText[text] = unique; } enumClassNameByDataTypeName[dataTypeKey] = scriptName; enumDefByDataTypeName[dataTypeKey] = def; usedEnumClassNames.insert( scriptName ); return scriptName; }; auto enumDefaultValueExpression = [&]( PdmFieldHandle* field, const QString& enumClassName, const QString& quotedDefault ) -> QString { // The existing getDefaultValue() returns the AppEnum text wrapped in double quotes (because // dataTypeString reports "str" for enum fields). Convert "MATRIX_MODEL" -> EnumName.MATRIX_MODEL. // If the default isn't a quoted string (e.g. the literal None for fields with no default), // leave it unchanged. if ( !quotedDefault.startsWith( '"' ) || !quotedDefault.endsWith( '"' ) || quotedDefault.size() < 2 ) { return quotedDefault; } QString stripped = quotedDefault.mid( 1, quotedDefault.size() - 2 ); if ( stripped.isEmpty() ) return quotedDefault; QString dataTypeName = field->capability()->dataTypeName(); auto it = enumDefByDataTypeName.find( dataTypeName ); QString memberName = ( it != enumDefByDataTypeName.end() && it->second.memberNameByText.count( stripped ) ) ? it->second.memberNameByText.at( stripped ) : sanitizeEnumMemberName( stripped ); return QString( "%1.%2" ).arg( enumClassName ).arg( memberName ); }; // First generate all attributes and comments to go into each object for ( std::shared_ptr object : dummyObjects ) { const std::list& classInheritanceStack = object->classInheritanceStack(); for ( auto it = classInheritanceStack.begin(); it != classInheritanceStack.end(); ++it ) { const QString& classKeyword = *it; QString scriptClassComment = PdmObjectScriptingCapabilityRegister::scriptClassComment( classKeyword ); std::map attributesGenerated; if ( !scriptClassComment.isEmpty() ) classCommentsGenerated[classKeyword] = scriptClassComment; if ( classKeyword == object->classKeyword() ) { std::vector fields = object->fields(); for ( auto field : fields ) { auto scriptability = field->template capability(); if ( scriptability != nullptr ) { QString snake_field_name = camelToSnakeCase( scriptability->scriptFieldName() ); QString comment; { QStringList commentComponents; commentComponents << field->capability()->uiName(); commentComponents << field->capability()->uiWhatsThis(); commentComponents.removeAll( QString( "" ) ); comment = commentComponents.join( ". " ); } auto pdmValueField = dynamic_cast( field ); auto pdmChildField = dynamic_cast( field ); auto pdmChildArrayField = dynamic_cast( field ); if ( pdmValueField ) { QString dataType = scriptability->dataType(); if ( dataType.isEmpty() ) { dataType = PdmPythonGenerator::dataTypeString( field, false ); } if ( field->xmlCapability()->isVectorField() ) { dataType = QString( "List[%1]" ).arg( dataType ); } bool shouldBeMethod = false; auto proxyField = dynamic_cast( field ); if ( proxyField && proxyField->isStreamingField() ) shouldBeMethod = true; if ( classAttributesGenerated[field->ownerClass()].count( snake_field_name ) ) continue; if ( classMethodsGenerated[field->ownerClass()].count( snake_field_name ) ) continue; QVariant valueVariant = pdmValueField->toQVariant(); if ( shouldBeMethod ) { if ( proxyField->hasGetter() ) { QString fullComment = QString( " \"\"\"%1\n\n Returns:\n " "%2\n \"\"\"" ) .arg( comment ) .arg( dataType ); QString fieldCode = QString( " def %1(self) -> %2:\n%3\n return " "self._call_get_method(\"%4\")\n" ) .arg( snake_field_name ) .arg( dataType ) .arg( fullComment ) .arg( scriptability->scriptFieldName() ); classMethodsGenerated[field->ownerClass()][snake_field_name] = fieldCode; } if ( proxyField->hasSetter() ) { QString fullComment = QString( " \"\"\"Set %1\n\n Arguments:\n" " values (%2): data\n \"\"\"" ) .arg( comment ) .arg( dataType ); QString fieldCode = QString( " def set_%1(self, values : %2) -> None:\n%3\n " "self._call_set_method(\"%4\", values)\n" ) .arg( snake_field_name ) .arg( dataType ) .arg( fullComment ) .arg( scriptability->scriptFieldName() ); classMethodsGenerated[field->ownerClass()][QString( "set_%1" ).arg( snake_field_name )] = fieldCode; } } else { QString defaultValue = getDefaultValue( field ); if ( dataType.toLower().contains( "optional" ) ) { auto strippedValue = defaultValue; strippedValue.remove( "\"" ); if ( strippedValue.isEmpty() ) { // An empty string as default value for an optional field is set to the None defaultValue = "None"; } } else if ( defaultValue == "None" ) { // TODO: Consider using PdmField> for optional fields instead of // using the default value to manipulate the type dataType = QString( "Optional[%1]" ).arg( dataType ); } QString enumClassName = enumClassNameForField( field, scriptability ); if ( !enumClassName.isEmpty() ) { dataType = enumClassName; defaultValue = enumDefaultValueExpression( field, enumClassName, defaultValue ); } QString fieldCode = QString( " self.%1: %2 = %3\n" ).arg( snake_field_name ).arg( dataType ).arg( defaultValue ); QString fullComment; { QString commentAndEnum = comment; QStringList enumTexts = scriptability->enumScriptTexts(); if ( !enumTexts.empty() ) { // Replace the comment text with enum values // The space is limited for the generation of documentation commentAndEnum = "One of [" + enumTexts.join( ", " ) + "]"; } fullComment = QString( "%1 (%2): %3\n" ).arg( snake_field_name ).arg( dataType ).arg( commentAndEnum ); } classAttributesGenerated[field->ownerClass()][snake_field_name].first = fieldCode; classAttributesGenerated[field->ownerClass()][snake_field_name].second = fullComment; } } else if ( pdmChildField || pdmChildArrayField ) { QString dataType = PdmPythonGenerator::dataTypeString( field, false ); QString scriptDataType = PdmObjectScriptingCapabilityRegister::scriptClassNameFromClassKeyword( dataType ); dataTypesInChildFields.insert( scriptDataType ); QString commentDataType = field->xmlCapability()->isVectorField() ? QString( "List[%1]" ).arg( scriptDataType ) : scriptDataType; QString fullComment = QString( " \"\"\"%1\n\n Returns:\n %2\n \"\"\"" ) .arg( comment ) .arg( commentDataType ); if ( pdmChildField ) { QString fieldCode = QString( " def %1(self) -> Optional[%4]:\n%2\n children = " "self.children(\"%3\", %4)\n return children[0] if " "len(children) > 0 else None\n" ) .arg( snake_field_name ) .arg( fullComment ) .arg( scriptability->scriptFieldName() ) .arg( scriptDataType ); classMethodsGenerated[field->ownerClass()][snake_field_name] = fieldCode; } else { QString fieldCode = QString( " def %1(self) -> List[%2]:\n%3\n return " "self.children(\"%4\", %5)\n" ) .arg( snake_field_name ) .arg( scriptDataType ) .arg( fullComment ) .arg( scriptability->scriptFieldName() ) .arg( scriptDataType ); classMethodsGenerated[field->ownerClass()][snake_field_name] = fieldCode; } } } } } for ( QString methodName : PdmObjectMethodFactory::instance()->registeredMethodNames( classKeyword ) ) { std::shared_ptr method = PdmObjectMethodFactory::instance()->createMethod( object.get(), methodName ); std::vector arguments = method->fields(); QString methodComment = method->uiCapability()->uiWhatsThis(); QString snake_method_name = camelToSnakeCase( methodName ); QStringList inputArgumentStrings; QStringList outputArgumentStrings; QStringList argumentComments; outputArgumentStrings.push_back( QString( "\"%1\"" ).arg( methodName ) ); QString returnDataType = "None"; QString returnComment; QString returnEnumClassName = enumClassNameForMethodReturn( method->returnEnumScriptName(), method->returnEnumTexts() ); QString containerClassName; if ( !method->classKeywordReturnedType().isEmpty() ) { QString classKeywordReturnedType = method->classKeywordReturnedType(); returnComment = PdmObjectScriptingCapabilityRegister::scriptClassNameFromClassKeyword( classKeywordReturnedType ); containerClassName = PdmObjectScriptingCapabilityRegister::scriptClassNameFromClassKeyword( classKeywordReturnedType ); returnDataType = containerClassName; outputArgumentStrings.push_back( QString( "%1" ).arg( containerClassName ) ); if ( method->isNullptrValidResult() ) { returnDataType = QString( "Optional[%1]" ).arg( returnDataType ); } if ( !returnEnumClassName.isEmpty() ) { // Method declares an enum return. Override the annotation; the body emission // below will unwrap the container's "value" field into the enum. returnDataType = returnEnumClassName; returnComment = returnEnumClassName; } } for ( auto field : arguments ) { auto scriptability = field->capability(); auto argumentName = camelToSnakeCase( scriptability->scriptFieldName() ); auto dataType = dataTypeString( field, false ); bool isList = field->xmlCapability()->isVectorField(); if ( isList ) dataType = QString( "List[%1]" ).arg( dataType ); QString defaultValue = getDefaultValue( field ); if ( defaultValue == "None" ) { // TODO: Consider using PdmField> for optional fields instead of using // the default value to manipulate the type dataType = QString( "Optional[%1]" ).arg( dataType ); } QString commentOrEnumDescription = field->uiCapability()->uiWhatsThis(); QStringList enumTexts = scriptability->enumScriptTexts(); if ( !enumTexts.empty() ) { // Replace the comment text with enum values // The space is limited for the generation of documentation commentOrEnumDescription = "One of [" + enumTexts.join( ", " ) + "]"; } QString enumClassName = enumClassNameForField( field, scriptability ); if ( !enumClassName.isEmpty() ) { dataType = enumClassName; defaultValue = enumDefaultValueExpression( field, enumClassName, defaultValue ); } inputArgumentStrings.push_back( QString( "%1: %2=%3" ).arg( argumentName ).arg( dataType ).arg( defaultValue ) ); outputArgumentStrings.push_back( QString( "%1=%1" ).arg( argumentName ) ); argumentComments.push_back( QString( "%1 (%2): %3" ).arg( argumentName ).arg( dataType ).arg( commentOrEnumDescription ) ); } QString fullComment = QString( " \"\"\"\n %1\n\n Arguments:\n " "%2\n Returns:\n %3\n \"\"\"" ) .arg( methodComment ) .arg( argumentComments.join( "\n " ) ) .arg( returnComment ); QString methodBody = QString( "self._call_pdm_method_void(%1)" ).arg( outputArgumentStrings.join( ", " ) ); if ( returnDataType != "None" ) { if ( !returnEnumClassName.isEmpty() ) { // Unwrap the typed container's "value" field into the enum at the call site. methodBody = QString( "_container = self._call_pdm_method_return_value(%1)\n" " return %2(_container.value)" ) .arg( outputArgumentStrings.join( ", " ) ) .arg( returnEnumClassName ); } else if ( method->isNullptrValidResult() ) { methodBody = QString( "return self._call_pdm_method_return_optional_value(%1)" ) .arg( outputArgumentStrings.join( ", " ) ); } else { methodBody = QString( "return self._call_pdm_method_return_value(%1)" ).arg( outputArgumentStrings.join( ", " ) ); } } QString methodCode = QString( " def %1(self, %2) -> %3:\n%4\n %5\n" ) .arg( snake_method_name ) .arg( inputArgumentStrings.join( ", " ) ) .arg( returnDataType ) .arg( fullComment ) .arg( methodBody ); internal::appendCodeForMethod( classMethodsGenerated, classInheritanceStack, classKeyword, snake_method_name, methodCode ); } } } // Write out classes std::set classesWritten; classesWritten.insert( "PdmObjectBase" ); out << "from __future__ import annotations\n"; out << "from rips.pdmobject import PdmObjectBase\n"; out << "import PdmObject_pb2\n"; out << "import grpc\n"; out << "from enum import StrEnum\n"; out << "from typing import Optional, Dict, List, Tuple, Type\n"; out << "\n"; // Emit enum classes sorted by name for stable diffs across regenerations. std::map enumDefByClassName; for ( const auto& [dataTypeName, def] : enumDefByDataTypeName ) { enumDefByClassName[def.className] = &def; } for ( const auto& [enumClassName, def] : enumDefByClassName ) { out << QString( "class %1(StrEnum):\n" ).arg( enumClassName ); for ( const auto& [memberName, originalText] : def->members ) { out << QString( " %1 = \"%2\"\n" ).arg( memberName ).arg( originalText ); } out << "\n"; } for ( std::shared_ptr object : dummyObjects ) { const std::list& classInheritanceStack = object->classInheritanceStack(); std::list scriptSuperClassNames; for ( auto it = classInheritanceStack.begin(); it != classInheritanceStack.end(); ++it ) { const QString& classKeyword = *it; QString scriptClassName = PdmObjectScriptingCapabilityRegister::scriptClassNameFromClassKeyword( classKeyword ); if ( scriptClassName.isEmpty() ) scriptClassName = classKeyword; if ( !classesWritten.count( scriptClassName ) ) { QString classCode; if ( scriptSuperClassNames.empty() ) { classCode = QString( "class %1:\n" ).arg( scriptClassName ); } else { classCode = QString( "class %1(%2):\n" ).arg( scriptClassName ).arg( scriptSuperClassNames.back() ); } if ( !classCommentsGenerated[classKeyword].isEmpty() || !classAttributesGenerated[classKeyword].empty() ) { classCode += " \"\"\"\n"; if ( !classCommentsGenerated[classKeyword].isEmpty() ) { if ( !classCommentsGenerated[classKeyword].isEmpty() ) { classCode += QString( " %1\n\n" ).arg( classCommentsGenerated[classKeyword] ); } } if ( !classAttributesGenerated[classKeyword].empty() ) { classCode += " Attributes:\n"; for ( auto keyWordValuePair : classAttributesGenerated[classKeyword] ) { classCode += " " + keyWordValuePair.second.second; } } classCode += " \"\"\"\n"; } classCode += QString( " __custom_init__ = None #: Assign a custom init routine to be run at __init__\n\n" ); classCode += QString( " def __init__(self, pb2_object: Optional[PdmObject_pb2.PdmObject]=None, channel: " "Optional[grpc.Channel]=None) -> None:\n" ); if ( !scriptSuperClassNames.empty() ) { // Own attributes. This initializes a lot of attributes to None. // This means it has to be done before we set any values. for ( auto keyWordValuePair : classAttributesGenerated[classKeyword] ) { classCode += keyWordValuePair.second.first; } // Parent constructor classCode += QString( " %1.__init__(self, pb2_object, channel)\n" ).arg( scriptSuperClassNames.back() ); } classCode += QString( " if %1.__custom_init__ is not None:\n" ).arg( scriptClassName ); classCode += QString( " %1.__custom_init__(self, pb2_object=pb2_object, channel=channel)\n" ) .arg( scriptClassName ); for ( auto keyWordValuePair : classMethodsGenerated[classKeyword] ) { classCode += "\n"; classCode += keyWordValuePair.second; classCode += "\n"; } out << classCode << "\n"; classesWritten.insert( scriptClassName ); } scriptSuperClassNames.push_back( scriptClassName ); } } out << "def class_dict() -> Dict[str, Type[PdmObjectBase]]:\n"; out << " classes : Dict[str, Type[PdmObjectBase]] = {}\n"; for ( QString classKeyword : classesWritten ) { out << QString( " classes['%1'] = %1\n" ).arg( classKeyword ); } out << " return classes\n\n"; out << "def class_from_keyword(class_keyword : str) -> Optional[Type[PdmObjectBase]]:\n"; out << " all_classes = class_dict()\n"; out << " if class_keyword in all_classes.keys():\n"; out << " return all_classes[class_keyword]\n"; out << " return None\n"; // Check if all referenced data types are exported as classes for ( const auto& scriptDataType : dataTypesInChildFields ) { if ( classesWritten.count( scriptDataType ) == 0 ) { QString errorText = "No export for data type " + scriptDataType; errorMessages.push_back( errorText ); } } if ( enumClassNameCollisionDetected ) return QString(); return generatedCode; } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- QString PdmPythonGenerator::getDefaultValue( PdmFieldHandle* field ) { QString defaultValue = "None"; bool isList = field->xmlCapability()->isVectorField(); if ( isList ) { defaultValue = "[]"; } else { QString valueString; // Always make sure the default value for a ptrField is empty string if ( !field->hasPtrReferencedObjects() ) { auto scriptability = field->template capability(); QTextStream valueStream( &valueString ); scriptability->readFromField( valueStream, true, true ); } if ( valueString.isEmpty() ) { valueString = defaultValue; } valueString = pythonifyDataValue( valueString ); #ifndef CAF_EXCLUDE_CVF // Convert a cvf::Color3f default ("r g b" space-separated floats) to a hex string // so the generated Python uses the same representation as user-facing examples. if ( field->xmlCapability()->dataTypeName() == QString::fromStdString( typeid( cvf::Color3f ).name() ) ) { QStringList parts = valueString.split( ' ', Qt::SkipEmptyParts ); if ( parts.size() == 3 ) { QColor qColor; qColor.setRgbF( parts[0].toFloat(), parts[1].toFloat(), parts[2].toFloat() ); if ( qColor.isValid() ) { valueString = qColor.name(); } } } #endif // Ensure string-typed defaults are quoted. The QString IO handler quotes its own output, // but other types mapped to "str" (e.g. cvf::Color3f) emit an unquoted representation that // would be invalid Python. if ( dataTypeString( field, false ) == "str" && !valueString.startsWith( '"' ) && valueString != "None" ) { valueString = "\"" + valueString + "\""; } defaultValue = valueString; } return defaultValue; } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- QString PdmPythonGenerator::camelToSnakeCase( const QString& camelString ) { static QRegularExpression re1( "(.)([A-Z][a-z]+)" ); static QRegularExpression re2( "([a-z0-9])([A-Z])" ); QString snake_case = camelString; snake_case.replace( re1, "\\1_\\2" ); snake_case.replace( re2, "\\1_\\2" ); return snake_case.toLower(); } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- QString PdmPythonGenerator::snakeToCamelCase( const QString& snakeString ) { QString camel; for ( const QString& part : snakeString.split( '_', Qt::SkipEmptyParts ) ) { camel += part.left( 1 ).toUpper() + part.mid( 1 ).toLower(); } return camel; } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- QString PdmPythonGenerator::dataTypeString( const PdmFieldHandle* field, bool useStrForUnknownDataTypes ) { auto xmlObj = field->capability(); auto scriptability = field->capability(); if ( scriptability && !scriptability->enumScriptTexts().empty() ) return "str"; QString dataType = PdmObjectScriptingCapabilityRegister::scriptClassNameFromClassKeyword( xmlObj->dataTypeName() ); std::map builtins = { { QString::fromStdString( typeid( double ).name() ), "float" }, { QString::fromStdString( typeid( float ).name() ), "float" }, { QString::fromStdString( typeid( int ).name() ), "int" }, { QString::fromStdString( typeid( bool ).name() ), "bool" }, { QString::fromStdString( typeid( time_t ).name() ), "int" }, { QString::fromStdString( typeid( QString ).name() ), "str" }, { QString::fromStdString( typeid( caf::FilePath ).name() ), "str" }, { QString::fromStdString( typeid( std::vector ).name() ), "List[float]" }, { QString::fromStdString( typeid( std::optional ).name() ), "Optional[float]" }, { QString::fromStdString( typeid( std::optional ).name() ), "Optional[float]" }, { QString::fromStdString( typeid( std::optional ).name() ), "Optional[int]" }, { QString::fromStdString( typeid( std::optional ).name() ), "Optional[bool]" }, { QString::fromStdString( typeid( std::optional ).name() ), "Optional[str]" }, { QString::fromStdString( typeid( std::pair ).name() ), "Tuple[bool, float]" }, { QString::fromStdString( typeid( std::pair ).name() ), "Tuple[bool, float]" }, }; #ifndef CAF_EXCLUDE_CVF builtins[QString::fromStdString( typeid( cvf::Vec3d ).name() )] = "List[float]"; builtins[QString::fromStdString( typeid( cvf::Color3f ).name() )] = "str"; builtins[QString::fromStdString( typeid( cvf::Mat4d ).name() )] = "List[float]"; #endif bool foundBuiltin = false; for ( auto builtin : builtins ) { if ( dataType == builtin.first ) { dataType.replace( builtin.first, builtin.second ); foundBuiltin = true; } } if ( !foundBuiltin && useStrForUnknownDataTypes ) { dataType = "str"; } return dataType; } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- QString PdmPythonGenerator::pythonifyDataValue( const QString& dataValue ) { QString outValue = dataValue; outValue.replace( "false", "False" ); outValue.replace( "true", "True" ); return outValue; }