QGIS/scripts/sipify.py
David Koňařík 4d86676c35 Fix SIP build errors by using /NoTypeName/
The typedef is only present in the .sip file, but without this
annotation SIP assumes it also exists in QGIS headers. This didn't
completely break the build, since we concatenate multiple SIP headers
into larger files, so the typedefs from one part "fixed" another. Run
sip-build without --concatenate to see the issue clearly.

This fix is currently sadly not fully effective due to a SIP bug:
https://github.com/Python-SIP/sip/issues/66
2025-01-06 21:31:40 +01:00

3151 lines
126 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 Any, Dict, List, Optional, Tuple
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) as f:
input_lines = f.read().splitlines()
except OSError 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) as f:
sip_config = yaml.safe_load(f)
except OSError 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)
self.deprecated_message = None
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_deprecated_message(message: str) -> str:
"""
Remove all doxygen specific command from deprecated message
"""
# SIP issue with ':' , see https://github.com/Python-SIP/sip/issues/59
return message.replace("\\see", "").replace(":", "")
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")
CONTEXT.deprecated_message = (
f"Since {version}. {process_deprecated_message(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_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/",
}
if not CONTEXT.is_qt6:
replacements[r"SIP_THROW\(\s*([\w\s,]+?)\s*\)"] = r"throw( \1 )"
else:
# these have no effect (and aren't required) on sip >= 6
replacements[r"SIP_THROW\(\s*([\w\s,]+?)\s*\)"] = ""
if CONTEXT.deprecated_message:
replacements[r"\bSIP_DEPRECATED\b"] = (
f'/Deprecated="{CONTEXT.deprecated_message}"/'
)
else:
replacements[r"\bSIP_DEPRECATED\b"] = f"/Deprecated/"
for _pattern, replacement in replacements.items():
line = re.sub(_pattern, replacement, line)
# Combine multiple annotations
while True:
new_line = re.sub(
r'/([\w,]+(="?[^"]+"?)?)/\s*/([\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:
# TYPE HEADER CODE
if CONTEXT.header_code and not re.match(r"^ *//.*$", CONTEXT.current_line):
CONTEXT.header_code = False
write_output("HCE", "%End\n")
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)
match = re.search(r'SIP_TYPEHEADER_INCLUDE\(\s*"(.*?)"\s*\)', CONTEXT.current_line)
if match:
dbg_info("found SIP_TYPEHEADER_INCLUDE")
write_output("STI", f'#include "{match.group(1)}"\n')
continue
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
and not re.match(r"^ *//.*$", CONTEXT.current_line)
):
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,
# since SIP doesn't allow inheriting from template classes directly
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 == "":
# We use /NoTypeName/ to say that this typedef is not present in actual QGIS headers
CONTEXT.current_line = f"\ntypedef {tpl}<{cls1}> {tpl}{cls1}Base /NoTypeName/;\n\n{CONTEXT.current_line}"
elif cls3 == "":
CONTEXT.current_line = f"\ntypedef {tpl}<{cls1},{cls2}> {tpl}{cls1}{cls2}Base /NoTypeName/;\n\n{CONTEXT.current_line}"
else:
CONTEXT.current_line = f"\ntypedef {tpl}<{cls1},{cls2},{cls3}> {tpl}{cls1}{cls2}{cls3}Base /NoTypeName/;\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"
complete_class_path = ".".join(CONTEXT.classname)
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} = {complete_class_path}.{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} = {complete_class_path}.{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"{complete_class_path}.{compat_name} = {complete_class_path}.{enum_qualname}.{enum_member}\n"
)
CONTEXT.output_python.append(
f"{complete_class_path}.{compat_name}.is_monkey_patched = True\n"
)
if CONTEXT.actual_class:
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'{".".join(CONTEXT.classname)}.{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<>, ]|::)+>)? \w+)(?: = [^()]+?(\((?:[^()]++|(?3))*\))?[^()]*?)?(?<endingchar>[|;]) *(\/\/.*?)?$
match = re.search(
r"^(?P<staticconst> *(?P<static>static )?const (\w+::)*\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)
if flag is None:
exit_with_error(f"error reading flags: {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, AttributeError):\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))