import importlib import os from inspect import getmembers, isfunction, signature import typing # Purpose of this file is to act as a shim around all the files in the practical folders. # It allows you to run specific functions and also means they can share common libraries in the lib folder # All functions must have type hints in order to work properly # Never try and do "reflection" in Python, if you can even call it that def main(): practical_week = input("Practical week: ") files = os.listdir(f"./week{practical_week}") if len(files) == 0: print("No files in week!") return elif not f"pract{practical_week}.py" in files: file_list: list[str] = [ x.split(".")[0] for x in files if not os.path.isdir(f"./week{practical_week}/{x}") and os.path.splitext(f"./week{practical_week}/{x}")[1] == ".py" ] for i in range(len(file_list)): print(f"{i}: {file_list[i]}") sel: int = int(input("Select file number: ")) if sel < 0 or sel >= len(file_list): print("Invalid number!") return os.system("cls" if os.name == "nt" else "clear") os.chdir(f"./week{practical_week}") importlib.import_module(f"week{practical_week}.{file_list[sel]}") else: print("Importing module...") func_module = importlib.import_module(f"week{practical_week}.pract{practical_week}") method_list = getmembers(func_module, isfunction) for i in range(len(method_list)): # Default blank explanation if no doc string exists, otherwise format it nicely doc_str: str if method_list[i][1].__doc__ == None: doc_str = "" else: doc_str = f"| {str(method_list[i][1].__doc__).replace('\n', '')}" print(f"{i} | {method_list[i][0]} {doc_str}") func_no = int(input("Select function number: ")) if func_no < 0 or func_no >= len(method_list): print("Invalid number") return # Any file reads are relative to the relevant practical file in it's directory. os.chdir(f"./week{practical_week}") func_sig = signature(method_list[func_no][1]) # If we don't have any parameters, then we can just can the function normally. # Otherwise we loop through the parameters, asking the user for a value and converting it to the appropriate type. # In the case of arrays, we ask the user for multiple array items, before stopping on Ctrl+C followed by enter. # Doesn't support dictionaries yet if len(func_sig.parameters) == 0: os.system("cls" if os.name == "nt" else "clear") val = method_list[func_no][1]() if not isinstance(val, type(None)): print(val) else: params = [] for param_name, param_type in func_sig.parameters.items(): param_val: any = None # If the type annotation is a list, then we have different logic # Below is the logic for a dict # Both only go 1 level deep, no recursion yet. # Finally is the logic for a base type e.g. int, str, float etc. if typing.get_origin(param_type.annotation) is list: # Continuously add arguments of the right type until user presses Ctrl+C followed by Enter param_val: list = [] while True: try: list_item = typing.get_args(param_type.annotation)[0]( input(f"list[{typing.get_args(param_type.annotation)[0].__name__}][{len(param_val)}] -> ") ) param_val.append(list_item) except KeyboardInterrupt: break params.append(param_val) elif typing.get_origin(param_type.annotation) is dict: param_val: dict = {} while True: try: key_type = typing.get_args(param_type.annotation)[0] value_type = typing.get_args(param_type.annotation)[1] key_val = key_type(input(f"dict[{len(param_val)}].key: {key_type.__name__} -> ")) value_val = value_type(input(f"dict[{len(param_val)}].value: {value_type.__name__} -> ")) param_val[key_val] = value_val except KeyboardInterrupt: break params.append(param_val) else: param_val = input(f"{param_name}: {param_type.annotation.__name__} -> ") params.append(param_type.annotation(param_val)) # One liner to clear the screen os.system("cls" if os.name == "nt" else "clear") val = method_list[func_no][1](*params) if not isinstance(val, type(None)): print(val) if __name__ == "__main__": main()