QGIS/scripts/sipify.py

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

3151 lines
126 KiB
Python
Raw Permalink Normal View History

2024-08-09 11:45:57 +10:00
#!/usr/bin/env python3
2024-08-13 09:54:19 +10:00
import argparse
import os
2024-08-09 11:45:57 +10:00
import re
import sys
2024-11-29 14:26:30 +01:00
from collections import defaultdict
2024-08-13 09:54:19 +10:00
from enum import Enum, auto
2024-08-28 11:14:23 +10:00
from typing import Any, Dict, List, Optional, Tuple
2024-08-09 11:45:57 +10:00
2024-08-13 09:54:19 +10:00
import yaml
class Visibility(Enum):
Private = auto()
Protected = auto()
Public = auto()
Signals = auto()
2024-08-13 09:54:19 +10:00
class CodeSnippetType(Enum):
NotCodeSnippet = auto()
NotSpecified = auto()
Cpp = auto()
class PrependType(Enum):
NoPrepend = auto()
Virtual = auto()
MakePrivate = auto()
2024-08-13 10:53:37 +10:00
class MultiLineType(Enum):
NotMultiline = auto()
Method = auto()
ConditionalStatement = auto()
2024-08-09 11:45:57 +10:00
# Parse command-line arguments
2024-08-13 09:54:19 +10:00
parser = argparse.ArgumentParser(description="Convert header file to SIP and Python")
2024-08-09 11:45:57 +10:00
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:
2024-08-09 11:45:57 +10:00
input_lines = f.read().splitlines()
except OSError as e:
print(
f"Couldn't open '{args.headerfile}' for reading because: {e}", file=sys.stderr
2024-08-13 09:54:19 +10:00
)
2024-08-09 11:45:57 +10:00
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:
2024-08-13 09:54:19 +10:00
print(
f"Couldn't open configuration file '{cfg_file}' because: {e}", file=sys.stderr
)
2024-08-09 11:45:57 +10:00
sys.exit(1)
2024-08-09 11:45:57 +10:00
# Initialize contexts
class Context:
def __init__(self):
self.debug: bool = False
self.is_qt6: bool = False
self.header_file: str = ""
2024-08-13 10:50:43 +10:00
self.current_line: str = ""
self.sip_run: bool = False
self.header_code: bool = False
self.access: list[Visibility] = [Visibility.Public]
2024-08-13 10:53:37 +10:00
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)
2024-08-28 11:14:23 +10:00
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)
2024-08-09 11:45:57 +10:00
ALLOWED_NON_CLASS_ENUMS = [
"QgsSipifyHeader::MyEnum",
"QgsSipifyHeader::OneLiner",
"CadConstraint::LockMode",
"ColorrampTable",
2024-08-10 13:22:37 +10:00
"QgsMesh::ElementType",
2024-08-09 11:45:57 +10:00
"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",
2024-08-09 11:45:57 +10:00
"QgsDiagramSettings::DiagramOrientation",
"QgsDiagramSettings::Direction",
"QgsDiagramSettings::LabelPlacementMethod",
"QgsDiagramSettings::StackedDiagramMode",
2024-08-09 11:45:57 +10:00
"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
2024-08-09 11:45:57 +10:00
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:
2024-08-09 11:45:57 +10:00
# sip for Qt6 chokes on QList/QVector<QVariantMap>, but is happy if you expand out the map explicitly
2024-08-13 09:54:19 +10:00
line = re.sub(
r"(QList<\s*|QVector<\s*)QVariantMap", r"\1QMap<QString, QVariant>", line
)
2024-08-09 11:45:57 +10:00
return line
def read_line():
global CONTEXT
2024-08-09 11:45:57 +10:00
new_line = CONTEXT.input_lines[CONTEXT.line_idx]
CONTEXT.line_idx += 1
2024-08-09 11:45:57 +10:00
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}"
)
2024-08-09 11:45:57 +10:00
new_line = replace_macros(new_line)
return new_line
def write_output(dbg_code, out, prepend="no"):
global CONTEXT
2024-08-09 11:45:57 +10:00
if CONTEXT.debug:
dbg_code = f"{CONTEXT.line_idx} {dbg_code:<4} :: "
2024-08-09 11:45:57 +10:00
else:
dbg_code = ""
if prepend == "prepend":
CONTEXT.output.insert(0, dbg_code + out)
2024-08-09 11:45:57 +10:00
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")
2024-08-09 11:45:57 +10:00
CONTEXT.if_feature_condition = ""
2024-08-09 11:45:57 +10:00
def dbg_info(info):
global CONTEXT
2024-08-09 11:45:57 +10:00
if CONTEXT.debug:
CONTEXT.output.append(f"{info}\n")
2024-08-13 09:54:19 +10:00
print(
f"{CONTEXT.line_idx} {len(CONTEXT.access)} {CONTEXT.sip_run} {CONTEXT.multiline_definition} {info}"
)
2024-08-09 11:45:57 +10:00
def exit_with_error(message):
global CONTEXT
2024-08-13 09:54:19 +10:00
sys.exit(
f"! Sipify error in {CONTEXT.header_file} at line :: {CONTEXT.line_idx}\n! {message}"
)
2024-08-09 11:45:57 +10:00
def sip_header_footer():
global CONTEXT
2024-08-09 11:45:57 +10:00
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)
2024-08-13 09:54:19 +10:00
header_footer.append(
"/************************************************************************\n"
)
header_footer.append(
" * This file has been generated automatically from *\n"
)
header_footer.append(
" * *\n"
)
2024-08-09 11:45:57 +10:00
header_footer.append(f" * {headerfile_x:<68} *\n")
2024-08-13 09:54:19 +10:00
header_footer.append(
" * *\n"
)
header_footer.append(
" * Do not edit manually ! Edit header and run scripts/sipify.py again *\n"
)
header_footer.append(
" ************************************************************************/\n"
)
2024-08-09 11:45:57 +10:00
return header_footer
def python_header():
global CONTEXT
2024-08-09 11:45:57 +10:00
header = []
headerfile_x = re.sub(r"src/core/3d", r"src/core/./3d", CONTEXT.header_file)
2024-08-09 11:45:57 +10:00
header.append("# The following has been generated automatically from ")
header.append(f"{headerfile_x}\n")
return header
def create_class_links(line):
global CONTEXT
2024-08-09 11:45:57 +10:00
# Replace Qgs classes (but not the current class) with :py:class: links
2024-08-14 08:19:33 +10:00
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:
2024-08-13 09:54:19 +10:00
line = re.sub(r"\b(Qgs[A-Z]\w+)\b(\.?$|\W{2})", r":py:class:`\1`\2", line)
2024-08-09 11:45:57 +10:00
# Replace Qgs class methods with :py:func: links
2024-08-13 09:54:19 +10:00
line = re.sub(r"\b((Qgs[A-Z]\w+|Qgis)\.[a-z]\w+\(\))(?!\w)", r":py:func:`\1`", line)
2024-08-09 11:45:57 +10:00
# Replace other methods with :py:func: links
if CONTEXT.actual_class:
2024-08-13 09:54:19 +10:00
line = re.sub(
r"(?<!\.)\b([a-z]\w+)\(\)(?!\w)",
rf":py:func:`~{CONTEXT.actual_class}.\1`",
line,
)
2024-08-09 11:45:57 +10:00
else:
2024-08-13 09:54:19 +10:00
line = re.sub(r"(?<!\.)\b([a-z]\w+)\(\)(?!\w)", r":py:func:`~\1`", line)
2024-08-09 11:45:57 +10:00
# Replace Qgs classes (but not the current class) with :py:class: links
2024-08-14 08:19:33 +10:00
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
):
2024-08-13 09:54:19 +10:00
line = re.sub(
r"\b(?<![`~])(Qgs[A-Z]\w+|Qgis)\b(?!\()", r":py:class:`\1`", line
)
2024-08-09 11:45:57 +10:00
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(":", "")
2024-08-14 08:19:33 +10:00
def process_doxygen_line(line: str) -> str:
global CONTEXT
2024-08-09 11:45:57 +10:00
# Handle SIP_RUN preprocessor directives
2024-08-09 16:44:15 +10:00
if re.search(r"\s*#ifdef SIP_RUN", line):
CONTEXT.doxy_inside_sip_run = 1
2024-08-09 11:45:57 +10:00
return ""
2024-08-09 16:44:15 +10:00
elif re.search(r"\s*#ifndef SIP_RUN", line):
CONTEXT.doxy_inside_sip_run = 2
2024-08-09 11:45:57 +10:00
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
2024-08-09 11:45:57 +10:00
return ""
elif CONTEXT.doxy_inside_sip_run != 0 and re.search(r"\s*#endif", line):
CONTEXT.doxy_inside_sip_run = 0
2024-08-09 11:45:57 +10:00
return ""
if CONTEXT.doxy_inside_sip_run == 2:
2024-08-09 11:45:57 +10:00
return ""
if r"\copydoc" in line:
exit_with_error(
"\\copydoc doxygen command cannot be used for methods exposed to Python"
)
2024-08-29 11:21:47 +10:00
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"
)
2024-08-29 12:29:07 +10:00
if re.search(r"<[ib]>", line):
exit_with_error(
"Don't use raw <i> or <b> tags in documentation. " "Use markdown instead"
)
2024-08-29 11:21:47 +10:00
2024-08-09 11:45:57 +10:00
# Detect code snippet
2024-08-09 16:44:15 +10:00
code_match = re.search(r"\\code(\{\.?(\w+)})?", line)
2024-08-09 11:45:57 +10:00
if code_match:
codelang = f" {code_match.group(2)}" if code_match.group(2) else ""
2024-08-09 16:44:15 +10:00
if not re.search(r"(cpp|py|unparsed)", codelang):
2024-08-09 11:45:57 +10:00
exit_with_error(f"invalid code snippet format: {codelang}")
CONTEXT.comment_code_snippet = CodeSnippetType.NotSpecified
2024-08-09 11:45:57 +10:00
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"
2024-11-29 14:26:30 +01:00
)
2024-08-09 16:44:15 +10:00
if re.search(r"\\endcode", line):
CONTEXT.comment_code_snippet = CodeSnippetType.NotCodeSnippet
2024-08-09 11:45:57 +10:00
return "\n"
if CONTEXT.comment_code_snippet != CodeSnippetType.NotCodeSnippet:
if CONTEXT.comment_code_snippet == CodeSnippetType.Cpp:
2024-08-09 11:45:57 +10:00
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:
2024-09-04 09:18:48 +10:00
sep = "^" if section_match.group("SUB") else "-"
2024-08-09 11:45:57 +10:00
line = re.sub(r"^\\(sub)?section \w+ ", "", line)
sep_line = re.sub(r"[\w ()]", sep, line)
line += f"\n{sep_line}"
# Convert ### style headings
2024-08-14 08:19:33 +10:00
heading_match = re.match(r"^###\s+(.*)$", line)
if heading_match:
line = f"{heading_match.group(1)}\n{'-' * (len(heading_match.group(1)) + 30)}"
2024-08-14 08:19:33 +10:00
heading_match = re.match(r"^##\s+(.*)$", line)
if heading_match:
line = f"{heading_match.group(1)}\n{'=' * (len(heading_match.group(1)) + 30)}"
2024-08-09 11:45:57 +10:00
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} "
2024-08-13 09:54:19 +10:00
elif not re.match(
r"^\s*[\\:]+(param|note|since|return|deprecated|warning|throws)", line
2024-08-13 10:53:37 +10:00
):
line = f"{CONTEXT.indent}{line}"
2024-08-09 11:45:57 +10:00
else:
CONTEXT.prev_indent = CONTEXT.indent
CONTEXT.indent = ""
2024-08-09 11:45:57 +10:00
# 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)
2024-08-09 11:45:57 +10:00
# 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
)
2024-08-09 11:45:57 +10:00
line = re.sub(r"\s*$", "", line)
CONTEXT.indent = " " * (line.index(":", 2) + 2)
2024-08-09 11:45:57 +10:00
if line.startswith(":param"):
if not CONTEXT.comment_param_list:
2024-08-09 11:45:57 +10:00
line = f"\n{line}"
CONTEXT.comment_param_list = True
CONTEXT.comment_last_line_note_warning = False
2024-08-09 11:45:57 +10:00
# Handle brief
2024-08-09 16:44:15 +10:00
if re.match(r"^\s*[\\@]brief", line):
2024-08-09 11:45:57 +10:00
line = re.sub(r"[\\@]brief\s*", "", line)
if CONTEXT.found_since:
2024-08-13 09:54:19 +10:00
exit_with_error(
f"{CONTEXT.header_file}::{CONTEXT.line_idx} Since annotation must come after brief"
)
CONTEXT.found_since = False
2024-08-09 11:45:57 +10:00
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 = ""
2024-08-09 11:45:57 +10:00
return ""
# Handle since
2024-08-09 16:44:15 +10:00
since_match = re.search(r"\\since .*?([\d.]+)", line, re.IGNORECASE)
2024-08-09 11:45:57 +10:00
if since_match:
CONTEXT.prev_indent = CONTEXT.indent
CONTEXT.indent = ""
CONTEXT.found_since = True
2024-08-09 11:45:57 +10:00
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>.*)?",
2024-08-13 09:54:19 +10:00
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"))
2024-08-09 11:45:57 +10:00
return create_class_links(depr_line)
# Handle see also
see_matches = list(
re.finditer(r"\\see +([\w:/.#-]+(\.\w+)*)(\([^()]*\))?(\.?)", line)
2024-11-29 14:26:30 +01:00
)
2024-08-11 07:44:31 +10:00
if see_matches:
for see_match in reversed(see_matches):
seealso = see_match.group(1)
seealso_suffix = see_match.group(4)
2024-08-11 07:44:31 +10:00
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 ''}"
2024-11-29 14:26:30 +01:00
)
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 ''}"
2024-11-29 14:26:30 +01:00
)
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"
2024-08-09 11:45:57 +10:00
else:
2024-08-11 07:44:31 +10:00
if seeline:
line = (
line[: see_match.start()]
+ seeline
+ seealso_suffix
2024-08-13 10:53:37 +10:00
+ line[see_match.end() :]
) # re.sub(r'\\see +(\w+(\.\w+)*(\(\))?)', seeline, line)
2024-08-11 07:44:31 +10:00
else:
line = line.replace("\\see", "see")
2024-08-09 16:44:15 +10:00
elif not re.search(r"\\throws.*", line):
2024-08-09 11:45:57 +10:00
line = create_class_links(line)
# Handle note, warning, and throws
2024-08-09 16:44:15 +10:00
note_match = re.search(r"[\\@]note (.*)", line)
2024-08-09 11:45:57 +10:00
if note_match:
CONTEXT.comment_last_line_note_warning = True
CONTEXT.prev_indent = CONTEXT.indent
CONTEXT.indent = ""
2024-08-09 11:45:57 +10:00
return f"\n.. note::\n\n {note_match.group(1)}\n"
2024-08-09 16:44:15 +10:00
warning_match = re.search(r"[\\@]warning (.*)", line)
2024-08-09 11:45:57 +10:00
if warning_match:
CONTEXT.prev_indent = CONTEXT.indent
CONTEXT.indent = ""
CONTEXT.comment_last_line_note_warning = True
2024-08-09 11:45:57 +10:00
return f"\n.. warning::\n\n {warning_match.group(1)}\n"
2024-08-09 16:44:15 +10:00
throws_match = re.search(r"[\\@]throws (.+?)\b\s*(.*)", line)
2024-08-09 11:45:57 +10:00
if throws_match:
CONTEXT.prev_indent = CONTEXT.indent
CONTEXT.indent = ""
CONTEXT.comment_last_line_note_warning = True
2024-08-09 11:45:57 +10:00
return f"\n:raises {throws_match.group(1)}: {throws_match.group(2)}\n"
if line.strip():
if CONTEXT.comment_last_line_note_warning:
2024-08-09 11:45:57 +10:00
dbg_info(f"prepend spaces for multiline warning/note xx{line}")
line = f" {line}"
else:
CONTEXT.comment_last_line_note_warning = False
2024-08-09 11:45:57 +10:00
return f"{line}\n"
def detect_and_remove_following_body_or_initializerlist():
2024-08-13 10:50:43 +10:00
global CONTEXT
2024-08-09 11:45:57 +10:00
2024-08-09 16:38:51 +10:00
signature = ""
2024-08-09 11:45:57 +10:00
# 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"
2024-08-13 10:50:43 +10:00
if (
re.match(pattern1, CONTEXT.current_line)
or re.search(pattern2, CONTEXT.current_line)
2024-08-13 10:53:37 +10:00
or re.match(pattern3, CONTEXT.current_line)
):
2024-08-09 11:45:57 +10:00
2024-08-13 09:54:19 +10:00
dbg_info(
"remove constructor definition, function bodies, member initializing list (1)"
)
2024-08-09 11:45:57 +10:00
# Extract the parts we want to keep
2024-08-14 08:19:33 +10:00
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)};"
2024-08-09 11:45:57 +10:00
else:
2024-08-13 10:50:43 +10:00
newline = CONTEXT.current_line
2024-08-09 11:45:57 +10:00
# Call remove_following_body_or_initializerlist() if necessary
2024-08-13 10:50:43 +10:00
if not re.search(r"{.*}(\s*SIP_\w+)*\s*(//.*)?$", CONTEXT.current_line):
2024-08-09 16:38:51 +10:00
signature = remove_following_body_or_initializerlist()
2024-08-09 11:45:57 +10:00
2024-08-13 10:50:43 +10:00
CONTEXT.current_line = newline
2024-08-09 11:45:57 +10:00
2024-08-09 16:38:51 +10:00
return signature
2024-08-09 11:45:57 +10:00
def remove_following_body_or_initializerlist():
global CONTEXT
2024-08-09 11:45:57 +10:00
2024-08-09 16:38:51 +10:00
signature = ""
2024-08-09 11:45:57 +10:00
2024-08-13 09:54:19 +10:00
dbg_info(
"remove constructor definition, function bodies, member initializing list (2)"
)
2024-08-09 11:45:57 +10:00
line = read_line()
# Python signature
if re.match(r"^\s*\[\s*(\w+\s*)?\(", line):
dbg_info("python signature detected")
2024-08-09 16:38:51 +10:00
_nesting_index = 0
while CONTEXT.line_idx < CONTEXT.line_count:
2024-08-09 16:38:51 +10:00
_nesting_index += line.count("[")
_nesting_index -= line.count("]")
if _nesting_index == 0:
2024-08-14 08:19:33 +10:00
line_match = re.match(r"^(.*);\s*(//.*)?$", line)
if line_match:
line = line_match.group(1) # remove semicolon (added later)
2024-08-09 16:38:51 +10:00
signature += f"\n{line}"
return signature
2024-08-09 11:45:57 +10:00
break
2024-08-09 16:38:51 +10:00
signature += f"\n{line}"
2024-08-09 11:45:57 +10:00
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):
2024-08-09 16:38:51 +10:00
_nesting_index = 0
while CONTEXT.line_idx < CONTEXT.line_count:
2024-08-09 11:45:57 +10:00
dbg_info(" remove body")
2024-08-09 16:38:51 +10:00
_nesting_index += line.count("{")
_nesting_index -= line.count("}")
if _nesting_index == 0:
2024-08-09 11:45:57 +10:00
break
line = read_line()
2024-08-09 16:38:51 +10:00
return signature
2024-08-09 11:45:57 +10:00
2024-08-12 20:46:27 +10:00
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
2024-08-14 09:36:36 +10:00
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}"
2024-08-09 11:45:57 +10:00
def fix_annotations(line):
global CONTEXT
2024-08-09 11:45:57 +10:00
# 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:
2024-08-09 11:45:57 +10:00
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]}")
2024-08-09 11:45:57 +10:00
2024-08-09 16:38:51 +10:00
_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]}")
2024-08-09 11:45:57 +10:00
# 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/"
2024-08-14 08:19:33 +10:00
for _pattern, replacement in replacements.items():
line = re.sub(_pattern, replacement, line)
2024-08-09 11:45:57 +10:00
# Combine multiple annotations
while True:
2024-08-13 09:54:19 +10:00
new_line = re.sub(
r'/([\w,]+(="?[^"]+"?)?)/\s*/([\w,]+(="?[^"]+"?)?]?)/', r"/\1,\3/", line
)
2024-08-09 11:45:57 +10:00
if new_line == line:
break
line = new_line
dbg_info("combine multiple annotations -- works only for 2")
# Unprinted annotations
2024-08-12 20:46:27 +10:00
line = replace_alternative_types(line)
2024-08-09 11:45:57 +10:00
line = re.sub(r"(\w+)\s+SIP_PYARGRENAME\(\s*(\w+)\s*\)", r"\2", line)
2024-08-09 17:42:09 +10:00
# 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)
2024-08-13 09:54:19 +10:00
line = re.sub(
r"""=\s+[^=]*?\s+SIP_PYARGDEFAULT\(\s*\'?([^()\']+)(\((?:[^()]|\([^()]*\))*\))?\'?\s*\)""",
r"= \1",
line,
)
2024-08-09 11:45:57 +10:00
# Remove argument
if "SIP_PYARGREMOVE" in line:
dbg_info("remove arg")
2024-08-13 10:53:37 +10:00
if CONTEXT.multiline_definition != MultiLineType.NotMultiline:
prev_line = CONTEXT.output.pop().rstrip()
2024-08-09 16:38:51 +10:00
# Update multi line status
parenthesis_balance = prev_line.count("(") - prev_line.count(")")
if parenthesis_balance == 1:
2024-08-13 10:53:37 +10:00
CONTEXT.multiline_definition = MultiLineType.NotMultiline
2024-08-09 16:38:51 +10:00
# Concatenate with above line to bring previous commas
line = f"{prev_line} {line.lstrip()}\n"
2024-08-09 11:45:57 +10:00
2024-08-10 07:45:46 +10:00
# original perl regex was:
# (?<coma>, +)?(const )?(\w+)(\<(?>[^<>]|(?4))*\>)?\s+[\w&*]+\s+SIP_PYARGREMOVE( = [^()]*(\(\s*(?:[^()]++|(?6))*\s*\))?)?(?(<coma>)|,?)//
2024-08-14 09:36:36 +10:00
if "SIP_PYARGREMOVE" in line:
line = remove_sip_pyargremove(line)
2024-08-09 11:45:57 +10:00
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)
2024-08-13 09:54:19 +10:00
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)
2024-08-09 11:45:57 +10:00
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
2024-08-13 10:50:43 +10:00
global CONTEXT
2024-08-09 11:45:57 +10:00
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 = []
2024-08-09 11:45:57 +10:00
2024-08-13 10:50:43 +10:00
if re.match(r"^\s*/\*", CONTEXT.current_line) or (
2024-08-13 10:53:37 +10:00
not strict_mode and "/*" in CONTEXT.current_line
):
2024-08-09 11:45:57 +10:00
dbg_info("found comment block")
2024-08-14 08:19:33 +10:00
CONTEXT.comment = process_doxygen_line(
2024-08-13 10:50:43 +10:00
re.sub(r"^\s*/\*(\*)?(.*?)\n?$", r"\2", CONTEXT.current_line)
2024-11-29 14:26:30 +01:00
)
CONTEXT.comment = re.sub(r"^\s*$", "", CONTEXT.comment)
2024-08-09 11:45:57 +10:00
2024-08-13 10:50:43 +10:00
while not re.search(r"\*/\s*(//.*?)?$", CONTEXT.current_line):
CONTEXT.current_line = read_line()
2024-08-14 08:19:33 +10:00
CONTEXT.comment += process_doxygen_line(
2024-08-13 10:50:43 +10:00
re.sub(r"\s*\*?(.*?)(/)?\n?$", r"\1", CONTEXT.current_line)
2024-11-29 14:26:30 +01:00
)
2024-08-09 11:45:57 +10:00
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)
2024-08-09 11:45:57 +10:00
return True
return False
def detect_non_method_member(line):
2024-08-09 17:03:52 +10:00
_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)
2024-08-09 11:45:57 +10:00
2024-08-28 11:14:23 +10:00
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:
2024-08-09 11:45:57 +10:00
CONTEXT.python_signature = ""
CONTEXT.actual_class = CONTEXT.classname[-1] if CONTEXT.classname else None
2024-08-13 10:50:43 +10:00
CONTEXT.current_line = read_line()
2024-08-09 11:45:57 +10:00
2024-08-13 10:50:43 +10:00
if re.match(r"^\s*(#define\s+)?SIP_IF_MODULE\(.*\)$", CONTEXT.current_line):
2024-08-09 11:45:57 +10:00
dbg_info("skipping SIP include condition macro")
continue
2024-08-13 10:50:43 +10:00
match = re.match(r"^(.*?)\s*//\s*cppcheck-suppress.*$", CONTEXT.current_line)
2024-08-09 11:45:57 +10:00
if match:
2024-08-13 10:50:43 +10:00
CONTEXT.current_line = match.group(1)
2024-08-09 11:45:57 +10:00
2024-08-13 10:50:43 +10:00
match = re.match(r"^\s*SIP_FEATURE\(\s*(\w+)\s*\)(.*)$", CONTEXT.current_line)
2024-08-09 11:45:57 +10:00
if match:
write_output("SF1", f"%Feature {match.group(1)}{match.group(2)}\n")
continue
2024-08-13 10:50:43 +10:00
match = re.match(r"^\s*SIP_PROPERTY\((.*)\)$", CONTEXT.current_line)
2024-08-09 11:45:57 +10:00
if match:
write_output("SF1", f"%Property({match.group(1)})\n")
continue
2024-08-13 10:50:43 +10:00
match = re.match(r"^\s*SIP_IF_FEATURE\(\s*(!?\w+)\s*\)(.*)$", CONTEXT.current_line)
2024-08-09 11:45:57 +10:00
if match:
write_output("SF2", f"%If ({match.group(1)}){match.group(2)}\n")
continue
2024-08-13 10:50:43 +10:00
match = re.match(r"^\s*SIP_CONVERT_TO_SUBCLASS_CODE(.*)$", CONTEXT.current_line)
2024-08-09 11:45:57 +10:00
if match:
2024-12-04 07:28:17 +01:00
# TYPE HEADER CODE
if CONTEXT.header_code and not re.match(r"^ *//.*$", CONTEXT.current_line):
CONTEXT.header_code = False
write_output("HCE", "%End\n")
2024-08-13 10:50:43 +10:00
CONTEXT.current_line = f"%ConvertToSubClassCode{match.group(1)}"
2024-08-09 11:45:57 +10:00
# Do not continue here, let the code process the next steps
2024-08-13 10:50:43 +10:00
match = re.match(r"^\s*SIP_VIRTUAL_CATCHER_CODE(.*)$", CONTEXT.current_line)
2024-08-09 11:45:57 +10:00
if match:
2024-08-13 10:50:43 +10:00
CONTEXT.current_line = f"%VirtualCatcherCode{match.group(1)}"
2024-08-09 11:45:57 +10:00
# Do not continue here, let the code process the next steps
2024-08-13 10:50:43 +10:00
match = re.match(r"^\s*SIP_END(.*)$", CONTEXT.current_line)
2024-08-09 11:45:57 +10:00
if match:
write_output("SEN", f"%End{match.group(1)}\n")
continue
2024-08-13 10:50:43 +10:00
match = re.search(r"SIP_WHEN_FEATURE\(\s*(.*?)\s*\)", CONTEXT.current_line)
2024-08-09 11:45:57 +10:00
if match:
dbg_info("found SIP_WHEN_FEATURE")
CONTEXT.if_feature_condition = match.group(1)
2024-08-09 11:45:57 +10:00
2024-12-05 15:58:53 +01:00
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:
2024-08-13 10:50:43 +10:00
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
)
2024-11-29 14:26:30 +01:00
2024-08-13 10:50:43 +10:00
if CONTEXT.is_qt6 and re.match(r"^\s*#ifdef SIP_PYQT5_RUN", CONTEXT.current_line):
2024-08-09 11:45:57 +10:00
dbg_info("do not process PYQT5 code")
2024-08-13 10:50:43 +10:00
while not re.match(r"^#endif", CONTEXT.current_line):
CONTEXT.current_line = read_line()
2024-08-09 11:45:57 +10:00
2024-08-13 10:50:43 +10:00
if not CONTEXT.is_qt6 and re.match(
r"^\s*#ifdef SIP_PYQT6_RUN", CONTEXT.current_line
):
2024-08-09 11:45:57 +10:00
dbg_info("do not process PYQT6 code")
2024-08-13 10:50:43 +10:00
while not re.match(r"^#endif", CONTEXT.current_line):
CONTEXT.current_line = read_line()
2024-08-09 11:45:57 +10:00
# Do not process SIP code %XXXCode
if CONTEXT.sip_run and re.match(
2024-12-03 14:31:12 +01:00
r"^ *[/]*% *(VirtualErrorHandler|MappedType|Type(?:Header)?Code|Module(?:Header)?Code|Convert(?:From|To)(?:Type|SubClass)Code|MethodCode|Docstring)(.*)?$",
2024-08-13 10:53:37 +10:00
CONTEXT.current_line,
):
2024-08-13 10:50:43 +10:00
CONTEXT.current_line = (
2024-12-03 14:31:12 +01:00
f"%{re.match(r'^ *[/]*% *(.*)$', CONTEXT.current_line).group(1)}"
2024-11-29 14:26:30 +01:00
)
CONTEXT.comment = ""
2024-08-09 11:45:57 +10:00
dbg_info("do not process SIP code")
2024-12-03 14:35:24 +01:00
while not re.match(r"^ *[/]*% *End", CONTEXT.current_line):
2024-08-13 10:50:43 +10:00
write_output("COD", CONTEXT.current_line + "\n")
CONTEXT.current_line = read_line()
if CONTEXT.is_qt6:
2024-08-13 10:50:43 +10:00
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
2024-11-29 14:26:30 +01:00
)
2024-08-13 10:50:43 +10:00
CONTEXT.current_line = re.sub(
2024-12-03 14:31:12 +01:00
r"^ *[/]*% *(VirtualErrorHandler|MappedType|Type(?:Header)?Code|Module(?:Header)?Code|Convert(?:From|To)(?:Type|SubClass)Code|MethodCode|Docstring)(.*)?$",
2024-11-29 14:26:30 +01:00
r"%\1\2",
2024-08-13 10:50:43 +10:00
CONTEXT.current_line,
)
CONTEXT.current_line = re.sub(
r"^\s*SIP_END(.*)$", r"%End\1", CONTEXT.current_line
)
2024-08-09 11:45:57 +10:00
2024-12-03 14:35:24 +01:00
CONTEXT.current_line = re.sub(r"^\s*[/]*% *End", "%End", CONTEXT.current_line)
2024-08-13 10:50:43 +10:00
write_output("COD", CONTEXT.current_line + "\n")
2024-08-09 11:45:57 +10:00
continue
# Do not process SIP code %Property
2024-12-03 14:35:24 +01:00
if CONTEXT.sip_run and re.match(
r"^ *[/]*% *(Property)(.*)?$", CONTEXT.current_line
):
2024-08-13 10:50:43 +10:00
CONTEXT.current_line = (
f"%{re.match(r'^ *% *(.*)$', CONTEXT.current_line).group(1)}"
2024-11-29 14:26:30 +01:00
)
CONTEXT.comment = ""
2024-08-13 10:50:43 +10:00
write_output("COD", CONTEXT.current_line + "\n")
2024-08-09 11:45:57 +10:00
continue
# Do not process SIP code %If %End
2024-12-03 14:35:24 +01:00
if CONTEXT.sip_run and re.match(r"^ *[/]*% *(If|End)(.*)?$", CONTEXT.current_line):
2024-08-13 10:50:43 +10:00
CONTEXT.current_line = (
f"%{re.match(r'^ *% (.*)$', CONTEXT.current_line).group(1)}"
2024-11-29 14:26:30 +01:00
)
CONTEXT.comment = ""
2024-08-13 10:50:43 +10:00
write_output("COD", CONTEXT.current_line)
2024-08-09 11:45:57 +10:00
continue
# Skip preprocessor directives
2024-08-13 10:50:43 +10:00
if re.match(r"^\s*#", CONTEXT.current_line):
2024-08-09 11:45:57 +10:00
# Skip #if 0 or #if defined(Q_OS_WIN) blocks
2024-08-13 10:50:43 +10:00
match = re.match(r"^\s*#if (0|defined\(Q_OS_WIN\))", CONTEXT.current_line)
2024-08-09 11:45:57 +10:00
if match:
dbg_info(f"skipping #if {match.group(1)} block")
nesting_index = 0
while CONTEXT.line_idx < CONTEXT.line_count:
2024-08-13 10:50:43 +10:00
CONTEXT.current_line = read_line()
if re.match(r"^\s*#if(def)?\s+", CONTEXT.current_line):
2024-08-09 11:45:57 +10:00
nesting_index += 1
2024-08-13 09:54:19 +10:00
elif nesting_index == 0 and re.match(
2024-08-13 10:50:43 +10:00
r"^\s*#(endif|else)", CONTEXT.current_line
):
CONTEXT.comment = ""
2024-08-09 11:45:57 +10:00
break
2024-08-13 10:50:43 +10:00
elif nesting_index != 0 and re.match(
r"^\s*#endif", CONTEXT.current_line
):
2024-08-09 11:45:57 +10:00
nesting_index -= 1
continue
2024-08-13 10:50:43 +10:00
if re.match(r"^\s*#ifdef SIP_RUN", CONTEXT.current_line):
CONTEXT.sip_run = True
if CONTEXT.access[-1] == Visibility.Private:
2024-08-09 17:19:08 +10:00
dbg_info("writing private content (1)")
if CONTEXT.private_section_line:
write_output("PRV1", CONTEXT.private_section_line + "\n")
CONTEXT.private_section_line = ""
2024-08-09 11:45:57 +10:00
continue
if CONTEXT.sip_run:
2024-08-13 10:50:43 +10:00
if re.match(r"^\s*#endif", CONTEXT.current_line):
if CONTEXT.ifdef_nesting_idx == 0:
CONTEXT.sip_run = False
2024-08-09 11:45:57 +10:00
continue
else:
CONTEXT.ifdef_nesting_idx -= 1
2024-08-09 11:45:57 +10:00
2024-08-13 10:50:43 +10:00
if re.match(r"^\s*#if(def)?\s+", CONTEXT.current_line):
CONTEXT.ifdef_nesting_idx += 1
2024-08-09 11:45:57 +10:00
# If there is an else at this level, code will be ignored (i.e., not SIP_RUN)
2024-08-13 10:50:43 +10:00
if (
re.match(r"^\s*#else", CONTEXT.current_line)
and CONTEXT.ifdef_nesting_idx == 0
):
while CONTEXT.line_idx < CONTEXT.line_count:
2024-08-13 10:50:43 +10:00
CONTEXT.current_line = read_line()
if re.match(r"^\s*#if(def)?\s+", CONTEXT.current_line):
CONTEXT.ifdef_nesting_idx += 1
2024-08-13 10:50:43 +10:00
elif re.match(r"^\s*#endif", CONTEXT.current_line):
if CONTEXT.ifdef_nesting_idx == 0:
CONTEXT.comment = ""
CONTEXT.sip_run = False
2024-08-09 11:45:57 +10:00
break
else:
CONTEXT.ifdef_nesting_idx -= 1
2024-08-09 11:45:57 +10:00
continue
2024-08-13 10:50:43 +10:00
elif re.match(r"^\s*#ifndef SIP_RUN", CONTEXT.current_line):
2024-08-09 11:45:57 +10:00
# Code is ignored here
while CONTEXT.line_idx < CONTEXT.line_count:
2024-08-13 10:50:43 +10:00
CONTEXT.current_line = read_line()
if re.match(r"^\s*#if(def)?\s+", CONTEXT.current_line):
CONTEXT.ifdef_nesting_idx += 1
2024-08-13 09:54:19 +10:00
elif (
2024-08-13 10:50:43 +10:00
re.match(r"^\s*#else", CONTEXT.current_line)
and CONTEXT.ifdef_nesting_idx == 0
):
2024-08-09 11:45:57 +10:00
# Code here will be printed out
if CONTEXT.access[-1] == Visibility.Private:
2024-08-09 17:19:08 +10:00
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
2024-08-09 11:45:57 +10:00
break
2024-08-13 10:50:43 +10:00
elif re.match(r"^\s*#endif", CONTEXT.current_line):
if CONTEXT.ifdef_nesting_idx == 0:
CONTEXT.sip_run = 0
2024-08-09 11:45:57 +10:00
break
else:
CONTEXT.ifdef_nesting_idx -= 1
2024-08-09 11:45:57 +10:00
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
2024-08-09 11:45:57 +10:00
write_output("HCE", "%End\n")
# Skip forward declarations
2024-08-13 09:54:19 +10:00
match = re.match(
r"^\s*(template ?<class T> |enum\s+)?(class|struct) \w+(?P<external> *SIP_EXTERNAL)?;\s*(//.*)?$",
2024-08-13 10:50:43 +10:00
CONTEXT.current_line,
)
2024-08-09 11:45:57 +10:00
if match:
if match.group("external"):
dbg_info("do not skip external forward declaration")
CONTEXT.comment = ""
2024-08-09 11:45:57 +10:00
else:
dbg_info("skipping forward declaration")
continue
# Skip friend declarations
2024-08-13 10:50:43 +10:00
if re.match(r"^\s*friend class \w+", CONTEXT.current_line):
2024-08-09 11:45:57 +10:00
continue
# Insert metaobject for Q_GADGET
2024-08-13 10:50:43 +10:00
if re.match(r"^\s*Q_GADGET\b.*?$", CONTEXT.current_line):
if not re.search(r"SIP_SKIP", CONTEXT.current_line):
2024-08-09 11:45:57 +10:00
dbg_info("Q_GADGET")
write_output("HCE", " public:\n")
2024-08-13 09:54:19 +10:00
write_output("HCE", " static const QMetaObject staticMetaObject;\n\n")
2024-08-09 11:45:57 +10:00
continue
# Insert in Python output (python/module/__init__.py)
2024-08-13 10:50:43 +10:00
match = re.search(r"Q_(ENUM|FLAG)\(\s*(\w+)\s*\)", CONTEXT.current_line)
2024-08-09 11:45:57 +10:00
if match:
2024-08-13 10:50:43 +10:00
if not re.search(r"SIP_SKIP", CONTEXT.current_line):
2024-08-09 11:45:57 +10:00
is_flag = 1 if match.group(1) == "FLAG" else 0
enum_helper = f"{CONTEXT.actual_class}.{match.group(2)}.baseClass = {CONTEXT.actual_class}"
2024-08-09 11:45:57 +10:00
dbg_info(f"Q_ENUM/Q_FLAG {enum_helper}")
if args.python_output:
if enum_helper != "":
CONTEXT.output_python.append(f"{enum_helper}\n")
2024-08-09 11:45:57 +10:00
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"
)
2024-08-09 11:45:57 +10:00
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.*?$",
2024-08-13 10:53:37 +10:00
CONTEXT.current_line,
):
2024-08-09 11:45:57 +10:00
continue
2024-08-13 10:50:43 +10:00
if re.match(r"^\s*QHASH_FOR_CLASS_ENUM", CONTEXT.current_line):
2024-08-09 11:45:57 +10:00
continue
2024-08-13 10:50:43 +10:00
if re.search(r"SIP_SKIP|SIP_PYTHON_SPECIAL_", CONTEXT.current_line):
2024-08-09 11:45:57 +10:00
dbg_info("SIP SKIP!")
# if multiline definition, remove previous lines
2024-08-13 10:53:37 +10:00
if CONTEXT.multiline_definition != MultiLineType.NotMultiline:
2024-08-09 11:45:57 +10:00
dbg_info("SIP_SKIP with MultiLine")
opening_line = ""
2024-08-13 09:54:19 +10:00
while not re.match(
r"^[^()]*\(([^()]*\([^()]*\)[^()]*)*[^()]*$", opening_line
):
opening_line = CONTEXT.output.pop()
if len(CONTEXT.output) < 1:
2024-08-09 11:45:57 +10:00
exit_with_error("could not reach opening definition")
dbg_info("removed multiline definition of SIP_SKIP method")
2024-08-13 10:53:37 +10:00
CONTEXT.multiline_definition = MultiLineType.NotMultiline
del CONTEXT.static_methods[CONTEXT.current_fully_qualified_class_name()][
CONTEXT.current_method_name
]
2024-08-09 11:45:57 +10:00
# also skip method body if there is one
detect_and_remove_following_body_or_initializerlist()
# line skipped, go to next iteration
2024-08-13 09:54:19 +10:00
match = re.search(
r'SIP_PYTHON_SPECIAL_(\w+)\(\s*(".*"|\w+)\s*\)', CONTEXT.current_line
2024-08-13 10:50:43 +10:00
)
2024-08-09 11:45:57 +10:00
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: "
2024-11-29 14:26:30 +01:00
)
2024-08-09 11:45:57 +10:00
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")
2024-08-09 11:45:57 +10:00
CONTEXT.comment = ""
2024-08-09 11:45:57 +10:00
continue
# Detect comment block
if detect_comment_block():
continue
2024-08-13 09:54:19 +10:00
struct_match = re.match(
2024-08-13 10:50:43 +10:00
r"^\s*struct(\s+\w+_EXPORT)?\s+(?P<structname>\w+)$", CONTEXT.current_line
)
2024-08-09 11:45:57 +10:00
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()
2024-11-29 14:26:30 +01:00
)
CONTEXT.access.append(Visibility.Public)
CONTEXT.exported.append(CONTEXT.exported[-1])
CONTEXT.bracket_nesting_idx.append(0)
2024-08-09 11:45:57 +10:00
# class declaration started
# https://regex101.com/r/KMQdF5/1 (older versions: https://regex101.com/r/6FWntP/16)
class_pattern = re.compile(
2024-08-09 16:38:51 +10:00
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*?(//.*|(?!;))$"""
2024-08-09 11:45:57 +10:00
)
2024-08-13 10:50:43 +10:00
class_pattern_match = class_pattern.match(CONTEXT.current_line)
2024-08-09 11:45:57 +10:00
if class_pattern_match:
dbg_info("class definition started")
CONTEXT.exported.append(0)
CONTEXT.bracket_nesting_idx.append(0)
2024-08-09 11:45:57 +10:00
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()
2024-11-29 14:26:30 +01:00
)
CONTEXT.access.append(Visibility.Public)
2024-08-09 11:45:57 +10:00
if len(CONTEXT.classname) == 1:
CONTEXT.declared_classes.append(CONTEXT.classname[-1])
2024-08-09 11:45:57 +10:00
dbg_info(f"class: {CONTEXT.classname[-1]}")
2024-08-09 11:45:57 +10:00
if (
2024-08-13 10:50:43 +10:00
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])
2024-08-09 11:45:57 +10:00
):
CONTEXT.exported[-1] += 1
2024-08-09 11:45:57 +10:00
2024-08-13 10:50:43 +10:00
CONTEXT.current_line = (
f"{class_pattern_match.group(1)} {class_pattern_match.group('classname')}"
2024-11-29 14:26:30 +01:00
)
2024-08-09 11:45:57 +10:00
# 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"
)
2024-08-09 11:45:57 +10:00
# 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|::)+)? *)?)? *>"
2024-11-29 14:26:30 +01:00
)
2024-08-09 11:45:57 +10:00
m = tpl_replace_pattern.sub(
2024-08-14 08:19:33 +10:00
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",
2024-11-29 14:26:30 +01:00
m,
2024-08-09 11:45:57 +10:00
)
m = re.sub(r"(\w+)< *(?:\w|::)+ *>", "", m)
m = re.sub(r"([:,])\s*,", r"\1", m)
m = re.sub(r"(\s*[:,])?\s*$", "", m)
2024-08-13 10:50:43 +10:00
CONTEXT.current_line += m
2024-08-09 11:45:57 +10:00
if class_pattern_match.group("annot"):
2024-08-13 10:50:43 +10:00
CONTEXT.current_line += class_pattern_match.group("annot")
CONTEXT.current_line = fix_annotations(CONTEXT.current_line)
2024-08-09 11:45:57 +10:00
2024-08-13 10:50:43 +10:00
CONTEXT.current_line += "\n{\n"
if CONTEXT.comment.strip():
2024-08-13 10:50:43 +10:00
CONTEXT.current_line += (
'%Docstring(signature="appended")\n' + CONTEXT.comment + "\n%End\n"
2024-11-29 14:26:30 +01:00
)
2024-08-09 11:45:57 +10:00
2024-08-13 10:50:43 +10:00
CONTEXT.current_line += (
f'\n%TypeHeaderCode\n#include "{os.path.basename(CONTEXT.header_file)}"'
2024-11-29 14:26:30 +01:00
)
2024-08-09 11:45:57 +10:00
# for template based inheritance, add a typedef to define the base type,
# since SIP doesn't allow inheriting from template classes directly
2024-08-09 11:45:57 +10:00
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}"
2024-08-09 11:45:57 +10:00
elif cls3 == "":
CONTEXT.current_line = f"\ntypedef {tpl}<{cls1},{cls2}> {tpl}{cls1}{cls2}Base /NoTypeName/;\n\n{CONTEXT.current_line}"
2024-08-09 11:45:57 +10:00
else:
CONTEXT.current_line = f"\ntypedef {tpl}<{cls1},{cls2},{cls3}> {tpl}{cls1}{cls2}{cls3}Base /NoTypeName/;\n\n{CONTEXT.current_line}"
2024-08-09 11:45:57 +10:00
if tpl not in CONTEXT.declared_classes:
2024-08-09 11:45:57 +10:00
tpl_header = f"{tpl.lower()}.h"
if tpl in sip_config["class_headerfile"]:
tpl_header = sip_config["class_headerfile"][tpl]
2024-08-13 10:50:43 +10:00
CONTEXT.current_line += f'\n#include "{tpl_header}"'
2024-08-09 11:45:57 +10:00
if cls2 == "":
2024-08-13 10:50:43 +10:00
CONTEXT.current_line += f"\ntypedef {tpl}<{cls1}> {tpl}{cls1}Base;"
2024-08-09 11:45:57 +10:00
elif cls3 == "":
2024-08-13 10:50:43 +10:00
CONTEXT.current_line += (
f"\ntypedef {tpl}<{cls1},{cls2}> {tpl}{cls1}{cls2}Base;"
2024-11-29 14:26:30 +01:00
)
2024-08-09 11:45:57 +10:00
else:
2024-08-13 10:50:43 +10:00
CONTEXT.current_line += f"\ntypedef {tpl}<{cls1},{cls2},{cls3}> {tpl}{cls1}{cls2}{cls3}Base;"
2024-08-09 11:45:57 +10:00
if (
any(x == Visibility.Private for x in CONTEXT.access)
2024-08-13 10:53:37 +10:00
and len(CONTEXT.access) != 1
):
2024-08-09 11:45:57 +10:00
dbg_info("skipping class in private context")
continue
CONTEXT.access[-1] = Visibility.Private # private by default
2024-08-13 10:50:43 +10:00
write_output("CLS", f"{CONTEXT.current_line}\n")
2024-08-09 11:45:57 +10:00
# 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
2024-08-09 11:45:57 +10:00
CONTEXT.comment = ""
CONTEXT.header_code = True
CONTEXT.access[-1] = Visibility.Private
2024-08-09 11:45:57 +10:00
continue
# Bracket balance in class/struct tree
if not CONTEXT.sip_run:
2024-08-09 11:45:57 +10:00
bracket_balance = 0
2024-08-13 10:50:43 +10:00
bracket_balance += CONTEXT.current_line.count("{")
bracket_balance -= CONTEXT.current_line.count("}")
2024-08-09 11:45:57 +10:00
if bracket_balance != 0:
CONTEXT.bracket_nesting_idx[-1] += bracket_balance
2024-08-09 11:45:57 +10:00
if CONTEXT.bracket_nesting_idx[-1] == 0:
2024-08-09 11:45:57 +10:00
dbg_info("going up in class/struct tree")
if len(CONTEXT.access) > 1:
CONTEXT.bracket_nesting_idx.pop()
CONTEXT.access.pop()
2024-08-09 11:45:57 +10:00
if CONTEXT.exported[-1] == 0 and CONTEXT.classname[
2024-08-13 10:53:37 +10:00
-1
] != sip_config.get("no_export_macro"):
2024-08-09 11:45:57 +10:00
exit_with_error(
f"Class {CONTEXT.classname[-1]} should be exported with appropriate [LIB]_EXPORT macro. "
2024-08-09 11:45:57 +10:00
f"If this should not be available in python, wrap it in a `#ifndef SIP_RUN` block."
)
CONTEXT.exported.pop()
2024-08-09 11:45:57 +10:00
if CONTEXT.classname:
CONTEXT.classname.pop()
CONTEXT.class_and_struct.pop()
2024-08-09 11:45:57 +10:00
if len(CONTEXT.access) == 1:
2024-08-09 11:45:57 +10:00
dbg_info("reached top level")
CONTEXT.access[-1] = (
2024-08-13 09:54:19 +10:00
Visibility.Public
) # Top level should stay public
2024-08-09 11:45:57 +10:00
CONTEXT.comment = ""
CONTEXT.return_type = ""
CONTEXT.private_section_line = ""
2024-08-09 11:45:57 +10:00
dbg_info(f"new bracket balance: {CONTEXT.bracket_nesting_idx}")
2024-08-09 11:45:57 +10:00
# Private members (exclude SIP_RUN)
2024-08-13 10:50:43 +10:00
if re.match(r"^\s*private( slots)?:", CONTEXT.current_line):
CONTEXT.access[-1] = Visibility.Private
2024-08-13 10:50:43 +10:00
CONTEXT.last_access_section_line = CONTEXT.current_line
CONTEXT.private_section_line = CONTEXT.current_line
CONTEXT.comment = ""
2024-08-09 11:45:57 +10:00
dbg_info("going private")
continue
elif re.match(r"^\s*(public( slots)?):.*$", CONTEXT.current_line):
2024-08-09 11:45:57 +10:00
dbg_info("going public")
2024-08-13 10:50:43 +10:00
CONTEXT.last_access_section_line = CONTEXT.current_line
CONTEXT.access[-1] = Visibility.Public
CONTEXT.comment = ""
2024-08-09 11:45:57 +10:00
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 = ""
2024-08-13 10:50:43 +10:00
elif re.match(r"^\s*(protected)( slots)?:.*$", CONTEXT.current_line):
2024-08-09 11:45:57 +10:00
dbg_info("going protected")
2024-08-13 10:50:43 +10:00
CONTEXT.last_access_section_line = CONTEXT.current_line
CONTEXT.access[-1] = Visibility.Protected
CONTEXT.comment = ""
2024-08-09 11:45:57 +10:00
2024-08-13 10:50:43 +10:00
elif (
2024-08-13 10:53:37 +10:00
CONTEXT.access[-1] == Visibility.Private and "SIP_FORCE" in CONTEXT.current_line
):
2024-08-09 11:45:57 +10:00
dbg_info("private with SIP_FORCE")
if CONTEXT.private_section_line:
write_output("PRV3", CONTEXT.private_section_line + "\n")
CONTEXT.private_section_line = ""
2024-08-09 11:45:57 +10:00
elif any(x == Visibility.Private for x in CONTEXT.access) and not CONTEXT.sip_run:
CONTEXT.comment = ""
2024-08-09 11:45:57 +10:00
continue
# Skip operators
if CONTEXT.access[-1] != Visibility.Private and re.search(
2024-08-13 10:53:37 +10:00
r"operator(=|<<|>>|->)\s*\(", CONTEXT.current_line
):
2024-08-09 11:45:57 +10:00
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:
2024-08-13 10:50:43 +10:00
if re.match(r"^\s*//", CONTEXT.current_line):
match = re.match(r"^\s*//!\s*(.*?)\n?$", CONTEXT.current_line)
2024-08-09 11:45:57 +10:00
if match:
CONTEXT.comment_param_list = False
CONTEXT.prev_indent = CONTEXT.indent
CONTEXT.indent = ""
CONTEXT.comment_last_line_note_warning = False
2024-08-14 08:19:33 +10:00
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 = ""
2024-08-09 11:45:57 +10:00
continue
# Handle Q_DECLARE_FLAGS in Qt6
if CONTEXT.is_qt6 and re.match(
2024-08-13 10:50:43 +10:00
r"^\s*Q_DECLARE_FLAGS\s*\(\s*(\w+)\s*,\s*(\w+)\s*\)", CONTEXT.current_line
2024-08-13 10:53:37 +10:00
):
2024-08-13 10:50:43 +10:00
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"
)
2024-08-09 11:45:57 +10:00
# 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*$",
2024-08-13 10:53:37 +10:00
CONTEXT.current_line,
):
2024-08-13 10:50:43 +10:00
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)
2024-08-13 09:54:19 +10:00
emkb = re.search(
r"SIP_MONKEYPATCH_FLAGS_UNNEST\s*\(\s*(\w+)\s*,\s*(\w+)\s*\)",
2024-08-13 10:50:43 +10:00
CONTEXT.current_line,
).group(1)
2024-08-13 09:54:19 +10:00
emkf = re.search(
r"SIP_MONKEYPATCH_FLAGS_UNNEST\s*\(\s*(\w+)\s*,\s*(\w+)\s*\)",
2024-08-13 10:50:43 +10:00
CONTEXT.current_line,
).group(2)
2024-08-09 11:45:57 +10:00
if f"{emkb}.{emkf}" != f"{CONTEXT.actual_class}.{flags_name}":
CONTEXT.output_python.append(
f"{emkb}.{emkf} = {CONTEXT.actual_class}.{flags_name}\n"
)
2024-08-09 11:45:57 +10:00
CONTEXT.enum_monkey_patched_types.append(
[CONTEXT.actual_class, flags_name, emkb, emkf]
)
2024-08-09 11:45:57 +10:00
2024-08-13 10:50:43 +10:00
CONTEXT.current_line = re.sub(
r"\s*SIP_MONKEYPATCH_FLAGS_UNNEST\(.*?\)", "", CONTEXT.current_line
)
2024-08-09 11:45:57 +10:00
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>.*)$",
2024-08-13 10:50:43 +10:00
CONTEXT.current_line,
)
2024-08-09 11:45:57 +10:00
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
2024-11-29 14:26:30 +01:00
)
2024-08-09 11:45:57 +10:00
if not isclass and enum_cpp_name not in ALLOWED_NON_CLASS_ENUMS:
2024-08-13 09:54:19 +10:00
exit_with_error(
f"Non class enum exposed to Python -- must be a enum class: {enum_cpp_name}"
)
2024-08-09 11:45:57 +10:00
oneliner = enum_match.group("oneliner")
2024-08-09 17:03:52 +10:00
is_scope_based = bool(isclass)
2024-08-09 11:45:57 +10:00
enum_decl = re.sub(r"\s*\bQ_DECL_DEPRECATED\b", "", enum_decl)
2024-08-13 09:54:19 +10:00
py_enum_type_match = re.search(
r"SIP_ENUM_BASETYPE\(\s*(.*?)\s*\)", CONTEXT.current_line
2024-08-13 10:50:43 +10:00
)
2024-08-13 09:54:19 +10:00
py_enum_type = py_enum_type_match.group(1) if py_enum_type_match else None
2024-08-09 11:45:57 +10:00
if py_enum_type == "IntFlag":
CONTEXT.enum_intflag_types.append(enum_cpp_name)
2024-08-09 11:45:57 +10:00
if enum_type in ["int", "quint32"]:
CONTEXT.enum_int_types.append(f"{CONTEXT.actual_class}.{enum_qualname}")
if CONTEXT.is_qt6:
2024-08-09 11:45:57 +10:00
enum_decl += f" /BaseType={py_enum_type or 'IntEnum'}/"
elif enum_type:
2024-08-13 09:54:19 +10:00
exit_with_error(f"Unhandled enum type {enum_type} for {enum_cpp_name}")
2024-08-09 11:45:57 +10:00
elif isclass:
CONTEXT.enum_class_non_int_types.append(
f"{CONTEXT.actual_class}.{enum_qualname}"
)
elif CONTEXT.is_qt6:
2024-08-09 11:45:57 +10:00
enum_decl += " /BaseType=IntEnum/"
write_output("ENU1", enum_decl)
if oneliner:
write_output("ENU1", oneliner)
write_output("ENU1", "\n")
2024-08-09 18:22:30 +10:00
_match = None
if is_scope_based:
_match = re.search(
2024-08-13 09:54:19 +10:00
r"SIP_MONKEYPATCH_SCOPEENUM(_UNNEST)?(:?\(\s*(?P<emkb>\w+)\s*,\s*(?P<emkf>\w+)\s*\))?",
2024-08-13 10:50:43 +10:00
CONTEXT.current_line,
)
2024-08-09 18:22:30 +10:00
monkeypatch = is_scope_based and _match
enum_mk_base = _match.group("emkb") if _match else ""
2024-08-09 11:45:57 +10:00
2024-08-09 16:38:51 +10:00
enum_old_name = ""
2024-08-09 18:22:30 +10:00
if _match and _match.group("emkf") and monkeypatch:
2024-08-09 18:23:05 +10:00
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"
)
2024-08-09 11:45:57 +10:00
else:
CONTEXT.output_python.append(
2024-08-13 09:54:19 +10:00
f"{enum_mk_base}.{enum_old_name} = {enum_qualname}\n"
)
2024-08-09 11:45:57 +10:00
2024-08-13 10:50:43 +10:00
if re.search(
r"\{((\s*\w+)(\s*=\s*[\w\s<|]+.*?)?(,?))+\s*}", CONTEXT.current_line
):
if "=" in CONTEXT.current_line:
2024-08-09 11:45:57 +10:00
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:
2024-08-13 10:50:43 +10:00
CONTEXT.current_line = read_line()
if not re.match(r"^\s*\{\s*$", CONTEXT.current_line):
2024-08-13 09:54:19 +10:00
exit_with_error("Unexpected content: enum should be followed by {")
2024-08-13 10:50:43 +10:00
write_output("ENU2", f"{CONTEXT.current_line}\n")
2024-08-09 11:45:57 +10:00
2024-08-09 17:03:52 +10:00
if is_scope_based:
CONTEXT.output_python.append("# monkey patching scoped based enum\n")
2024-08-09 11:45:57 +10:00
enum_members_doc = []
while CONTEXT.line_idx < CONTEXT.line_count:
2024-08-13 10:50:43 +10:00
CONTEXT.current_line = read_line()
2024-08-09 11:45:57 +10:00
if detect_comment_block():
continue
2024-08-13 10:50:43 +10:00
if re.search(r"};", CONTEXT.current_line):
2024-08-09 11:45:57 +10:00
break
2024-08-13 09:54:19 +10:00
if re.match(
2024-08-13 10:50:43 +10:00
r"^\s*\w+\s*\|", CONTEXT.current_line
): # multi line declaration as sum of enums
2024-08-09 11:45:57 +10:00
continue
enum_match = re.match(
2024-08-14 08:19:33 +10:00
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>.*)|.*)$",
2024-08-13 10:50:43 +10:00
CONTEXT.current_line,
)
2024-11-29 14:26:30 +01:00
2024-08-13 10:50:43 +10:00
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
2024-11-29 14:26:30 +01:00
)
2024-08-13 09:54:19 +10:00
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 ""
2024-11-29 14:26:30 +01:00
)
2024-08-13 09:54:19 +10:00
enum_value = enum_match.group("enum_value") or "" if enum_match else ""
2024-11-29 14:26:30 +01:00
2024-08-13 09:54:19 +10:00
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 )?(.*)",
2024-08-13 09:54:19 +10:00
r"\\n.. deprecated:: \1\\n",
value_comment,
flags=re.I,
)
2024-08-09 19:45:01 +10:00
value_comment = re.sub(r"^\\n+", "", value_comment)
value_comment = re.sub(r"\\n+$", "", value_comment)
2024-08-13 09:54:19 +10:00
dbg_info(
f"is_scope_based:{is_scope_based} enum_mk_base:{enum_mk_base} monkeypatch:{monkeypatch}"
)
2024-08-09 19:45:01 +10:00
2024-08-13 09:54:19 +10:00
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
):
2024-08-09 19:45:01 +10:00
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"
)
2024-08-09 19:45:01 +10:00
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"
2024-11-18 15:38:52 +01:00
complete_class_path = ".".join(CONTEXT.classname)
2024-08-09 19:45:01 +10:00
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(
2024-11-18 15:38:52 +01:00
f"{enum_mk_base}.{compat_name} = {complete_class_path}.{enum_qualname}.{enum_member}\n"
)
2024-08-09 19:45:01 +10:00
if enum_old_name and compat_name != enum_member:
CONTEXT.output_python.append(
2024-11-18 15:38:52 +01:00
f"{enum_mk_base}.{enum_old_name}.{compat_name} = {complete_class_path}.{enum_qualname}.{enum_member}\n"
)
CONTEXT.output_python.append(
2024-08-13 09:54:19 +10:00
f"{enum_mk_base}.{compat_name}.is_monkey_patched = True\n"
)
CONTEXT.output_python.append(
2024-08-13 09:54:19 +10:00
f'{enum_mk_base}.{compat_name}.__doc__ = "{value_comment}"\n'
)
2024-08-09 19:45:01 +10:00
enum_members_doc.append(
f"* ``{enum_member}``: {value_comment_indented}"
)
2024-08-09 11:45:57 +10:00
else:
CONTEXT.output_python.append(
2024-08-13 09:54:19 +10:00
f"{enum_mk_base}.{compat_name} = {enum_qualname}.{enum_member}\n"
)
CONTEXT.output_python.append(
2024-08-13 09:54:19 +10:00
f"{enum_mk_base}.{compat_name}.is_monkey_patched = True\n"
)
CONTEXT.output_python.append(
2024-08-13 09:54:19 +10:00
f'{enum_mk_base}.{compat_name}.__doc__ = "{value_comment}"\n'
)
2024-08-09 19:45:01 +10:00
enum_members_doc.append(
f"* ``{enum_member}``: {value_comment_indented}"
)
2024-08-09 19:45:01 +10:00
else:
if compat_name != enum_member:
value_comment_indented += f"\n\n Available as ``{CONTEXT.actual_class}.{compat_name}`` in older QGIS releases.\n"
2024-08-09 19:45:01 +10:00
if monkeypatch:
CONTEXT.output_python.append(
2024-11-18 15:38:52 +01:00
f"{complete_class_path}.{compat_name} = {complete_class_path}.{enum_qualname}.{enum_member}\n"
)
CONTEXT.output_python.append(
2024-11-18 15:38:52 +01:00
f"{complete_class_path}.{compat_name}.is_monkey_patched = True\n"
)
if CONTEXT.actual_class:
CONTEXT.output_python.append(
2024-08-09 19:45:01 +10:00
f'{complete_class_path}.{enum_qualname}.{compat_name}.__doc__ = "{value_comment}"\n'
)
enum_members_doc.append(
f"* ``{enum_member}``: {value_comment_indented}"
)
2024-08-09 19:45:01 +10:00
else:
CONTEXT.output_python.append(
2024-08-13 09:54:19 +10:00
f'{enum_qualname}.{compat_name}.__doc__ = "{value_comment}"\n'
)
2024-08-09 19:45:01 +10:00
enum_members_doc.append(
f"* ``{enum_member}``: {value_comment_indented}"
)
2024-08-09 19:45:01 +10:00
if not is_scope_based and CONTEXT.is_qt6 and enum_member:
basename = ".".join(CONTEXT.class_and_struct)
2024-08-09 19:45:01 +10:00
if basename:
enum_member = "None_" if enum_member == "None" else enum_member
CONTEXT.output_python.append(
2024-08-09 19:45:01 +10:00
f"{basename}.{enum_member} = {basename}.{enum_qualname}.{enum_member}\n"
)
2024-08-09 11:45:57 +10:00
2024-08-09 19:45:01 +10:00
enum_decl = fix_annotations(enum_decl)
write_output("ENU3", f"{enum_decl}\n")
2024-08-09 11:45:57 +10:00
detect_comment_block(strict_mode=False)
2024-08-13 10:50:43 +10:00
write_output("ENU4", f"{CONTEXT.current_line}\n")
2024-08-09 11:45:57 +10:00
2024-08-09 17:55:04 +10:00
if is_scope_based:
enum_member_doc_string = "\n".join(enum_members_doc)
if CONTEXT.actual_class:
CONTEXT.output_python.append(
2024-11-18 15:22:24 +01:00
f'{".".join(CONTEXT.classname)}.{enum_qualname}.__doc__ = """{CONTEXT.comment}\n\n{enum_member_doc_string}\n\n"""\n# --\n'
)
2024-08-09 11:45:57 +10:00
else:
CONTEXT.output_python.append(
f'{enum_qualname}.__doc__ = """{CONTEXT.comment}\n\n{enum_member_doc_string}\n\n"""\n# --\n'
)
2024-08-09 11:45:57 +10:00
# enums don't have Docstring apparently
CONTEXT.comment = ""
2024-08-09 11:45:57 +10:00
continue
# Check for invalid use of doxygen command
2024-08-13 10:50:43 +10:00
if re.search(r".*//!<", CONTEXT.current_line):
2024-08-13 09:54:19 +10:00
exit_with_error(
'"\\!<" doxygen command must only be used for enum documentation'
)
2024-08-09 11:45:57 +10:00
# Handle override, final, and make private keywords
2024-08-13 10:50:43 +10:00
if re.search(r"\boverride\b", CONTEXT.current_line):
CONTEXT.is_override_or_make_private = PrependType.Virtual
2024-08-13 10:50:43 +10:00
if re.search(r"\bFINAL\b", CONTEXT.current_line):
CONTEXT.is_override_or_make_private = PrependType.Virtual
2024-08-13 10:50:43 +10:00
if re.search(r"\bSIP_MAKE_PRIVATE\b", CONTEXT.current_line):
CONTEXT.is_override_or_make_private = PrependType.MakePrivate
2024-08-09 11:45:57 +10:00
# Remove Q_INVOKABLE
2024-08-13 10:50:43 +10:00
CONTEXT.current_line = re.sub(r"^(\s*)Q_INVOKABLE ", r"\1", CONTEXT.current_line)
2024-08-09 11:45:57 +10:00
# Keyword fixes
2024-08-13 10:50:43 +10:00
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+>)(.*)$",
2024-08-13 09:54:19 +10:00
r"\1\2,\3\4",
2024-08-13 10:50:43 +10:00
CONTEXT.current_line,
2024-11-29 14:26:30 +01:00
)
2024-08-13 10:50:43 +10:00
CONTEXT.current_line = re.sub(
2024-08-13 09:54:19 +10:00
r"^(\s*template\s*<)(?:class|typename) (\w+) *, *(?:class|typename) (\w+) *, *(?:class|typename) (\w+>)(.*)$",
r"\1\2,\3,\4\5",
2024-08-13 10:50:43 +10:00
CONTEXT.current_line,
2024-11-29 14:26:30 +01:00
)
2024-08-13 10:50:43 +10:00
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)
2024-08-09 11:45:57 +10:00
# Handle export macros
2024-08-13 10:50:43 +10:00
if re.search(r"\b\w+_EXPORT\b", CONTEXT.current_line):
CONTEXT.exported[-1] += 1
2024-08-13 10:50:43 +10:00
CONTEXT.current_line = re.sub(r"\b\w+_EXPORT\s+", "", CONTEXT.current_line)
2024-08-09 11:45:57 +10:00
# Skip non-method member declaration in non-public sections
if (
not CONTEXT.sip_run
2024-08-13 10:53:37 +10:00
and CONTEXT.access[-1] != Visibility.Public
and detect_non_method_member(CONTEXT.current_line)
):
2024-08-09 11:45:57 +10:00
dbg_info("skip non-method member declaration in non-public sections")
continue
# Remove static const value assignment
# https://regex101.com/r/DyWkgn/6
2024-08-13 10:50:43 +10:00
if re.search(r"^\s*const static \w+", CONTEXT.current_line):
2024-08-13 09:54:19 +10:00
exit_with_error(
f"const static should be written static const in {CONTEXT.classname[-1]}"
)
2024-08-09 11:45:57 +10:00
# TODO needs fixing!!
2024-08-09 17:42:09 +10:00
# original perl regex was:
# ^(?<staticconst> *(?<static>static )?const (\w+::)*\w+(?:<(?:[\w<>, ]|::)+>)? \w+)(?: = [^()]+?(\((?:[^()]++|(?3))*\))?[^()]*?)?(?<endingchar>[|;]) *(\/\/.*?)?$
2024-08-09 17:42:09 +10:00
match = re.search(
r"^(?P<staticconst> *(?P<static>static )?const (\w+::)*\w+(?:<(?:[\w<>, ]|::)+>)? \w+)(?: = [^()]+?(\((?:[^()]|\([^()]*\))*\))?[^()]*?)?(?P<endingchar>[|;]) *(//.*)?$",
2024-08-13 10:50:43 +10:00
CONTEXT.current_line,
2024-08-09 11:45:57 +10:00
)
if match:
2024-08-13 10:50:43 +10:00
CONTEXT.current_line = f"{match.group('staticconst')};"
2024-08-09 11:45:57 +10:00
if match.group("static") is None:
CONTEXT.comment = ""
2024-08-09 11:45:57 +10:00
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:
2024-08-10 15:59:58 +10:00
# original perl regex: ^(\s*\w+[\w<> *&:,]* \*?\w+) = ([\-\w\:\.]+(< *\w+( \*)? *>)?)+(\([^()]*\))?\s*;
2024-08-13 10:50:43 +10:00
# dbg_info(f"attempt struct member assignment '{CONTEXT.current_line}'")
2024-08-10 15:59:58 +10:00
python_regex_verbose = r"""
^ # Start of the line
2024-08-10 17:51:01 +10:00
( # Start of capturing group for the left-hand side
\s* # Optional leading whitespace
2024-08-10 15:59:58 +10:00
(?: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
2024-08-10 17:51:01 +10:00
-? # 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
)
2024-08-10 15:59:58 +10:00
) # 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
2024-11-29 14:26:30 +01:00
"""
2024-08-13 09:54:19 +10:00
regex_verbose = re.compile(python_regex_verbose, re.VERBOSE | re.MULTILINE)
2024-08-13 10:50:43 +10:00
match = regex_verbose.match(CONTEXT.current_line)
2024-08-09 11:45:57 +10:00
if match:
2024-08-10 15:59:58 +10:00
dbg_info(f"remove struct member assignment '={match.group(2)}'")
2024-08-13 10:50:43 +10:00
CONTEXT.current_line = f"{match.group(1)};"
2024-08-09 11:45:57 +10:00
# Catch Q_DECLARE_FLAGS
2024-08-13 09:54:19 +10:00
match = re.search(
r"^(\s*)Q_DECLARE_FLAGS\(\s*(.*?)\s*,\s*(.*?)\s*\)\s*$", CONTEXT.current_line
2024-08-13 10:50:43 +10:00
)
2024-08-09 11:45:57 +10:00
if match:
CONTEXT.actual_class = (
f"{CONTEXT.classname[-1]}::" if len(CONTEXT.classname) >= 0 else ""
)
dbg_info(f"Declare flags: {CONTEXT.actual_class}")
2024-08-13 10:50:43 +10:00
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)}"
2024-11-29 14:26:30 +01:00
)
if f"{CONTEXT.actual_class}{match.group(3)}" not in CONTEXT.enum_intflag_types:
2024-08-09 11:45:57 +10:00
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"
)
2024-08-09 11:45:57 +10:00
# Catch Q_DECLARE_OPERATORS_FOR_FLAGS
2024-08-13 09:54:19 +10:00
match = re.search(
2024-08-13 10:50:43 +10:00
r"^(\s*)Q_DECLARE_OPERATORS_FOR_FLAGS\(\s*(.*?)\s*\)\s*$", CONTEXT.current_line
)
2024-08-09 11:45:57 +10:00
if match:
flags = match.group(2)
flag = CONTEXT.qflag_hash.get(flags)
if flag is None:
exit_with_error(f"error reading flags: {flags}")
2024-08-13 10:50:43 +10:00
CONTEXT.current_line = (
f"{match.group(1)}QFlags<{flag}> operator|({flag} f1, QFlags<{flag}> f2);\n"
2024-11-29 14:26:30 +01:00
)
2024-08-09 11:45:57 +10:00
py_flag = flag.replace("::", ".")
if py_flag in CONTEXT.enum_class_non_int_types:
2024-08-09 11:45:57 +10:00
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:
2024-08-09 11:45:57 +10:00
dbg_info("monkey patching operators for non-class enum")
if not CONTEXT.has_pushed_force_int:
CONTEXT.output_python.append(
2024-08-09 11:45:57 +10:00
"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(
2024-08-13 09:54:19 +10:00
f"{py_flag}.__bool__ = lambda flag: bool(_force_int(flag))\n"
)
CONTEXT.output_python.append(
2024-08-09 11:45:57 +10:00
f"{py_flag}.__eq__ = lambda flag1, flag2: _force_int(flag1) == _force_int(flag2)\n"
)
CONTEXT.output_python.append(
2024-08-09 11:45:57 +10:00
f"{py_flag}.__and__ = lambda flag1, flag2: _force_int(flag1) & _force_int(flag2)\n"
)
CONTEXT.output_python.append(
2024-08-09 11:45:57 +10:00
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:
2024-08-09 11:45:57 +10:00
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(
2024-08-09 11:45:57 +10:00
"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(
2024-08-09 11:45:57 +10:00
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:
2024-08-09 11:45:57 +10:00
# Handle multiline definition to add virtual keyword or make private on opening line
2024-08-13 10:53:37 +10:00
if CONTEXT.multiline_definition != MultiLineType.NotMultiline:
2024-08-13 10:50:43 +10:00
rolling_line = CONTEXT.current_line
rolling_line_idx = CONTEXT.line_idx
2024-08-13 09:54:19 +10:00
dbg_info(
"handle multiline definition to add virtual keyword or making private on opening line"
)
while not re.match(
r"^[^()]*\(([^()]*\([^()]*\)[^()]*)*[^()]*$", rolling_line
):
2024-08-09 11:45:57 +10:00
rolling_line_idx -= 1
rolling_line = CONTEXT.input_lines[rolling_line_idx]
2024-08-09 11:45:57 +10:00
if rolling_line_idx < 0:
exit_with_error("could not reach opening definition")
2024-08-10 07:35:36 +10:00
dbg_info(f"rolled back to {rolling_line_idx}: {rolling_line}")
2024-08-09 11:45:57 +10:00
if (
CONTEXT.is_override_or_make_private == PrependType.Virtual
2024-08-13 09:54:19 +10:00
and not re.match(r"^(\s*)virtual\b(.*)$", rolling_line)
2024-08-13 10:53:37 +10:00
):
idx = rolling_line_idx - CONTEXT.line_idx + 1
CONTEXT.output[idx] = fix_annotations(
2024-08-13 09:54:19 +10:00
re.sub(r"^(\s*?)\b(.*)$", r"\1 virtual \2\n", rolling_line)
)
elif CONTEXT.is_override_or_make_private == PrependType.MakePrivate:
2024-08-09 11:45:57 +10:00
dbg_info("prepending private access")
idx = rolling_line_idx - CONTEXT.line_idx
2024-08-13 09:54:19 +10:00
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:
2024-08-09 11:45:57 +10:00
dbg_info("prepending private access")
2024-08-13 10:50:43 +10:00
CONTEXT.current_line = (
re.sub(
r"(protected|public)", "private", CONTEXT.last_access_section_line
)
+ "\n"
+ CONTEXT.current_line
+ "\n"
2024-11-29 14:26:30 +01:00
)
elif (
CONTEXT.is_override_or_make_private == PrependType.Virtual
2024-08-13 10:53:37 +10:00
and not re.match(r"^(\s*)virtual\b(.*)$", CONTEXT.current_line)
):
2024-08-09 11:45:57 +10:00
# 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")
2024-08-13 10:50:43 +10:00
CONTEXT.current_line = re.sub(
r"^(\s*?)\b(.*)$", r"\1virtual \2\n", CONTEXT.current_line
)
2024-08-09 11:45:57 +10:00
# remove constructor definition, function bodies, member initializing list
CONTEXT.python_signature = detect_and_remove_following_body_or_initializerlist()
2024-08-09 11:45:57 +10:00
# remove inline declarations
2024-08-10 17:52:00 +10:00
match = re.search(
2024-08-14 08:16:22 +10:00
r"^(\s*)?(static |const )*(([(?:long )\w]+(<.*?>)?\s+([*&])?)?(\w+)( const*?)*)\s*(\{.*});(\s*//.*)?$",
2024-08-13 10:50:43 +10:00
CONTEXT.current_line,
)
2024-08-09 11:45:57 +10:00
if match:
2024-08-13 10:50:43 +10:00
CONTEXT.current_line = f"{match.group(1)}{match.group(3)};"
2024-08-09 11:45:57 +10:00
2024-08-28 11:14:23 +10:00
pattern = r"^\s*((?:const |virtual |static |inline ))*(?!explicit)([(?:long )\w:]+(?:<.*?>)?)\s+(?:\*|&)?(\w+|operator.{1,2})(\(.*)$"
2024-08-13 10:50:43 +10:00
match = re.match(pattern, CONTEXT.current_line)
2024-08-09 11:45:57 +10:00
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
2024-08-28 11:14:23 +10:00
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)
2024-11-29 14:26:30 +01:00
)
2024-08-28 11:14:23 +10:00
if ");" in match.group(4):
CONTEXT.signal_arguments[class_name][CONTEXT.current_method_name] = (
CONTEXT.current_signal_args[:]
2024-11-29 14:26:30 +01:00
)
2024-08-28 11:14:23 +10:00
dbg_info(
"SIGARG finalizing"
+ CONTEXT.current_method_name
+ " "
+ str(CONTEXT.current_signal_args)
2024-11-29 14:26:30 +01:00
)
2024-08-13 09:54:19 +10:00
if not re.search(
r"(void|SIP_PYOBJECT|operator|return|QFlag)", return_type_candidate
):
2024-08-09 11:45:57 +10:00
# replace :: with . (changes c++ style namespace/class directives to Python style)
CONTEXT.return_type = return_type_candidate.replace("::", ".")
2024-08-09 11:45:57 +10:00
# 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
)
2024-11-29 14:26:30 +01:00
2024-08-13 09:54:19 +10:00
list_match = re.match(
r"^(?:QList|QVector)<\s*(.*?)[\s*]*>$", CONTEXT.return_type
)
2024-08-09 11:45:57 +10:00
if list_match:
CONTEXT.return_type = f"list of {list_match.group(1)}"
2024-08-09 11:45:57 +10:00
set_match = re.match(r"^QSet<\s*(.*?)[\s*]*>$", CONTEXT.return_type)
2024-08-09 11:45:57 +10:00
if set_match:
CONTEXT.return_type = f"set of {set_match.group(1)}"
2024-08-28 11:14:23 +10:00
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)
2024-11-29 14:26:30 +01:00
)
2024-08-28 11:14:23 +10:00
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[:]
2024-11-29 14:26:30 +01:00
)
2024-08-28 11:14:23 +10:00
dbg_info(
"SIGARG finalizing"
+ CONTEXT.current_method_name
+ " "
+ str(CONTEXT.current_signal_args)
2024-11-29 14:26:30 +01:00
)
2024-08-09 11:45:57 +10:00
# deleted functions
if re.match(
2024-08-09 16:38:51 +10:00
r"^(\s*)?(const )?(virtual |static )?((\w+(<.*?>)?\s+([*&])?)?(\w+|operator.{1,2})\(.*?(\(.*\))*.*\)( const)?)\s*= delete;(\s*//.*)?$",
2024-08-13 10:53:37 +10:00
CONTEXT.current_line,
):
CONTEXT.comment = ""
2024-08-09 11:45:57 +10:00
continue
# remove export macro from struct definition
2024-08-13 10:50:43 +10:00
CONTEXT.current_line = re.sub(
r"^(\s*struct )\w+_EXPORT (.+)$", r"\1\2", CONTEXT.current_line
)
2024-08-09 11:45:57 +10:00
# Skip comments
2024-08-13 09:54:19 +10:00
if re.match(
r"^\s*typedef\s+\w+\s*<\s*\w+\s*>\s+\w+\s+.*SIP_DOC_TEMPLATE",
2024-08-13 10:50:43 +10:00
CONTEXT.current_line,
):
2024-08-09 11:45:57 +10:00
# support Docstring for template based classes in SIP 4.19.7+
CONTEXT.comment_template_docstring = True
2024-08-13 10:53:37 +10:00
elif CONTEXT.multiline_definition == MultiLineType.NotMultiline and (
2024-08-13 10:50:43 +10:00
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)
2024-08-13 10:53:37 +10:00
):
dbg_info(f"skipping comment for {CONTEXT.current_line}")
2024-08-13 10:50:43 +10:00
if re.search(r"\s*typedef.*?(?!SIP_DOC_TEMPLATE)", CONTEXT.current_line):
2024-08-09 11:45:57 +10:00
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
2024-08-09 11:45:57 +10:00
2024-08-13 10:50:43 +10:00
CONTEXT.current_line = fix_constants(CONTEXT.current_line)
CONTEXT.current_line = fix_annotations(CONTEXT.current_line)
2024-08-09 11:45:57 +10:00
# fix astyle placing space after % character
2024-08-13 10:50:43 +10:00
CONTEXT.current_line = re.sub(
r"/\s+GetWrapper\s+/", "/GetWrapper/", CONTEXT.current_line
)
2024-08-09 11:45:57 +10:00
# MISSING
# handle enum/flags QgsSettingsEntryEnumFlag
2024-08-13 09:54:19 +10:00
match = re.match(
r"^(\s*)const QgsSettingsEntryEnumFlag<(.*)> (.+);$", CONTEXT.current_line
2024-08-13 10:50:43 +10:00
)
2024-08-09 11:45:57 +10:00
if match:
CONTEXT.indent, enum_type, var_name = match.groups()
2024-08-09 11:45:57 +10:00
prep_line = f"""class QgsSettingsEntryEnumFlag_{var_name}
{{
%TypeHeaderCode
#include "{os.path.basename(CONTEXT.header_file)}"
2024-08-09 11:45:57 +10:00
#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;
}};"""
2024-08-13 10:50:43 +10:00
CONTEXT.current_line = (
f"{CONTEXT.indent}const QgsSettingsEntryEnumFlag_{var_name} {var_name};"
2024-11-29 14:26:30 +01:00
)
CONTEXT.comment = ""
2024-08-09 11:45:57 +10:00
write_output("ENF", f"{prep_line}\n", "prepend")
2024-08-13 10:50:43 +10:00
write_output("NOR", f"{CONTEXT.current_line}\n")
2024-08-09 11:45:57 +10:00
# append to class map file
if args.class_map and CONTEXT.actual_class:
2024-08-13 09:54:19 +10:00
match = re.match(
r"^ *(const |virtual |static )* *[\w:]+ +\*?(?P<method>\w+)\(.*$",
2024-08-13 10:50:43 +10:00
CONTEXT.current_line,
)
2024-08-09 11:45:57 +10:00
if match:
with open(args.class_map, "a") as f:
2024-08-13 09:54:19 +10:00
f.write(
f"{'.'.join(CONTEXT.classname)}.{match.group('method')}: {CONTEXT.header_file}#L{CONTEXT.line_idx}\n"
)
2024-08-09 11:45:57 +10:00
if CONTEXT.python_signature:
write_output("PSI", f"{CONTEXT.python_signature}\n")
2024-08-09 11:45:57 +10:00
# multiline definition (parenthesis left open)
2024-08-13 10:53:37 +10:00
if CONTEXT.multiline_definition != MultiLineType.NotMultiline:
2024-08-09 11:45:57 +10:00
dbg_info("on multiline")
# https://regex101.com/r/DN01iM/4
# TODO - original regex is incompatible with python -- it was:
# ^([^()]+(\((?:[^()]++|(?1))*\)))*[^()]*\)([^()](throw\([^()]+\))?)*$:
2024-08-13 09:54:19 +10:00
if re.match(
r"^([^()]+(\((?:[^()]|\([^()]*\))*\)))*[^()]*\)([^()](throw\([^()]+\))?)*",
2024-08-13 10:53:37 +10:00
CONTEXT.current_line,
):
2024-08-09 11:45:57 +10:00
dbg_info("ending multiline")
# remove potential following body
2024-08-13 10:53:37 +10:00
if (
CONTEXT.multiline_definition != MultiLineType.ConditionalStatement
2024-08-13 09:54:19 +10:00
and not re.search(r"(\{.*}|;)\s*(//.*)?$", CONTEXT.current_line)
2024-08-13 10:53:37 +10:00
):
2024-08-09 11:45:57 +10:00
dbg_info("remove following body of multiline def")
2024-08-13 10:50:43 +10:00
last_line = CONTEXT.current_line
2024-08-09 11:45:57 +10:00
last_line += remove_following_body_or_initializerlist()
# add missing semi column
CONTEXT.output.pop()
2024-08-09 11:45:57 +10:00
write_output("MLT", f"{last_line};\n")
2024-08-13 10:53:37 +10:00
CONTEXT.multiline_definition = MultiLineType.NotMultiline
2024-08-09 11:45:57 +10:00
else:
continue
2024-08-13 10:50:43 +10:00
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):
2024-08-13 10:53:37 +10:00
CONTEXT.multiline_definition = MultiLineType.ConditionalStatement
2024-08-09 11:45:57 +10:00
else:
2024-08-13 10:53:37 +10:00
CONTEXT.multiline_definition = MultiLineType.Method
2024-08-09 11:45:57 +10:00
continue
# write comment
2024-08-13 10:50:43 +10:00
if re.match(r"^\s*$", CONTEXT.current_line):
2024-08-09 11:45:57 +10:00
dbg_info("no more override / private")
CONTEXT.is_override_or_make_private = PrependType.NoPrepend
2024-08-09 11:45:57 +10:00
continue
2024-08-13 10:50:43 +10:00
if re.match(r"^\s*template\s*<.*>", CONTEXT.current_line):
2024-08-09 11:45:57 +10:00
# 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()
):
2024-08-09 11:45:57 +10:00
# overridden method with no new docs - so don't create a Docstring and use
# parent class Docstring
pass
else:
2024-08-14 08:16:22 +10:00
dbg_info("writing comment")
if CONTEXT.comment.strip():
2024-08-09 11:45:57 +10:00
dbg_info("comment non-empty")
doc_prepend = (
"@DOCSTRINGSTEMPLATE@" if CONTEXT.comment_template_docstring else ""
2024-11-29 14:26:30 +01:00
)
2024-08-09 11:45:57 +10:00
write_output("CM1", f"{doc_prepend}%Docstring\n")
doc_string = ""
comment_lines = CONTEXT.comment.split("\n")
2024-08-09 11:45:57 +10:00
skipping_param = 0
out_params = []
waiting_for_return_to_end = False
2024-09-03 16:46:38 +10:00
comment_line_idx = 0
while comment_line_idx < len(comment_lines):
comment_line = comment_lines[comment_line_idx]
comment_line_idx += 1
2024-08-13 09:54:19 +10:00
if (
2024-08-13 10:53:37 +10:00
"versionadded:" in comment_line or "deprecated:" in comment_line
) and out_params:
2024-08-09 11:45:57 +10:00
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"
2024-08-09 11:45:57 +10:00
for out_param in out_params:
doc_string += f"{doc_prepend} - {out_param}\n"
doc_string += f"{doc_prepend}\n"
2024-08-09 11:45:57 +10:00
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"
)
2024-08-13 09:54:19 +10:00
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*",
2024-08-13 09:54:19 +10:00
"",
comment_line,
)
2024-08-09 11:45:57 +10:00
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
2024-08-13 09:54:19 +10:00
comment_line = comment_line.replace(":return:", ":return: -")
doc_string += f"{doc_prepend}{comment_line}\n"
2024-09-03 16:46:38 +10:00
# scan forward to find end of return description
scan_forward_idx = comment_line_idx
2024-09-04 07:31:17 +10:00
needs_blank_line_after_return = False
2024-09-03 16:46:38 +10:00
while scan_forward_idx < len(comment_lines):
scan_forward_line = comment_lines[scan_forward_idx]
scan_forward_idx += 1
2024-09-04 07:31:17 +10:00
if (
not scan_forward_line.strip()
and scan_forward_idx < len(comment_lines) - 1
):
# check if following line is start of list
2024-09-04 08:13:12 +10:00
if re.match(
r"^\s*-(?!-)", comment_lines[scan_forward_idx + 1]
):
2024-09-04 07:31:17 +10:00
doc_string += "\n"
comment_line_idx += 1
needs_blank_line_after_return = True
continue
2024-09-03 16:46:38 +10:00
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
2024-09-04 07:31:17 +10:00
if needs_blank_line_after_return:
doc_string += "\n"
2024-08-09 11:45:57 +10:00
for out_param in out_params:
doc_string += f"{doc_prepend} - {out_param}\n"
2024-08-09 11:45:57 +10:00
out_params = []
else:
doc_string += f"{doc_prepend}{comment_line}\n"
2024-08-09 11:45:57 +10:00
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
2024-09-10 11:46:47 +10:00
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"
2024-11-29 14:26:30 +01:00
)
2024-09-10 11:46:47 +10:00
else:
doc_string += f"{doc_prepend} - {out_param}\n"
2024-08-09 11:45:57 +10:00
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
2024-08-09 11:45:57 +10:00
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
2024-08-09 11:45:57 +10:00
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
2024-08-09 11:45:57 +10:00
# 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))
2024-08-09 11:45:57 +10:00
f.write("".join(sip_header_footer()))
else:
2024-08-12 20:11:19 +10:00
print(
"".join(sip_header_footer())
+ "".join(CONTEXT.output)
2024-08-12 20:15:12 +10:00
+ "".join(sip_header_footer()).rstrip()
2024-11-29 14:26:30 +01:00
)
2024-08-09 11:45:57 +10:00
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})"
)
2024-08-28 11:14:23 +10:00
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:
2024-08-09 11:45:57 +10:00
with open(args.python_output, "w") as f:
f.write("".join(python_header()))
f.write("".join(CONTEXT.output_python))