@autorest/python 6.1.4 → 6.1.6

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/ChangeLog.md CHANGED
@@ -1,5 +1,40 @@
1
1
  # Release History
2
2
 
3
+ ### 2022-09-15 - 6.1.6
4
+
5
+ | Library | Min Version |
6
+ | ----------------------------------------------------------------------- | ----------- |
7
+ | `@autorest/core` | `3.8.4` |
8
+ | `@autorest/modelerfour` | `4.23.5` |
9
+ | `azure-core` dep of generated code | `1.24.0` |
10
+ | `isodate` dep of generated code | `0.6.1` |
11
+ | `msrest` dep of generated code (If generating legacy code) | `0.7.1` |
12
+ | `azure-mgmt-core` dep of generated code (If generating mgmt plane code) | `1.3.2` |
13
+
14
+ **Bug Fixes**
15
+
16
+ - Fix `--clear-output-folder` when `--black=true` #1410
17
+
18
+ ### 2022-09-06 - 6.1.5
19
+
20
+ | Library | Min Version |
21
+ | ----------------------------------------------------------------------- | ----------- |
22
+ | `@autorest/core` | `3.8.4` |
23
+ | `@autorest/modelerfour` | `4.23.5` |
24
+ | `azure-core` dep of generated code | `1.24.0` |
25
+ | `isodate` dep of generated code | `0.6.1` |
26
+ | `msrest` dep of generated code (If generating legacy code) | `0.7.1` |
27
+ | `azure-mgmt-core` dep of generated code (If generating mgmt plane code) | `1.3.2` |
28
+
29
+ **Bug Fixes**
30
+
31
+ - Fix `api_version` error when there are multi different `api-version`(not multiapi) #1429
32
+ - Fix generator raising `KeyError` in corner case when generating an LRO-paging operation #1425
33
+
34
+ **Other Changes**
35
+
36
+ - Default `304` errors to throw `azure.core.exception.ResourceNotFoundError`s #1415
37
+
3
38
  ### 2022-08-31 - 6.1.4
4
39
 
5
40
  | Library | Min Version |
@@ -7,7 +7,7 @@ import logging
7
7
  from pathlib import Path
8
8
  import json
9
9
  from abc import ABC, abstractmethod
10
- from typing import Any, Dict, Union
10
+ from typing import Any, Dict, Union, List
11
11
 
12
12
  import yaml
13
13
 
@@ -22,6 +22,7 @@ _LOGGER = logging.getLogger(__name__)
22
22
  class ReaderAndWriter:
23
23
  def __init__(self, *, output_folder: Union[str, Path], **kwargs: Any) -> None:
24
24
  self.output_folder = Path(output_folder)
25
+ self._list_file: List[str] = []
25
26
  try:
26
27
  with open(
27
28
  Path(self.output_folder) / Path("..") / Path("python.json"), "r"
@@ -37,7 +38,7 @@ class ReaderAndWriter:
37
38
  self.options.update(python_json)
38
39
 
39
40
  def read_file(self, path: Union[str, Path]) -> str:
40
- """How does one read a file in cadl?"""
41
+ """Directly reading from disk"""
41
42
  # make path relative to output folder
42
43
  try:
43
44
  with open(self.output_folder / Path(path), "r") as fd:
@@ -46,13 +47,16 @@ class ReaderAndWriter:
46
47
  return ""
47
48
 
48
49
  def write_file(self, filename: Union[str, Path], file_content: str) -> None:
49
- """How does writing work in cadl?"""
50
+ """Directly writing to disk"""
50
51
  file_folder = Path(filename).parent
51
52
  if not Path.is_dir(self.output_folder / file_folder):
52
53
  Path.mkdir(self.output_folder / file_folder, parents=True)
53
54
  with open(self.output_folder / Path(filename), "w") as fd:
54
55
  fd.write(file_content)
55
56
 
57
+ def list_file(self) -> List[str]:
58
+ return [str(f) for f in self.output_folder.glob("**/*") if f.is_file()]
59
+
56
60
 
57
61
  class ReaderAndWriterAutorest(ReaderAndWriter):
58
62
  def __init__(
@@ -67,6 +71,9 @@ class ReaderAndWriterAutorest(ReaderAndWriter):
67
71
  def write_file(self, filename: Union[str, Path], file_content: str) -> None:
68
72
  return self._autorestapi.write_file(filename, file_content)
69
73
 
74
+ def list_file(self) -> List[str]:
75
+ return self._autorestapi.list_inputs()
76
+
70
77
 
71
78
  class Plugin(ReaderAndWriter, ABC):
72
79
  """A base class for autorest plugin.
@@ -30,16 +30,10 @@ class BlackScriptPlugin(Plugin): # pylint: disable=abstract-method
30
30
 
31
31
  def process(self) -> bool:
32
32
  # apply format_file on every file in the output folder
33
- list(
34
- map(
35
- self.format_file,
36
- [f for f in self.output_folder.glob("**/*") if f.is_file()],
37
- )
38
- )
33
+ list(map(self.format_file, [Path(f) for f in self.list_file()]))
39
34
  return True
40
35
 
41
- def format_file(self, full_path) -> None:
42
- file = full_path.relative_to(self.output_folder)
36
+ def format_file(self, file: Path) -> None:
43
37
  file_content = self.read_file(file)
44
38
  if not file.suffix == ".py":
45
39
  self.write_file(file, file_content)
@@ -31,7 +31,9 @@ if TYPE_CHECKING:
31
31
  _LOGGER = logging.getLogger(__name__)
32
32
 
33
33
 
34
- class BaseBuilder(Generic[ParameterListType], BaseModel):
34
+ class BaseBuilder(
35
+ Generic[ParameterListType], BaseModel
36
+ ): # pylint: disable=too-many-instance-attributes
35
37
  """Base class for Operations and Request Builders"""
36
38
 
37
39
  def __init__(
@@ -56,6 +58,7 @@ class BaseBuilder(Generic[ParameterListType], BaseModel):
56
58
  self.group_name: str = yaml_data["groupName"]
57
59
  self.is_overload: bool = yaml_data["isOverload"]
58
60
  self.api_versions: List[str] = yaml_data["apiVersions"]
61
+ self.added_on: Optional[str] = yaml_data.get("addedOn")
59
62
 
60
63
  if code_model.options["version_tolerant"] and yaml_data.get("abstract"):
61
64
  _LOGGER.warning(
@@ -159,18 +159,18 @@ class TokenCredentialType(
159
159
  """Type of a token credential. Used by BearerAuth and ARMChallenge policies"""
160
160
 
161
161
  def type_annotation(self, **kwargs: Any) -> str: # pylint: disable=no-self-use
162
- if kwargs.pop("async_mode"):
162
+ if kwargs.get("async_mode"):
163
163
  return '"AsyncTokenCredential"'
164
164
  return '"TokenCredential"'
165
165
 
166
166
  def docstring_type(self, **kwargs: Any) -> str: # pylint: disable=no-self-use
167
- if kwargs.pop("async_mode"):
167
+ if kwargs.get("async_mode"):
168
168
  return "~azure.core.credentials_async.AsyncTokenCredential"
169
169
  return "~azure.core.credentials.TokenCredential"
170
170
 
171
171
  def imports(self, **kwargs: Any) -> FileImport: # pylint: disable=no-self-use
172
172
  file_import = FileImport()
173
- if kwargs.pop("async_mode"):
173
+ if kwargs.get("async_mode"):
174
174
  file_import.add_submodule_import(
175
175
  "azure.core.credentials_async",
176
176
  "AsyncTokenCredential",
@@ -209,6 +209,12 @@ class OperationBase( # pylint: disable=too-many-public-methods
209
209
  file_import.add_submodule_import(
210
210
  "typing", "Union", ImportType.STDLIB, TypingSection.CONDITIONAL
211
211
  )
212
+ if self.added_on:
213
+ file_import.add_submodule_import(
214
+ f"{'.' if async_mode else ''}.._validation",
215
+ "api_version_validation",
216
+ ImportType.LOCAL,
217
+ )
212
218
  return file_import
213
219
 
214
220
  def imports_for_multiapi(self, async_mode: bool, **kwargs: Any) -> FileImport:
@@ -244,6 +250,11 @@ class OperationBase( # pylint: disable=too-many-public-methods
244
250
  for kwarg in kwargs_to_pop
245
251
  )
246
252
 
253
+ @property
254
+ def need_validation(self) -> bool:
255
+ """Whether we need parameter / operation validation. For API version."""
256
+ return bool(self.added_on) or any(p for p in self.parameters if p.added_on)
257
+
247
258
  def get_request_builder_import(
248
259
  self,
249
260
  request_builder: Union[RequestBuilder, OverloadedRequestBuilder],
@@ -291,27 +302,22 @@ class OperationBase( # pylint: disable=too-many-public-methods
291
302
  file_import.merge(self.parameters.body_parameter.type.imports(**kwargs))
292
303
 
293
304
  # Exceptions
294
- file_import.add_submodule_import(
295
- "azure.core.exceptions", "map_error", ImportType.AZURECORE
296
- )
305
+ errors = [
306
+ "map_error",
307
+ "HttpResponseError",
308
+ "ClientAuthenticationError",
309
+ "ResourceNotFoundError",
310
+ "ResourceExistsError",
311
+ "ResourceNotModifiedError",
312
+ ]
313
+ for error in errors:
314
+ file_import.add_submodule_import(
315
+ "azure.core.exceptions", error, ImportType.AZURECORE
316
+ )
297
317
  if self.code_model.options["azure_arm"]:
298
318
  file_import.add_submodule_import(
299
319
  "azure.mgmt.core.exceptions", "ARMErrorFormat", ImportType.AZURECORE
300
320
  )
301
- file_import.add_submodule_import(
302
- "azure.core.exceptions", "HttpResponseError", ImportType.AZURECORE
303
- )
304
- file_import.add_submodule_import(
305
- "azure.core.exceptions",
306
- "ClientAuthenticationError",
307
- ImportType.AZURECORE,
308
- )
309
- file_import.add_submodule_import(
310
- "azure.core.exceptions", "ResourceNotFoundError", ImportType.AZURECORE
311
- )
312
- file_import.add_submodule_import(
313
- "azure.core.exceptions", "ResourceExistsError", ImportType.AZURECORE
314
- )
315
321
 
316
322
  if self.has_kwargs_to_pop_with_default(
317
323
  self.parameters.kwargs_to_pop, ParameterLocation.HEADER
@@ -66,6 +66,11 @@ class OperationGroup(BaseModel):
66
66
  return " # type: ignore"
67
67
  return ""
68
68
 
69
+ @property
70
+ def need_validation(self) -> bool:
71
+ """Whether any of its operations need validation"""
72
+ return any(o for o in self.operations if o.need_validation)
73
+
69
74
  def imports(self, async_mode: bool) -> FileImport:
70
75
  file_import = FileImport()
71
76
 
@@ -82,6 +82,7 @@ class _ParameterBase(
82
82
  self.in_flattened_body: bool = self.yaml_data.get("inFlattenedBody", False)
83
83
  self.grouper: bool = self.yaml_data.get("grouper", False)
84
84
  self.check_client_input: bool = self.yaml_data.get("checkClientInput", False)
85
+ self.added_on: Optional[str] = self.yaml_data.get("addedOn")
85
86
 
86
87
  @property
87
88
  def constant(self) -> bool:
@@ -152,6 +153,12 @@ class _ParameterBase(
152
153
  )
153
154
  if self.optional and self.client_default_value is None:
154
155
  file_import.add_submodule_import("typing", "Optional", ImportType.STDLIB)
156
+ if self.added_on:
157
+ file_import.add_submodule_import(
158
+ f"{'.' if async_mode else ''}.._validation",
159
+ "api_version_validation",
160
+ ImportType.LOCAL,
161
+ )
155
162
  return file_import
156
163
 
157
164
  @property
@@ -122,13 +122,13 @@ class BinaryIteratorType(PrimitiveType):
122
122
  return "IO"
123
123
 
124
124
  def docstring_type(self, **kwargs: Any) -> str:
125
- return "AsyncIterator[bytes]" if kwargs.pop("async_mode") else "Iterator[bytes]"
125
+ return "AsyncIterator[bytes]" if kwargs.get("async_mode") else "Iterator[bytes]"
126
126
 
127
127
  def type_annotation(self, **kwargs: Any) -> str:
128
128
  return self.docstring_type(**kwargs)
129
129
 
130
130
  def docstring_text(self, **kwargs: Any) -> str:
131
- iterator = "Async iterator" if kwargs.pop("async_mode") else "Iterator"
131
+ iterator = "Async iterator" if kwargs.get("async_mode") else "Iterator"
132
132
  return f"{iterator} of the response bytes"
133
133
 
134
134
  @property
@@ -137,7 +137,7 @@ class BinaryIteratorType(PrimitiveType):
137
137
 
138
138
  def imports(self, **kwargs: Any) -> FileImport:
139
139
  file_import = FileImport()
140
- iterator = "AsyncIterator" if kwargs.pop("async_mode") else "Iterator"
140
+ iterator = "AsyncIterator" if kwargs.get("async_mode") else "Iterator"
141
141
  file_import.add_submodule_import("typing", iterator, ImportType.STDLIB)
142
142
  return file_import
143
143
 
@@ -170,7 +170,7 @@ class PagingResponse(Response):
170
170
 
171
171
  def _imports_shared(self, **kwargs: Any) -> FileImport:
172
172
  file_import = super()._imports_shared(**kwargs)
173
- async_mode = kwargs.pop("async_mode")
173
+ async_mode = kwargs.get("async_mode", False)
174
174
  pager_import_path = ".".join(self.get_pager_path(async_mode).split(".")[:-1])
175
175
  pager = self.get_pager(async_mode)
176
176
 
@@ -179,7 +179,7 @@ class PagingResponse(Response):
179
179
 
180
180
  def imports(self, **kwargs: Any) -> FileImport:
181
181
  file_import = self._imports_shared(**kwargs)
182
- async_mode = kwargs.pop("async_mode")
182
+ async_mode = kwargs.get("async_mode")
183
183
  if async_mode:
184
184
  file_import.add_submodule_import(
185
185
  "azure.core.async_paging", "AsyncList", ImportType.AZURECORE
@@ -234,16 +234,14 @@ class LROResponse(Response):
234
234
  return self.get_base_polling_method_path(async_mode).split(".")[-1]
235
235
 
236
236
  def type_annotation(self, **kwargs: Any) -> str:
237
- return f"{self.get_poller(kwargs.pop('async_mode'))}[{super().type_annotation(**kwargs)}]"
237
+ return f"{self.get_poller(kwargs.get('async_mode', False))}[{super().type_annotation(**kwargs)}]"
238
238
 
239
239
  def docstring_type(self, **kwargs: Any) -> str:
240
- return f"~{self.get_poller_path(kwargs.pop('async_mode'))}[{super().docstring_type(**kwargs)}]"
240
+ return f"~{self.get_poller_path(kwargs.get('async_mode', False))}[{super().docstring_type(**kwargs)}]"
241
241
 
242
242
  def docstring_text(self, **kwargs) -> str:
243
243
  super_text = super().docstring_text(**kwargs)
244
- base_description = (
245
- f"An instance of {self.get_poller(kwargs.pop('async_mode'))} that returns "
246
- )
244
+ base_description = f"An instance of {self.get_poller(kwargs.get('async_mode', False))} that returns "
247
245
  if not self.code_model.options["version_tolerant"]:
248
246
  base_description += "either "
249
247
  return base_description + super_text
@@ -297,11 +295,11 @@ class LROResponse(Response):
297
295
  class LROPagingResponse(LROResponse, PagingResponse):
298
296
  def type_annotation(self, **kwargs: Any) -> str:
299
297
  paging_type_annotation = PagingResponse.type_annotation(self, **kwargs)
300
- return f"{self.get_poller(kwargs.pop('async_mode'))}[{paging_type_annotation}]"
298
+ return f"{self.get_poller(kwargs.get('async_mode', False))}[{paging_type_annotation}]"
301
299
 
302
300
  def docstring_type(self, **kwargs: Any) -> str:
303
301
  paging_docstring_type = PagingResponse.docstring_type(self, **kwargs)
304
- return f"~{self.get_poller_path(kwargs.pop('async_mode'))}[{paging_docstring_type}]"
302
+ return f"~{self.get_poller_path(kwargs.get('async_mode', False))}[{paging_docstring_type}]"
305
303
 
306
304
  def docstring_text(self, **kwargs) -> str:
307
305
  base_description = (
@@ -428,6 +428,12 @@ class JinjaSerializer(ReaderAndWriter): # pylint: disable=abstract-method
428
428
  general_serializer.serialize_serialization_file(),
429
429
  )
430
430
 
431
+ if any(og for og in self.code_model.operation_groups if og.need_validation):
432
+ self.write_file(
433
+ namespace_path / Path("_validation.py"),
434
+ general_serializer.serialize_validation_file(),
435
+ )
436
+
431
437
  # Write the config file
432
438
  if self.code_model.request_builders:
433
439
  self.write_file(
@@ -173,6 +173,22 @@ def _get_json_response_template_to_status_codes(
173
173
  return retval
174
174
 
175
175
 
176
+ def _api_version_validation(builder: OperationType) -> str:
177
+ retval: List[str] = []
178
+ if builder.added_on:
179
+ retval.append(f' method_added_on="{builder.added_on}",')
180
+ params_added_on = defaultdict(list)
181
+ for parameter in builder.parameters:
182
+ if parameter.added_on:
183
+ params_added_on[parameter.added_on].append(parameter.client_name)
184
+ if params_added_on:
185
+ retval.append(f" params_added_on={dict(params_added_on)},")
186
+ if retval:
187
+ retval_str = "\n".join(retval)
188
+ return f"@api_version_validation(\n{retval_str}\n)"
189
+ return ""
190
+
191
+
176
192
  class _BuilderBaseSerializer(Generic[BuilderType]): # pylint: disable=abstract-method
177
193
  def __init__(self, code_model: CodeModel, async_mode: bool) -> None:
178
194
  self.code_model = code_model
@@ -590,8 +606,10 @@ class _OperationSerializer(
590
606
 
591
607
  def decorators(self, builder: OperationType) -> List[str]:
592
608
  """Decorators for the method"""
593
- super_decorators = super().decorators(builder)
594
- return super_decorators
609
+ retval = super().decorators(builder)
610
+ if _api_version_validation(builder):
611
+ retval.append(_api_version_validation(builder))
612
+ return retval
595
613
 
596
614
  def param_description(
597
615
  self, builder: OperationType
@@ -1043,6 +1061,8 @@ class _OperationSerializer(
1043
1061
  retval.append(" 404: ResourceNotFoundError,")
1044
1062
  if not 409 in builder.non_default_error_status_codes:
1045
1063
  retval.append(" 409: ResourceExistsError,")
1064
+ if not 304 in builder.non_default_error_status_codes:
1065
+ retval.append(" 304: ResourceNotModifiedError,")
1046
1066
  for excep in builder.non_default_errors:
1047
1067
  error_model_str = ""
1048
1068
  if (
@@ -1071,6 +1091,11 @@ class _OperationSerializer(
1071
1091
  " 409: lambda response: ResourceExistsError(response=response"
1072
1092
  f"{error_model_str}{error_format_str}),"
1073
1093
  )
1094
+ elif status_code == 304:
1095
+ retval.append(
1096
+ " 304: lambda response: ResourceNotModifiedError(response=response"
1097
+ f"{error_model_str}{error_format_str}),"
1098
+ )
1074
1099
  elif not error_model_str and not error_format_str:
1075
1100
  retval.append(f" {status_code}: HttpResponseError,")
1076
1101
  else:
@@ -1080,7 +1105,8 @@ class _OperationSerializer(
1080
1105
  )
1081
1106
  else:
1082
1107
  retval.append(
1083
- " 401: ClientAuthenticationError, 404: ResourceNotFoundError, 409: ResourceExistsError"
1108
+ " 401: ClientAuthenticationError, 404: ResourceNotFoundError, 409: ResourceExistsError, "
1109
+ "304: ResourceNotModifiedError"
1084
1110
  )
1085
1111
  retval.append("}")
1086
1112
  retval.append("error_map.update(kwargs.pop('error_map', {}) or {})")
@@ -1130,6 +1156,8 @@ class _PagingOperationSerializer(
1130
1156
  return ["@overload"]
1131
1157
  if self.code_model.options["tracing"] and builder.want_tracing:
1132
1158
  retval.append("@distributed_trace")
1159
+ if _api_version_validation(builder):
1160
+ retval.append(_api_version_validation(builder))
1133
1161
  return retval
1134
1162
 
1135
1163
  def call_next_link_request_builder(self, builder: PagingOperationType) -> List[str]:
@@ -120,3 +120,7 @@ class GeneralSerializer:
120
120
  def serialize_serialization_file(self) -> str:
121
121
  template = self.env.get_template("serialization.py.jinja2")
122
122
  return template.render(code_model=self.code_model)
123
+
124
+ def serialize_validation_file(self) -> str:
125
+ template = self.env.get_template("validation.py.jinja2")
126
+ return template.render(code_model=self.code_model)
@@ -17,4 +17,4 @@
17
17
  {% endif %}
18
18
  {% for operation_group in operation_groups %}
19
19
  {% include "operation_group.py.jinja2" %}
20
- {% endfor %}
20
+ {% endfor %}
@@ -0,0 +1,38 @@
1
+ {{ code_model.options['license_header'] }}
2
+ import functools
3
+
4
+ def api_version_validation(**kwargs):
5
+ params_added_on = kwargs.pop("params_added_on", {})
6
+ method_added_on = kwargs.pop("method_added_on", "")
7
+
8
+ def decorator(func):
9
+ @functools.wraps(func)
10
+ def wrapper(*args, **kwargs):
11
+ try:
12
+ # this assumes the client has an _api_version attribute
13
+ client = args[0]
14
+ client_api_version = client._config.api_version # pylint: disable=protected-access
15
+ except AttributeError:
16
+ return func(*args, **kwargs)
17
+
18
+ if method_added_on > client_api_version:
19
+ raise ValueError(
20
+ f"'{func.__name__}' is not available in API version "
21
+ f"{client_api_version}. Pass service API version {method_added_on} or newer to your client."
22
+ )
23
+
24
+ unsupported = {
25
+ parameter: api_version
26
+ for api_version, parameters in params_added_on.items()
27
+ for parameter in parameters
28
+ if parameter in kwargs and api_version > client_api_version
29
+ }
30
+ if unsupported:
31
+ raise ValueError("".join([
32
+ f"'{param}' is not available in API version {client_api_version}. "
33
+ f"Use service API version {version} or newer.\n"
34
+ for param, version in unsupported.items()
35
+ ]))
36
+ return func(*args, **kwargs)
37
+ return wrapper
38
+ return decorator
@@ -426,6 +426,10 @@ class M4Reformatter(
426
426
  ): # pylint: disable=too-many-public-methods
427
427
  """Add Python naming information."""
428
428
 
429
+ def __init__(self, *args, **kwargs) -> None:
430
+ super().__init__(*args, **kwargs)
431
+ self.check_client_input: bool = False
432
+
429
433
  @property
430
434
  def azure_arm(self) -> bool:
431
435
  return bool(self._autorestapi.get_boolean_value("azure-arm"))
@@ -802,7 +806,7 @@ class M4Reformatter(
802
806
  param["inDocstring"] = False
803
807
  if self.legacy:
804
808
  param["implementation"] = "Method"
805
- param["checkClientInput"] = True
809
+ param["checkClientInput"] = self.check_client_input
806
810
  if has_flattened_body and param.get("targetProperty"):
807
811
  retval.append(self.update_flattened_parameter(param, body_parameter))
808
812
  continue
@@ -935,6 +939,8 @@ class M4Reformatter(
935
939
 
936
940
  client_name = "base_url" if self.legacy else "endpoint"
937
941
  global_parameter["language"]["default"]["description"] = "Service URL."
942
+ elif name == "api_version":
943
+ self.check_client_input = True
938
944
  global_params.append(
939
945
  self.update_parameter(
940
946
  global_parameter, override_client_name=client_name
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@autorest/python",
3
- "version": "6.1.4",
3
+ "version": "6.1.6",
4
4
  "description": "The Python extension for generators in AutoRest.",
5
5
  "scripts": {
6
6
  "prepare": "node run-python3.js prepare.py",