diff --git a/astro.config.mjs b/astro.config.mjs index 3d6f086..e721ca8 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -1,16 +1,31 @@ import { defineConfig } from "astro/config"; +import { viteStaticCopy } from "vite-plugin-static-copy"; +import { createLogger } from "vite"; import mdx from "@astrojs/mdx"; import svelte from "@astrojs/svelte"; import tailwind from "@astrojs/tailwind"; import icon from "astro-icon"; +const logger = createLogger(); +const loggerWarn = logger.warn; + +logger.warn = (msg, options) => { + // Ignore warnings from pyodide distribution + if (msg.includes("/public/pyodide/")) return; + loggerWarn(msg, options); +}; + // https://astro.build/config export default defineConfig({ site: "https://x0k.github.io", base: "/ppp", integrations: [tailwind(), icon(), mdx(), svelte()], vite: { + customLogger: logger, + optimizeDeps: { + exclude: ["pyodide"], + }, assetsInclude: ["**/*.wasm"], build: { rollupOptions: { @@ -20,7 +35,16 @@ export default defineConfig({ }, }, }, - plugins: [], + plugins: [ + viteStaticCopy({ + targets: [ + { + src: "node_modules/pyodide/*.*", + dest: "./assets/pyodide", + }, + ], + }), + ], }, markdown: { shikiConfig: { diff --git a/bun.lockb b/bun.lockb index 4180276..9ee446a 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 0437091..8903e12 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "astro-icon": "^1.1.0", "fast-deep-equal": "^3.1.3", "monaco-editor": "^0.49.0", + "pyodide": "^0.26.0", "tailwindcss": "^3.4.3", "typescript": "^5.4.5" }, @@ -29,6 +30,7 @@ "@types/color": "^3.0.6", "color": "^4.2.3", "daisyui": "^4.11.1", - "svelte": "5.0.0-next.133" + "svelte": "5.0.0-next.133", + "vite-plugin-static-copy": "^1.0.5" } } diff --git a/src/adapters/monaco.ts b/src/adapters/monaco.ts index 3caae2c..fd5b1eb 100644 --- a/src/adapters/monaco.ts +++ b/src/adapters/monaco.ts @@ -3,4 +3,6 @@ import { Language } from "@/lib/testing"; export const MONACO_LANGUAGE_ID: Record = { [Language.PHP]: "php", [Language.TypeScript]: "typescript", + [Language.JavaScript]: "javascript", + [Language.Python]: "python", }; diff --git a/src/content/design-patterns/factory/editor.svelte b/src/content/design-patterns/factory/editor.svelte index cc434c0..81c3bde 100644 --- a/src/content/design-patterns/factory/editor.svelte +++ b/src/content/design-patterns/factory/editor.svelte @@ -7,11 +7,13 @@ import { testCases, CaseType, type Inputs, type Outputs, CASE_TYPES } from './test-cases' import { testRunnerFactories as phpTestRunnerFactories } from './php/test-runners' import { tsTestRunnerFactories, jsTestRunnerFactories } from './js/test-runners' + import { testRunnerFactories as pyTestRunnerFactories } from './python/test-runners' const INITIAL_VALUES: Record> = { [Language.PHP]: import('./php/code.php?raw').then(m => m.default), [Language.TypeScript]: import('./js/code.ts?raw').then(m => m.default), [Language.JavaScript]: import('./js/code.js?raw').then(m => m.default), + [Language.Python]: import('./python/code.py?raw').then(m => m.default), } const CASES_FACTORIES: Record TestCasesStates> = { @@ -36,13 +38,20 @@ testCase: testCases[id], testRunner: jsTestRunnerFactories[id], })) as TestCasesStates, + [Language.Python]: () => CASE_TYPES.map(id => ({ + id, + isRunning: false, + lastTestId: -1, + testCase: testCases[id], + testRunner: pyTestRunnerFactories[id], + })) as TestCasesStates, } const defaultLanguage = Language.PHP; { model.setValue(await INITIAL_VALUES[lang]) }} diff --git a/src/content/design-patterns/factory/js/code.ts b/src/content/design-patterns/factory/js/code.ts index 6f36021..dd504b2 100644 --- a/src/content/design-patterns/factory/js/code.ts +++ b/src/content/design-patterns/factory/js/code.ts @@ -4,6 +4,12 @@ const enum PaymentSystemType { CatBank = "cat-bank", } + + + + + + export function case1( type: PaymentSystemType, base: number, @@ -12,6 +18,10 @@ export function case1( throw new Error("Not implemented"); } +const enum PaymentSystemActionType { + Payment = "payment", + Payout = "payout", +} @@ -19,13 +29,6 @@ export function case1( - - -const enum PaymentSystemActionType { - Payment = "payment", - Payout = "payout", -} - export function case2( type: PaymentSystemType, base: number, diff --git a/src/content/design-patterns/factory/python/code.py b/src/content/design-patterns/factory/python/code.py new file mode 100644 index 0000000..adf681b --- /dev/null +++ b/src/content/design-patterns/factory/python/code.py @@ -0,0 +1,38 @@ +from enum import Enum + +class PaymentSystemType(Enum): + PAYPAL = "paypal" + WEBMONEY = "webmoney" + CAT_BANK = "cat-bank" + + + + + + + +def case1( + type: PaymentSystemType, + base: int, + amount: int +) -> int: + raise NotImplementedError + +class PaymentSystemActionType(Enum): + PAYMENT = "payment" + PAYOUT = "payout" + + + + + + + + +def case2( + type: PaymentSystemType, + base: int, + action: PaymentSystemActionType, + amount: int +) -> int: + raise NotImplementedError \ No newline at end of file diff --git a/src/content/design-patterns/factory/python/test-runners.ts b/src/content/design-patterns/factory/python/test-runners.ts new file mode 100644 index 0000000..6cc0e1a --- /dev/null +++ b/src/content/design-patterns/factory/python/test-runners.ts @@ -0,0 +1,40 @@ +import type { TestRunnerFactory } from "@/lib/testing"; +import { PyTestRunner, pyRuntimeFactory } from "@/lib/testing/python"; + +import { + type SimpleTestInput, + type ComplexTestInput, + CaseType, + type Inputs, + type Outputs, +} from "../test-cases"; + +class SimpleTestRunner extends PyTestRunner { + protected caseExecutionCode({ + paymentSystem, + base, + amount, + }: SimpleTestInput): string { + return `case1("${paymentSystem}", ${base}, ${amount})`; + } +} + +class ComplexTestRunner extends PyTestRunner { + protected caseExecutionCode({ + paymentSystem, + base, + action, + amount, + }: ComplexTestInput): string { + return `case2("${paymentSystem}", ${base}, "${action}", ${amount})`; + } +} + +export const testRunnerFactories: { + [k in CaseType]: TestRunnerFactory; +} = { + [CaseType.Simple]: async (code: string) => + new SimpleTestRunner(await pyRuntimeFactory(), code), + [CaseType.Complex]: async (code: string) => + new ComplexTestRunner(await pyRuntimeFactory(), code), +}; diff --git a/src/lib/testing/js/index.ts b/src/lib/testing/js/index.ts index 01361c3..217dd13 100644 --- a/src/lib/testing/js/index.ts +++ b/src/lib/testing/js/index.ts @@ -1,2 +1,3 @@ +export * from "./model"; export * from "./js-test-runner"; export * from "./ts-test-runner"; diff --git a/src/lib/testing/js/js-test-runner.ts b/src/lib/testing/js/js-test-runner.ts index 888862f..bac715e 100644 --- a/src/lib/testing/js/js-test-runner.ts +++ b/src/lib/testing/js/js-test-runner.ts @@ -11,7 +11,7 @@ export abstract class JsTestRunner implements TestRunner { async run(input: I): Promise { const transformedCode = this.transformCode(this.code); - const m = await import(transformedCode); + const m = await import(/* @vite-ignore */ transformedCode); return this.executeTest(m, input); } diff --git a/src/lib/testing/js/model.ts b/src/lib/testing/js/model.ts new file mode 100644 index 0000000..e897135 --- /dev/null +++ b/src/lib/testing/js/model.ts @@ -0,0 +1 @@ +export { version as tsVersion } from "typescript"; diff --git a/src/lib/testing/languages.ts b/src/lib/testing/languages.ts index 917498c..65f97dc 100644 --- a/src/lib/testing/languages.ts +++ b/src/lib/testing/languages.ts @@ -1,13 +1,17 @@ -import { version } from "typescript"; +import { tsVersion } from './js' +import { version as phpVersion } from './php' +import { version as pyVersion } from './python' export enum Language { PHP = "php", + Python = "python", TypeScript = "typescript", JavaScript = "javascript", } export const LANGUAGE_TITLE: Record = { - [Language.PHP]: "PHP 8.3", - [Language.TypeScript]: `TypeScript ${version}`, + [Language.PHP]: `PHP ${phpVersion}`, + [Language.TypeScript]: `TypeScript ${tsVersion}`, + [Language.Python]: `Python ${pyVersion}`, [Language.JavaScript]: "JavaScript", }; diff --git a/src/lib/testing/php/index.ts b/src/lib/testing/php/index.ts index bbb9466..0cdc49e 100644 --- a/src/lib/testing/php/index.ts +++ b/src/lib/testing/php/index.ts @@ -1,3 +1,4 @@ +export * from "./model"; export * from "./php-test-runner"; export * from "./fail-safe-php"; export * from "./php-runtime-factory"; diff --git a/src/lib/testing/php/model.ts b/src/lib/testing/php/model.ts new file mode 100644 index 0000000..994324c --- /dev/null +++ b/src/lib/testing/php/model.ts @@ -0,0 +1 @@ +export const version = "8.3"; diff --git a/src/lib/testing/php/php-runtime-factory.ts b/src/lib/testing/php/php-runtime-factory.ts index 97ffc3e..c399488 100644 --- a/src/lib/testing/php/php-runtime-factory.ts +++ b/src/lib/testing/php/php-runtime-factory.ts @@ -1,3 +1,5 @@ -import { WebPHP } from '@php-wasm/web'; +import { WebPHP } from "@php-wasm/web"; -export const phpRuntimeFactory = () => WebPHP.loadRuntime('8.3') +import { version } from "./model"; + +export const phpRuntimeFactory = () => WebPHP.loadRuntime(version); diff --git a/src/lib/testing/python/index.ts b/src/lib/testing/python/index.ts new file mode 100644 index 0000000..496381b --- /dev/null +++ b/src/lib/testing/python/index.ts @@ -0,0 +1,3 @@ +export * from './model' +export * from './py-test-runner' +export * from './py-runtime-factory' diff --git a/src/lib/testing/python/model.ts b/src/lib/testing/python/model.ts new file mode 100644 index 0000000..db57298 --- /dev/null +++ b/src/lib/testing/python/model.ts @@ -0,0 +1,5 @@ +// This is only package version, maybe we need it in the future +// export { version } from "pyodide"; + +// According to https://github.com/pyodide/pyodide/pull/4435 +export const version = "3.12.1"; diff --git a/src/lib/testing/python/py-runtime-factory.ts b/src/lib/testing/python/py-runtime-factory.ts new file mode 100644 index 0000000..3049531 --- /dev/null +++ b/src/lib/testing/python/py-runtime-factory.ts @@ -0,0 +1,8 @@ +import { loadPyodide } from "pyodide"; + +export const pyRuntimeFactory = () => + loadPyodide({ + indexURL: import.meta.env.DEV + ? undefined + : `${import.meta.env.BASE_URL}/assets/pyodide`, + }); diff --git a/src/lib/testing/python/py-test-runner.ts b/src/lib/testing/python/py-test-runner.ts new file mode 100644 index 0000000..34b1a41 --- /dev/null +++ b/src/lib/testing/python/py-test-runner.ts @@ -0,0 +1,46 @@ +import { loadPyodide } from "pyodide"; +import type { PyProxy } from "pyodide/ffi"; + +import type { TestRunner } from "../testing"; + +function isPyProxy(obj: any): obj is PyProxy { + return typeof obj === "object" && obj; +} + +export abstract class PyTestRunner implements TestRunner { + private proxies: PyProxy[] = []; + + constructor( + protected readonly python: Awaited>, + protected readonly code: string + ) {} + + protected caseExecutionCode(input: I): string { + throw new Error("Not implemented"); + } + + protected transformCode(input: I): string { + return `${this.code}\n${this.caseExecutionCode(input)}`; + } + + protected transformResult(result: any): O { + if (isPyProxy(result)) { + this.proxies.push(result); + return result.toJs({ pyproxies: this.proxies }); + } + return result; + } + + async run(inputIn: I): Promise { + return this.transformResult( + await this.python.runPythonAsync(this.transformCode(inputIn)) + ); + } + + [Symbol.dispose](): void { + for (const p of this.proxies) { + p.destroy(); + } + this.proxies.length = 0; + } +}