Files
ResInsight/Fwk/AppFwk/cafPdmScripting/cafPdmPythonGenerator.cpp
T

977 lines
47 KiB
C++

//##################################################################################################
//
// 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 <<http://www.gnu.org/licenses/gpl.html>>
// 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 <<http://www.gnu.org/licenses/lgpl-2.1.html>>
// 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 <QColor>
#include <QRegularExpression>
#include <QTextStream>
#include <list>
#include <memory>
#include <set>
#include <utility>
using namespace caf;
CAF_PDM_CODE_GENERATOR_SOURCE_INIT( PdmPythonGenerator, "py" );
namespace internal
{
void appendCodeForMethod( std::map<QString, std::map<QString, QString>>& classMethods,
const std::list<QString>& 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<QString>& errorMessages ) const
{
QString generatedCode;
QTextStream out( &generatedCode );
std::vector<QString> classKeywords = factory->classKeywords();
std::vector<std::shared_ptr<PdmObject>> dummyObjects;
for ( QString classKeyword : classKeywords )
{
auto objectHandle = factory->create( classKeyword );
PdmObject* object = dynamic_cast<PdmObject*>( objectHandle );
CAF_ASSERT( object );
std::shared_ptr<PdmObject> 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<PdmObject> lhs, std::shared_ptr<PdmObject> rhs )
{
if ( lhs->inheritsClassWithKeyword( rhs->classKeyword() ) )
{
return false;
}
return lhs->classKeyword() < rhs->classKeyword();
} );
std::map<QString, std::map<QString, std::pair<QString, QString>>> classAttributesGenerated;
std::map<QString, std::map<QString, QString>> classMethodsGenerated;
std::map<QString, QString> classCommentsGenerated;
std::set<QString> 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<std::pair<QString, QString>> members; // (memberName, originalText), in declaration order
std::map<QString, QString> memberNameByText;
};
std::map<QString, EnumClassDef> enumDefByDataTypeName;
std::map<QString, QString> enumClassNameByDataTypeName;
std::map<QString, std::pair<QString, QString>> inferredEnumNameAndFieldByDataTypeName;
std::map<QString, std::pair<QString, QString>> enumTypeAndFieldByBaseName;
std::set<QString> ambiguousEnumDataTypesReported;
std::set<QString> 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<QString> usedEnumClassNames = { "PdmObjectBase",
"PdmObject_pb2",
"grpc",
"StrEnum",
"Optional",
"Dict",
"List",
"Tuple",
"Type",
"class_dict",
"class_from_keyword" };
for ( std::shared_ptr<PdmObject> 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<QString> 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<PdmXmlFieldHandle>()->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<QString> 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<QString> 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<PdmXmlFieldHandle>()->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<PdmObject> object : dummyObjects )
{
const std::list<QString>& classInheritanceStack = object->classInheritanceStack();
for ( auto it = classInheritanceStack.begin(); it != classInheritanceStack.end(); ++it )
{
const QString& classKeyword = *it;
QString scriptClassComment = PdmObjectScriptingCapabilityRegister::scriptClassComment( classKeyword );
std::map<QString, QString> attributesGenerated;
if ( !scriptClassComment.isEmpty() ) classCommentsGenerated[classKeyword] = scriptClassComment;
if ( classKeyword == object->classKeyword() )
{
std::vector<PdmFieldHandle*> fields = object->fields();
for ( auto field : fields )
{
auto scriptability = field->template capability<PdmAbstractFieldScriptingCapability>();
if ( scriptability != nullptr )
{
QString snake_field_name = camelToSnakeCase( scriptability->scriptFieldName() );
QString comment;
{
QStringList commentComponents;
commentComponents << field->capability<PdmUiFieldHandle>()->uiName();
commentComponents << field->capability<PdmUiFieldHandle>()->uiWhatsThis();
commentComponents.removeAll( QString( "" ) );
comment = commentComponents.join( ". " );
}
auto pdmValueField = dynamic_cast<const PdmValueField*>( field );
auto pdmChildField = dynamic_cast<const PdmChildFieldHandle*>( field );
auto pdmChildArrayField = dynamic_cast<const PdmChildArrayFieldHandle*>( 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<const PdmProxyFieldHandle*>( 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<std::optional<T>> 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<PdmObjectMethod> method =
PdmObjectMethodFactory::instance()->createMethod( object.get(), methodName );
std::vector<PdmFieldHandle*> 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<PdmAbstractFieldScriptingCapability>();
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<std::optional<T>> 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<QString> 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<QString, const EnumClassDef*> 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<PdmObject> object : dummyObjects )
{
const std::list<QString>& classInheritanceStack = object->classInheritanceStack();
std::list<QString> 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<PdmAbstractFieldScriptingCapability>();
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<PdmXmlFieldHandle>();
auto scriptability = field->capability<PdmAbstractFieldScriptingCapability>();
if ( scriptability && !scriptability->enumScriptTexts().empty() ) return "str";
QString dataType = PdmObjectScriptingCapabilityRegister::scriptClassNameFromClassKeyword( xmlObj->dataTypeName() );
std::map<QString, QString> 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<double> ).name() ), "List[float]" },
{ QString::fromStdString( typeid( std::optional<double> ).name() ), "Optional[float]" },
{ QString::fromStdString( typeid( std::optional<float> ).name() ), "Optional[float]" },
{ QString::fromStdString( typeid( std::optional<int> ).name() ), "Optional[int]" },
{ QString::fromStdString( typeid( std::optional<bool> ).name() ), "Optional[bool]" },
{ QString::fromStdString( typeid( std::optional<QString> ).name() ), "Optional[str]" },
{ QString::fromStdString( typeid( std::pair<bool, double> ).name() ), "Tuple[bool, float]" },
{ QString::fromStdString( typeid( std::pair<bool, float> ).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;
}