Use Enum for visibility

This commit is contained in:
Nyall Dawson 2024-08-13 09:54:19 +10:00
parent 52f186007f
commit cc8bf97092

View File

@ -1,17 +1,23 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import argparse
import os
import re import re
import sys import sys
import os from enum import Enum, auto
import argparse
import yaml
from typing import List, Dict, Any from typing import List, Dict, Any
import yaml
class Visibility(Enum):
Private = auto()
Protected = auto()
Public = auto()
# Constants # Constants
PRIVATE = 0
PROTECTED = 1
PUBLIC = 2
STRICT = 10 STRICT = 10
UNSTRICT = 11 UNSTRICT = 11
MULTILINE_NO = 20 MULTILINE_NO = 20
@ -27,7 +33,8 @@ PREPEND_CODE_MAKE_PRIVATE = 42
LINE = '' LINE = ''
# Parse command-line arguments # Parse command-line arguments
parser = argparse.ArgumentParser(description="Convert header file to SIP and Python") parser = argparse.ArgumentParser(
description="Convert header file to SIP and Python")
parser.add_argument("-debug", action="store_true", help="Enable debug mode") parser.add_argument("-debug", action="store_true", help="Enable debug mode")
parser.add_argument("-qt6", action="store_true", help="Enable Qt6 mode") parser.add_argument("-qt6", action="store_true", help="Enable Qt6 mode")
parser.add_argument("-sip_output", help="SIP output file") parser.add_argument("-sip_output", help="SIP output file")
@ -44,7 +51,8 @@ try:
with open(headerfile, "r") as f: with open(headerfile, "r") as f:
input_lines = f.read().splitlines() input_lines = f.read().splitlines()
except IOError as e: except IOError as e:
print(f"Couldn't open '{headerfile}' for reading because: {e}", file=sys.stderr) print(f"Couldn't open '{headerfile}' for reading because: {e}",
file=sys.stderr)
sys.exit(1) sys.exit(1)
# Read configuration # Read configuration
@ -53,13 +61,14 @@ try:
with open(cfg_file, 'r') as f: with open(cfg_file, 'r') as f:
sip_config = yaml.safe_load(f) sip_config = yaml.safe_load(f)
except IOError as e: except IOError as e:
print(f"Couldn't open configuration file '{cfg_file}' because: {e}", file=sys.stderr) print(f"Couldn't open configuration file '{cfg_file}' because: {e}",
file=sys.stderr)
sys.exit(1) sys.exit(1)
# Initialize contexts # Initialize contexts
sip_run = False sip_run = False
header_code = False header_code = False
access = [PUBLIC] access: List[Visibility] = [Visibility.Public]
classname: List[str] = [] classname: List[str] = []
class_and_struct: List[str] = [] class_and_struct: List[str] = []
declared_classes: List[str] = [] declared_classes: List[str] = []
@ -525,7 +534,8 @@ def replace_macros(line):
if is_qt6: if is_qt6:
# sip for Qt6 chokes on QList/QVector<QVariantMap>, but is happy if you expand out the map explicitly # sip for Qt6 chokes on QList/QVector<QVariantMap>, but is happy if you expand out the map explicitly
line = re.sub(r'(QList<\s*|QVector<\s*)QVariantMap', r'\1QMap<QString, QVariant>', line) line = re.sub(r'(QList<\s*|QVector<\s*)QVariantMap',
r'\1QMap<QString, QVariant>', line)
return line return line
@ -572,12 +582,14 @@ def dbg_info(info):
if debug == 1: if debug == 1:
output.append(f"{info}\n") output.append(f"{info}\n")
print(f"{line_idx} {len(access)} {sip_run} {multiline_definition} {info}") print(
f"{line_idx} {len(access)} {sip_run} {multiline_definition} {info}")
def exit_with_error(message): def exit_with_error(message):
global headerfile, line_idx global headerfile, line_idx
sys.exit(f"! Sipify error in {headerfile} at line :: {line_idx}\n! {message}") sys.exit(
f"! Sipify error in {headerfile} at line :: {line_idx}\n! {message}")
def sip_header_footer(): def sip_header_footer():
@ -588,13 +600,19 @@ def sip_header_footer():
# and over there we have to use ./3d/X.h entries because SIP parser does not allow a number # 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 # as the first letter of a relative path
headerfile_x = re.sub(r'src/core/3d', r'src/core/./3d', headerfile) headerfile_x = re.sub(r'src/core/3d', r'src/core/./3d', headerfile)
header_footer.append("/************************************************************************\n") header_footer.append(
header_footer.append(" * This file has been generated automatically from *\n") "/************************************************************************\n")
header_footer.append(" * *\n") header_footer.append(
" * This file has been generated automatically from *\n")
header_footer.append(
" * *\n")
header_footer.append(f" * {headerfile_x:<68} *\n") header_footer.append(f" * {headerfile_x:<68} *\n")
header_footer.append(" * *\n") header_footer.append(
header_footer.append(" * Do not edit manually ! Edit header and run scripts/sipify.py again *\n") " * *\n")
header_footer.append(" ************************************************************************/\n") header_footer.append(
" * Do not edit manually ! Edit header and run scripts/sipify.py again *\n")
header_footer.append(
" ************************************************************************/\n")
return header_footer return header_footer
@ -614,22 +632,27 @@ def create_class_links(line):
_match = re.search(r'\b(Qgs[A-Z]\w+|Qgis)\b(\.?$|\W{2})', line) _match = re.search(r'\b(Qgs[A-Z]\w+|Qgis)\b(\.?$|\W{2})', line)
if _match: if _match:
if actual_class and _match.group(1) != actual_class: if actual_class and _match.group(1) != actual_class:
line = re.sub(r'\b(Qgs[A-Z]\w+)\b(\.?$|\W{2})', r':py:class:`\1`\2', line) line = re.sub(r'\b(Qgs[A-Z]\w+)\b(\.?$|\W{2})',
r':py:class:`\1`\2', line)
# Replace Qgs class methods with :py:func: links # Replace Qgs class methods with :py:func: links
line = re.sub(r'\b((Qgs[A-Z]\w+|Qgis)\.[a-z]\w+\(\))(?!\w)', r':py:func:`\1`', line) line = re.sub(r'\b((Qgs[A-Z]\w+|Qgis)\.[a-z]\w+\(\))(?!\w)',
r':py:func:`\1`', line)
# Replace other methods with :py:func: links # Replace other methods with :py:func: links
if actual_class: if actual_class:
line = re.sub(r'(?<!\.)\b([a-z]\w+)\(\)(?!\w)', rf':py:func:`~{actual_class}.\1`', line) line = re.sub(r'(?<!\.)\b([a-z]\w+)\(\)(?!\w)',
rf':py:func:`~{actual_class}.\1`', line)
else: else:
line = re.sub(r'(?<!\.)\b([a-z]\w+)\(\)(?!\w)', r':py:func:`~\1`', line) line = re.sub(r'(?<!\.)\b([a-z]\w+)\(\)(?!\w)', r':py:func:`~\1`',
line)
# Replace Qgs classes (but not the current class) with :py:class: links # Replace Qgs classes (but not the current class) with :py:class: links
_match = re.search(r'\b(?<![`~])(Qgs[A-Z]\w+|Qgis)\b(?!\()', line) _match = re.search(r'\b(?<![`~])(Qgs[A-Z]\w+|Qgis)\b(?!\()', line)
if _match: if _match:
if not actual_class or _match.group(1) != actual_class: if not actual_class or _match.group(1) != actual_class:
line = re.sub(r'\b(?<![`~])(Qgs[A-Z]\w+|Qgis)\b(?!\()', r':py:class:`\1`', line) line = re.sub(r'\b(?<![`~])(Qgs[A-Z]\w+|Qgis)\b(?!\()',
r':py:class:`\1`', line)
return line return line
@ -707,7 +730,9 @@ def processDoxygenLine(line):
if re.match(r'^\s*[\-#]', line): if re.match(r'^\s*[\-#]', line):
line = f"{prev_indent}{line}" line = f"{prev_indent}{line}"
indent = f"{prev_indent} " indent = f"{prev_indent} "
elif not re.match(r'^\s*[\\:]+(param|note|since|return|deprecated|warning|throws)', line): elif not re.match(
r'^\s*[\\:]+(param|note|since|return|deprecated|warning|throws)',
line):
line = f"{indent}{line}" line = f"{indent}{line}"
else: else:
prev_indent = indent prev_indent = indent
@ -734,7 +759,8 @@ def processDoxygenLine(line):
if re.match(r'^\s*[\\@]brief', line): if re.match(r'^\s*[\\@]brief', line):
line = re.sub(r'[\\@]brief\s*', '', line) line = re.sub(r'[\\@]brief\s*', '', line)
if found_since: if found_since:
exit_with_error(f"{headerfile}::{line_idx} Since annotation must come after brief") exit_with_error(
f"{headerfile}::{line_idx} Since annotation must come after brief")
found_since = False found_since = False
if re.match(r'^\s*$', line): if re.match(r'^\s*$', line):
return "" return ""
@ -755,7 +781,8 @@ def processDoxygenLine(line):
# Handle deprecated # Handle deprecated
deprecated_match = re.search( deprecated_match = re.search(
r'\\deprecated(?:\s+since\s+QGIS\s+(?P<DEPR_VERSION>[0-9.]+)(,\s*)?)?(?P<DEPR_MESSAGE>.*)?', line, r'\\deprecated(?:\s+since\s+QGIS\s+(?P<DEPR_VERSION>[0-9.]+)(,\s*)?)?(?P<DEPR_MESSAGE>.*)?',
line,
re.IGNORECASE) re.IGNORECASE)
if deprecated_match: if deprecated_match:
prev_indent = indent prev_indent = indent
@ -841,7 +868,8 @@ def detect_and_remove_following_body_or_initializerlist():
re.search(pattern2, LINE) or re.search(pattern2, LINE) or
re.match(pattern3, LINE)): re.match(pattern3, LINE)):
dbg_info("remove constructor definition, function bodies, member initializing list (1)") dbg_info(
"remove constructor definition, function bodies, member initializing list (1)")
# Extract the parts we want to keep # Extract the parts we want to keep
_match = re.match(pattern1, LINE) _match = re.match(pattern1, LINE)
@ -864,7 +892,8 @@ def remove_following_body_or_initializerlist():
signature = '' signature = ''
dbg_info("remove constructor definition, function bodies, member initializing list (2)") dbg_info(
"remove constructor definition, function bodies, member initializing list (2)")
line = read_line() line = read_line()
# Python signature # Python signature
@ -967,7 +996,9 @@ def fix_annotations(line):
# Combine multiple annotations # Combine multiple annotations
while True: while True:
new_line = re.sub(r'/([\w,]+(="?[\w, \[\]]+"?)?)/\s*/([\w,]+(="?[\w, \[\]]+"?)?]?)/', r'/\1,\3/', line) new_line = re.sub(
r'/([\w,]+(="?[\w, \[\]]+"?)?)/\s*/([\w,]+(="?[\w, \[\]]+"?)?]?)/',
r'/\1,\3/', line)
if new_line == line: if new_line == line:
break break
line = new_line line = new_line
@ -979,7 +1010,9 @@ def fix_annotations(line):
# Note: this was the original perl regex, which isn't compatible with Python: # Note: this was the original perl regex, which isn't compatible with Python:
# line = re.sub(r"""=\s+[^=]*?\s+SIP_PYARGDEFAULT\(\s*\'?([^()']+)(\(\s*(?:[^()]++|(?2))*\s*\))?\'?\s*\)""", r'= \1', line) # line = re.sub(r"""=\s+[^=]*?\s+SIP_PYARGDEFAULT\(\s*\'?([^()']+)(\(\s*(?:[^()]++|(?2))*\s*\))?\'?\s*\)""", r'= \1', line)
line = re.sub(r"""=\s+[^=]*?\s+SIP_PYARGDEFAULT\(\s*\'?([^()\']+)(\((?:[^()]|\([^()]*\))*\))?\'?\s*\)""", r'= \1', line = re.sub(
r"""=\s+[^=]*?\s+SIP_PYARGDEFAULT\(\s*\'?([^()\']+)(\((?:[^()]|\([^()]*\))*\))?\'?\s*\)""",
r'= \1',
line) line)
# Remove argument # Remove argument
@ -1019,10 +1052,14 @@ def fix_annotations(line):
def fix_constants(line): def fix_constants(line):
line = re.sub(r'\bstd::numeric_limits<double>::max\(\)', 'DBL_MAX', line) line = re.sub(r'\bstd::numeric_limits<double>::max\(\)', 'DBL_MAX', line)
line = re.sub(r'\bstd::numeric_limits<double>::lowest\(\)', '-DBL_MAX', line) line = re.sub(r'\bstd::numeric_limits<double>::lowest\(\)', '-DBL_MAX',
line = re.sub(r'\bstd::numeric_limits<double>::epsilon\(\)', 'DBL_EPSILON', line) line)
line = re.sub(r'\bstd::numeric_limits<qlonglong>::min\(\)', 'LLONG_MIN', line) line = re.sub(r'\bstd::numeric_limits<double>::epsilon\(\)', 'DBL_EPSILON',
line = re.sub(r'\bstd::numeric_limits<qlonglong>::max\(\)', 'LLONG_MAX', line) line)
line = re.sub(r'\bstd::numeric_limits<qlonglong>::min\(\)', 'LLONG_MIN',
line)
line = re.sub(r'\bstd::numeric_limits<qlonglong>::max\(\)', 'LLONG_MAX',
line)
line = re.sub(r'\bstd::numeric_limits<int>::max\(\)', 'INT_MAX', line) line = re.sub(r'\bstd::numeric_limits<int>::max\(\)', 'INT_MAX', line)
line = re.sub(r'\bstd::numeric_limits<int>::min\(\)', 'INT_MIN', line) line = re.sub(r'\bstd::numeric_limits<int>::min\(\)', 'INT_MIN', line)
return line return line
@ -1043,12 +1080,14 @@ def detect_comment_block(strict_mode=True):
if re.match(r'^\s*/\*', LINE) or (not strict_mode and '/*' in LINE): if re.match(r'^\s*/\*', LINE) or (not strict_mode and '/*' in LINE):
dbg_info("found comment block") dbg_info("found comment block")
comment = processDoxygenLine(re.sub(r'^\s*/\*(\*)?(.*?)\n?$', r'\2', LINE)) comment = processDoxygenLine(
re.sub(r'^\s*/\*(\*)?(.*?)\n?$', r'\2', LINE))
comment = re.sub(r'^\s*$', '', comment) comment = re.sub(r'^\s*$', '', comment)
while not re.search(r'\*/\s*(//.*?)?$', LINE): while not re.search(r'\*/\s*(//.*?)?$', LINE):
LINE = read_line() LINE = read_line()
comment += processDoxygenLine(re.sub(r'\s*\*?(.*?)(/)?\n?$', r'\1', LINE)) comment += processDoxygenLine(
re.sub(r'\s*\*?(.*?)(/)?\n?$', r'\1', LINE))
comment = re.sub(r'\n\s+\n', '\n\n', comment) comment = re.sub(r'\n\s+\n', '\n\n', comment)
comment = re.sub(r'\n{3,}', '\n\n', comment) comment = re.sub(r'\n{3,}', '\n\n', comment)
@ -1115,7 +1154,8 @@ while line_idx < line_count:
if is_qt6: if is_qt6:
LINE = re.sub(r'int\s*__len__\s*\(\s*\)', 'Py_ssize_t __len__()', LINE) LINE = re.sub(r'int\s*__len__\s*\(\s*\)', 'Py_ssize_t __len__()', LINE)
LINE = re.sub(r'long\s*__hash__\s*\(\s*\)', 'Py_hash_t __hash__()', LINE) LINE = re.sub(r'long\s*__hash__\s*\(\s*\)', 'Py_hash_t __hash__()',
LINE)
if is_qt6 and re.match(r'^\s*#ifdef SIP_PYQT5_RUN', LINE): if is_qt6 and re.match(r'^\s*#ifdef SIP_PYQT5_RUN', LINE):
dbg_info("do not process PYQT5 code") dbg_info("do not process PYQT5 code")
@ -1174,7 +1214,8 @@ while line_idx < line_count:
LINE = read_line() LINE = read_line()
if re.match(r'^\s*#if(def)?\s+', LINE): if re.match(r'^\s*#if(def)?\s+', LINE):
nesting_index += 1 nesting_index += 1
elif nesting_index == 0 and re.match(r'^\s*#(endif|else)', LINE): elif nesting_index == 0 and re.match(r'^\s*#(endif|else)',
LINE):
comment = '' comment = ''
break break
elif nesting_index != 0 and re.match(r'^\s*#endif', LINE): elif nesting_index != 0 and re.match(r'^\s*#endif', LINE):
@ -1183,7 +1224,7 @@ while line_idx < line_count:
if re.match(r'^\s*#ifdef SIP_RUN', LINE): if re.match(r'^\s*#ifdef SIP_RUN', LINE):
sip_run = True sip_run = True
if access[-1] == PRIVATE: if access[-1] == Visibility.Private:
dbg_info("writing private content (1)") dbg_info("writing private content (1)")
if private_section_line: if private_section_line:
write_output("PRV1", private_section_line + "\n") write_output("PRV1", private_section_line + "\n")
@ -1222,9 +1263,10 @@ while line_idx < line_count:
LINE = read_line() LINE = read_line()
if re.match(r'^\s*#if(def)?\s+', LINE): if re.match(r'^\s*#if(def)?\s+', LINE):
glob_ifdef_nesting_idx += 1 glob_ifdef_nesting_idx += 1
elif re.match(r'^\s*#else', LINE) and glob_ifdef_nesting_idx == 0: elif re.match(r'^\s*#else',
LINE) and glob_ifdef_nesting_idx == 0:
# Code here will be printed out # Code here will be printed out
if access[-1] == PRIVATE: if access[-1] == Visibility.Private:
dbg_info("writing private content (2)") dbg_info("writing private content (2)")
if private_section_line != '': if private_section_line != '':
write_output("PRV2", private_section_line + "\n") write_output("PRV2", private_section_line + "\n")
@ -1248,7 +1290,8 @@ while line_idx < line_count:
write_output("HCE", "%End\n") write_output("HCE", "%End\n")
# Skip forward declarations # Skip forward declarations
match = re.match(r'^\s*(template ?<class T> |enum\s+)?(class|struct) \w+(?P<external> *SIP_EXTERNAL)?;\s*(//.*)?$', match = re.match(
r'^\s*(template ?<class T> |enum\s+)?(class|struct) \w+(?P<external> *SIP_EXTERNAL)?;\s*(//.*)?$',
LINE) LINE)
if match: if match:
if match.group('external'): if match.group('external'):
@ -1267,7 +1310,8 @@ while line_idx < line_count:
if not re.search(r'SIP_SKIP', LINE): if not re.search(r'SIP_SKIP', LINE):
dbg_info('Q_GADGET') dbg_info('Q_GADGET')
write_output("HCE", " public:\n") write_output("HCE", " public:\n")
write_output("HCE", " static const QMetaObject staticMetaObject;\n\n") write_output("HCE",
" static const QMetaObject staticMetaObject;\n\n")
continue continue
# Insert in Python output (python/module/__init__.py) # Insert in Python output (python/module/__init__.py)
@ -1302,7 +1346,8 @@ while line_idx < line_count:
if multiline_definition != MULTILINE_NO: if multiline_definition != MULTILINE_NO:
dbg_info('SIP_SKIP with MultiLine') dbg_info('SIP_SKIP with MultiLine')
opening_line = '' opening_line = ''
while not re.match(r'^[^()]*\(([^()]*\([^()]*\)[^()]*)*[^()]*$', opening_line): while not re.match(r'^[^()]*\(([^()]*\([^()]*\)[^()]*)*[^()]*$',
opening_line):
opening_line = output.pop() opening_line = output.pop()
if len(output) < 1: if len(output) < 1:
exit_with_error('could not reach opening definition') exit_with_error('could not reach opening definition')
@ -1313,7 +1358,8 @@ while line_idx < line_count:
detect_and_remove_following_body_or_initializerlist() detect_and_remove_following_body_or_initializerlist()
# line skipped, go to next iteration # line skipped, go to next iteration
match = re.search(r'SIP_PYTHON_SPECIAL_(\w+)\(\s*(".*"|\w+)\s*\)', LINE) match = re.search(r'SIP_PYTHON_SPECIAL_(\w+)\(\s*(".*"|\w+)\s*\)',
LINE)
if match: if match:
method_or_code = match.group(2) method_or_code = match.group(2)
dbg_info(f"PYTHON SPECIAL method or code: {method_or_code}") dbg_info(f"PYTHON SPECIAL method or code: {method_or_code}")
@ -1333,13 +1379,14 @@ while line_idx < line_count:
if detect_comment_block(): if detect_comment_block():
continue continue
struct_match = re.match(r'^\s*struct(\s+\w+_EXPORT)?\s+(?P<structname>\w+)$', LINE) struct_match = re.match(
r'^\s*struct(\s+\w+_EXPORT)?\s+(?P<structname>\w+)$', LINE)
if struct_match: if struct_match:
dbg_info(" going to struct => public") dbg_info(" going to struct => public")
class_and_struct.append(struct_match.group('structname')) class_and_struct.append(struct_match.group('structname'))
classname.append(classname[-1] if classname else struct_match.group( classname.append(classname[-1] if classname else struct_match.group(
'structname')) # fake new class since struct has considered similarly 'structname')) # fake new class since struct has considered similarly
access.append(PUBLIC) access.append(Visibility.Public)
exported.append(exported[-1]) exported.append(exported[-1])
glob_bracket_nesting_idx.append(0) glob_bracket_nesting_idx.append(0)
@ -1352,7 +1399,7 @@ while line_idx < line_count:
if class_pattern_match: if class_pattern_match:
dbg_info("class definition started") dbg_info("class definition started")
access.append(PUBLIC) access.append(Visibility.Public)
exported.append(0) exported.append(0)
glob_bracket_nesting_idx.append(0) glob_bracket_nesting_idx.append(0)
template_inheritance_template = [] template_inheritance_template = []
@ -1452,11 +1499,11 @@ while line_idx < line_count:
else: else:
LINE += f"\ntypedef {tpl}<{cls1},{cls2},{cls3}> {tpl}{cls1}{cls2}{cls3}Base;" LINE += f"\ntypedef {tpl}<{cls1},{cls2},{cls3}> {tpl}{cls1}{cls2}{cls3}Base;"
if any(x == PRIVATE for x in access) and len(access) != 1: if any(x == Visibility.Private for x in access) and len(access) != 1:
dbg_info("skipping class in private context") dbg_info("skipping class in private context")
continue continue
access[-1] = PRIVATE # private by default access[-1] = Visibility.Private # private by default
write_output("CLS", f"{LINE}\n") write_output("CLS", f"{LINE}\n")
# Skip opening curly bracket, incrementing hereunder # Skip opening curly bracket, incrementing hereunder
@ -1467,7 +1514,7 @@ while line_idx < line_count:
comment = '' comment = ''
header_code = True header_code = True
access[-1] = PRIVATE access[-1] = Visibility.Private
continue continue
# Bracket balance in class/struct tree # Bracket balance in class/struct tree
@ -1486,7 +1533,8 @@ while line_idx < line_count:
glob_bracket_nesting_idx.pop() glob_bracket_nesting_idx.pop()
access.pop() access.pop()
if exported[-1] == 0 and classname[-1] != sip_config.get('no_export_macro'): if exported[-1] == 0 and classname[-1] != sip_config.get(
'no_export_macro'):
exit_with_error( exit_with_error(
f"Class {classname[-1]} should be exported with appropriate [LIB]_EXPORT macro. " f"Class {classname[-1]} should be exported with appropriate [LIB]_EXPORT macro. "
f"If this should not be available in python, wrap it in a `#ifndef SIP_RUN` block." f"If this should not be available in python, wrap it in a `#ifndef SIP_RUN` block."
@ -1499,7 +1547,8 @@ while line_idx < line_count:
if len(access) == 1: if len(access) == 1:
dbg_info("reached top level") dbg_info("reached top level")
access[-1] = PUBLIC # Top level should stay public access[
-1] = Visibility.Public # Top level should stay public
comment = '' comment = ''
return_type = '' return_type = ''
@ -1509,7 +1558,7 @@ while line_idx < line_count:
# Private members (exclude SIP_RUN) # Private members (exclude SIP_RUN)
if re.match(r'^\s*private( slots)?:', LINE): if re.match(r'^\s*private( slots)?:', LINE):
access[-1] = PRIVATE access[-1] = Visibility.Private
last_access_section_line = LINE last_access_section_line = LINE
private_section_line = LINE private_section_line = LINE
comment = '' comment = ''
@ -1519,27 +1568,28 @@ while line_idx < line_count:
elif re.match(r'^\s*(public( slots)?|signals):.*$', LINE): elif re.match(r'^\s*(public( slots)?|signals):.*$', LINE):
dbg_info("going public") dbg_info("going public")
last_access_section_line = LINE last_access_section_line = LINE
access[-1] = PUBLIC access[-1] = Visibility.Public
comment = '' comment = ''
elif re.match(r'^\s*(protected)( slots)?:.*$', LINE): elif re.match(r'^\s*(protected)( slots)?:.*$', LINE):
dbg_info("going protected") dbg_info("going protected")
last_access_section_line = LINE last_access_section_line = LINE
access[-1] = PROTECTED access[-1] = Visibility.Protected
comment = '' comment = ''
elif access[-1] == PRIVATE and 'SIP_FORCE' in LINE: elif access[-1] == Visibility.Private and 'SIP_FORCE' in LINE:
dbg_info("private with SIP_FORCE") dbg_info("private with SIP_FORCE")
if private_section_line: if private_section_line:
write_output("PRV3", private_section_line + "\n") write_output("PRV3", private_section_line + "\n")
private_section_line = '' private_section_line = ''
elif any(x == PRIVATE for x in access) and not sip_run: elif any(x == Visibility.Private for x in access) and not sip_run:
comment = '' comment = ''
continue continue
# Skip operators # Skip operators
if access[-1] != PRIVATE and re.search(r'operator(=|<<|>>|->)\s*\(', LINE): if access[-1] != Visibility.Private and re.search(
r'operator(=|<<|>>|->)\s*\(', LINE):
dbg_info("skip operator") dbg_info("skip operator")
detect_and_remove_following_body_or_initializerlist() detect_and_remove_following_body_or_initializerlist()
continue continue
@ -1560,10 +1610,12 @@ while line_idx < line_count:
continue continue
# Handle Q_DECLARE_FLAGS in Qt6 # Handle Q_DECLARE_FLAGS in Qt6
if is_qt6 and re.match(r'^\s*Q_DECLARE_FLAGS\s*\(\s*(\w+)\s*,\s*(\w+)\s*\)', LINE): if is_qt6 and re.match(
r'^\s*Q_DECLARE_FLAGS\s*\(\s*(\w+)\s*,\s*(\w+)\s*\)', LINE):
flags_name = re.search(r'\(\s*(\w+)\s*,\s*(\w+)\s*\)', LINE).group(1) flags_name = re.search(r'\(\s*(\w+)\s*,\s*(\w+)\s*\)', LINE).group(1)
flag_name = re.search(r'\(\s*(\w+)\s*,\s*(\w+)\s*\)', LINE).group(2) flag_name = re.search(r'\(\s*(\w+)\s*,\s*(\w+)\s*\)', LINE).group(2)
output_python.append(f"{actual_class}.{flags_name} = lambda flags=0: {actual_class}.{flag_name}(flags)\n") output_python.append(
f"{actual_class}.{flags_name} = lambda flags=0: {actual_class}.{flag_name}(flags)\n")
# Enum declaration # Enum declaration
# For scoped and type-based enum, the type has to be removed # For scoped and type-based enum, the type has to be removed
@ -1572,13 +1624,19 @@ while line_idx < line_count:
LINE): LINE):
flags_name = re.search(r'\(\s*(\w+)\s*,\s*(\w+)\s*\)', LINE).group(1) flags_name = re.search(r'\(\s*(\w+)\s*,\s*(\w+)\s*\)', LINE).group(1)
flag_name = re.search(r'\(\s*(\w+)\s*,\s*(\w+)\s*\)', LINE).group(2) flag_name = re.search(r'\(\s*(\w+)\s*,\s*(\w+)\s*\)', LINE).group(2)
emkb = re.search(r'SIP_MONKEYPATCH_FLAGS_UNNEST\s*\(\s*(\w+)\s*,\s*(\w+)\s*\)', LINE).group(1) emkb = re.search(
emkf = re.search(r'SIP_MONKEYPATCH_FLAGS_UNNEST\s*\(\s*(\w+)\s*,\s*(\w+)\s*\)', LINE).group(2) r'SIP_MONKEYPATCH_FLAGS_UNNEST\s*\(\s*(\w+)\s*,\s*(\w+)\s*\)',
LINE).group(1)
emkf = re.search(
r'SIP_MONKEYPATCH_FLAGS_UNNEST\s*\(\s*(\w+)\s*,\s*(\w+)\s*\)',
LINE).group(2)
if f"{emkb}.{emkf}" != f"{actual_class}.{flags_name}": if f"{emkb}.{emkf}" != f"{actual_class}.{flags_name}":
output_python.append(f"{emkb}.{emkf} = {actual_class}.{flags_name}\n") output_python.append(
f"{emkb}.{emkf} = {actual_class}.{flags_name}\n")
enum_monkey_patched_types.append([actual_class, flags_name, emkb, emkf]) enum_monkey_patched_types.append(
[actual_class, flags_name, emkb, emkf])
LINE = re.sub(r'\s*SIP_MONKEYPATCH_FLAGS_UNNEST\(.*?\)', '', LINE) LINE = re.sub(r'\s*SIP_MONKEYPATCH_FLAGS_UNNEST\(.*?\)', '', LINE)
@ -1593,14 +1651,17 @@ while line_idx < line_count:
enum_cpp_name = f"{actual_class}::{enum_qualname}" if actual_class else enum_qualname enum_cpp_name = f"{actual_class}::{enum_qualname}" if actual_class else enum_qualname
if not isclass and enum_cpp_name not in ALLOWED_NON_CLASS_ENUMS: if not isclass and enum_cpp_name not in ALLOWED_NON_CLASS_ENUMS:
exit_with_error(f"Non class enum exposed to Python -- must be a enum class: {enum_cpp_name}") exit_with_error(
f"Non class enum exposed to Python -- must be a enum class: {enum_cpp_name}")
oneliner = enum_match.group('oneliner') oneliner = enum_match.group('oneliner')
is_scope_based = bool(isclass) is_scope_based = bool(isclass)
enum_decl = re.sub(r'\s*\bQ_DECL_DEPRECATED\b', '', enum_decl) enum_decl = re.sub(r'\s*\bQ_DECL_DEPRECATED\b', '', enum_decl)
py_enum_type_match = re.search(r'SIP_ENUM_BASETYPE\(\s*(.*?)\s*\)', LINE) py_enum_type_match = re.search(r'SIP_ENUM_BASETYPE\(\s*(.*?)\s*\)',
py_enum_type = py_enum_type_match.group(1) if py_enum_type_match else None LINE)
py_enum_type = py_enum_type_match.group(
1) if py_enum_type_match else None
if py_enum_type == "IntFlag": if py_enum_type == "IntFlag":
enum_intflag_types.append(enum_cpp_name) enum_intflag_types.append(enum_cpp_name)
@ -1610,7 +1671,8 @@ while line_idx < line_count:
if is_qt6: if is_qt6:
enum_decl += f" /BaseType={py_enum_type or 'IntEnum'}/" enum_decl += f" /BaseType={py_enum_type or 'IntEnum'}/"
elif enum_type: elif enum_type:
exit_with_error(f"Unhandled enum type {enum_type} for {enum_cpp_name}") exit_with_error(
f"Unhandled enum type {enum_type} for {enum_cpp_name}")
elif isclass: elif isclass:
enum_class_non_int_types.append(f"{actual_class}.{enum_qualname}") enum_class_non_int_types.append(f"{actual_class}.{enum_qualname}")
elif is_qt6: elif is_qt6:
@ -1624,7 +1686,8 @@ while line_idx < line_count:
_match = None _match = None
if is_scope_based: if is_scope_based:
_match = re.search( _match = re.search(
r'SIP_MONKEYPATCH_SCOPEENUM(_UNNEST)?(:?\(\s*(?P<emkb>\w+)\s*,\s*(?P<emkf>\w+)\s*\))?', LINE) r'SIP_MONKEYPATCH_SCOPEENUM(_UNNEST)?(:?\(\s*(?P<emkb>\w+)\s*,\s*(?P<emkf>\w+)\s*\))?',
LINE)
monkeypatch = is_scope_based and _match monkeypatch = is_scope_based and _match
enum_mk_base = _match.group('emkb') if _match else '' enum_mk_base = _match.group('emkb') if _match else ''
@ -1633,9 +1696,11 @@ while line_idx < line_count:
enum_old_name = _match.group('emkf') enum_old_name = _match.group('emkf')
if actual_class: if actual_class:
if f"{enum_mk_base}.{enum_old_name}" != f"{actual_class}.{enum_qualname}": if f"{enum_mk_base}.{enum_old_name}" != f"{actual_class}.{enum_qualname}":
output_python.append(f"{enum_mk_base}.{enum_old_name} = {actual_class}.{enum_qualname}\n") output_python.append(
f"{enum_mk_base}.{enum_old_name} = {actual_class}.{enum_qualname}\n")
else: else:
output_python.append(f"{enum_mk_base}.{enum_old_name} = {enum_qualname}\n") output_python.append(
f"{enum_mk_base}.{enum_old_name} = {enum_qualname}\n")
if re.search(r'\{((\s*\w+)(\s*=\s*[\w\s<|]+.*?)?(,?))+\s*}', LINE): if re.search(r'\{((\s*\w+)(\s*=\s*[\w\s<|]+.*?)?(,?))+\s*}', LINE):
if '=' in LINE: if '=' in LINE:
@ -1645,7 +1710,8 @@ while line_idx < line_count:
else: else:
LINE = read_line() LINE = read_line()
if not re.match(r'^\s*\{\s*$', LINE): if not re.match(r'^\s*\{\s*$', LINE):
exit_with_error('Unexpected content: enum should be followed by {') exit_with_error(
'Unexpected content: enum should be followed by {')
write_output("ENU2", f"{LINE}\n") write_output("ENU2", f"{LINE}\n")
if is_scope_based: if is_scope_based:
@ -1659,7 +1725,8 @@ while line_idx < line_count:
continue continue
if re.search(r'};', LINE): if re.search(r'};', LINE):
break break
if re.match(r'^\s*\w+\s*\|', LINE): # multi line declaration as sum of enums if re.match(r'^\s*\w+\s*\|',
LINE): # multi line declaration as sum of enums
continue continue
enum_match = re.match( enum_match = re.match(
@ -1667,20 +1734,32 @@ while line_idx < line_count:
LINE) LINE)
enum_decl = f"{enum_match.group(1) or ''}{enum_match.group(3) or ''}{enum_match.group('optional_comma') or ''}" if enum_match else LINE enum_decl = f"{enum_match.group(1) or ''}{enum_match.group(3) or ''}{enum_match.group('optional_comma') or ''}" if enum_match else LINE
enum_member = enum_match.group('em') or '' if enum_match else '' enum_member = enum_match.group(
value_comment = enum_match.group('co') or '' if enum_match else '' 'em') or '' if enum_match else ''
compat_name = enum_match.group('compat') or enum_member if enum_match else '' value_comment = enum_match.group(
enum_value = enum_match.group('enum_value') or '' if enum_match else '' 'co') or '' if enum_match else ''
compat_name = enum_match.group(
'compat') or enum_member if enum_match else ''
enum_value = enum_match.group(
'enum_value') or '' if enum_match else ''
value_comment = value_comment.replace('::', '.').replace('"', '\\"') value_comment = 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 (.*)', r'\\n.. deprecated:: \1\\n', value_comment, flags=re.I) value_comment = re.sub(r'\\since .*?([\d.]+)',
r'\\n.. versionadded:: \1\\n',
value_comment, flags=re.I)
value_comment = re.sub(r'\\deprecated (.*)',
r'\\n.. deprecated:: \1\\n',
value_comment, flags=re.I)
value_comment = re.sub(r'^\\n+', '', value_comment) value_comment = re.sub(r'^\\n+', '', value_comment)
value_comment = re.sub(r'\\n+$', '', value_comment) value_comment = re.sub(r'\\n+$', '', value_comment)
dbg_info(f"is_scope_based:{is_scope_based} enum_mk_base:{enum_mk_base} monkeypatch:{monkeypatch}") dbg_info(
f"is_scope_based:{is_scope_based} enum_mk_base:{enum_mk_base} monkeypatch:{monkeypatch}")
if enum_value and (re.search(r'.*<<.*', enum_value) or re.search(r'.*0x0.*', enum_value)): if enum_value and (
re.search(r'.*<<.*', enum_value) or re.search(r'.*0x0.*',
enum_value)):
if f"{actual_class}::{enum_qualname}" not in enum_intflag_types: if f"{actual_class}::{enum_qualname}" not in enum_intflag_types:
exit_with_error( exit_with_error(
f"{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") f"{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")
@ -1693,21 +1772,27 @@ while line_idx < line_count:
if enum_old_name and compat_name != enum_member: if enum_old_name and compat_name != enum_member:
output_python.append( output_python.append(
f"{enum_mk_base}.{enum_old_name}.{compat_name} = {actual_class}.{enum_qualname}.{enum_member}\n") f"{enum_mk_base}.{enum_old_name}.{compat_name} = {actual_class}.{enum_qualname}.{enum_member}\n")
output_python.append(f"{enum_mk_base}.{compat_name}.is_monkey_patched = True\n") output_python.append(
output_python.append(f"{enum_mk_base}.{compat_name}.__doc__ = \"{value_comment}\"\n") f"{enum_mk_base}.{compat_name}.is_monkey_patched = True\n")
output_python.append(
f"{enum_mk_base}.{compat_name}.__doc__ = \"{value_comment}\"\n")
enum_members_doc.append( enum_members_doc.append(
f"'* ``{compat_name}``: ' + {actual_class}.{enum_qualname}.{enum_member}.__doc__") f"'* ``{compat_name}``: ' + {actual_class}.{enum_qualname}.{enum_member}.__doc__")
else: else:
output_python.append(f"{enum_mk_base}.{compat_name} = {enum_qualname}.{enum_member}\n") output_python.append(
output_python.append(f"{enum_mk_base}.{compat_name}.is_monkey_patched = True\n") f"{enum_mk_base}.{compat_name} = {enum_qualname}.{enum_member}\n")
output_python.append(f"{enum_mk_base}.{compat_name}.__doc__ = \"{value_comment}\"\n") output_python.append(
f"{enum_mk_base}.{compat_name}.is_monkey_patched = True\n")
output_python.append(
f"{enum_mk_base}.{compat_name}.__doc__ = \"{value_comment}\"\n")
enum_members_doc.append( enum_members_doc.append(
f"'* ``{compat_name}``: ' + {enum_qualname}.{enum_member}.__doc__") f"'* ``{compat_name}``: ' + {enum_qualname}.{enum_member}.__doc__")
else: else:
if monkeypatch: if monkeypatch:
output_python.append( output_python.append(
f"{actual_class}.{compat_name} = {actual_class}.{enum_qualname}.{enum_member}\n") f"{actual_class}.{compat_name} = {actual_class}.{enum_qualname}.{enum_member}\n")
output_python.append(f"{actual_class}.{compat_name}.is_monkey_patched = True\n") output_python.append(
f"{actual_class}.{compat_name}.is_monkey_patched = True\n")
if actual_class: if actual_class:
complete_class_path = '.'.join(classname) complete_class_path = '.'.join(classname)
output_python.append( output_python.append(
@ -1715,7 +1800,8 @@ while line_idx < line_count:
enum_members_doc.append( enum_members_doc.append(
f"'* ``{compat_name}``: ' + {actual_class}.{enum_qualname}.{enum_member}.__doc__") f"'* ``{compat_name}``: ' + {actual_class}.{enum_qualname}.{enum_member}.__doc__")
else: else:
output_python.append(f"{enum_qualname}.{compat_name}.__doc__ = \"{value_comment}\"\n") output_python.append(
f"{enum_qualname}.{compat_name}.__doc__ = \"{value_comment}\"\n")
enum_members_doc.append( enum_members_doc.append(
f"'* ``{compat_name}``: ' + {enum_qualname}.{enum_member}.__doc__") f"'* ``{compat_name}``: ' + {enum_qualname}.{enum_member}.__doc__")
@ -1737,10 +1823,12 @@ while line_idx < line_count:
comment = comment.replace('\n', '\\n').replace('"', '\\"') comment = comment.replace('\n', '\\n').replace('"', '\\"')
if actual_class: if actual_class:
output_python.append(f'{actual_class}.{enum_qualname}.__doc__ = "{comment}\\n\\n" + ' + output_python.append(
f'{actual_class}.{enum_qualname}.__doc__ = "{comment}\\n\\n" + ' +
" + '\\n' + ".join(enum_members_doc) + '\n# --\n') " + '\\n' + ".join(enum_members_doc) + '\n# --\n')
else: else:
output_python.append(f'{enum_qualname}.__doc__ = \'{comment}\\n\\n\' + ' + output_python.append(
f'{enum_qualname}.__doc__ = \'{comment}\\n\\n\' + ' +
" + '\\n' + ".join(enum_members_doc) + '\n# --\n') " + '\\n' + ".join(enum_members_doc) + '\n# --\n')
# enums don't have Docstring apparently # enums don't have Docstring apparently
@ -1749,7 +1837,8 @@ while line_idx < line_count:
# Check for invalid use of doxygen command # Check for invalid use of doxygen command
if re.search(r'.*//!<', LINE): if re.search(r'.*//!<', LINE):
exit_with_error('"\\!<" doxygen command must only be used for enum documentation') exit_with_error(
'"\\!<" doxygen command must only be used for enum documentation')
# Handle override, final, and make private keywords # Handle override, final, and make private keywords
if re.search(r'\boverride\b', LINE): if re.search(r'\boverride\b', LINE):
@ -1763,8 +1852,11 @@ while line_idx < line_count:
LINE = re.sub(r'^(\s*)Q_INVOKABLE ', r'\1', LINE) LINE = re.sub(r'^(\s*)Q_INVOKABLE ', r'\1', LINE)
# Keyword fixes # Keyword fixes
LINE = re.sub(r'^(\s*template\s*<)(?:class|typename) (\w+>)(.*)$', r'\1\2\3', LINE) LINE = re.sub(r'^(\s*template\s*<)(?:class|typename) (\w+>)(.*)$',
LINE = re.sub(r'^(\s*template\s*<)(?:class|typename) (\w+) *, *(?:class|typename) (\w+>)(.*)$', r'\1\2,\3\4', r'\1\2\3', LINE)
LINE = re.sub(
r'^(\s*template\s*<)(?:class|typename) (\w+) *, *(?:class|typename) (\w+>)(.*)$',
r'\1\2,\3\4',
LINE) LINE)
LINE = re.sub( LINE = re.sub(
r'^(\s*template\s*<)(?:class|typename) (\w+) *, *(?:class|typename) (\w+) *, *(?:class|typename) (\w+>)(.*)$', r'^(\s*template\s*<)(?:class|typename) (\w+) *, *(?:class|typename) (\w+) *, *(?:class|typename) (\w+>)(.*)$',
@ -1787,14 +1879,16 @@ while line_idx < line_count:
LINE = re.sub(r'\b\w+_EXPORT\s+', '', LINE) LINE = re.sub(r'\b\w+_EXPORT\s+', '', LINE)
# Skip non-method member declaration in non-public sections # Skip non-method member declaration in non-public sections
if not sip_run and access[-1] != PUBLIC and detect_non_method_member(LINE): if not sip_run and access[
-1] != Visibility.Public and detect_non_method_member(LINE):
dbg_info("skip non-method member declaration in non-public sections") dbg_info("skip non-method member declaration in non-public sections")
continue continue
# Remove static const value assignment # Remove static const value assignment
# https://regex101.com/r/DyWkgn/6 # https://regex101.com/r/DyWkgn/6
if re.search(r'^\s*const static \w+', LINE): if re.search(r'^\s*const static \w+', LINE):
exit_with_error(f"const static should be written static const in {classname[-1]}") exit_with_error(
f"const static should be written static const in {classname[-1]}")
# TODO needs fixing!! # TODO needs fixing!!
# original perl regex was: # original perl regex was:
@ -1816,7 +1910,7 @@ while line_idx < line_count:
# Remove struct member assignment # Remove struct member assignment
# https://regex101.com/r/OUwV75/1 # https://regex101.com/r/OUwV75/1
if not sip_run and access[-1] == PUBLIC: if not sip_run and access[-1] == Visibility.Public:
# original perl regex: ^(\s*\w+[\w<> *&:,]* \*?\w+) = ([\-\w\:\.]+(< *\w+( \*)? *>)?)+(\([^()]*\))?\s*; # original perl regex: ^(\s*\w+[\w<> *&:,]* \*?\w+) = ([\-\w\:\.]+(< *\w+( \*)? *>)?)+(\([^()]*\))?\s*;
# dbg_info(f"attempt struct member assignment '{LINE}'") # dbg_info(f"attempt struct member assignment '{LINE}'")
@ -1862,26 +1956,30 @@ while line_idx < line_count:
(?:\/\/.*)? # Optional single-line comment (?:\/\/.*)? # Optional single-line comment
$ # End of the line $ # End of the line
''' '''
regex_verbose = re.compile(python_regex_verbose, re.VERBOSE | re.MULTILINE) regex_verbose = re.compile(python_regex_verbose,
re.VERBOSE | re.MULTILINE)
match = regex_verbose.match(LINE) match = regex_verbose.match(LINE)
if match: if match:
dbg_info(f"remove struct member assignment '={match.group(2)}'") dbg_info(f"remove struct member assignment '={match.group(2)}'")
LINE = f"{match.group(1)};" LINE = f"{match.group(1)};"
# Catch Q_DECLARE_FLAGS # Catch Q_DECLARE_FLAGS
match = re.search(r'^(\s*)Q_DECLARE_FLAGS\(\s*(.*?)\s*,\s*(.*?)\s*\)\s*$', LINE) match = re.search(r'^(\s*)Q_DECLARE_FLAGS\(\s*(.*?)\s*,\s*(.*?)\s*\)\s*$',
LINE)
if match: if match:
actual_class = f"{classname[-1]}::" if len(classname) >= 0 else '' actual_class = f"{classname[-1]}::" if len(classname) >= 0 else ''
dbg_info(f"Declare flags: {actual_class}") dbg_info(f"Declare flags: {actual_class}")
LINE = f"{match.group(1)}typedef QFlags<{actual_class}{match.group(3)}> {match.group(2)};\n" LINE = f"{match.group(1)}typedef QFlags<{actual_class}{match.group(3)}> {match.group(2)};\n"
qflag_hash[f"{actual_class}{match.group(2)}"] = f"{actual_class}{match.group(3)}" qflag_hash[
f"{actual_class}{match.group(2)}"] = f"{actual_class}{match.group(3)}"
if f"{actual_class}{match.group(3)}" not in enum_intflag_types: if f"{actual_class}{match.group(3)}" not in enum_intflag_types:
exit_with_error( exit_with_error(
f"{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") f"{actual_class}{match.group(3)} is a flags type, but was not declared with IntFlag type. Add 'SIP_ENUM_BASETYPE(IntFlag)' to the enum class declaration line")
# Catch Q_DECLARE_OPERATORS_FOR_FLAGS # Catch Q_DECLARE_OPERATORS_FOR_FLAGS
match = re.search(r'^(\s*)Q_DECLARE_OPERATORS_FOR_FLAGS\(\s*(.*?)\s*\)\s*$', LINE) match = re.search(
r'^(\s*)Q_DECLARE_OPERATORS_FOR_FLAGS\(\s*(.*?)\s*\)\s*$', LINE)
if match: if match:
flags = match.group(2) flags = match.group(2)
flag = qflag_hash.get(flags) flag = qflag_hash.get(flags)
@ -1899,7 +1997,8 @@ while line_idx < line_count:
output_python.append( output_python.append(
"from enum import Enum\n\n\ndef _force_int(v): return int(v.value) if isinstance(v, Enum) else v\n\n\n") "from enum import Enum\n\n\ndef _force_int(v): return int(v.value) if isinstance(v, Enum) else v\n\n\n")
has_pushed_force_int = True has_pushed_force_int = True
output_python.append(f"{py_flag}.__bool__ = lambda flag: bool(_force_int(flag))\n") output_python.append(
f"{py_flag}.__bool__ = lambda flag: bool(_force_int(flag))\n")
output_python.append( output_python.append(
f"{py_flag}.__eq__ = lambda flag1, flag2: _force_int(flag1) == _force_int(flag2)\n") f"{py_flag}.__eq__ = lambda flag1, flag2: _force_int(flag1) == _force_int(flag2)\n")
output_python.append( output_python.append(
@ -1924,28 +2023,36 @@ while line_idx < line_count:
if multiline_definition != MULTILINE_NO: if multiline_definition != MULTILINE_NO:
rolling_line = LINE rolling_line = LINE
rolling_line_idx = line_idx rolling_line_idx = line_idx
dbg_info("handle multiline definition to add virtual keyword or making private on opening line") dbg_info(
while not re.match(r'^[^()]*\(([^()]*\([^()]*\)[^()]*)*[^()]*$', rolling_line): "handle multiline definition to add virtual keyword or making private on opening line")
while not re.match(r'^[^()]*\(([^()]*\([^()]*\)[^()]*)*[^()]*$',
rolling_line):
rolling_line_idx -= 1 rolling_line_idx -= 1
rolling_line = input_lines[rolling_line_idx] rolling_line = input_lines[rolling_line_idx]
if rolling_line_idx < 0: if rolling_line_idx < 0:
exit_with_error('could not reach opening definition') exit_with_error('could not reach opening definition')
dbg_info(f'rolled back to {rolling_line_idx}: {rolling_line}') dbg_info(f'rolled back to {rolling_line_idx}: {rolling_line}')
if is_override_or_make_private == PREPEND_CODE_VIRTUAL and not re.match(r'^(\s*)virtual\b(.*)$', if is_override_or_make_private == PREPEND_CODE_VIRTUAL and not re.match(
r'^(\s*)virtual\b(.*)$',
rolling_line): rolling_line):
idx = rolling_line_idx - line_idx + 1 idx = rolling_line_idx - line_idx + 1
output[idx] = fix_annotations(re.sub(r'^(\s*?)\b(.*)$', r'\1 virtual \2\n', rolling_line)) output[idx] = fix_annotations(
re.sub(r'^(\s*?)\b(.*)$', r'\1 virtual \2\n',
rolling_line))
elif is_override_or_make_private == PREPEND_CODE_MAKE_PRIVATE: elif is_override_or_make_private == PREPEND_CODE_MAKE_PRIVATE:
dbg_info("prepending private access") dbg_info("prepending private access")
idx = rolling_line_idx - line_idx idx = rolling_line_idx - line_idx
private_access = re.sub(r'(protected|public)', 'private', last_access_section_line) private_access = re.sub(r'(protected|public)', 'private',
last_access_section_line)
output.insert(idx + 1, private_access + "\n") output.insert(idx + 1, private_access + "\n")
output[idx + 1] = fix_annotations(rolling_line) + "\n" output[idx + 1] = fix_annotations(rolling_line) + "\n"
elif is_override_or_make_private == PREPEND_CODE_MAKE_PRIVATE: elif is_override_or_make_private == PREPEND_CODE_MAKE_PRIVATE:
dbg_info("prepending private access") dbg_info("prepending private access")
LINE = re.sub(r'(protected|public)', 'private', last_access_section_line) + "\n" + LINE + "\n" LINE = re.sub(r'(protected|public)', 'private',
elif is_override_or_make_private == PREPEND_CODE_VIRTUAL and not re.match(r'^(\s*)virtual\b(.*)$', LINE): last_access_section_line) + "\n" + LINE + "\n"
elif is_override_or_make_private == PREPEND_CODE_VIRTUAL and not re.match(
r'^(\s*)virtual\b(.*)$', LINE):
# SIP often requires the virtual keyword to be present, or it chokes on covariant return types # SIP often requires the virtual keyword to be present, or it chokes on covariant return types
# in overridden methods # in overridden methods
dbg_info('adding virtual keyword for overridden method') dbg_info('adding virtual keyword for overridden method')
@ -1965,15 +2072,18 @@ while line_idx < line_count:
match = re.match(pattern, LINE) match = re.match(pattern, LINE)
if match: if match:
return_type_candidate = match.group(1) return_type_candidate = match.group(1)
if not re.search(r'(void|SIP_PYOBJECT|operator|return|QFlag)', return_type_candidate): if not re.search(r'(void|SIP_PYOBJECT|operator|return|QFlag)',
return_type_candidate):
# replace :: with . (changes c++ style namespace/class directives to Python style) # replace :: with . (changes c++ style namespace/class directives to Python style)
return_type = return_type_candidate.replace('::', '.') return_type = return_type_candidate.replace('::', '.')
# replace with builtin Python types # replace with builtin Python types
return_type = re.sub(r'\bdouble\b', 'float', return_type) return_type = re.sub(r'\bdouble\b', 'float', return_type)
return_type = re.sub(r'\bQString\b', 'str', return_type) return_type = re.sub(r'\bQString\b', 'str', return_type)
return_type = re.sub(r'\bQStringList\b', 'list of str', return_type) return_type = re.sub(r'\bQStringList\b', 'list of str',
return_type)
list_match = re.match(r'^(?:QList|QVector)<\s*(.*?)[\s*]*>$', return_type) list_match = re.match(r'^(?:QList|QVector)<\s*(.*?)[\s*]*>$',
return_type)
if list_match: if list_match:
return_type = f"list of {list_match.group(1)}" return_type = f"list of {list_match.group(1)}"
@ -1992,7 +2102,8 @@ while line_idx < line_count:
LINE = re.sub(r'^(\s*struct )\w+_EXPORT (.+)$', r'\1\2', LINE) LINE = re.sub(r'^(\s*struct )\w+_EXPORT (.+)$', r'\1\2', LINE)
# Skip comments # Skip comments
if re.match(r'^\s*typedef\s+\w+\s*<\s*\w+\s*>\s+\w+\s+.*SIP_DOC_TEMPLATE', LINE): if re.match(r'^\s*typedef\s+\w+\s*<\s*\w+\s*>\s+\w+\s+.*SIP_DOC_TEMPLATE',
LINE):
# support Docstring for template based classes in SIP 4.19.7+ # support Docstring for template based classes in SIP 4.19.7+
comment_template_docstring = True comment_template_docstring = True
elif (multiline_definition == MULTILINE_NO and elif (multiline_definition == MULTILINE_NO and
@ -2022,7 +2133,8 @@ while line_idx < line_count:
# MISSING # MISSING
# handle enum/flags QgsSettingsEntryEnumFlag # handle enum/flags QgsSettingsEntryEnumFlag
match = re.match(r'^(\s*)const QgsSettingsEntryEnumFlag<(.*)> (.+);$', LINE) match = re.match(r'^(\s*)const QgsSettingsEntryEnumFlag<(.*)> (.+);$',
LINE)
if match: if match:
indent, enum_type, var_name = match.groups() indent, enum_type, var_name = match.groups()
@ -2047,10 +2159,13 @@ while line_idx < line_count:
# append to class map file # append to class map file
if args.class_map and actual_class: if args.class_map and actual_class:
match = re.match(r'^ *(const |virtual |static )* *[\w:]+ +\*?(?P<method>\w+)\(.*$', LINE) match = re.match(
r'^ *(const |virtual |static )* *[\w:]+ +\*?(?P<method>\w+)\(.*$',
LINE)
if match: if match:
with open(args.class_map, 'a') as f: with open(args.class_map, 'a') as f:
f.write(f"{'.'.join(classname)}.{match.group('method')}: {headerfile}#L{line_idx}\n") f.write(
f"{'.'.join(classname)}.{match.group('method')}: {headerfile}#L{line_idx}\n")
if python_signature: if python_signature:
write_output("PSI", f"{python_signature}\n") write_output("PSI", f"{python_signature}\n")
@ -2061,10 +2176,13 @@ while line_idx < line_count:
# https://regex101.com/r/DN01iM/4 # https://regex101.com/r/DN01iM/4
# TODO - original regex is incompatible with python -- it was: # TODO - original regex is incompatible with python -- it was:
# ^([^()]+(\((?:[^()]++|(?1))*\)))*[^()]*\)([^()](throw\([^()]+\))?)*$: # ^([^()]+(\((?:[^()]++|(?1))*\)))*[^()]*\)([^()](throw\([^()]+\))?)*$:
if re.match(r'^([^()]+(\((?:[^()]|\([^()]*\))*\)))*[^()]*\)([^()](throw\([^()]+\))?)*', LINE): if re.match(
r'^([^()]+(\((?:[^()]|\([^()]*\))*\)))*[^()]*\)([^()](throw\([^()]+\))?)*',
LINE):
dbg_info("ending multiline") dbg_info("ending multiline")
# remove potential following body # remove potential following body
if multiline_definition != MULTILINE_CONDITIONAL_STATEMENT and not re.search(r'(\{.*}|;)\s*(//.*)?$', if multiline_definition != MULTILINE_CONDITIONAL_STATEMENT and not re.search(
r'(\{.*}|;)\s*(//.*)?$',
LINE): LINE):
dbg_info("remove following body of multiline def") dbg_info("remove following body of multiline def")
last_line = LINE last_line = LINE
@ -2111,14 +2229,17 @@ while line_idx < line_count:
waiting_for_return_to_end = False waiting_for_return_to_end = False
for comment_line in comment_lines: for comment_line in comment_lines:
if ('versionadded:' in comment_line or 'deprecated:' in comment_line) and out_params: if (
'versionadded:' in comment_line or 'deprecated:' in comment_line) and out_params:
dbg_info('out style parameters remain to flush!') dbg_info('out style parameters remain to flush!')
# member has /Out/ parameters, but no return type, so flush out out_params docs now # member has /Out/ parameters, but no return type, so flush out out_params docs now
first_out_param = out_params.pop(0) first_out_param = out_params.pop(0)
write_output("CM7", f"{doc_prepend}:return: - {first_out_param}\n") write_output("CM7",
f"{doc_prepend}:return: - {first_out_param}\n")
for out_param in out_params: for out_param in out_params:
write_output("CM7", f"{doc_prepend} - {out_param}\n") write_output("CM7",
f"{doc_prepend} - {out_param}\n")
write_output("CM7", f"{doc_prepend}\n") write_output("CM7", f"{doc_prepend}\n")
out_params = [] out_params = []
@ -2127,8 +2248,12 @@ while line_idx < line_count:
param_name = param_match.group(1) param_name = param_match.group(1)
if param_name in skipped_params_out or param_name in skipped_params_remove: if param_name in skipped_params_out or param_name in skipped_params_remove:
if param_name in skipped_params_out: if param_name in skipped_params_out:
comment_line = re.sub(r'^:param\s+(\w+):\s*(.*?)$', r'\1: \2', comment_line) comment_line = re.sub(
comment_line = re.sub(r'(?:optional|if specified|if given),?\s*', '', r'^:param\s+(\w+):\s*(.*?)$', r'\1: \2',
comment_line)
comment_line = re.sub(
r'(?:optional|if specified|if given),?\s*',
'',
comment_line) comment_line)
out_params.append(comment_line) out_params.append(comment_line)
skipping_param = 2 skipping_param = 2
@ -2148,10 +2273,12 @@ while line_idx < line_count:
if ':return:' in comment_line and out_params: if ':return:' in comment_line and out_params:
waiting_for_return_to_end = True waiting_for_return_to_end = True
comment_line = comment_line.replace(':return:', ':return: -') comment_line = comment_line.replace(':return:',
':return: -')
write_output("CM2", f"{doc_prepend}{comment_line}\n") write_output("CM2", f"{doc_prepend}{comment_line}\n")
for out_param in out_params: for out_param in out_params:
write_output("CM7", f"{doc_prepend} - {out_param}\n") write_output("CM7",
f"{doc_prepend} - {out_param}\n")
out_params = [] out_params = []
else: else:
write_output("CM2", f"{doc_prepend}{comment_line}\n") write_output("CM2", f"{doc_prepend}{comment_line}\n")