@typespec/http-client-python 0.32.0 → 0.33.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.
@@ -11,8 +11,31 @@ SWAGGER_PACKAGE_MODE = ["mgmtplane", "dataplane"] # for backward compatibility
11
11
  TYPESPEC_PACKAGE_MODE = ["azure-mgmt", "azure-dataplane", "generic"]
12
12
  VALID_PACKAGE_MODE = SWAGGER_PACKAGE_MODE + TYPESPEC_PACKAGE_MODE
13
13
 
14
+ CODE_BLOCK_MARKER = ".. code-block::"
15
+
16
+
17
+ def description_ends_with_code_block(description: str) -> bool:
18
+ """Return True when the description's trailing content is an RST ``.. code-block::``.
19
+
20
+ True when the last ``.. code-block::`` directive (starting its own line, so inline
21
+ mentions are ignored) is followed only by blank or indented lines, i.e. the literal
22
+ block runs to the end of the description.
23
+ """
24
+ lines = description.rstrip().splitlines()
25
+ directives = [
26
+ i for i, line in enumerate(lines) if line.lstrip().startswith(CODE_BLOCK_MARKER)
27
+ ]
28
+ if not directives:
29
+ return False
30
+ return all(
31
+ not line.strip() or line.startswith((" ", "\t"))
32
+ for line in lines[directives[-1] + 1 :]
33
+ )
34
+
14
35
 
15
- def update_enum_value(name: str, value: Any, description: str, enum_type: dict[str, Any]) -> dict[str, Any]:
36
+ def update_enum_value(
37
+ name: str, value: Any, description: str, enum_type: dict[str, Any]
38
+ ) -> dict[str, Any]:
16
39
  return {
17
40
  "name": name,
18
41
  "type": "enumvalue",
@@ -39,7 +62,12 @@ def to_snake_case(name: str) -> str:
39
62
  and len(name) - next_non_upper_case_char_location > 1
40
63
  and name[next_non_upper_case_char_location].isalpha()
41
64
  ):
42
- return prefix + match_str[: len(match_str) - 1] + "_" + match_str[len(match_str) - 1]
65
+ return (
66
+ prefix
67
+ + match_str[: len(match_str) - 1]
68
+ + "_"
69
+ + match_str[len(match_str) - 1]
70
+ )
43
71
 
44
72
  return prefix + match_str
45
73
 
@@ -86,7 +114,9 @@ def parse_args(
86
114
  return value
87
115
 
88
116
  unknown_args_ret = {
89
- ua.strip("--").split("=", maxsplit=1)[0]: _get_value(ua.strip("--").split("=", maxsplit=1)[1])
117
+ ua.strip("--").split("=", maxsplit=1)[0]: _get_value(
118
+ ua.strip("--").split("=", maxsplit=1)[1]
119
+ )
90
120
  for ua in unknown_args
91
121
  }
92
122
  return args, unknown_args_ret
@@ -128,7 +158,11 @@ def build_policies(
128
158
  "self._config.user_agent_policy",
129
159
  "self._config.proxy_policy",
130
160
  "policies.ContentDecodePolicy(**kwargs)",
131
- (f"{async_prefix}ARMAutoResourceProviderRegistrationPolicy()" if is_arm else None),
161
+ (
162
+ f"{async_prefix}ARMAutoResourceProviderRegistrationPolicy()"
163
+ if is_arm
164
+ else None
165
+ ),
132
166
  "self._config.redirect_policy",
133
167
  "self._config.retry_policy",
134
168
  "self._config.authentication_policy",
@@ -29,6 +29,7 @@ class OptionsDict(MutableMapping):
29
29
  "from-typespec": False,
30
30
  "generate-sample": False,
31
31
  "keep-setup-py": False,
32
+ "keep-pyproject-fields": "",
32
33
  "generate-test": False,
33
34
  "head-as-boolean": True,
34
35
  "keep-version-file": False,
@@ -7,23 +7,24 @@ from typing import TypeVar
7
7
 
8
8
  from enum import Enum
9
9
 
10
+ from ...utils import description_ends_with_code_block
11
+
10
12
  T = TypeVar("T")
11
13
  OrderedSet = dict[T, None]
12
14
 
13
15
 
14
- CODE_BLOCK_MARKER = ".. code-block::"
15
-
16
-
17
16
  def add_to_description(description: str, entry: str) -> str:
18
17
  if not description:
19
18
  return entry
20
- # When the description contains a code block, the entry (e.g. "Required.") must be
21
- # inserted into the prose *before* the code block. Appending it after the code block
22
- # (e.g. "]. Required.") leaves it dangling at the end of the rendered block and breaks
23
- # Sphinx rendering.
24
- if CODE_BLOCK_MARKER in description:
25
- prose, _, code_block = description.partition(CODE_BLOCK_MARKER)
26
- return f"{prose.rstrip()} {entry}\n\n{CODE_BLOCK_MARKER}{code_block}"
19
+ if not entry:
20
+ return description
21
+ # When the description ends with a code block, prepend the entry (e.g. "Required.") so
22
+ # it appears before the prose instead of after the block. Appending inline (e.g.
23
+ # "]. Required.") would land it inside the rendered literal block and break Sphinx, and a
24
+ # trailing paragraph reads awkwardly. The code block stays at the very end where it renders
25
+ # cleanly. (Code-block descriptions only ever carry a single annotation in practice.)
26
+ if description_ends_with_code_block(description):
27
+ return f"{entry} {description.lstrip()}"
27
28
  return f"{description} {entry}"
28
29
 
29
30
 
@@ -44,6 +45,10 @@ class NamespaceType(str, Enum):
44
45
 
45
46
  LOCALS_LENGTH_LIMIT = 25
46
47
 
47
- REQUEST_BUILDER_BODY_VARIABLES_LENGTH = 6 # how many body variables are present in a request builder
48
+ REQUEST_BUILDER_BODY_VARIABLES_LENGTH = (
49
+ 6 # how many body variables are present in a request builder
50
+ )
48
51
 
49
- OPERATION_BODY_VARIABLES_LENGTH = 14 # how many body variables are present in an operation
52
+ OPERATION_BODY_VARIABLES_LENGTH = (
53
+ 14 # how many body variables are present in an operation
54
+ )
@@ -66,7 +66,26 @@ class GeneralSerializer(BaseSerializer):
66
66
  if dep_version > default_version:
67
67
  version_map[dep_name] = str(dep_version)
68
68
 
69
- def external_lib_version_map(self, file_content: str, additional_version_map: dict[str, str]) -> dict:
69
+ # Project-level pyproject.toml fields that may be preserved across regeneration
70
+ # via the "keep-pyproject-fields" option.
71
+ KEEPABLE_PROJECT_FIELDS = ("authors", "description", "classifiers", "urls")
72
+
73
+ @staticmethod
74
+ def _parse_keep_pyproject_fields(value: Any) -> tuple[str, ...]:
75
+ # The "keep-pyproject-fields" option may be a comma-separated string (e.g.
76
+ # "authors,description") or an already-parsed sequence of field names.
77
+ if not value:
78
+ return ()
79
+ if isinstance(value, str):
80
+ return tuple(field.strip() for field in value.split(",") if field.strip())
81
+ return tuple(value)
82
+
83
+ def external_lib_version_map(
84
+ self,
85
+ file_content: str,
86
+ additional_version_map: dict[str, str],
87
+ keep_pyproject_fields: tuple[str, ...] = (),
88
+ ) -> dict:
70
89
  # Load the pyproject.toml file if it exists and extract fields to keep.
71
90
  result: dict = {"KEEP_FIELDS": {}}
72
91
  try:
@@ -85,6 +104,14 @@ class GeneralSerializer(BaseSerializer):
85
104
 
86
105
  # Process dependencies
87
106
  if "project" in loaded_pyproject_toml:
107
+ project = loaded_pyproject_toml["project"]
108
+
109
+ # Keep manually customized project fields the emitter would otherwise overwrite.
110
+ # Only the fields explicitly listed in the "keep-pyproject-fields" option are preserved.
111
+ for field in keep_pyproject_fields:
112
+ if field in self.KEEPABLE_PROJECT_FIELDS and field in project:
113
+ result["KEEP_FIELDS"][f"project.{field}"] = project[field]
114
+
88
115
  # Handle main dependencies
89
116
  if "dependencies" in loaded_pyproject_toml["project"]:
90
117
  kept_deps = []
@@ -127,7 +154,10 @@ class GeneralSerializer(BaseSerializer):
127
154
 
128
155
  # Add fields to keep from an existing pyproject.toml
129
156
  if template_name == "pyproject.toml.jinja2":
130
- params = self.external_lib_version_map(file_content, additional_version_map)
157
+ keep_pyproject_fields = self._parse_keep_pyproject_fields(
158
+ self.code_model.options.get("keep-pyproject-fields")
159
+ )
160
+ params = self.external_lib_version_map(file_content, additional_version_map, keep_pyproject_fields)
131
161
  else:
132
162
  params = {}
133
163
 
@@ -11,15 +11,32 @@ build-backend = "setuptools.build_meta"
11
11
  [project]
12
12
  name = "{{ options.get('package-name')|lower }}"
13
13
  {% if options.get('package-mode') %}
14
+ {% if KEEP_FIELDS and KEEP_FIELDS.get('project.authors') %}
15
+ authors = [
16
+ {% for author in KEEP_FIELDS.get('project.authors') %}
17
+ { {% for key, val in author.items() %}{{ key }} = {{ val|tojson }}{% if not loop.last %}, {% endif %}{% endfor %} },
18
+ {% endfor %}
19
+ ]
20
+ {% else %}
14
21
  authors = [
15
22
  { name = "{{ code_model.company_name }}"{% if code_model.is_azure_flavor %}, email = "azpysdkhelp@microsoft.com"{% endif %} },
16
23
  ]
17
- {% if options.get("azure-arm") %}
24
+ {% endif %}
25
+ {% if KEEP_FIELDS and KEEP_FIELDS.get('project.description') %}
26
+ description = {{ KEEP_FIELDS.get('project.description')|tojson }}
27
+ {% elif options.get("azure-arm") %}
18
28
  description = "Microsoft Azure {{ options.get('package-pprint-name') }} Client Library for Python"
19
29
  {% else %}
20
30
  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"
21
31
  {% endif %}
22
32
  license = "MIT"
33
+ {% if KEEP_FIELDS and KEEP_FIELDS.get('project.classifiers') %}
34
+ classifiers = [
35
+ {% for classifier in KEEP_FIELDS.get('project.classifiers') %}
36
+ {{ classifier|tojson }},
37
+ {% endfor %}
38
+ ]
39
+ {% else %}
23
40
  classifiers = [
24
41
  "Development Status :: {{ dev_status }}",
25
42
  "Programming Language :: Python",
@@ -29,10 +46,15 @@ classifiers = [
29
46
  "Programming Language :: Python :: 3.{{ version }}",
30
47
  {% endfor %}
31
48
  ]
49
+ {% endif %}
32
50
  requires-python = ">={{ MIN_PYTHON_VERSION }}"
33
51
  {% else %}
52
+ {% if KEEP_FIELDS and KEEP_FIELDS.get('project.description') %}
53
+ description = {{ KEEP_FIELDS.get('project.description')|tojson }}
54
+ {% else %}
34
55
  description = "{{ options.get('package-name') }}"
35
56
  {% endif %}
57
+ {% endif %}
36
58
  {% if code_model.is_azure_flavor %}
37
59
  keywords = ["azure", "azure sdk"]
38
60
  {% endif %}
@@ -77,7 +99,13 @@ version = "{{ options.get("package-version", "unknown") }}"
77
99
  ]
78
100
  {% endfor %}
79
101
  {% endif %}
80
- {% if code_model.is_azure_flavor %}
102
+ {% if KEEP_FIELDS and KEEP_FIELDS.get('project.urls') %}
103
+
104
+ [project.urls]
105
+ {% for key, val in KEEP_FIELDS.get('project.urls').items() %}
106
+ {{ key }} = {{ val|tojson }}
107
+ {% endfor %}
108
+ {% elif code_model.is_azure_flavor %}
81
109
 
82
110
  [project.urls]
83
111
  repository = "https://github.com/Azure/azure-sdk-for-python"