#!/usr/bin/env python3 # MIT License # # Copyright (c) 2025 Eugenio Parodi # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import re import json import argparse import inspect from typing import List, Optional, Dict, Tuple, Any def read_file_to_lines(file_path: str) -> List[str]: """ Reads a file and returns a list of strings, one for each line. Args: file_path (str): The path to the file. Returns: list: A list of strings, where each string is a line from the file. Returns None if the file cannot be opened. """ with open(file_path, 'r') as f: lines: List[str] = f.readlines() return lines raise Exception() def write_lines_to_file(lines: List[str], output_path: str) -> bool: """ Writes a list of strings to a file, one string per line. Args: lines (list): The list of strings to write. output_path (str): The path to the output file. Returns: bool: True if the write was successful, False otherwise. """ try: with open(output_path, 'w') as f: for line in lines: f.write(line) return True except Exception as e: print(f"Error: An error occurred while writing to the file: {e}") return False def _index_of(_m:str, lines:List[str]) -> int: for i,l in enumerate(lines): if _m in l: return i raise ValueError(f"End Delimiter '{_m}' not found in the filtered lines") marker_start = "#--FORWARD-AUTOGEN-START--#" marker_end = "#--FORWARD-AUTOGEN-END--#" from TermTk.TTkAbstract.abstractscrollarea import _ForwardData def autogen_methods(data: _ForwardData) -> List[str]: """ Generates a list of method signatures and return types for a given class. Args: data (Dict[str, Any]): A dictionary containing the class name and a list of methods. Example: {'class': 'TTkTexteditView', 'methods': ['clear', 'setText', ...]} Returns: List[str]: A list of strings, where each string is a method signature and return type. """ import TermTk as ttk class_name = data.forwardClass.__name__ signatures: List[str] = [] for method_name in data.methods: try: # Get the method from the class method = getattr(data.forwardClass, method_name) sig = inspect.signature(method) doc = inspect.getdoc(method) return_type = sig.return_annotation params = ', '.join([f"{_p}={_p}" for _p in sig.parameters.keys() if _p != 'self']) source = inspect.getsource(method) # Extract the first line, which should be the method definition lines = source.splitlines() index_func = _index_of(' def ',lines) lines = lines[:index_func+1] doc_indent = " " lines.extend([ doc_indent + f"'''", doc_indent + f".. seealso:: this method is forwarded to :py:meth:`{class_name}.{method_name}`\n", ]) if doc: lines.extend([f"{doc_indent}{_l}" if _l else '' for _l in doc.split('\n')]) lines.append(doc_indent + "'''") # Format the signature string signatures.extend([ *[f"{_l}\n" for _l in lines], # f" def {method_name}{sig}:", ]) if '=kwargs' in params and '=args' in params: signatures.append(f" return {data.instance}.{method_name}(*args, **kwargs)\n") elif '=kwargs' in params: signatures.append(f" return {data.instance}.{method_name}(**kwargs)\n") elif '=args' in params: signatures.append(f" return {data.instance}.{method_name}(*args)\n") else: signatures.append(f" return {data.instance}.{method_name}({params})\n") except AttributeError: print(f"Error: Method '{method_name}' not found in class '{class_name}'.") except Exception as e: print(f"Error: An error occurred while processing method '{method_name}': {e}") return signatures def autogen_signals(data: _ForwardData) -> List[str]: """ Generates a list of signals signatures and return types for a given class. Args: data (Dict[str, Any]): A dictionary containing the class name and a list of methods. Example: {'class': 'TTkTexteditView', 'methods': ['clear', 'setText', ...]} Returns: List[str]: A list of strings, where each string is a signal signature and return type. """ import TermTk as ttk class_name = data.forwardClass.__name__ signatures: List[str] = [] for signal_name in data.signals: try: # Get the method from the class signal = getattr(data.forwardClass, signal_name) # sig = inspect.signature(signal) doc = inspect.getdoc(signal) # return_type = sig.return_annotation # params = ', '.join([f"{_p}={_p}" for _p in sig.parameters.keys() if _p != 'self']) # source = inspect.getsource(signal) # Extract the first line, which should be the method definition # lines = source.splitlines() # index_func = _index_of(' def ',lines) # lines = lines[:index_func+1] doc_lines:List[str] = [] doc_indent = " " doc_lines.extend([ doc_indent + f"'''", doc_indent + f".. seealso:: this method is forwarded to :py:meth:`{class_name}.{signal_name}`\n", ]) if doc: doc_lines.extend([doc_indent + _l for _l in doc.split('\n')]) doc_lines.append(doc_indent + "'''") # Format the signature string signatures.extend([ " @property\n", f" def {signal_name}(self) -> pyTTkSignal:\n", *[f"{_l}\n" for _l in doc_lines], f" return {data.instance}.{signal_name}\n" ]) except AttributeError: print(f"Error: Method '{signal_name}' not found in class '{class_name}'.") except Exception as e: print(f"Error: An error occurred while processing method '{signal_name}': {e}") return signatures def get_classes_with_source_from_module(module) -> List[Dict[str, Any]]: classes_with_source: List[Dict[str, Any]] = [] for name, obj in inspect.getmembers(module): if inspect.isclass(obj) and hasattr(obj,'_ttk_forward'): try: source = inspect.getsource(obj) filename = inspect.getfile(obj) classes_with_source.append({ 'class': obj, 'name': name, 'forward': obj._ttk_forward, 'module': module.__name__, 'filename': filename, 'source': source }) except OSError as e: print(f"Could not get source for class {name}: {e}") except Exception as e: print(f"Unexpected error getting source for class {name}: {e}") return classes_with_source if __name__ == "__main__": parser = argparse.ArgumentParser(description="Read a file and write its lines to another file.") parser.add_argument('--apply', action='store_true', help='Apply the changes') parser.add_argument('--clear', action='store_true', help='Remove the changes') args = parser.parse_args() import TermTk as ttk classes = get_classes_with_source_from_module(ttk) args = parser.parse_args() if classes: for class_data in classes: print(f"Class Name: {class_data['name']}") print(f" Class: {class_data['class']}") print(f" Forward: {class_data['forward']}") print(f" Module: {class_data['module']}") print(f" Filename: {class_data['filename']}") # print(f" Source:\n{class_data['source']}") autogenenerated = autogen_signals(class_data['forward']) autogenenerated += autogen_methods(class_data['forward']) if args.apply: lines = read_file_to_lines(class_data['filename']) index_start = _index_of(marker_start,lines) index_end = _index_of(marker_end,lines) lines[index_start+1:index_end] = autogenenerated write_lines_to_file(lines,class_data['filename']) elif args.clear: lines = read_file_to_lines(class_data['filename']) index_start = _index_of(marker_start,lines) index_end = _index_of(marker_end,lines) lines[index_start+1:index_end] = '' write_lines_to_file(lines,class_data['filename']) else: print(''.join(autogenenerated)) else: print("No classes found in the module.")