from enum import StrEnum
from typing import List
from PySide6.QtCore import QMetaObject, QMetaMethod
from pywebchannel.Utils import Utils
[docs]
class SupportedTypes(StrEnum):
Controller = "Controller"
Model = "BaseModel"
[docs]
class CodeAnalyzer:
"""A class that analyzes the code of a given class and determines its type and acceptability.
Attributes:
MetaClass (type): The class object to be analyzed.
_classType (str): The type of the class object, one of the supported types.
_isAcceptable (bool): A flag indicating whether the class object is acceptable for analysis or not.
"""
def __init__(self, MetaClass):
"""Initializes the CodeAnalyzer with the class object.
Args:
MetaClass (type): The class object to be analyzed.
"""
self.MetaClass = MetaClass
# One of the supported types
self._classType: str = ""
self._isAcceptable = False
# Get the inheritance tree of the class object
baseClasses = Utils.getInheritanceTree(MetaClass)
# Check if the class object inherits from one of the supported types
for sType in SupportedTypes:
if sType in baseClasses:
self._isAcceptable = True
self._classType = sType
[docs]
def isAcceptable(self):
"""Returns whether the class object is acceptable for analysis or not.
Returns:
bool: True if the class object is acceptable, False otherwise.
"""
return self._isAcceptable
[docs]
def classType(self):
"""Returns the type of the class object, one of the supported types.
Returns:
str: The type of the class object, or an empty string if not acceptable.
"""
return self._classType
[docs]
def run(self):
"""Runs the analysis on the class object and returns an interface object.
Returns:
The interface object corresponding to the class object's type, or None if not acceptable.
"""
# If the class object is a controller, return a controller interface object
if self._classType == SupportedTypes.Controller:
return ControllerInterface(self.MetaClass)
# If the class object is a model, return a model interface object
elif self._classType == SupportedTypes.Model:
return ModelInterface(self.MetaClass)
else:
# The class object is not acceptable, return None
return None
[docs]
class Interface:
"""A base class that represents the interface of a class.
An interface is a set of properties, signals, and slots that define the communication
and functionality of a class.
Attributes:
MetaClass (type): The metaclass of the class that implements the interface.
name (str): The name of the interface.
objectDict (dict): The dictionary of the meta class's attributes and methods.
staticMetaObject (QMetaObject): The static meta-object of the meta class.
props (list of Property): The properties of the interface.
signals (list of Signal): The signals of the interface.
slots (list of Slot): The slots of the interface.
"""
def __init__(self, MetaClass):
"""Initializes the Interface with the meta class of the class that implements the interface.
Args:
MetaClass (type): The meta class of the class that implements the interface.
"""
self.MetaClass = MetaClass
# Get the name, dictionary, and static meta object of the meta class
self.name = self.MetaClass.__name__
self.objectDict = MetaClass.__dict__
self.staticMetaObject: QMetaObject = self.objectDict.get("staticMetaObject")
# Initialize the properties, signals, and slots as empty lists
self.props: List[Property] = []
self.signals: List[Signal] = []
self.slots: List[Slot] = []
[docs]
def classType(self):
"""Returns the type of the interface, which is "Interface".
Returns:
str: The type of the interface.
"""
return "Interface"
[docs]
def dependencies(self):
"""Returns the list of dependencies of the interface.
Dependencies are the types that are used by the properties, signals, and slots of the interface.
Returns:
The list of dependencies, without duplicates.
"""
return []
[docs]
class ControllerInterface(Interface):
"""A class that represents the interface of a controller class.
A controller class is a subclass of QObject that defines properties, signals, and slots
that can be used to communicate with other classes or components.
Attributes:
props (list of Property): The properties of the controller class.
signals (list of Signal): The signals of the controller class.
slots (list of Slot): The slots of the controller class.
"""
def __init__(self, MetaClass):
"""Initializes the ControllerInterface with the meta class of the controller class.
Args:
MetaClass (type): The meta class of the controller class.
"""
super().__init__(MetaClass)
# Extract the properties, signals, and slots from the meta class
self.props = self._extractProperties()
self.signals = self._extractSignals()
self.slots = self._extractSlots()
# Convert the types and codes of the properties, signals, and slots
for prop in self.props:
prop.convertType()
prop.convertCode()
for signal in self.signals:
signal.convertType()
signal.convertCode()
for slot in self.slots:
slot.convertType()
slot.convertCode()
[docs]
def classType(self):
"""Returns the type of the interface, which is SupportedTypes.Controller.
Returns:
str: The type of the interface.
"""
return SupportedTypes.Controller
[docs]
def dependencies(self):
"""Returns the list of dependencies of the interface.
Dependencies are the types that are used by the properties, signals, and slots of the interface.
Returns:
list[str]: The list of dependencies, without duplicates.
"""
dep = []
# Add the dependencies of the properties
for prop in self.props:
dep.extend(prop.dependencies())
# Add the dependencies of the signals
for signal in self.signals:
dep.extend(signal.dependencies())
# Add the dependencies of the slots
for slot in self.slots:
dep.extend(slot.dependencies())
# Remove any duplicates from the list
return list(dict.fromkeys(dep))
def _extractProperties(self):
"""Extracts the properties from the meta class of the controller class.
Returns:
list[Property]: The list of properties of the controller class.
"""
props: list[Property] = []
# Iterate over the properties of the meta class
for i in range(1, self.staticMetaObject.propertyCount()):
prop = self.staticMetaObject.property(i)
# Get the name and type name of the property
name = prop.name()
typeName = prop.typeName()
# Get the type map of the properties, if any
propTypesMap: dict = getattr(self.MetaClass, "propsTypes", None)
propType = None
if propTypesMap is not None:
propType = propTypesMap[name]
# If the property has a type map, use it to get the simplified type name
if propType is not None:
typeName = Utils.simplyVariableType(Utils.type_to_string(propType))
# Create a Property object and append it to the list
props.append(Property(name, typeName))
return props
def _extractSignals(self):
"""Extracts the signals from the meta class of the controller class.
Returns:
list[Signal]: The list of signals of the controller class.
"""
signalInfos: list[Signal] = []
# Iterate over the methods of the meta class
for i in range(self.staticMetaObject.methodCount()):
method: QMetaMethod = self.staticMetaObject.method(i)
# Skip the methods that are not signals or are not defined by the controller class
if method.methodType().name != "Signal":
continue
if method.enclosingMetaObject().className() != self.MetaClass.__name__:
continue
# Get the name and return type of the signal
name = method.name().toStdString()
returnType = Return(method.returnMetaType().name())
# Fill with the parameters of the signal
parameters = []
# Get the signal arguments map, if any
signalArgsMap: dict = getattr(self.MetaClass, "signalArgsMap", None)
if signalArgsMap is not None:
signalArgsMap = signalArgsMap[name]
# If the signal has an arguments map, use it to get the parameter names and types
if signalArgsMap is not None:
for argName, argType in signalArgsMap.items():
paramTypeStr = Utils.simplyVariableType(
Utils.type_to_string(argType)
)
parameters.append(Parameter(argName, paramTypeStr))
else:
# If the signal does not have an arguments map, use the method information to get the parameter names
# and types
if method.parameterCount() > 0:
for pi in range(method.parameterCount()):
parameters.append(
Parameter(
method.parameterNames()[pi].toStdString(),
method.parameterTypeName(pi).toStdString(),
)
)
# Create a Signal object and append it to the list
signalInfos.append(Signal(name, parameters, returnType))
return signalInfos
def _extractSlots(self):
"""Extracts the slots from the meta class of the controller class.
Returns:
list[Slot]: The list of slots of the controller class.
"""
slotInfos: list[Slot] = []
# Iterate over the methods of the meta class
for i in range(self.staticMetaObject.methodCount()):
method: QMetaMethod = self.staticMetaObject.method(i)
# Skip the methods that are not slots or are not defined by the controller class
if method.methodType().name != "Slot":
continue
if method.enclosingMetaObject().className() != self.MetaClass.__name__:
continue
# Extract the name of the slot
name = method.name().toStdString()
# Skip the private slots
if name.startswith("_"):
continue
# Get the function object of the slot
methodFunc = self.objectDict.get(name)
# Extract the return type and parameters of the slot
# - 1. Check via user annotation (this has privilege)
# - 2. Check via QMetaObject system
# Parse the slot function with inspect
paramNames, paramTypes, returnType = Utils.parseWithInspect(methodFunc)
# If the return type is not found by type annotation, use the QMetaObject system
if returnType == "":
returnType = Return(method.returnMetaType().name())
else:
returnType = Return(returnType)
parameters = []
# If the slot has parameters, extract their names and types
if method.parameterCount() > 0:
for pi in range(method.parameterCount()):
# If the parameter type is not found by type annotation, use the QMetaObject system
par = Parameter(
paramNames[pi],
paramTypes[pi]
if paramTypes[pi] != ""
else method.parameterTypeName(pi).toStdString(),
)
parameters.append(par)
# Create a Slot object and append it to the list
slotInfos.append(Slot(name, parameters, returnType))
return slotInfos
[docs]
class ModelInterface(Interface):
"""A class that represents the interface of a model class.
Attributes:
props (list of Property): The properties of the model class.
"""
def __init__(self, MetaClass):
"""Initializes the ModelInterface with the meta class of the model class.
Args:
MetaClass (type): The meta class of the model class.
"""
super().__init__(MetaClass)
# Get the fields of the model class
fields = self.objectDict["__annotations__"]
# Create a property for each field
for fieldName, field in fields.items():
self.props.append(
Property(fieldName, Utils.simplyVariableType(field.__name__))
)
# Convert the types and codes of the properties
for prop in self.props:
prop.convertType()
prop.convertCode()
[docs]
def classType(self):
"""Returns the type of the interface, which is SupportedTypes.Model.
Returns:
str: The type of the interface.
"""
return SupportedTypes.Model
[docs]
def dependencies(self):
"""Returns the list of dependencies of the interface.
Dependencies are the types that are used by the properties of the interface.
Returns:
list[str]: The list of dependencies, without duplicates.
"""
dep = []
# Add the dependencies of the properties
for prop in self.props:
dep.extend(prop.dependencies())
# Remove any duplicates from the list
return list(dict.fromkeys(dep))
[docs]
class Parameter:
"""A class to represent a parameter in TypeScript.
Attributes:
name (str): The name of the parameter.
type (str): The type of the parameter in TypeScript syntax.
code (str): The code representation of the parameter.
"""
def __init__(self, name: str, typeStr: str) -> None:
"""Create a new Parameter instance.
Args:
name (str): The name of the parameter.
typeStr (str): The type of the parameter in TypeScript syntax.
"""
# Assign the name and type to the instance attributes
self.name = name
self.type = typeStr
# Initialize the code attribute as an empty string
self.code = ""
def __repr__(self) -> str:
"""Return the string representation of the parameter.
Returns:
str: The code attribute if not empty, otherwise a formatted string with the name and type.
"""
# If the code attribute is not empty, return it
if self.code != "":
return self.code
# Otherwise, return a formatted string with the name and type
return f"Parameter({self.name}, {self.type})"
[docs]
def convertType(self) -> None:
"""Convert the type attribute to a TypeScript compatible type."""
# Use the Utils class to convert the type
self.type = Utils.convertType(self.type)
[docs]
def convertCode(self) -> None:
"""Generate the code representation of the parameter."""
# Format the code attribute with the name and type
self.code = f"{self.name}: {self.type}"
[docs]
def dependencies(self) -> list[str]:
"""Return a list of the dependencies of the parameter type.
Returns:
list[str]: A list of the types that the parameter type depends on, without brackets.
"""
return [self.type.replace("[]", "")]
[docs]
class Return:
def __init__(self, typeStr: str) -> None:
"""
Initialize a Return object.
Args:
typeStr (str): The type of the return value.
"""
# Assign the type attribute
self.type = typeStr
# Initialize the code attribute as an empty string
self.code = ""
def __repr__(self) -> str:
"""
Return the code or a string representation of the return type.
Returns:
str: The code or a string representation of the return type.
"""
# Check if the code attribute is not empty
if self.code != "":
# Return the code attribute
return self.code
# Return a formatted string with the type attribute
return f"Return({self.type})"
[docs]
def convertType(self) -> None:
"""
Convert the type attribute to a TypeScript compatible type.
"""
# Use the convertType method from the Utils class to assign the type attribute
self.type = Utils.convertType(self.type)
[docs]
def convertCode(self) -> None:
"""
Generate the code attribute for the return type.
"""
# Use the type attribute to assign the code attribute as a TypeScript return type declaration
self.code = f"{self.type}"
[docs]
def dependencies(self):
"""
Return the dependencies of the return type.
Returns:
list: A list of the types that the return type depends on.
"""
# Return a list with the type attribute without the array notation
return [self.type.replace("[]", "")]
[docs]
class Slot:
"""
A class to represent a slot of a TypeScript class.
Attributes:
name (str): The name of the slot.
parameters (list[Parameter]): A list of Parameter objects that represent the parameters of the slot function.
returnType (Return): A Return object that represents the return type of the slot function.
code (str): The code for the slot declaration in TypeScript.
"""
def __init__(
self, name: str, parameters: list[Parameter], returnType: Return
) -> None:
"""
Initialize a Slot object.
Args:
name (str): The name of the slot.
parameters (list[Parameter]): List of Parameter objects that represent the parameters of the slot function.
returnType (Return): A Return object that represents the return type of the slot function.
"""
# Assign the name, parameters and returnType attributes
self.name = name
self.parameters = parameters
self.returnType = returnType
# Initialize the code attribute as an empty string
self.code = ""
def __repr__(self) -> str:
"""
Return the code or a string representation of the slot.
Returns:
str: The code or a string representation of the slot.
"""
# Check if the code attribute is not empty
if self.code != "":
# Return the code attribute
return self.code
# Return a formatted string with the name, parameters and returnType attributes
return f"Slot( {self.name}, {self.parameters}, {self.returnType} )"
[docs]
def convertType(self) -> None:
"""
Convert the type attributes of the parameters and the returnType to TypeScript compatible types.
"""
# Loop through the parameters list
for param in self.parameters:
# Call the convertType method of the Parameter object
param.convertType()
# Call the convertType method of the Return object
self.returnType.convertType()
[docs]
def convertCode(self) -> None:
"""
Generate the code attribute for the slot.
"""
# Define a helper function to convert a Parameter object to a TypeScript parameter declaration
def paramConvert(param: Parameter) -> str:
# Call the convertCode method of the Parameter object
param.convertCode()
# Return the code attribute of the Parameter object
return param.code
# Use the map function to apply the paramConvert function to each element of the parameters list
# Use the join method to combine the resulting list into a string separated by commas
paramCodesCombined = ", ".join([*map(paramConvert, self.parameters)])
# Call the convertCode method of the Return object
self.returnType.convertCode()
# Use the name, paramCodesCombined and returnType attributes to assign the code attribute as a TypeScript
# slot declaration
self.code = (
f"{self.name}({paramCodesCombined}): Promise<{self.returnType.code}>;"
)
[docs]
def dependencies(self):
"""
Return the dependencies of the slot.
Returns:
list: A list of the types that the slot depends on.
"""
# Initialize an empty list to store the dependencies
dep = []
# Loop through the parameters list
for param in self.parameters:
# Extend the dep list with the dependencies of the Parameter object
dep.extend(param.dependencies())
# Extend the dep list with the dependencies of the Return object
dep.extend(self.returnType.dependencies())
# Return the dep list
return dep
[docs]
class Property:
def __init__(self, name: str, typeStr: str) -> None:
"""
Initialize a Property object.
Args:
name (str): The name of the property.
typeStr (str): The type of the property.
"""
# Assign the name and type attributes
self.name = name
self.type = typeStr
# Initialize the code attribute as an empty string
self.code = ""
def __repr__(self) -> str:
"""
Return the code or a string representation of the property.
Returns:
str: The code or a string representation of the property.
"""
# Check if the code attribute is not empty
if self.code != "":
# Return the code attribute
return self.code
# Return a formatted string with the name and type attributes
return f"Property({self.name}, {self.type})"
[docs]
def convertType(self) -> None:
"""
Convert the type attribute to a TypeScript compatible type.
"""
# Use the convertType method from the Utils class to assign the type attribute
self.type = Utils.convertType(self.type)
[docs]
def convertCode(self) -> None:
"""
Generate the code attribute for the property.
"""
# Use the name and type attributes to assign the code attribute as a TypeScript property declaration
self.code = f"{self.name}: {self.type};"
[docs]
def dependencies(self):
"""
Return the dependencies of the property.
Returns:
list: A list of the types that the property depends on.
"""
# Return a list with the type attribute
return [self.type.replace("[]", "")]
[docs]
class Signal:
def __init__(
self, name: str, parameters: list[Parameter], returnType: Return
) -> None:
"""
Initialize a Signal object.
Args:
name (str): The name of the signal.
parameters (list[Parameter]): List of Parameter objects that represent parameters of the signal function.
returnType (Return): A Return object that represents the return type of the signal function.
"""
# Assign the name, parameters and returnType attributes
self.name = name
self.parameters = parameters
self.returnType = returnType
# Initialize the code attribute as an empty string
self.code = ""
def __repr__(self) -> str:
"""
Return the code or a string representation of the signal.
Returns:
str: The code or a string representation of the signal.
"""
# Check if the code attribute is not empty
if self.code != "":
# Return the code attribute
return self.code
# Return a formatted string with the name, parameters and returnType attributes
return f"Signal( {self.name}, {self.parameters}, {self.returnType} )"
[docs]
def convertType(self) -> None:
"""
Convert the type attributes of the parameters and the returnType to TypeScript compatible types.
"""
# Loop through the parameters list
for param in self.parameters:
# Call the convertType method of the Parameter object
param.convertType()
# Call the convertType method of the Return object
self.returnType.convertType()
[docs]
def convertCode(self) -> None:
"""
Generate the code attribute for the signal.
"""
# Define a helper function to convert a Parameter object to a TypeScript parameter declaration
def paramConvert(param: Parameter) -> str:
# Call the convertCode method of the Parameter object
param.convertCode()
# Return the code attribute of the Parameter object
return param.code
# Use the map function to apply the paramConvert function to each element of the parameters list
# Use the join method to combine the resulting list into a string separated by commas
paramCodesCombined = ", ".join([*map(paramConvert, self.parameters)])
# Call the convertCode method of the Return object
self.returnType.convertCode()
# Use the name, paramCodesCombined and returnType attributes to assign the code attribute as a TypeScript
# signal declaration
self.code = (
f"{self.name}: Signal<({paramCodesCombined}) => {self.returnType.code}>;"
)
[docs]
def dependencies(self):
"""
Return the dependencies of the signal.
Returns:
list: A list of the types that the signal depends on.
"""
# Initialize an empty list to store the dependencies
dep = []
# Loop through the parameters list
for param in self.parameters:
# Extend the dep list with the dependencies of the Parameter object
dep.extend(param.dependencies())
# Extend the dep list with the dependencies of the Return object
dep.extend(self.returnType.dependencies())
# Append the string "Signal" to the dep list
dep.append("Signal")
# Return the dep list
return dep