Source code for PyXA.Additions.Utils

""".. versionadded:: 0.1.1

A collection of classes for interacting with macOS features in various ways.
"""

import time
import AppKit
import ScriptingBridge
import xml.etree.ElementTree as ET
from typing import Union, Callable, Any, Literal
from PyObjCTools import AppHelper
from PyXA import XABase
from datetime import datetime, timedelta
from enum import Enum
from time import sleep

import PyXA.XABase
import PyXA.XABaseScriptable
from PyXA.XAErrors import ApplicationNotFoundError


[docs] class AppBuilder: """A class for constructing on-the-fly PyXA Application classes for scriptable applications that do not have a pre-defined class. .. warning:: This is an experimental feature and may not work as expected. .. versionadded:: 0.3.0 """ def __init__(self, name: str): """Initializes a new AppBuilder instance. Does not create the associated application class. :param name: The name of the application to build a class for. :type name: str .. versionadded:: 0.3.0 """ self.name = name self.xa_elem = None self.xa_scel = None app_bundle_path = self.__xa_get_path_to_app(name) app_bundle_url = AppKit.NSURL.fileURLWithPath_(app_bundle_path) app_bundle = AppKit.NSBundle.bundleWithURL_(app_bundle_url) app_bundle_id = app_bundle.bundleIdentifier() self.sdef_path = app_bundle.pathsForResourcesOfType_inDirectory_( "sdef", "" ).firstObject() self._xa_wksp = AppKit.NSWorkspace.sharedWorkspace() url = self._xa_wksp.URLForApplicationWithBundleIdentifier_(app_bundle_id) config = AppKit.NSWorkspaceOpenConfiguration.alloc().init() config.setActivates_(False) config.setHides_(True) app_ref = None def _launch_completion_handler(app, _error): nonlocal app_ref self.xa_elem = ( AppKit.NSRunningApplication.runningApplicationsWithBundleIdentifier_( app_bundle_id ).firstObject() ) if self.sdef_path is not None: self.xa_scel = ScriptingBridge.SBApplication.applicationWithURL_( app_bundle_url ) app_ref = 1 self._xa_wksp.openApplicationAtURL_configuration_completionHandler_( url, config, _launch_completion_handler ) while app_ref is None: time.sleep(0.01) def __xa_get_path_to_app(self, app_identifier: str) -> str: app_paths = self.__xa_load_app_paths() candidate = None for path in app_paths: app_path_component = path.split("/")[-1][:-4] if ( app_identifier.lower() == path.lower() or app_identifier.lower() == app_path_component.lower() ): return path if app_identifier.lower() in path.lower(): candidate = path if candidate is not None: return candidate raise ApplicationNotFoundError(app_identifier) def __xa_load_app_paths(self): search = XABase.XASpotlight() search.predicate = "kMDItemContentType == 'com.apple.application-bundle'" search.run() return [x.path for x in search.results]
[docs] def application(self): """Creates and instantiates a new PyXA Application class for the application specified in the AppBuilder's name attribute. :return: An instance of the newly created PyXA Application class. :rtype: PyXA.Application """ parser = SDEFParser(self.sdef_path) parser.parse() app_class = XABase.XAApplication classes = {} def create_class(class_name: str, parent_classes: list[type], class_dict: dict): new_class = type(class_name, parent_classes, class_dict) setattr(self, class_name, new_class) return new_class for suite in parser.scripting_suites: for scripting_class in suite["classes"]: list_class_dict = {} list_class_dict["__doc__"] = ( "A wrapper around lists of " + scripting_class["name"].lower() + "s that employs fast enumeration techniques. All properties of " + scripting_class["name"].lower() + "s can be called as methods on the wrapped list, returning a list containing each " + scripting_class["name"].lower() + "'s value for the property.\n\n.. versionadded:: " + XABase.VERSION ) def __init__(self, *args): super(XABase.XAList, self).__init__() self.xa_elem = args[0]["element"] self.xa_scel = args[0]["appref"] self.xa_prnt = args[0]["parent"] self.xa_ocls = args[1] list_class_dict["__init__"] = lambda self, properties, filter: __init__( self, properties, classes[scripting_class["name"]], filter ) for property in scripting_class["properties"]: list_class_dict[ property["name"] ] = lambda self, property=property: list( self.xa_elem.arrayByApplyingSelector_(property["name"]) ) cls = create_class( scripting_class["name"].replace(" ", "") + "List", (XABase.XAList,), list_class_dict, ) classes[scripting_class["name"].replace(" ", "") + "List"] = cls class_dict = {} class_dict["__doc__"] = ( scripting_class["comment"] + "\n\n.. versionadded:: " + XABase.VERSION ) for property in scripting_class["properties"]: class_dict[ property["name"] ] = lambda self, property=property: self.xa_elem.__getattribute__( property["name"] )() for element in scripting_class["elements"]: class_dict[ element["name"] ] = lambda self, filter=None, element=element: self._new_element( self.xa_elem.__getattribute__(element["name"])(), classes[element["type"].replace(" ", "") + "List"], filter, ) for command in scripting_class["responds-to"]: if command in suite["commands"]: class_dict[ suite["commands"][command]["name"] ] = lambda self, suite=suite, **kwargs: self.xa_elem.__getattribute__( suite["commands"][command]["name"] )( **kwargs ) cls = create_class( scripting_class["name"], (XABase.XAObject,), class_dict ) classes[scripting_class["name"]] = cls if scripting_class["name"].endswith("Application"): app_class = cls properties = { "parent": None, "element": self.xa_scel, "appref": self.xa_elem, } return app_class(properties)
[docs] class SDEFParser: """A class for parsing SDEF files and generating Python code for interacting with scriptable applications. .. versionadded:: 0.1.1 """ def __init__(self, sdef_file: Union["XABase.XAPath", str]): """Initializes a new SDEFParser instance. :param sdef_file: The full path to the SDEF file to parse. :type sdef_file: Union[XABase.XAPath, str] .. versionadded:: 0.1.1 """ if isinstance(sdef_file, str): sdef_file = XABase.XAPath(sdef_file) self.file = sdef_file #: The full path to the SDEF file to parse self.app_name = "" self.scripting_suites = []
[docs] def parse(self): """Parses the SDEF file specified in the SDEFParser's file attribute. :return: A list of scripting suites, each containing a list of classes and a dictionary of commands. :rtype: list[dict[str, Any]] .. versionadded:: 0.1.1 """ app_name = self.file.path.split("/")[-1][:-5].title() xa_prefix = "XA" + app_name tree = ET.parse(self.file.path) suites = [] scripting_suites = tree.findall("suite") for suite in scripting_suites: classes = [] commands = {} ### Class Extensions class_extensions = suite.findall("class-extension") for extension in class_extensions: properties = [] elements = [] responds_to_commands = [] class_name = xa_prefix + extension.attrib.get("extends", "").title() class_comment = extension.attrib.get("description", "") ## Class Extension Properties class_properties = extension.findall("property") for property in class_properties: property_type = property.attrib.get("type", "") if property_type == "text": property_type = "str" elif property_type == "boolean": property_type = "bool" elif property_type == "number": property_type = "float" elif property_type == "integer": property_type = "int" elif property_type == "rectangle": property_type = "tuple[int, int, int, int]" else: property_type = "XA" + app_name + property_type.title() property_name = ( property.attrib.get("name", "").replace(" ", "_").lower() ) property_comment = property.attrib.get("description", "") properties.append( { "type": property_type, "name": property_name, "comment": property_comment, } ) ## Class Extension Elements class_elements = extension.findall("element") for element in class_elements: element_name = ( (element.attrib.get("type", "") + "s").replace(" ", "_").lower() ) element_type = ( "XA" + app_name + element.attrib.get("type", "").title() ) elements.append({"name": element_name, "type": element_type}) ## Class Extension Responds-To Commands class_responds_to_commands = extension.findall("responds-to") for command in class_responds_to_commands: command_name = ( command.attrib.get("command", "").replace(" ", "_").lower() ) responds_to_commands.append(command_name) classes.append( { "name": class_name, "comment": class_comment, "properties": properties, "elements": elements, "responds-to": responds_to_commands, } ) ### Classes scripting_classes = suite.findall("class") for scripting_class in scripting_classes: properties = [] elements = [] responds_to_commands = [] class_name = xa_prefix + scripting_class.attrib.get("name", "").title() class_comment = scripting_class.attrib.get("description", "") ## Class Properties class_properties = scripting_class.findall("property") for property in class_properties: property_type = property.attrib.get("type", "") if property_type == "text": property_type = "str" elif property_type == "boolean": property_type = "bool" elif property_type == "number": property_type = "float" elif property_type == "integer": property_type = "int" elif property_type == "rectangle": property_type = "tuple[int, int, int, int]" else: property_type = "XA" + app_name + property_type.title() property_name = ( property.attrib.get("name", "").replace(" ", "_").lower() ) property_comment = property.attrib.get("description", "") properties.append( { "type": property_type, "name": property_name, "comment": property_comment, } ) ## Class Elements class_elements = scripting_class.findall("element") for element in class_elements: element_name = ( (element.attrib.get("type", "") + "s").replace(" ", "_").lower() ) element_type = ( "XA" + app_name + element.attrib.get("type", "").title() ) elements.append({"name": element_name, "type": element_type}) ## Class Responds-To Commands class_responds_to_commands = scripting_class.findall("responds-to") for command in class_responds_to_commands: command_name = ( command.attrib.get("command", "").replace(" ", "_").lower() ) responds_to_commands.append(command_name) classes.append( { "name": class_name, "comment": class_comment, "properties": properties, "elements": elements, "responds-to": responds_to_commands, } ) ### Commands script_commands = suite.findall("command") for command in script_commands: command_name = command.attrib.get("name", "").lower().replace(" ", "_") command_comment = command.attrib.get("description", "") parameters = [] direct_param = command.find("direct-parameter") if direct_param is not None: direct_parameter_type = direct_param.attrib.get("type", "") if direct_parameter_type == "specifier": direct_parameter_type = "XABase.XAObject" direct_parameter_comment = direct_param.attrib.get("description") parameters.append( { "name": "direct_param", "type": direct_parameter_type, "comment": direct_parameter_comment, } ) if not "_" in command_name and len(parameters) > 0: command_name += "_" command_parameters = command.findall("parameter") for parameter in command_parameters: parameter_type = parameter.attrib.get("type", "") if parameter_type == "specifier": parameter_type = "XAObject" parameter_name = ( parameter.attrib.get("name", "").lower().replace(" ", "_") ) parameter_comment = parameter.attrib.get("description", "") parameters.append( { "name": parameter_name, "type": parameter_type, "comment": parameter_comment, } ) commands[command_name] = { "name": command_name, "comment": command_comment, "parameters": parameters, } suites.append({"classes": classes, "commands": commands}) self.scripting_suites = suites return suites
[docs] def export(self, output_file: Union["XABase.XAPath", str]): """Exports the scripting suites parsed from the SDEF file to a Python module. :param output_file: The full path to the file to export module code to. :type output_file: Union[XABase.XAPath, str] """ if isinstance(output_file, XABase.XAPath): output_file = output_file.path lines = [] lines.append("from typing import Any, Callable, Union") lines.append("\nfrom PyXA import XABase") lines.append("from PyXA.XABase import OSType") lines.append("from PyXA import XABaseScriptable") for suite in self.scripting_suites: for scripting_class in suite["classes"]: lines.append("\n\n") lines.append( "class " + scripting_class["name"].replace(" ", "") + "List:" ) lines.append( '\t"""A wrapper around lists of ' + scripting_class["name"].lower() + "s that employs fast enumeration techniques." ) lines.append( "\n\tAll properties of tabs can be called as methods on the wrapped list, returning a list containing each tab's value for the property." ) lines.append("\n\t.. versionadded:: " + XABase.VERSION) lines.append('\t"""') lines.append( "\tdef __init__(self, properties: dict, filter: Union[dict, None] = None):" ) lines.append( "\t\tsuper().__init__(properties, " + scripting_class["name"].replace(" ", "") + ", filter)" ) for property in scripting_class["properties"]: lines.append("") lines.append( "\tdef " + property["name"] + "(self) -> list['" + property["type"].replace(" ", "") + "']:" ) lines.append( '\t\t"""' + property["comment"] + "\n\n\t\t.. versionadded:: " + XABase.VERSION + '\n\t\t"""' ) lines.append( '\t\treturn list(self.xa_elem.arrayByApplyingSelector_("' + property["name"] + '"))' ) for property in scripting_class["properties"]: lines.append("") lines.append( "\tdef by_" + property["name"] + "(self, " + property["name"] + ") -> '" + scripting_class["name"].replace(" ", "") + "':" ) lines.append( '\t\t"""Retrieves the ' + scripting_class["comment"] + "whose " + property["name"] + " matches the given " + property["name"] + ".\n\n\t\t.. versionadded:: " + XABase.VERSION + '\n\t\t"""' ) lines.append( '\t\treturn self.by_property("' + property["name"] + '", ' + property["name"] + ")" ) lines.append("") lines.append("class " + scripting_class["name"].replace(" ", "") + ":") lines.append( '\t"""' + scripting_class["comment"] + "\n\n\t.. versionadded:: " + XABase.VERSION + '\n\t"""' ) for property in scripting_class["properties"]: lines.append("") lines.append("\t@property") lines.append( "\tdef " + property["name"] + "(self) -> '" + property["type"].replace(" ", "") + "':" ) lines.append( '\t\t"""' + property["comment"] + "\n\n\t\t.. versionadded:: " + XABase.VERSION + '\n\t\t"""' ) lines.append("\t\treturn self.xa_elem." + property["name"] + "()") for element in scripting_class["elements"]: lines.append("") lines.append( "\tdef " + element["name"].replace(" ", "") + "(self, filter: Union[dict, None] = None) -> '" + element["type"].replace(" ", "") + "':" ) lines.append( '\t\t"""Returns a list of ' + element["name"] + ", as PyXA objects, matching the given filter." ) lines.append("\n\t\t.. versionadded:: " + XABase.VERSION) lines.append('\t\t"""') lines.append( "\t\tself._new_element(self.xa_elem." + element["name"] + "(), " + element["type"].replace(" ", "") + "List, filter)" ) for command in scripting_class["responds-to"]: if command in suite["commands"]: lines.append("") command_str = ( "\tdef " + suite["commands"][command]["name"] + "(self, " ) for parameter in suite["commands"][command]["parameters"]: command_str += ( parameter["name"] + ": '" + parameter["type"] + "', " ) command_str = command_str[:-2] + "):" lines.append(command_str) lines.append('\t\t"""' + suite["commands"][command]["comment"]) lines.append("\n\t\t.. versionadded:: " + XABase.VERSION) lines.append('\t\t"""') cmd_call_str = ( "self.xa_elem." + suite["commands"][command]["name"] + "(" ) if len(suite["commands"][command]["parameters"]) > 0: for parameter in suite["commands"][command]["parameters"]: cmd_call_str += parameter["name"] + ", " cmd_call_str = cmd_call_str[:-2] + ")" else: cmd_call_str += ")" lines.append("\t\t" + cmd_call_str) data = "\n".join(lines) with open(output_file, "w") as f: f.write(data)