From cf209b88448927d2a917850e8508f8cf1a9cf2a0 Mon Sep 17 00:00:00 2001 From: Shoujian Zheng Date: Fri, 8 Mar 2024 11:24:33 +0800 Subject: [PATCH] feat(deducer): support evaluating the values of literal types and direct calls to data class constructors --- .../deducers/python-pyright/package.json | 6 +- .../deducers/python-pyright/src/index.ts | 39 +++- ...al-type.valid.py => special_type_valid.py} | 0 .../src/test/samples/value_evaluator_valid.py | 58 +++++ .../python-pyright/src/test/test-utils.ts | 31 +++ .../src/test/type-searcher.test.ts | 2 +- .../src/test/value-evaluator.test.ts | 197 ++++++++++++++++ .../python-pyright/src/type-searcher.ts | 6 +- .../deducers/python-pyright/src/type-utils.ts | 25 ++ .../python-pyright/src/value-evaluator.ts | 219 ++++++++++++++++++ examples/quickstart-python/src/main.py | 16 +- pnpm-lock.yaml | 6 + 12 files changed, 595 insertions(+), 10 deletions(-) rename components/deducers/python-pyright/src/test/samples/{special-type.valid.py => special_type_valid.py} (100%) create mode 100644 components/deducers/python-pyright/src/test/samples/value_evaluator_valid.py create mode 100644 components/deducers/python-pyright/src/test/value-evaluator.test.ts create mode 100644 components/deducers/python-pyright/src/value-evaluator.ts diff --git a/components/deducers/python-pyright/package.json b/components/deducers/python-pyright/package.json index e349328b..280470e7 100644 --- a/components/deducers/python-pyright/package.json +++ b/components/deducers/python-pyright/package.json @@ -15,16 +15,18 @@ "scripts": { "build": "bash scripts/prepare-pyright-pkg.sh && tsc", "watch": "npm run build -- --watch", - "test": "jest", - "test:watch": "jest --watch", + "test": "jest --verbose --coverage", + "test:watch": "jest --verbose --coverage --watch", "lint": "eslint .", "clean": "tsc --build --clean" }, "dependencies": { "@plutolang/base": "workspace:^", + "fs-extra": "^11.1.1", "pyright-internal": "file:./libs/pyright-internal" }, "devDependencies": { + "@types/fs-extra": "^11.0.4", "@types/jest": "^29.5.12", "@types/node": "^17.0.45", "jest": "^29.7.0", diff --git a/components/deducers/python-pyright/src/index.ts b/components/deducers/python-pyright/src/index.ts index 0c851a6c..09122e68 100644 --- a/components/deducers/python-pyright/src/index.ts +++ b/components/deducers/python-pyright/src/index.ts @@ -7,7 +7,12 @@ import { LogLevel } from "pyright-internal/dist/common/console"; import * as TextUtils from "./text-utils"; import * as ProgramUtils from "./program-utils"; +import * as TypeConsts from "./type-consts"; import { TypeSearcher } from "./type-searcher"; +import { ArgumentCategory, CallNode } from "pyright-internal/dist/parser/parseNodes"; +import { TypeEvaluator } from "pyright-internal/dist/analyzer/typeEvaluatorTypes"; +import { TypeCategory } from "pyright-internal/dist/analyzer/types"; +import { Value, ValueEvaluator } from "./value-evaluator"; export default class PyrightDeducer extends Deducer { //eslint-disable-next-line @typescript-eslint/no-var-requires @@ -52,7 +57,39 @@ function doTypeSearch(program: Program, sourceFile: SourceFile) { walker.specialNodeMap.forEach((nodes, key) => { console.log("Special Node:", key); nodes.forEach((node) => { - console.log(" ", TextUtils.getTextOfNode(node, sourceFile)); + console.log("/--------------------\\"); + console.log("|", TextUtils.getTextOfNode(node, sourceFile)); + if ( + key === TypeConsts.IRESOURCE_FULL_NAME || + key === TypeConsts.IRESOURCE_INFRA_API_FULL_NAME + ) { + getArgumentValue(node, sourceFile, program.evaluator!); + } + console.log("\\--------------------/\n\n"); }); }); } + +function getArgumentValue( + callNode: CallNode, + sourceFile: SourceFile, + typeEvaluator: TypeEvaluator +) { + callNode.arguments.forEach((arg) => { + console.log("| Argument:"); + console.log("| Text: ", TextUtils.getTextOfNode(arg, sourceFile)); + + const valueNodeType = typeEvaluator.getType(arg.valueExpression); + if (valueNodeType?.category === TypeCategory.Function) { + console.log("| Value is a function, we need to encapsulate it into closures afterward."); + return; + } + + if (arg.argumentCategory === ArgumentCategory.Simple) { + const valueEvaluator = new ValueEvaluator(typeEvaluator); + const value = valueEvaluator.getValue(arg.valueExpression); + console.log("| Value: ", value); + console.log("| Stringified: ", Value.toString(value)); + } + }); +} diff --git a/components/deducers/python-pyright/src/test/samples/special-type.valid.py b/components/deducers/python-pyright/src/test/samples/special_type_valid.py similarity index 100% rename from components/deducers/python-pyright/src/test/samples/special-type.valid.py rename to components/deducers/python-pyright/src/test/samples/special_type_valid.py diff --git a/components/deducers/python-pyright/src/test/samples/value_evaluator_valid.py b/components/deducers/python-pyright/src/test/samples/value_evaluator_valid.py new file mode 100644 index 00000000..1dccd67a --- /dev/null +++ b/components/deducers/python-pyright/src/test/samples/value_evaluator_valid.py @@ -0,0 +1,58 @@ +from dataclasses import dataclass +from typing import Literal + + +@dataclass +class Base: + name: str + age: int + + +@dataclass +class Model: + base: Base + gender: Literal["male", "female"] + nullable: int | None = None + tup: tuple[int, int, int] = (1, 2, 3) + + +# Direct literal value evaluation +num_1 = 1 +str_1 = "str1" +bool_1 = True +null_1 = None + +num_2 = num_1 + 1 +str_2 = str_1 + "str2" "str2_plus" + + +def fn_1(a: int, b: str, c: bool): + pass + + +fn_1(num_2, b=str_1, c=bool_1) + + +# tuple value evaluation +num_tuple_1 = (1, 2, 3) +str_tuple_1 = ("str2", "str3", "str4") +bool_tuple_1 = (True, False, True) + +mix_tuple_1 = (1, "str5", True) +nested_tuple_1 = ((1, 2), ("str6", "str7"), (True, False)) +nested_mix_tuple_1 = ((1, "str8", True), (2, "str9", False)) + +""" +The reason the following expression isn't assigned to a variable is that if it were, the test case +would attempt to evaluate the variable's value. However, since the variable contains a data class +instance, and the value evaluator can't deduce the data class instance, the test case would fail. +""" +(Base(name="name", age=19), Base(name="name2", age=20)) +( + (1, "str10", True), + (2, "str11", False), + (3, "str12", Base(name="name3", age=21)), +) + +# data class value evaluation +Model(base=Base("name", age=19), gender="male", nullable=null_1) diff --git a/components/deducers/python-pyright/src/test/test-utils.ts b/components/deducers/python-pyright/src/test/test-utils.ts index 4bf06c2d..67bdd6c8 100644 --- a/components/deducers/python-pyright/src/test/test-utils.ts +++ b/components/deducers/python-pyright/src/test/test-utils.ts @@ -1,3 +1,5 @@ +import * as os from "os"; +import * as fs from "fs-extra"; import * as path from "path"; import { Uri } from "pyright-internal/dist/common/uri/uri"; import { Program } from "pyright-internal/dist/analyzer/program"; @@ -29,3 +31,32 @@ export function parseFiles(filePaths: string[]) { const sourceFiles = uris.map((uri) => program.getSourceFile(uri)!); return { program, sourceFiles }; } + +export function parseCode(code: string, filename: string = "tmp.py") { + const program = ProgramUtils.createProgram({ + logLevel: LogLevel.Warn, + extraPaths: [ + path.resolve(__dirname, "../../../../../packages/base-py"), + path.resolve(__dirname, "../../../../../packages/pluto-py"), + ], + }); + + const tmpdir = fs.mkdtempSync(path.join(os.tmpdir(), "pyright-deducer-")); + const tmpfile = path.join(tmpdir, filename); + fs.writeFileSync(tmpfile, code); + + const uri = Uri.file(tmpfile); + program.addTrackedFile(uri); + + // eslint-disable-next-line no-empty + while (program.analyze()) {} + + const sourceFile = program.getSourceFile(uri)!; + return { + program, + sourceFile, + clean: () => { + fs.rmSync(path.dirname(sourceFile.getUri().key), { recursive: true }); + }, + }; +} diff --git a/components/deducers/python-pyright/src/test/type-searcher.test.ts b/components/deducers/python-pyright/src/test/type-searcher.test.ts index 7c0588f9..27769aad 100644 --- a/components/deducers/python-pyright/src/test/type-searcher.test.ts +++ b/components/deducers/python-pyright/src/test/type-searcher.test.ts @@ -7,7 +7,7 @@ const SAMPLES_ROOT = path.join(__dirname, "samples"); test("TypeSearcher should correctly identify special types in the parse tree", () => { // Set up - const samplePath = path.join(SAMPLES_ROOT, "special-type.valid.py"); + const samplePath = path.join(SAMPLES_ROOT, "special_type_valid.py"); const { program, sourceFiles } = TestUtils.parseFiles([samplePath]); // Ensure there is only one source file diff --git a/components/deducers/python-pyright/src/test/value-evaluator.test.ts b/components/deducers/python-pyright/src/test/value-evaluator.test.ts new file mode 100644 index 00000000..b53379cc --- /dev/null +++ b/components/deducers/python-pyright/src/test/value-evaluator.test.ts @@ -0,0 +1,197 @@ +import * as path from "path"; +import { ParseTreeWalker } from "pyright-internal/dist/analyzer/parseTreeWalker"; +import { + ExpressionNode, + ParseNode, + ParseNodeType, + isExpressionNode, +} from "pyright-internal/dist/parser/parseNodes"; +import { SourceFile } from "pyright-internal/dist/analyzer/sourceFile"; +import { Value, ValueEvaluator } from "../value-evaluator"; +import * as TextUtils from "../text-utils"; +import * as TestUtils from "./test-utils"; + +const SAMPLES_ROOT = path.join(__dirname, "samples"); + +const DATACLASS_DEF = ` +from dataclasses import dataclass +from typing import Literal + + +@dataclass +class Base: + name: str + age: int + + +@dataclass +class Model: + base: Base + gender: Literal["male", "female"] + nullable: int | None = None + tup: tuple[int, int, int] = (1, 2, 3) +`; + +type Validator = (node: ExpressionNode) => void; + +class ExpressionWalker extends ParseTreeWalker { + constructor( + private readonly sourceFile: SourceFile, + private readonly validator: Validator + ) { + super(); + } + + override visit(node: ParseNode): boolean { + switch (node.nodeType) { + case ParseNodeType.Function: + case ParseNodeType.Import: + case ParseNodeType.ImportAs: + case ParseNodeType.ImportFrom: + case ParseNodeType.ImportFromAs: + case ParseNodeType.Class: + case ParseNodeType.Lambda: + return false; + } + + if (isExpressionNode(node)) { + this.evaluateValue(node); + return false; + } + return true; + } + + private evaluateValue(node: ExpressionNode) { + const nodeText = TextUtils.getTextOfNode(node, this.sourceFile); + if (nodeText?.startsWith("fn_")) { + // Skip the function calls. + return; + } + + this.validator(node); + } +} + +test("ValueEvaluator should correctly evaluate the values", () => { + // Set up + const samplePath = path.join(SAMPLES_ROOT, "value_evaluator_valid.py"); + const { program, sourceFiles } = TestUtils.parseFiles([samplePath]); + + // Ensure there is only one source file + expect(sourceFiles.length).toEqual(1); + + // Get the parse tree of the source file + const parseTree = sourceFiles[0].getParseResults()?.parseTree; + expect(parseTree).toBeDefined(); + + const valueEvaluator = new ValueEvaluator(program.evaluator!); + + const walker = new ExpressionWalker(sourceFiles[0]!, (node) => { + const value = valueEvaluator.getValue(node); + expect(value).toBeDefined(); + }); + walker.walk(parseTree!); +}); + +describe("evaluateValueForBuiltin", () => { + test("should throw an error when try to deducer a non-literal type", () => { + const code = ` +from typing import Any +import random + +var_0: Any = 1 +var_1 = random.randint(1, 10) +var_2 = 1.0 +var_3 = bytearray(b"bytearr") +var_4 = b"bytes" +var_5 = {"a": 1} +var_7 = frozenset([1]) +var_8 = [1] +var_9 = {1} +`; + + testInlineCode(code, (valueEvaluator, sourceFile) => { + return (node) => { + const text = TextUtils.getTextOfNode(node, sourceFile); + if (text?.startsWith("var_")) { + expect(() => valueEvaluator.getValue(node)).toThrow(); + } + }; + }); + }); +}); + +describe("evaluateValueForTuple", () => { + test("should throw an error when a type argument is not of category Class", () => { + const code = ` +${DATACLASS_DEF} + +from typing import Any + +any_var: Any = 1 +tuple_1 = (any_var, 2, 3) + +model = Base(name="John", age=25) +tuple_2 = (model,) +`; + + testInlineCode(code, (valueEvaluator, sourceFile) => { + return (node) => { + const text = TextUtils.getTextOfNode(node, sourceFile); + if (text?.startsWith("tuple_") || /^\(.*\)$/g.test(text!)) { + expect(() => valueEvaluator.getValue(node)).toThrow(); + } + }; + }); + }); +}); + +describe("evaluateValueForDataClass", () => { + test("should throw an error when try to deducer a data class instance", () => { + const code = ` +${DATACLASS_DEF} + +model = Model(Base("name", 25), gender="male") +`; + + testInlineCode(code, (valueEvaluator, sourceFile) => { + return (node) => { + const text = TextUtils.getTextOfNode(node, sourceFile); + if (text === "model") { + expect(() => valueEvaluator.getValue(node)).toThrow(); + } + + if (text === 'Model(Base("name", 25), gender="male")') { + const value = valueEvaluator.getValue(node); + expect(value).toBeDefined(); + + const stringified = Value.toString(value); + expect(stringified.startsWith("tmp.Model")).toBeTruthy(); + + const match = stringified.match(/\(.*\)/); + expect(match).not.toBeNull(); + expect(match![0]).toContain('base=tmp.Base(name="name", age=25)'); + expect(match![0]).toContain('gender="male"'); + expect(match![0]).toContain("nullable=None"); + expect(match![0]).toContain("tup=(1, 2, 3)"); + } + }; + }); + }); +}); + +function testInlineCode( + code: string, + validatorBuilder: (evaluator: ValueEvaluator, sourceFile: SourceFile) => Validator +) { + const { program, sourceFile, clean } = TestUtils.parseCode(code); + + const parseTree = sourceFile.getParseResults()?.parseTree; + expect(parseTree).toBeDefined(); + + const valueEvaluator = new ValueEvaluator(program.evaluator!); + const walker = new ExpressionWalker(sourceFile, validatorBuilder(valueEvaluator, sourceFile)); + walker.walk(parseTree!); + + clean(); +} diff --git a/components/deducers/python-pyright/src/type-searcher.ts b/components/deducers/python-pyright/src/type-searcher.ts index cbf37b24..d67ca130 100644 --- a/components/deducers/python-pyright/src/type-searcher.ts +++ b/components/deducers/python-pyright/src/type-searcher.ts @@ -2,7 +2,7 @@ import assert from "assert"; import { SourceFile } from "pyright-internal/dist/analyzer/sourceFile"; import { ParseTreeWalker } from "pyright-internal/dist/analyzer/parseTreeWalker"; import { TypeEvaluator } from "pyright-internal/dist/analyzer/typeEvaluatorTypes"; -import { CallNode, ParseNode, ParseNodeType } from "pyright-internal/dist/parser/parseNodes"; +import { CallNode, ParseNodeType } from "pyright-internal/dist/parser/parseNodes"; import { ClassType, FunctionType, TypeCategory } from "pyright-internal/dist/analyzer/types"; import * as TypeUtils from "./type-utils"; @@ -15,7 +15,7 @@ import * as TextUtils from "./text-utils"; * special method of a resource object. */ export class TypeSearcher extends ParseTreeWalker { - private readonly _specialNodeMap: Map = new Map(); + private readonly _specialNodeMap: Map = new Map(); constructor( private readonly typeEvaluator: TypeEvaluator, @@ -24,7 +24,7 @@ export class TypeSearcher extends ParseTreeWalker { super(); } - get specialNodeMap(): Map { + get specialNodeMap(): Map { return this._specialNodeMap; } diff --git a/components/deducers/python-pyright/src/type-utils.ts b/components/deducers/python-pyright/src/type-utils.ts index 01fd7ec4..5edfae9a 100644 --- a/components/deducers/python-pyright/src/type-utils.ts +++ b/components/deducers/python-pyright/src/type-utils.ts @@ -105,3 +105,28 @@ function containsMethod(classNode: ClassNode, methodName: string): boolean { walker.walk(classNode); return walker.found; } + +export function getTypeName(type: Type): string { + switch (type.category) { + case TypeCategory.Class: + return `class ${type.details.fullName}`; + case TypeCategory.Function: + return "function"; + case TypeCategory.Module: + return "module"; + case TypeCategory.OverloadedFunction: + return "overloaded function"; + case TypeCategory.Union: + return "union"; + case TypeCategory.Unknown: + return "unknown"; + case TypeCategory.Never: + return "never"; + case TypeCategory.Any: + return "any"; + case TypeCategory.TypeVar: + return "type var"; + case TypeCategory.Unbound: + return "unbound"; + } +} diff --git a/components/deducers/python-pyright/src/value-evaluator.ts b/components/deducers/python-pyright/src/value-evaluator.ts new file mode 100644 index 00000000..602e8817 --- /dev/null +++ b/components/deducers/python-pyright/src/value-evaluator.ts @@ -0,0 +1,219 @@ +import { TypeEvaluator } from "pyright-internal/dist/analyzer/typeEvaluatorTypes"; +import { ExpressionNode, ParseNodeType } from "pyright-internal/dist/parser/parseNodes"; +import { ClassType, LiteralValue, TypeCategory } from "pyright-internal/dist/analyzer/types"; +import * as TypeUtils from "./type-utils"; + +export interface Value { + /** + * If the type is undefined, it means the value is a primitive, including boolean, integer, + * string, and float. + */ + type?: string; + + value?: LiteralValue | Record | Array; +} + +// eslint-disable-next-line @typescript-eslint/no-namespace +export namespace Value { + export function toString(value: Value): string { + if (value.type === undefined) { + // This is a primitive. We can directly return the JSON stringified value. + return JSON.stringify(value.value); + } + + if (value.type === "types.NoneType") { + return "None"; + } + + switch (value.type) { + case "builtins.tuple": { + const values = value.value! as Value[]; + return `(${values.map((v) => Value.toString(v)).join(", ")})`; + } + default: { + // This is a data class object. + const values = value.value! as Record; + const params = Object.entries(values) + .map(([k, v]) => `${k}=${Value.toString(v)}`) + .join(", "); + return `${value.type}(${params})`; + } + } + } +} + +/** + * This class evaluates the value of an expression node. + * + * For now, we're only able to evaluate the values of literal types and direct calls to data class + * constructors. There are a couple of scenarios where we hit a brick wall: + * + * 1. Values that could change on the fly — think of those unpredictable elements like the output + * from a random function or a timestamp. We just can't evaluate their values during static + * analysis. + * 2. Values that shift depending on where you are in the call chain, such as the arguments + * accessed within a function. Take the example below: we're building a resource object inside + * a function body, and there's just no way for us to deduce the actual value of the variable + * fed into the constructor, which in this case is `queueName`. + * ```python + * def createQueue(queueName: str): + * return Queue(queueName) + * ``` + * + * In the first scenario, since we can't determine the real-time values during static analysis, + * we'll throw an error to flag it. As for the second, we're going to need future updates to trace + * through the call chains and make sense of those values. + */ +export class ValueEvaluator { + constructor(private readonly typeEvaluator: TypeEvaluator) {} + + public getValue(valueNode: ExpressionNode): Value { + // If the node is a literal, we can directly return the value. + switch (valueNode.nodeType) { + case ParseNodeType.Number: + return { value: valueNode.value }; + case ParseNodeType.String: + return { value: valueNode.value }; + } + + // Currently, we only support the variable of literal type. So, we can get the literal value + // from the type of the variable. + const valueType = this.typeEvaluator.getType(valueNode); + if (valueType === undefined) { + throw new Error(`Cannot determine the type of this node; its type is ${valueNode.nodeType}`); + } + + if (valueType.category !== TypeCategory.Class) { + // If the type is not a class, it must not be a literal type or a data class type. + throw new Error( + `Unable to evaluate the value of the '${TypeUtils.getTypeName(valueType)}' type variable.` + ); + } + + return this.evaluateValue(valueType, valueNode); + } + + private evaluateValue(type: ClassType, valueNode?: ExpressionNode): Value { + const typeFullName = type.details.fullName; + + if (ClassType.isBuiltIn(type, "NoneType")) { + return { type: typeFullName }; + } + + if (ClassType.isBuiltIn(type)) { + return this.evaluateValueForBuiltin(type, valueNode); + } + + if (ClassType.isDataClass(type) && valueNode) { + return this.evaluateValueForDataClass(type, valueNode); + } + + throw new Error( + `Currently, we only support literal types and direct calls to data class constructors. The value of a variable with the type '${typeFullName}' cannot be evaluated yet.` + ); + } + + private evaluateValueForBuiltin(type: ClassType, valueNode?: ExpressionNode): Value { + const typeFullName = type.details.fullName; + + let finalValue: Value | undefined; + switch (type.details.fullName) { + case "builtins.bool": + case "builtins.int": + case "builtins.str": + if (type.literalValue === undefined) { + // This is a builtin type, but not a literal type. + throw new Error( + `Currently, we only support bool, int, and str literals as arguments. The '${typeFullName}' is a builtin type, but not a literal type.` + ); + } + finalValue = { value: type.literalValue }; + break; + + case "builtins.tuple": + finalValue = this.evaluateValueForTuple(type, valueNode); + break; + case "builtins.float": + case "builtins.bytearray": + case "builtins.bytes": + case "builtins.dict": + case "builtins.frozenset": + case "builtins.list": + case "builtins.set": + throw new Error( + `We don't support type '${typeFullName}' as argument yet. Currently, we only support bool, int, and str literals as arguments.` + ); + } + return finalValue!; + } + + /** + * For the tuple type, if the expression node is a tuple construction expression, we'll evaluate + * the value of each child expression in the tuple. If it's a tuple variable, we'll attempt to + * evaluate its value based on its type. + */ + private evaluateValueForTuple(type: ClassType, valueNode?: ExpressionNode): Value { + const values: Value[] = []; + if (valueNode && valueNode.nodeType === ParseNodeType.Tuple) { + // The expression node is a tuple construction expression. + valueNode.expressions.forEach((expression) => { + values.push(this.getValue(expression)); + }); + } else { + // The expression node is a variable of tuple type. + const tupleTypeArguments = type.tupleTypeArguments!; + tupleTypeArguments.forEach((type) => { + if (type.type.category !== TypeCategory.Class) { + throw new Error(`We don't support the type '${TypeUtils.getTypeName(type.type)}' yet.`); + } + + // Here, we have no way to access the expression node corresponding to the `type.type`, + // because the argument `valueNode` is a tuple variable, not the tuple construction + // expression. + values.push(this.evaluateValue(type.type)); + }); + } + return { type: type.details.fullName, value: values }; + } + + /** + * For the data class type, we extract the values of this instance to a dictionary. The values + * include the arguments passed to the constructor and the default values of the data class. + */ + private evaluateValueForDataClass(type: ClassType, valueNode: ExpressionNode): Value { + if (valueNode.nodeType !== ParseNodeType.Call) { + /** + * We can deduce the cases as shown the first and second lines of the code below. However, the + * third line is not supported. Therefore, if we encounter a situation where we need to deduce + * the data class instance but not the constructor, we'll throw an error. + * ``` + * var1 = BaseModel(param=1) + * var2 = TopModel(base=BaseModel(param=1)) + * var3 = TopModel(base=var1) + * ``` + */ + throw new Error( + `We only support deducing the data class constructor directly, not the instance of the data class. This is because the instance of the data class may change its value after the constructor.` + ); + } + + const values: Record = {}; + + // If the data class has default values, we evaluate the default values first. + const entries = ClassType.getDataClassEntries(type); + entries.forEach((entry) => { + if (entry.hasDefault) { + const name = entry.name; + values[name] = this.getValue(entry.defaultValueExpression!); + } + }); + + // Then we evaluate the values of the arguments passed to the constructor. + valueNode.arguments.forEach((arg, idx) => { + const name = arg.name?.value ?? entries[idx].name; + values[name] = this.getValue(arg.valueExpression); + }); + + return { type: type.details.fullName, value: values }; + } +} diff --git a/examples/quickstart-python/src/main.py b/examples/quickstart-python/src/main.py index 206d1ee5..5866f573 100644 --- a/examples/quickstart-python/src/main.py +++ b/examples/quickstart-python/src/main.py @@ -1,6 +1,14 @@ import json from datetime import datetime -from pluto_client import KVStore, Queue, Router, HttpRequest, HttpResponse, Function, FunctionOptions +from pluto_client import ( + KVStore, + Queue, + Router, + HttpRequest, + HttpResponse, + Function, + FunctionOptions, +) router = Router("router") @@ -14,7 +22,7 @@ def hello_handler(req: HttpRequest) -> HttpResponse: name = ",".join(name) message = f"{name} access at {int(datetime.now().timestamp() * 1000)}" queue.push(json.dumps({"name": name, "message": message})) - return HttpResponse(200, f"Publish a message: {message}.") + return HttpResponse(status_code=200, body=f"Publish a message: {message}.") def store_handler(req: HttpRequest) -> HttpResponse: @@ -22,7 +30,9 @@ def store_handler(req: HttpRequest) -> HttpResponse: if isinstance(name, list): name = ",".join(name) message = kvstore.get(str(name)) - return HttpResponse(200, f"Fetch {name} access message: {message}.") + return HttpResponse( + status_code=200, body=f"Fetch {name} access message: {message}." + ) def handle_queue_event(event_data): diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cdb23298..20bd68be 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -176,10 +176,16 @@ importers: '@plutolang/base': specifier: workspace:^ version: link:../../../packages/base + fs-extra: + specifier: ^11.1.1 + version: 11.1.1 pyright-internal: specifier: file:./libs/pyright-internal version: file:components/deducers/python-pyright/libs/pyright-internal devDependencies: + '@types/fs-extra': + specifier: ^11.0.4 + version: 11.0.4 '@types/jest': specifier: ^29.5.12 version: 29.5.12