openapi-python-client 0.28.4__py3-none-any.whl → 0.29.1__py3-none-any.whl

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 (92) hide show
  1. openapi_python_client/__init__.py +26 -17
  2. openapi_python_client/cli.py +1 -2
  3. openapi_python_client/config.py +5 -3
  4. openapi_python_client/parser/bodies.py +14 -23
  5. openapi_python_client/parser/openapi.py +58 -48
  6. openapi_python_client/parser/properties/__init__.py +34 -33
  7. openapi_python_client/parser/properties/any.py +11 -10
  8. openapi_python_client/parser/properties/boolean.py +13 -13
  9. openapi_python_client/parser/properties/const.py +11 -10
  10. openapi_python_client/parser/properties/date.py +15 -14
  11. openapi_python_client/parser/properties/datetime.py +15 -14
  12. openapi_python_client/parser/properties/enum_property.py +23 -20
  13. openapi_python_client/parser/properties/file.py +11 -10
  14. openapi_python_client/parser/properties/float.py +13 -13
  15. openapi_python_client/parser/properties/int.py +11 -11
  16. openapi_python_client/parser/properties/list_property.py +22 -22
  17. openapi_python_client/parser/properties/literal_enum_property.py +20 -20
  18. openapi_python_client/parser/properties/merge_properties.py +9 -6
  19. openapi_python_client/parser/properties/model_property.py +34 -33
  20. openapi_python_client/parser/properties/none.py +11 -11
  21. openapi_python_client/parser/properties/protocol.py +42 -32
  22. openapi_python_client/parser/properties/schemas.py +30 -23
  23. openapi_python_client/parser/properties/string.py +10 -11
  24. openapi_python_client/parser/properties/union.py +24 -22
  25. openapi_python_client/parser/properties/uuid.py +11 -11
  26. openapi_python_client/parser/responses.py +30 -23
  27. openapi_python_client/schema/__init__.py +2 -1
  28. openapi_python_client/schema/data_type.py +2 -2
  29. openapi_python_client/schema/openapi_schema_pydantic/callback.py +3 -1
  30. openapi_python_client/schema/openapi_schema_pydantic/components.py +10 -9
  31. openapi_python_client/schema/openapi_schema_pydantic/contact.py +5 -3
  32. openapi_python_client/schema/openapi_schema_pydantic/discriminator.py +4 -2
  33. openapi_python_client/schema/openapi_schema_pydantic/encoding.py +4 -3
  34. openapi_python_client/schema/openapi_schema_pydantic/example.py +5 -3
  35. openapi_python_client/schema/openapi_schema_pydantic/external_documentation.py +4 -2
  36. openapi_python_client/schema/openapi_schema_pydantic/header.py +20 -6
  37. openapi_python_client/schema/openapi_schema_pydantic/info.py +5 -4
  38. openapi_python_client/schema/openapi_schema_pydantic/license.py +4 -2
  39. openapi_python_client/schema/openapi_schema_pydantic/link.py +5 -4
  40. openapi_python_client/schema/openapi_schema_pydantic/media_type.py +3 -2
  41. openapi_python_client/schema/openapi_schema_pydantic/oauth_flow.py +6 -4
  42. openapi_python_client/schema/openapi_schema_pydantic/open_api.py +5 -3
  43. openapi_python_client/schema/openapi_schema_pydantic/operation.py +6 -5
  44. openapi_python_client/schema/openapi_schema_pydantic/parameter.py +6 -5
  45. openapi_python_client/schema/openapi_schema_pydantic/path_item.py +5 -3
  46. openapi_python_client/schema/openapi_schema_pydantic/paths.py +2 -1
  47. openapi_python_client/schema/openapi_schema_pydantic/reference.py +3 -1
  48. openapi_python_client/schema/openapi_schema_pydantic/request_body.py +3 -2
  49. openapi_python_client/schema/openapi_schema_pydantic/response.py +5 -4
  50. openapi_python_client/schema/openapi_schema_pydantic/responses.py +2 -1
  51. openapi_python_client/schema/openapi_schema_pydantic/schema.py +7 -6
  52. openapi_python_client/schema/openapi_schema_pydantic/security_requirement.py +3 -1
  53. openapi_python_client/schema/openapi_schema_pydantic/security_scheme.py +8 -7
  54. openapi_python_client/schema/openapi_schema_pydantic/server.py +4 -3
  55. openapi_python_client/schema/openapi_schema_pydantic/server_variable.py +5 -3
  56. openapi_python_client/schema/openapi_schema_pydantic/tag.py +3 -2
  57. openapi_python_client/schema/openapi_schema_pydantic/xml.py +5 -3
  58. openapi_python_client/schema/parameter_location.py +7 -22
  59. openapi_python_client/schema/ref.py +52 -0
  60. openapi_python_client/schema/untrusted_string.py +56 -0
  61. openapi_python_client/strings.py +245 -0
  62. openapi_python_client/templates/README.md.jinja +3 -3
  63. openapi_python_client/templates/client.py.jinja +5 -5
  64. openapi_python_client/templates/endpoint_macros.py.jinja +18 -18
  65. openapi_python_client/templates/endpoint_module.py.jinja +20 -14
  66. openapi_python_client/templates/helpers.jinja +3 -7
  67. openapi_python_client/templates/literal_enum.py.jinja +3 -3
  68. openapi_python_client/templates/model.py.jinja +18 -18
  69. openapi_python_client/templates/package_init.py.jinja +2 -2
  70. openapi_python_client/templates/property_templates/const_property.py.jinja +3 -3
  71. openapi_python_client/templates/property_templates/date_property.py.jinja +2 -2
  72. openapi_python_client/templates/property_templates/datetime_property.py.jinja +2 -2
  73. openapi_python_client/templates/property_templates/enum_property.py.jinja +1 -1
  74. openapi_python_client/templates/property_templates/file_property.py.jinja +1 -1
  75. openapi_python_client/templates/property_templates/list_property.py.jinja +3 -3
  76. openapi_python_client/templates/property_templates/literal_enum_property.py.jinja +2 -2
  77. openapi_python_client/templates/property_templates/model_property.py.jinja +1 -1
  78. openapi_python_client/templates/property_templates/property_macros.py.jinja +1 -1
  79. openapi_python_client/templates/property_templates/union_property.py.jinja +9 -9
  80. openapi_python_client/templates/property_templates/uuid_property.py.jinja +1 -1
  81. openapi_python_client/templates/pyproject_pdm.toml.jinja +4 -5
  82. openapi_python_client/templates/pyproject_poetry.toml.jinja +4 -5
  83. openapi_python_client/templates/pyproject_uv.toml.jinja +5 -6
  84. openapi_python_client/templates/setup.py.jinja +4 -4
  85. openapi_python_client/templates/str_enum.py.jinja +2 -2
  86. {openapi_python_client-0.28.4.dist-info → openapi_python_client-0.29.1.dist-info}/METADATA +6 -9
  87. openapi_python_client-0.29.1.dist-info/RECORD +115 -0
  88. {openapi_python_client-0.28.4.dist-info → openapi_python_client-0.29.1.dist-info}/WHEEL +1 -1
  89. openapi_python_client/utils.py +0 -122
  90. openapi_python_client-0.28.4.dist-info/RECORD +0 -113
  91. {openapi_python_client-0.28.4.dist-info → openapi_python_client-0.29.1.dist-info}/entry_points.txt +0 -0
  92. {openapi_python_client-0.28.4.dist-info → openapi_python_client-0.29.1.dist-info}/licenses/LICENSE +0 -0
@@ -3,14 +3,14 @@ from __future__ import annotations
3
3
  from itertools import chain
4
4
  from typing import Any, ClassVar, NamedTuple
5
5
 
6
- from attrs import define, evolve
6
+ from attrs import define, evolve, field
7
7
 
8
- from ... import Config, utils
8
+ from ... import Config, strings
9
9
  from ... import schema as oai
10
- from ...utils import PythonIdentifier
10
+ from ...strings import PythonIdentifier
11
11
  from ..errors import ParseError, PropertyError
12
12
  from .any import AnyProperty
13
- from .protocol import PropertyProtocol, Value
13
+ from .protocol import PropertyProtocol, Value, convert_example
14
14
  from .schemas import Class, ReferencePath, Schemas, parse_reference_path
15
15
 
16
16
 
@@ -18,15 +18,15 @@ from .schemas import Class, ReferencePath, Schemas, parse_reference_path
18
18
  class ModelProperty(PropertyProtocol):
19
19
  """A property which refers to another Schema"""
20
20
 
21
- name: str
21
+ name: oai.UntrustedString
22
22
  required: bool
23
23
  default: Value | None
24
- python_name: utils.PythonIdentifier
25
- example: str | None
24
+ python_name: strings.PythonIdentifier
25
+ example: oai.UntrustedString | None = field(converter=convert_example)
26
26
  class_info: Class
27
27
  data: oai.Schema
28
- description: str
29
- roots: set[ReferencePath | utils.ClassName]
28
+ description: oai.UntrustedString | None
29
+ roots: set[ReferencePath | strings.ClassName]
30
30
  required_properties: list[Property] | None
31
31
  optional_properties: list[Property] | None
32
32
  relative_imports: set[str] | None
@@ -43,13 +43,13 @@ class ModelProperty(PropertyProtocol):
43
43
  cls,
44
44
  *,
45
45
  data: oai.Schema,
46
- name: str,
46
+ name: oai.UntrustedString,
47
47
  schemas: Schemas,
48
48
  required: bool,
49
49
  parent_name: str | None,
50
50
  config: Config,
51
51
  process_properties: bool,
52
- roots: set[ReferencePath | utils.ClassName],
52
+ roots: set[ReferencePath | strings.ClassName],
53
53
  ) -> tuple[ModelProperty | PropertyError, Schemas]:
54
54
  """
55
55
  A single ModelProperty from its OAI data
@@ -65,12 +65,13 @@ class ModelProperty(PropertyProtocol):
65
65
  roots: Set of strings that identify schema objects on which the new ModelProperty will depend
66
66
  process_properties: Determines whether the new ModelProperty will be initialized with property data
67
67
  """
68
+ class_string: str | oai.UntrustedString
68
69
  if not config.use_path_prefixes_for_title_model_names and data.title:
69
70
  class_string = data.title
70
71
  else:
71
72
  title = data.title or name
72
73
  if parent_name:
73
- class_string = f"{utils.pascal_case(parent_name)}{utils.pascal_case(title)}"
74
+ class_string = f"{strings.pascal_case(parent_name)}{strings.pascal_case(title)}"
74
75
  else:
75
76
  class_string = title
76
77
  class_info = Class.from_string(string=class_string, config=config)
@@ -92,7 +93,7 @@ class ModelProperty(PropertyProtocol):
92
93
  relative_imports = property_data.relative_imports
93
94
  lazy_imports = property_data.lazy_imports
94
95
  for root in roots:
95
- if isinstance(root, utils.ClassName):
96
+ if isinstance(root, strings.ClassName):
96
97
  continue
97
98
  schemas.add_dependencies(root, {class_info.name})
98
99
 
@@ -105,11 +106,11 @@ class ModelProperty(PropertyProtocol):
105
106
  relative_imports=relative_imports,
106
107
  lazy_imports=lazy_imports,
107
108
  additional_properties=additional_properties,
108
- description=data.description or "",
109
+ description=data.description,
109
110
  default=None,
110
111
  required=required,
111
112
  name=name,
112
- python_name=utils.PythonIdentifier(value=name, prefix=config.field_prefix),
113
+ python_name=strings.PythonIdentifier(value=name, prefix=config.field_prefix),
113
114
  example=data.example,
114
115
  )
115
116
  if class_info.name in schemas.classes_by_name:
@@ -126,7 +127,7 @@ class ModelProperty(PropertyProtocol):
126
127
  return prop, schemas
127
128
 
128
129
  @classmethod
129
- def convert_value(cls, value: Any) -> Value | None | PropertyError:
130
+ def convert_value(cls, value: Any) -> Value | PropertyError | None:
130
131
  if value is not None:
131
132
  return PropertyError(detail="ModelProperty cannot have a default value") # pragma: no cover
132
133
  return None
@@ -140,8 +141,8 @@ class ModelProperty(PropertyProtocol):
140
141
  """Constructs a self import statement from this ModelProperty's attributes"""
141
142
  return f"models.{self.class_info.module_name} import {self.class_info.name}"
142
143
 
143
- def get_base_type_string(self) -> str:
144
- return self.class_info.name
144
+ def get_base_type_string(self) -> strings.PythonCode:
145
+ return strings.PythonCode(self.class_info.name)
145
146
 
146
147
  def get_imports(self, *, prefix: str) -> set[str]:
147
148
  """
@@ -188,9 +189,9 @@ class ModelProperty(PropertyProtocol):
188
189
  self,
189
190
  no_optional: bool = False,
190
191
  json: bool = False,
191
- ) -> str:
192
+ ) -> strings.PythonCode:
192
193
  """
193
- Get a string representation of type that should be used when declaring this property
194
+ Get a Python code representation of type that should be used when declaring this property
194
195
 
195
196
  Args:
196
197
  no_optional: Do not include Optional or Unset even if the value is optional (needed for isinstance checks)
@@ -203,7 +204,7 @@ class ModelProperty(PropertyProtocol):
203
204
 
204
205
  if no_optional or self.required:
205
206
  return type_string
206
- return f"{type_string} | Unset"
207
+ return strings.PythonCode(f"{type_string.as_unembedded_code()} | Unset")
207
208
 
208
209
 
209
210
  from .property import Property # noqa: E402
@@ -232,14 +233,14 @@ def _process_properties( # noqa: PLR0912, PLR0911
232
233
  *,
233
234
  data: oai.Schema,
234
235
  schemas: Schemas,
235
- class_name: utils.ClassName,
236
+ class_name: strings.ClassName,
236
237
  config: Config,
237
- roots: set[ReferencePath | utils.ClassName],
238
+ roots: set[ReferencePath | strings.ClassName],
238
239
  ) -> _PropertyData | PropertyError:
239
240
  from . import property_from_data # noqa: PLC0415
240
241
  from .merge_properties import merge_properties # noqa: PLC0415
241
242
 
242
- properties: dict[str, Property] = {}
243
+ properties: dict[oai.UntrustedString, Property] = {}
243
244
  relative_imports: set[str] = set()
244
245
  lazy_imports: set[str] = set()
245
246
  required_set = set(data.required or [])
@@ -265,7 +266,7 @@ def _process_properties( # noqa: PLR0912, PLR0911
265
266
  properties[merged_prop.name] = merged_prop
266
267
  return None
267
268
 
268
- unprocessed_props: list[tuple[str, oai.Reference | oai.Schema]] = (
269
+ unprocessed_props: list[tuple[oai.UntrustedString, oai.Reference | oai.Schema]] = (
269
270
  list(data.properties.items()) if data.properties else []
270
271
  )
271
272
  for sub_prop in data.allOf:
@@ -275,7 +276,7 @@ def _process_properties( # noqa: PLR0912, PLR0911
275
276
  return PropertyError(detail=ref_path.detail, data=sub_prop)
276
277
  sub_model = schemas.classes_by_reference.get(ref_path)
277
278
  if sub_model is None:
278
- return PropertyError(f"Reference {sub_prop.ref} not found")
279
+ return PropertyError(f"Reference {sub_prop.ref.get_untrusted_value()} not found")
279
280
  if not isinstance(sub_model, ModelProperty):
280
281
  return PropertyError("Cannot take allOf a non-object")
281
282
  # Properties of allOf references first should be processed first
@@ -335,10 +336,10 @@ def _process_properties( # noqa: PLR0912, PLR0911
335
336
 
336
337
 
337
338
  ANY_ADDITIONAL_PROPERTY = AnyProperty.build(
338
- name="additional",
339
+ name=oai.UntrustedString("additional"),
339
340
  required=True,
340
341
  default=None,
341
- description="",
342
+ description=None,
342
343
  python_name=PythonIdentifier(value="additional", prefix=""),
343
344
  example=None,
344
345
  )
@@ -348,10 +349,10 @@ def _get_additional_properties(
348
349
  *,
349
350
  schema_additional: None | (bool | (oai.Reference | oai.Schema)),
350
351
  schemas: Schemas,
351
- class_name: utils.ClassName,
352
+ class_name: strings.ClassName,
352
353
  config: Config,
353
- roots: set[ReferencePath | utils.ClassName],
354
- ) -> tuple[Property | None | PropertyError, Schemas]:
354
+ roots: set[ReferencePath | strings.ClassName],
355
+ ) -> tuple[Property | PropertyError | None, Schemas]:
355
356
  from . import property_from_data # noqa: PLC0415
356
357
 
357
358
  if schema_additional is None:
@@ -367,7 +368,7 @@ def _get_additional_properties(
367
368
  return ANY_ADDITIONAL_PROPERTY, schemas
368
369
 
369
370
  additional_properties, schemas = property_from_data(
370
- name="AdditionalProperty",
371
+ name=oai.UntrustedString("AdditionalProperty"),
371
372
  required=True, # in the sense that if present in the dict will not be None
372
373
  data=schema_additional,
373
374
  schemas=schemas,
@@ -384,7 +385,7 @@ def _process_property_data(
384
385
  schemas: Schemas,
385
386
  class_info: Class,
386
387
  config: Config,
387
- roots: set[ReferencePath | utils.ClassName],
388
+ roots: set[ReferencePath | strings.ClassName],
388
389
  ) -> tuple[tuple[_PropertyData, Property | None] | PropertyError, Schemas]:
389
390
  property_data = _process_properties(
390
391
  data=data, schemas=schemas, class_name=class_info.name, config=config, roots=roots
@@ -2,24 +2,24 @@ from __future__ import annotations
2
2
 
3
3
  from typing import Any, ClassVar
4
4
 
5
- from attr import define
5
+ from attr import define, field
6
6
 
7
7
  from ... import schema as oai
8
- from ...utils import PythonIdentifier
8
+ from ...strings import PythonCode, PythonIdentifier
9
9
  from ..errors import PropertyError
10
- from .protocol import PropertyProtocol, Value
10
+ from .protocol import PropertyProtocol, Value, convert_example
11
11
 
12
12
 
13
13
  @define
14
14
  class NoneProperty(PropertyProtocol):
15
15
  """A property that can only be None"""
16
16
 
17
- name: str
17
+ name: oai.UntrustedString
18
18
  required: bool
19
19
  default: Value | None
20
20
  python_name: PythonIdentifier
21
- description: str | None
22
- example: str | None
21
+ description: oai.UntrustedString | None
22
+ example: oai.UntrustedString | None = field(converter=convert_example)
23
23
 
24
24
  _allowed_locations: ClassVar[set[oai.ParameterLocation]] = {
25
25
  oai.ParameterLocation.QUERY,
@@ -32,12 +32,12 @@ class NoneProperty(PropertyProtocol):
32
32
  @classmethod
33
33
  def build(
34
34
  cls,
35
- name: str,
35
+ name: oai.UntrustedString,
36
36
  required: bool,
37
37
  default: Any,
38
38
  python_name: PythonIdentifier,
39
- description: str | None,
40
- example: str | None,
39
+ description: oai.UntrustedString | None,
40
+ example: Any,
41
41
  ) -> NoneProperty | PropertyError:
42
42
  checked_default = cls.convert_value(default)
43
43
  if isinstance(checked_default, PropertyError):
@@ -52,10 +52,10 @@ class NoneProperty(PropertyProtocol):
52
52
  )
53
53
 
54
54
  @classmethod
55
- def convert_value(cls, value: Any) -> Value | None | PropertyError:
55
+ def convert_value(cls, value: Any) -> Value | PropertyError | None:
56
56
  if value is None or isinstance(value, Value):
57
57
  return value
58
58
  if isinstance(value, str):
59
59
  if value == "None":
60
- return Value(python_code=value, raw_value=value)
60
+ return Value(python_code=PythonCode(value), raw_value=value)
61
61
  return PropertyError(f"Value {value} is not valid, only None is allowed")
@@ -1,20 +1,29 @@
1
1
  from __future__ import annotations
2
2
 
3
- __all__ = ["PropertyProtocol", "Value"]
3
+ __all__ = ["PropertyProtocol", "Value", "convert_example"]
4
4
 
5
5
  from abc import abstractmethod
6
6
  from dataclasses import dataclass
7
- from typing import TYPE_CHECKING, Any, ClassVar, Protocol, TypeVar
7
+ from typing import Any, ClassVar, Protocol, TypeVar
8
8
 
9
9
  from ... import Config
10
10
  from ... import schema as oai
11
- from ...utils import PythonIdentifier
11
+ from ...strings import PythonCode, PythonIdentifier, safe_for_docstring
12
12
  from ..errors import ParseError, PropertyError
13
13
 
14
- if TYPE_CHECKING: # pragma: no cover
15
- from .model_property import ModelProperty
16
- else:
17
- ModelProperty = "ModelProperty"
14
+
15
+ def convert_example(example: Any) -> oai.UntrustedString | None:
16
+ """
17
+ Convert a raw OpenAPI `example` (which can be anything) into an `UntrustedString` (or None).
18
+
19
+ Examples only ever appear in docstrings, so non-string examples are stringified. Wrapping ensures a
20
+ template which uses an example without escaping renders a safe-but-broken value instead of an exploit.
21
+ """
22
+ if example is None or isinstance(example, oai.UntrustedString):
23
+ return example
24
+ if isinstance(example, str):
25
+ return oai.UntrustedString(example)
26
+ return oai.UntrustedString(str(example))
18
27
 
19
28
 
20
29
  @dataclass
@@ -24,7 +33,7 @@ class Value:
24
33
  (with string escaping, for example). We still keep the `raw_value` around for merging `allOf`.
25
34
  """
26
35
 
27
- python_code: str
36
+ python_code: PythonCode
28
37
  raw_value: Any
29
38
 
30
39
 
@@ -45,7 +54,7 @@ class PropertyProtocol(Protocol):
45
54
  ValidationError: Raised when the default value fails to be converted to the expected type
46
55
  """
47
56
 
48
- name: str
57
+ name: oai.UntrustedString
49
58
  required: bool
50
59
  _type_string: ClassVar[str] = ""
51
60
  _json_type_string: ClassVar[str] = "" # Type of the property after JSON serialization
@@ -56,26 +65,26 @@ class PropertyProtocol(Protocol):
56
65
  }
57
66
  default: Value | None
58
67
  python_name: PythonIdentifier
59
- description: str | None
60
- example: str | None
68
+ description: oai.UntrustedString | None
69
+ example: oai.UntrustedString | None
61
70
 
62
71
  template: ClassVar[str] = "any_property.py.jinja"
63
72
  json_is_dict: ClassVar[bool] = False
64
73
 
65
74
  @abstractmethod
66
- def convert_value(self, value: Any) -> Value | None | PropertyError:
75
+ def convert_value(self, value: Any) -> Value | PropertyError | None:
67
76
  """Convert a string value to a Value object"""
68
77
  raise NotImplementedError() # pragma: no cover
69
78
 
70
79
  def validate_location(self, location: oai.ParameterLocation) -> ParseError | None:
71
80
  """Returns an error if this type of property is not allowed in the given location"""
72
81
  if location not in self._allowed_locations:
73
- return ParseError(detail=f"{self.get_type_string()} is not allowed in {location}")
82
+ return ParseError(detail=f"{self.get_type_string().as_unembedded_code()} is not allowed in {location}")
74
83
  if location == oai.ParameterLocation.PATH and not self.required:
75
84
  return ParseError(detail="Path parameter must be required")
76
85
  return None
77
86
 
78
- def set_python_name(self, new_name: str, config: Config, skip_snake_case: bool = False) -> None:
87
+ def set_python_name(self, new_name: oai.UntrustedString, config: Config, skip_snake_case: bool = False) -> None:
79
88
  """Mutates this Property to set a new python_name.
80
89
 
81
90
  Required to mutate due to how Properties are stored and the difficulty of updating them in-dict.
@@ -88,21 +97,21 @@ class PropertyProtocol(Protocol):
88
97
  PythonIdentifier(value=new_name, prefix=config.field_prefix, skip_snake_case=skip_snake_case),
89
98
  )
90
99
 
91
- def get_base_type_string(self) -> str:
92
- """Get the string describing the Python type of this property. Base types no require quoting."""
93
- return self._type_string
100
+ def get_base_type_string(self) -> PythonCode:
101
+ """Get the code describing the Python type of this property. Base types no require quoting."""
102
+ return PythonCode(self._type_string)
94
103
 
95
- def get_base_json_type_string(self) -> str:
96
- """Get the string describing the JSON type of this property. Base types no require quoting."""
97
- return self._json_type_string
104
+ def get_base_json_type_string(self) -> PythonCode:
105
+ """Get the code describing the JSON type of this property. Base types no require quoting."""
106
+ return PythonCode(self._json_type_string)
98
107
 
99
108
  def get_type_string(
100
109
  self,
101
110
  no_optional: bool = False,
102
111
  json: bool = False,
103
- ) -> str:
112
+ ) -> PythonCode:
104
113
  """
105
- Get a string representation of type that should be used when declaring this property
114
+ Get a Python code representation of type that should be used when declaring this property
106
115
 
107
116
  Args:
108
117
  no_optional: Do not include Optional or Unset even if the value is optional (needed for isinstance checks)
@@ -115,10 +124,10 @@ class PropertyProtocol(Protocol):
115
124
 
116
125
  if no_optional or self.required:
117
126
  return type_string
118
- return f"{type_string} | Unset"
127
+ return PythonCode(f"{type_string.as_unembedded_code()} | Unset")
119
128
 
120
- def get_instance_type_string(self) -> str:
121
- """Get a string representation of runtime type that should be used for `isinstance` checks"""
129
+ def get_instance_type_string(self) -> PythonCode:
130
+ """Get a Python code representation of runtime type that should be used for `isinstance` checks"""
122
131
  return self.get_type_string(no_optional=True)
123
132
 
124
133
  # noinspection PyUnusedLocal
@@ -144,25 +153,26 @@ class PropertyProtocol(Protocol):
144
153
  """
145
154
  return set()
146
155
 
147
- def to_string(self) -> str:
156
+ def to_string(self) -> PythonCode:
148
157
  """How this should be declared in a dataclass"""
149
158
  default: str | None
150
159
  if self.default is not None:
151
- default = self.default.python_code
160
+ default = self.default.python_code.as_unembedded_code()
152
161
  elif not self.required:
153
162
  default = "UNSET"
154
163
  else:
155
164
  default = None
156
165
 
166
+ declaration = f"{self.python_name}: {self.get_type_string().as_unembedded_code()}"
157
167
  if default is not None:
158
- return f"{self.python_name}: {self.get_type_string()} = {default}"
159
- return f"{self.python_name}: {self.get_type_string()}"
168
+ declaration += f" = {default}"
169
+ return PythonCode(declaration)
160
170
 
161
171
  def to_docstring(self) -> str:
162
172
  """Returns property docstring"""
163
- doc = f"{self.python_name} ({self.get_type_string()}): {self.description or ''}"
173
+ doc = f"{self.python_name} ({safe_for_docstring(self.get_type_string())}): {safe_for_docstring(self.description or '')}"
164
174
  if self.default:
165
- doc += f" Default: {self.default.python_code}."
175
+ doc += f" Default: {safe_for_docstring(self.default.python_code)}."
166
176
  if self.example:
167
- doc += f" Example: {self.example}."
177
+ doc += f" Example: {safe_for_docstring(self.example)}."
168
178
  return doc
@@ -10,7 +10,7 @@ __all__ = [
10
10
  "update_schemas_with_data",
11
11
  ]
12
12
 
13
- from typing import TYPE_CHECKING, NewType, cast
13
+ from typing import TYPE_CHECKING, NewType
14
14
  from urllib.parse import urlparse
15
15
 
16
16
  from attrs import define, evolve, field
@@ -18,7 +18,9 @@ from attrs import define, evolve, field
18
18
  from ... import Config
19
19
  from ... import schema as oai
20
20
  from ...schema.openapi_schema_pydantic import Parameter
21
- from ...utils import ClassName, PythonIdentifier
21
+ from ...schema.ref import Ref
22
+ from ...schema.untrusted_string import UntrustedString
23
+ from ...strings import ClassName, PythonIdentifier
22
24
  from ..errors import ParameterError, ParseError, PropertyError
23
25
 
24
26
  if TYPE_CHECKING: # pragma: no cover
@@ -29,10 +31,10 @@ else:
29
31
  Property = "Property"
30
32
 
31
33
 
32
- ReferencePath = NewType("ReferencePath", str)
34
+ ReferencePath = NewType("ReferencePath", UntrustedString)
33
35
 
34
36
 
35
- def parse_reference_path(ref_path_raw: str) -> ReferencePath | ParseError:
37
+ def parse_reference_path(ref_path_raw: Ref) -> ReferencePath | ParseError:
36
38
  """
37
39
  Takes a raw string provided in a `$ref` and turns it into a validated `_ReferencePath` or a `ParseError` if
38
40
  validation fails.
@@ -40,17 +42,17 @@ def parse_reference_path(ref_path_raw: str) -> ReferencePath | ParseError:
40
42
  See Also:
41
43
  - https://swagger.io/docs/specification/using-ref/
42
44
  """
43
- parsed = urlparse(ref_path_raw)
45
+ parsed = urlparse(ref_path_raw.get_untrusted_value())
44
46
  if parsed.scheme or parsed.path:
45
- return ParseError(detail=f"Remote references such as {ref_path_raw} are not supported yet.")
46
- return cast(ReferencePath, parsed.fragment)
47
+ return ParseError(detail=f"Remote references such as {parsed} are not supported yet.")
48
+ return ReferencePath(UntrustedString(parsed.fragment))
47
49
 
48
50
 
49
- def get_reference_simple_name(ref_path: str) -> str:
51
+ def get_reference_simple_name(ref_path: Ref | UntrustedString) -> UntrustedString:
50
52
  """
51
53
  Takes a path like `/components/schemas/NameOfThing` and returns a string like `NameOfThing`.
52
54
  """
53
- return ref_path.rsplit("/", maxsplit=1)[-1]
55
+ return UntrustedString(ref_path.get_untrusted_value().rsplit("/", maxsplit=1)[-1])
54
56
 
55
57
 
56
58
  @define
@@ -61,10 +63,10 @@ class Class:
61
63
  module_name: PythonIdentifier
62
64
 
63
65
  @staticmethod
64
- def from_string(*, string: str, config: Config) -> "Class":
66
+ def from_string(*, string: str | UntrustedString, config: Config) -> "Class":
65
67
  """Get a Class from an arbitrary string"""
66
- class_name = get_reference_simple_name(string) # Get rid of ref path stuff
67
- class_name = ClassName(class_name, config.field_prefix)
68
+ untrusted_class_name = get_reference_simple_name(Ref(string)) # Get rid of ref path stuff
69
+ class_name = ClassName(untrusted_class_name, config.field_prefix)
68
70
  override = config.class_overrides.get(class_name)
69
71
 
70
72
  if override is not None and override.class_name is not None:
@@ -134,12 +136,15 @@ def update_schemas_with_data(
134
136
  )
135
137
 
136
138
  if isinstance(prop, PropertyError):
137
- prop.detail = f"{prop.header}: {prop.detail}"
138
- prop.header = f"Unable to parse schema {ref_path}"
139
- if isinstance(prop.data, oai.Reference) and prop.data.ref.endswith(ref_path): # pragma: nocover
140
- prop.detail += (
139
+ detail = f"{prop.header}: {prop.detail}" if prop.detail is not None else prop.header
140
+ prop.header = f"Unable to parse schema {ref_path.get_untrusted_value()}"
141
+ if isinstance(prop.data, oai.Reference) and prop.data.ref.endswith(
142
+ ref_path.get_untrusted_value()
143
+ ): # pragma: nocover
144
+ detail += (
141
145
  "\n\nRecursive and circular references are not supported directly in an array schema's 'items' section"
142
146
  )
147
+ prop.detail = detail
143
148
  return prop
144
149
 
145
150
  schemas = evolve(schemas, classes_by_reference={ref_path: prop, **schemas.classes_by_reference})
@@ -157,7 +162,7 @@ class Parameters:
157
162
 
158
163
  def parameter_from_data(
159
164
  *,
160
- name: str,
165
+ name: UntrustedString,
161
166
  data: oai.Reference | oai.Parameter,
162
167
  parameters: Parameters,
163
168
  config: Config,
@@ -171,10 +176,10 @@ def parameter_from_data(
171
176
  return ParameterError("Parameter has no schema"), parameters
172
177
 
173
178
  new_param = Parameter(
174
- name=name,
179
+ name=name.get_untrusted_value(),
175
180
  required=data.required,
176
181
  explode=data.explode,
177
- style=data.style,
182
+ style=data.style.get_untrusted_value() if data.style is not None else None,
178
183
  param_schema=data.param_schema,
179
184
  param_in=data.param_in,
180
185
  )
@@ -203,10 +208,12 @@ def update_parameters_with_data(
203
208
  """
204
209
  param, parameters = parameter_from_data(data=data, name=data.name, parameters=parameters, config=config)
205
210
 
206
- if isinstance(param, ParameterError):
211
+ if isinstance(param, ParameterError): # pragma: no cover
207
212
  param.detail = f"{param.header}: {param.detail}"
208
- param.header = f"Unable to parse parameter {ref_path}"
209
- if isinstance(param.data, oai.Reference) and param.data.ref.endswith(ref_path): # pragma: nocover
213
+ param.header = f"Unable to parse parameter {ref_path.get_untrusted_value()}"
214
+ if isinstance(param.data, oai.Reference) and param.data.ref.endswith(
215
+ ref_path.get_untrusted_value()
216
+ ): # pragma: nocover
210
217
  param.detail += (
211
218
  "\n\nRecursive and circular references are not supported. "
212
219
  "See https://github.com/openapi-generators/openapi-python-client/issues/466"
@@ -245,5 +252,5 @@ def parameter_from_reference(
245
252
 
246
253
  _resolved_parameter_class = parameters.classes_by_reference.get(ref_path, None)
247
254
  if _resolved_parameter_class is None:
248
- return ParameterError(detail=f"Reference `{ref_path}` not found.")
255
+ return ParameterError(detail=f"Reference `{ref_path.get_untrusted_value()}` not found.")
249
256
  return _resolved_parameter_class
@@ -2,25 +2,24 @@ from __future__ import annotations
2
2
 
3
3
  from typing import Any, ClassVar, overload
4
4
 
5
- from attr import define
5
+ from attr import define, field
6
6
 
7
7
  from ... import schema as oai
8
- from ... import utils
9
- from ...utils import PythonIdentifier
8
+ from ...strings import PythonCode, PythonIdentifier, remove_string_escapes
10
9
  from ..errors import PropertyError
11
- from .protocol import PropertyProtocol, Value
10
+ from .protocol import PropertyProtocol, Value, convert_example
12
11
 
13
12
 
14
13
  @define
15
14
  class StringProperty(PropertyProtocol):
16
15
  """A property of type str"""
17
16
 
18
- name: str
17
+ name: oai.UntrustedString
19
18
  required: bool
20
19
  default: Value | None
21
20
  python_name: PythonIdentifier
22
- description: str | None
23
- example: str | None
21
+ description: oai.UntrustedString | None
22
+ example: oai.UntrustedString | None = field(converter=convert_example)
24
23
  _type_string: ClassVar[str] = "str"
25
24
  _json_type_string: ClassVar[str] = "str"
26
25
  _allowed_locations: ClassVar[set[oai.ParameterLocation]] = {
@@ -33,12 +32,12 @@ class StringProperty(PropertyProtocol):
33
32
  @classmethod
34
33
  def build(
35
34
  cls,
36
- name: str,
35
+ name: oai.UntrustedString,
37
36
  required: bool,
38
37
  default: Any,
39
38
  python_name: PythonIdentifier,
40
- description: str | None,
41
- example: str | None,
39
+ description: oai.UntrustedString | None,
40
+ example: Any,
42
41
  ) -> StringProperty | PropertyError:
43
42
  checked_default = cls.convert_value(default)
44
43
  return cls(
@@ -65,4 +64,4 @@ class StringProperty(PropertyProtocol):
65
64
  return value
66
65
  if not isinstance(value, str):
67
66
  value = str(value)
68
- return Value(python_code=repr(utils.remove_string_escapes(value)), raw_value=value)
67
+ return Value(python_code=PythonCode(repr(remove_string_escapes(value))), raw_value=value)