#!/usr/bin/env perl use strict; use warnings; use File::Basename; use File::Spec; use Getopt::Long; use YAML::Tiny; use List::Util qw(any); use List::Util qw(none); no if $] >= 5.018000, warnings => 'experimental::smartmatch'; use constant PRIVATE => 0; use constant PROTECTED => 1; use constant PUBLIC => 2; use constant STRICT => 10; use constant UNSTRICT => 11; use constant MULTILINE_NO => 20; use constant MULTILINE_METHOD => 21; use constant MULTILINE_CONDITIONAL_STATEMENT => 22; use constant CODE_SNIPPET => 30; use constant CODE_SNIPPET_CPP => 31; use constant PREPEND_CODE_NO => 40; use constant PREPEND_CODE_VIRTUAL => 41; use constant PREPEND_CODE_MAKE_PRIVATE => 42; # read arguments my $debug = 0; my $sip_output = ''; my $is_qt6 = 0; my $python_output = ''; my $class_map_file = ''; #my $SUPPORT_TEMPLATE_DOCSTRING = 0; #die("usage: $0 [-debug] [-template-doc] headerfile\n") unless GetOptions ("debug" => \$debug, "template-doc" => \$SUPPORT_TEMPLATE_DOCSTRING) && @ARGV == 1; die("usage: $0 [-debug] [-qt6] [-sip_output FILE] [-python_output FILE] [-class_map FILE] headerfile\n") unless GetOptions ("debug" => \$debug, "sip_output=s" => \$sip_output, "python_output=s" => \$python_output, "qt6" => \$is_qt6, "class_map=s" => \$class_map_file) && @ARGV == 1; my $headerfile = $ARGV[0]; # read file open(my $handle, "<", $headerfile) || die "Couldn't open '".$headerfile."' for reading because: ".$!; chomp(my @INPUT_LINES = <$handle>); close $handle; # config my $cfg_file = File::Spec->catfile( dirname(__FILE__), '../python/sipify.yaml' ); my $yaml = YAML::Tiny->read( $cfg_file ); my $SIP_CONFIG = $yaml->[0]; # contexts my $SIP_RUN = 0; my $HEADER_CODE = 0; my @ACCESS = (PUBLIC); my @CLASSNAME = (); my @CLASS_AND_STRUCT = (); my @DECLARED_CLASSES = (); my @EXPORTED = (0); my $MULTILINE_DEFINITION = MULTILINE_NO; my $ACTUAL_CLASS = ''; my $PYTHON_SIGNATURE = ''; my @ENUM_INT_TYPES = (); my @ENUM_INTFLAG_TYPES = (); my @ENUM_CLASS_NON_INT_TYPES = (); my @ENUM_MONKEY_PATCHED_TYPES = (); my $INDENT = ''; my $PREV_INDENT = ''; my $COMMENT = ''; my $COMMENT_PARAM_LIST = 0; my $COMMENT_LAST_LINE_NOTE_WARNING = 0; my $COMMENT_CODE_SNIPPET = 0; my $COMMENT_TEMPLATE_DOCSTRING = 0; my @SKIPPED_PARAMS_OUT = (); my @SKIPPED_PARAMS_REMOVE = (); my $GLOB_IFDEF_NESTING_IDX = 0; my @GLOB_BRACKET_NESTING_IDX = (0); my $PRIVATE_SECTION_LINE = ''; my $LAST_ACCESS_SECTION_LINE = ''; my $RETURN_TYPE = ''; my $IS_OVERRIDE_OR_MAKE_PRIVATE = PREPEND_CODE_NO; my $IF_FEATURE_CONDITION = ''; my $FOUND_SINCE = 0; my %QFLAG_HASH; my $LINE_COUNT = @INPUT_LINES; my $LINE_IDX = 0; my $LINE; my @OUTPUT = (); my @OUTPUT_PYTHON = (); my $DOXY_INSIDE_SIP_RUN = 0; my $HAS_PUSHED_FORCE_INT = 0; my @ALLOWED_NON_CLASS_ENUMS = ( "QgsSipifyHeader::MyEnum", "QgsSipifyHeader::OneLiner", "CadConstraint::LockMode", "ColorrampTable", "ElementType", "LabelSettingsTable", "Qgis::MessageLevel", "Qgs3DMapScene::SceneState", "Qgs3DTypes::CullingMode", "Qgs3DTypes::Flag3DRenderer", "QgsAbstractDatabaseProviderConnection::Capability", "QgsAbstractDatabaseProviderConnection::GeometryColumnCapability", "QgsAbstractFeatureIterator::CompileStatus", "QgsAbstractGeometry::AxisOrder", "QgsAbstractGeometry::SegmentationToleranceType", "QgsAbstractGeometry::WkbFlag", "QgsAbstractReportSection::SubSection", "QgsAdvancedDigitizingDockWidget::CadCapacity", "QgsAdvancedDigitizingDockWidget::WidgetSetMode", "QgsApplication::Cursor", "QgsApplication::StyleSheetType", "QgsApplication::endian_t", "QgsArrowSymbolLayer::ArrowType", "QgsArrowSymbolLayer::HeadType", "QgsAttributeEditorContext::FormMode", "QgsAttributeEditorContext::Mode", "QgsAttributeEditorContext::RelationMode", "QgsAttributeEditorRelation::Button", "QgsAttributeForm::FilterType", "QgsAttributeForm::Mode", "QgsAttributeFormWidget::Mode", "QgsAttributeTableConfig::ActionWidgetStyle", "QgsAttributeTableConfig::Type", "QgsAttributeTableFilterModel::ColumnType", "QgsAttributeTableFilterModel::FilterMode", "QgsAuthCertUtils::CaCertSource", "QgsAuthCertUtils::CertTrustPolicy", "QgsAuthCertUtils::CertUsageType", "QgsAuthCertUtils::ConstraintGroup", "QgsAuthImportCertDialog::CertFilter", "QgsAuthImportCertDialog::CertInput", "QgsAuthImportIdentityDialog::BundleTypes", "QgsAuthImportIdentityDialog::IdentityType", "QgsAuthImportIdentityDialog::Validity", "QgsAuthManager::MessageLevel", "QgsAuthMethod::Expansion", "QgsAuthSettingsWidget::WarningType", "QgsBasicNumericFormat::RoundingType", "QgsBearingNumericFormat::FormatDirectionOption", "QgsBlockingNetworkRequest::ErrorCode", "QgsBlurEffect::BlurMethod", "QgsBookmarkManagerModel::Columns", "QgsBrowserProxyModel::FilterSyntax", "QgsCallout::AnchorPoint", "QgsCallout::DrawOrder", "QgsCallout::LabelAnchorPoint", "QgsCheckBoxFieldFormatter::TextDisplayMethod", "QgsClassificationLogarithmic::NegativeValueHandling", "QgsClassificationMethod::ClassPosition", "QgsClassificationMethod::MethodProperty", "QgsClipper::Boundary", "QgsColorButton::Behavior", "QgsColorRampLegendNodeSettings::Direction", "QgsColorRampShader::ClassificationMode", "QgsColorRampShader::Type", "QgsColorRampWidget::Orientation", "QgsColorScheme::SchemeFlag", "QgsColorTextWidget::ColorTextFormat", "QgsColorWidget::ColorComponent", "QgsCompoundColorWidget::Layout", "QgsContrastEnhancement::ContrastEnhancementAlgorithm", "QgsCoordinateFormatter::Format", "QgsCoordinateFormatter::FormatFlag", "QgsCoordinateReferenceSystem::CrsType", "QgsCoordinateReferenceSystemProxyModel::Filter", "QgsCptCityBrowserModel::ViewType", "QgsCptCityDataItem::Type", "QgsCurvedLineCallout::Orientation", "QgsDartMeasurement::Type", "QgsDataDefinedSizeLegend::LegendType", "QgsDataDefinedSizeLegend::VerticalAlignment", "QgsDataProvider::DataCapability", "QgsDataProvider::ProviderProperty", "QgsDataProvider::ReadFlag", "QgsDataSourceUri::SslMode", "QgsDiagramLayerSettings::LinePlacementFlag", "QgsDiagramLayerSettings::Placement", "QgsDiagramLayerSettings::DiagramType", "QgsDiagramSettings::DiagramOrientation", "QgsDiagramSettings::Direction", "QgsDiagramSettings::LabelPlacementMethod", "QgsDiagramSettings::StackedDiagramMode", "QgsDoubleSpinBox::ClearValueMode", "QgsDualView::FeatureListBrowsingAction", "QgsDualView::ViewMode", "QgsDxfExport::DxfPolylineFlag", "QgsDxfExport::Flag", "QgsEditorWidgetWrapper::ConstraintResult", "QgsEllipseSymbolLayer::Shape", "QgsErrorMessage::Format", "QgsExpression::ParserErrorType", "QgsExpression::SpatialOperator", "QgsExpressionBuilderWidget::Flag", "QgsExpressionItem::ItemType", "QgsExpressionNode::NodeType", "QgsExpressionNodeBinaryOperator::BinaryOperator", "QgsExpressionNodeUnaryOperator::UnaryOperator", "QgsExtentGroupBox::ExtentState", "QgsExtentWidget::ExtentState", "QgsExtentWidget::WidgetStyle", "QgsExternalResourceWidget::DocumentViewerContent", "QgsFeatureListModel::Role", "QgsFeatureListViewDelegate::Element", "QgsFeatureRenderer::Capability", "QgsFeatureSink::Flag", "QgsFeatureSink::SinkFlag", "QgsFetchedContent::ContentStatus", "QgsFieldConstraints::Constraint", "QgsFieldConstraints::ConstraintOrigin", "QgsFieldConstraints::ConstraintStrength", "QgsFieldFormatter::Flag", "QgsFieldProxyModel::Filter", "QgsFields::FieldOrigin", "QgsFileWidget::RelativeStorage", "QgsFileWidget::StorageMode", "QgsFilterLineEdit::ClearMode", "QgsFloatingWidget::AnchorPoint", "QgsFontButton::Mode", "QgsGeometryCheck::ChangeType", "QgsGeometryCheck::ChangeWhat", "QgsGeometryCheck::CheckType", "QgsGeometryCheck::Flag", "QgsGeometryCheckError::Status", "QgsGeometryCheckError::ValueType", "QgsGeometryEngine::EngineOperationResult", "QgsGeometryRubberBand::IconType", "QgsGeometrySnapper::SnapMode", "QgsGlowEffect::GlowColorType", "QgsGpsConnection::Status", "QgsGraduatedSymbolRenderer::Mode", "QgsGui::HigFlag", "QgsGui::ProjectCrsBehavior", "QgsHueSaturationFilter::GrayscaleMode", "QgsIdentifyMenu::MenuLevel", "QgsImageOperation::FlipType", "QgsImageOperation::GrayscaleMode", "QgsInterpolatedLineColor::ColoringMethod", "QgsInterpolator::Result", "QgsInterpolator::SourceType", "QgsInterpolator::ValueSource", "QgsKernelDensityEstimation::KernelShape", "QgsKernelDensityEstimation::OutputValues", "QgsKernelDensityEstimation::Result", "QgsLabelingEngineSettings::Search", "QgsLayerMetadataResultsModel::Roles", "QgsLayerMetadataResultsModel::Sections", "QgsLayerTreeLayer::LegendNodesSplitBehavior", "QgsLayerTreeModel::Flag", "QgsLayerTreeModelLegendNode::NodeTypes", "QgsLayerTreeNode::NodeType", "QgsLayout::UndoCommand", "QgsLayout::ZValues", "QgsLayoutAligner::Alignment", "QgsLayoutAligner::Distribution", "QgsLayoutAligner::Resize", "QgsLayoutDesignerInterface::StandardTool", "QgsLayoutExporter::ExportResult", "QgsLayoutGridSettings::Style", "QgsLayoutItem::ExportLayerBehavior", "QgsLayoutItem::Flag", "QgsLayoutItem::ReferencePoint", "QgsLayoutItem::UndoCommand", "QgsLayoutItemAbstractGuiMetadata::Flag", "QgsLayoutItemAttributeTable::ContentSource", "QgsLayoutItemHtml::ContentMode", "QgsLayoutItemLabel::Mode", "QgsLayoutItemMap::AtlasScalingMode", "QgsLayoutItemMap::MapItemFlag", "QgsLayoutItemMapGrid::AnnotationCoordinate", "QgsLayoutItemMapGrid::AnnotationDirection", "QgsLayoutItemMapGrid::AnnotationFormat", "QgsLayoutItemMapGrid::AnnotationPosition", "QgsLayoutItemMapGrid::BorderSide", "QgsLayoutItemMapGrid::DisplayMode", "QgsLayoutItemMapGrid::FrameSideFlag", "QgsLayoutItemMapGrid::FrameStyle", "QgsLayoutItemMapGrid::GridStyle", "QgsLayoutItemMapGrid::GridUnit", "QgsLayoutItemMapGrid::TickLengthMode", "QgsLayoutItemMapItem::StackingPosition", "QgsLayoutItemPage::Orientation", "QgsLayoutItemPage::UndoCommand", "QgsLayoutItemPicture::Format", "QgsLayoutItemPicture::NorthMode", "QgsLayoutItemPicture::ResizeMode", "QgsLayoutItemPolyline::MarkerMode", "QgsLayoutItemRegistry::ItemType", "QgsLayoutItemShape::Shape", "QgsLayoutManagerProxyModel::Filter", "QgsLayoutModel::Columns", "QgsLayoutMultiFrame::ResizeMode", "QgsLayoutMultiFrame::UndoCommand", "QgsLayoutNorthArrowHandler::NorthMode", "QgsLayoutObject::PropertyValueType", "QgsLayoutRenderContext::Flag", "QgsLayoutTable::CellStyleGroup", "QgsLayoutTable::EmptyTableMode", "QgsLayoutTable::HeaderHAlignment", "QgsLayoutTable::HeaderMode", "QgsLayoutTable::WrapBehavior", "QgsLayoutView::ClipboardOperation", "QgsLayoutView::PasteMode", "QgsLayoutViewTool::Flag", "QgsLegendStyle::Side", "QgsLegendStyle::Style", "QgsLineSymbolLayer::RenderRingFilter", "QgsLocatorFilter::Flag", "QgsLocatorFilter::Priority", "QgsManageConnectionsDialog::Mode", "QgsManageConnectionsDialog::Type", "QgsMapBoxGlStyleConverter::Result", "QgsMapCanvasAnnotationItem::MouseMoveAction", "QgsMapLayer::LayerFlag", "QgsMapLayer::PropertyType", "QgsMapLayer::ReadFlag", "QgsMapLayer::StyleCategory", "QgsMapLayerDependency::Origin", "QgsMapLayerDependency::Type", "QgsMapLayerElevationProperties::Flag", "QgsMapRendererTask::ErrorType", "QgsMapToPixelSimplifier::SimplifyAlgorithm", "QgsMapToPixelSimplifier::SimplifyFlag", "QgsMapTool::Flag", "QgsMapToolCapture::Capability", "QgsMapToolCapture::CaptureMode", "QgsMapToolEdit::TopologicalResult", "QgsMapToolIdentify::IdentifyMode", "QgsMapToolIdentify::Type", "QgsMarkerSymbolLayer::HorizontalAnchorPoint", "QgsMarkerSymbolLayer::VerticalAnchorPoint", "QgsMasterLayoutInterface::Type", "QgsMediaWidget::Mode", "QgsMergedFeatureRenderer::GeometryOperation", "QgsMesh3DAveragingMethod::Method", "QgsMeshCalculator::Result", "QgsMeshDataBlock::DataType", "QgsMeshDataProviderTemporalCapabilities::MatchingTemporalDatasetMethod", "QgsMeshDatasetGroup::Type", "QgsMeshDatasetGroupMetadata::DataType", "QgsMeshDriverMetadata::MeshDriverCapability", "QgsMeshRendererScalarSettings::DataResamplingMethod", "QgsMeshRendererVectorArrowSettings::ArrowScalingMethod", "QgsMeshRendererVectorSettings::Symbology", "QgsMeshRendererVectorStreamlineSettings::SeedingStartPointsMethod", "QgsMeshTimeSettings::TimeUnit", "QgsMessageOutput::MessageType", "QgsMetadataWidget::Mode", "QgsModelArrowItem::Marker", "QgsModelComponentGraphicItem::Flag", "QgsModelComponentGraphicItem::State", "QgsModelGraphicsScene::Flag", "QgsModelGraphicsScene::ZValues", "QgsModelGraphicsView::ClipboardOperation", "QgsModelGraphicsView::PasteMode", "QgsMultiEditToolButton::State", "QgsNetworkRequestParameters::RequestAttributes", "QgsNewGeoPackageLayerDialog::OverwriteBehavior", "QgsNewHttpConnection::ConnectionType", "QgsNewHttpConnection::Flag", "QgsNewHttpConnection::WfsVersionIndex", "QgsOfflineEditing::ContainerType", "QgsOfflineEditing::ProgressMode", "QgsOgcUtils::FilterVersion", "QgsOgcUtils::GMLVersion", "QgsPaintEffect::DrawMode", "QgsPercentageNumericFormat::InputValues", "QgsPictureSourceLineEditBase::Format", "QgsPointCloud3DSymbol::RenderingStyle", "QgsPointCloudAttribute::DataType", "QgsPointCloudAttributeProxyModel::Filter", "QgsPointCloudDataProvider::Capability", "QgsPointCloudDataProvider::PointCloudIndexGenerationState", "QgsPointDisplacementRenderer::Placement", "QgsPointLocator::Type", "QgsPreviewEffect::PreviewMode", "QgsProcessing::SourceType", "QgsProcessingAlgorithm::Flag", "QgsProcessingAlgorithm::PropertyAvailability", "QgsProcessingAlgorithmDialogBase::LogFormat", "QgsProcessingContext::Flag", "QgsProcessingContext::LogLevel", "QgsProcessingFeatureSource::Flag", "QgsProcessingGui::WidgetType", "QgsProcessingParameterDateTime::Type", "QgsProcessingParameterDefinition::Flag", "QgsProcessingParameterField::DataType", "QgsProcessingParameterFile::Behavior", "QgsProcessingParameterNumber::Type", "QgsProcessingParameterTinInputLayers::Type", "QgsProcessingParameterType::ParameterFlag", "QgsProcessingProvider::Flag", "QgsProcessingToolboxModelNode::NodeType", "QgsProcessingToolboxProxyModel::Filter", "QgsProjectBadLayerHandler::DataType", "QgsProjectBadLayerHandler::ProviderType", "QgsProjectServerValidator::ValidationError", "QgsProjectionSelectionWidget::CrsOption", "QgsPropertyDefinition::DataType", "QgsPropertyDefinition::StandardPropertyTemplate", "QgsPropertyTransformer::Type", "QgsProviderMetadata::ProviderCapability", "QgsProviderMetadata::ProviderMetadataCapability", "QgsProviderRegistry::WidgetMode", "QgsQuadrilateral::ConstructionOption", "QgsQuadrilateral::Point", "QgsRasterCalcNode::Operator", "QgsRasterCalcNode::Type", "QgsRasterCalculator::Result", "QgsRasterDataProvider::ProviderCapability", "QgsRasterDataProvider::TransformType", "QgsRasterFileWriter::RasterFormatOption", "QgsRasterFormatSaveOptionsWidget::Type", "QgsRasterInterface::Capability", "QgsRasterLayerSaveAsDialog::CrsState", "QgsRasterLayerSaveAsDialog::Mode", "QgsRasterLayerSaveAsDialog::ResolutionState", "QgsRasterMatrix::OneArgOperator", "QgsRasterMatrix::TwoArgOperator", "QgsRasterMinMaxOrigin::Extent", "QgsRasterMinMaxOrigin::Limits", "QgsRasterMinMaxOrigin::StatAccuracy", "QgsRasterProjector::Precision", "QgsRasterRange::BoundsType", "QgsReadWriteLocker::Mode", "QgsRegularPolygon::ConstructionOption", "QgsRelationEditorWidget::Button", "QgsRelationReferenceWidget::CanvasExtent", "QgsRendererAbstractMetadata::LayerType", "QgsReportSectionFieldGroup::SectionVisibility", "QgsRubberBand::IconType", "QgsRuleBasedRenderer::FeatureFlags", "QgsSQLStatement::BinaryOperator", "QgsSQLStatement::JoinType", "QgsSQLStatement::NodeType", "QgsSQLStatement::UnaryOperator", "QgsScaleBarSettings::Alignment", "QgsScaleBarSettings::LabelHorizontalPlacement", "QgsScaleBarSettings::LabelVerticalPlacement", "QgsScaleBarSettings::SegmentSizeMode", "QgsSearchWidgetWrapper::FilterFlag", "QgsServerOgcApi::ContentType", "QgsServerOgcApi::Rel", "QgsServerParameter::Name", "QgsServerRequest::Method", "QgsServerRequest::RequestHeader", "QgsServerSettingsEnv::EnvVar", "QgsServerSettingsEnv::Source", "QgsServerWmsDimensionProperties::DefaultDisplay", "QgsServerWmsDimensionProperties::PredefinedWmsDimensionName", "QgsSettings::Section", "QgsSimplifyMethod::MethodType", "QgsSingleBandGrayRenderer::Gradient", "QgsSizeScaleTransformer::ScaleType", "QgsSnappingConfig::ScaleDependencyMode", "QgsSnappingConfig::SnappingType", "QgsSnappingUtils::IndexingStrategy", "QgsSourceSelectProvider::Ordering", "QgsSpatialIndex::Flag", "QgsSpinBox::ClearValueMode", "QgsStatusBar::Anchor", "QgsStoredExpression::Category", "QgsStyle::StyleEntity", "QgsStyleExportImportDialog::Mode", "QgsStyleModel::Column", "QgsSublayersDialog::PromptMode", "QgsSublayersDialog::ProviderType", "QgsTask::Flag", "QgsTask::SubTaskDependency", "QgsTask::TaskStatus", "QgsTemporalProperty::Flag", "QgsTextBackgroundSettings::RotationType", "QgsTextBackgroundSettings::ShapeType", "QgsTextBackgroundSettings::SizeType", "QgsTextDiagram::Orientation", "QgsTextDiagram::Shape", "QgsTextFormatWidget::Mode", "QgsTextMaskSettings::MaskType", "QgsTextShadowSettings::ShadowPlacement", "QgsTicksScaleBarRenderer::TickPosition", "QgsTinInterpolator::TinInterpolation", "QgsTracer::PathError", "QgsValidityCheckContext::ContextType", "QgsValidityCheckResult::Type", "QgsVectorDataProvider::Capability", "QgsVectorFieldSymbolLayer::AngleOrientation", "QgsVectorFieldSymbolLayer::AngleUnits", "QgsVectorFieldSymbolLayer::VectorFieldType", "QgsVectorFileWriter::ActionOnExistingFile", "QgsVectorFileWriter::EditionCapability", "QgsVectorFileWriter::FieldNameSource", "QgsVectorFileWriter::OptionType", "QgsVectorFileWriter::VectorFormatOption", "QgsVectorFileWriter::WriterError", "QgsVectorLayerDirector::Direction", "QgsVectorLayerUtils::CascadedFeatureFlag", "QgsVectorSimplifyMethod::SimplifyAlgorithm", "QgsVectorSimplifyMethod::SimplifyHint", "QgsVertexMarker::IconType", "QgsWeakRelation::WeakRelationType", "QgsWindowManagerInterface::StandardDialog", "Rule::RegisterResult", "Rule::RenderResult", "SmartgroupTable", "SymbolTable", "TagTable", "TagmapTable", "TextFormatTable" ); sub read_line { my $new_line = $INPUT_LINES[$LINE_IDX]; $LINE_IDX++; $debug == 0 or print sprintf('LIN:%d DEPTH:%d ACC:%d BRCK:%d SIP:%d MLT:%d OVR: %d CLSS: %s/%d', $LINE_IDX, $#ACCESS, $ACCESS[$#ACCESS], $GLOB_BRACKET_NESTING_IDX[$#GLOB_BRACKET_NESTING_IDX], $SIP_RUN, $MULTILINE_DEFINITION, $IS_OVERRIDE_OR_MAKE_PRIVATE, $ACTUAL_CLASS, $#CLASSNAME)." :: ".$new_line."\n"; $new_line = replace_macros($new_line); return $new_line; } sub write_output { my ($dbg_code, $out, $prepend) = @_; $prepend //= "no"; if ($debug == 1){ $dbg_code = sprintf("%d %-4s :: ", $LINE_IDX, $dbg_code); } else{ $dbg_code = ''; } if ($prepend eq "prepend") { unshift @OUTPUT, $dbg_code . $out; } else { push @OUTPUT, "%If ($IF_FEATURE_CONDITION)\n" if $IF_FEATURE_CONDITION ne ''; push @OUTPUT, $dbg_code . $out; push @OUTPUT, "%End\n" if $IF_FEATURE_CONDITION ne ''; } $IF_FEATURE_CONDITION = ''; } sub dbg_info { my $info = $_[0]; if ($debug == 1){ push @OUTPUT, $info."\n"; print $LINE_IDX." ".@ACCESS." ".$SIP_RUN." ".$MULTILINE_DEFINITION." ".$info."\n"; } } sub exit_with_error { die "! Sipify error in $headerfile at line :: $LINE_IDX\n! $_[0]\n"; } sub sip_header_footer { my @header_footer = (); # small hack to turn files src/core/3d/X.h to src/core/./3d/X.h # otherwise "sip up to date" test fails. This is because the test uses %Include entries # and over there we have to use ./3d/X.h entries because SIP parser does not allow a number # as the first letter of a relative path my $headerfile_x = $headerfile; $headerfile_x =~ s/src\/core\/3d/src\/core\/.\/3d/; push @header_footer, "/************************************************************************\n"; push @header_footer, " * This file has been generated automatically from *\n"; push @header_footer, " * *\n"; push @header_footer, sprintf " * %-*s *\n", 68, $headerfile_x; push @header_footer, " * *\n"; push @header_footer, " * Do not edit manually ! Edit header and run scripts/sipify.pl again *\n"; push @header_footer, " ************************************************************************/\n"; return @header_footer; } sub python_header { my @header = (); my $headerfile_x = $headerfile; $headerfile_x =~ s/src\/core\/3d/src\/core\/.\/3d/; push @header, "# The following has been generated automatically from "; push @header, sprintf "%s\n", $headerfile_x; return @header; } sub create_class_links { my $line = $_[0]; if ( $line =~ m/\b((?:Qgs[A-Z]\w+)|(?:Qgis))\b(\.?$|[^\w]{2})/) { if ( defined $ACTUAL_CLASS && $1 !~ $ACTUAL_CLASS ) { $line =~ s/\b(Qgs[A-Z]\w+)\b(\.?$|[^\w]{2})/:py:class:`$1`$2/g; } } $line =~ s/\b(((?:Qgs[A-Z]\w+)|(?:Qgis))\.[a-z]\w+\(\))(?!\w)/:py:func:`$1`/g; if ( defined $ACTUAL_CLASS && $ACTUAL_CLASS) { $line =~ s/(?sub)?section/) { my $sep = "-"; $sep = "~" if defined $+{SUB}; $line =~ s/^\\(sub)?section \w+ //; my $sep_line = $line =~ s/[\w ()]/$sep/gr; $line .= "\n".$sep_line; } # convert ### style headings if ( $line =~ m/^###\s+(.*)$/) { $line = "$1\n".('-' x length($1)); } if ( $line =~ m/^##\s+(.*)$/) { $line = "$1\n".('=' x length($1)); } if ( $line eq '*' ) { $line = ''; } # handle multi-line parameters/returns/lists if ($line ne '') { if ( $line =~ m/^\s*[\-#]/ ){ # start of a list item, ensure following lines are correctly indented $line = "$PREV_INDENT$line"; $INDENT = $PREV_INDENT." "; } elsif ( $line !~ m/^\s*[\\:]+(param|note|since|return|deprecated|warning|throws)/ ) { # if inside multi-line parameter, ensure additional lines are indented $line = "$INDENT$line"; } } else { $PREV_INDENT = $INDENT; $INDENT = ''; } # replace \returns with :return: if ( $line =~ m/\\return(s)?/ ){ $line =~ s/\s*\\return(s)?\s*/\n:return: /; # remove any trailing spaces, will be present now for empty 'returns' tags $line =~ s/\s*$//g; $INDENT = ' 'x( index($line,':',4) + 1); } # params if ( $line =~ m/\\param / ){ $line =~ s/\s*\\param\s+(\w+)\b\s*/:param $1: /g; # remove any trailing spaces, will be present now for empty 'param' tags $line =~ s/\s*$//g; $INDENT = ' 'x( index($line,':',2) + 2); if ( $line =~ m/^:param/ ){ if ( $COMMENT_PARAM_LIST == 0 ) { $line = "\n$line"; } $COMMENT_PARAM_LIST = 1; $COMMENT_LAST_LINE_NOTE_WARNING = 0; } } if ( $line =~ m/^\s*[\\@]brief/){ $line =~ s/[\\@]brief\s*//; if ( $FOUND_SINCE eq 1 ) { exit_with_error("$headerfile\:\:$LINE_IDX Since annotation must come after brief") } $FOUND_SINCE = 0; if ( $line =~ m/^\s*$/ ){ return ""; } } if ( $line =~ m/[\\@](ingroup|class)/ ) { $PREV_INDENT = $INDENT; $INDENT = ''; return ""; } if ( $line =~ m/\\since .*?([\d\.]+)/i ) { $PREV_INDENT = $INDENT; $INDENT = ''; $FOUND_SINCE = 1; return "\n.. versionadded:: $1\n"; } if ( $line =~ m/\\deprecated(?:\s+since\s+(?:QGIS\s+)(?[0-9.]+)(,\s*)?)?(?.*)?/i ) { $PREV_INDENT = $INDENT; $INDENT = ''; my $depr_line = "\n.. deprecated::"; $depr_line .= " QGIS $+{DEPR_VERSION}" if (defined $+{DEPR_VERSION}); $depr_line .= "\n $+{DEPR_MESSAGE}\n" if (defined $+{DEPR_MESSAGE}); return create_class_links($depr_line); } # create links in see also if ( $line =~ m/\\see +(\w+(\.\w+)*)(\([^()]*\))?/ ) { my $seealso = $1; my $seeline = ''; dbg_info("see also: $seealso"); if ( $seealso =~ m/^Qgs[A-Z]\w+(\([^()]*\))?$/ ) { dbg_info("\\see py:class"); $seeline = ":py:class:`$seealso`"; } elsif ( $seealso =~ m/^(Qgs[A-Z]\w+)\.(\w+)(\([^()]*\))?$/ ) { dbg_info("\\see py:func with param"); $seeline = ":py:func:`$1.$2`"; } elsif ( $seealso =~ m/^[a-z]\w+(\([^()]*\))?$/ ) { dbg_info("\\see py:func"); $seeline = ":py:func:`$seealso`"; } if ( $line =~ m/^\s*\\see/ ){ if ( $seeline ne ''){ return "\n.. seealso:: $seeline\n"; } else { return "\n.. seealso:: $seealso\n"; } } else { if ( $seeline ne ''){ $line =~ s/\\see +(\w+(\.\w+)*(\(\))?)/$seeline/; } else { $line =~s/\\see/see/; } } } elsif ( $line !~ m/\\throws.*/ ) { # create links in plain text too (less performant) # we don't do this for "throws" lines, as Sphinx does not format these correctly $line = create_class_links($line) } if ( $line =~ m/[\\@]note (.*)/ ) { $COMMENT_LAST_LINE_NOTE_WARNING = 1; $PREV_INDENT = $INDENT; $INDENT = ''; return "\n.. note::\n\n $1\n"; } if ( $line =~ m/[\\@]warning (.*)/ ) { $PREV_INDENT = $INDENT; $INDENT = ''; $COMMENT_LAST_LINE_NOTE_WARNING = 1; return "\n.. warning::\n\n $1\n"; } if ( $line =~ m/[\\@]throws (.+?)\b\s*(.*)/ ) { $PREV_INDENT = $INDENT; $INDENT = ''; $COMMENT_LAST_LINE_NOTE_WARNING = 1; return "\n:raises $1: $2\n"; } if ( $line !~ m/^\s*$/ ){ if ( $COMMENT_LAST_LINE_NOTE_WARNING == 1 ){ dbg_info("prepend spaces for multiline warning/note xx$line"); $line = " $line"; } } else { $COMMENT_LAST_LINE_NOTE_WARNING = 0; } return "$line\n"; } sub detect_and_remove_following_body_or_initializerlist { # https://regex101.com/r/ZaP3tC/8 my $python_signature = ''; do {no warnings 'uninitialized'; if ( $LINE =~ m/^(\s*)?((?:(?:explicit|static|const|unsigned|virtual)\s+)*)(([(?:long )\w:]+(<.*?>)?\s+[*&]?)?(~?\w+|(\w+::)?operator.{1,2})\s*\(([\w=()\/ ,&*<>."-]|::)*\)( +(?:const|SIP_[\w_]+?))*)\s*((\s*[:,]\s+\w+\(.*\))*\s*\{.*\}\s*(?:SIP_[\w_]+)?;?|(?!;))(\s*\/\/.*)?$/ || $LINE =~ m/SIP_SKIP\s*(?!;)\s*(\/\/.*)?$/ || $LINE =~ m/^\s*class.*SIP_SKIP/ ){ dbg_info("remove constructor definition, function bodies, member initializing list"); my $newline = "$1$2$3;"; $python_signature = remove_following_body_or_initializerlist() unless $LINE =~ m/{.*}(\s*SIP_\w+)*\s*(\/\/.*)?$/; $LINE = $newline; } }; return $python_signature; } sub remove_following_body_or_initializerlist { my $python_signature = ''; do {no warnings 'uninitialized'; dbg_info("remove constructor definition, function bodies, member initializing list"); my $line = read_line(); # python signature if ($line =~ m/^\s*\[\s*(\w+\s*)?\(/){ dbg_info("python signature detected"); my $nesting_index = 0; while ($LINE_IDX < $LINE_COUNT){ $nesting_index += $line =~ tr/\[//; $nesting_index -= $line =~ tr/\]//; if ($nesting_index == 0){ if ($line =~ m/^(.*);\s*(\/\/.*)?$/){ $line = $1; # remove semicolon (added later) $python_signature .= "\n$line"; return $python_signature; } last; } $python_signature .= "\n$line"; $line = read_line(); } } # member initializing list while ( $line =~ m/^\s*[:,]\s+([\w<>]|::)+\(.*?\)/){ dbg_info("member initializing list"); $line = read_line(); } # body if ( $line =~ m/^\s*\{/ ){ my $nesting_index = 0; while ($LINE_IDX < $LINE_COUNT){ dbg_info(" remove body"); $nesting_index += $line =~ tr/\{//; $nesting_index -= $line =~ tr/\}//; if ($nesting_index == 0){ last; } $line = read_line(); } } }; return $python_signature; } sub fix_annotations { my $line = $_[0]; # get removed params to be able to drop them out of the API doc if ( $line =~ m/(\w+)\s+SIP_PYARGREMOVE/){ my @removed_params = $line =~ m/(\w+)\s+SIP_PYARGREMOVE/g; if ( $is_qt6 ){ my @removed_params = $line =~ m/(\w+)\s+SIP_PYARGREMOVE6{0,1}/g; } foreach ( @removed_params ) { push @SKIPPED_PARAMS_REMOVE, $_; dbg_info("caught removed param: $SKIPPED_PARAMS_REMOVE[$#SKIPPED_PARAMS_REMOVE]"); } } if ( $line =~ m/(\w+)\s+SIP_OUT/ ){ my @out_params = $line =~ m/(\w+)\s+SIP_OUT/g; foreach ( @out_params ) { push @SKIPPED_PARAMS_OUT, $_; dbg_info("caught removed param: $SKIPPED_PARAMS_OUT[$#SKIPPED_PARAMS_OUT]"); } } # printed annotations $line =~ s/\/\/\s*SIP_ABSTRACT\b/\/Abstract\//; $line =~ s/\bSIP_ABSTRACT\b/\/Abstract\//; $line =~ s/\bSIP_ALLOWNONE\b/\/AllowNone\//; $line =~ s/\bSIP_ARRAY\b/\/Array\//g; $line =~ s/\bSIP_ARRAYSIZE\b/\/ArraySize\//g; $line =~ s/\bSIP_DEPRECATED\b/\/Deprecated\//g; $line =~ s/\bSIP_CONSTRAINED\b/\/Constrained\//g; $line =~ s/\bSIP_EXTERNAL\b/\/External\//g; $line =~ s/\bSIP_FACTORY\b/\/Factory\//; $line =~ s/\bSIP_IN\b/\/In\//g; $line =~ s/\bSIP_INOUT\b/\/In,Out\//g; $line =~ s/\bSIP_KEEPREFERENCE\b/\/KeepReference\//; $line =~ s/\bSIP_NODEFAULTCTORS\b/\/NoDefaultCtors\//; $line =~ s/\bSIP_OUT\b/\/Out\//g; $line =~ s/\bSIP_RELEASEGIL\b/\/ReleaseGIL\//; $line =~ s/\bSIP_HOLDGIL\b/\/HoldGIL\//; $line =~ s/\bSIP_TRANSFER\b/\/Transfer\//g; $line =~ s/\bSIP_TRANSFERBACK\b/\/TransferBack\//; $line =~ s/\bSIP_TRANSFERTHIS\b/\/TransferThis\//; $line =~ s/\bSIP_GETWRAPPER\b/\/GetWrapper\//; $line =~ s/SIP_PYNAME\(\s*(\w+)\s*\)/\/PyName=$1\//; $line =~ s/SIP_TYPEHINT\(\s*([\w\.\s,\[\]]+?)\s*\)/\/TypeHint="$1"\//g; $line =~ s/SIP_VIRTUALERRORHANDLER\(\s*(\w+)\s*\)/\/VirtualErrorHandler=$1\//; $line =~ s/SIP_THROW\(\s*([\w\s,]+?)\s*\)/throw\( $1 \)/; # combine multiple annotations # https://regex101.com/r/uvCt4M/5 do {no warnings 'uninitialized'; $line =~ s/\/([\w,]+(=\"?[\w, [\]]+\"?)?)\/\s*\/([\w,]+(=\"?[\w, [\]]+\"?)?)\//\/$1,$3\//; (! $3) or dbg_info("combine multiple annotations -- works only for 2"); }; # unprinted annotations $line =~ s/(\w+)(\<(?>[^<>]|(?2))*\>)?\s+SIP_PYALTERNATIVETYPE\(\s*\'?([^()']+)(\(\s*(?:[^()]++|(?2))*\s*\))?\'?\s*\)/$3/g; $line =~ s/(\w+)\s+SIP_PYARGRENAME\(\s*(\w+)\s*\)/$2/g; $line =~ s/=\s+[^=]*?\s+SIP_PYARGDEFAULT\(\s*\'?([^()']+)(\(\s*(?:[^()]++|(?2))*\s*\))?\'?\s*\)/= $1/g; # remove argument if ($line =~ m/SIP_PYARGREMOVE/){ dbg_info("remove arg"); if ( $MULTILINE_DEFINITION != MULTILINE_NO ){ my $prev_line = pop(@OUTPUT) =~ s/\n$//r; # update multi line status my $parenthesis_balance = 0; $parenthesis_balance += $prev_line =~ tr/\(//; $parenthesis_balance -= $prev_line =~ tr/\)//; if ($parenthesis_balance == 1){ $MULTILINE_DEFINITION = MULTILINE_NO; } # concat with above line to bring previous commas $line =~ s/^\s+//; $line = "$prev_line $line\n"; } # see https://regex101.com/r/5iNptO/4 if ( $is_qt6 ){ $line =~ s/(?, +)?(const )?(\w+)(\<(?>[^<>]|(?4))*\>)?\s+[\w&*]+\s+SIP_PYARGREMOVE6{0,1}( = [^()]*(\(\s*(?:[^()]++|(?6))*\s*\))?)?(?()|,?)//g; } else { $line =~ s/SIP_PYARGREMOVE6\s*//g; $line =~ s/(?, +)?(const )?(\w+)(\<(?>[^<>]|(?4))*\>)?\s+[\w&*]+\s+SIP_PYARGREMOVE( = [^()]*(\(\s*(?:[^()]++|(?6))*\s*\))?)?(?()|,?)//g; } $line =~ s/\(\s+\)/()/; } $line =~ s/SIP_FORCE//; $line =~ s/SIP_DOC_TEMPLATE//; $line =~ s/\s+;$/;/; return $line; } sub fix_constants { my $line = $_[0]; $line =~ s/\bstd::numeric_limits::max\(\)/DBL_MAX/g; $line =~ s/\bstd::numeric_limits::lowest\(\)/-DBL_MAX/g; $line =~ s/\bstd::numeric_limits::epsilon\(\)/DBL_EPSILON/g; $line =~ s/\bstd::numeric_limits::min\(\)/LLONG_MIN/g; $line =~ s/\bstd::numeric_limits::max\(\)/LLONG_MAX/g; $line =~ s/\bstd::numeric_limits::max\(\)/INT_MAX/g; $line =~ s/\bstd::numeric_limits::min\(\)/INT_MIN/g; return $line; } sub replace_macros { my $line = $_[0]; $line =~ s/\bTRUE\b/``True``/g; $line =~ s/\bFALSE\b/``False``/g; $line =~ s/\bNULLPTR\b/``None``/g; if ( $is_qt6 ) { # sip for Qt6 chokes on QList/QVector, but is happy if you expand out the map explicitly $line =~ s/(QList<\s*|QVector<\s*)QVariantMap/$1QMap/g; } return $line; } # detect a comment block, return 1 if found # if STRICT comment block shall begin at beginning of line (no code in front) sub detect_comment_block{ my %args = ( strict_mode => STRICT, @_ ); # dbg_info("detect comment strict:" . $args{strict_mode} ); $COMMENT_PARAM_LIST = 0; $INDENT = ''; $PREV_INDENT = ''; $COMMENT_CODE_SNIPPET = 0; $COMMENT_LAST_LINE_NOTE_WARNING = 0; $FOUND_SINCE = 0; @SKIPPED_PARAMS_OUT = (); @SKIPPED_PARAMS_REMOVE = (); if ( $LINE =~ m/^\s*\/\*/ || $args{strict_mode} == UNSTRICT && $LINE =~ m/\/\*/ ){ dbg_info("found comment block"); do {no warnings 'uninitialized'; $COMMENT = processDoxygenLine( $LINE =~ s/^\s*\/\*(\*)?(.*?)\n?$/$2/r ); }; $COMMENT =~ s/^\s*$//; while ($LINE !~ m/\*\/\s*(\/\/.*?)?$/){ $LINE = read_line(); $COMMENT .= processDoxygenLine( $LINE =~ s/\s*\*?(.*?)(\/)?\n?$/$1/r ); } $COMMENT =~ s/\n\s+\n/\n\n/; $COMMENT =~ s/\n{3,}/\n\n/; $COMMENT =~ s/\n+$//; return 1; } return 0; } # Detect if line is a non method member declaration # https://regex101.com/r/gUBZUk/14 sub detect_non_method_member{ return 1 if $LINE =~ m/^\s*(?:template\s*<\w+>\s+)?(?:(const|mutable|static|friend|unsigned)\s+)*\w+(::\w+)?(<([\w<> *&,()]|::)+>)?(,?\s+\*?\w+( = (-?\d+(\.\d+)?|((QMap|QList)<[^()]+>\(\))|(\w+::)*\w+(\([^()]?\))?)|\[\d+\])?)+;/; return 0; } # write some code in front of line to know where the output comes from $debug == 0 or push @OUTPUT, "CODE SIP_RUN MultiLine\n"; # main loop while ($LINE_IDX < $LINE_COUNT){ $PYTHON_SIGNATURE = ''; $ACTUAL_CLASS = $CLASSNAME[$#CLASSNAME] unless $#CLASSNAME < 0; $LINE = read_line(); if ( $LINE =~ m/^\s*(#define )?+SIP_IF_MODULE\(.*\)$/ ){ dbg_info('skipping SIP include condition macro'); next; } if ( $LINE =~ m/^(.*?)\s*\/\/\s*cppcheck-suppress.*$/ ){ $LINE = "$1"; } if ($LINE =~ m/^\s*SIP_FEATURE\( (\w+) \)(.*)$/){ write_output("SF1", "%Feature $1$2\n"); next; } if ($LINE =~ m/^\s*SIP_PROPERTY\((.*)\)$/){ write_output("SF1", "%Property($1)\n"); next; } if ($LINE =~ m/^\s*SIP_IF_FEATURE\( (\!?\w+) \)(.*)$/){ write_output("SF2", "%If ($1)$2\n"); next; } if ($LINE =~ m/^\s*SIP_CONVERT_TO_SUBCLASS_CODE(.*)$/){ $LINE = "%ConvertToSubClassCode$1"; # do not go next, let run the "do not process SIP code" } if ($LINE =~ m/^\s*SIP_VIRTUAL_CATCHER_CODE(.*)$/){ $LINE = "%VirtualCatcherCode$1"; # do not go next, let run the "do not process SIP code" } if ($LINE =~ m/^\s*SIP_END(.*)$/){ write_output("SEN", "%End$1\n"); next; } if ( $LINE =~ s/SIP_WHEN_FEATURE\(\s*(.*?)\s*\)// ){ dbg_info('found SIP_WHEN_FEATURE'); $IF_FEATURE_CONDITION = $1; } if ( $is_qt6 ){ $LINE =~ s/int\s*__len__\s*\(\s*\)/Py_ssize_t __len__\(\)/; $LINE =~ s/long\s*__hash__\s*\(\s*\)/Py_hash_t __hash__\(\)/; } # do not PYQT5 code if we are in qt6 if ( $is_qt6 && $LINE =~ m/^\s*#ifdef SIP_PYQT5_RUN/){ dbg_info("do not process PYQT5 code"); while ( $LINE !~ m/^#endif/ ){ $LINE = read_line(); } } # skip PYQT6 code if we are in qt5 if ( !$is_qt6 && $LINE =~ m/^\s*#ifdef SIP_PYQT6_RUN/){ dbg_info("do not process PYQT6 code"); while ( $LINE !~ m/^#endif/ ){ $LINE = read_line(); } } # do not process SIP code %XXXCode if ( $SIP_RUN == 1 && $LINE =~ m/^ *% *(VirtualErrorHandler|MappedType|Type(?:Header)?Code|Module(?:Header)?Code|Convert(?:From|To)(?:Type|SubClass)Code|MethodCode|Docstring)(.*)?$/ ){ $LINE = "%$1$2"; $COMMENT = ''; dbg_info("do not process SIP code"); while ( $LINE !~ m/^ *% *End/ ){ write_output("COD", $LINE."\n"); $LINE = read_line(); if ( $is_qt6 ){ $LINE =~ s/SIP_SSIZE_T/Py_ssize_t/g; $LINE =~ s/SIPLong_AsLong/PyLong_AsLong/g; } $LINE =~ s/^ *% *(VirtualErrorHandler|MappedType|Type(?:Header)?Code|Module(?:Header)?Code|Convert(?:From|To)(?:Type|SubClass)Code|MethodCode|Docstring)(.*)?$/%$1$2/; $LINE =~ s/^\s*SIP_END(.*)$/%End$1/; } $LINE =~ s/^\s*% End/%End/; write_output("COD", $LINE."\n"); next; } # do not process SIP code %Property if ( $SIP_RUN == 1 && $LINE =~ m/^ *% *(Property)(.*)?$/ ){ $LINE = "%$1$2"; $COMMENT = ''; write_output("COD", $LINE."\n"); next; } # do not process SIP code %If %End if ( $SIP_RUN == 1 && $LINE =~ m/^ *% (If|End)(.*)?$/ ){ $LINE = "%$1$2"; $COMMENT = ''; write_output("COD", $LINE); next; } # Skip preprocessor stuff if ($LINE =~ m/^\s*#/){ # skip #if 0 blocks if ( $LINE =~ m/^\s*#if (0|defined\(Q_OS_WIN\))/){ dbg_info("skipping #if $1 block"); my $nesting_index = 0; while ($LINE_IDX < $LINE_COUNT){ $LINE = read_line(); if ( $LINE =~ m/^\s*#if(def)?\s+/ ){ $nesting_index++; } elsif ( $nesting_index == 0 && $LINE =~ m/^\s*#(endif|else)/ ){ $COMMENT = ''; last; } elsif ( $nesting_index != 0 && $LINE =~ m/^\s*#(endif)/ ){ $nesting_index--; } } next; } if ( $LINE =~ m/^\s*#ifdef SIP_RUN/){ $SIP_RUN = 1; if ($ACCESS[$#ACCESS] == PRIVATE){ dbg_info("writing private content"); write_output("PRV1", $PRIVATE_SECTION_LINE."\n") if $PRIVATE_SECTION_LINE ne ''; $PRIVATE_SECTION_LINE = ''; } next; } if ( $SIP_RUN == 1 ){ if ( $LINE =~ m/^\s*#endif/ ){ if ( $GLOB_IFDEF_NESTING_IDX == 0 ){ $SIP_RUN = 0; next; } else { $GLOB_IFDEF_NESTING_IDX--; } } if ( $LINE =~ m/^\s*#if(def)?\s+/ ){ $GLOB_IFDEF_NESTING_IDX++; } # if there is an else at this level, code will be ignored i.e. not SIP_RUN if ( $LINE =~ m/^\s*#else/ && $GLOB_IFDEF_NESTING_IDX == 0){ while ($LINE_IDX < $LINE_COUNT){ $LINE = read_line(); if ( $LINE =~ m/^\s*#if(def)?\s+/ ){ $GLOB_IFDEF_NESTING_IDX++; } elsif ( $LINE =~ m/^\s*#endif/ ){ if ( $GLOB_IFDEF_NESTING_IDX == 0 ){ $COMMENT = ''; $SIP_RUN = 0; last; } else { $GLOB_IFDEF_NESTING_IDX--; } } } next; } } elsif ( $LINE =~ m/^\s*#ifndef SIP_RUN/){ # code is ignored here while ($LINE_IDX < $LINE_COUNT){ $LINE = read_line(); if ( $LINE =~ m/^\s*#if(def)?\s+/ ){ $GLOB_IFDEF_NESTING_IDX++; } elsif ( $LINE =~ m/^\s*#else/ && $GLOB_IFDEF_NESTING_IDX == 0 ){ # code here will be printed out if ($ACCESS[$#ACCESS] == PRIVATE){ dbg_info("writing private content"); write_output("PRV2", $PRIVATE_SECTION_LINE."\n") if $PRIVATE_SECTION_LINE ne ''; $PRIVATE_SECTION_LINE = ''; } $SIP_RUN = 1; last; } elsif ( $LINE =~ m/^\s*#endif/ ){ if ( $GLOB_IFDEF_NESTING_IDX == 0 ){ $COMMENT = ''; $SIP_RUN = 0; last; } else { $GLOB_IFDEF_NESTING_IDX--; } } } next; } else { next; } } # TYPE HEADER CODE if ( $HEADER_CODE && $SIP_RUN == 0 ){ $HEADER_CODE = 0; write_output("HCE", "%End\n"); } # Skip forward declarations if ($LINE =~ m/^\s*(template ?\ |enum\s+)?(class|struct) \w+(? *SIP_EXTERNAL)?;\s*(\/\/.*)?$/){ if ($+{external}){ dbg_info('do not skip external forward declaration'); $COMMENT = ''; } else { dbg_info('skipping forward declaration'); next; } } # skip friends if ( $LINE =~ m/^\s*friend class \w+/ ){ next; } # insert metaobject for Q_GADGET if ($LINE =~ m/^\s*Q_GADGET\b.*?$/){ if ($LINE !~ m/SIP_SKIP/){ dbg_info('Q_GADGET'); write_output("HCE", " public:\n"); write_output("HCE", " static const QMetaObject staticMetaObject;\n\n"); } next; } # insert in python output (python/module/__init__.py) if ($LINE =~ m/Q_(ENUM|FLAG)\(\s*(\w+)\s*\)/ ){ if ($LINE !~ m/SIP_SKIP/){ my $is_flag = $1 eq 'FLAG' ? 1 : 0; my $enum_helper = "$ACTUAL_CLASS.$2.baseClass = $ACTUAL_CLASS"; dbg_info("Q_ENUM/Q_FLAG $enum_helper"); if ($python_output ne ''){ if ($enum_helper ne ''){ push @OUTPUT_PYTHON, "$enum_helper\n"; if ($is_flag == 1){ # SIP seems to introduce the flags in the module rather than in the class itself # as a dirty hack, inject directly in module, hopefully we don't have flags with the same name.... push @OUTPUT_PYTHON, "$2 = $ACTUAL_CLASS # dirty hack since SIP seems to introduce the flags in module\n"; } } } } next; } # Skip Q_OBJECT, Q_PROPERTY, Q_ENUM etc. if ($LINE =~ m/^\s*Q_(OBJECT|ENUMS|ENUM|FLAG|PROPERTY|DECLARE_METATYPE|DECLARE_TYPEINFO|NOWARN_DEPRECATED_(PUSH|POP))\b.*?$/){ next; } if ($LINE =~ m/^\s*QHASH_FOR_CLASS_ENUM/){ next; } # SIP_SKIP if ( $LINE =~ m/SIP_SKIP|SIP_PYTHON_SPECIAL_/ ){ dbg_info('SIP SKIP!'); # if multiline definition, remove previous lines if ( $MULTILINE_DEFINITION != MULTILINE_NO){ dbg_info('SIP_SKIP with MultiLine'); my $opening_line = ''; while ( $opening_line !~ m/^[^()]*\(([^()]*\([^()]*\)[^()]*)*[^()]*$/){ $opening_line = pop(@OUTPUT); $#OUTPUT >= 0 or exit_with_error('could not reach opening definition'); } dbg_info("removed multiline definition of SIP_SKIP method"); $MULTILINE_DEFINITION = MULTILINE_NO; } # also skip method body if there is one detect_and_remove_following_body_or_initializerlist(); # line skipped, go to next iteration if ($LINE =~ m/SIP_PYTHON_SPECIAL_(\w+)\(\s*(".*"|\w+)\s*\)/ ){ my $method_or_code = $2; dbg_info("PYTHON SPECIAL method or code: $method_or_code"); my $pyop = "${ACTUAL_CLASS}.__" . lc($1) . "__ = lambda self: "; if ( $method_or_code =~ m/^"(.*)"$/ ){ $pyop .= $1; } else { $pyop .= "self.${method_or_code}()"; } dbg_info("PYTHON SPECIAL $pyop"); if ($python_output ne ''){ push @OUTPUT_PYTHON, "$pyop\n"; } } $COMMENT = ''; next; } # Detect comment block if (detect_comment_block()){ next; } if ( $LINE =~ m/^\s*struct(\s+\w+_EXPORT)?\s+(?\w+)$/ ) { dbg_info(" going to struct => public"); push @CLASS_AND_STRUCT, $+{structname}; push @CLASSNAME, $CLASSNAME[$#CLASSNAME]; # fake new class since struct has considered similarly push @ACCESS, PUBLIC; push @EXPORTED, $EXPORTED[-1]; push @GLOB_BRACKET_NESTING_IDX, 0; } # class declaration started # https://regex101.com/r/KMQdF5/1 (older versions: https://regex101.com/r/6FWntP/16) if ( $LINE =~ m/^(\s*(class))\s+([A-Z0-9_]+_EXPORT\s+)?(Q_DECL_DEPRECATED\s+)?(?\w+)(?\s*\:\s*(public|protected|private)\s+\w+(< *(\w|::)+ *(, *(\w|::)+ *)*>)?(::\w+(<(\w|::)+(, *(\w|::)+)*>)?)*(,\s*(public|protected|private)\s+\w+(< *(\w|::)+ *(, *(\w|::)+)*>)?(::\w+(<\w+(, *(\w|::)+)?>)?)*)*)?(?\s*\/?\/?\s*SIP_\w+)?\s*?(\/\/.*|(?!;))$/ ){ dbg_info("class definition started"); push @ACCESS, PUBLIC; push @EXPORTED, 0; push @GLOB_BRACKET_NESTING_IDX, 0; my @template_inheritance_template = (); my @template_inheritance_class1 = (); my @template_inheritance_class2 = (); my @template_inheritance_class3 = (); do {no warnings 'uninitialized'; push @CLASSNAME, $+{classname}; push @CLASS_AND_STRUCT, $+{classname}; if ($#CLASSNAME == 0){ # might be worth to add in-class classes later on # in case of a tamplate based class declaration # based on an in-class and in the same file push @DECLARED_CLASSES, $CLASSNAME[$#CLASSNAME]; } dbg_info("class: ".$CLASSNAME[$#CLASSNAME]); if ($LINE =~ m/\b[A-Z0-9_]+_EXPORT\b/ || $#CLASSNAME != 0 || $INPUT_LINES[$LINE_IDX-2] =~ m/^\s*template\s*>', $class_map_file) or die $!; print FH3 join(".", @CLASSNAME) . ": $headerfile#L".$LINE_IDX."\n"; close(FH3); } # Inheritance if (defined $+{domain}){ my $m = $+{domain}; $m =~ s/public +(\w+, *)*(Ui::\w+,? *)+//g; # remove Ui::xxx public inheritance as the namespace is causing troubles $m =~ s/public +//g; $m =~ s/[,:]?\s*private +\w+(::\w+)?//g; # detect template based inheritance # https://regex101.com/r/9LGhyy/1 while ($m =~ /[,:]\s+(?(?!QList)\w+)< *(?(\w|::)+) *(, *(?(\w|::)+)? *(, *(?(\w|::)+)? *)?)? *>/g){ dbg_info("template class"); push @template_inheritance_template, $+{tpl}; push @template_inheritance_class1, $+{cls1}; push @template_inheritance_class2, $+{cls2} // ""; push @template_inheritance_class3, $+{cls3} // ""; # dbg_info("template classes (max 3): $+{cls1} $+{cls2} $+{cls3}"); } dbg_info("domain: $m"); do {no warnings 'uninitialized'; # https://regex101.com/r/nOLg2r/1 $m =~ s/\b(?(?!QList)\w+)< *(?(\w|::)+) *(, *(?(\w|::)+)? *(, *(?(\w|::)+)? *)?)? *>/$+{tpl}$+{cls1}$+{cls2}$+{cls3}Base/g; # use the typeded as template inheritance }; $m =~ s/(\w+)< *(?:\w|::)+ *>//g; # remove remaining templates $m =~ s/([:,])\s*,/$1/g; $m =~ s/(\s*[:,])?\s*$//; $LINE .= $m; } if (defined $+{annot}) { $LINE .= "$+{annot}"; $LINE = fix_annotations($LINE); } $LINE .= "\n{\n"; if ( $COMMENT !~ m/^\s*$/ ){ $LINE .= "%Docstring(signature=\"appended\")\n$COMMENT\n%End\n"; } $LINE .= "\n%TypeHeaderCode\n#include \"" . basename($headerfile) . "\""; # for template based inheritance, add a typedef to define the base type # add it to the class and to the TypeHeaderCode # also include the template header # see https://www.riverbankcomputing.com/pipermail/pyqt/2015-May/035893.html while ( @template_inheritance_template ) { my $tpl = pop @template_inheritance_template; my $cls1 = pop @template_inheritance_class1; my $cls2 = pop @template_inheritance_class2; my $cls3 = pop @template_inheritance_class3; if ( $cls2 eq ""){ $LINE = "\ntypedef $tpl<$cls1> ${tpl}${cls1}Base;\n\n$LINE"; } elsif ( $cls3 eq ""){ $LINE = "\ntypedef $tpl<$cls1,$cls2> ${tpl}${cls1}${cls2}Base;\n\n$LINE"; } else { $LINE = "\ntypedef $tpl<$cls1,$cls2,$cls3> ${tpl}${cls1}${cls2}${cls3}Base;\n\n$LINE"; } if ( none { $_ eq $tpl } @DECLARED_CLASSES ){ my $tpl_header = lc $tpl . ".h"; if ( exists $SIP_CONFIG->{class_headerfile}->{$tpl} ){ $tpl_header = $SIP_CONFIG->{class_headerfile}->{$tpl}; } $LINE .= "\n#include \"" . $tpl_header . "\""; } if ( $cls2 eq ""){ $LINE .= "\ntypedef $tpl<$cls1> ${tpl}${cls1}Base;"; } elsif ( $cls3 eq ""){ $LINE .= "\ntypedef $tpl<$cls1,$cls2> ${tpl}${cls1}${cls2}Base;"; } else { $LINE .= "\ntypedef $tpl<$cls1,$cls2,$cls3> ${tpl}${cls1}${cls2}${cls3}Base;"; } } if ( ( any{ $_ == PRIVATE } @ACCESS ) && $#ACCESS != 0){ # do not write anything in PRIVATE context and not top level dbg_info("skipping class in private context"); next; } $ACCESS[$#ACCESS] = PRIVATE; # private by default write_output("CLS", "$LINE\n"); # Skip opening curly bracket, incrementing hereunder my $skip = read_line(); $skip =~ m/^\s*{\s*$/ or exit_with_error("expecting { after class definition"); $GLOB_BRACKET_NESTING_IDX[$#GLOB_BRACKET_NESTING_IDX]++; $COMMENT = ''; $HEADER_CODE = 1; $ACCESS[$#ACCESS] = PRIVATE; next; } # bracket balance in class/struct tree if ($SIP_RUN == 0){ my $bracket_balance = 0; $bracket_balance += $LINE =~ tr/\{//; $bracket_balance -= $LINE =~ tr/\}//; if ($bracket_balance != 0){ $GLOB_BRACKET_NESTING_IDX[$#GLOB_BRACKET_NESTING_IDX] += $bracket_balance; if ($GLOB_BRACKET_NESTING_IDX[$#GLOB_BRACKET_NESTING_IDX] == 0){ dbg_info(" going up in class/struct tree"); if ($#ACCESS > 0){ pop(@GLOB_BRACKET_NESTING_IDX); pop(@ACCESS); exit_with_error("Class $CLASSNAME[$#CLASSNAME] should be exported with appropriate [LIB]_EXPORT macro. If this should not be available in python, wrap it in a `#ifndef SIP_RUN` block.") if $EXPORTED[-1] == 0 and ($CLASSNAME[$#CLASSNAME] ne $SIP_CONFIG->{no_export_macro}); pop @EXPORTED; } pop(@CLASSNAME); pop(@CLASS_AND_STRUCT); if ($#ACCESS == 0){ dbg_info("reached top level"); # top level should stay public $ACCESS[$#ACCESS] = PUBLIC; } $COMMENT = ''; $RETURN_TYPE = ''; $PRIVATE_SECTION_LINE = ''; } dbg_info("new bracket balance: @GLOB_BRACKET_NESTING_IDX"); } } # Private members (exclude SIP_RUN) if ( $LINE =~ m/^\s*private( slots)?:/ ){ $ACCESS[$#ACCESS] = PRIVATE; $LAST_ACCESS_SECTION_LINE = $LINE; $PRIVATE_SECTION_LINE = $LINE; $COMMENT = ''; dbg_info("going private"); next; } elsif ( $LINE =~ m/^\s*(public( slots)?|signals):.*$/ ){ dbg_info("going public"); $LAST_ACCESS_SECTION_LINE = $LINE; $ACCESS[$#ACCESS] = PUBLIC; $COMMENT = ''; } elsif ( $LINE =~ m/^\s*(protected)( slots)?:.*$/ ){ dbg_info("going protected"); $LAST_ACCESS_SECTION_LINE = $LINE; $ACCESS[$#ACCESS] = PROTECTED; $COMMENT = ''; } elsif ( $ACCESS[$#ACCESS] == PRIVATE && $LINE =~ m/SIP_FORCE/){ dbg_info("private with SIP_FORCE"); write_output("PRV3", $PRIVATE_SECTION_LINE."\n") if $PRIVATE_SECTION_LINE ne ''; $PRIVATE_SECTION_LINE = ''; } elsif ( ( any{ $_ == PRIVATE } @ACCESS ) && $SIP_RUN == 0 ) { $COMMENT = ''; next; } # Skip operators if ( $ACCESS[$#ACCESS] != PRIVATE && $LINE =~ m/operator(=|<<|>>|->)\s*\(/ ){ dbg_info("skip operator"); detect_and_remove_following_body_or_initializerlist(); next; } # save comments and do not print them, except in SIP_RUN if ( $SIP_RUN == 0 ){ if ( $LINE =~ m/^\s*\/\// ){ if ($LINE =~ m/^\s*\/\/\!\s*(.*?)\n?$/){ $COMMENT_PARAM_LIST = 0; $PREV_INDENT = $INDENT; $INDENT = ''; $COMMENT_LAST_LINE_NOTE_WARNING = 0; $COMMENT = processDoxygenLine( $1 ); $COMMENT =~ s/\n+$//; } elsif ($INPUT_LINES[$LINE_IDX-1] !~ m/\*\/.*/) { $COMMENT = ''; } next; } } if ( $is_qt6 eq 1 and $LINE =~ m/^\s*Q_DECLARE_FLAGS\s*\(\s*(?\w+)\s*,\s*(?\w+)\s*\)/ ){ # In PyQt6 Flags are mapped to Python enum flag and the plural doesn't exist anymore # https://www.riverbankcomputing.com/static/Docs/PyQt6/pyqt5_differences.html # So we mock it to avoid API break push @OUTPUT_PYTHON, "$ACTUAL_CLASS.$+{flags_name} = lambda flags=0: $ACTUAL_CLASS.$+{flag_name}(flags)\n"; } # Enum declaration # For scoped and type based enum, the type has to be removed if ( $LINE =~ m/^\s*Q_DECLARE_FLAGS\s*\(\s*(?\w+)\s*,\s*(?\w+)\s*\)\s*SIP_MONKEYPATCH_FLAGS_UNNEST\s*\(\s*(?\w+)\s*,\s*(?\w+)\s*\)\s*$/ ){ if ("$+{emkb}.$+{emkf}" ne "$ACTUAL_CLASS.$+{flags_name}" ) { push @OUTPUT_PYTHON, "$+{emkb}.$+{emkf} = $ACTUAL_CLASS.$+{flags_name}\n"; } push @ENUM_MONKEY_PATCHED_TYPES, [$ACTUAL_CLASS, $+{flags_name}, $+{emkb}, $+{emkf}]; $LINE =~ s/\s*SIP_MONKEYPATCH_FLAGS_UNNEST\(.*?\)//; } if ( $LINE =~ m/^(\s*enum(\s+Q_DECL_DEPRECATED)?\s+(?class\s+)?(?\w+))(:?\s+SIP_[^:]*)?(\s*:\s*(?\w+))?(?:\s*SIP_ENUM_BASETYPE\s*\(\s*(?\w+)\s*\))?(?.*)$/ ){ my $enum_decl = $1; my $enum_qualname = $+{enum_qualname}; my $enum_type = $+{enum_type}; my $isclass = $+{isclass}; my $enum_cpp_name = (defined $ACTUAL_CLASS and $ACTUAL_CLASS) ? ($ACTUAL_CLASS."::$enum_qualname") : "$enum_qualname"; if (not defined $isclass and none { $_ eq $enum_cpp_name } @ALLOWED_NON_CLASS_ENUMS ) { exit_with_error("Non class enum exposed to Python -- must be a enum class: $enum_cpp_name"); } my $oneliner = $+{oneliner}; my $is_scope_based = "0"; $is_scope_based = "1" if defined $isclass; $enum_decl =~ s/\s*\bQ_DECL_DEPRECATED\b//; my $py_enum_type; if ( $LINE =~ m/SIP_ENUM_BASETYPE\(\s*(.*?)\s*\)/ ) { $py_enum_type = $1; } if (defined $py_enum_type and $py_enum_type eq "IntFlag") { push @ENUM_INTFLAG_TYPES, $enum_cpp_name; } if (defined $enum_type and ($enum_type eq "int" or $enum_type eq "quint32")) { push @ENUM_INT_TYPES, "$ACTUAL_CLASS.$enum_qualname"; if ( $is_qt6 eq 1 ) { if (defined $py_enum_type) { $enum_decl .= " /BaseType=$py_enum_type/" } else { $enum_decl .= " /BaseType=IntEnum/" } } } elsif (defined $enum_type) { exit_with_error("Unhandled enum type $enum_type for $enum_cpp_name"); } elsif (defined $isclass) { push @ENUM_CLASS_NON_INT_TYPES, "$ACTUAL_CLASS.$enum_qualname"; } elsif ($is_qt6 eq 1) { # non class enum in qt6 -- we always want these to be IntEnums # for compatibility with qt5 code $enum_decl .= " /BaseType=IntEnum/" } write_output("ENU1", "$enum_decl"); write_output("ENU1", $oneliner) if defined $oneliner; write_output("ENU1", "\n"); my $monkeypatch = "0"; $monkeypatch = "1" if defined $is_scope_based eq "1" and $LINE =~ m/SIP_MONKEYPATCH_SCOPEENUM(_UNNEST)?(:?\(\s*(?\w+)\s*,\s*(?\w+)\s*\))?/; my $enum_mk_base = ""; $enum_mk_base = $+{emkb} if defined $+{emkb}; my $enum_old_name = ""; if (defined $+{emkf} and $monkeypatch eq "1"){ $enum_old_name = $+{emkf}; if ( $ACTUAL_CLASS ne "" ) { if ($enum_mk_base.$+{emkf} ne $ACTUAL_CLASS.$enum_qualname) { push @OUTPUT_PYTHON, "$enum_mk_base.$+{emkf} = $ACTUAL_CLASS.$enum_qualname\n"; } } else { push @OUTPUT_PYTHON, "$enum_mk_base.$+{emkf} = $enum_qualname\n"; } } if ($LINE =~ m/\{((\s*\w+)(\s*=\s*[\w\s\d<|]+.*?)?(,?))+\s*\}/){ # one line declaration $LINE !~ m/=/ or exit_with_error("Sipify does not handle enum one liners with value assignment. Use multiple lines instead. Or jusr write a new parser."); next; } else { $LINE = read_line(); $LINE =~ m/^\s*\{\s*$/ or exit_with_error('Unexpected content: enum should be followed by {'); write_output("ENU2", "$LINE\n"); push @OUTPUT_PYTHON, "# monkey patching scoped based enum\n" if $is_scope_based eq "1"; my @enum_members_doc = (); while ($LINE_IDX < $LINE_COUNT){ $LINE = read_line(); if (detect_comment_block()){ next; } last if ($LINE =~ m/\};/); next if ($LINE =~ m/^\s*\w+\s*\|/); # multi line declaration as sum of enums do {no warnings 'uninitialized'; my $enum_decl = $LINE =~ s/^(\s*(?\w+))(\s+SIP_PYNAME(?:\(\s*(?[^() ]+)\s*\)\s*)?)?(\s+SIP_MONKEY\w+(?:\(\s*(?[^() ]+)\s*\)\s*)?)?(?:\s*=\s*(?(:?[\w\s\d|+-]|::|<<)+))?(?,?)(:?\s*\/\/!<\s*(?.*)|.*)$/$1$3$+{optional_comma}/r; my $enum_member = $+{em}; my $comment = $+{co}; my $compat_name = $+{compat} ? $+{compat} : $enum_member; my $enum_value = $+{enum_value}; # replace :: with . (changes c++ style namespace/class directives to Python style) $comment =~ s/::/./g; $comment =~ s/\"/\\"/g; $comment =~ s/\\since .*?([\d\.]+)/\\n.. versionadded:: $1\\n/i; $comment =~ s/\\deprecated (.*)/\\n.. deprecated:: $1\\n/i; $comment =~ s/^\\n+//; $comment =~ s/\\n+$//; dbg_info("is_scope_based:$is_scope_based enum_mk_base:$enum_mk_base monkeypatch:$monkeypatch"); if ( defined $enum_value and ($enum_value =~ m/.*\<\<.*/ or $enum_value =~ m/.*0x0.*/)) { if (none { $_ eq "${ACTUAL_CLASS}::$enum_qualname" } @ENUM_INTFLAG_TYPES) { exit_with_error("${ACTUAL_CLASS}::$enum_qualname is a flags type, but was not declared with IntFlag type. Add 'SIP_ENUM_BASETYPE(IntFlag)' to the enum class declaration line"); } } if ($is_scope_based eq "1" and $enum_member ne "") { if ( $monkeypatch eq 1 and $enum_mk_base ne ""){ if ( $ACTUAL_CLASS ne "" ) { push @OUTPUT_PYTHON, "$enum_mk_base.$compat_name = $ACTUAL_CLASS.$enum_qualname.$enum_member\n"; if ( $enum_old_name && $compat_name ne $enum_member ) { push @OUTPUT_PYTHON, "$enum_mk_base.$enum_old_name.$compat_name = $ACTUAL_CLASS.$enum_qualname.$enum_member\n"; } push @OUTPUT_PYTHON, "$enum_mk_base.$compat_name.is_monkey_patched = True\n"; push @OUTPUT_PYTHON, "$enum_mk_base.$compat_name.__doc__ = \"$comment\"\n"; push @enum_members_doc, "'* ``$compat_name``: ' + $ACTUAL_CLASS.$enum_qualname.$enum_member.__doc__"; } else { push @OUTPUT_PYTHON, "$enum_mk_base.$compat_name = $enum_qualname.$enum_member\n"; push @OUTPUT_PYTHON, "$enum_mk_base.$compat_name.is_monkey_patched = True\n"; push @OUTPUT_PYTHON, "$enum_mk_base.$compat_name.__doc__ = \"$comment\"\n"; push @enum_members_doc, "'* ``$compat_name``: ' + $enum_qualname.$enum_member.__doc__"; } } else { if ( $monkeypatch eq 1 ) { push @OUTPUT_PYTHON, "$ACTUAL_CLASS.$compat_name = $ACTUAL_CLASS.$enum_qualname.$enum_member\n"; push @OUTPUT_PYTHON, "$ACTUAL_CLASS.$compat_name.is_monkey_patched = True\n"; } if ( $ACTUAL_CLASS ne "" ){ my $complete_class_path = join('.', @CLASSNAME); push @OUTPUT_PYTHON, "$complete_class_path.$enum_qualname.$compat_name.__doc__ = \"$comment\"\n"; push @enum_members_doc, "'* ``$compat_name``: ' + $ACTUAL_CLASS.$enum_qualname.$enum_member.__doc__"; } else { push @OUTPUT_PYTHON, "$enum_qualname.$compat_name.__doc__ = \"$comment\"\n"; push @enum_members_doc, "'* ``$compat_name``: ' + $enum_qualname.$enum_member.__doc__"; } } } if ( $is_scope_based eq "0" and $is_qt6 eq 1 and $enum_member ne "" ) { my $basename = join( ".", @CLASS_AND_STRUCT ); if ( $basename ne "" ){ $enum_member =~ s/^None$/None_/; # With PyQt6, you have to specify the scope to access the enum: https://www.riverbankcomputing.com/static/Docs/PyQt5/gotchas.html#enums # so we mock push @OUTPUT_PYTHON, "$basename.$enum_member = $basename.$enum_qualname.$enum_member\n"; } } $enum_decl = fix_annotations($enum_decl); write_output("ENU3", "$enum_decl\n"); }; detect_comment_block(strict_mode => UNSTRICT); } write_output("ENU4", "$LINE\n"); if ($is_scope_based eq "1") { $COMMENT =~ s/\n/\\n/g; $COMMENT =~ s/\"/\\"/g; if ( $ACTUAL_CLASS ne "" ){ push @OUTPUT_PYTHON, "$ACTUAL_CLASS.$enum_qualname.__doc__ = \"$COMMENT\\n\\n\" + " . join(" + '\\n' + ", @enum_members_doc) . "\n# --\n"; } else { push @OUTPUT_PYTHON, "$enum_qualname.__doc__ = '$COMMENT\\n\\n' + " . join(" + '\\n' + ", @enum_members_doc) . "\n# --\n"; } } # enums don't have Docstring apparently $COMMENT = ''; next; } } if( $LINE =~ /.*\/\/\!\)(.*)$/$1$2$3/; $LINE =~ s/^(\s*template\s*<)(?:class|typename) (\w+) *, *(?:class|typename) (\w+>)(.*)$/$1$2,$3$4/; # https://regex101.com/r/EB1mpx/1 $LINE =~ s/^(\s*template\s*<)(?:class|typename) (\w+) *, *(?:class|typename) (\w+) *, *(?:class|typename) (\w+>)(.*)$/$1$2,$3,$4$5/; $LINE =~ s/\s*\boverride\b//; $LINE =~ s/\s*\bSIP_MAKE_PRIVATE\b//; $LINE =~ s/\s*\bFINAL\b/ \${SIP_FINAL}/; $LINE =~ s/\s*\bextern \b//; $LINE =~ s/\s*\bMAYBE_UNUSED \b//; $LINE =~ s/\s*\bNODISCARD \b//; $LINE =~ s/\s*\bQ_DECL_DEPRECATED\b//; $LINE =~ s/^(\s*)?(const |virtual |static )*inline /$1$2/; $LINE =~ s/\bconstexpr\b/const/; $LINE =~ s/\bnullptr\b/0/g; $LINE =~ s/\s*=\s*default\b//g; }; if( $LINE =~ /\b\w+_EXPORT\b/ ) { $EXPORTED[-1]++; $LINE =~ s/\b\w+_EXPORT\s+//g; } # skip non-method member declaration in non-public sections if ( $SIP_RUN != 1 && $ACCESS[$#ACCESS] != PUBLIC && detect_non_method_member() == 1){ dbg_info("skip non-method member declaration in non-public sections"); next; } # remove static const value assignment # https://regex101.com/r/DyWkgn/6 do {no warnings 'uninitialized'; $LINE !~ m/^\s*const static \w+/ or exit_with_error("const static should be written static const in $CLASSNAME[$#CLASSNAME]"); $LINE =~ s/^(? *(?static )?const \w+(?:<(?:[\w<>, ]|::)+>)? \w+)(?: = [^()]+?(\((?:[^()]++|(?3))*\))?[^()]*?)?(?[|;]) *(\/\/.*?)?$/$1;/; $COMMENT = '' if (defined $+{staticconst} && ! defined $+{static}); if ( defined $+{endingchar} && $+{endingchar} =~ m/\|/ ){ dbg_info("multiline const static assignment"); my $skip = ''; while ( $skip !~ m/;\s*(\/\/.*?)?$/ ){ $skip = read_line(); } } }; # remove struct member assignment # https://regex101.com/r/OUwV75/1 if ( $SIP_RUN != 1 && $ACCESS[$#ACCESS] == PUBLIC && $LINE =~ m/^(\s*\w+[\w<> *&:,]* \*?\w+) = ([\-\w\:\.]+(< *\w+( \*)? *>)?)+(\([^()]*\))?\s*;/ ){ dbg_info("remove struct member assignment"); $LINE = "$1;"; } # catch Q_DECLARE_FLAGS if ( $LINE =~ m/^(\s*)Q_DECLARE_FLAGS\(\s*(.*?)\s*,\s*(.*?)\s*\)\s*$/ ){ my $ACTUAL_CLASS = $#CLASSNAME >= 0 ? $CLASSNAME[$#CLASSNAME].'::' : ''; dbg_info("Declare flags: $ACTUAL_CLASS"); $LINE = "$1typedef QFlags<${ACTUAL_CLASS}$3> $2;\n"; $QFLAG_HASH{"${ACTUAL_CLASS}$2"} = "${ACTUAL_CLASS}$3"; if ( none { $_ eq "${ACTUAL_CLASS}$3" } @ENUM_INTFLAG_TYPES ){ exit_with_error("${ACTUAL_CLASS}$3 is a flags type, but was not declared with IntFlag type. Add 'SIP_ENUM_BASETYPE(IntFlag)' to the enum class declaration line"); } } # catch Q_DECLARE_OPERATORS_FOR_FLAGS if ( $LINE =~ m/^(\s*)Q_DECLARE_OPERATORS_FOR_FLAGS\(\s*(.*?)\s*\)\s*$/ ){ my $flags = $2; my $flag = $QFLAG_HASH{$flags}; $LINE = "$1QFlags<$flag> operator|($flag f1, QFlags<$flag> f2);\n"; my $py_flag = $flag; $py_flag =~ s/::/./; if ( any { $_ eq $py_flag } @ENUM_CLASS_NON_INT_TYPES ){ exit_with_error("$flag is a flags type, but was not declared with int type. Add ': int' to the enum class declaration line"); } elsif ( none { $_ eq $py_flag } @ENUM_INT_TYPES ){ if ( $is_qt6 ) { dbg_info("monkey patching operators for non class enum"); if ($HAS_PUSHED_FORCE_INT eq 0) { push @OUTPUT_PYTHON, "from enum import Enum\n\n\ndef _force_int(v): return int(v.value) if isinstance(v, Enum) else v\n\n\n"; $HAS_PUSHED_FORCE_INT = 1; } push @OUTPUT_PYTHON, "$py_flag.__bool__ = lambda flag: bool(_force_int(flag))\n"; push @OUTPUT_PYTHON, "$py_flag.__eq__ = lambda flag1, flag2: _force_int(flag1) == _force_int(flag2)\n"; push @OUTPUT_PYTHON, "$py_flag.__and__ = lambda flag1, flag2: _force_int(flag1) & _force_int(flag2)\n"; push @OUTPUT_PYTHON, "$py_flag.__or__ = lambda flag1, flag2: $py_flag(_force_int(flag1) | _force_int(flag2))\n"; } } if ( !$is_qt6 ) { foreach ( @ENUM_MONKEY_PATCHED_TYPES ) { if ( $flags eq "$_->[0]::$_->[1]" ) { dbg_info("monkey patching flags"); if ($HAS_PUSHED_FORCE_INT eq 0) { push @OUTPUT_PYTHON, "from enum import Enum\n\n\ndef _force_int(v): return int(v.value) if isinstance(v, Enum) else v\n\n\n"; $HAS_PUSHED_FORCE_INT = 1; } push @OUTPUT_PYTHON, "$py_flag.__or__ = lambda flag1, flag2: $_->[0].$_->[1](_force_int(flag1) | _force_int(flag2))\n"; } } } } do {no warnings 'uninitialized'; # remove keywords if ( $IS_OVERRIDE_OR_MAKE_PRIVATE != PREPEND_CODE_NO ){ # handle multiline definition to add virtual keyword or making private on opening line if ( $MULTILINE_DEFINITION != MULTILINE_NO ){ my $rolling_line = $LINE; my $rolling_line_idx = $LINE_IDX; dbg_info("handle multiline definition to add virtual keyword or making private on opening line"); while ( $rolling_line !~ m/^[^()]*\(([^()]*\([^()]*\)[^()]*)*[^()]*$/){ $rolling_line_idx--; $rolling_line = $INPUT_LINES[$rolling_line_idx]; $rolling_line_idx >= 0 or exit_with_error('could not reach opening definition'); } if ( $IS_OVERRIDE_OR_MAKE_PRIVATE == PREPEND_CODE_VIRTUAL && $rolling_line !~ m/^(\s*)virtual\b(.*)$/ ){ my $idx = $#OUTPUT-$LINE_IDX+$rolling_line_idx+2; #print "len: $#OUTPUT line_idx: $LINE_IDX virt: $rolling_line_idx\n"idx: $idx\n$OUTPUT[$idx]\n"; $OUTPUT[$idx] = fix_annotations($rolling_line =~ s/^(\s*?)\b(.*)$/$1 virtual $2\n/r); } elsif ( $IS_OVERRIDE_OR_MAKE_PRIVATE == PREPEND_CODE_MAKE_PRIVATE ) { dbg_info("prepending private access"); my $idx = $#OUTPUT-$LINE_IDX+$rolling_line_idx+2; my $private_access = $LAST_ACCESS_SECTION_LINE =~ s/(protected|public)/private/r; splice @OUTPUT, $idx, 0, $private_access . "\n"; $OUTPUT[$idx+1] = fix_annotations($rolling_line) . "\n"; } } elsif ( $IS_OVERRIDE_OR_MAKE_PRIVATE == PREPEND_CODE_MAKE_PRIVATE ) { dbg_info("prepending private access"); $LINE = $LAST_ACCESS_SECTION_LINE =~ s/(protected|public)/private/r . "\n" . $LINE . "\n"; } elsif ( $IS_OVERRIDE_OR_MAKE_PRIVATE == PREPEND_CODE_VIRTUAL && $LINE !~ m/^(\s*)virtual\b(.*)$/ ){ #sip often requires the virtual keyword to be present, or it chokes on covariant return types #in overridden methods dbg_info('adding virtual keyword for overridden method'); $LINE =~ s/^(\s*?)\b(.*)$/$1virtual $2\n/; } } # remove constructor definition, function bodies, member initializing list $PYTHON_SIGNATURE = detect_and_remove_following_body_or_initializerlist(); # remove inline declarations if ( $LINE =~ m/^(\s*)?(static |const )*(([(?:long )\w:]+(<.*?>)?\s+(\*|&)?)?(\w+)( (?:const*?))*)\s*(\{.*\});(\s*\/\/.*)?$/ ){ my $newline = "$1$3;"; $LINE = $newline; } if ( $LINE =~ m/^\s*(?:const |virtual |static |inline )*(?!explicit)([(?:long )\w:]+(?:<.*?>)?)\s+(?:\*|&)?(?:\w+|operator.{1,2})\(.*$/ ){ if ($1 !~ m/(void|SIP_PYOBJECT|operator|return|QFlag)/ ){ $RETURN_TYPE = $1; # replace :: with . (changes c++ style namespace/class directives to Python style) $RETURN_TYPE =~ s/::/./g; # replace with builtin Python types $RETURN_TYPE =~ s/\bdouble\b/float/; $RETURN_TYPE =~ s/\bQString\b/str/; $RETURN_TYPE =~ s/\bQStringList\b/list of str/; if ( $RETURN_TYPE =~ m/^(?:QList|QVector)<\s*(.*?)[\s*\*]*>$/ ){ $RETURN_TYPE = "list of $1"; } if ( $RETURN_TYPE =~ m/^QSet<\s*(.*?)[\s*\*]*>$/ ){ $RETURN_TYPE = "set of $1"; } } } }; # deleted functions if ( $LINE =~ m/^(\s*)?(const )?(virtual |static )?((\w+(<.*?>)?\s+(\*|&)?)?(\w+|operator.{1,2})\(.*?(\(.*\))*.*\)( const)?)\s*= delete;(\s*\/\/.*)?$/ ){ $COMMENT = ''; next; } # remove export macro from struct definition $LINE =~ s/^(\s*struct )\w+_EXPORT (.+)$/$1$2/; # Skip comments $COMMENT_TEMPLATE_DOCSTRING = 0; if ( $LINE =~ m/^\s*typedef\s+\w+\s*<\s*\w+\s*>\s+\w+\s+.*SIP_DOC_TEMPLATE/ ) { # support Docstring for template based classes in SIP 4.19.7+ $COMMENT_TEMPLATE_DOCSTRING = 1; } elsif ( $MULTILINE_DEFINITION == MULTILINE_NO && ($LINE =~ m/\/\// || $LINE =~ m/^\s*typedef / || $LINE =~ m/\s*struct / || $LINE =~ m/operator\[\]\(/ || $LINE =~ m/^\s*operator\b/ || $LINE =~ m/operator\s?[!+-=*\/\[\]<>]{1,2}/ || $LINE =~ m/^\s*%\w+(.*)?$/ || $LINE =~ m/^\s*namespace\s+\w+/ || $LINE =~ m/^\s*(virtual\s*)?~/ || detect_non_method_member() == 1 ) ){ dbg_info('skipping comment'); dbg_info('because typedef') if ($LINE =~ m/\s*typedef.*?(?!SIP_DOC_TEMPLATE)/); $COMMENT = ''; $RETURN_TYPE = ''; $IS_OVERRIDE_OR_MAKE_PRIVATE = PREPEND_CODE_NO; } $LINE = fix_constants($LINE); $LINE = fix_annotations($LINE); # fix astyle placing space after % character $LINE =~ s/\/\s+GetWrapper\s+\//\/GetWrapper\//; # handle enum/flags QgsSettingsEntryEnumFlag if ( $LINE =~ m/^(\s*)const QgsSettingsEntryEnumFlag<(.*)> (.+);$/ ) { my $prep_line = "class QgsSettingsEntryEnumFlag_$3 { %TypeHeaderCode #include \"" .basename($headerfile) . "\" #include \"qgssettingsentry.h\" typedef QgsSettingsEntryEnumFlag<$2> QgsSettingsEntryEnumFlag_$3; %End public: QgsSettingsEntryEnumFlag_$3( const QString &key, QgsSettings::Section section, const $2 &defaultValue, const QString &description = QString() ); QString key( const QString &dynamicKeyPart = QString() ) const; $2 value( const QString &dynamicKeyPart = QString(), bool useDefaultValueOverride = false, const $2 &defaultValueOverride = $2() ) const; };"; $LINE = "$1const QgsSettingsEntryEnumFlag_$3 $3;"; $COMMENT = ''; write_output("ENF", "$prep_line\n", "prepend"); } write_output("NOR", "$LINE\n"); # append to class map file if ( $class_map_file ne '' && defined $ACTUAL_CLASS && $ACTUAL_CLASS ne '' ){ if ($LINE =~ m/^ *(const |virtual |static )* *[\w:]+ +\*?(?\w+)\(.*$/){ open(FH3, '>>', $class_map_file) or die $!; print FH3 join(".", @CLASSNAME) . "." . $+{method} .": $headerfile#L".$LINE_IDX."\n"; close(FH3); } } if ($PYTHON_SIGNATURE ne '') { write_output("PSI", "$PYTHON_SIGNATURE\n"); } # multiline definition (parenthesis left open) if ( $MULTILINE_DEFINITION != MULTILINE_NO ){ dbg_info("on multiline"); # https://regex101.com/r/DN01iM/4 if ( $LINE =~ m/^([^()]+(\((?:[^()]++|(?1))*\)))*[^()]*\)([^()](throw\([^()]+\))?)*$/){ dbg_info("ending multiline"); # remove potential following body if ( $MULTILINE_DEFINITION != MULTILINE_CONDITIONAL_STATEMENT && $LINE !~ m/(\{.*\}|;)\s*(\/\/.*)?$/ ){ dbg_info("remove following body of multiline def"); my $last_line = $LINE; $last_line .= remove_following_body_or_initializerlist(); # add missing semi column my $dummy = pop(@OUTPUT); write_output("MLT", "$last_line;\n"); } $MULTILINE_DEFINITION = MULTILINE_NO; } else { next; } } elsif ( $LINE =~ m/^[^()]+\([^()]*([^()]*\([^()]*\)[^()]*)*[^)]*$/ ){ dbg_info("Multiline detected:: $LINE"); if ( $LINE =~ m/^\s*((else )?if|while|for) *\(/ ){ $MULTILINE_DEFINITION = MULTILINE_CONDITIONAL_STATEMENT; } else { $MULTILINE_DEFINITION = MULTILINE_METHOD; } next; } # write comment if ( $LINE =~ m/^\s*$/ ) { dbg_info("no more override / private"); $IS_OVERRIDE_OR_MAKE_PRIVATE = PREPEND_CODE_NO; next; } if ( $LINE =~ m/^\s*template\s*<.*>/ ){ # do not comment now for templates, wait for class definition next; } if ( $COMMENT !~ m/^\s*$/ || $RETURN_TYPE ne ''){ if ( $IS_OVERRIDE_OR_MAKE_PRIVATE != PREPEND_CODE_VIRTUAL && $COMMENT =~ m/^\s*$/ ){ # overridden method with no new docs - so don't create a Docstring and use # parent class Docstring } else { dbg_info('writing comment'); if ( $COMMENT !~ m/^\s*$/ ){ dbg_info('comment non-empty'); my $doc_prepend = ""; $doc_prepend = "\@DOCSTRINGSTEMPLATE\@" if $COMMENT_TEMPLATE_DOCSTRING == 1; write_output("CM1", "$doc_prepend%Docstring\n"); my @comment_lines = split /\n/, $COMMENT; my $skipping_param = 0; my @out_params = (); my $waiting_for_return_to_end = 0; foreach my $comment_line (@comment_lines) { if ( ( $comment_line =~ m/versionadded:/ || $comment_line =~ m/deprecated:/ ) && $#out_params >= 0 ){ dbg_info('out style parameters remain to flush!'); # member has /Out/ parameters, but no return type, so flush out out_params docs now my $first_out_param = shift(@out_params); write_output("CM7", "$doc_prepend:return: - $first_out_param\n"); foreach my $out_param (@out_params) { write_output("CM7", "$doc_prepend - $out_param\n"); } write_output("CM7", "$doc_prepend\n"); @out_params = (); } # if ( $RETURN_TYPE ne '' && $comment_line =~ m/^\s*\.\. \w/ ){ # # return type must be added before any other paragraph-level markup # write_output("CM5", ":rtype: $RETURN_TYPE\n\n"); # $RETURN_TYPE = ''; # } if ( $comment_line =~ m/^:param\s+(\w+)/) { if ( (any { $_ eq $1 } @SKIPPED_PARAMS_OUT) || (any { $_ eq $1 } @SKIPPED_PARAMS_REMOVE) ) { if ( any { $_ eq $1 } @SKIPPED_PARAMS_OUT ) { $comment_line =~ s/^:param\s+(\w+):\s*(.*?)$/$1: $2/; $comment_line =~ s/(?:optional|if specified|if given)[,]?\s*//g; push @out_params, $comment_line ; $skipping_param = 2; } else { $skipping_param = 1; } next; } } if ( $skipping_param > 0 ) { if ( $comment_line =~ m/^(:.*|\.\..*|\s*)$/ ){ $skipping_param = 0; } elsif ( $skipping_param == 2 ) { $comment_line =~ s/^\s+/ /; $out_params[$#out_params] .= $comment_line; # exit_with_error('Skipped param (SIP_OUT) should have their doc on a single line'); next; } else { next; } } if ( $comment_line =~ m/:return:/ && $#out_params >= 0 ){ $waiting_for_return_to_end = 1; $comment_line =~ s/:return:/:return: -/; write_output("CM2", "$doc_prepend$comment_line\n"); foreach my $out_param (@out_params) { write_output("CM7", "$doc_prepend - $out_param\n"); } @out_params = (); } else { write_output("CM2", "$doc_prepend$comment_line\n"); } if ( $waiting_for_return_to_end == 1 ) { if ($comment_line =~ m/^(:.*|\.\..*|\s*)$/) { $waiting_for_return_to_end = 0; } else { # exit_with_error('Return docstring should be single line with SIP_OUT params'); } } # if ( $RETURN_TYPE ne '' && $comment_line =~ m/:return:/ ){ # # return type must be added before any other paragraph-level markup # write_output("CM5", ":rtype: $RETURN_TYPE\n\n"); # $RETURN_TYPE = ''; # } } exit_with_error("A method with output parameters must contain a return directive (method returns ${RETURN_TYPE})") if $#out_params >= 0 and $RETURN_TYPE ne ''; write_output("CM4", "$doc_prepend%End\n"); } # if ( $RETURN_TYPE ne '' ){ # write_output("CM3", "\n:rtype: $RETURN_TYPE\n"); # } } $COMMENT = ''; $RETURN_TYPE = ''; if ($IS_OVERRIDE_OR_MAKE_PRIVATE == PREPEND_CODE_MAKE_PRIVATE){ write_output("MKP", $LAST_ACCESS_SECTION_LINE); } $IS_OVERRIDE_OR_MAKE_PRIVATE = PREPEND_CODE_NO; } else { if ($IS_OVERRIDE_OR_MAKE_PRIVATE == PREPEND_CODE_MAKE_PRIVATE){ write_output("MKP", $LAST_ACCESS_SECTION_LINE); } $IS_OVERRIDE_OR_MAKE_PRIVATE = PREPEND_CODE_NO; } } if ( $sip_output ne ''){ open(FH, '>', $sip_output) or die $!; print FH join('', sip_header_footer()); print FH join('',@OUTPUT); print FH join('', sip_header_footer()); close(FH); } else { print join('', sip_header_footer()); print join('',@OUTPUT); print join('', sip_header_footer()); } if ( $python_output ne '' ){ unlink $python_output or 1; if ( @OUTPUT_PYTHON ){ open(FH2, '>', $python_output) or die $!; print FH2 join('', python_header()); print FH2 join('', @OUTPUT_PYTHON); close(FH2); } }