pyopenapi-gen 0.14.0__py3-none-any.whl → 0.14.2__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 (80) hide show
  1. pyopenapi_gen/cli.py +3 -3
  2. pyopenapi_gen/context/import_collector.py +10 -10
  3. pyopenapi_gen/context/render_context.py +13 -13
  4. pyopenapi_gen/core/auth/plugins.py +7 -7
  5. pyopenapi_gen/core/http_status_codes.py +2 -4
  6. pyopenapi_gen/core/http_transport.py +19 -19
  7. pyopenapi_gen/core/loader/operations/parser.py +2 -2
  8. pyopenapi_gen/core/loader/operations/request_body.py +3 -3
  9. pyopenapi_gen/core/loader/parameters/parser.py +3 -3
  10. pyopenapi_gen/core/loader/responses/parser.py +2 -2
  11. pyopenapi_gen/core/loader/schemas/extractor.py +4 -4
  12. pyopenapi_gen/core/pagination.py +3 -3
  13. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/list_response.py +3 -3
  14. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/missing_ref.py +2 -2
  15. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/new_schema.py +3 -3
  16. pyopenapi_gen/core/parsing/common/ref_resolution/helpers/stripped_suffix.py +3 -3
  17. pyopenapi_gen/core/parsing/common/ref_resolution/resolve_schema_ref.py +2 -2
  18. pyopenapi_gen/core/parsing/common/type_parser.py +2 -3
  19. pyopenapi_gen/core/parsing/context.py +10 -10
  20. pyopenapi_gen/core/parsing/cycle_helpers.py +5 -2
  21. pyopenapi_gen/core/parsing/keywords/all_of_parser.py +5 -5
  22. pyopenapi_gen/core/parsing/keywords/any_of_parser.py +4 -4
  23. pyopenapi_gen/core/parsing/keywords/array_items_parser.py +4 -4
  24. pyopenapi_gen/core/parsing/keywords/one_of_parser.py +4 -4
  25. pyopenapi_gen/core/parsing/keywords/properties_parser.py +5 -5
  26. pyopenapi_gen/core/parsing/schema_finalizer.py +15 -15
  27. pyopenapi_gen/core/parsing/schema_parser.py +44 -25
  28. pyopenapi_gen/core/parsing/transformers/inline_enum_extractor.py +4 -4
  29. pyopenapi_gen/core/parsing/transformers/inline_object_promoter.py +7 -4
  30. pyopenapi_gen/core/parsing/unified_cycle_detection.py +10 -10
  31. pyopenapi_gen/core/schemas.py +10 -10
  32. pyopenapi_gen/core/streaming_helpers.py +5 -7
  33. pyopenapi_gen/core/telemetry.py +4 -4
  34. pyopenapi_gen/core/utils.py +7 -7
  35. pyopenapi_gen/core/writers/code_writer.py +2 -2
  36. pyopenapi_gen/core/writers/documentation_writer.py +18 -18
  37. pyopenapi_gen/core/writers/line_writer.py +3 -3
  38. pyopenapi_gen/core/writers/python_construct_renderer.py +10 -10
  39. pyopenapi_gen/emit/models_emitter.py +2 -2
  40. pyopenapi_gen/emitters/core_emitter.py +3 -5
  41. pyopenapi_gen/emitters/endpoints_emitter.py +24 -16
  42. pyopenapi_gen/emitters/exceptions_emitter.py +4 -3
  43. pyopenapi_gen/emitters/models_emitter.py +6 -6
  44. pyopenapi_gen/generator/client_generator.py +6 -6
  45. pyopenapi_gen/helpers/endpoint_utils.py +16 -18
  46. pyopenapi_gen/helpers/type_cleaner.py +66 -53
  47. pyopenapi_gen/helpers/type_helper.py +7 -7
  48. pyopenapi_gen/helpers/type_resolution/array_resolver.py +4 -4
  49. pyopenapi_gen/helpers/type_resolution/composition_resolver.py +5 -5
  50. pyopenapi_gen/helpers/type_resolution/finalizer.py +38 -22
  51. pyopenapi_gen/helpers/type_resolution/named_resolver.py +4 -5
  52. pyopenapi_gen/helpers/type_resolution/object_resolver.py +11 -11
  53. pyopenapi_gen/helpers/type_resolution/primitive_resolver.py +1 -2
  54. pyopenapi_gen/helpers/type_resolution/resolver.py +2 -3
  55. pyopenapi_gen/ir.py +32 -34
  56. pyopenapi_gen/types/contracts/protocols.py +5 -5
  57. pyopenapi_gen/types/contracts/types.py +2 -3
  58. pyopenapi_gen/types/resolvers/reference_resolver.py +4 -4
  59. pyopenapi_gen/types/resolvers/response_resolver.py +6 -4
  60. pyopenapi_gen/types/resolvers/schema_resolver.py +32 -16
  61. pyopenapi_gen/types/services/type_service.py +55 -9
  62. pyopenapi_gen/types/strategies/response_strategy.py +6 -7
  63. pyopenapi_gen/visit/client_visitor.py +5 -7
  64. pyopenapi_gen/visit/endpoint/generators/docstring_generator.py +7 -7
  65. pyopenapi_gen/visit/endpoint/generators/request_generator.py +5 -5
  66. pyopenapi_gen/visit/endpoint/generators/response_handler_generator.py +38 -17
  67. pyopenapi_gen/visit/endpoint/generators/signature_generator.py +4 -4
  68. pyopenapi_gen/visit/endpoint/generators/url_args_generator.py +17 -17
  69. pyopenapi_gen/visit/endpoint/processors/import_analyzer.py +8 -8
  70. pyopenapi_gen/visit/endpoint/processors/parameter_processor.py +13 -13
  71. pyopenapi_gen/visit/model/alias_generator.py +1 -4
  72. pyopenapi_gen/visit/model/dataclass_generator.py +139 -10
  73. pyopenapi_gen/visit/model/model_visitor.py +2 -3
  74. pyopenapi_gen/visit/visitor.py +3 -3
  75. {pyopenapi_gen-0.14.0.dist-info → pyopenapi_gen-0.14.2.dist-info}/METADATA +1 -1
  76. pyopenapi_gen-0.14.2.dist-info/RECORD +132 -0
  77. pyopenapi_gen-0.14.0.dist-info/RECORD +0 -132
  78. {pyopenapi_gen-0.14.0.dist-info → pyopenapi_gen-0.14.2.dist-info}/WHEEL +0 -0
  79. {pyopenapi_gen-0.14.0.dist-info → pyopenapi_gen-0.14.2.dist-info}/entry_points.txt +0 -0
  80. {pyopenapi_gen-0.14.0.dist-info → pyopenapi_gen-0.14.2.dist-info}/licenses/LICENSE +0 -0
@@ -8,7 +8,7 @@ usage telemetry for PyOpenAPI Generator. Telemetry is opt-in only.
8
8
  import json
9
9
  import os
10
10
  import time
11
- from typing import Any, Dict, Optional
11
+ from typing import Any
12
12
 
13
13
 
14
14
  class TelemetryClient:
@@ -24,7 +24,7 @@ class TelemetryClient:
24
24
  enabled: Whether telemetry is currently enabled
25
25
  """
26
26
 
27
- def __init__(self, enabled: Optional[bool] = None) -> None:
27
+ def __init__(self, enabled: bool | None = None) -> None:
28
28
  """
29
29
  Initialize a new TelemetryClient.
30
30
 
@@ -38,7 +38,7 @@ class TelemetryClient:
38
38
  else:
39
39
  self.enabled = enabled
40
40
 
41
- def track_event(self, event: str, properties: Optional[Dict[str, Any]] = None) -> None:
41
+ def track_event(self, event: str, properties: dict[str, Any] | None = None) -> None:
42
42
  """
43
43
  Track a telemetry event if telemetry is enabled.
44
44
 
@@ -52,7 +52,7 @@ class TelemetryClient:
52
52
  if not self.enabled:
53
53
  return
54
54
 
55
- data: Dict[str, Any] = {
55
+ data: dict[str, Any] = {
56
56
  "event": event,
57
57
  "properties": properties or {},
58
58
  "timestamp": time.time(),
@@ -8,7 +8,7 @@ import keyword
8
8
  import logging
9
9
  import re
10
10
  from datetime import datetime
11
- from typing import Any, Dict, Set, Type, TypeVar, cast
11
+ from typing import Any, Set, Type, TypeVar, cast
12
12
 
13
13
  logger = logging.getLogger(__name__)
14
14
 
@@ -228,7 +228,7 @@ class ParamSubstitutor:
228
228
  """Helper for rendering path templates with path parameters."""
229
229
 
230
230
  @staticmethod
231
- def render_path(template: str, values: Dict[str, Any]) -> str:
231
+ def render_path(template: str, values: dict[str, Any]) -> str:
232
232
  """Replace placeholders in a URL path template using provided values."""
233
233
  rendered = template
234
234
  for key, val in values.items():
@@ -240,7 +240,7 @@ class KwargsBuilder:
240
240
  """Builder for assembling HTTP request keyword arguments."""
241
241
 
242
242
  def __init__(self) -> None:
243
- self._kwargs: Dict[str, Any] = {}
243
+ self._kwargs: dict[str, Any] = {}
244
244
 
245
245
  def with_params(self, **params: Any) -> "KwargsBuilder":
246
246
  """Add query parameters, skipping None values."""
@@ -254,7 +254,7 @@ class KwargsBuilder:
254
254
  self._kwargs["json"] = body
255
255
  return self
256
256
 
257
- def build(self) -> Dict[str, Any]:
257
+ def build(self) -> dict[str, Any]:
258
258
  """Return the assembled kwargs dictionary."""
259
259
  return self._kwargs
260
260
 
@@ -263,10 +263,10 @@ class Formatter:
263
263
  """Helper to format code using Black, falling back to unformatted content if Black is unavailable or errors."""
264
264
 
265
265
  def __init__(self) -> None:
266
- from typing import Any, Callable, Optional
266
+ from typing import Any, Callable
267
267
 
268
- self._file_mode: Optional[Any] = None
269
- self._format_str: Optional[Callable[..., str]] = None
268
+ self._file_mode: Any | None = None
269
+ self._format_str: Callable[..., str] | None = None
270
270
  try:
271
271
  from black import FileMode, format_str
272
272
 
@@ -6,7 +6,7 @@ writing lines and blocks, and supporting wrapped output for code and docstrings.
6
6
  to be used by code generation visitors and emitters to ensure consistent, readable output.
7
7
  """
8
8
 
9
- from typing import List, Optional
9
+ from typing import List
10
10
 
11
11
  from .line_writer import LineWriter
12
12
 
@@ -88,7 +88,7 @@ class CodeWriter:
88
88
  self.writer.max_width = old_width
89
89
 
90
90
  def write_function_signature(
91
- self, name: str, args: List[str], return_type: Optional[str] = None, async_: bool = False
91
+ self, name: str, args: List[str], return_type: str | None = None, async_: bool = False
92
92
  ) -> None:
93
93
  """
94
94
  Write a function or method signature, with each argument on its own line and correct indentation.
@@ -6,7 +6,7 @@ for building comprehensive, type-rich docstrings for generated Python code. It s
6
6
  alignment, line wrapping, and section formatting for Args, Returns, and Raises.
7
7
  """
8
8
 
9
- from typing import List, Optional, Tuple, Union
9
+ from typing import List, Tuple, Union
10
10
 
11
11
  from .line_writer import LineWriter
12
12
 
@@ -16,36 +16,36 @@ class DocumentationBlock:
16
16
  Data container for docstring content.
17
17
 
18
18
  Attributes:
19
- summary (Optional[str]): The summary line for the docstring.
20
- description (Optional[str]): The detailed description.
19
+ summary (str | None): The summary line for the docstring.
20
+ description (str | None): The detailed description.
21
21
  args (Optional[List[Union[Tuple[str, str, str], Tuple[str, str]]]]):
22
22
  List of arguments as (name, type, desc) or (type, desc) tuples.
23
- returns (Optional[Tuple[str, str]]): The return type and description.
24
- raises (Optional[List[Tuple[str, str]]]): List of (exception type, description) tuples.
23
+ returns (Tuple[str, str] | None): The return type and description.
24
+ raises (List[Tuple[str, str]] | None): List of (exception type, description) tuples.
25
25
  """
26
26
 
27
27
  def __init__(
28
28
  self,
29
- summary: Optional[str] = None,
30
- description: Optional[str] = None,
31
- args: Optional[List[Union[Tuple[str, str, str], Tuple[str, str]]]] = None,
32
- returns: Optional[Tuple[str, str]] = None,
33
- raises: Optional[List[Tuple[str, str]]] = None,
29
+ summary: str | None = None,
30
+ description: str | None = None,
31
+ args: List[Union[Tuple[str, str, str], Tuple[str, str]]] | None = None,
32
+ returns: Tuple[str, str] | None = None,
33
+ raises: List[Tuple[str, str]] | None = None,
34
34
  ) -> None:
35
35
  """
36
36
  Initialize a DocumentationBlock.
37
37
 
38
38
  Args:
39
- summary (Optional[str]): The summary line.
40
- description (Optional[str]): The detailed description.
39
+ summary (str | None): The summary line.
40
+ description (str | None): The detailed description.
41
41
  args (Optional[List[Union[Tuple[str, str, str], Tuple[str, str]]]]): Arguments.
42
- returns (Optional[Tuple[str, str]]): Return type and description.
43
- raises (Optional[List[Tuple[str, str]]]): Exceptions.
42
+ returns (Tuple[str, str] | None): Return type and description.
43
+ raises (List[Tuple[str, str]] | None): Exceptions.
44
44
  """
45
- self.summary: Optional[str] = summary
46
- self.description: Optional[str] = description
45
+ self.summary: str | None = summary
46
+ self.description: str | None = description
47
47
  self.args: List[Union[Tuple[str, str, str], Tuple[str, str]]] = args or []
48
- self.returns: Optional[Tuple[str, str]] = returns
48
+ self.returns: Tuple[str, str] | None = returns
49
49
  self.raises: List[Tuple[str, str]] = raises or []
50
50
 
51
51
 
@@ -58,7 +58,7 @@ class DocumentationFormatter:
58
58
  self.width: int = width
59
59
  self.min_desc_col: int = min_desc_col
60
60
 
61
- def wrap(self, text: str, indent: int, prefix: Optional[str] = None) -> List[str]:
61
+ def wrap(self, text: str, indent: int, prefix: str | None = None) -> List[str]:
62
62
  if not text:
63
63
  return []
64
64
  writer = LineWriter(max_width=self.width)
@@ -5,7 +5,7 @@ This class is designed for use in both code and documentation generation, provid
5
5
  new lines, and query the current line's width.
6
6
  """
7
7
 
8
- from typing import List, Optional
8
+ from typing import List
9
9
 
10
10
 
11
11
  class LineWriter:
@@ -169,7 +169,7 @@ class LineWriter:
169
169
  self.lines[-1] += " " * (col - current - 1)
170
170
  # If already at or past col, do nothing
171
171
 
172
- def append_wrapped_at_column(self, text: str, width: int, col: Optional[int] = None) -> None:
172
+ def append_wrapped_at_column(self, text: str, width: int, col: int | None = None) -> None:
173
173
  """
174
174
  Append text, wrapping as needed, so that the first line continues from the current position,
175
175
  and all subsequent lines start at column `col`.
@@ -180,7 +180,7 @@ class LineWriter:
180
180
  Args:
181
181
  text (str) : The text to append and wrap.
182
182
  width (int) : The maximum line width.
183
- col (Optional[int]) : The column at which to start wrapped lines. If None, uses current
183
+ col (int | None) : The column at which to start wrapped lines. If None, uses current
184
184
  line width.
185
185
  """
186
186
  import textwrap
@@ -7,7 +7,7 @@ It handles all the details of formatting, import registration, and docstring gen
7
7
  for these constructs.
8
8
  """
9
9
 
10
- from typing import Dict, List, Optional, Tuple
10
+ from typing import List, Tuple
11
11
 
12
12
  from pyopenapi_gen.context.render_context import RenderContext
13
13
 
@@ -35,7 +35,7 @@ class PythonConstructRenderer:
35
35
  self,
36
36
  alias_name: str,
37
37
  target_type: str,
38
- description: Optional[str],
38
+ description: str | None,
39
39
  context: RenderContext,
40
40
  ) -> str:
41
41
  """
@@ -80,7 +80,7 @@ class PythonConstructRenderer:
80
80
  enum_name: str,
81
81
  base_type: str, # 'str' or 'int'
82
82
  values: List[Tuple[str, str | int]], # List of (MEMBER_NAME, value)
83
- description: Optional[str],
83
+ description: str | None,
84
84
  context: RenderContext,
85
85
  ) -> str:
86
86
  """
@@ -143,10 +143,10 @@ class PythonConstructRenderer:
143
143
  def render_dataclass(
144
144
  self,
145
145
  class_name: str,
146
- fields: List[Tuple[str, str, Optional[str], Optional[str]]], # name, type_hint, default_expr, description
147
- description: Optional[str],
146
+ fields: List[Tuple[str, str, str | None, str | None]], # name, type_hint, default_expr, description
147
+ description: str | None,
148
148
  context: RenderContext,
149
- field_mappings: Optional[Dict[str, str]] = None,
149
+ field_mappings: dict[str, str] | None = None,
150
150
  ) -> str:
151
151
  """
152
152
  Render a dataclass as Python code with BaseSchema support.
@@ -168,7 +168,7 @@ class PythonConstructRenderer:
168
168
  \"\"\"User information with automatic JSON field mapping.\"\"\"
169
169
  id_: str
170
170
  first_name: str
171
- email: Optional[str] = None
171
+ email: str | None = None
172
172
  is_active: bool = True
173
173
 
174
174
  class Meta:
@@ -274,9 +274,9 @@ class PythonConstructRenderer:
274
274
  def render_class(
275
275
  self,
276
276
  class_name: str,
277
- base_classes: Optional[List[str]],
278
- docstring: Optional[str],
279
- body_lines: Optional[List[str]],
277
+ base_classes: List[str] | None,
278
+ docstring: str | None,
279
+ body_lines: List[str] | None,
280
280
  context: RenderContext,
281
281
  ) -> str:
282
282
  """
@@ -1,6 +1,6 @@
1
1
  import logging
2
2
  from pathlib import Path
3
- from typing import List, Optional
3
+ from typing import List
4
4
 
5
5
  from pyopenapi_gen.context.render_context import RenderContext
6
6
  from pyopenapi_gen.core.utils import NameSanitizer
@@ -20,7 +20,7 @@ class ModelsEmitter:
20
20
  # self.writer an instance CodeWriter() here seems unused globally for this emitter.
21
21
  # Each file generation part either writes directly or uses a local CodeWriter.
22
22
 
23
- def _generate_model_file(self, schema_ir: IRSchema, models_dir: Path) -> Optional[str]:
23
+ def _generate_model_file(self, schema_ir: IRSchema, models_dir: Path) -> str | None:
24
24
  """Generates a single Python file for a given IRSchema. Returns file path if generated."""
25
25
  if not schema_ir.name:
26
26
  logger.warning(f"Skipping model generation for schema without a name: {schema_ir}")
@@ -1,6 +1,6 @@
1
1
  import importlib.resources
2
2
  import os
3
- from typing import List, Optional
3
+ from typing import List
4
4
 
5
5
  from pyopenapi_gen.context.file_manager import FileManager
6
6
 
@@ -22,12 +22,10 @@ CORE_README_TEMPLATE_FILENAME = "README.md"
22
22
 
23
23
  CONFIG_TEMPLATE = """
24
24
  from dataclasses import dataclass
25
- from typing import Optional
26
-
27
25
  @dataclass
28
26
  class ClientConfig:
29
27
  base_url: str
30
- timeout: Optional[float] = 30.0
28
+ timeout: float | None = 30.0
31
29
  """
32
30
 
33
31
 
@@ -35,7 +33,7 @@ class CoreEmitter:
35
33
  """Copies all required runtime files into the generated core module."""
36
34
 
37
35
  def __init__(
38
- self, core_dir: str = "core", core_package: str = "core", exception_alias_names: Optional[List[str]] = None
36
+ self, core_dir: str = "core", core_package: str = "core", exception_alias_names: List[str] | None = None
39
37
  ):
40
38
  # core_dir is the relative path WITHIN the output package, e.g., "core" or "shared/core"
41
39
  # core_package is the Python import name, e.g., "core" or "shared.core"
@@ -1,6 +1,6 @@
1
1
  import logging
2
2
  from pathlib import Path
3
- from typing import Dict, List, Optional, Tuple
3
+ from typing import List, Tuple
4
4
 
5
5
  from pyopenapi_gen import IROperation, IRParameter, IRRequestBody
6
6
  from pyopenapi_gen.context.render_context import RenderContext
@@ -17,7 +17,7 @@ PARAM_TYPE_MAPPING = {
17
17
  "boolean": "bool",
18
18
  "string": "str",
19
19
  "array": "List",
20
- "object": "Dict[str, Any]",
20
+ "object": "dict[str, Any]",
21
21
  }
22
22
  # Format-specific overrides
23
23
  PARAM_FORMAT_MAPPING = {
@@ -47,7 +47,7 @@ def schema_to_type(schema: IRParameter) -> str:
47
47
  # Array handling
48
48
  elif s.type == "array" and s.items:
49
49
  # For array items, we recursively call schema_to_type.
50
- # The nullability of the item_type itself (e.g. List[Optional[int]])
50
+ # The nullability of the item_type itself (e.g. List[int | None])
51
51
  # will be handled by the recursive call based on s.items.is_nullable.
52
52
  item_schema_as_param = IRParameter(name="_item", param_in="_internal", required=False, schema=s.items)
53
53
  item_type_str = schema_to_type(item_schema_as_param)
@@ -70,8 +70,8 @@ def schema_to_type(schema: IRParameter) -> str:
70
70
  # 2. Apply nullability based on IRSchema's is_nullable field
71
71
  # This s.is_nullable should be the source of truth from the IR after parsing.
72
72
  if s.is_nullable:
73
- # Ensure "Any" also gets wrapped, e.g. Optional[Any]
74
- py_type = f"Optional[{py_type}]"
73
+ # Ensure "Any" also gets wrapped, e.g. Any | None
74
+ py_type = f"{py_type} | None"
75
75
 
76
76
  return py_type
77
77
 
@@ -82,7 +82,7 @@ def _get_request_body_type(body: IRRequestBody) -> str:
82
82
  if "json" in mt.lower():
83
83
  return schema_to_type(IRParameter(name="body", param_in="body", required=body.required, schema=sch))
84
84
  # Fallback to generic dict
85
- return "Dict[str, Any]"
85
+ return "dict[str, Any]"
86
86
 
87
87
 
88
88
  def _deduplicate_tag_clients(client_classes: List[Tuple[str, str]]) -> List[Tuple[str, str]]:
@@ -106,16 +106,20 @@ class EndpointsEmitter:
106
106
  def __init__(self, context: RenderContext) -> None:
107
107
  self.context = context
108
108
  self.formatter = Formatter()
109
- self.visitor: Optional[EndpointVisitor] = None
109
+ self.visitor: EndpointVisitor | None = None
110
110
 
111
- def _deduplicate_operation_ids(self, operations: List[IROperation]) -> None:
111
+ def _deduplicate_operation_ids_globally(self, operations: List[IROperation]) -> None:
112
112
  """
113
- Ensures all operations have unique method names within a tag.
113
+ Ensures all operations have unique method names globally across all tags.
114
+
115
+ This prevents the bug where operations with multiple tags share the same
116
+ IROperation object reference, causing _deduplicate_operation_ids() to
117
+ modify the same object multiple times and accumulate _2_2 suffixes.
114
118
 
115
119
  Args:
116
- operations: List of operations for a single tag.
120
+ operations: List of all operations across all tags.
117
121
  """
118
- seen_methods: Dict[str, int] = {}
122
+ seen_methods: dict[str, int] = {}
119
123
  for op in operations:
120
124
  method_name = NameSanitizer.sanitize_method_name(op.operation_id)
121
125
  if method_name in seen_methods:
@@ -144,7 +148,7 @@ class EndpointsEmitter:
144
148
  self.context.file_manager.write_file(str(file_path), content)
145
149
 
146
150
  # Ensure parsed_schemas is at least an empty dict if None,
147
- # as EndpointVisitor expects Dict[str, IRSchema]
151
+ # as EndpointVisitor expects dict[str, IRSchema]
148
152
  current_parsed_schemas = self.context.parsed_schemas
149
153
  if current_parsed_schemas is None:
150
154
  logger.warning(
@@ -156,8 +160,12 @@ class EndpointsEmitter:
156
160
  if self.visitor is None:
157
161
  self.visitor = EndpointVisitor(current_parsed_schemas) # Pass the (potentially defaulted) dict
158
162
 
159
- tag_key_to_ops: Dict[str, List[IROperation]] = {}
160
- tag_key_to_candidates: Dict[str, List[str]] = {}
163
+ # Deduplicate operation IDs globally BEFORE tag grouping to prevent
164
+ # multi-tag operations from accumulating _2_2 suffixes
165
+ self._deduplicate_operation_ids_globally(operations)
166
+
167
+ tag_key_to_ops: dict[str, List[IROperation]] = {}
168
+ tag_key_to_candidates: dict[str, List[str]] = {}
161
169
  for op in operations:
162
170
  tags = op.tags or [DEFAULT_TAG]
163
171
  for tag in tags:
@@ -175,7 +183,7 @@ class EndpointsEmitter:
175
183
  upper = sum(1 for c in t if c.isupper())
176
184
  return (is_pascal, word_count, upper, t)
177
185
 
178
- tag_map: Dict[str, str] = {}
186
+ tag_map: dict[str, str] = {}
179
187
  for key, candidates in tag_key_to_candidates.items():
180
188
  best_tag_for_key = DEFAULT_TAG # Default if no candidates somehow
181
189
  if candidates:
@@ -194,7 +202,7 @@ class EndpointsEmitter:
194
202
  # This will set current_file and reset+reinit import_collector's context
195
203
  self.context.set_current_file(str(file_path))
196
204
 
197
- self._deduplicate_operation_ids(ops_for_tag)
205
+ # Deduplication now done globally before tag grouping (see above)
198
206
 
199
207
  # EndpointVisitor must exist here due to check above
200
208
  if self.visitor is None:
@@ -1,7 +1,6 @@
1
1
  import json
2
2
  import os
3
3
  from pathlib import Path
4
- from typing import Optional
5
4
 
6
5
  from pyopenapi_gen import IRSpec
7
6
  from pyopenapi_gen.context.render_context import RenderContext
@@ -22,13 +21,13 @@ class ExceptionsEmitter:
22
21
  exceptions are available.
23
22
  """
24
23
 
25
- def __init__(self, core_package_name: str = "core", overall_project_root: Optional[str] = None) -> None:
24
+ def __init__(self, core_package_name: str = "core", overall_project_root: str | None = None) -> None:
26
25
  self.visitor = ExceptionVisitor()
27
26
  self.core_package_name = core_package_name
28
27
  self.overall_project_root = overall_project_root
29
28
 
30
29
  def emit(
31
- self, spec: IRSpec, output_dir: str, client_package_name: Optional[str] = None
30
+ self, spec: IRSpec, output_dir: str, client_package_name: str | None = None
32
31
  ) -> tuple[list[str], list[str]]:
33
32
  """Generate exception aliases for the given spec.
34
33
 
@@ -61,6 +60,8 @@ class ExceptionsEmitter:
61
60
 
62
61
  generated_imports = context.render_imports()
63
62
 
63
+ alias_names.sort()
64
+
64
65
  # Add __all__ list with proper spacing (2 blank lines after last class - Ruff E305)
65
66
  if alias_names:
66
67
  all_list_str = ", ".join([f'"{name}"' for name in alias_names])
@@ -1,6 +1,6 @@
1
1
  import logging
2
2
  from pathlib import Path
3
- from typing import Dict, List, Optional, Set
3
+ from typing import List, Set
4
4
 
5
5
  from pyopenapi_gen import IRSchema, IRSpec
6
6
  from pyopenapi_gen.context.render_context import RenderContext
@@ -22,15 +22,15 @@ class ModelsEmitter:
22
22
  Handles creation of __init__.py and py.typed files.
23
23
  """
24
24
 
25
- def __init__(self, context: RenderContext, parsed_schemas: Dict[str, IRSchema]):
25
+ def __init__(self, context: RenderContext, parsed_schemas: dict[str, IRSchema]):
26
26
  self.context: RenderContext = context
27
27
  # Store a reference to the schemas that were passed in.
28
28
  # These schemas will have their .generation_name and .final_module_stem updated.
29
- self.parsed_schemas: Dict[str, IRSchema] = parsed_schemas
29
+ self.parsed_schemas: dict[str, IRSchema] = parsed_schemas
30
30
  self.import_collector = self.context.import_collector
31
31
  self.writer = CodeWriter()
32
32
 
33
- def _generate_model_file(self, schema_ir: IRSchema, models_dir: Path) -> Optional[str]:
33
+ def _generate_model_file(self, schema_ir: IRSchema, models_dir: Path) -> str | None:
34
34
  """Generates a single Python file for a given IRSchema."""
35
35
  if not schema_ir.name: # Original name, used for logging/initial identification
36
36
  logger.warning(f"Skipping model generation for schema without an original name: {schema_ir}")
@@ -168,7 +168,7 @@ class ModelsEmitter:
168
168
  generated_content = init_writer.get_code()
169
169
  return generated_content
170
170
 
171
- def emit(self, spec: IRSpec, output_root: str) -> Dict[str, List[str]]:
171
+ def emit(self, spec: IRSpec, output_root: str) -> dict[str, List[str]]:
172
172
  """Emits all model files derived from IR schemas.
173
173
 
174
174
  Contracts:
@@ -352,7 +352,7 @@ class ModelsEmitter:
352
352
 
353
353
  # Fetch the schema_ir object using the key from all_schemas_for_generation
354
354
  # This ensures we are working with the potentially newly created & named schemas.
355
- current_schema_ir_obj: Optional[IRSchema] = all_schemas_for_generation.get(schema_key)
355
+ current_schema_ir_obj: IRSchema | None = all_schemas_for_generation.get(schema_key)
356
356
 
357
357
  if not current_schema_ir_obj:
358
358
  logger.warning(f"Schema key '{schema_key}' from all_schemas_for_generation not found. Skipping.")
@@ -9,7 +9,7 @@ import tempfile
9
9
  import time
10
10
  from datetime import datetime
11
11
  from pathlib import Path
12
- from typing import Any, Dict, List, Optional
12
+ from typing import Any, List
13
13
 
14
14
  from pyopenapi_gen.context.render_context import RenderContext
15
15
  from pyopenapi_gen.core.loader.loader import load_ir_from_spec
@@ -47,9 +47,9 @@ class ClientGenerator:
47
47
  """
48
48
  self.verbose = verbose
49
49
  self.start_time = time.time()
50
- self.timings: Dict[str, float] = {}
50
+ self.timings: dict[str, float] = {}
51
51
 
52
- def _log_progress(self, message: str, stage: Optional[str] = None) -> None:
52
+ def _log_progress(self, message: str, stage: str | None = None) -> None:
53
53
  """
54
54
  Log a progress message with timestamp.
55
55
 
@@ -89,7 +89,7 @@ class ClientGenerator:
89
89
  output_package: str,
90
90
  force: bool = False,
91
91
  no_postprocess: bool = False,
92
- core_package: Optional[str] = None,
92
+ core_package: str | None = None,
93
93
  ) -> List[Path]:
94
94
  """
95
95
  Generate the client code from the OpenAPI spec.
@@ -99,10 +99,10 @@ class ClientGenerator:
99
99
  project_root (Path): Path to the root of the Python project (absolute or relative).
100
100
  output_package (str): Python package path for the generated client (e.g., 'pyapis.my_api_client').
101
101
  force (bool): Overwrite output without diff check.
102
- name (Optional[str]): Custom client package name (not used).
102
+ name (str | None): Custom client package name (not used).
103
103
  docs (bool): Kept for interface compatibility.
104
104
  telemetry (bool): Kept for interface compatibility.
105
- auth (Optional[str]): Kept for interface compatibility.
105
+ auth (str | None): Kept for interface compatibility.
106
106
  no_postprocess (bool): Skip post-processing (type checking, etc.).
107
107
  core_package (str): Python package path for the core package.
108
108