mirror of
https://github.com/qgis/QGIS.git
synced 2025-10-06 00:07:29 -04:00
2849 lines
122 KiB
Python
Executable File
2849 lines
122 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
|
|
|
|
import argparse
|
|
import os
|
|
import re
|
|
import sys
|
|
from collections import defaultdict
|
|
from enum import Enum, auto
|
|
from typing import List, Dict, Any, Tuple, Optional
|
|
|
|
import yaml
|
|
|
|
|
|
class Visibility(Enum):
|
|
Private = auto()
|
|
Protected = auto()
|
|
Public = auto()
|
|
Signals = auto()
|
|
|
|
|
|
class CodeSnippetType(Enum):
|
|
NotCodeSnippet = auto()
|
|
NotSpecified = auto()
|
|
Cpp = auto()
|
|
|
|
|
|
class PrependType(Enum):
|
|
NoPrepend = auto()
|
|
Virtual = auto()
|
|
MakePrivate = auto()
|
|
|
|
|
|
class MultiLineType(Enum):
|
|
NotMultiline = auto()
|
|
Method = auto()
|
|
ConditionalStatement = auto()
|
|
|
|
|
|
# Parse command-line arguments
|
|
parser = argparse.ArgumentParser(
|
|
description="Convert header file to SIP and Python")
|
|
parser.add_argument("-debug", action="store_true", help="Enable debug mode")
|
|
parser.add_argument("-qt6", action="store_true", help="Enable Qt6 mode")
|
|
parser.add_argument("-sip_output", help="SIP output file")
|
|
parser.add_argument("-python_output", help="Python output file")
|
|
parser.add_argument("-class_map", help="Class map file")
|
|
parser.add_argument("headerfile", help="Input header file")
|
|
args = parser.parse_args()
|
|
|
|
# Read the input file
|
|
try:
|
|
with open(args.headerfile, "r") as f:
|
|
input_lines = f.read().splitlines()
|
|
except IOError as e:
|
|
print(f"Couldn't open '{args.headerfile}' for reading because: {e}",
|
|
file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
# Read configuration
|
|
cfg_file = os.path.join(os.path.dirname(__file__), '../python/sipify.yaml')
|
|
try:
|
|
with open(cfg_file, 'r') as f:
|
|
sip_config = yaml.safe_load(f)
|
|
except IOError as e:
|
|
print(f"Couldn't open configuration file '{cfg_file}' because: {e}",
|
|
file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
|
|
# Initialize contexts
|
|
|
|
|
|
class Context:
|
|
|
|
def __init__(self):
|
|
self.debug: bool = False
|
|
self.is_qt6: bool = False
|
|
self.header_file: str = ''
|
|
|
|
self.current_line: str = ''
|
|
self.sip_run: bool = False
|
|
self.header_code: bool = False
|
|
self.access: List[Visibility] = [Visibility.Public]
|
|
self.multiline_definition: MultiLineType = MultiLineType.NotMultiline
|
|
self.classname: List[str] = []
|
|
self.class_and_struct: List[str] = []
|
|
self.declared_classes: List[str] = []
|
|
self.all_fully_qualified_class_names: List[str] = []
|
|
self.exported: List[int] = [0]
|
|
self.actual_class: str = ''
|
|
self.python_signature: str = ''
|
|
self.enum_int_types: List[str] = []
|
|
self.enum_intflag_types: List[str] = []
|
|
self.enum_class_non_int_types: List[str] = []
|
|
self.enum_monkey_patched_types: List = []
|
|
self.indent: str = ''
|
|
self.prev_indent: str = ''
|
|
self.comment: str = ''
|
|
self.comment_param_list: bool = False
|
|
self.comment_last_line_note_warning: bool = False
|
|
self.comment_code_snippet: CodeSnippetType = CodeSnippetType.NotCodeSnippet
|
|
self.comment_template_docstring: bool = False
|
|
self.skipped_params_out: List[str] = []
|
|
self.skipped_params_remove: List[str] = []
|
|
self.ifdef_nesting_idx: int = 0
|
|
self.bracket_nesting_idx: List[int] = [0]
|
|
self.private_section_line: str = ''
|
|
self.last_access_section_line: str = ''
|
|
self.return_type: str = ''
|
|
self.is_override_or_make_private: PrependType = PrependType.NoPrepend
|
|
self.if_feature_condition: str = ''
|
|
self.found_since: bool = False
|
|
self.qflag_hash: Dict[str, Any] = {}
|
|
self.input_lines: List[str] = []
|
|
self.line_count: int = len(input_lines)
|
|
self.line_idx: int = 0
|
|
self.output: List[str] = []
|
|
self.output_python: List[str] = []
|
|
self.doxy_inside_sip_run: int = 0
|
|
self.has_pushed_force_int: bool = False
|
|
self.attribute_docstrings = defaultdict(dict)
|
|
self.struct_docstrings = defaultdict(dict)
|
|
self.current_method_name: str = ''
|
|
self.static_methods = defaultdict(dict)
|
|
self.current_signal_args = []
|
|
self.signal_arguments = defaultdict(dict)
|
|
|
|
def current_fully_qualified_class_name(self) -> str:
|
|
return '.'.join(
|
|
_c for _c in ([c for c in self.classname if c != self.actual_class] + [
|
|
self.actual_class]) if _c)
|
|
|
|
def current_fully_qualified_struct_name(self) -> str:
|
|
return '.'.join(self.class_and_struct)
|
|
|
|
|
|
CONTEXT = Context()
|
|
CONTEXT.debug = args.debug
|
|
CONTEXT.is_qt6 = args.qt6
|
|
CONTEXT.header_file = args.headerfile
|
|
CONTEXT.input_lines = input_lines
|
|
CONTEXT.line_count = len(input_lines)
|
|
|
|
ALLOWED_NON_CLASS_ENUMS = [
|
|
"QgsSipifyHeader::MyEnum",
|
|
"QgsSipifyHeader::OneLiner",
|
|
"CadConstraint::LockMode",
|
|
"ColorrampTable",
|
|
"QgsMesh::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"
|
|
]
|
|
|
|
|
|
def replace_macros(line):
|
|
global CONTEXT
|
|
|
|
line = re.sub(r'\bTRUE\b', '``True``', line)
|
|
line = re.sub(r'\bFALSE\b', '``False``', line)
|
|
line = re.sub(r'\bNULLPTR\b', '``None``', line)
|
|
|
|
if CONTEXT.is_qt6:
|
|
# sip for Qt6 chokes on QList/QVector<QVariantMap>, but is happy if you expand out the map explicitly
|
|
line = re.sub(r'(QList<\s*|QVector<\s*)QVariantMap',
|
|
r'\1QMap<QString, QVariant>', line)
|
|
|
|
return line
|
|
|
|
|
|
def read_line():
|
|
global CONTEXT
|
|
|
|
new_line = CONTEXT.input_lines[CONTEXT.line_idx]
|
|
CONTEXT.line_idx += 1
|
|
|
|
if CONTEXT.debug:
|
|
print(
|
|
f'LIN:{CONTEXT.line_idx} DEPTH:{len(CONTEXT.access)} ACC:{CONTEXT.access[-1]} '
|
|
f'BRCK:{CONTEXT.bracket_nesting_idx[-1]} SIP:{CONTEXT.sip_run} MLT:{CONTEXT.multiline_definition} '
|
|
f'OVR: {CONTEXT.is_override_or_make_private} CLSS: {CONTEXT.actual_class}/{len(CONTEXT.classname)} :: {new_line}')
|
|
|
|
new_line = replace_macros(new_line)
|
|
return new_line
|
|
|
|
|
|
def write_output(dbg_code, out, prepend="no"):
|
|
global CONTEXT
|
|
|
|
if CONTEXT.debug:
|
|
dbg_code = f"{CONTEXT.line_idx} {dbg_code:<4} :: "
|
|
else:
|
|
dbg_code = ''
|
|
|
|
if prepend == "prepend":
|
|
CONTEXT.output.insert(0, dbg_code + out)
|
|
else:
|
|
if CONTEXT.if_feature_condition != '':
|
|
CONTEXT.output.append(f"%If ({CONTEXT.if_feature_condition})\n")
|
|
CONTEXT.output.append(dbg_code + out)
|
|
if CONTEXT.if_feature_condition != '':
|
|
CONTEXT.output.append("%End\n")
|
|
|
|
CONTEXT.if_feature_condition = ''
|
|
|
|
|
|
def dbg_info(info):
|
|
global CONTEXT
|
|
|
|
if CONTEXT.debug:
|
|
CONTEXT.output.append(f"{info}\n")
|
|
print(
|
|
f"{CONTEXT.line_idx} {len(CONTEXT.access)} {CONTEXT.sip_run} {CONTEXT.multiline_definition} {info}")
|
|
|
|
|
|
def exit_with_error(message):
|
|
global CONTEXT
|
|
sys.exit(
|
|
f"! Sipify error in {CONTEXT.header_file} at line :: {CONTEXT.line_idx}\n! {message}")
|
|
|
|
|
|
def sip_header_footer():
|
|
global CONTEXT
|
|
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
|
|
headerfile_x = re.sub(r'src/core/3d', r'src/core/./3d',
|
|
CONTEXT.header_file)
|
|
header_footer.append(
|
|
"/************************************************************************\n")
|
|
header_footer.append(
|
|
" * This file has been generated automatically from *\n")
|
|
header_footer.append(
|
|
" * *\n")
|
|
header_footer.append(f" * {headerfile_x:<68} *\n")
|
|
header_footer.append(
|
|
" * *\n")
|
|
header_footer.append(
|
|
" * Do not edit manually ! Edit header and run scripts/sipify.py again *\n")
|
|
header_footer.append(
|
|
" ************************************************************************/\n")
|
|
return header_footer
|
|
|
|
|
|
def python_header():
|
|
global CONTEXT
|
|
header = []
|
|
headerfile_x = re.sub(r'src/core/3d', r'src/core/./3d',
|
|
CONTEXT.header_file)
|
|
header.append("# The following has been generated automatically from ")
|
|
header.append(f"{headerfile_x}\n")
|
|
return header
|
|
|
|
|
|
def create_class_links(line):
|
|
global CONTEXT
|
|
|
|
# Replace Qgs classes (but not the current class) with :py:class: links
|
|
class_link_match = re.search(r'\b(Qgs[A-Z]\w+|Qgis)\b(\.?$|\W{2})', line)
|
|
if class_link_match:
|
|
if CONTEXT.actual_class and class_link_match.group(1) != CONTEXT.actual_class:
|
|
line = re.sub(r'\b(Qgs[A-Z]\w+)\b(\.?$|\W{2})',
|
|
r':py:class:`\1`\2', line)
|
|
|
|
# Replace Qgs class methods with :py:func: links
|
|
line = re.sub(r'\b((Qgs[A-Z]\w+|Qgis)\.[a-z]\w+\(\))(?!\w)',
|
|
r':py:func:`\1`', line)
|
|
|
|
# Replace other methods with :py:func: links
|
|
if CONTEXT.actual_class:
|
|
line = re.sub(r'(?<!\.)\b([a-z]\w+)\(\)(?!\w)',
|
|
rf':py:func:`~{CONTEXT.actual_class}.\1`', line)
|
|
else:
|
|
line = re.sub(r'(?<!\.)\b([a-z]\w+)\(\)(?!\w)', r':py:func:`~\1`',
|
|
line)
|
|
|
|
# Replace Qgs classes (but not the current class) with :py:class: links
|
|
class_link_match = re.search(r'\b(?<![`~])(Qgs[A-Z]\w+|Qgis)\b(?!\()', line)
|
|
if class_link_match:
|
|
if not CONTEXT.actual_class or class_link_match.group(1) != CONTEXT.actual_class:
|
|
line = re.sub(r'\b(?<![`~])(Qgs[A-Z]\w+|Qgis)\b(?!\()',
|
|
r':py:class:`\1`', line)
|
|
|
|
return line
|
|
|
|
|
|
def process_doxygen_line(line: str) -> str:
|
|
global CONTEXT
|
|
|
|
# Handle SIP_RUN preprocessor directives
|
|
if re.search(r'\s*#ifdef SIP_RUN', line):
|
|
CONTEXT.doxy_inside_sip_run = 1
|
|
return ""
|
|
elif re.search(r'\s*#ifndef SIP_RUN', line):
|
|
CONTEXT.doxy_inside_sip_run = 2
|
|
return ""
|
|
elif CONTEXT.doxy_inside_sip_run != 0 and re.search(r'\s*#else', line):
|
|
CONTEXT.doxy_inside_sip_run = 2 if CONTEXT.doxy_inside_sip_run == 1 else 1
|
|
return ""
|
|
elif CONTEXT.doxy_inside_sip_run != 0 and re.search(r'\s*#endif', line):
|
|
CONTEXT.doxy_inside_sip_run = 0
|
|
return ""
|
|
|
|
if CONTEXT.doxy_inside_sip_run == 2:
|
|
return ""
|
|
|
|
if r'\copydoc' in line:
|
|
exit_with_error(
|
|
'\\copydoc doxygen command cannot be used for methods exposed to Python')
|
|
|
|
if re.search(r'<(?:dl|dt|dd>)', line):
|
|
exit_with_error(
|
|
"Don't use raw html <dl>, <dt> or <dd> tags in documentation. "
|
|
"Use markdown headings instead")
|
|
if re.search(r'<h\d>', line):
|
|
exit_with_error(
|
|
"Don't use raw html heading tags in documentation. "
|
|
"Use markdown headings instead")
|
|
if re.search(r'<li>', line):
|
|
exit_with_error(
|
|
"Don't use raw html lists in documentation. "
|
|
"Use markdown lists instead")
|
|
if re.search(r'<[ib]>', line):
|
|
exit_with_error(
|
|
"Don't use raw <i> or <b> tags in documentation. "
|
|
"Use markdown instead")
|
|
|
|
# Detect code snippet
|
|
code_match = re.search(r'\\code(\{\.?(\w+)})?', line)
|
|
if code_match:
|
|
codelang = f" {code_match.group(2)}" if code_match.group(2) else ""
|
|
if not re.search(r'(cpp|py|unparsed)', codelang):
|
|
exit_with_error(f"invalid code snippet format: {codelang}")
|
|
CONTEXT.comment_code_snippet = CodeSnippetType.NotSpecified
|
|
if re.search(r'cpp', codelang):
|
|
CONTEXT.comment_code_snippet = CodeSnippetType.Cpp
|
|
codelang = codelang.replace('py', 'python').replace('unparsed', 'text')
|
|
return "\n" if CONTEXT.comment_code_snippet == CodeSnippetType.Cpp else f"\n.. code-block::{codelang}\n\n"
|
|
|
|
if re.search(r'\\endcode', line):
|
|
CONTEXT.comment_code_snippet = CodeSnippetType.NotCodeSnippet
|
|
return "\n"
|
|
|
|
if CONTEXT.comment_code_snippet != CodeSnippetType.NotCodeSnippet:
|
|
if CONTEXT.comment_code_snippet == CodeSnippetType.Cpp:
|
|
return ""
|
|
else:
|
|
return f" {line}\n" if line != '' else "\n"
|
|
|
|
# Remove prepending spaces and apply various replacements
|
|
line = re.sub(r'^\s+', '', line)
|
|
line = re.sub(r'\\a (.+?)\b', r'``\1``', line)
|
|
line = line.replace('::', '.')
|
|
line = re.sub(r'\bnullptr\b', 'None', line)
|
|
|
|
# Handle section and subsection
|
|
section_match = re.match(r'^\\(?P<SUB>sub)?section', line)
|
|
if section_match:
|
|
sep = "^" if section_match.group('SUB') else "-"
|
|
line = re.sub(r'^\\(sub)?section \w+ ', '', line)
|
|
sep_line = re.sub(r'[\w ()]', sep, line)
|
|
line += f"\n{sep_line}"
|
|
|
|
# Convert ### style headings
|
|
heading_match = re.match(r'^###\s+(.*)$', line)
|
|
if heading_match:
|
|
line = f"{heading_match.group(1)}\n{'-' * (len(heading_match.group(1)) + 30)}"
|
|
heading_match = re.match(r'^##\s+(.*)$', line)
|
|
if heading_match:
|
|
line = f"{heading_match.group(1)}\n{'=' * (len(heading_match.group(1)) + 30)}"
|
|
|
|
if line == '*':
|
|
line = ''
|
|
|
|
# Handle multi-line parameters/returns/lists
|
|
if line != '':
|
|
if re.match(r'^\s*[\-#]', line):
|
|
line = f"{CONTEXT.prev_indent}{line}"
|
|
CONTEXT.indent = f"{CONTEXT.prev_indent} "
|
|
elif not re.match(
|
|
r'^\s*[\\:]+(param|note|since|return|deprecated|warning|throws)',
|
|
line):
|
|
line = f"{CONTEXT.indent}{line}"
|
|
else:
|
|
CONTEXT.prev_indent = CONTEXT.indent
|
|
CONTEXT.indent = ''
|
|
|
|
# Replace \returns with :return:
|
|
if re.search(r'\\return(s)?', line):
|
|
line = re.sub(r'\s*\\return(s)?\s*', '\n:return: ', line)
|
|
line = re.sub(r'\s*$', '', line)
|
|
CONTEXT.indent = ' ' * (line.index(':', 4) + 1)
|
|
|
|
# Handle params
|
|
if re.search(r'\\param(?:\[(?:out|in|,)+])? ', line):
|
|
line = re.sub(r'\s*\\param(?:\[(?:out|in|,)+])?\s+(\w+)\b\s*', r':param \1: ', line)
|
|
line = re.sub(r'\s*$', '', line)
|
|
CONTEXT.indent = ' ' * (line.index(':', 2) + 2)
|
|
if line.startswith(':param'):
|
|
if not CONTEXT.comment_param_list:
|
|
line = f"\n{line}"
|
|
CONTEXT.comment_param_list = True
|
|
CONTEXT.comment_last_line_note_warning = False
|
|
|
|
# Handle brief
|
|
if re.match(r'^\s*[\\@]brief', line):
|
|
line = re.sub(r'[\\@]brief\s*', '', line)
|
|
if CONTEXT.found_since:
|
|
exit_with_error(
|
|
f"{CONTEXT.header_file}::{CONTEXT.line_idx} Since annotation must come after brief")
|
|
CONTEXT.found_since = False
|
|
if re.match(r'^\s*$', line):
|
|
return ""
|
|
|
|
# Handle ingroup and class
|
|
if re.search(r'[\\@](ingroup|class)', line):
|
|
CONTEXT.prev_indent = CONTEXT.indent
|
|
CONTEXT.indent = ''
|
|
return ""
|
|
|
|
# Handle since
|
|
since_match = re.search(r'\\since .*?([\d.]+)', line, re.IGNORECASE)
|
|
if since_match:
|
|
CONTEXT.prev_indent = CONTEXT.indent
|
|
CONTEXT.indent = ''
|
|
CONTEXT.found_since = True
|
|
return f"\n.. versionadded:: {since_match.group(1)}\n"
|
|
|
|
# Handle deprecated
|
|
if deprecated_match := re.search(
|
|
r'\\deprecated QGIS (?P<DEPR_VERSION>[0-9.]+)\s*(?P<DEPR_MESSAGE>.*)?',
|
|
line,
|
|
re.IGNORECASE):
|
|
CONTEXT.prev_indent = CONTEXT.indent
|
|
CONTEXT.indent = ''
|
|
version = deprecated_match.group('DEPR_VERSION')
|
|
if version.endswith('.'):
|
|
version = version[:-1]
|
|
depr_line = f"\n.. deprecated:: {version}"
|
|
message = deprecated_match.group('DEPR_MESSAGE')
|
|
if message:
|
|
depr_line += "\n"
|
|
depr_line += "\n".join(f"\n {_m}" for _m in message.split('\n'))
|
|
return create_class_links(depr_line)
|
|
|
|
# Handle see also
|
|
see_matches = list(re.finditer(r'\\see +([\w:/.#-]+(\.\w+)*)(\([^()]*\))?(\.?)', line))
|
|
if see_matches:
|
|
for see_match in reversed(see_matches):
|
|
seealso = see_match.group(1)
|
|
seealso_suffix = see_match.group(4)
|
|
|
|
seeline = ''
|
|
dbg_info(f"see also: `{seealso}`")
|
|
if re.match(r'^http', seealso):
|
|
seeline = f"{seealso}"
|
|
elif seealso_match := re.match(r'^(Qgs[A-Z]\w+(\([^()]*\))?)(\.)?$', seealso):
|
|
dbg_info(f"\\see :py:class:`{seealso_match.group(1)}`")
|
|
seeline = f":py:class:`{seealso_match.group(1)}`{seealso_match.group(3) or ''}"
|
|
elif seealso_match := re.match(r'^((Qgs[A-Z]\w+)\.(\w+)(\([^()]*\))?)(\.)?$', seealso):
|
|
dbg_info(f"\\see py:func with param: :py:func:`{seealso_match.group(1)}`")
|
|
seeline = f":py:func:`{seealso_match.group(1)}`{seealso_match.group(5) or ''}"
|
|
elif seealso_match := re.match(r'^([a-z]\w+(\([^()]*\))?)(\.)?$', seealso):
|
|
dbg_info(f"\\see :py:func:`{seealso_match.group(1)}`")
|
|
seeline = f":py:func:`{seealso_match.group(1)}`{seealso_match.group(3) or ''}"
|
|
|
|
if full_line_match := re.match(r'^\s*\\see +(\w+(?:\.\w+)*)(?:\([^()]*\))?[\s,.:-]*(.*?)$', line):
|
|
if seeline.startswith('http'):
|
|
return f"\n.. seealso:: {seeline}\n"
|
|
suffix = full_line_match.group(2)
|
|
if suffix:
|
|
return f"\n.. seealso:: {seeline or seealso} {suffix.strip()}\n"
|
|
else:
|
|
return f"\n.. seealso:: {seeline or seealso}\n"
|
|
else:
|
|
if seeline:
|
|
line = line[:see_match.start()] + seeline + seealso_suffix + line[
|
|
see_match.end():] # re.sub(r'\\see +(\w+(\.\w+)*(\(\))?)', seeline, line)
|
|
else:
|
|
line = line.replace('\\see', 'see')
|
|
elif not re.search(r'\\throws.*', line):
|
|
line = create_class_links(line)
|
|
|
|
# Handle note, warning, and throws
|
|
note_match = re.search(r'[\\@]note (.*)', line)
|
|
if note_match:
|
|
CONTEXT.comment_last_line_note_warning = True
|
|
CONTEXT.prev_indent = CONTEXT.indent
|
|
CONTEXT.indent = ''
|
|
return f"\n.. note::\n\n {note_match.group(1)}\n"
|
|
|
|
warning_match = re.search(r'[\\@]warning (.*)', line)
|
|
if warning_match:
|
|
CONTEXT.prev_indent = CONTEXT.indent
|
|
CONTEXT.indent = ''
|
|
CONTEXT.comment_last_line_note_warning = True
|
|
return f"\n.. warning::\n\n {warning_match.group(1)}\n"
|
|
|
|
throws_match = re.search(r'[\\@]throws (.+?)\b\s*(.*)', line)
|
|
if throws_match:
|
|
CONTEXT.prev_indent = CONTEXT.indent
|
|
CONTEXT.indent = ''
|
|
CONTEXT.comment_last_line_note_warning = True
|
|
return f"\n:raises {throws_match.group(1)}: {throws_match.group(2)}\n"
|
|
|
|
if line.strip():
|
|
if CONTEXT.comment_last_line_note_warning:
|
|
dbg_info(f"prepend spaces for multiline warning/note xx{line}")
|
|
line = f" {line}"
|
|
else:
|
|
CONTEXT.comment_last_line_note_warning = False
|
|
|
|
return f"{line}\n"
|
|
|
|
|
|
def detect_and_remove_following_body_or_initializerlist():
|
|
global CONTEXT
|
|
|
|
signature = ''
|
|
|
|
# Complex regex pattern to match various C++ function declarations and definitions
|
|
pattern1 = r'^(\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*\/\/.*)?$'
|
|
pattern2 = r'SIP_SKIP\s*(?!;)\s*(\/\/.*)?$'
|
|
pattern3 = r'^\s*class.*SIP_SKIP'
|
|
|
|
if (re.match(pattern1, CONTEXT.current_line) or
|
|
re.search(pattern2, CONTEXT.current_line) or
|
|
re.match(pattern3, CONTEXT.current_line)):
|
|
|
|
dbg_info(
|
|
"remove constructor definition, function bodies, member initializing list (1)")
|
|
|
|
# Extract the parts we want to keep
|
|
initializer_match = re.match(pattern1, CONTEXT.current_line)
|
|
if initializer_match:
|
|
newline = f"{initializer_match.group(1) or ''}{initializer_match.group(2) or ''}{initializer_match.group(3)};"
|
|
else:
|
|
newline = CONTEXT.current_line
|
|
|
|
# Call remove_following_body_or_initializerlist() if necessary
|
|
if not re.search(r'{.*}(\s*SIP_\w+)*\s*(//.*)?$',
|
|
CONTEXT.current_line):
|
|
signature = remove_following_body_or_initializerlist()
|
|
|
|
CONTEXT.current_line = newline
|
|
|
|
return signature
|
|
|
|
|
|
def remove_following_body_or_initializerlist():
|
|
global CONTEXT
|
|
|
|
signature = ''
|
|
|
|
dbg_info(
|
|
"remove constructor definition, function bodies, member initializing list (2)")
|
|
line = read_line()
|
|
|
|
# Python signature
|
|
if re.match(r'^\s*\[\s*(\w+\s*)?\(', line):
|
|
dbg_info("python signature detected")
|
|
_nesting_index = 0
|
|
while CONTEXT.line_idx < CONTEXT.line_count:
|
|
_nesting_index += line.count('[')
|
|
_nesting_index -= line.count(']')
|
|
if _nesting_index == 0:
|
|
line_match = re.match(r'^(.*);\s*(//.*)?$', line)
|
|
if line_match:
|
|
line = line_match.group(1) # remove semicolon (added later)
|
|
signature += f"\n{line}"
|
|
return signature
|
|
break
|
|
signature += f"\n{line}"
|
|
line = read_line()
|
|
|
|
# Member initializing list
|
|
while re.match(r'^\s*[:,]\s+([\w<>]|::)+\(.*?\)', line):
|
|
dbg_info("member initializing list")
|
|
line = read_line()
|
|
|
|
# Body
|
|
if re.match(r'^\s*\{', line):
|
|
_nesting_index = 0
|
|
while CONTEXT.line_idx < CONTEXT.line_count:
|
|
dbg_info(" remove body")
|
|
_nesting_index += line.count('{')
|
|
_nesting_index -= line.count('}')
|
|
if _nesting_index == 0:
|
|
break
|
|
line = read_line()
|
|
|
|
return signature
|
|
|
|
|
|
def replace_alternative_types(text):
|
|
"""
|
|
Handle SIP_PYALTERNATIVETYPE annotation
|
|
"""
|
|
# Original perl regex was:
|
|
# s/(\w+)(\<(?>[^<>]|(?2))*\>)?\s+SIP_PYALTERNATIVETYPE\(\s*\'?([^()']+)(\(\s*(?:[^()]++|(?2))*\s*\))?\'?\s*\)/$3/g;
|
|
_pattern = r'(\w+)(<(?:[^<>]|<(?:[^<>]|<[^<>]*>)*>)*>)?\s+SIP_PYALTERNATIVETYPE\(\s*\'?([^()\']+)(\(\s*(?:[^()]|\([^()]*\))*\s*\))?\'?\s*\)'
|
|
|
|
while True:
|
|
new_text = re.sub(_pattern, r'\3', text, flags=re.S)
|
|
if new_text == text:
|
|
return text
|
|
text = new_text
|
|
|
|
|
|
def split_args(args_string: str) -> List[str]:
|
|
"""
|
|
Tries to split a line of arguments into separate parts
|
|
"""
|
|
res = []
|
|
current_arg = ''
|
|
paren_level = 0
|
|
angle_level = 0
|
|
|
|
for char in args_string:
|
|
if char == ',' and paren_level == 0 and angle_level == 0:
|
|
res.append(current_arg.strip())
|
|
current_arg = ''
|
|
else:
|
|
current_arg += char
|
|
if char == '(':
|
|
paren_level += 1
|
|
elif char == ')':
|
|
paren_level -= 1
|
|
elif char == '<':
|
|
angle_level += 1
|
|
elif char == '>':
|
|
angle_level -= 1
|
|
|
|
if current_arg:
|
|
res.append(current_arg.strip())
|
|
|
|
return res
|
|
|
|
|
|
def remove_sip_pyargremove(input_string: str) -> str:
|
|
"""
|
|
Remove SIP_PYARGREMOVE annotated arguments
|
|
"""
|
|
global CONTEXT
|
|
# Split the string into function signature and body
|
|
signature_split = re.match(r'(.*?)\((.*)\)(.*)', input_string)
|
|
if signature_split and 'SIP_PYARGREMOVE' not in signature_split.group(1):
|
|
prefix, arguments, suffix = signature_split.groups()
|
|
prefix += '('
|
|
suffix = ')' + suffix
|
|
else:
|
|
signature_split = re.match(r'(\s*)(.*)\)(.*)', input_string)
|
|
if signature_split:
|
|
prefix, arguments, suffix = signature_split.groups()
|
|
suffix = ')' + suffix
|
|
else:
|
|
prefix = ''
|
|
arguments = input_string
|
|
suffix = ''
|
|
|
|
arguments_list = split_args(arguments)
|
|
|
|
if CONTEXT.is_qt6:
|
|
filtered_args = [arg for arg in arguments_list if
|
|
'SIP_PYARGREMOVE' not in arg]
|
|
else:
|
|
filtered_args = [re.sub(r'\s*SIP_PYARGREMOVE6\s*', ' ', arg)
|
|
for arg in arguments_list if
|
|
not ('SIP_PYARGREMOVE' in arg and 'SIP_PYARGREMOVE6' not in arg)]
|
|
|
|
# Reassemble the function signature
|
|
remaining_args = ', '.join(filtered_args)
|
|
if remaining_args and prefix.strip():
|
|
prefix += ' '
|
|
if remaining_args and suffix.strip():
|
|
suffix = ' ' + suffix
|
|
return f"{prefix}{remaining_args}{suffix}"
|
|
|
|
|
|
def fix_annotations(line):
|
|
global CONTEXT
|
|
|
|
# Get removed params to be able to drop them out of the API doc
|
|
removed_params = re.findall(r'(\w+)\s+SIP_PYARGREMOVE', line)
|
|
if CONTEXT.is_qt6:
|
|
removed_params = re.findall(r'(\w+)\s+SIP_PYARGREMOVE6?', line)
|
|
for param in removed_params:
|
|
CONTEXT.skipped_params_remove.append(param)
|
|
dbg_info(f"caught removed param: {CONTEXT.skipped_params_remove[-1]}")
|
|
|
|
_out_params = re.findall(r'(\w+)\s+SIP_OUT', line)
|
|
for param in _out_params:
|
|
CONTEXT.skipped_params_out.append(param)
|
|
dbg_info(f"caught removed param: {CONTEXT.skipped_params_out[-1]}")
|
|
|
|
# Printed annotations
|
|
replacements = {
|
|
r'//\s*SIP_ABSTRACT\b': '/Abstract/',
|
|
r'\bSIP_ABSTRACT\b': '/Abstract/',
|
|
r'\bSIP_ALLOWNONE\b': '/AllowNone/',
|
|
r'\bSIP_ARRAY\b': '/Array/',
|
|
r'\bSIP_ARRAYSIZE\b': '/ArraySize/',
|
|
r'\bSIP_DEPRECATED\b': '/Deprecated/',
|
|
r'\bSIP_CONSTRAINED\b': '/Constrained/',
|
|
r'\bSIP_EXTERNAL\b': '/External/',
|
|
r'\bSIP_FACTORY\b': '/Factory/',
|
|
r'\bSIP_IN\b': '/In/',
|
|
r'\bSIP_INOUT\b': '/In,Out/',
|
|
r'\bSIP_KEEPREFERENCE\b': '/KeepReference/',
|
|
r'\bSIP_NODEFAULTCTORS\b': '/NoDefaultCtors/',
|
|
r'\bSIP_OUT\b': '/Out/',
|
|
r'\bSIP_RELEASEGIL\b': '/ReleaseGIL/',
|
|
r'\bSIP_HOLDGIL\b': '/HoldGIL/',
|
|
r'\bSIP_TRANSFER\b': '/Transfer/',
|
|
r'\bSIP_TRANSFERBACK\b': '/TransferBack/',
|
|
r'\bSIP_TRANSFERTHIS\b': '/TransferThis/',
|
|
r'\bSIP_GETWRAPPER\b': '/GetWrapper/',
|
|
r'SIP_PYNAME\(\s*(\w+)\s*\)': r'/PyName=\1/',
|
|
r'SIP_TYPEHINT\(\s*([\w\.\s,\[\]]+?)\s*\)': r'/TypeHint="\1"/',
|
|
r'SIP_VIRTUALERRORHANDLER\(\s*(\w+)\s*\)': r'/VirtualErrorHandler=\1/',
|
|
r'SIP_THROW\(\s*([\w\s,]+?)\s*\)': r'throw( \1 )',
|
|
}
|
|
|
|
for _pattern, replacement in replacements.items():
|
|
line = re.sub(_pattern, replacement, line)
|
|
|
|
# Combine multiple annotations
|
|
while True:
|
|
new_line = re.sub(
|
|
r'/([\w,]+(="?[\w, \[\]]+"?)?)/\s*/([\w,]+(="?[\w, \[\]]+"?)?]?)/',
|
|
r'/\1,\3/', line)
|
|
if new_line == line:
|
|
break
|
|
line = new_line
|
|
dbg_info("combine multiple annotations -- works only for 2")
|
|
|
|
# Unprinted annotations
|
|
line = replace_alternative_types(line)
|
|
line = re.sub(r'(\w+)\s+SIP_PYARGRENAME\(\s*(\w+)\s*\)', r'\2', line)
|
|
|
|
# Note: this was the original perl regex, which isn't compatible with Python:
|
|
# line = re.sub(r"""=\s+[^=]*?\s+SIP_PYARGDEFAULT\(\s*\'?([^()']+)(\(\s*(?:[^()]++|(?2))*\s*\))?\'?\s*\)""", r'= \1', line)
|
|
line = re.sub(
|
|
r"""=\s+[^=]*?\s+SIP_PYARGDEFAULT\(\s*\'?([^()\']+)(\((?:[^()]|\([^()]*\))*\))?\'?\s*\)""",
|
|
r'= \1',
|
|
line)
|
|
|
|
# Remove argument
|
|
if 'SIP_PYARGREMOVE' in line:
|
|
dbg_info("remove arg")
|
|
if CONTEXT.multiline_definition != MultiLineType.NotMultiline:
|
|
prev_line = CONTEXT.output.pop().rstrip()
|
|
# Update multi line status
|
|
parenthesis_balance = prev_line.count('(') - prev_line.count(')')
|
|
if parenthesis_balance == 1:
|
|
CONTEXT.multiline_definition = MultiLineType.NotMultiline
|
|
# Concatenate with above line to bring previous commas
|
|
line = f"{prev_line} {line.lstrip()}\n"
|
|
|
|
# original perl regex was:
|
|
# (?<coma>, +)?(const )?(\w+)(\<(?>[^<>]|(?4))*\>)?\s+[\w&*]+\s+SIP_PYARGREMOVE( = [^()]*(\(\s*(?:[^()]++|(?6))*\s*\))?)?(?(<coma>)|,?)//
|
|
if 'SIP_PYARGREMOVE' in line:
|
|
line = remove_sip_pyargremove(line)
|
|
|
|
line = re.sub(r'\(\s+\)', '()', line)
|
|
|
|
line = re.sub(r'SIP_FORCE', '', line)
|
|
line = re.sub(r'SIP_DOC_TEMPLATE', '', line)
|
|
line = re.sub(r'\s+;$', ';', line)
|
|
|
|
return line
|
|
|
|
|
|
def fix_constants(line):
|
|
line = re.sub(r'\bstd::numeric_limits<double>::max\(\)', 'DBL_MAX', line)
|
|
line = re.sub(r'\bstd::numeric_limits<double>::lowest\(\)', '-DBL_MAX',
|
|
line)
|
|
line = re.sub(r'\bstd::numeric_limits<double>::epsilon\(\)', 'DBL_EPSILON',
|
|
line)
|
|
line = re.sub(r'\bstd::numeric_limits<qlonglong>::min\(\)', 'LLONG_MIN',
|
|
line)
|
|
line = re.sub(r'\bstd::numeric_limits<qlonglong>::max\(\)', 'LLONG_MAX',
|
|
line)
|
|
line = re.sub(r'\bstd::numeric_limits<int>::max\(\)', 'INT_MAX', line)
|
|
line = re.sub(r'\bstd::numeric_limits<int>::min\(\)', 'INT_MIN', line)
|
|
return line
|
|
|
|
|
|
def detect_comment_block(strict_mode=True):
|
|
# Initialize global or module-level variables if necessary
|
|
global CONTEXT
|
|
|
|
CONTEXT.comment_param_list = False
|
|
CONTEXT.indent = ''
|
|
CONTEXT.prev_indent = ''
|
|
CONTEXT.comment_code_snippet = CodeSnippetType.NotCodeSnippet
|
|
CONTEXT.comment_last_line_note_warning = False
|
|
CONTEXT.found_since = False
|
|
if CONTEXT.multiline_definition == MultiLineType.NotMultiline:
|
|
CONTEXT.skipped_params_out = []
|
|
CONTEXT.skipped_params_remove = []
|
|
|
|
if re.match(r'^\s*/\*', CONTEXT.current_line) or (
|
|
not strict_mode and '/*' in CONTEXT.current_line):
|
|
dbg_info("found comment block")
|
|
CONTEXT.comment = process_doxygen_line(
|
|
re.sub(r'^\s*/\*(\*)?(.*?)\n?$', r'\2', CONTEXT.current_line))
|
|
CONTEXT.comment = re.sub(r'^\s*$', '', CONTEXT.comment)
|
|
|
|
while not re.search(r'\*/\s*(//.*?)?$', CONTEXT.current_line):
|
|
CONTEXT.current_line = read_line()
|
|
CONTEXT.comment += process_doxygen_line(
|
|
re.sub(r'\s*\*?(.*?)(/)?\n?$', r'\1', CONTEXT.current_line))
|
|
|
|
CONTEXT.comment = re.sub(r'\n\s+\n', '\n\n', CONTEXT.comment)
|
|
CONTEXT.comment = re.sub(r'\n{3,}', '\n\n', CONTEXT.comment)
|
|
CONTEXT.comment = re.sub(r'\n+$', '', CONTEXT.comment)
|
|
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
def detect_non_method_member(line):
|
|
_pattern = r'''^\s*(?:template\s*<\w+>\s+)?(?:(const|mutable|static|friend|unsigned)\s+)*\w+(::\w+)?(<([\w<> *&,()]|::)+>)?(,?\s+\*?\w+( = (-?\d+(\.\d+)?|((QMap|QList)<[^()]+>\(\))|(\w+::)*\w+(\([^()]?\))?)|\[\d+\])?)+;'''
|
|
return re.match(_pattern, line)
|
|
|
|
|
|
def convert_type(cpp_type: str) -> str:
|
|
"""
|
|
Converts C++ types to Python types
|
|
"""
|
|
type_mapping = {
|
|
'int': 'int',
|
|
'float': 'float',
|
|
'double': 'float',
|
|
'bool': 'bool',
|
|
'char': 'str',
|
|
'QString': 'str',
|
|
'void': 'None',
|
|
'qint64': 'int',
|
|
'unsigned long long': 'int',
|
|
'long long': 'int',
|
|
'qlonglong': 'int',
|
|
'long': 'int',
|
|
'QStringList': 'List[str]',
|
|
'QVariantList': 'List[object]',
|
|
'QVariantMap': 'Dict[str, object]',
|
|
'QVariant': 'object'
|
|
}
|
|
|
|
# Handle templates
|
|
template_match = re.match(r'(\w+)\s*<\s*(.+)\s*>', cpp_type)
|
|
if template_match:
|
|
container, inner_type = template_match.groups()
|
|
if container in ('QVector', 'QList'):
|
|
return f"List[{convert_type(inner_type.strip())}]"
|
|
elif container in ('QSet',):
|
|
return f"Set[{convert_type(inner_type.strip())}]"
|
|
elif container in ('QHash', 'QMap'):
|
|
key_type, value_type = [t.strip() for t in inner_type.split(',')]
|
|
return f"Dict[{convert_type(key_type)}, {convert_type(value_type)}]"
|
|
else:
|
|
return f"{container}[{convert_type(inner_type.strip())}]"
|
|
|
|
if cpp_type not in type_mapping:
|
|
if cpp_type.startswith('Q'):
|
|
cpp_type = cpp_type.replace('::', '.')
|
|
return cpp_type
|
|
|
|
assert False, cpp_type
|
|
|
|
return type_mapping[cpp_type]
|
|
|
|
|
|
def parse_argument(arg: str) -> Tuple[str, str, Optional[str]]:
|
|
# Remove leading/trailing whitespace and 'const'
|
|
arg = re.sub(r'^\s*const\s+', '', arg.strip())
|
|
|
|
# Extract default value if present
|
|
default_match = re.search(r'=\s*(.+)$', arg)
|
|
default_value = default_match.group(1).strip() if default_match else None
|
|
arg = re.sub(r'\s*=\s*.+$', '', arg)
|
|
|
|
# Handle pointers and references
|
|
is_pointer = '*' in arg
|
|
arg = arg.replace('*', '').replace('&', '').strip()
|
|
|
|
# Split type and variable name
|
|
parts = arg.split()
|
|
if len(parts) > 1:
|
|
cpp_type = ' '.join(parts[:-1])
|
|
var_name = parts[-1]
|
|
else:
|
|
cpp_type = arg
|
|
var_name = ''
|
|
|
|
python_type = convert_type(cpp_type)
|
|
if is_pointer and default_value:
|
|
python_type = f"Optional[{python_type}]"
|
|
|
|
# Convert default value
|
|
if default_value:
|
|
default_value_map = {
|
|
'QVariantList()': '[]'
|
|
}
|
|
if default_value in default_value_map:
|
|
default_value = default_value_map[default_value]
|
|
elif default_value == "nullptr":
|
|
default_value = "None"
|
|
elif python_type == 'int':
|
|
pass
|
|
elif cpp_type in ("QString", ):
|
|
if default_value == 'QString()':
|
|
default_value = 'None'
|
|
python_type = f'Optional[{python_type}]'
|
|
elif default_value.startswith('Q'):
|
|
default_value = default_value.replace('::', '.')
|
|
else:
|
|
default_value = f'"{default_value}"'
|
|
elif cpp_type in ("bool",):
|
|
default_value = f'{"False" if default_value == "false" else "True"}'
|
|
elif cpp_type.startswith('Q'):
|
|
default_value = default_value.replace('::', '.')
|
|
else:
|
|
assert False, (default_value, cpp_type)
|
|
|
|
return var_name, python_type, default_value
|
|
|
|
|
|
def cpp_to_python_signature(cpp_function: str) -> str:
|
|
|
|
# Extract function name and arguments
|
|
match = re.match(r'(\w+)\s*\((.*)\)\s*(?:const)?\s*(?:->)?\s*([\w:]+)?', cpp_function)
|
|
if not match:
|
|
raise ValueError("Invalid C++ function signature")
|
|
|
|
func_name, args_str, return_type = match.groups()
|
|
args = [arg.strip() for arg in args_str.split(',') if arg.strip()]
|
|
|
|
# Parse arguments
|
|
python_args = []
|
|
for arg in args:
|
|
var_name, python_type, default_value = parse_argument(arg)
|
|
if default_value:
|
|
python_args.append(f"{var_name}: {python_type} = {default_value}")
|
|
else:
|
|
python_args.append(f"{var_name}: {python_type}")
|
|
|
|
# Construct Python function signature
|
|
python_signature = f"def {func_name}({', '.join(python_args)})"
|
|
if return_type:
|
|
python_signature += f" -> {convert_type(return_type)}"
|
|
|
|
return python_signature
|
|
|
|
|
|
while CONTEXT.line_idx < CONTEXT.line_count:
|
|
|
|
CONTEXT.python_signature = ''
|
|
CONTEXT.actual_class = CONTEXT.classname[-1] if CONTEXT.classname else None
|
|
CONTEXT.current_line = read_line()
|
|
|
|
if re.match(r'^\s*(#define\s+)?SIP_IF_MODULE\(.*\)$',
|
|
CONTEXT.current_line):
|
|
dbg_info('skipping SIP include condition macro')
|
|
continue
|
|
|
|
match = re.match(r'^(.*?)\s*//\s*cppcheck-suppress.*$',
|
|
CONTEXT.current_line)
|
|
if match:
|
|
CONTEXT.current_line = match.group(1)
|
|
|
|
match = re.match(r'^\s*SIP_FEATURE\(\s*(\w+)\s*\)(.*)$',
|
|
CONTEXT.current_line)
|
|
if match:
|
|
write_output("SF1", f"%Feature {match.group(1)}{match.group(2)}\n")
|
|
continue
|
|
|
|
match = re.match(r'^\s*SIP_PROPERTY\((.*)\)$', CONTEXT.current_line)
|
|
if match:
|
|
write_output("SF1", f"%Property({match.group(1)})\n")
|
|
continue
|
|
|
|
match = re.match(r'^\s*SIP_IF_FEATURE\(\s*(!?\w+)\s*\)(.*)$',
|
|
CONTEXT.current_line)
|
|
if match:
|
|
write_output("SF2", f"%If ({match.group(1)}){match.group(2)}\n")
|
|
continue
|
|
|
|
match = re.match(r'^\s*SIP_CONVERT_TO_SUBCLASS_CODE(.*)$',
|
|
CONTEXT.current_line)
|
|
if match:
|
|
CONTEXT.current_line = f"%ConvertToSubClassCode{match.group(1)}"
|
|
# Do not continue here, let the code process the next steps
|
|
|
|
match = re.match(r'^\s*SIP_VIRTUAL_CATCHER_CODE(.*)$',
|
|
CONTEXT.current_line)
|
|
if match:
|
|
CONTEXT.current_line = f"%VirtualCatcherCode{match.group(1)}"
|
|
# Do not continue here, let the code process the next steps
|
|
|
|
match = re.match(r'^\s*SIP_END(.*)$', CONTEXT.current_line)
|
|
if match:
|
|
write_output("SEN", f"%End{match.group(1)}\n")
|
|
continue
|
|
|
|
match = re.search(r'SIP_WHEN_FEATURE\(\s*(.*?)\s*\)', CONTEXT.current_line)
|
|
if match:
|
|
dbg_info('found SIP_WHEN_FEATURE')
|
|
CONTEXT.if_feature_condition = match.group(1)
|
|
|
|
if CONTEXT.is_qt6:
|
|
CONTEXT.current_line = re.sub(r'int\s*__len__\s*\(\s*\)',
|
|
'Py_ssize_t __len__()',
|
|
CONTEXT.current_line)
|
|
CONTEXT.current_line = re.sub(r'long\s*__hash__\s*\(\s*\)',
|
|
'Py_hash_t __hash__()',
|
|
CONTEXT.current_line)
|
|
|
|
if CONTEXT.is_qt6 and re.match(r'^\s*#ifdef SIP_PYQT5_RUN',
|
|
CONTEXT.current_line):
|
|
dbg_info("do not process PYQT5 code")
|
|
while not re.match(r'^#endif', CONTEXT.current_line):
|
|
CONTEXT.current_line = read_line()
|
|
|
|
if not CONTEXT.is_qt6 and re.match(r'^\s*#ifdef SIP_PYQT6_RUN',
|
|
CONTEXT.current_line):
|
|
dbg_info("do not process PYQT6 code")
|
|
while not re.match(r'^#endif', CONTEXT.current_line):
|
|
CONTEXT.current_line = read_line()
|
|
|
|
# Do not process SIP code %XXXCode
|
|
if CONTEXT.sip_run and re.match(
|
|
r'^ *% *(VirtualErrorHandler|MappedType|Type(?:Header)?Code|Module(?:Header)?Code|Convert(?:From|To)(?:Type|SubClass)Code|MethodCode|Docstring)(.*)?$',
|
|
CONTEXT.current_line):
|
|
CONTEXT.current_line = f"%{re.match(r'^ *% *(.*)$', CONTEXT.current_line).group(1)}"
|
|
CONTEXT.comment = ''
|
|
dbg_info("do not process SIP code")
|
|
while not re.match(r'^ *% *End', CONTEXT.current_line):
|
|
write_output("COD", CONTEXT.current_line + "\n")
|
|
CONTEXT.current_line = read_line()
|
|
if CONTEXT.is_qt6:
|
|
CONTEXT.current_line = re.sub(r'SIP_SSIZE_T', 'Py_ssize_t',
|
|
CONTEXT.current_line)
|
|
CONTEXT.current_line = re.sub(r'SIPLong_AsLong',
|
|
'PyLong_AsLong',
|
|
CONTEXT.current_line)
|
|
CONTEXT.current_line = re.sub(
|
|
r'^ *% *(VirtualErrorHandler|MappedType|Type(?:Header)?Code|Module(?:Header)?Code|Convert(?:From|To)(?:Type|SubClass)Code|MethodCode|Docstring)(.*)?$',
|
|
r'%\1\2', CONTEXT.current_line)
|
|
CONTEXT.current_line = re.sub(r'^\s*SIP_END(.*)$', r'%End\1',
|
|
CONTEXT.current_line)
|
|
|
|
CONTEXT.current_line = re.sub(r'^\s*% End', '%End',
|
|
CONTEXT.current_line)
|
|
write_output("COD", CONTEXT.current_line + "\n")
|
|
continue
|
|
|
|
# Do not process SIP code %Property
|
|
if CONTEXT.sip_run and re.match(r'^ *% *(Property)(.*)?$',
|
|
CONTEXT.current_line):
|
|
CONTEXT.current_line = f"%{re.match(r'^ *% *(.*)$', CONTEXT.current_line).group(1)}"
|
|
CONTEXT.comment = ''
|
|
write_output("COD", CONTEXT.current_line + "\n")
|
|
continue
|
|
|
|
# Do not process SIP code %If %End
|
|
if CONTEXT.sip_run and re.match(r'^ *% (If|End)(.*)?$',
|
|
CONTEXT.current_line):
|
|
CONTEXT.current_line = f"%{re.match(r'^ *% (.*)$', CONTEXT.current_line).group(1)}"
|
|
CONTEXT.comment = ''
|
|
write_output("COD", CONTEXT.current_line)
|
|
continue
|
|
|
|
# Skip preprocessor directives
|
|
if re.match(r'^\s*#', CONTEXT.current_line):
|
|
# Skip #if 0 or #if defined(Q_OS_WIN) blocks
|
|
match = re.match(r'^\s*#if (0|defined\(Q_OS_WIN\))',
|
|
CONTEXT.current_line)
|
|
if match:
|
|
dbg_info(f"skipping #if {match.group(1)} block")
|
|
nesting_index = 0
|
|
while CONTEXT.line_idx < CONTEXT.line_count:
|
|
CONTEXT.current_line = read_line()
|
|
if re.match(r'^\s*#if(def)?\s+', CONTEXT.current_line):
|
|
nesting_index += 1
|
|
elif nesting_index == 0 and re.match(r'^\s*#(endif|else)',
|
|
CONTEXT.current_line):
|
|
CONTEXT.comment = ''
|
|
break
|
|
elif nesting_index != 0 and re.match(r'^\s*#endif',
|
|
CONTEXT.current_line):
|
|
nesting_index -= 1
|
|
continue
|
|
|
|
if re.match(r'^\s*#ifdef SIP_RUN', CONTEXT.current_line):
|
|
CONTEXT.sip_run = True
|
|
if CONTEXT.access[-1] == Visibility.Private:
|
|
dbg_info("writing private content (1)")
|
|
if CONTEXT.private_section_line:
|
|
write_output("PRV1", CONTEXT.private_section_line + "\n")
|
|
CONTEXT.private_section_line = ''
|
|
continue
|
|
|
|
if CONTEXT.sip_run:
|
|
if re.match(r'^\s*#endif', CONTEXT.current_line):
|
|
if CONTEXT.ifdef_nesting_idx == 0:
|
|
CONTEXT.sip_run = False
|
|
continue
|
|
else:
|
|
CONTEXT.ifdef_nesting_idx -= 1
|
|
|
|
if re.match(r'^\s*#if(def)?\s+', CONTEXT.current_line):
|
|
CONTEXT.ifdef_nesting_idx += 1
|
|
|
|
# If there is an else at this level, code will be ignored (i.e., not SIP_RUN)
|
|
if re.match(r'^\s*#else',
|
|
CONTEXT.current_line) and CONTEXT.ifdef_nesting_idx == 0:
|
|
while CONTEXT.line_idx < CONTEXT.line_count:
|
|
CONTEXT.current_line = read_line()
|
|
if re.match(r'^\s*#if(def)?\s+', CONTEXT.current_line):
|
|
CONTEXT.ifdef_nesting_idx += 1
|
|
elif re.match(r'^\s*#endif', CONTEXT.current_line):
|
|
if CONTEXT.ifdef_nesting_idx == 0:
|
|
CONTEXT.comment = ''
|
|
CONTEXT.sip_run = False
|
|
break
|
|
else:
|
|
CONTEXT.ifdef_nesting_idx -= 1
|
|
continue
|
|
|
|
elif re.match(r'^\s*#ifndef SIP_RUN', CONTEXT.current_line):
|
|
# Code is ignored here
|
|
while CONTEXT.line_idx < CONTEXT.line_count:
|
|
CONTEXT.current_line = read_line()
|
|
if re.match(r'^\s*#if(def)?\s+', CONTEXT.current_line):
|
|
CONTEXT.ifdef_nesting_idx += 1
|
|
elif re.match(r'^\s*#else',
|
|
CONTEXT.current_line) and CONTEXT.ifdef_nesting_idx == 0:
|
|
# Code here will be printed out
|
|
if CONTEXT.access[-1] == Visibility.Private:
|
|
dbg_info("writing private content (2)")
|
|
if CONTEXT.private_section_line != '':
|
|
write_output("PRV2",
|
|
CONTEXT.private_section_line + "\n")
|
|
CONTEXT.private_section_line = ''
|
|
CONTEXT.sip_run = True
|
|
break
|
|
elif re.match(r'^\s*#endif', CONTEXT.current_line):
|
|
if CONTEXT.ifdef_nesting_idx == 0:
|
|
CONTEXT.sip_run = 0
|
|
break
|
|
else:
|
|
CONTEXT.ifdef_nesting_idx -= 1
|
|
continue
|
|
|
|
else:
|
|
continue
|
|
|
|
# TYPE HEADER CODE
|
|
if CONTEXT.header_code and not CONTEXT.sip_run:
|
|
CONTEXT.header_code = False
|
|
write_output("HCE", "%End\n")
|
|
|
|
# Skip forward declarations
|
|
match = re.match(
|
|
r'^\s*(template ?<class T> |enum\s+)?(class|struct) \w+(?P<external> *SIP_EXTERNAL)?;\s*(//.*)?$',
|
|
CONTEXT.current_line)
|
|
if match:
|
|
if match.group('external'):
|
|
dbg_info('do not skip external forward declaration')
|
|
CONTEXT.comment = ''
|
|
else:
|
|
dbg_info('skipping forward declaration')
|
|
continue
|
|
|
|
# Skip friend declarations
|
|
if re.match(r'^\s*friend class \w+', CONTEXT.current_line):
|
|
continue
|
|
|
|
# Insert metaobject for Q_GADGET
|
|
if re.match(r'^\s*Q_GADGET\b.*?$', CONTEXT.current_line):
|
|
if not re.search(r'SIP_SKIP', CONTEXT.current_line):
|
|
dbg_info('Q_GADGET')
|
|
write_output("HCE", " public:\n")
|
|
write_output("HCE",
|
|
" static const QMetaObject staticMetaObject;\n\n")
|
|
continue
|
|
|
|
# Insert in Python output (python/module/__init__.py)
|
|
match = re.search(r'Q_(ENUM|FLAG)\(\s*(\w+)\s*\)', CONTEXT.current_line)
|
|
if match:
|
|
if not re.search(r'SIP_SKIP', CONTEXT.current_line):
|
|
is_flag = 1 if match.group(1) == 'FLAG' else 0
|
|
enum_helper = f"{CONTEXT.actual_class}.{match.group(2)}.baseClass = {CONTEXT.actual_class}"
|
|
dbg_info(f"Q_ENUM/Q_FLAG {enum_helper}")
|
|
if args.python_output:
|
|
if enum_helper != '':
|
|
CONTEXT.output_python.append(f"{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...
|
|
CONTEXT.output_python.append(
|
|
f"{match.group(2)} = {CONTEXT.actual_class} # dirty hack since SIP seems to introduce the flags in module\n")
|
|
continue
|
|
|
|
# Skip Q_OBJECT, Q_PROPERTY, Q_ENUM, etc.
|
|
if re.match(
|
|
r'^\s*Q_(OBJECT|ENUMS|ENUM|FLAG|PROPERTY|DECLARE_METATYPE|DECLARE_TYPEINFO|NOWARN_DEPRECATED_(PUSH|POP))\b.*?$',
|
|
CONTEXT.current_line):
|
|
continue
|
|
|
|
if re.match(r'^\s*QHASH_FOR_CLASS_ENUM', CONTEXT.current_line):
|
|
continue
|
|
|
|
if re.search(r'SIP_SKIP|SIP_PYTHON_SPECIAL_', CONTEXT.current_line):
|
|
dbg_info('SIP SKIP!')
|
|
# if multiline definition, remove previous lines
|
|
if CONTEXT.multiline_definition != MultiLineType.NotMultiline:
|
|
dbg_info('SIP_SKIP with MultiLine')
|
|
opening_line = ''
|
|
while not re.match(r'^[^()]*\(([^()]*\([^()]*\)[^()]*)*[^()]*$',
|
|
opening_line):
|
|
opening_line = CONTEXT.output.pop()
|
|
if len(CONTEXT.output) < 1:
|
|
exit_with_error('could not reach opening definition')
|
|
dbg_info("removed multiline definition of SIP_SKIP method")
|
|
CONTEXT.multiline_definition = MultiLineType.NotMultiline
|
|
del CONTEXT.static_methods[CONTEXT.current_fully_qualified_class_name()][CONTEXT.current_method_name]
|
|
|
|
# also skip method body if there is one
|
|
detect_and_remove_following_body_or_initializerlist()
|
|
|
|
# line skipped, go to next iteration
|
|
match = re.search(r'SIP_PYTHON_SPECIAL_(\w+)\(\s*(".*"|\w+)\s*\)',
|
|
CONTEXT.current_line)
|
|
if match:
|
|
method_or_code = match.group(2)
|
|
dbg_info(f"PYTHON SPECIAL method or code: {method_or_code}")
|
|
pyop = f"{CONTEXT.actual_class}.__{match.group(1).lower()}__ = lambda self: "
|
|
if re.match(r'^".*"$', method_or_code):
|
|
pyop += method_or_code.strip('"')
|
|
else:
|
|
pyop += f"self.{method_or_code}()"
|
|
dbg_info(f"PYTHON SPECIAL {pyop}")
|
|
if args.python_output:
|
|
CONTEXT.output_python.append(f"{pyop}\n")
|
|
|
|
CONTEXT.comment = ''
|
|
continue
|
|
|
|
# Detect comment block
|
|
if detect_comment_block():
|
|
continue
|
|
|
|
struct_match = re.match(
|
|
r'^\s*struct(\s+\w+_EXPORT)?\s+(?P<structname>\w+)$',
|
|
CONTEXT.current_line)
|
|
if struct_match:
|
|
dbg_info(" going to struct => public")
|
|
CONTEXT.class_and_struct.append(struct_match.group('structname'))
|
|
CONTEXT.classname.append(
|
|
CONTEXT.classname[-1] if CONTEXT.classname else struct_match.group(
|
|
'structname')) # fake new class since struct has considered similarly
|
|
if CONTEXT.access[-1] != Visibility.Private:
|
|
CONTEXT.all_fully_qualified_class_names.append(CONTEXT.current_fully_qualified_struct_name())
|
|
CONTEXT.access.append(Visibility.Public)
|
|
CONTEXT.exported.append(CONTEXT.exported[-1])
|
|
CONTEXT.bracket_nesting_idx.append(0)
|
|
|
|
# class declaration started
|
|
# https://regex101.com/r/KMQdF5/1 (older versions: https://regex101.com/r/6FWntP/16)
|
|
class_pattern = re.compile(
|
|
r"""^(\s*(class))\s+([A-Z0-9_]+_EXPORT\s+)?(Q_DECL_DEPRECATED\s+)?(?P<classname>\w+)(?P<domain>\s*:\s*(public|protected|private)\s+\w+(< *(\w|::)+ *(, *(\w|::)+ *)*>)?(::\w+(<(\w|::)+(, *(\w|::)+)*>)?)*(,\s*(public|protected|private)\s+\w+(< *(\w|::)+ *(, *(\w|::)+)*>)?(::\w+(<\w+(, *(\w|::)+)?>)?)*)*)?(?P<annot>\s*/?/?\s*SIP_\w+)?\s*?(//.*|(?!;))$"""
|
|
)
|
|
class_pattern_match = class_pattern.match(CONTEXT.current_line)
|
|
|
|
if class_pattern_match:
|
|
dbg_info("class definition started")
|
|
CONTEXT.exported.append(0)
|
|
CONTEXT.bracket_nesting_idx.append(0)
|
|
template_inheritance_template = []
|
|
template_inheritance_class1 = []
|
|
template_inheritance_class2 = []
|
|
template_inheritance_class3 = []
|
|
|
|
CONTEXT.classname.append(class_pattern_match.group('classname'))
|
|
CONTEXT.class_and_struct.append(class_pattern_match.group('classname'))
|
|
if CONTEXT.access[-1] != Visibility.Private:
|
|
CONTEXT.all_fully_qualified_class_names.append(CONTEXT.current_fully_qualified_struct_name())
|
|
CONTEXT.access.append(Visibility.Public)
|
|
|
|
if len(CONTEXT.classname) == 1:
|
|
CONTEXT.declared_classes.append(CONTEXT.classname[-1])
|
|
|
|
dbg_info(f"class: {CONTEXT.classname[-1]}")
|
|
|
|
if (
|
|
re.search(r'\b[A-Z0-9_]+_EXPORT\b', CONTEXT.current_line)
|
|
or len(CONTEXT.classname) != 1
|
|
or re.search(r'^\s*template\s*<',
|
|
CONTEXT.input_lines[CONTEXT.line_idx - 2])
|
|
):
|
|
CONTEXT.exported[-1] += 1
|
|
|
|
CONTEXT.current_line = f"{class_pattern_match.group(1)} {class_pattern_match.group('classname')}"
|
|
|
|
# append to class map file
|
|
if args.class_map:
|
|
with open(args.class_map, 'a') as fh3:
|
|
fh3.write(
|
|
f"{'.'.join(CONTEXT.classname)}: {CONTEXT.header_file}#L{CONTEXT.line_idx}\n")
|
|
|
|
# Inheritance
|
|
if class_pattern_match.group('domain'):
|
|
m = class_pattern_match.group('domain')
|
|
m = re.sub(r'public +(\w+, *)*(Ui::\w+,? *)+', '', m)
|
|
m = re.sub(r'public +', '', m)
|
|
m = re.sub(r'[,:]?\s*private +\w+(::\w+)?', '', m)
|
|
|
|
# detect template based inheritance
|
|
# https://regex101.com/r/9LGhyy/1
|
|
tpl_pattern = re.compile(
|
|
r'[,:]\s+(?P<tpl>(?!QList)\w+)< *(?P<cls1>(\w|::)+) *(, *(?P<cls2>(\w|::)+)? *(, *(?P<cls3>(\w|::)+)? *)?)? *>'
|
|
)
|
|
|
|
for match in tpl_pattern.finditer(m):
|
|
dbg_info("template class")
|
|
template_inheritance_template.append(match.group('tpl'))
|
|
template_inheritance_class1.append(match.group('cls1'))
|
|
template_inheritance_class2.append(match.group('cls2') or "")
|
|
template_inheritance_class3.append(match.group('cls3') or "")
|
|
|
|
dbg_info(f"domain: {m}")
|
|
|
|
tpl_replace_pattern = re.compile(
|
|
r'\b(?P<tpl>(?!QList)\w+)< *(?P<cls1>(\w|::)+) *(, *(?P<cls2>(\w|::)+)? *(, *(?P<cls3>(\w|::)+)? *)?)? *>'
|
|
)
|
|
m = tpl_replace_pattern.sub(lambda
|
|
tpl_match: f"{tpl_match.group('tpl') or ''}{tpl_match.group('cls1') or ''}{tpl_match.group('cls2') or ''}{tpl_match.group('cls3') or ''}Base",
|
|
m)
|
|
m = re.sub(r'(\w+)< *(?:\w|::)+ *>', '', m)
|
|
m = re.sub(r'([:,])\s*,', r'\1', m)
|
|
m = re.sub(r'(\s*[:,])?\s*$', '', m)
|
|
CONTEXT.current_line += m
|
|
|
|
if class_pattern_match.group('annot'):
|
|
CONTEXT.current_line += class_pattern_match.group('annot')
|
|
CONTEXT.current_line = fix_annotations(CONTEXT.current_line)
|
|
|
|
CONTEXT.current_line += "\n{\n"
|
|
if CONTEXT.comment.strip():
|
|
CONTEXT.current_line += "%Docstring(signature=\"appended\")\n" + CONTEXT.comment + "\n%End\n"
|
|
|
|
CONTEXT.current_line += f"\n%TypeHeaderCode\n#include \"{os.path.basename(CONTEXT.header_file)}\""
|
|
|
|
# for template based inheritance, add a typedef to define the base type
|
|
while template_inheritance_template:
|
|
tpl = template_inheritance_template.pop()
|
|
cls1 = template_inheritance_class1.pop()
|
|
cls2 = template_inheritance_class2.pop()
|
|
cls3 = template_inheritance_class3.pop()
|
|
|
|
if cls2 == "":
|
|
CONTEXT.current_line = f"\ntypedef {tpl}<{cls1}> {tpl}{cls1}Base;\n\n{CONTEXT.current_line}"
|
|
elif cls3 == "":
|
|
CONTEXT.current_line = f"\ntypedef {tpl}<{cls1},{cls2}> {tpl}{cls1}{cls2}Base;\n\n{CONTEXT.current_line}"
|
|
else:
|
|
CONTEXT.current_line = f"\ntypedef {tpl}<{cls1},{cls2},{cls3}> {tpl}{cls1}{cls2}{cls3}Base;\n\n{CONTEXT.current_line}"
|
|
|
|
if tpl not in CONTEXT.declared_classes:
|
|
tpl_header = f"{tpl.lower()}.h"
|
|
if tpl in sip_config['class_headerfile']:
|
|
tpl_header = sip_config['class_headerfile'][tpl]
|
|
CONTEXT.current_line += f"\n#include \"{tpl_header}\""
|
|
|
|
if cls2 == "":
|
|
CONTEXT.current_line += f"\ntypedef {tpl}<{cls1}> {tpl}{cls1}Base;"
|
|
elif cls3 == "":
|
|
CONTEXT.current_line += f"\ntypedef {tpl}<{cls1},{cls2}> {tpl}{cls1}{cls2}Base;"
|
|
else:
|
|
CONTEXT.current_line += f"\ntypedef {tpl}<{cls1},{cls2},{cls3}> {tpl}{cls1}{cls2}{cls3}Base;"
|
|
|
|
if any(x == Visibility.Private for x in CONTEXT.access) and len(
|
|
CONTEXT.access) != 1:
|
|
dbg_info("skipping class in private context")
|
|
continue
|
|
|
|
CONTEXT.access[-1] = Visibility.Private # private by default
|
|
write_output("CLS", f"{CONTEXT.current_line}\n")
|
|
|
|
# Skip opening curly bracket, incrementing hereunder
|
|
skip = read_line()
|
|
if not re.match(r'^\s*{\s*$', skip):
|
|
exit_with_error("expecting { after class definition")
|
|
CONTEXT.bracket_nesting_idx[-1] += 1
|
|
|
|
CONTEXT.comment = ''
|
|
CONTEXT.header_code = True
|
|
CONTEXT.access[-1] = Visibility.Private
|
|
continue
|
|
|
|
# Bracket balance in class/struct tree
|
|
if not CONTEXT.sip_run:
|
|
bracket_balance = 0
|
|
bracket_balance += CONTEXT.current_line.count('{')
|
|
bracket_balance -= CONTEXT.current_line.count('}')
|
|
|
|
if bracket_balance != 0:
|
|
CONTEXT.bracket_nesting_idx[-1] += bracket_balance
|
|
|
|
if CONTEXT.bracket_nesting_idx[-1] == 0:
|
|
dbg_info("going up in class/struct tree")
|
|
|
|
if len(CONTEXT.access) > 1:
|
|
CONTEXT.bracket_nesting_idx.pop()
|
|
CONTEXT.access.pop()
|
|
|
|
if CONTEXT.exported[-1] == 0 and CONTEXT.classname[
|
|
-1] != sip_config.get(
|
|
'no_export_macro'):
|
|
exit_with_error(
|
|
f"Class {CONTEXT.classname[-1]} should be exported with appropriate [LIB]_EXPORT macro. "
|
|
f"If this should not be available in python, wrap it in a `#ifndef SIP_RUN` block."
|
|
)
|
|
CONTEXT.exported.pop()
|
|
|
|
if CONTEXT.classname:
|
|
CONTEXT.classname.pop()
|
|
CONTEXT.class_and_struct.pop()
|
|
|
|
if len(CONTEXT.access) == 1:
|
|
dbg_info("reached top level")
|
|
CONTEXT.access[
|
|
-1] = Visibility.Public # Top level should stay public
|
|
|
|
CONTEXT.comment = ''
|
|
CONTEXT.return_type = ''
|
|
CONTEXT.private_section_line = ''
|
|
|
|
dbg_info(f"new bracket balance: {CONTEXT.bracket_nesting_idx}")
|
|
|
|
# Private members (exclude SIP_RUN)
|
|
if re.match(r'^\s*private( slots)?:', CONTEXT.current_line):
|
|
CONTEXT.access[-1] = Visibility.Private
|
|
CONTEXT.last_access_section_line = CONTEXT.current_line
|
|
CONTEXT.private_section_line = CONTEXT.current_line
|
|
CONTEXT.comment = ''
|
|
dbg_info("going private")
|
|
continue
|
|
|
|
elif re.match(r'^\s*(public( slots)?):.*$', CONTEXT.current_line):
|
|
dbg_info("going public")
|
|
CONTEXT.last_access_section_line = CONTEXT.current_line
|
|
CONTEXT.access[-1] = Visibility.Public
|
|
CONTEXT.comment = ''
|
|
|
|
elif re.match(r'^\s*signals:.*$', CONTEXT.current_line):
|
|
dbg_info("going public for signals")
|
|
CONTEXT.last_access_section_line = CONTEXT.current_line
|
|
CONTEXT.access[-1] = Visibility.Signals
|
|
CONTEXT.comment = ''
|
|
|
|
elif re.match(r'^\s*(protected)( slots)?:.*$', CONTEXT.current_line):
|
|
dbg_info("going protected")
|
|
CONTEXT.last_access_section_line = CONTEXT.current_line
|
|
CONTEXT.access[-1] = Visibility.Protected
|
|
CONTEXT.comment = ''
|
|
|
|
elif CONTEXT.access[
|
|
-1] == Visibility.Private and 'SIP_FORCE' in CONTEXT.current_line:
|
|
dbg_info("private with SIP_FORCE")
|
|
if CONTEXT.private_section_line:
|
|
write_output("PRV3", CONTEXT.private_section_line + "\n")
|
|
CONTEXT.private_section_line = ''
|
|
|
|
elif any(x == Visibility.Private for x in
|
|
CONTEXT.access) and not CONTEXT.sip_run:
|
|
CONTEXT.comment = ''
|
|
continue
|
|
|
|
# Skip operators
|
|
if CONTEXT.access[-1] != Visibility.Private and re.search(
|
|
r'operator(=|<<|>>|->)\s*\(', CONTEXT.current_line):
|
|
dbg_info("skip operator")
|
|
detect_and_remove_following_body_or_initializerlist()
|
|
continue
|
|
|
|
# Save comments and do not print them, except in SIP_RUN
|
|
if not CONTEXT.sip_run:
|
|
if re.match(r'^\s*//', CONTEXT.current_line):
|
|
match = re.match(r'^\s*//!\s*(.*?)\n?$', CONTEXT.current_line)
|
|
if match:
|
|
CONTEXT.comment_param_list = False
|
|
CONTEXT.prev_indent = CONTEXT.indent
|
|
CONTEXT.indent = ''
|
|
CONTEXT.comment_last_line_note_warning = False
|
|
CONTEXT.comment = process_doxygen_line(match.group(1))
|
|
CONTEXT.comment = CONTEXT.comment.rstrip()
|
|
elif not re.search(r'\*/',
|
|
CONTEXT.input_lines[CONTEXT.line_idx - 1]):
|
|
CONTEXT.comment = ''
|
|
continue
|
|
|
|
# Handle Q_DECLARE_FLAGS in Qt6
|
|
if CONTEXT.is_qt6 and re.match(
|
|
r'^\s*Q_DECLARE_FLAGS\s*\(\s*(\w+)\s*,\s*(\w+)\s*\)',
|
|
CONTEXT.current_line):
|
|
flags_name = re.search(r'\(\s*(\w+)\s*,\s*(\w+)\s*\)',
|
|
CONTEXT.current_line).group(1)
|
|
flag_name = re.search(r'\(\s*(\w+)\s*,\s*(\w+)\s*\)',
|
|
CONTEXT.current_line).group(2)
|
|
CONTEXT.output_python.append(
|
|
f"{CONTEXT.actual_class}.{flags_name} = lambda flags=0: {CONTEXT.actual_class}.{flag_name}(flags)\n")
|
|
|
|
# Enum declaration
|
|
# For scoped and type-based enum, the type has to be removed
|
|
if re.match(
|
|
r'^\s*Q_DECLARE_FLAGS\s*\(\s*(\w+)\s*,\s*(\w+)\s*\)\s*SIP_MONKEYPATCH_FLAGS_UNNEST\s*\(\s*(\w+)\s*,\s*(\w+)\s*\)\s*$',
|
|
CONTEXT.current_line):
|
|
flags_name = re.search(r'\(\s*(\w+)\s*,\s*(\w+)\s*\)',
|
|
CONTEXT.current_line).group(1)
|
|
flag_name = re.search(r'\(\s*(\w+)\s*,\s*(\w+)\s*\)',
|
|
CONTEXT.current_line).group(2)
|
|
emkb = re.search(
|
|
r'SIP_MONKEYPATCH_FLAGS_UNNEST\s*\(\s*(\w+)\s*,\s*(\w+)\s*\)',
|
|
CONTEXT.current_line).group(1)
|
|
emkf = re.search(
|
|
r'SIP_MONKEYPATCH_FLAGS_UNNEST\s*\(\s*(\w+)\s*,\s*(\w+)\s*\)',
|
|
CONTEXT.current_line).group(2)
|
|
|
|
if f"{emkb}.{emkf}" != f"{CONTEXT.actual_class}.{flags_name}":
|
|
CONTEXT.output_python.append(
|
|
f"{emkb}.{emkf} = {CONTEXT.actual_class}.{flags_name}\n")
|
|
|
|
CONTEXT.enum_monkey_patched_types.append(
|
|
[CONTEXT.actual_class, flags_name, emkb, emkf])
|
|
|
|
CONTEXT.current_line = re.sub(
|
|
r'\s*SIP_MONKEYPATCH_FLAGS_UNNEST\(.*?\)', '',
|
|
CONTEXT.current_line)
|
|
|
|
enum_match = re.match(
|
|
r'^(\s*enum(\s+Q_DECL_DEPRECATED)?\s+(?P<isclass>class\s+)?(?P<enum_qualname>\w+))(:?\s+SIP_[^:]*)?(\s*:\s*(?P<enum_type>\w+))?(?:\s*SIP_ENUM_BASETYPE\s*\(\s*(?P<py_enum_type>\w+)\s*\))?(?P<oneliner>.*)$',
|
|
CONTEXT.current_line)
|
|
if enum_match:
|
|
enum_decl = enum_match.group(1)
|
|
enum_qualname = enum_match.group('enum_qualname')
|
|
enum_type = enum_match.group('enum_type')
|
|
isclass = enum_match.group('isclass')
|
|
enum_cpp_name = f"{CONTEXT.actual_class}::{enum_qualname}" if CONTEXT.actual_class else enum_qualname
|
|
|
|
if not isclass and enum_cpp_name not in ALLOWED_NON_CLASS_ENUMS:
|
|
exit_with_error(
|
|
f"Non class enum exposed to Python -- must be a enum class: {enum_cpp_name}")
|
|
|
|
oneliner = enum_match.group('oneliner')
|
|
is_scope_based = bool(isclass)
|
|
enum_decl = re.sub(r'\s*\bQ_DECL_DEPRECATED\b', '', enum_decl)
|
|
|
|
py_enum_type_match = re.search(r'SIP_ENUM_BASETYPE\(\s*(.*?)\s*\)',
|
|
CONTEXT.current_line)
|
|
py_enum_type = py_enum_type_match.group(
|
|
1) if py_enum_type_match else None
|
|
|
|
if py_enum_type == "IntFlag":
|
|
CONTEXT.enum_intflag_types.append(enum_cpp_name)
|
|
|
|
if enum_type in ["int", "quint32"]:
|
|
CONTEXT.enum_int_types.append(
|
|
f"{CONTEXT.actual_class}.{enum_qualname}")
|
|
if CONTEXT.is_qt6:
|
|
enum_decl += f" /BaseType={py_enum_type or 'IntEnum'}/"
|
|
elif enum_type:
|
|
exit_with_error(
|
|
f"Unhandled enum type {enum_type} for {enum_cpp_name}")
|
|
elif isclass:
|
|
CONTEXT.enum_class_non_int_types.append(
|
|
f"{CONTEXT.actual_class}.{enum_qualname}")
|
|
elif CONTEXT.is_qt6:
|
|
enum_decl += " /BaseType=IntEnum/"
|
|
|
|
write_output("ENU1", enum_decl)
|
|
if oneliner:
|
|
write_output("ENU1", oneliner)
|
|
write_output("ENU1", "\n")
|
|
|
|
_match = None
|
|
if is_scope_based:
|
|
_match = re.search(
|
|
r'SIP_MONKEYPATCH_SCOPEENUM(_UNNEST)?(:?\(\s*(?P<emkb>\w+)\s*,\s*(?P<emkf>\w+)\s*\))?',
|
|
CONTEXT.current_line)
|
|
monkeypatch = is_scope_based and _match
|
|
enum_mk_base = _match.group('emkb') if _match else ''
|
|
|
|
enum_old_name = ''
|
|
if _match and _match.group('emkf') and monkeypatch:
|
|
enum_old_name = _match.group('emkf')
|
|
if CONTEXT.actual_class:
|
|
if f"{enum_mk_base}.{enum_old_name}" != f"{CONTEXT.actual_class}.{enum_qualname}":
|
|
CONTEXT.output_python.append(
|
|
f"{enum_mk_base}.{enum_old_name} = {CONTEXT.actual_class}.{enum_qualname}\n")
|
|
else:
|
|
CONTEXT.output_python.append(
|
|
f"{enum_mk_base}.{enum_old_name} = {enum_qualname}\n")
|
|
|
|
if re.search(r'\{((\s*\w+)(\s*=\s*[\w\s<|]+.*?)?(,?))+\s*}',
|
|
CONTEXT.current_line):
|
|
if '=' in CONTEXT.current_line:
|
|
exit_with_error(
|
|
"Sipify does not handle enum one liners with value assignment. Use multiple lines instead. Or just write a new parser.")
|
|
continue
|
|
else:
|
|
CONTEXT.current_line = read_line()
|
|
if not re.match(r'^\s*\{\s*$', CONTEXT.current_line):
|
|
exit_with_error(
|
|
'Unexpected content: enum should be followed by {')
|
|
write_output("ENU2", f"{CONTEXT.current_line}\n")
|
|
|
|
if is_scope_based:
|
|
CONTEXT.output_python.append(
|
|
"# monkey patching scoped based enum\n")
|
|
|
|
enum_members_doc = []
|
|
|
|
while CONTEXT.line_idx < CONTEXT.line_count:
|
|
CONTEXT.current_line = read_line()
|
|
if detect_comment_block():
|
|
continue
|
|
if re.search(r'};', CONTEXT.current_line):
|
|
break
|
|
if re.match(r'^\s*\w+\s*\|',
|
|
CONTEXT.current_line): # multi line declaration as sum of enums
|
|
continue
|
|
|
|
enum_match = re.match(
|
|
r'^(\s*(?P<em>\w+))(\s+SIP_PYNAME(?:\(\s*(?P<pyname>[^() ]+)\s*\)\s*)?)?(\s+SIP_MONKEY\w+(?:\(\s*(?P<compat>[^() ]+)\s*\)\s*)?)?(?:\s*=\s*(?P<enum_value>(:?[\w\s|+-]|::|<<)+))?(?P<optional_comma>,?)(:?\s*//!<\s*(?P<co>.*)|.*)$',
|
|
CONTEXT.current_line)
|
|
|
|
enum_decl = f"{enum_match.group(1) or ''}{enum_match.group(3) or ''}{enum_match.group('optional_comma') or ''}" if enum_match else CONTEXT.current_line
|
|
enum_member = enum_match.group(
|
|
'em') or '' if enum_match else ''
|
|
value_comment = enum_match.group(
|
|
'co') or '' if enum_match else ''
|
|
compat_name = enum_match.group(
|
|
'compat') or enum_member if enum_match else ''
|
|
enum_value = enum_match.group(
|
|
'enum_value') or '' if enum_match else ''
|
|
|
|
value_comment = value_comment.replace('::', '.').replace('"',
|
|
'\\"')
|
|
value_comment = re.sub(r'\\since .*?([\d.]+)',
|
|
r'\\n.. versionadded:: \1\\n',
|
|
value_comment, flags=re.I)
|
|
value_comment = re.sub(r'\\deprecated (?:QGIS )?(.*)',
|
|
r'\\n.. deprecated:: \1\\n',
|
|
value_comment, flags=re.I)
|
|
value_comment = re.sub(r'^\\n+', '', value_comment)
|
|
value_comment = re.sub(r'\\n+$', '', value_comment)
|
|
|
|
dbg_info(
|
|
f"is_scope_based:{is_scope_based} enum_mk_base:{enum_mk_base} monkeypatch:{monkeypatch}")
|
|
|
|
if enum_value and (
|
|
re.search(r'.*<<.*', enum_value) or re.search(r'.*0x0.*',
|
|
enum_value)):
|
|
if f"{CONTEXT.actual_class}::{enum_qualname}" not in CONTEXT.enum_intflag_types:
|
|
exit_with_error(
|
|
f"{CONTEXT.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 and enum_member:
|
|
value_comment_parts = value_comment.replace('\\n', '\n').split('\n')
|
|
value_comment_indented = ''
|
|
for part_idx, part in enumerate(value_comment_parts):
|
|
if part_idx == 0:
|
|
if part.strip().startswith('..'):
|
|
exit_with_error(f'Enum member description missing for {CONTEXT.actual_class}::{enum_qualname}')
|
|
value_comment_indented += part.rstrip()
|
|
else:
|
|
if part.startswith(".."):
|
|
value_comment_indented += "\n"
|
|
|
|
value_comment_indented += ' ' + part.rstrip()
|
|
if part.startswith(".."):
|
|
value_comment_indented += "\n"
|
|
|
|
if part_idx < len(value_comment_parts) - 1:
|
|
value_comment_indented += '\n'
|
|
|
|
if monkeypatch and enum_mk_base:
|
|
if compat_name != enum_member:
|
|
value_comment_indented += f'\n\n Available as ``{enum_mk_base}.{compat_name}`` in older QGIS releases.\n'
|
|
if CONTEXT.actual_class:
|
|
CONTEXT.output_python.append(
|
|
f"{enum_mk_base}.{compat_name} = {CONTEXT.actual_class}.{enum_qualname}.{enum_member}\n")
|
|
if enum_old_name and compat_name != enum_member:
|
|
CONTEXT.output_python.append(
|
|
f"{enum_mk_base}.{enum_old_name}.{compat_name} = {CONTEXT.actual_class}.{enum_qualname}.{enum_member}\n")
|
|
CONTEXT.output_python.append(
|
|
f"{enum_mk_base}.{compat_name}.is_monkey_patched = True\n")
|
|
CONTEXT.output_python.append(
|
|
f"{enum_mk_base}.{compat_name}.__doc__ = \"{value_comment}\"\n")
|
|
enum_members_doc.append(
|
|
f"* ``{enum_member}``: {value_comment_indented}")
|
|
else:
|
|
CONTEXT.output_python.append(
|
|
f"{enum_mk_base}.{compat_name} = {enum_qualname}.{enum_member}\n")
|
|
CONTEXT.output_python.append(
|
|
f"{enum_mk_base}.{compat_name}.is_monkey_patched = True\n")
|
|
CONTEXT.output_python.append(
|
|
f"{enum_mk_base}.{compat_name}.__doc__ = \"{value_comment}\"\n")
|
|
enum_members_doc.append(
|
|
f"* ``{enum_member}``: {value_comment_indented}")
|
|
else:
|
|
if compat_name != enum_member:
|
|
value_comment_indented += f'\n\n Available as ``{CONTEXT.actual_class}.{compat_name}`` in older QGIS releases.\n'
|
|
|
|
if monkeypatch:
|
|
CONTEXT.output_python.append(
|
|
f"{CONTEXT.actual_class}.{compat_name} = {CONTEXT.actual_class}.{enum_qualname}.{enum_member}\n")
|
|
CONTEXT.output_python.append(
|
|
f"{CONTEXT.actual_class}.{compat_name}.is_monkey_patched = True\n")
|
|
if CONTEXT.actual_class:
|
|
complete_class_path = '.'.join(CONTEXT.classname)
|
|
CONTEXT.output_python.append(
|
|
f"{complete_class_path}.{enum_qualname}.{compat_name}.__doc__ = \"{value_comment}\"\n")
|
|
enum_members_doc.append(
|
|
f"* ``{enum_member}``: {value_comment_indented}")
|
|
else:
|
|
CONTEXT.output_python.append(
|
|
f"{enum_qualname}.{compat_name}.__doc__ = \"{value_comment}\"\n")
|
|
enum_members_doc.append(
|
|
f"* ``{enum_member}``: {value_comment_indented}")
|
|
|
|
if not is_scope_based and CONTEXT.is_qt6 and enum_member:
|
|
basename = '.'.join(CONTEXT.class_and_struct)
|
|
if basename:
|
|
enum_member = 'None_' if enum_member == 'None' else enum_member
|
|
CONTEXT.output_python.append(
|
|
f"{basename}.{enum_member} = {basename}.{enum_qualname}.{enum_member}\n")
|
|
|
|
enum_decl = fix_annotations(enum_decl)
|
|
write_output("ENU3", f"{enum_decl}\n")
|
|
|
|
detect_comment_block(strict_mode=False)
|
|
|
|
write_output("ENU4", f"{CONTEXT.current_line}\n")
|
|
|
|
if is_scope_based:
|
|
enum_member_doc_string = "\n".join(enum_members_doc)
|
|
if CONTEXT.actual_class:
|
|
CONTEXT.output_python.append(
|
|
f'{CONTEXT.actual_class}.{enum_qualname}.__doc__ = """{CONTEXT.comment}\n\n{enum_member_doc_string}\n\n"""\n# --\n')
|
|
else:
|
|
CONTEXT.output_python.append(
|
|
f'{enum_qualname}.__doc__ = """{CONTEXT.comment}\n\n{enum_member_doc_string}\n\n"""\n# --\n')
|
|
|
|
# enums don't have Docstring apparently
|
|
CONTEXT.comment = ''
|
|
continue
|
|
|
|
# Check for invalid use of doxygen command
|
|
if re.search(r'.*//!<', CONTEXT.current_line):
|
|
exit_with_error(
|
|
'"\\!<" doxygen command must only be used for enum documentation')
|
|
|
|
# Handle override, final, and make private keywords
|
|
if re.search(r'\boverride\b', CONTEXT.current_line):
|
|
CONTEXT.is_override_or_make_private = PrependType.Virtual
|
|
if re.search(r'\bFINAL\b', CONTEXT.current_line):
|
|
CONTEXT.is_override_or_make_private = PrependType.Virtual
|
|
if re.search(r'\bSIP_MAKE_PRIVATE\b', CONTEXT.current_line):
|
|
CONTEXT.is_override_or_make_private = PrependType.MakePrivate
|
|
|
|
# Remove Q_INVOKABLE
|
|
CONTEXT.current_line = re.sub(r'^(\s*)Q_INVOKABLE ', r'\1',
|
|
CONTEXT.current_line)
|
|
|
|
# Keyword fixes
|
|
CONTEXT.current_line = re.sub(
|
|
r'^(\s*template\s*<)(?:class|typename) (\w+>)(.*)$',
|
|
r'\1\2\3', CONTEXT.current_line)
|
|
CONTEXT.current_line = re.sub(
|
|
r'^(\s*template\s*<)(?:class|typename) (\w+) *, *(?:class|typename) (\w+>)(.*)$',
|
|
r'\1\2,\3\4',
|
|
CONTEXT.current_line)
|
|
CONTEXT.current_line = re.sub(
|
|
r'^(\s*template\s*<)(?:class|typename) (\w+) *, *(?:class|typename) (\w+) *, *(?:class|typename) (\w+>)(.*)$',
|
|
r'\1\2,\3,\4\5', CONTEXT.current_line)
|
|
CONTEXT.current_line = re.sub(r'\s*\boverride\b', '', CONTEXT.current_line)
|
|
CONTEXT.current_line = re.sub(r'\s*\bSIP_MAKE_PRIVATE\b', '',
|
|
CONTEXT.current_line)
|
|
CONTEXT.current_line = re.sub(r'\s*\bFINAL\b', ' ${SIP_FINAL}',
|
|
CONTEXT.current_line)
|
|
CONTEXT.current_line = re.sub(r'\s*\bextern \b', '', CONTEXT.current_line)
|
|
CONTEXT.current_line = re.sub(r'\s*\bMAYBE_UNUSED \b', '',
|
|
CONTEXT.current_line)
|
|
CONTEXT.current_line = re.sub(r'\s*\bNODISCARD \b', '',
|
|
CONTEXT.current_line)
|
|
CONTEXT.current_line = re.sub(r'\s*\bQ_DECL_DEPRECATED\b', '',
|
|
CONTEXT.current_line)
|
|
CONTEXT.current_line = re.sub(r'^(\s*)?(const |virtual |static )*inline ',
|
|
r'\1\2', CONTEXT.current_line)
|
|
CONTEXT.current_line = re.sub(r'\bconstexpr\b', 'const',
|
|
CONTEXT.current_line)
|
|
CONTEXT.current_line = re.sub(r'\bnullptr\b', '0', CONTEXT.current_line)
|
|
CONTEXT.current_line = re.sub(r'\s*=\s*default\b', '',
|
|
CONTEXT.current_line)
|
|
|
|
# Handle export macros
|
|
if re.search(r'\b\w+_EXPORT\b', CONTEXT.current_line):
|
|
CONTEXT.exported[-1] += 1
|
|
CONTEXT.current_line = re.sub(r'\b\w+_EXPORT\s+', '',
|
|
CONTEXT.current_line)
|
|
|
|
# Skip non-method member declaration in non-public sections
|
|
if not CONTEXT.sip_run and CONTEXT.access[
|
|
-1] != Visibility.Public and detect_non_method_member(
|
|
CONTEXT.current_line):
|
|
dbg_info("skip non-method member declaration in non-public sections")
|
|
continue
|
|
|
|
# Remove static const value assignment
|
|
# https://regex101.com/r/DyWkgn/6
|
|
if re.search(r'^\s*const static \w+', CONTEXT.current_line):
|
|
exit_with_error(
|
|
f"const static should be written static const in {CONTEXT.classname[-1]}")
|
|
|
|
# TODO needs fixing!!
|
|
# original perl regex was:
|
|
# ^(?<staticconst> *(?<static>static )?const \w+(?:<(?:[\w<>, ]|::)+>)? \w+)(?: = [^()]+?(\((?:[^()]++|(?3))*\))?[^()]*?)?(?<endingchar>[|;]) *(\/\/.*?)?$
|
|
match = re.search(
|
|
r'^(?P<staticconst> *(?P<static>static )?const \w+(?:<(?:[\w<>, ]|::)+>)? \w+)(?: = [^()]+?(\((?:[^()]|\([^()]*\))*\))?[^()]*?)?(?P<endingchar>[|;]) *(//.*)?$',
|
|
CONTEXT.current_line
|
|
)
|
|
if match:
|
|
CONTEXT.current_line = f"{match.group('staticconst')};"
|
|
if match.group('static') is None:
|
|
CONTEXT.comment = ''
|
|
|
|
if match.group('endingchar') == '|':
|
|
dbg_info("multiline const static assignment")
|
|
skip = ''
|
|
while not re.search(r';\s*(//.*?)?$', skip):
|
|
skip = read_line()
|
|
|
|
# Remove struct member assignment
|
|
# https://regex101.com/r/OUwV75/1
|
|
if not CONTEXT.sip_run and CONTEXT.access[-1] == Visibility.Public:
|
|
# original perl regex: ^(\s*\w+[\w<> *&:,]* \*?\w+) = ([\-\w\:\.]+(< *\w+( \*)? *>)?)+(\([^()]*\))?\s*;
|
|
# dbg_info(f"attempt struct member assignment '{CONTEXT.current_line}'")
|
|
|
|
python_regex_verbose = r'''
|
|
^ # Start of the line
|
|
( # Start of capturing group for the left-hand side
|
|
\s* # Optional leading whitespace
|
|
(?:const\s+)? # Optional const qualifier
|
|
(?: # Start of non-capturing group for type
|
|
(?:unsigned\s+)? # Optional unsigned qualifier
|
|
(?:long\s+long|long|int|short|char|float|double|bool|auto|void|size_t|time_t) # Basic types
|
|
| # OR
|
|
[\w:]+(?:<[^>]+>)? # Custom types (with optional template)
|
|
)
|
|
(?:\s+const)? # Optional const qualifier after type
|
|
\s+ # Whitespace after type
|
|
\**\s* # Optional additional pointer asterisks
|
|
\w+ # Variable name
|
|
) # End of capturing group for the left-hand side
|
|
\s*=\s* # Equals sign with optional surrounding whitespace
|
|
( # Start of capturing group for the right-hand side
|
|
-? # Optional negative sign
|
|
(?: # Start of non-capturing group for value
|
|
\d+(?:\.\d*)? # Integer or floating-point number
|
|
| # OR
|
|
nullptr # nullptr keyword
|
|
| # OR
|
|
(?:std::)? # Optional std:: prefix
|
|
\w+ # Word characters for function/class names
|
|
(?:<[^>]+>)? # Optional template arguments
|
|
(?:::[\w<>]+)* # Optional nested name specifiers
|
|
(?: # Start of optional group for function calls
|
|
\( # Opening parenthesis
|
|
[^()]* # Any characters except parentheses
|
|
(?:\([^()]*\))* # Allows for one level of nested parentheses
|
|
[^()]* # Any characters except parentheses
|
|
\) # Closing parenthesis
|
|
)? # End of optional group for function calls
|
|
)
|
|
) # End of capturing group for the right-hand side
|
|
\s*; # Optional whitespace and semicolon
|
|
\s* # Optional whitespace after semicolon
|
|
(?:\/\/.*)? # Optional single-line comment
|
|
$ # End of the line
|
|
'''
|
|
regex_verbose = re.compile(python_regex_verbose,
|
|
re.VERBOSE | re.MULTILINE)
|
|
match = regex_verbose.match(CONTEXT.current_line)
|
|
if match:
|
|
dbg_info(f"remove struct member assignment '={match.group(2)}'")
|
|
CONTEXT.current_line = f"{match.group(1)};"
|
|
|
|
# Catch Q_DECLARE_FLAGS
|
|
match = re.search(r'^(\s*)Q_DECLARE_FLAGS\(\s*(.*?)\s*,\s*(.*?)\s*\)\s*$',
|
|
CONTEXT.current_line)
|
|
if match:
|
|
CONTEXT.actual_class = f"{CONTEXT.classname[-1]}::" if len(
|
|
CONTEXT.classname) >= 0 else ''
|
|
dbg_info(f"Declare flags: {CONTEXT.actual_class}")
|
|
CONTEXT.current_line = f"{match.group(1)}typedef QFlags<{CONTEXT.actual_class}{match.group(3)}> {match.group(2)};\n"
|
|
CONTEXT.qflag_hash[
|
|
f"{CONTEXT.actual_class}{match.group(2)}"] = f"{CONTEXT.actual_class}{match.group(3)}"
|
|
|
|
if f"{CONTEXT.actual_class}{match.group(3)}" not in CONTEXT.enum_intflag_types:
|
|
exit_with_error(
|
|
f"{CONTEXT.actual_class}{match.group(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
|
|
match = re.search(
|
|
r'^(\s*)Q_DECLARE_OPERATORS_FOR_FLAGS\(\s*(.*?)\s*\)\s*$',
|
|
CONTEXT.current_line)
|
|
if match:
|
|
flags = match.group(2)
|
|
flag = CONTEXT.qflag_hash.get(flags)
|
|
CONTEXT.current_line = f"{match.group(1)}QFlags<{flag}> operator|({flag} f1, QFlags<{flag}> f2);\n"
|
|
|
|
py_flag = flag.replace("::", ".")
|
|
|
|
if py_flag in CONTEXT.enum_class_non_int_types:
|
|
exit_with_error(
|
|
f"{flag} is a flags type, but was not declared with int type. Add ': int' to the enum class declaration line")
|
|
elif py_flag not in CONTEXT.enum_int_types:
|
|
if CONTEXT.is_qt6:
|
|
dbg_info("monkey patching operators for non-class enum")
|
|
if not CONTEXT.has_pushed_force_int:
|
|
CONTEXT.output_python.append(
|
|
"from enum import Enum\n\n\ndef _force_int(v): return int(v.value) if isinstance(v, Enum) else v\n\n\n")
|
|
CONTEXT.has_pushed_force_int = True
|
|
CONTEXT.output_python.append(
|
|
f"{py_flag}.__bool__ = lambda flag: bool(_force_int(flag))\n")
|
|
CONTEXT.output_python.append(
|
|
f"{py_flag}.__eq__ = lambda flag1, flag2: _force_int(flag1) == _force_int(flag2)\n")
|
|
CONTEXT.output_python.append(
|
|
f"{py_flag}.__and__ = lambda flag1, flag2: _force_int(flag1) & _force_int(flag2)\n")
|
|
CONTEXT.output_python.append(
|
|
f"{py_flag}.__or__ = lambda flag1, flag2: {py_flag}(_force_int(flag1) | _force_int(flag2))\n")
|
|
|
|
if not CONTEXT.is_qt6:
|
|
for patched_type in CONTEXT.enum_monkey_patched_types:
|
|
if flags == f"{patched_type[0]}::{patched_type[1]}":
|
|
dbg_info("monkey patching flags")
|
|
if not CONTEXT.has_pushed_force_int:
|
|
CONTEXT.output_python.append(
|
|
"from enum import Enum\n\n\ndef _force_int(v): return int(v.value) if isinstance(v, Enum) else v\n\n\n")
|
|
CONTEXT.has_pushed_force_int = True
|
|
CONTEXT.output_python.append(
|
|
f"{py_flag}.__or__ = lambda flag1, flag2: {patched_type[0]}.{patched_type[1]}(_force_int(flag1) | _force_int(flag2))\n")
|
|
|
|
# Remove keywords
|
|
if CONTEXT.is_override_or_make_private != PrependType.NoPrepend:
|
|
# Handle multiline definition to add virtual keyword or make private on opening line
|
|
if CONTEXT.multiline_definition != MultiLineType.NotMultiline:
|
|
rolling_line = CONTEXT.current_line
|
|
rolling_line_idx = CONTEXT.line_idx
|
|
dbg_info(
|
|
"handle multiline definition to add virtual keyword or making private on opening line")
|
|
while not re.match(r'^[^()]*\(([^()]*\([^()]*\)[^()]*)*[^()]*$',
|
|
rolling_line):
|
|
rolling_line_idx -= 1
|
|
rolling_line = CONTEXT.input_lines[rolling_line_idx]
|
|
if rolling_line_idx < 0:
|
|
exit_with_error('could not reach opening definition')
|
|
dbg_info(f'rolled back to {rolling_line_idx}: {rolling_line}')
|
|
|
|
if CONTEXT.is_override_or_make_private == PrependType.Virtual and not re.match(
|
|
r'^(\s*)virtual\b(.*)$',
|
|
rolling_line):
|
|
idx = rolling_line_idx - CONTEXT.line_idx + 1
|
|
CONTEXT.output[idx] = fix_annotations(
|
|
re.sub(r'^(\s*?)\b(.*)$', r'\1 virtual \2\n',
|
|
rolling_line))
|
|
elif CONTEXT.is_override_or_make_private == PrependType.MakePrivate:
|
|
dbg_info("prepending private access")
|
|
idx = rolling_line_idx - CONTEXT.line_idx
|
|
private_access = re.sub(r'(protected|public)', 'private',
|
|
CONTEXT.last_access_section_line)
|
|
CONTEXT.output.insert(idx + 1, private_access + "\n")
|
|
CONTEXT.output[idx + 1] = fix_annotations(rolling_line) + "\n"
|
|
elif CONTEXT.is_override_or_make_private == PrependType.MakePrivate:
|
|
dbg_info("prepending private access")
|
|
CONTEXT.current_line = re.sub(r'(protected|public)', 'private',
|
|
CONTEXT.last_access_section_line) + "\n" + CONTEXT.current_line + "\n"
|
|
elif CONTEXT.is_override_or_make_private == PrependType.Virtual and not re.match(
|
|
r'^(\s*)virtual\b(.*)$', CONTEXT.current_line):
|
|
# 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')
|
|
CONTEXT.current_line = re.sub(r'^(\s*?)\b(.*)$', r'\1virtual \2\n',
|
|
CONTEXT.current_line)
|
|
|
|
# remove constructor definition, function bodies, member initializing list
|
|
CONTEXT.python_signature = detect_and_remove_following_body_or_initializerlist()
|
|
|
|
# remove inline declarations
|
|
match = re.search(
|
|
r'^(\s*)?(static |const )*(([(?:long )\w]+(<.*?>)?\s+([*&])?)?(\w+)( const*?)*)\s*(\{.*});(\s*//.*)?$',
|
|
CONTEXT.current_line)
|
|
if match:
|
|
CONTEXT.current_line = f"{match.group(1)}{match.group(3)};"
|
|
|
|
pattern = r'^\s*((?:const |virtual |static |inline ))*(?!explicit)([(?:long )\w:]+(?:<.*?>)?)\s+(?:\*|&)?(\w+|operator.{1,2})(\(.*)$'
|
|
match = re.match(pattern, CONTEXT.current_line)
|
|
if match:
|
|
CONTEXT.current_method_name = match.group(3)
|
|
return_type_candidate = match.group(2)
|
|
is_static = bool(match.group(1) and 'static' in match.group(1))
|
|
class_name = CONTEXT.current_fully_qualified_class_name()
|
|
if CONTEXT.current_method_name in CONTEXT.static_methods[class_name]:
|
|
if CONTEXT.static_methods[class_name][CONTEXT.current_method_name] != is_static:
|
|
CONTEXT.static_methods[class_name][
|
|
CONTEXT.current_method_name] = False
|
|
else:
|
|
CONTEXT.static_methods[class_name][CONTEXT.current_method_name] = is_static
|
|
|
|
if CONTEXT.access[-1] == Visibility.Signals:
|
|
CONTEXT.current_signal_args = []
|
|
signal_args = match.group(4).strip()
|
|
if signal_args.startswith('('):
|
|
signal_args = signal_args[1:]
|
|
if signal_args.endswith(');'):
|
|
signal_args = signal_args[:-2]
|
|
|
|
if signal_args.strip():
|
|
CONTEXT.current_signal_args = split_args(signal_args)
|
|
dbg_info('SIGARG ' + CONTEXT.current_method_name + " " + str(CONTEXT.current_signal_args))
|
|
if ');' in match.group(4):
|
|
CONTEXT.signal_arguments[class_name][
|
|
CONTEXT.current_method_name] = CONTEXT.current_signal_args[:]
|
|
dbg_info('SIGARG finalizing' + CONTEXT.current_method_name + " " + str(CONTEXT.current_signal_args))
|
|
|
|
if not re.search(r'(void|SIP_PYOBJECT|operator|return|QFlag)',
|
|
return_type_candidate):
|
|
# replace :: with . (changes c++ style namespace/class directives to Python style)
|
|
CONTEXT.return_type = return_type_candidate.replace('::', '.')
|
|
# replace with builtin Python types
|
|
CONTEXT.return_type = re.sub(r'\bdouble\b', 'float',
|
|
CONTEXT.return_type)
|
|
CONTEXT.return_type = re.sub(r'\bQString\b', 'str',
|
|
CONTEXT.return_type)
|
|
CONTEXT.return_type = re.sub(r'\bQStringList\b', 'list of str',
|
|
CONTEXT.return_type)
|
|
|
|
list_match = re.match(r'^(?:QList|QVector)<\s*(.*?)[\s*]*>$',
|
|
CONTEXT.return_type)
|
|
if list_match:
|
|
CONTEXT.return_type = f"list of {list_match.group(1)}"
|
|
|
|
set_match = re.match(r'^QSet<\s*(.*?)[\s*]*>$',
|
|
CONTEXT.return_type)
|
|
if set_match:
|
|
CONTEXT.return_type = f"set of {set_match.group(1)}"
|
|
elif CONTEXT.access[-1] == Visibility.Signals and CONTEXT.current_line.strip() not in ('', 'signals:'):
|
|
dbg_info('SIGARG4 ' + CONTEXT.current_method_name + " " + CONTEXT.current_line)
|
|
signal_args = CONTEXT.current_line.strip()
|
|
if signal_args.endswith(');'):
|
|
signal_args = signal_args[:-2]
|
|
|
|
if signal_args.strip():
|
|
CONTEXT.current_signal_args.extend(split_args(signal_args))
|
|
dbg_info('SIGARG5 ' + CONTEXT.current_method_name + " " + str(CONTEXT.current_signal_args))
|
|
if ');' in CONTEXT.current_line:
|
|
class_name = CONTEXT.current_fully_qualified_class_name()
|
|
CONTEXT.signal_arguments[class_name][
|
|
CONTEXT.current_method_name] = CONTEXT.current_signal_args[:]
|
|
dbg_info('SIGARG finalizing' + CONTEXT.current_method_name + " " + str(CONTEXT.current_signal_args))
|
|
|
|
# deleted functions
|
|
if re.match(
|
|
r'^(\s*)?(const )?(virtual |static )?((\w+(<.*?>)?\s+([*&])?)?(\w+|operator.{1,2})\(.*?(\(.*\))*.*\)( const)?)\s*= delete;(\s*//.*)?$',
|
|
CONTEXT.current_line):
|
|
CONTEXT.comment = ''
|
|
continue
|
|
|
|
# remove export macro from struct definition
|
|
CONTEXT.current_line = re.sub(r'^(\s*struct )\w+_EXPORT (.+)$', r'\1\2',
|
|
CONTEXT.current_line)
|
|
|
|
# Skip comments
|
|
if re.match(r'^\s*typedef\s+\w+\s*<\s*\w+\s*>\s+\w+\s+.*SIP_DOC_TEMPLATE',
|
|
CONTEXT.current_line):
|
|
# support Docstring for template based classes in SIP 4.19.7+
|
|
CONTEXT.comment_template_docstring = True
|
|
elif (CONTEXT.multiline_definition == MultiLineType.NotMultiline and
|
|
(re.search(r'//', CONTEXT.current_line) or
|
|
re.match(r'^\s*typedef ', CONTEXT.current_line) or
|
|
re.search(r'\s*struct ', CONTEXT.current_line) or
|
|
re.search(r'operator\[]\(', CONTEXT.current_line) or
|
|
re.match(r'^\s*operator\b', CONTEXT.current_line) or
|
|
re.search(r'operator\s?[!+-=*/\[\]<>]{1,2}',
|
|
CONTEXT.current_line) or
|
|
re.match(r'^\s*%\w+(.*)?$', CONTEXT.current_line) or
|
|
re.match(r'^\s*namespace\s+\w+', CONTEXT.current_line) or
|
|
re.match(r'^\s*(virtual\s*)?~', CONTEXT.current_line) or
|
|
detect_non_method_member(CONTEXT.current_line)
|
|
)):
|
|
dbg_info(f'skipping comment for {CONTEXT.current_line}')
|
|
if re.search(r'\s*typedef.*?(?!SIP_DOC_TEMPLATE)',
|
|
CONTEXT.current_line):
|
|
dbg_info('because typedef')
|
|
elif CONTEXT.actual_class and detect_non_method_member(
|
|
CONTEXT.current_line) and CONTEXT.comment:
|
|
attribute_name_match = re.match(r'^.*?\s[*&]*(\w+);.*$',
|
|
CONTEXT.current_line)
|
|
class_name = CONTEXT.current_fully_qualified_struct_name()
|
|
dbg_info(
|
|
f'storing attribute docstring for {class_name} : {attribute_name_match.group(1)}')
|
|
CONTEXT.attribute_docstrings[class_name][
|
|
attribute_name_match.group(1)] = CONTEXT.comment
|
|
elif CONTEXT.current_fully_qualified_struct_name() and re.search(r'\s*struct ', CONTEXT.current_line) and CONTEXT.comment:
|
|
class_name = CONTEXT.current_fully_qualified_struct_name()
|
|
dbg_info(
|
|
f'storing struct docstring for {class_name}')
|
|
CONTEXT.struct_docstrings[class_name] = CONTEXT.comment
|
|
|
|
CONTEXT.comment = ''
|
|
CONTEXT.return_type = ''
|
|
CONTEXT.is_override_or_make_private = PrependType.NoPrepend
|
|
|
|
CONTEXT.current_line = fix_constants(CONTEXT.current_line)
|
|
CONTEXT.current_line = fix_annotations(CONTEXT.current_line)
|
|
|
|
# fix astyle placing space after % character
|
|
CONTEXT.current_line = re.sub(r'/\s+GetWrapper\s+/', '/GetWrapper/',
|
|
CONTEXT.current_line)
|
|
|
|
# MISSING
|
|
# handle enum/flags QgsSettingsEntryEnumFlag
|
|
match = re.match(r'^(\s*)const QgsSettingsEntryEnumFlag<(.*)> (.+);$',
|
|
CONTEXT.current_line)
|
|
if match:
|
|
CONTEXT.indent, enum_type, var_name = match.groups()
|
|
|
|
prep_line = f"""class QgsSettingsEntryEnumFlag_{var_name}
|
|
{{
|
|
%TypeHeaderCode
|
|
#include "{os.path.basename(CONTEXT.header_file)}"
|
|
#include "qgssettingsentry.h"
|
|
typedef QgsSettingsEntryEnumFlag<{enum_type}> QgsSettingsEntryEnumFlag_{var_name};
|
|
%End
|
|
public:
|
|
QgsSettingsEntryEnumFlag_{var_name}( const QString &key, QgsSettings::Section section, const {enum_type} &defaultValue, const QString &description = QString() );
|
|
QString key( const QString &dynamicKeyPart = QString() ) const;
|
|
{enum_type} value( const QString &dynamicKeyPart = QString(), bool useDefaultValueOverride = false, const {enum_type} &defaultValueOverride = {enum_type}() ) const;
|
|
}};"""
|
|
|
|
CONTEXT.current_line = f"{CONTEXT.indent}const QgsSettingsEntryEnumFlag_{var_name} {var_name};"
|
|
CONTEXT.comment = ''
|
|
write_output("ENF", f"{prep_line}\n", "prepend")
|
|
|
|
write_output("NOR", f"{CONTEXT.current_line}\n")
|
|
|
|
# append to class map file
|
|
if args.class_map and CONTEXT.actual_class:
|
|
match = re.match(
|
|
r'^ *(const |virtual |static )* *[\w:]+ +\*?(?P<method>\w+)\(.*$',
|
|
CONTEXT.current_line)
|
|
if match:
|
|
with open(args.class_map, 'a') as f:
|
|
f.write(
|
|
f"{'.'.join(CONTEXT.classname)}.{match.group('method')}: {CONTEXT.header_file}#L{CONTEXT.line_idx}\n")
|
|
|
|
if CONTEXT.python_signature:
|
|
write_output("PSI", f"{CONTEXT.python_signature}\n")
|
|
|
|
# multiline definition (parenthesis left open)
|
|
if CONTEXT.multiline_definition != MultiLineType.NotMultiline:
|
|
dbg_info("on multiline")
|
|
# https://regex101.com/r/DN01iM/4
|
|
# TODO - original regex is incompatible with python -- it was:
|
|
# ^([^()]+(\((?:[^()]++|(?1))*\)))*[^()]*\)([^()](throw\([^()]+\))?)*$:
|
|
if re.match(
|
|
r'^([^()]+(\((?:[^()]|\([^()]*\))*\)))*[^()]*\)([^()](throw\([^()]+\))?)*',
|
|
CONTEXT.current_line):
|
|
dbg_info("ending multiline")
|
|
# remove potential following body
|
|
if CONTEXT.multiline_definition != MultiLineType.ConditionalStatement and not re.search(
|
|
r'(\{.*}|;)\s*(//.*)?$',
|
|
CONTEXT.current_line):
|
|
dbg_info("remove following body of multiline def")
|
|
last_line = CONTEXT.current_line
|
|
last_line += remove_following_body_or_initializerlist()
|
|
# add missing semi column
|
|
CONTEXT.output.pop()
|
|
write_output("MLT", f"{last_line};\n")
|
|
CONTEXT.multiline_definition = MultiLineType.NotMultiline
|
|
else:
|
|
continue
|
|
elif re.match(r'^[^()]+\([^()]*(?:\([^()]*\)[^()]*)*[^)]*$',
|
|
CONTEXT.current_line):
|
|
dbg_info(f"Multiline detected:: {CONTEXT.current_line}")
|
|
if re.match(r'^\s*((else )?if|while|for) *\(', CONTEXT.current_line):
|
|
CONTEXT.multiline_definition = MultiLineType.ConditionalStatement
|
|
else:
|
|
CONTEXT.multiline_definition = MultiLineType.Method
|
|
continue
|
|
|
|
# write comment
|
|
if re.match(r'^\s*$', CONTEXT.current_line):
|
|
dbg_info("no more override / private")
|
|
CONTEXT.is_override_or_make_private = PrependType.NoPrepend
|
|
continue
|
|
|
|
if re.match(r'^\s*template\s*<.*>', CONTEXT.current_line):
|
|
# do not comment now for templates, wait for class definition
|
|
continue
|
|
|
|
if CONTEXT.comment.strip() or CONTEXT.return_type:
|
|
if CONTEXT.is_override_or_make_private != PrependType.Virtual and not CONTEXT.comment.strip():
|
|
# overridden method with no new docs - so don't create a Docstring and use
|
|
# parent class Docstring
|
|
pass
|
|
else:
|
|
dbg_info('writing comment')
|
|
if CONTEXT.comment.strip():
|
|
dbg_info('comment non-empty')
|
|
doc_prepend = "@DOCSTRINGSTEMPLATE@" if CONTEXT.comment_template_docstring else ""
|
|
write_output("CM1", f"{doc_prepend}%Docstring\n")
|
|
|
|
doc_string = ''
|
|
comment_lines = CONTEXT.comment.split('\n')
|
|
skipping_param = 0
|
|
out_params = []
|
|
waiting_for_return_to_end = False
|
|
|
|
comment_line_idx = 0
|
|
while comment_line_idx < len(comment_lines):
|
|
comment_line = comment_lines[comment_line_idx]
|
|
comment_line_idx += 1
|
|
if (
|
|
'versionadded:' in comment_line or 'deprecated:' in comment_line) and out_params:
|
|
dbg_info('out style parameters remain to flush!')
|
|
# member has /Out/ parameters, but no return type, so flush out out_params docs now
|
|
first_out_param = out_params.pop(0)
|
|
doc_string += f"{doc_prepend}:return: - {first_out_param}\n"
|
|
|
|
for out_param in out_params:
|
|
doc_string += f"{doc_prepend} - {out_param}\n"
|
|
|
|
doc_string += f"{doc_prepend}\n"
|
|
out_params = []
|
|
|
|
param_match = re.match(r'^:param\s+(\w+)', comment_line)
|
|
if param_match:
|
|
param_name = param_match.group(1)
|
|
dbg_info(f'found parameter: {param_name}')
|
|
if param_name in CONTEXT.skipped_params_out or param_name in CONTEXT.skipped_params_remove:
|
|
dbg_info(str(CONTEXT.skipped_params_out))
|
|
if param_name in CONTEXT.skipped_params_out:
|
|
dbg_info(f'deferring docs for parameter {param_name} marked as SIP_OUT')
|
|
comment_line = re.sub(
|
|
r'^:param\s+(\w+):\s*(.*?)$', r'\1: \2',
|
|
comment_line)
|
|
comment_line = re.sub(
|
|
r'(?:optional|if specified|if given|storage for|will be set to),?\s*',
|
|
'',
|
|
comment_line)
|
|
out_params.append(comment_line)
|
|
skipping_param = 2
|
|
else:
|
|
skipping_param = 1
|
|
continue
|
|
|
|
if skipping_param > 0:
|
|
if re.match(r'^(:.*|\.\..*|\s*)$', comment_line):
|
|
skipping_param = 0
|
|
elif skipping_param == 2:
|
|
comment_line = re.sub(r'^\s+', ' ', comment_line)
|
|
out_params[-1] += comment_line
|
|
continue
|
|
else:
|
|
continue
|
|
|
|
if ':return:' in comment_line and out_params:
|
|
waiting_for_return_to_end = True
|
|
comment_line = comment_line.replace(':return:',
|
|
':return: -')
|
|
doc_string += f"{doc_prepend}{comment_line}\n"
|
|
|
|
# scan forward to find end of return description
|
|
scan_forward_idx = comment_line_idx
|
|
needs_blank_line_after_return = False
|
|
while scan_forward_idx < len(comment_lines):
|
|
scan_forward_line = comment_lines[scan_forward_idx]
|
|
scan_forward_idx += 1
|
|
if not scan_forward_line.strip() and scan_forward_idx < len(comment_lines) - 1:
|
|
# check if following line is start of list
|
|
if re.match(r'^\s*-(?!-)', comment_lines[scan_forward_idx + 1]):
|
|
doc_string += "\n"
|
|
comment_line_idx += 1
|
|
needs_blank_line_after_return = True
|
|
continue
|
|
|
|
if re.match(r'^(:.*|\.\..*|\s*)$', scan_forward_line) or not scan_forward_line.strip():
|
|
break
|
|
|
|
doc_string += f"{doc_prepend} {scan_forward_line}\n"
|
|
comment_line_idx += 1
|
|
|
|
if needs_blank_line_after_return:
|
|
doc_string += "\n"
|
|
|
|
for out_param in out_params:
|
|
doc_string += f"{doc_prepend} - {out_param}\n"
|
|
out_params = []
|
|
else:
|
|
doc_string += f"{doc_prepend}{comment_line}\n"
|
|
|
|
if waiting_for_return_to_end:
|
|
if re.match(r'^(:.*|\.\..*|\s*)$', comment_line):
|
|
waiting_for_return_to_end = False
|
|
else:
|
|
pass # Return docstring should be single line with SIP_OUT params
|
|
|
|
if out_params:
|
|
if CONTEXT.return_type:
|
|
exit_with_error(
|
|
f"A method with output parameters must contain a return directive ({CONTEXT.current_method_name} method returns {CONTEXT.return_type})")
|
|
else:
|
|
doc_string += "\n"
|
|
|
|
for out_param_idx, out_param in enumerate(out_params):
|
|
if out_param_idx == 0:
|
|
if len(out_params) > 1:
|
|
doc_string += f":return: - {out_param}\n"
|
|
else:
|
|
arg_name_match = re.match(r'^(.*?):\s*(.*?)$', out_param)
|
|
doc_string += f":return: {arg_name_match.group(2)}\n"
|
|
else:
|
|
doc_string += f"{doc_prepend} - {out_param}\n"
|
|
|
|
dbg_info(f'doc_string is {doc_string}')
|
|
write_output("DS", doc_string)
|
|
if CONTEXT.access[-1] == Visibility.Signals and doc_string:
|
|
dbg_info('storing signal docstring')
|
|
class_name = '.'.join(CONTEXT.classname)
|
|
CONTEXT.attribute_docstrings[class_name][
|
|
CONTEXT.current_method_name] = doc_string
|
|
write_output("CM4", f"{doc_prepend}%End\n")
|
|
|
|
CONTEXT.comment = ''
|
|
CONTEXT.return_type = ''
|
|
if CONTEXT.is_override_or_make_private == PrependType.MakePrivate:
|
|
write_output("MKP", CONTEXT.last_access_section_line)
|
|
CONTEXT.is_override_or_make_private = PrependType.NoPrepend
|
|
else:
|
|
if CONTEXT.is_override_or_make_private == PrependType.MakePrivate:
|
|
write_output("MKP", CONTEXT.last_access_section_line)
|
|
CONTEXT.is_override_or_make_private = PrependType.NoPrepend
|
|
|
|
# Output results
|
|
if args.sip_output:
|
|
with open(args.sip_output, 'w') as f:
|
|
f.write(''.join(sip_header_footer()))
|
|
f.write(''.join(CONTEXT.output))
|
|
f.write(''.join(sip_header_footer()))
|
|
else:
|
|
print(''.join(sip_header_footer()) +
|
|
''.join(CONTEXT.output) +
|
|
''.join(sip_header_footer()).rstrip())
|
|
|
|
class_additions = defaultdict(list)
|
|
|
|
for class_name, attribute_docstrings in CONTEXT.attribute_docstrings.items():
|
|
class_additions[class_name].append(f'{class_name}.__attribute_docs__ = {str(attribute_docstrings)}')
|
|
|
|
for class_name, static_methods in CONTEXT.static_methods.items():
|
|
for method_name, is_static in static_methods.items():
|
|
if not is_static:
|
|
continue
|
|
|
|
# TODO -- fix
|
|
if class_name == 'QgsProcessingUtils' and method_name == 'createFeatureSinkPython':
|
|
method_name = 'createFeatureSink'
|
|
elif class_name == 'QgsRasterAttributeTable' and method_name == 'usageInformationInt':
|
|
method_name = 'usageInformation'
|
|
elif class_name == 'QgsSymbolLayerUtils' and method_name == 'wellKnownMarkerFromSld':
|
|
method_name = 'wellKnownMarkerFromSld2'
|
|
elif class_name == 'QgsZonalStatistics' and method_name in ('calculateStatisticsInt', 'calculateStatistics'):
|
|
continue
|
|
elif class_name == 'QgsServerApiUtils' and method_name == 'temporalExtentList':
|
|
method_name = 'temporalExtent'
|
|
|
|
class_additions[class_name].append(
|
|
f'{class_name}.{method_name} = staticmethod({class_name}.{method_name})')
|
|
|
|
for class_name, signal_arguments in CONTEXT.signal_arguments.items():
|
|
python_signatures = {}
|
|
|
|
for signal, arguments in signal_arguments.items():
|
|
python_args = []
|
|
for argument in arguments:
|
|
var_name, python_type, default_value = parse_argument(argument)
|
|
if default_value:
|
|
python_args.append(f"{var_name}: {python_type} = {default_value}")
|
|
else:
|
|
python_args.append(f"{var_name}: {python_type}")
|
|
if python_args:
|
|
python_signatures[signal] = python_args
|
|
|
|
if python_signatures:
|
|
class_additions[class_name].append(
|
|
f'{class_name}.__signal_arguments__ = {str(python_signatures)}')
|
|
|
|
for class_name, doc_string in CONTEXT.struct_docstrings.items():
|
|
class_additions[class_name].append(f'{class_name}.__doc__ = """{doc_string}"""')
|
|
|
|
group_match = re.match('^.*src/[a-z0-9_]+/(.*?)/[^/]+$', CONTEXT.header_file)
|
|
if group_match:
|
|
groups = list(group for group in group_match.group(1).split('/') if group and group != '.')
|
|
if groups:
|
|
for class_name in CONTEXT.all_fully_qualified_class_names:
|
|
class_additions[class_name].append(
|
|
f'{class_name}.__group__ = {groups}')
|
|
|
|
for _class, additions in class_additions.items():
|
|
if additions:
|
|
this_class_additions = "\n".join(" " + c for c in additions)
|
|
CONTEXT.output_python.append(
|
|
f'try:\n{this_class_additions}\nexcept NameError:\n pass\n')
|
|
|
|
|
|
if args.python_output and CONTEXT.output_python:
|
|
|
|
with open(args.python_output, 'w') as f:
|
|
f.write(''.join(python_header()))
|
|
f.write(''.join(CONTEXT.output_python))
|