@autorest/python 6.37.1 → 6.38.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.
Files changed (29) hide show
  1. package/autorest/__init__.py +13 -0
  2. package/autorest/codegen.py +1 -0
  3. package/generator/build/lib/pygen/__init__.py +29 -2
  4. package/generator/build/lib/pygen/codegen/models/code_model.py +32 -0
  5. package/generator/build/lib/pygen/codegen/models/enum_type.py +4 -1
  6. package/generator/build/lib/pygen/codegen/models/model_type.py +4 -1
  7. package/generator/build/lib/pygen/codegen/models/operation.py +3 -4
  8. package/generator/build/lib/pygen/codegen/models/paging_operation.py +1 -1
  9. package/generator/build/lib/pygen/codegen/serializers/__init__.py +87 -72
  10. package/generator/build/lib/pygen/codegen/serializers/builder_serializer.py +4 -3
  11. package/generator/build/lib/pygen/codegen/serializers/general_serializer.py +86 -13
  12. package/generator/build/lib/pygen/codegen/serializers/model_serializer.py +1 -1
  13. package/generator/build/lib/pygen/codegen/templates/packaging_templates/pyproject.toml.jinja2 +109 -0
  14. package/generator/component-detection-pip-report.json +1 -1
  15. package/generator/dist/pygen-0.1.0-py3-none-any.whl +0 -0
  16. package/generator/pygen/__init__.py +29 -2
  17. package/generator/pygen/codegen/models/code_model.py +32 -0
  18. package/generator/pygen/codegen/models/enum_type.py +4 -1
  19. package/generator/pygen/codegen/models/model_type.py +4 -1
  20. package/generator/pygen/codegen/models/operation.py +3 -4
  21. package/generator/pygen/codegen/models/paging_operation.py +1 -1
  22. package/generator/pygen/codegen/serializers/__init__.py +87 -72
  23. package/generator/pygen/codegen/serializers/builder_serializer.py +4 -3
  24. package/generator/pygen/codegen/serializers/general_serializer.py +86 -13
  25. package/generator/pygen/codegen/serializers/model_serializer.py +1 -1
  26. package/generator/pygen/codegen/templates/packaging_templates/pyproject.toml.jinja2 +109 -0
  27. package/generator/pygen.egg-info/SOURCES.txt +1 -0
  28. package/package.json +2 -2
  29. package/scripts/__pycache__/venvtools.cpython-310.pyc +0 -0
@@ -4,7 +4,10 @@
4
4
  # license information.
5
5
  # --------------------------------------------------------------------------
6
6
  import json
7
- from typing import Any, List
7
+ from typing import Any, List, TYPE_CHECKING
8
+ import re
9
+ import tomli as tomllib
10
+ from packaging.version import parse as parse_version
8
11
  from .import_serializer import FileImportSerializer, TypingSection
9
12
  from ..models.imports import MsrestImportType, FileImport
10
13
  from ..models import (
@@ -16,6 +19,9 @@ from ..models.utils import NamespaceType
16
19
  from .client_serializer import ClientSerializer, ConfigSerializer
17
20
  from .base_serializer import BaseSerializer
18
21
 
22
+ if TYPE_CHECKING:
23
+ from pathlib import Path
24
+
19
25
  VERSION_MAP = {
20
26
  "msrest": "0.7.1",
21
27
  "isodate": "0.6.1",
@@ -42,8 +48,73 @@ class GeneralSerializer(BaseSerializer):
42
48
  params.update({"options": self.code_model.options})
43
49
  return template.render(code_model=self.code_model, **params)
44
50
 
45
- def serialize_package_file(self, template_name: str, **kwargs: Any) -> str:
51
+ def _extract_min_dependency(self, s):
52
+ # Extract the minimum version from a dependency string.
53
+ #
54
+ # Handles formats like:
55
+ # - >=1.2.3
56
+ # - >=0.1.0b1 (beta versions)
57
+ # - >=1.2.3rc2 (release candidates)
58
+ #
59
+ # Returns the parsed version if found, otherwise version "0".
60
+ m = re.search(r"[>=]=?([\d.]+(?:[a-z]+\d+)?)", s)
61
+ return parse_version(m.group(1)) if m else parse_version("0")
62
+
63
+ def _keep_pyproject_fields(self, file_path: "Path") -> dict:
64
+ # Load the pyproject.toml file if it exists and extract fields to keep.
65
+ result: dict = {"KEEP_FIELDS": {}}
66
+ try:
67
+ with open(file_path, "rb") as f:
68
+ loaded_pyproject_toml = tomllib.load(f)
69
+ except Exception: # pylint: disable=broad-except
70
+ # If parsing the pyproject.toml fails, we assume the it does not exist or is incorrectly formatted.
71
+ return result
72
+
73
+ # Keep azure-sdk-build configuration
74
+ if "tool" in loaded_pyproject_toml and "azure-sdk-build" in loaded_pyproject_toml["tool"]:
75
+ result["KEEP_FIELDS"]["tool.azure-sdk-build"] = loaded_pyproject_toml["tool"]["azure-sdk-build"]
76
+
77
+ # Process dependencies
78
+ if "project" in loaded_pyproject_toml:
79
+ # Handle main dependencies
80
+ if "dependencies" in loaded_pyproject_toml["project"]:
81
+ kept_deps = []
82
+ for dep in loaded_pyproject_toml["project"]["dependencies"]:
83
+ dep_name = re.split(r"[<>=\[]", dep)[0].strip()
84
+
85
+ # Check if dependency is one we track in VERSION_MAP
86
+ if dep_name in VERSION_MAP:
87
+ # For tracked dependencies, check if the version is higher than our default
88
+ default_version = parse_version(VERSION_MAP[dep_name])
89
+ dep_version = self._extract_min_dependency(dep)
90
+ # If the version is higher than the default, update VERSION_MAP
91
+ # with higher min dependency version
92
+ if dep_version > default_version:
93
+ VERSION_MAP[dep_name] = str(dep_version)
94
+ else:
95
+ # Keep non-default dependencies
96
+ kept_deps.append(dep)
97
+
98
+ if kept_deps:
99
+ result["KEEP_FIELDS"]["project.dependencies"] = kept_deps
100
+
101
+ # Keep optional dependencies
102
+ if "optional-dependencies" in loaded_pyproject_toml["project"]:
103
+ result["KEEP_FIELDS"]["project.optional-dependencies"] = loaded_pyproject_toml["project"][
104
+ "optional-dependencies"
105
+ ]
106
+
107
+ return result
108
+
109
+ def serialize_package_file(self, template_name: str, file_path: "Path", **kwargs: Any) -> str:
46
110
  template = self.env.get_template(template_name)
111
+
112
+ # Add fields to keep from an existing pyproject.toml
113
+ if template_name == "pyproject.toml.jinja2":
114
+ params = self._keep_pyproject_fields(file_path)
115
+ else:
116
+ params = {}
117
+
47
118
  package_parts = (
48
119
  self.code_model.namespace.split(".")[:-1]
49
120
  if self.code_model.is_tsp
@@ -57,17 +128,19 @@ class GeneralSerializer(BaseSerializer):
57
128
  dev_status = "4 - Beta"
58
129
  else:
59
130
  dev_status = "5 - Production/Stable"
60
- params = {
61
- "code_model": self.code_model,
62
- "dev_status": dev_status,
63
- "token_credential": token_credential,
64
- "pkgutil_names": [".".join(package_parts[: i + 1]) for i in range(len(package_parts))],
65
- "init_names": ["/".join(package_parts[: i + 1]) + "/__init__.py" for i in range(len(package_parts))],
66
- "client_name": self.code_model.clients[0].name if self.code_model.clients else "",
67
- "VERSION_MAP": VERSION_MAP,
68
- "MIN_PYTHON_VERSION": MIN_PYTHON_VERSION,
69
- "MAX_PYTHON_VERSION": MAX_PYTHON_VERSION,
70
- }
131
+ params.update(
132
+ {
133
+ "code_model": self.code_model,
134
+ "dev_status": dev_status,
135
+ "token_credential": token_credential,
136
+ "pkgutil_names": [".".join(package_parts[: i + 1]) for i in range(len(package_parts))],
137
+ "init_names": ["/".join(package_parts[: i + 1]) + "/__init__.py" for i in range(len(package_parts))],
138
+ "client_name": self.code_model.clients[0].name if self.code_model.clients else "",
139
+ "VERSION_MAP": VERSION_MAP,
140
+ "MIN_PYTHON_VERSION": MIN_PYTHON_VERSION,
141
+ "MAX_PYTHON_VERSION": MAX_PYTHON_VERSION,
142
+ }
143
+ )
71
144
  params.update({"options": self.code_model.options})
72
145
  params.update(kwargs)
73
146
  return template.render(file_import=FileImport(self.code_model), **params)
@@ -266,7 +266,7 @@ class DpgModelSerializer(_ModelSerializer):
266
266
  )
267
267
  if model.is_polymorphic:
268
268
  file_import.add_submodule_import("typing", "Dict", ImportType.STDLIB)
269
- if not model.internal and self.init_line(model):
269
+ if self.need_init(model):
270
270
  file_import.add_submodule_import("typing", "overload", ImportType.STDLIB)
271
271
  file_import.add_submodule_import("typing", "Mapping", ImportType.STDLIB)
272
272
  file_import.add_submodule_import("typing", "Any", ImportType.STDLIB)
@@ -0,0 +1,109 @@
1
+ {% set min_version = MIN_PYTHON_VERSION.split('.')[1] | int %}
2
+ {% set max_version = MAX_PYTHON_VERSION.split('.')[1] | int %}
3
+ {% if code_model.license_header %}
4
+ {{ code_model.license_header }}
5
+ {% endif %}
6
+
7
+ [build-system]
8
+ requires = ["setuptools>=61.0.0", "wheel"] # Requires 61.0.0 for dynamic version
9
+ build-backend = "setuptools.build_meta"
10
+
11
+ [project]
12
+ name = "{{ options.get('package-name')|lower }}"
13
+ {% if options.get('package-mode') %}
14
+ authors = [
15
+ { name = "{{ code_model.company_name }}"{% if code_model.is_azure_flavor %}, email = "azpysdkhelp@microsoft.com"{% endif %} },
16
+ ]
17
+ description = "{{ code_model.company_name }} {% if code_model.is_azure_flavor and not options.get('package-pprint-name').startswith('Azure ') %}Azure {% endif %}{{ options.get('package-pprint-name') }} Client Library for Python"
18
+ license = {text = "MIT License"}
19
+ classifiers = [
20
+ "Development Status :: {{ dev_status }}",
21
+ "Programming Language :: Python",
22
+ "Programming Language :: Python :: 3 :: Only",
23
+ "Programming Language :: Python :: 3",
24
+ {% for version in range(min_version, max_version + 1) %}
25
+ "Programming Language :: Python :: 3.{{ version }}",
26
+ {% endfor %}
27
+ "License :: OSI Approved :: MIT License",
28
+ ]
29
+ requires-python = ">={{ MIN_PYTHON_VERSION }}"
30
+ {% else %}
31
+ description = "{{ options.get('package-name') }}"
32
+ {% endif %}
33
+ {% if code_model.is_azure_flavor %}
34
+ keywords = ["azure", "azure sdk"]
35
+ {% endif %}
36
+
37
+ dependencies = [
38
+ {% if code_model.is_legacy %}
39
+ "msrest>={{ VERSION_MAP['msrest'] }}",
40
+ {% else %}
41
+ "isodate>={{ VERSION_MAP['isodate'] }}",
42
+ {% endif %}
43
+ {% if options.get('azure_arm') %}
44
+ "azure-mgmt-core>={{ VERSION_MAP['azure-mgmt-core'] }}",
45
+ {% elif code_model.is_azure_flavor %}
46
+ "azure-core>={{ VERSION_MAP['azure-core'] }}",
47
+ {% else %}
48
+ "corehttp[requests]>={{ VERSION_MAP['corehttp'] }}",
49
+ {% endif %}
50
+ "typing-extensions>={{ VERSION_MAP['typing-extensions'] }}",
51
+ {% if KEEP_FIELDS and KEEP_FIELDS.get('project.dependencies') %}
52
+ {% for dep in KEEP_FIELDS.get('project.dependencies') %}
53
+ "{{ dep }}",
54
+ {% endfor %}
55
+ {% endif %}
56
+ ]
57
+ dynamic = [
58
+ {% if options.get('package-mode') %}"version", {% endif %}"readme"
59
+ ]
60
+ {% if not options.get('package-mode') %}
61
+ version = "{{ options.get("package-version", "unknown") }}"
62
+ {% endif %}
63
+ {% if KEEP_FIELDS and KEEP_FIELDS.get('project.optional-dependencies') %}
64
+
65
+ [project.optional-dependencies]
66
+ {% for key, val in KEEP_FIELDS.get('project.optional-dependencies').items() %}
67
+ {{ key }} = [
68
+ {% for dep in val %}
69
+ "{{ dep }}",
70
+ {% endfor %}
71
+ ]
72
+ {% endfor %}
73
+ {% endif %}
74
+ {% if code_model.is_azure_flavor %}
75
+
76
+ [project.urls]
77
+ repository = "https://github.com/Azure/azure-sdk-for-python/tree/main/sdk"
78
+ {% endif %}
79
+
80
+ [tool.setuptools.dynamic]
81
+ {% if options.get('package-mode') %}
82
+ {% if code_model.is_tsp %}
83
+ version = {attr = "{{ code_model.namespace|lower }}._version.VERSION"}
84
+ {% else %}
85
+ version = {attr = "{{ options.get('package-name')|lower|replace('-', '.') }}._version.VERSION"}
86
+ {% endif %}
87
+ {% endif %}
88
+ readme = {file = ["README.md"], content-type = "text/markdown"}
89
+ {% if options.get('package-mode') %}
90
+
91
+ [tool.setuptools.packages.find]
92
+ exclude = [
93
+ "tests*",
94
+ "samples*",
95
+ {% for pkgutil_name in pkgutil_names %}
96
+ "{{ pkgutil_name }}",
97
+ {% endfor %}
98
+ ]
99
+
100
+ [tool.setuptools.package-data]
101
+ pytyped = ["py.typed"]
102
+ {% endif %}
103
+ {% if KEEP_FIELDS and KEEP_FIELDS.get('tool.azure-sdk-build') %}
104
+
105
+ [tool.azure-sdk-build]
106
+ {% for key, val in KEEP_FIELDS.get('tool.azure-sdk-build').items() %}
107
+ {{ key }} = {{ val|tojson }}
108
+ {% endfor %}
109
+ {% endif %}
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "version": "1",
3
- "pip_version": "25.1.1",
3
+ "pip_version": "25.2",
4
4
  "install": [
5
5
  {
6
6
  "download_info": {
@@ -25,10 +25,10 @@ class OptionsDict(MutableMapping):
25
25
  "azure-arm": False,
26
26
  "basic-setup-py": False,
27
27
  "client-side-validation": False,
28
- "emit-cross-language-definition-file": False,
29
28
  "flavor": "azure", # need to default to azure in shared code so we don't break swagger generation
30
29
  "from-typespec": False,
31
30
  "generate-sample": False,
31
+ "keep-setup-py": False,
32
32
  "generate-test": False,
33
33
  "head-as-boolean": True,
34
34
  "keep-version-file": False,
@@ -39,13 +39,14 @@ class OptionsDict(MutableMapping):
39
39
  "polymorphic-examples": 5,
40
40
  "validate-versioning": True,
41
41
  "version-tolerant": True,
42
+ "generation-subdir": None, # subdirectory to generate the code in
42
43
  }
43
44
 
44
45
  def __init__(self, options: Optional[Dict[str, Any]] = None) -> None:
45
46
  self._data = options.copy() if options else {}
46
47
  self._validate_combinations()
47
48
 
48
- def __getitem__(self, key: str) -> Any:
49
+ def __getitem__(self, key: str) -> Any: # pylint: disable=too-many-return-statements
49
50
  if key == "head-as-boolean" and self.get("azure-arm"):
50
51
  # override to always true if azure-arm is set
51
52
  return True
@@ -60,6 +61,15 @@ class OptionsDict(MutableMapping):
60
61
  if key == "package-mode" and self._data.get("packaging-files-dir"):
61
62
  # if packaging-files-dir is set, use it as package-mode
62
63
  return self._data["packaging-files-dir"]
64
+ if key == "generation-subdir":
65
+ data = self._data.get("generation-subdir")
66
+ if data:
67
+ # Remove leading dot or ./ from generation-subdir
68
+ if data.startswith("./"):
69
+ data = data[2:]
70
+ elif data.startswith("."):
71
+ data = data[1:]
72
+ return data
63
73
  return self._get_default(key)
64
74
 
65
75
  def __setitem__(self, key: str, value: Any) -> None:
@@ -106,6 +116,8 @@ class OptionsDict(MutableMapping):
106
116
  models_mode_default = "dpg"
107
117
  # switch to falsy value for easier code writing
108
118
  return models_mode_default
119
+ if key == "emit-cross-language-definition-file":
120
+ return self.get("flavor") == "azure"
109
121
  return self.DEFAULTS[key]
110
122
 
111
123
  def _validate_combinations(self) -> None:
@@ -134,6 +146,10 @@ class OptionsDict(MutableMapping):
134
146
  "We are working on creating a new multiapi SDK for version tolerant and it is not available yet."
135
147
  )
136
148
 
149
+ # If multiapi, do not generate default pyproject.toml
150
+ if self.get("multiapi"):
151
+ self["keep-setup-py"] = True
152
+
137
153
  if self.get("client-side-validation") and self.get("version-tolerant"):
138
154
  raise ValueError("Can not generate version tolerant with --client-side-validation. ")
139
155
 
@@ -210,6 +226,9 @@ class ReaderAndWriter:
210
226
  _LOGGER.warning("Loading python.json file. This behavior will be depreacted")
211
227
  self.options.update(python_json)
212
228
 
229
+ def get_output_folder(self) -> Path:
230
+ return self.output_folder
231
+
213
232
  def read_file(self, path: Union[str, Path]) -> str:
214
233
  """Directly reading from disk"""
215
234
  # make path relative to output folder
@@ -227,6 +246,14 @@ class ReaderAndWriter:
227
246
  with open(self.output_folder / Path(filename), "w", encoding="utf-8") as fd:
228
247
  fd.write(file_content)
229
248
 
249
+ def remove_file(self, filename: Union[str, Path]) -> None:
250
+ try:
251
+ file_path = self.output_folder / Path(filename)
252
+ if file_path.is_file():
253
+ file_path.unlink()
254
+ except FileNotFoundError:
255
+ pass
256
+
230
257
  def list_file(self) -> List[str]:
231
258
  return [str(f.relative_to(self.output_folder)) for f in self.output_folder.glob("**/*") if f.is_file()]
232
259
 
@@ -3,6 +3,7 @@
3
3
  # Licensed under the MIT License. See License.txt in the project root for
4
4
  # license information.
5
5
  # --------------------------------------------------------------------------
6
+ from pathlib import Path
6
7
  from typing import List, Dict, Any, Set, Union, Literal, Optional, cast
7
8
 
8
9
  from .base import BaseType
@@ -452,3 +453,34 @@ class CodeModel: # pylint: disable=too-many-public-methods, disable=too-many-in
452
453
  return self.yaml_data.get("licenseInfo", {}).get("company", "")
453
454
  # typespec azure case without custom license and swagger case
454
455
  return "Microsoft Corporation"
456
+
457
+ def get_root_dir(self) -> Path:
458
+ if self.options["no-namespace-folders"]:
459
+ # when output folder contains parts different from the namespace, we fall back to current folder directly.
460
+ return Path(".")
461
+ return Path(*self.namespace.split("."))
462
+
463
+ def get_generation_dir(self, namespace: str) -> Path:
464
+ """The directory to generate the code in.
465
+
466
+ If 'generation-subdir' is specified, it will be used as a subdirectory.
467
+ """
468
+ root_dir = self.get_root_dir()
469
+ retval = self._get_relative_generation_dir(root_dir, namespace)
470
+ return retval
471
+
472
+ def _get_relative_generation_dir(self, root_dir: Path, namespace: str) -> Path:
473
+ if self.options["no-namespace-folders"]:
474
+ return Path(".")
475
+ if self.options.get("generation-subdir"):
476
+ # For the main namespace, return root_dir + generation-subdir
477
+ if namespace in ("", self.namespace):
478
+ return root_dir / self.options["generation-subdir"]
479
+
480
+ # For subnamespaces, extract the subnamespace part and append it to generation-subdir
481
+ if namespace.startswith(self.namespace + "."):
482
+ subnamespace_parts = namespace[len(self.namespace) + 1 :].split(".")
483
+ return root_dir / self.options["generation-subdir"] / Path(*subnamespace_parts)
484
+
485
+ # No generation-subdir specified, use the namespace path directly
486
+ return Path(*namespace.split("."))
@@ -52,7 +52,10 @@ class EnumValue(BaseType):
52
52
  """The python type used for RST syntax input and type annotation."""
53
53
 
54
54
  type_annotation = self.value_type.type_annotation(**kwargs)
55
- enum_type_annotation = f"{self.enum_type.client_namespace}.models.{self.name}"
55
+ client_namespace = self.enum_type.client_namespace
56
+ if self.code_model.options.get("generation-subdir"):
57
+ client_namespace += f".{self.code_model.options['generation-subdir']}"
58
+ enum_type_annotation = f"{client_namespace}.models.{self.name}"
56
59
  return f"{type_annotation} or ~{enum_type_annotation}"
57
60
 
58
61
  def get_json_template_representation(
@@ -292,7 +292,10 @@ class GeneratedModelType(ModelType):
292
292
 
293
293
  def docstring_type(self, **kwargs: Any) -> str:
294
294
  type_annotation = self.type_annotation(need_model_alias=False, skip_quote=True, **kwargs)
295
- return f"~{self.client_namespace}.models.{type_annotation}"
295
+ client_namespace = self.client_namespace
296
+ if self.code_model.options.get("generation-subdir"):
297
+ client_namespace += f".{self.code_model.options['generation-subdir']}"
298
+ return f"~{client_namespace}.models.{type_annotation}"
296
299
 
297
300
  def docstring_text(self, **kwargs: Any) -> str:
298
301
  return self.name
@@ -194,14 +194,13 @@ class OperationBase( # pylint: disable=too-many-public-methods,too-many-instanc
194
194
  def any_response_has_headers(self) -> bool:
195
195
  return any(response.headers for response in self.responses)
196
196
 
197
- @property
198
- def default_error_deserialization(self) -> Optional[str]:
197
+ def default_error_deserialization(self, serialize_namespace: str) -> Optional[str]:
199
198
  default_exceptions = [e for e in self.exceptions if "default" in e.status_codes and e.type]
200
199
  if not default_exceptions:
201
200
  return None
202
201
  exception_schema = default_exceptions[0].type
203
202
  if isinstance(exception_schema, ModelType):
204
- return exception_schema.type_annotation(skip_quote=True)
203
+ return exception_schema.type_annotation(skip_quote=True, serialize_namespace=serialize_namespace)
205
204
  return None if self.code_model.options["models-mode"] == "dpg" else "'object'"
206
205
 
207
206
  @property
@@ -469,7 +468,7 @@ class OperationBase( # pylint: disable=too-many-public-methods,too-many-instanc
469
468
  file_import.add_submodule_import(relative_path, "_deserialize_xml", ImportType.LOCAL)
470
469
  elif self.need_deserialize:
471
470
  file_import.add_submodule_import(relative_path, "_deserialize", ImportType.LOCAL)
472
- if self.default_error_deserialization or self.non_default_errors:
471
+ if self.default_error_deserialization(serialize_namespace) or self.non_default_errors:
473
472
  file_import.add_submodule_import(relative_path, "_failsafe_deserialize", ImportType.LOCAL)
474
473
  return file_import
475
474
 
@@ -173,7 +173,7 @@ class PagingOperationBase(OperationBase[PagingResponseType]):
173
173
  serialize_namespace, module_name="_utils.model_base"
174
174
  )
175
175
  file_import.merge(self.item_type.imports(**kwargs))
176
- if self.default_error_deserialization or self.need_deserialize:
176
+ if self.default_error_deserialization(serialize_namespace) or self.need_deserialize:
177
177
  file_import.add_submodule_import(relative_path, "_deserialize", ImportType.LOCAL)
178
178
  return file_import
179
179