clan-app: working nix run .#clan-app, working open_file with tkinter

This commit is contained in:
Qubasa
2025-01-06 15:34:48 +01:00
parent 06879c1d34
commit 1b1fa8c71b
6 changed files with 37 additions and 63 deletions

View File

@@ -40,7 +40,9 @@ def open_file(
root.withdraw() # Hide the main window root.withdraw() # Hide the main window
root.attributes("-topmost", True) # Bring the dialogs to the front root.attributes("-topmost", True) # Bring the dialogs to the front
file_paths: list[str] | None = None
file_path: str = ""
multiple_files: list[str] = []
if file_request.mode == "open_file": if file_request.mode == "open_file":
file_path = filedialog.askopenfilename( file_path = filedialog.askopenfilename(
@@ -49,12 +51,12 @@ def open_file(
initialfile=file_request.initial_file, initialfile=file_request.initial_file,
filetypes=_apply_filters(file_request.filters), filetypes=_apply_filters(file_request.filters),
) )
file_paths = [file_path]
elif file_request.mode == "select_folder": elif file_request.mode == "select_folder":
file_path = filedialog.askdirectory( file_path = filedialog.askdirectory(
title=file_request.title, initialdir=file_request.initial_folder title=file_request.title, initialdir=file_request.initial_folder
) )
file_paths = [file_path]
elif file_request.mode == "save": elif file_request.mode == "save":
file_path = filedialog.asksaveasfilename( file_path = filedialog.asksaveasfilename(
title=file_request.title, title=file_request.title,
@@ -62,21 +64,21 @@ def open_file(
initialfile=file_request.initial_file, initialfile=file_request.initial_file,
filetypes=_apply_filters(file_request.filters), filetypes=_apply_filters(file_request.filters),
) )
file_paths = [file_path]
elif file_request.mode == "open_multiple_files": elif file_request.mode == "open_multiple_files":
file_paths = list( tresult = filedialog.askopenfilenames(
filedialog.askopenfilenames(
title=file_request.title, title=file_request.title,
initialdir=file_request.initial_folder, initialdir=file_request.initial_folder,
filetypes=_apply_filters(file_request.filters), filetypes=_apply_filters(file_request.filters),
) )
) multiple_files = list(tresult)
if not file_paths: if len(file_path) == 0 and len(multiple_files) == 0:
msg = "No file selected or operation canceled by the user" msg = "No file selected"
raise ValueError(msg) # noqa: TRY301 raise ValueError(msg) # noqa: TRY301
return SuccessDataClass(op_key, status="success", data=file_paths) multiple_files = [file_path] if len(multiple_files) == 0 else multiple_files
return SuccessDataClass(op_key, status="success", data=multiple_files)
except Exception as e: except Exception as e:
log.exception("Error opening file") log.exception("Error opening file")

View File

@@ -29,6 +29,7 @@ def app_run(app_opts: ClanAppOptions) -> int:
setup_logging(logging.DEBUG, root_log_name="clan_cli") setup_logging(logging.DEBUG, root_log_name="clan_cli")
else: else:
setup_logging(logging.INFO, root_log_name=__name__.split(".")[0]) setup_logging(logging.INFO, root_log_name=__name__.split(".")[0])
setup_logging(logging.INFO, root_log_name="clan_cli")
log.debug("Debug mode enabled") log.debug("Debug mode enabled")

View File

@@ -54,7 +54,12 @@ def update_wrapper_signature(wrapper: Callable, wrapped: Callable) -> None:
params = list(sig.parameters.values()) params = list(sig.parameters.values())
# Add 'op_key' parameter # Add 'op_key' parameter
op_key_param = Parameter("op_key", Parameter.KEYWORD_ONLY, annotation=str) op_key_param = Parameter("op_key",
Parameter.KEYWORD_ONLY,
# we add a None default value so that typescript code gen drops the parameter
# FIXME: this is a hack, we should filter out op_key in the typescript code gen
default=None,
annotation=str)
params.append(op_key_param) params.append(op_key_param)
# Create a new signature # Create a new signature
@@ -117,6 +122,17 @@ API.register(open_file)
fn_signature = signature(fn) fn_signature = signature(fn)
abstract_signature = signature(self._registry[fn_name]) abstract_signature = signature(self._registry[fn_name])
# Remove the default argument of op_key from abstract_signature
# FIXME: This is a hack to make the signature comparison work
# because the other hack above where default value of op_key is None in the wrapper
abstract_params = list(abstract_signature.parameters.values())
for i, param in enumerate(abstract_params):
if param.name == "op_key":
abstract_params[i] = param.replace(default=Parameter.empty)
break
abstract_signature = abstract_signature.replace(parameters=abstract_params)
if fn_signature != abstract_signature: if fn_signature != abstract_signature:
msg = f"Expected signature: {abstract_signature}\nActual signature: {fn_signature}" msg = f"Expected signature: {abstract_signature}\nActual signature: {fn_signature}"
raise ClanError(msg) raise ClanError(msg)

View File

@@ -43,27 +43,14 @@ export interface GtkResponse<T> {
op_key: string; op_key: string;
} }
const operations = schema.properties;
const operationNames = Object.keys(operations) as OperationNames[];
export const callApi = <K extends OperationNames>( export const callApi = async <K extends OperationNames>(
method: K, method: K,
args: OperationArgs<K>, args: OperationArgs<K>,
) => { ): Promise<OperationResponse<K>> => {
console.log("Calling API", method, args); console.log("Calling API", method, args);
return (window as any)[method](args); const response = await (window as unknown as Record<OperationNames, (args: OperationArgs<OperationNames>) => Promise<OperationResponse<OperationNames>>>)[method](args);
return response as OperationResponse<K>;
}; };
const deserialize =
<T>(fn: (response: T) => void) =>
(r: unknown) => {
try {
fn(r as T);
} catch (e) {
console.error("Error parsing JSON: ", e);
window.localStorage.setItem("error", JSON.stringify(r));
console.error(r);
console.error("See localStorage 'error'");
alert(`Error parsing JSON: ${e}`);
}
};

View File

@@ -26,7 +26,6 @@ export const client = new QueryClient();
const root = document.getElementById("app"); const root = document.getElementById("app");
window.clan = window.clan || {};
if (import.meta.env.DEV && !(root instanceof HTMLElement)) { if (import.meta.env.DEV && !(root instanceof HTMLElement)) {
throw new Error( throw new Error(

View File

@@ -1,40 +1,9 @@
import { describe, expectTypeOf, it } from "vitest"; import { describe, it } from "vitest";
import { OperationNames, pyApi } from "@/src/api";
describe.concurrent("API types work properly", () => { describe.concurrent("API types work properly", () => {
// Test some basic types // Test some basic types
it("distinct success/error unions", async () => { it("distinct success/error unions", async () => {
const k: OperationNames = "create_clan" as OperationNames; // Just a random key, since
expectTypeOf(pyApi[k].receive).toBeFunction();
expectTypeOf(pyApi[k].receive).parameter(0).toBeFunction();
// receive is a function that takes a function, which takes the response parameter
expectTypeOf(pyApi[k].receive)
.parameter(0)
.parameter(0)
.toMatchTypeOf<
{ status: "success"; data?: any } | { status: "error"; errors: any[] }
>();
});
it("Cannot access data of error response", async () => {
const k: OperationNames = "create_clan" as OperationNames; // Just a random key, since
expectTypeOf(pyApi[k].receive).toBeFunction();
expectTypeOf(pyApi[k].receive).parameter(0).toBeFunction();
expectTypeOf(pyApi[k].receive).parameter(0).parameter(0).toMatchTypeOf<
// @ts-expect-error: data is not defined in error responses
| { status: "success"; data?: any }
| { status: "error"; errors: any[]; data: any }
>();
});
it("Machine list receives a records of names and machine info.", async () => {
expectTypeOf(pyApi.list_inventory_machines.receive)
.parameter(0)
.parameter(0)
.toMatchTypeOf<
| { status: "success"; data: Record<string, object> }
| { status: "error"; errors: any }
>();
}); });
}); });