From 8971ebfc9c4aa296320960255288d45912301f79 Mon Sep 17 00:00:00 2001 From: Kristian Bendiksen Date: Fri, 22 May 2026 10:59:51 +0200 Subject: [PATCH] #14001 Python: Auto-generate StrEnum return types for scriptable methods MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the hand-written PropertyDataType wrapper with first-class enum return support in the code generator. Scriptable methods that derive from the new caf::PdmEnumObjectMethod template return a typed enum value across the gRPC boundary; the Python generator emits a StrEnum class with the texts pulled from the underlying caf::AppEnum and wraps the call site so callers receive `EnumClass(value)` directly. caf::PdmDataContainerEnum is the small PdmObject the framework uses to carry the enum text on the wire (single scriptable QString field). RimcEclipseCase_propertyDataType becomes declarative: implement executeEnum() returning the typed RiaDefines::ResultDataType and pick a returnEnumScriptName() — the base handles serialization and reports the four AppEnum members (UNKNOWN, FLOAT, DOUBLE, INTEGER) to the generator. Drop the hand-written PropertyDataType in enums.py — the generated one in resinsight_classes is wire-equivalent and now the single source of truth. --- .../RimcEclipseCase.cpp | 17 +----- .../RimcEclipseCase.h | 12 ++-- Fwk/AppFwk/cafPdmScripting/CMakeLists.txt | 2 + .../cafPdmDataContainerEnum.cpp | 55 +++++++++++++++++ .../cafPdmScripting/cafPdmDataContainerEnum.h | 60 +++++++++++++++++++ .../cafPdmScripting/cafPdmObjectMethod.h | 55 +++++++++++++++++ .../cafPdmScripting/cafPdmPythonGenerator.cpp | 59 +++++++++++++++++- GrpcInterface/Python/rips/__init__.py | 6 +- GrpcInterface/Python/rips/case.py | 33 +--------- GrpcInterface/Python/rips/enums.py | 17 ++---- 10 files changed, 248 insertions(+), 68 deletions(-) create mode 100644 Fwk/AppFwk/cafPdmScripting/cafPdmDataContainerEnum.cpp create mode 100644 Fwk/AppFwk/cafPdmScripting/cafPdmDataContainerEnum.h diff --git a/ApplicationLibCode/ProjectDataModelCommands/RimcEclipseCase.cpp b/ApplicationLibCode/ProjectDataModelCommands/RimcEclipseCase.cpp index 818ee2a3f2..57220693c1 100644 --- a/ApplicationLibCode/ProjectDataModelCommands/RimcEclipseCase.cpp +++ b/ApplicationLibCode/ProjectDataModelCommands/RimcEclipseCase.cpp @@ -414,7 +414,7 @@ CAF_PDM_OBJECT_METHOD_SOURCE_INIT( RimEclipseCase, RimcEclipseCase_propertyDataT /// //-------------------------------------------------------------------------------------------------- RimcEclipseCase_propertyDataType::RimcEclipseCase_propertyDataType( caf::PdmObjectHandle* self ) - : caf::PdmObjectMethod( self, PdmObjectMethod::NullPointerType::NULL_IS_INVALID, PdmObjectMethod::ResultType::PERSISTENT_FALSE ) + : caf::PdmEnumObjectMethod( self ) { CAF_PDM_InitObject( "Property Data Type", "", "", "Get the data type (FLOAT or INTEGER) of a grid property" ); @@ -426,7 +426,7 @@ RimcEclipseCase_propertyDataType::RimcEclipseCase_propertyDataType( caf::PdmObje //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- -std::expected RimcEclipseCase_propertyDataType::execute() +std::expected RimcEclipseCase_propertyDataType::executeEnum() { auto eclipseCase = self(); if ( !eclipseCase ) return std::unexpected( "No eclipse case" ); @@ -453,20 +453,9 @@ std::expected RimcEclipseCase_propertyDataType:: { if ( address.resultCatType() == resultCatTypeEnum && address.resultName() == m_propertyName() ) { - auto* dataObject = new RimcDataContainerString(); - const bool isInteger = address.dataType() == RiaDefines::ResultDataType::INTEGER; - dataObject->m_stringValues = { isInteger ? QString( "INTEGER" ) : QString( "FLOAT" ) }; - return dataObject; + return address.dataType(); } } return std::unexpected( QString( "Property not found: %1" ).arg( m_propertyName() ) ); } - -//-------------------------------------------------------------------------------------------------- -/// -//-------------------------------------------------------------------------------------------------- -QString RimcEclipseCase_propertyDataType::classKeywordReturnedType() const -{ - return RimcDataContainerString::classKeywordStatic(); -} diff --git a/ApplicationLibCode/ProjectDataModelCommands/RimcEclipseCase.h b/ApplicationLibCode/ProjectDataModelCommands/RimcEclipseCase.h index 99b2cf3604..a7470cf6f2 100644 --- a/ApplicationLibCode/ProjectDataModelCommands/RimcEclipseCase.h +++ b/ApplicationLibCode/ProjectDataModelCommands/RimcEclipseCase.h @@ -18,6 +18,8 @@ #pragma once +#include "RiaDefines.h" + #include "cafPdmField.h" #include "cafPdmObjectHandle.h" #include "cafPdmObjectMethod.h" @@ -117,18 +119,18 @@ public: }; //================================================================================================== -/// Return the data type ("FLOAT" or "INTEGER") of a grid property. Inverse of the data_type -/// flag passed to set_active_cell_property / set_grid_property. +/// Return the data type (one of caf::AppEnum) of a grid property. +/// Inverse of the data_type flag passed to set_active_cell_property / set_grid_property. //================================================================================================== -class RimcEclipseCase_propertyDataType : public caf::PdmObjectMethod +class RimcEclipseCase_propertyDataType : public caf::PdmEnumObjectMethod { CAF_PDM_HEADER_INIT; public: explicit RimcEclipseCase_propertyDataType( caf::PdmObjectHandle* self ); - std::expected execute() override; - QString classKeywordReturnedType() const override; + std::expected executeEnum() override; + QString returnEnumScriptName() const override { return "PropertyDataType"; } private: caf::PdmField m_propertyType; diff --git a/Fwk/AppFwk/cafPdmScripting/CMakeLists.txt b/Fwk/AppFwk/cafPdmScripting/CMakeLists.txt index 7b1ddcc8bc..f7db1e2860 100644 --- a/Fwk/AppFwk/cafPdmScripting/CMakeLists.txt +++ b/Fwk/AppFwk/cafPdmScripting/CMakeLists.txt @@ -12,6 +12,8 @@ set(PROJECT_FILES cafPdmAbstractFieldScriptingCapability.cpp cafPdmFieldScriptingCapability.h cafPdmFieldScriptingCapability.cpp + cafPdmDataContainerEnum.h + cafPdmDataContainerEnum.cpp cafPdmObjectMethod.h cafPdmObjectMethod.cpp cafPdmObjectScriptingCapabilityRegister.h diff --git a/Fwk/AppFwk/cafPdmScripting/cafPdmDataContainerEnum.cpp b/Fwk/AppFwk/cafPdmScripting/cafPdmDataContainerEnum.cpp new file mode 100644 index 0000000000..ab31a8f023 --- /dev/null +++ b/Fwk/AppFwk/cafPdmScripting/cafPdmDataContainerEnum.cpp @@ -0,0 +1,55 @@ +//################################################################################################## +// +// Custom Visualization Core library +// Copyright (C) 2026- Equinor ASA +// +// 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 "cafPdmDataContainerEnum.h" + +#include "cafPdmFieldScriptingCapability.h" +#include "cafPdmObjectScriptingCapability.h" + +namespace caf +{ +CAF_PDM_SOURCE_INIT( PdmDataContainerEnum, "DataContainerEnum" ); + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +PdmDataContainerEnum::PdmDataContainerEnum() +{ + CAF_PDM_InitScriptableObject( "Data Container Enum" ); + CAF_PDM_InitScriptableField( &m_value, "value", QString(), "Value" ); +} + +} // namespace caf diff --git a/Fwk/AppFwk/cafPdmScripting/cafPdmDataContainerEnum.h b/Fwk/AppFwk/cafPdmScripting/cafPdmDataContainerEnum.h new file mode 100644 index 0000000000..8292dbf246 --- /dev/null +++ b/Fwk/AppFwk/cafPdmScripting/cafPdmDataContainerEnum.h @@ -0,0 +1,60 @@ +//################################################################################################## +// +// Custom Visualization Core library +// Copyright (C) 2026- Equinor ASA +// +// 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. +// +//################################################################################################## +#pragma once + +#include "cafPdmField.h" +#include "cafPdmObject.h" + +#include + +namespace caf +{ +//================================================================================================== +/// Container returned by scriptable methods that produce a single enum value. The Python code +/// generator detects methods declaring an enum return type (see PdmEnumObjectMethod) and wraps +/// the container's `value` field into the corresponding Python StrEnum at the call site. +//================================================================================================== +class PdmDataContainerEnum : public PdmObject +{ + CAF_PDM_HEADER_INIT; + +public: + PdmDataContainerEnum(); + + PdmField m_value; +}; + +} // namespace caf diff --git a/Fwk/AppFwk/cafPdmScripting/cafPdmObjectMethod.h b/Fwk/AppFwk/cafPdmScripting/cafPdmObjectMethod.h index edaa9d7316..adcaa7fd77 100644 --- a/Fwk/AppFwk/cafPdmScripting/cafPdmObjectMethod.h +++ b/Fwk/AppFwk/cafPdmScripting/cafPdmObjectMethod.h @@ -35,10 +35,14 @@ //################################################################################################## #pragma once +#include "cafAppEnum.h" #include "cafAssert.h" +#include "cafPdmDataContainerEnum.h" #include "cafPdmObject.h" #include "cafPdmPointer.h" +#include + #include /// CAF_PDM_OBJECT_METHOD_SOURCE_INIT associates the self class keyword and the method keyword with the method factory @@ -84,6 +88,14 @@ public: virtual QString selfClassKeyword() const; virtual QString classKeywordReturnedType() const = 0; + /// Optional metadata describing an enum return value. When returnEnumScriptName() is non-empty + /// the Python code generator emits a StrEnum class with the listed texts and wraps the call + /// site so the caller receives a typed enum value rather than the raw container. execute() + /// must still return a PdmObject whose scriptable "value" field carries the enum text — use + /// PdmDataContainerEnum (or derive from PdmEnumObjectMethod which handles this for you). + virtual QString returnEnumScriptName() const { return {}; } + virtual QStringList returnEnumTexts() const { return {}; } + bool isNullptrValidResult() const; bool resultIsPersistent() const; @@ -126,6 +138,49 @@ public: PdmObjectCreationMethod( PdmObjectHandle* self ); }; +//================================================================================================== +/// Base class for scriptable methods that return a single value drawn from a caf::AppEnum. +/// Subclasses only implement executeEnum() (returning the typed enum value) and returnEnumScriptName() +/// (the desired Python class name). The base wraps the value into a PdmDataContainerEnum at gRPC +/// boundaries and feeds the AppEnum's registered texts to the Python code generator so the call site +/// returns a typed StrEnum value. +//================================================================================================== +template +class PdmEnumObjectMethod : public PdmObjectMethod +{ +public: + explicit PdmEnumObjectMethod( PdmObjectHandle* self ) + : PdmObjectMethod( self, NullPointerType::NULL_IS_INVALID, ResultType::PERSISTENT_FALSE ) + { + } + + /// Subclasses produce the typed enum value. The base wraps it into the PdmDataContainerEnum + /// returned across gRPC. + virtual std::expected executeEnum() = 0; + + std::expected execute() final + { + auto result = executeEnum(); + if ( !result ) return std::unexpected( result.error() ); + + auto* container = new PdmDataContainerEnum(); + container->m_value = caf::AppEnum::text( *result ); + return container; + } + + QString classKeywordReturnedType() const final { return PdmDataContainerEnum::classKeywordStatic(); } + + QStringList returnEnumTexts() const final + { + QStringList texts; + for ( size_t i = 0; i < caf::AppEnum::size(); ++i ) + { + texts.push_back( caf::AppEnum::textFromIndex( i ) ); + } + return texts; + } +}; + //================================================================================================== /// PdmObject script method factory /// Register methods with this factory to be able to create and call methods. diff --git a/Fwk/AppFwk/cafPdmScripting/cafPdmPythonGenerator.cpp b/Fwk/AppFwk/cafPdmScripting/cafPdmPythonGenerator.cpp index 1dd6986171..1e0d9b7ebd 100644 --- a/Fwk/AppFwk/cafPdmScripting/cafPdmPythonGenerator.cpp +++ b/Fwk/AppFwk/cafPdmScripting/cafPdmPythonGenerator.cpp @@ -249,6 +249,39 @@ QString caf::PdmPythonGenerator::generate( PdmObjectFactory* factory, std::vecto return candidate; }; + 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 { @@ -479,19 +512,31 @@ QString caf::PdmPythonGenerator::generate( PdmObjectFactory* factory, std::vecto QString returnDataType = "None"; QString returnComment; + QString returnEnumClassName = + enumClassNameForMethodReturn( method->returnEnumScriptName(), method->returnEnumTexts() ); + QString containerClassName; if ( !method->classKeywordReturnedType().isEmpty() ) { QString classKeywordReturnedType = method->classKeywordReturnedType(); returnComment = classKeywordReturnedType; - returnDataType = + containerClassName = PdmObjectScriptingCapabilityRegister::scriptClassNameFromClassKeyword( classKeywordReturnedType ); - outputArgumentStrings.push_back( QString( "%1" ).arg( returnDataType ) ); + 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 ) @@ -544,7 +589,15 @@ QString caf::PdmPythonGenerator::generate( PdmObjectFactory* factory, std::vecto QString methodBody = QString( "self._call_pdm_method_void(%1)" ).arg( outputArgumentStrings.join( ", " ) ); if ( returnDataType != "None" ) { - if ( method->isNullptrValidResult() ) + 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( ", " ) ); diff --git a/GrpcInterface/Python/rips/__init__.py b/GrpcInterface/Python/rips/__init__.py index 2807d97df5..5834c49f34 100644 --- a/GrpcInterface/Python/rips/__init__.py +++ b/GrpcInterface/Python/rips/__init__.py @@ -31,11 +31,11 @@ from . import well_events as well_events # noqa: F401, E402 from . import category_mapping as category_mapping # noqa: F401, E402 # Enums for proto types not visible to the AppEnum auto-generator. -# PorosityModelType is auto-generated into resinsight_classes (already imported -# above) and intentionally not re-imported here to avoid a duplicate symbol. +# PorosityModelType and PropertyDataType are auto-generated into resinsight_classes +# (already imported above) and intentionally not re-imported here to avoid a +# duplicate symbol. from .enums import ( # noqa: E402 PropertyType as PropertyType, - PropertyDataType as PropertyDataType, NNCPropertyType as NNCPropertyType, ) diff --git a/GrpcInterface/Python/rips/case.py b/GrpcInterface/Python/rips/case.py index 29c7129232..926d5fc6ec 100644 --- a/GrpcInterface/Python/rips/case.py +++ b/GrpcInterface/Python/rips/case.py @@ -51,10 +51,10 @@ import NNCProperties_pb2 import NNCProperties_pb2_grpc from .resinsight_classes import ( Case as Case, - DataContainerString as DataContainerString, EclipseCase as EclipseCase, GeoMechCase as GeoMechCase, PorosityModelType as PorosityModelType, + PropertyDataType as PropertyDataType, Reservoir as Reservoir, WellBoreStabilityPlot as WellBoreStabilityPlot, WbsParameters as WbsParameters, @@ -68,7 +68,6 @@ from .view import View as View from .simulation_well import SimulationWell from .enums import ( PropertyType, - PropertyDataType, ) import rips.project # full name import due to circular dependency @@ -674,36 +673,6 @@ def available_properties( return self.__properties_stub.GetAvailableProperties(request).property_names -@add_method(Reservoir) -def property_data_type( - self, - property_type: PropertyType, - property_name: str, - porosity_model: PorosityModelType = PorosityModelType.MATRIX_MODEL, -) -> PropertyDataType: - """Get the data type (FLOAT or INTEGER) of a grid property. - - Inverse of the data_type argument to set_active_cell_property and - set_grid_property. - - Arguments: - property_type (str): string corresponding to property_type enum. - property_name (str): name of an Eclipse property. - porosity_model (str): 'MATRIX_MODEL' or 'FRACTURE_MODEL'. - - Returns: - PropertyDataType.FLOAT or PropertyDataType.INTEGER. - """ - result = self._call_pdm_method_return_value( - "property_data_type", - DataContainerString, - property_type=property_type, - property_name=property_name, - porosity_model=porosity_model, - ) - return PropertyDataType(result.values[0]) - - @add_method(Case) def active_cell_property_async( self, diff --git a/GrpcInterface/Python/rips/enums.py b/GrpcInterface/Python/rips/enums.py index 8d013168b2..b14a99b31a 100644 --- a/GrpcInterface/Python/rips/enums.py +++ b/GrpcInterface/Python/rips/enums.py @@ -5,11 +5,12 @@ and ``NNCProperties.proto``. They give users typed, autocompletable enum members while remaining wire-compatible with the existing string-based API (``StrEnum`` members are strings). -Auto-generated ``StrEnum`` classes for ``caf::AppEnum`` scriptable fields live in -``rips.generated.generated_classes`` and are emitted by ``cafPdmPythonGenerator``. -``PorosityModelType`` is one of those auto-generated enums and is imported from -``resinsight_classes``; the enums in this module cover the gRPC-only proto enums -that the auto-generator does not see. +Auto-generated ``StrEnum`` classes for ``caf::AppEnum`` scriptable fields and for +PdmObjectMethod enum return values live in ``rips.generated.generated_classes`` +and are emitted by ``cafPdmPythonGenerator``. ``PorosityModelType`` and +``PropertyDataType`` are auto-generated and imported from ``resinsight_classes``; +the enums in this module cover the gRPC-only proto enums that the auto-generator +does not see. """ from enum import StrEnum @@ -29,12 +30,6 @@ class PropertyType(StrEnum): UNDEFINED = "UNDEFINED" -# Keep in sync with PropertyDataType in GrpcInterface/GrpcProtos/Properties.proto -class PropertyDataType(StrEnum): - FLOAT = "FLOAT" - INTEGER = "INTEGER" - - # Keep in sync with NNCPropertyType in GrpcInterface/GrpcProtos/NNCProperties.proto class NNCPropertyType(StrEnum): NNC_DYNAMIC = "NNC_DYNAMIC"