@azure-tools/typespec-python 0.28.0 → 0.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/scripts/regenerate.js +8 -9
- package/dist/scripts/regenerate.js.map +1 -1
- package/dist/scripts/run-python3.d.ts +2 -0
- package/dist/scripts/run-python3.d.ts.map +1 -0
- package/dist/scripts/run-python3.js +23 -0
- package/dist/scripts/run-python3.js.map +1 -0
- package/dist/scripts/run-tests.d.ts +2 -0
- package/dist/scripts/run-tests.d.ts.map +1 -0
- package/dist/scripts/run-tests.js +50 -0
- package/dist/scripts/run-tests.js.map +1 -0
- package/dist/scripts/system-requirements.d.ts +17 -0
- package/dist/scripts/system-requirements.d.ts.map +1 -0
- package/{scripts/system-requirements.cjs → dist/scripts/system-requirements.js} +80 -97
- package/dist/scripts/system-requirements.js.map +1 -0
- package/dist/src/code-model.d.ts.map +1 -1
- package/dist/src/code-model.js +19 -16
- package/dist/src/code-model.js.map +1 -1
- package/dist/src/emitter.js +1 -1
- package/dist/src/emitter.js.map +1 -1
- package/dist/src/external-process.js +1 -1
- package/dist/src/external-process.js.map +1 -1
- package/dist/src/http.js +23 -18
- package/dist/src/http.js.map +1 -1
- package/dist/src/types.d.ts +7 -1
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/types.js +37 -2
- package/dist/src/types.js.map +1 -1
- package/dist/src/utils.d.ts +1 -1
- package/dist/src/utils.d.ts.map +1 -1
- package/dist/src/utils.js +6 -8
- package/dist/src/utils.js.map +1 -1
- package/generator/pygen/codegen/models/__init__.py +2 -0
- package/generator/pygen/codegen/models/code_model.py +2 -4
- package/generator/pygen/codegen/models/model_type.py +1 -2
- package/generator/pygen/codegen/models/operation.py +13 -16
- package/generator/pygen/codegen/models/primitive_types.py +26 -0
- package/generator/pygen/codegen/models/property.py +1 -9
- package/generator/pygen/codegen/serializers/builder_serializer.py +2 -2
- package/generator/pygen/codegen/serializers/general_serializer.py +0 -1
- package/generator/pygen/codegen/serializers/model_serializer.py +2 -0
- package/generator/pygen/codegen/serializers/sample_serializer.py +19 -12
- package/generator/pygen/codegen/templates/model_base.py.jinja2 +24 -18
- package/generator/pygen/codegen/templates/vendor.py.jinja2 +0 -2
- package/package.json +12 -21
- package/scripts/__pycache__/venvtools.cpython-310.pyc +0 -0
- package/scripts/regenerate.ts +7 -8
- package/scripts/run-python3.ts +25 -0
- package/scripts/run-tests.ts +57 -0
- package/scripts/system-requirements.ts +253 -0
- package/scripts/run-python3.cjs +0 -22
|
@@ -42,7 +42,7 @@ class SampleSerializer(BaseSerializer):
|
|
|
42
42
|
self.operation = operation
|
|
43
43
|
self.sample = sample
|
|
44
44
|
self.file_name = file_name
|
|
45
|
-
self.sample_params =
|
|
45
|
+
self.sample_params = sample.get("parameters", {})
|
|
46
46
|
|
|
47
47
|
def _imports(self) -> FileImportSerializer:
|
|
48
48
|
imports = FileImport(self.code_model)
|
|
@@ -66,8 +66,8 @@ class SampleSerializer(BaseSerializer):
|
|
|
66
66
|
"AzureKeyCredential",
|
|
67
67
|
ImportType.SDKCORE,
|
|
68
68
|
)
|
|
69
|
-
for param in self.operation.parameters.positional:
|
|
70
|
-
if not param.client_default_value and not param.optional and param.
|
|
69
|
+
for param in self.operation.parameters.positional + self.operation.parameters.keyword_only:
|
|
70
|
+
if not param.client_default_value and not param.optional and param.wire_name in self.sample_params:
|
|
71
71
|
imports.merge(param.type.imports_for_sample())
|
|
72
72
|
return FileImportSerializer(imports, True)
|
|
73
73
|
|
|
@@ -80,15 +80,19 @@ class SampleSerializer(BaseSerializer):
|
|
|
80
80
|
elif isinstance(credential_type, KeyCredentialType):
|
|
81
81
|
special_param.update({"credential": 'AzureKeyCredential(key=os.getenv("AZURE_KEY"))'})
|
|
82
82
|
|
|
83
|
-
|
|
84
|
-
p
|
|
83
|
+
params = [
|
|
84
|
+
p
|
|
85
|
+
for p in (
|
|
86
|
+
self.code_model.clients[0].parameters.positional + self.code_model.clients[0].parameters.keyword_only
|
|
87
|
+
)
|
|
88
|
+
if not (p.optional or p.client_default_value)
|
|
85
89
|
]
|
|
86
90
|
client_params = {
|
|
87
91
|
p.client_name: special_param.get(
|
|
88
92
|
p.client_name,
|
|
89
|
-
f'"{self.sample_params.get(p.
|
|
93
|
+
f'"{self.sample_params.get(p.wire_name) or p.client_name.upper()}"',
|
|
90
94
|
)
|
|
91
|
-
for p in
|
|
95
|
+
for p in params
|
|
92
96
|
}
|
|
93
97
|
|
|
94
98
|
return client_params
|
|
@@ -103,15 +107,18 @@ class SampleSerializer(BaseSerializer):
|
|
|
103
107
|
|
|
104
108
|
# prepare operation parameters
|
|
105
109
|
def _operation_params(self) -> Dict[str, Any]:
|
|
106
|
-
|
|
110
|
+
params = [
|
|
111
|
+
p
|
|
112
|
+
for p in (self.operation.parameters.positional + self.operation.parameters.keyword_only)
|
|
113
|
+
if not p.client_default_value
|
|
114
|
+
]
|
|
107
115
|
failure_info = "fail to find required param named {}"
|
|
108
116
|
operation_params = {}
|
|
109
|
-
for param in
|
|
110
|
-
name = param.client_name
|
|
111
|
-
param_value = self.sample_params.get(name)
|
|
117
|
+
for param in params:
|
|
112
118
|
if not param.optional:
|
|
119
|
+
param_value = self.sample_params.get(param.wire_name)
|
|
113
120
|
if not param_value:
|
|
114
|
-
raise Exception(failure_info.format(
|
|
121
|
+
raise Exception(failure_info.format(param.client_name)) # pylint: disable=broad-exception-raised
|
|
115
122
|
operation_params[param.client_name] = self.handle_param(param, param_value)
|
|
116
123
|
return operation_params
|
|
117
124
|
|
|
@@ -489,6 +489,9 @@ def _create_value(rf: typing.Optional["_RestField"], value: typing.Any) -> typin
|
|
|
489
489
|
|
|
490
490
|
class Model(_MyMutableMapping):
|
|
491
491
|
_is_model = True
|
|
492
|
+
# label whether current class's _attr_to_rest_field has been calculated
|
|
493
|
+
# could not see _attr_to_rest_field directly because subclass inherits it from parent class
|
|
494
|
+
_calculated: typing.Set[str] = set()
|
|
492
495
|
|
|
493
496
|
def __init__(self, *args: typing.Any, **kwargs: typing.Any) -> None:
|
|
494
497
|
class_name = self.__class__.__name__
|
|
@@ -521,24 +524,27 @@ class Model(_MyMutableMapping):
|
|
|
521
524
|
return Model(self.__dict__)
|
|
522
525
|
|
|
523
526
|
def __new__(cls, *args: typing.Any, **kwargs: typing.Any) -> Self: # pylint: disable=unused-argument
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
rf.
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
527
|
+
if f"{cls.__module__}.{cls.__qualname__}" not in cls._calculated:
|
|
528
|
+
# we know the last nine classes in mro are going to be 'Model', '_MyMutableMapping', 'MutableMapping',
|
|
529
|
+
# 'Mapping', 'Collection', 'Sized', 'Iterable', 'Container' and 'object'
|
|
530
|
+
mros = cls.__mro__[:-9][::-1] # ignore parents, and reverse the mro order
|
|
531
|
+
attr_to_rest_field: typing.Dict[str, _RestField] = { # map attribute name to rest_field property
|
|
532
|
+
k: v for mro_class in mros for k, v in mro_class.__dict__.items() if k[0] != "_" and hasattr(v, "_type")
|
|
533
|
+
}
|
|
534
|
+
annotations = {
|
|
535
|
+
k: v
|
|
536
|
+
for mro_class in mros
|
|
537
|
+
if hasattr(mro_class, "__annotations__") # pylint: disable=no-member
|
|
538
|
+
for k, v in mro_class.__annotations__.items() # pylint: disable=no-member
|
|
539
|
+
}
|
|
540
|
+
for attr, rf in attr_to_rest_field.items():
|
|
541
|
+
rf._module = cls.__module__
|
|
542
|
+
if not rf._type:
|
|
543
|
+
rf._type = rf._get_deserialize_callable_from_annotation(annotations.get(attr, None))
|
|
544
|
+
if not rf._rest_name_input:
|
|
545
|
+
rf._rest_name_input = attr
|
|
546
|
+
cls._attr_to_rest_field: typing.Dict[str, _RestField] = dict(attr_to_rest_field.items())
|
|
547
|
+
cls._calculated.add(f"{cls.__module__}.{cls.__qualname__}")
|
|
542
548
|
|
|
543
549
|
return super().__new__(cls) # pylint: disable=no-value-for-parameter
|
|
544
550
|
|
|
@@ -70,8 +70,6 @@ FileType = Union[
|
|
|
70
70
|
Tuple[Optional[str], FileContent, Optional[str]],
|
|
71
71
|
]
|
|
72
72
|
|
|
73
|
-
FilesType = Union[Mapping[str, FileType], Sequence[Tuple[str, FileType]]]
|
|
74
|
-
|
|
75
73
|
def serialize_multipart_data_entry(data_entry: Any) -> Any:
|
|
76
74
|
if isinstance(data_entry, (list, tuple, dict, Model)):
|
|
77
75
|
return json.dumps(data_entry, cls=SdkJSONEncoder, exclude_readonly=True)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@azure-tools/typespec-python",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.29.0",
|
|
4
4
|
"author": "Microsoft Corporation",
|
|
5
5
|
"description": "TypeSpec emitter for Python SDKs",
|
|
6
6
|
"homepage": "https://github.com/Azure/autorest.python",
|
|
@@ -38,43 +38,35 @@
|
|
|
38
38
|
"@azure-tools/typespec-autorest": ">=0.45.0 <1.0.0",
|
|
39
39
|
"@azure-tools/typespec-client-generator-core": ">=0.45.1 <1.0.0",
|
|
40
40
|
"@azure-tools/typespec-azure-rulesets": ">=0.45.0 <3.0.0",
|
|
41
|
-
"@typespec/compiler": ">=0.59.
|
|
41
|
+
"@typespec/compiler": ">=0.59.1 <1.0.0",
|
|
42
42
|
"@typespec/http": ">=0.59.0 <1.0.0",
|
|
43
43
|
"@typespec/rest": ">=0.59.0 <1.0.0",
|
|
44
44
|
"@typespec/versioning": ">=0.59.0 <1.0.0",
|
|
45
45
|
"@typespec/openapi": ">=0.59.0 <1.0.0"
|
|
46
46
|
},
|
|
47
|
-
"dependenciesMeta": {
|
|
48
|
-
"@azure-tools/typespec-client-generator-core": {
|
|
49
|
-
"injected": true
|
|
50
|
-
}
|
|
51
|
-
},
|
|
52
47
|
"dependencies": {
|
|
53
48
|
"js-yaml": "~4.1.0",
|
|
54
49
|
"@typespec/openapi3": "~0.59.0",
|
|
55
|
-
"
|
|
56
|
-
"
|
|
57
|
-
"semver": "~7.6.2"
|
|
50
|
+
"semver": "~7.6.2",
|
|
51
|
+
"tsx": "4.17.0"
|
|
58
52
|
},
|
|
59
53
|
"devDependencies": {
|
|
60
54
|
"@azure-tools/typespec-azure-resource-manager": "~0.45.0",
|
|
61
55
|
"@azure-tools/typespec-autorest": "~0.45.0",
|
|
62
|
-
"@azure-tools/cadl-ranch-expect": "~0.
|
|
63
|
-
"@azure-tools/cadl-ranch-specs": "~0.
|
|
56
|
+
"@azure-tools/cadl-ranch-expect": "~0.15.1",
|
|
57
|
+
"@azure-tools/cadl-ranch-specs": "~0.35.4",
|
|
64
58
|
"@types/js-yaml": "~4.0.5",
|
|
65
|
-
"@types/mocha": "~10.0.1",
|
|
66
59
|
"@types/node": "^18.16.3",
|
|
67
60
|
"@types/yargs": "17.0.32",
|
|
61
|
+
"@types/semver": "7.5.8",
|
|
68
62
|
"@typespec/eslint-config-typespec": "~0.55.0",
|
|
69
63
|
"@typespec/openapi": "~0.59.0",
|
|
70
64
|
"c8": "~7.13.0",
|
|
71
|
-
"eslint": "^8.57.0",
|
|
72
|
-
"mocha": "~10.2.0",
|
|
73
65
|
"rimraf": "~5.0.0",
|
|
74
66
|
"typescript": "~5.1.3",
|
|
75
67
|
"@azure-tools/typespec-azure-core": "~0.45.0",
|
|
76
68
|
"@azure-tools/typespec-client-generator-core": "0.45.1",
|
|
77
|
-
"@typespec/compiler": "~0.59.
|
|
69
|
+
"@typespec/compiler": "~0.59.1",
|
|
78
70
|
"@typespec/http": "~0.59.0",
|
|
79
71
|
"@typespec/rest": "~0.59.0",
|
|
80
72
|
"@typespec/versioning": "~0.59.0",
|
|
@@ -82,14 +74,13 @@
|
|
|
82
74
|
"yargs": "~17.2.1"
|
|
83
75
|
},
|
|
84
76
|
"scripts": {
|
|
85
|
-
"clean": "rimraf ./dist ./temp",
|
|
77
|
+
"clean": "rimraf ./dist ./temp ./venv ./node_modules",
|
|
86
78
|
"build": "tsc -p .",
|
|
87
79
|
"watch": "tsc -p . --watch",
|
|
88
|
-
"test": "mocha",
|
|
89
|
-
"test-official": "c8 mocha --forbid-only",
|
|
90
80
|
"lint": "eslint . --ext .ts --max-warnings=0",
|
|
91
81
|
"lint:fix": "eslint . --fix --ext .ts",
|
|
92
|
-
"install": "
|
|
93
|
-
"regenerate": "
|
|
82
|
+
"install": "tsx ./scripts/run-python3.ts ./scripts/install.py",
|
|
83
|
+
"regenerate": "tsx ./scripts/regenerate.ts",
|
|
84
|
+
"test": "tsx ./scripts/run-tests.ts"
|
|
94
85
|
}
|
|
95
86
|
}
|
|
Binary file
|
package/scripts/regenerate.ts
CHANGED
|
@@ -11,7 +11,7 @@ import { fileURLToPath } from "url";
|
|
|
11
11
|
const exec = promisify(execCallback);
|
|
12
12
|
|
|
13
13
|
// Get the directory of the current file
|
|
14
|
-
const PLUGIN_DIR = resolve(fileURLToPath(import.meta.url), "
|
|
14
|
+
const PLUGIN_DIR = resolve(fileURLToPath(import.meta.url), "../../");
|
|
15
15
|
const CADL_RANCH_DIR = resolve(PLUGIN_DIR, "node_modules/@azure-tools/cadl-ranch-specs/http");
|
|
16
16
|
|
|
17
17
|
const EMITTER_OPTIONS: Record<string, Record<string, string> | Record<string, string>[]> = {
|
|
@@ -162,6 +162,12 @@ async function getSubdirectories(baseDir: string, flags: RegenerateFlags): Promi
|
|
|
162
162
|
const mainTspRelativePath = toPosix(relative(baseDir, mainTspPath));
|
|
163
163
|
if (flags.flavor === "unbranded" && mainTspRelativePath.includes("azure")) return;
|
|
164
164
|
|
|
165
|
+
// after xml support, remove this check
|
|
166
|
+
if (mainTspRelativePath.includes("xml")) return;
|
|
167
|
+
|
|
168
|
+
// after fix test generation for nested operation group, remove this check
|
|
169
|
+
if (mainTspRelativePath.includes("client-operation-group")) return;
|
|
170
|
+
|
|
165
171
|
const hasMainTsp = await promises
|
|
166
172
|
.access(mainTspPath)
|
|
167
173
|
.then(() => true)
|
|
@@ -256,13 +262,6 @@ async function regenerate(flags: RegenerateFlagsInput): Promise<boolean> {
|
|
|
256
262
|
}
|
|
257
263
|
}
|
|
258
264
|
|
|
259
|
-
// try {
|
|
260
|
-
// const output = await executeCommand('tsp compile');
|
|
261
|
-
// console.log(`Command output: ${output}`);
|
|
262
|
-
// } catch (error) {
|
|
263
|
-
// console.error(`Command failed: ${error}`);
|
|
264
|
-
// }
|
|
265
|
-
|
|
266
265
|
// PARSE INPUT ARGUMENTS
|
|
267
266
|
const argv = yargs(hideBin(process.argv))
|
|
268
267
|
.option("flavor", {
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// This script wraps logic in @azure-tools/extension to resolve
|
|
2
|
+
// the path to Python 3 so that a Python script file can be run
|
|
3
|
+
// from an npm script in package.json. It uses the same Python 3
|
|
4
|
+
// path resolution algorithm as AutoRest so that the behavior
|
|
5
|
+
// is fully consistent (and also supports AUTOREST_PYTHON_EXE).
|
|
6
|
+
//
|
|
7
|
+
// Invoke it like so: "tsx run-python3.ts script.py"
|
|
8
|
+
|
|
9
|
+
import cp from "child_process";
|
|
10
|
+
import { patchPythonPath } from "./system-requirements.js";
|
|
11
|
+
|
|
12
|
+
async function runPython3(...args: string[]) {
|
|
13
|
+
const command = await patchPythonPath(["python", ...args], {
|
|
14
|
+
version: ">=3.8",
|
|
15
|
+
environmentVariable: "AUTOREST_PYTHON_EXE",
|
|
16
|
+
});
|
|
17
|
+
cp.execSync(command.join(" "), {
|
|
18
|
+
stdio: [0, 1, 2],
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
runPython3(...process.argv.slice(2)).catch((err) => {
|
|
23
|
+
console.error(err.toString()); // eslint-disable-line no-console
|
|
24
|
+
process.exit(1);
|
|
25
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
2
|
+
import { execSync } from "child_process";
|
|
3
|
+
import yargs from "yargs";
|
|
4
|
+
import { hideBin } from "yargs/helpers";
|
|
5
|
+
|
|
6
|
+
interface Arguments {
|
|
7
|
+
folder?: string;
|
|
8
|
+
command?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const validFolders = ["azure", "unbranded"];
|
|
12
|
+
|
|
13
|
+
const validCommands = ["ci", "lint", "mypy", "pyright", "apiview"];
|
|
14
|
+
|
|
15
|
+
// Parse command-line arguments using yargs
|
|
16
|
+
const argv = yargs(hideBin(process.argv))
|
|
17
|
+
.option("folder", {
|
|
18
|
+
alias: "f",
|
|
19
|
+
describe: "Specify the folder to use",
|
|
20
|
+
choices: validFolders,
|
|
21
|
+
type: "string",
|
|
22
|
+
})
|
|
23
|
+
.option("command", {
|
|
24
|
+
alias: "c",
|
|
25
|
+
describe: "Specify the command to run",
|
|
26
|
+
choices: validCommands,
|
|
27
|
+
type: "string",
|
|
28
|
+
}).argv as Arguments;
|
|
29
|
+
|
|
30
|
+
const foldersToProcess = argv.folder ? [argv.folder] : validFolders;
|
|
31
|
+
|
|
32
|
+
const commandToRun = argv.command || "all";
|
|
33
|
+
|
|
34
|
+
function getCommand(command: string, folder: string) {
|
|
35
|
+
if (!validCommands.includes(command)) throw new Error(`Unknown command '${command}'.`);
|
|
36
|
+
return `FOLDER=${folder} tox -c ./test/${folder}/tox.ini -e ${command}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
foldersToProcess.forEach((folder) => {
|
|
40
|
+
try {
|
|
41
|
+
if (commandToRun === "all") {
|
|
42
|
+
for (const key of validCommands) {
|
|
43
|
+
console.log(`Running ${key} for folder ${folder}...`);
|
|
44
|
+
execSync(getCommand(key, folder), { stdio: "inherit" });
|
|
45
|
+
}
|
|
46
|
+
} else if (getCommand(commandToRun, folder)) {
|
|
47
|
+
console.log(`Running ${commandToRun} for folder ${folder}...`);
|
|
48
|
+
execSync(getCommand(commandToRun, folder), { stdio: "inherit" });
|
|
49
|
+
} else {
|
|
50
|
+
console.error(`Error: Unknown command '${commandToRun}'.`);
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
} catch (error) {
|
|
54
|
+
console.error(`Error executing command for folder ${folder}: ${(error as Error).message}`);
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { SpawnOptions, ChildProcess, spawn } from "child_process";
|
|
2
|
+
import { coerce, satisfies } from "semver";
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* Copied from @autorest/system-requirements
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const execute = (command: string, cmdlineargs: Array<string>, options: MoreOptions = {}): Promise<ExecResult> => {
|
|
9
|
+
return new Promise((resolve, reject) => {
|
|
10
|
+
const cp = spawn(command, cmdlineargs, { ...options, stdio: "pipe", shell: true });
|
|
11
|
+
if (options.onCreate) {
|
|
12
|
+
options.onCreate(cp);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
options.onStdOutData ? cp.stdout.on("data", options.onStdOutData) : cp;
|
|
16
|
+
options.onStdErrData ? cp.stderr.on("data", options.onStdErrData) : cp;
|
|
17
|
+
|
|
18
|
+
let err = "";
|
|
19
|
+
let out = "";
|
|
20
|
+
let all = "";
|
|
21
|
+
cp.stderr.on("data", (chunk) => {
|
|
22
|
+
err += chunk;
|
|
23
|
+
all += chunk;
|
|
24
|
+
});
|
|
25
|
+
cp.stdout.on("data", (chunk) => {
|
|
26
|
+
out += chunk;
|
|
27
|
+
all += chunk;
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
cp.on("error", (err) => {
|
|
31
|
+
reject(err);
|
|
32
|
+
});
|
|
33
|
+
cp.on("close", (code, signal) =>
|
|
34
|
+
resolve({
|
|
35
|
+
stdout: out,
|
|
36
|
+
stderr: err,
|
|
37
|
+
log: all,
|
|
38
|
+
error: code ? new Error("Process Failed.") : null,
|
|
39
|
+
code,
|
|
40
|
+
}),
|
|
41
|
+
);
|
|
42
|
+
});
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const versionIsSatisfied = (version: string, requirement: string): boolean => {
|
|
46
|
+
const cleanedVersion = coerce(version);
|
|
47
|
+
if (!cleanedVersion) {
|
|
48
|
+
throw new Error(`Invalid version ${version}.`);
|
|
49
|
+
}
|
|
50
|
+
return satisfies(cleanedVersion, requirement, true);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Validate the provided system requirement resolution is satisfying the version requirement if applicable.
|
|
55
|
+
* @param resolution Command resolution.
|
|
56
|
+
* @param actualVersion Version for that resolution.
|
|
57
|
+
* @param requirement Requirement.
|
|
58
|
+
* @returns the resolution if it is valid or an @see SystemRequirementError if not.
|
|
59
|
+
*/
|
|
60
|
+
const validateVersionRequirement = (
|
|
61
|
+
resolution: SystemRequirementResolution,
|
|
62
|
+
actualVersion: string,
|
|
63
|
+
requirement: SystemRequirement,
|
|
64
|
+
): SystemRequirementResolution | SystemRequirementError => {
|
|
65
|
+
if (!requirement.version) {
|
|
66
|
+
return resolution; // No version requirement.
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
if (versionIsSatisfied(actualVersion, requirement.version)) {
|
|
71
|
+
return resolution;
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
...resolution,
|
|
75
|
+
error: true,
|
|
76
|
+
message: `'${resolution.command}' version is '${actualVersion}' but doesn't satisfy requirement '${requirement.version}'. Please update.`,
|
|
77
|
+
actualVersion: actualVersion,
|
|
78
|
+
neededVersion: requirement.version,
|
|
79
|
+
};
|
|
80
|
+
} catch {
|
|
81
|
+
return {
|
|
82
|
+
...resolution,
|
|
83
|
+
error: true,
|
|
84
|
+
message: `Couldn't parse the version ${actualVersion}. This is not a valid semver version.`,
|
|
85
|
+
actualVersion: actualVersion,
|
|
86
|
+
neededVersion: requirement.version,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const tryPython = async (
|
|
92
|
+
requirement: SystemRequirement,
|
|
93
|
+
command: string,
|
|
94
|
+
additionalArgs: string[] = [],
|
|
95
|
+
): Promise<SystemRequirementResolution | SystemRequirementError> => {
|
|
96
|
+
const resolution: SystemRequirementResolution = {
|
|
97
|
+
name: PythonRequirement,
|
|
98
|
+
command,
|
|
99
|
+
additionalArgs: additionalArgs.length > 0 ? additionalArgs : undefined,
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
const result = await execute(command, [...additionalArgs, "-c", `"${PRINT_PYTHON_VERSION_SCRIPT}"`]);
|
|
104
|
+
return validateVersionRequirement(resolution, result.stdout.trim(), requirement);
|
|
105
|
+
} catch (e) {
|
|
106
|
+
return {
|
|
107
|
+
error: true,
|
|
108
|
+
...resolution,
|
|
109
|
+
message: `'${command}' command line is not found in the path. Make sure to have it installed.`,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Returns the path to the executable as asked in the requirement.
|
|
116
|
+
* @param requirement System requirement definition.
|
|
117
|
+
* @returns If the requirement provide an environment variable for the path returns the value of that environment variable. undefined otherwise.
|
|
118
|
+
*/
|
|
119
|
+
const getExecutablePath = (requirement: SystemRequirement): string | undefined =>
|
|
120
|
+
requirement.environmentVariable && process.env[requirement.environmentVariable];
|
|
121
|
+
|
|
122
|
+
const createPythonErrorMessage = (
|
|
123
|
+
requirement: SystemRequirement,
|
|
124
|
+
errors: SystemRequirementError[],
|
|
125
|
+
): SystemRequirementError => {
|
|
126
|
+
const versionReq = requirement.version ?? "*";
|
|
127
|
+
const lines = [
|
|
128
|
+
`Couldn't find a valid python interpreter satisfying the requirement (version: ${versionReq}). Tried:`,
|
|
129
|
+
...errors.map((x) => ` - ${x.command} (${x.message})`),
|
|
130
|
+
];
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
error: true,
|
|
134
|
+
name: "python",
|
|
135
|
+
command: "python",
|
|
136
|
+
message: lines.join("\n"),
|
|
137
|
+
};
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const resolvePythonRequirement = async (
|
|
141
|
+
requirement: SystemRequirement,
|
|
142
|
+
): Promise<SystemRequirementResolution | SystemRequirementError> => {
|
|
143
|
+
// Hardcoding AUTOREST_PYTHON_EXE is for backward compatibility
|
|
144
|
+
const path = getExecutablePath(requirement) ?? process.env["AUTOREST_PYTHON_EXE"];
|
|
145
|
+
if (path) {
|
|
146
|
+
return await tryPython(requirement, path);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const errors: SystemRequirementError[] = [];
|
|
150
|
+
// On windows try `py` executable with `-3` flag.
|
|
151
|
+
if (process.platform === "win32") {
|
|
152
|
+
const pyResult = await tryPython(requirement, "py", ["-3"]);
|
|
153
|
+
if ("error" in pyResult) {
|
|
154
|
+
errors.push(pyResult);
|
|
155
|
+
} else {
|
|
156
|
+
return pyResult;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const python3Result = await tryPython(requirement, "python3");
|
|
161
|
+
if ("error" in python3Result) {
|
|
162
|
+
errors.push(python3Result);
|
|
163
|
+
} else {
|
|
164
|
+
return python3Result;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const pythonResult = await tryPython(requirement, "python");
|
|
168
|
+
if ("error" in pythonResult) {
|
|
169
|
+
errors.push(pythonResult);
|
|
170
|
+
} else {
|
|
171
|
+
return pythonResult;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return createPythonErrorMessage(requirement, errors);
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* @param command list of the command and arguments. First item in array must be a python exe @see KnownPythonExe. (e.g. ["python", "mypythonfile.py"]
|
|
179
|
+
* @param requirement
|
|
180
|
+
*/
|
|
181
|
+
export const patchPythonPath = async (
|
|
182
|
+
command: PythonCommandLine,
|
|
183
|
+
requirement: SystemRequirement,
|
|
184
|
+
): Promise<string[]> => {
|
|
185
|
+
const [_, ...args] = command;
|
|
186
|
+
const resolution = await resolvePythonRequirement(requirement);
|
|
187
|
+
if ("error" in resolution) {
|
|
188
|
+
throw new Error(`Failed to find compatible python version. ${resolution.message}`);
|
|
189
|
+
}
|
|
190
|
+
return [resolution.command, ...(resolution.additionalArgs ?? []), ...args];
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
194
|
+
// TYPES
|
|
195
|
+
const PythonRequirement = "python";
|
|
196
|
+
const PRINT_PYTHON_VERSION_SCRIPT = "import sys; print('.'.join(map(str, sys.version_info[:3])))";
|
|
197
|
+
|
|
198
|
+
type KnownPythonExe = "python.exe" | "python3.exe" | "python" | "python3";
|
|
199
|
+
type PythonCommandLine = [KnownPythonExe, ...string[]];
|
|
200
|
+
|
|
201
|
+
interface MoreOptions extends SpawnOptions {
|
|
202
|
+
onCreate?(cp: ChildProcess): void;
|
|
203
|
+
onStdOutData?(chunk: any): void;
|
|
204
|
+
onStdErrData?(chunk: any): void;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
interface SystemRequirement {
|
|
208
|
+
version?: string;
|
|
209
|
+
/**
|
|
210
|
+
* Name of an environment variable where the user could provide the path to the exe.
|
|
211
|
+
* @example "AUTOREST_PYTHON_PATH"
|
|
212
|
+
*/
|
|
213
|
+
environmentVariable?: string;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
interface SystemRequirementResolution {
|
|
217
|
+
/**
|
|
218
|
+
* Name of the requirement.
|
|
219
|
+
* @example python, java, etc.
|
|
220
|
+
*/
|
|
221
|
+
name: string;
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Name of the command
|
|
225
|
+
* @example python3, /home/myuser/python39/python, java, etc.
|
|
226
|
+
*/
|
|
227
|
+
command: string;
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* List of additional arguments to pass to this command.
|
|
231
|
+
* @example '-3' for 'py' to specify to use python 3
|
|
232
|
+
*/
|
|
233
|
+
additionalArgs?: string[];
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
interface ExecResult {
|
|
237
|
+
stdout: string;
|
|
238
|
+
stderr: string;
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Union of stdout and stderr.
|
|
242
|
+
*/
|
|
243
|
+
log: string;
|
|
244
|
+
error: Error | null;
|
|
245
|
+
code: number | null;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
interface SystemRequirementError extends SystemRequirementResolution {
|
|
249
|
+
error: true;
|
|
250
|
+
message: string;
|
|
251
|
+
neededVersion?: string;
|
|
252
|
+
actualVersion?: string;
|
|
253
|
+
}
|
package/scripts/run-python3.cjs
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
// This script wraps logic in @azure-tools/extension to resolve
|
|
2
|
-
// the path to Python 3 so that a Python script file can be run
|
|
3
|
-
// from an npm script in package.json. It uses the same Python 3
|
|
4
|
-
// path resolution algorithm as AutoRest so that the behavior
|
|
5
|
-
// is fully consistent (and also supports AUTOREST_PYTHON_EXE).
|
|
6
|
-
//
|
|
7
|
-
// Invoke it like so: "node run-python3.cjs script.py"
|
|
8
|
-
|
|
9
|
-
const cp = require("child_process");
|
|
10
|
-
const extension = require("./system-requirements.cjs");
|
|
11
|
-
|
|
12
|
-
async function runPython3(scriptName, ...args) {
|
|
13
|
-
const command = await extension.patchPythonPath(["python", scriptName, ...args], { version: ">=3.8", environmentVariable: "AUTOREST_PYTHON_EXE" });
|
|
14
|
-
cp.execSync(command.join(" "), {
|
|
15
|
-
stdio: [0, 1, 2]
|
|
16
|
-
});
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
runPython3(...process.argv.slice(2)).catch(err => {
|
|
20
|
-
console.error(err.toString());
|
|
21
|
-
process.exit(1);
|
|
22
|
-
});
|