pyopenapi-gen 0.13.0__py3-none-any.whl → 0.14.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 (82) 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 +218 -0
  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/postprocess_manager.py +85 -12
  32. pyopenapi_gen/core/schemas.py +10 -10
  33. pyopenapi_gen/core/streaming_helpers.py +5 -7
  34. pyopenapi_gen/core/telemetry.py +4 -4
  35. pyopenapi_gen/core/utils.py +7 -7
  36. pyopenapi_gen/core/writers/code_writer.py +2 -2
  37. pyopenapi_gen/core/writers/documentation_writer.py +18 -18
  38. pyopenapi_gen/core/writers/line_writer.py +3 -3
  39. pyopenapi_gen/core/writers/python_construct_renderer.py +15 -11
  40. pyopenapi_gen/emit/models_emitter.py +2 -2
  41. pyopenapi_gen/emitters/core_emitter.py +3 -5
  42. pyopenapi_gen/emitters/endpoints_emitter.py +12 -12
  43. pyopenapi_gen/emitters/exceptions_emitter.py +153 -18
  44. pyopenapi_gen/emitters/models_emitter.py +6 -6
  45. pyopenapi_gen/generator/client_generator.py +10 -8
  46. pyopenapi_gen/helpers/endpoint_utils.py +16 -18
  47. pyopenapi_gen/helpers/type_cleaner.py +66 -53
  48. pyopenapi_gen/helpers/type_helper.py +7 -7
  49. pyopenapi_gen/helpers/type_resolution/array_resolver.py +4 -4
  50. pyopenapi_gen/helpers/type_resolution/composition_resolver.py +5 -5
  51. pyopenapi_gen/helpers/type_resolution/finalizer.py +38 -22
  52. pyopenapi_gen/helpers/type_resolution/named_resolver.py +4 -5
  53. pyopenapi_gen/helpers/type_resolution/object_resolver.py +11 -11
  54. pyopenapi_gen/helpers/type_resolution/primitive_resolver.py +1 -2
  55. pyopenapi_gen/helpers/type_resolution/resolver.py +2 -3
  56. pyopenapi_gen/ir.py +32 -34
  57. pyopenapi_gen/types/contracts/protocols.py +5 -5
  58. pyopenapi_gen/types/contracts/types.py +2 -3
  59. pyopenapi_gen/types/resolvers/reference_resolver.py +4 -4
  60. pyopenapi_gen/types/resolvers/response_resolver.py +6 -4
  61. pyopenapi_gen/types/resolvers/schema_resolver.py +32 -16
  62. pyopenapi_gen/types/services/type_service.py +55 -9
  63. pyopenapi_gen/types/strategies/response_strategy.py +6 -7
  64. pyopenapi_gen/visit/client_visitor.py +5 -7
  65. pyopenapi_gen/visit/endpoint/generators/docstring_generator.py +7 -7
  66. pyopenapi_gen/visit/endpoint/generators/request_generator.py +5 -5
  67. pyopenapi_gen/visit/endpoint/generators/response_handler_generator.py +41 -19
  68. pyopenapi_gen/visit/endpoint/generators/signature_generator.py +4 -4
  69. pyopenapi_gen/visit/endpoint/generators/url_args_generator.py +17 -17
  70. pyopenapi_gen/visit/endpoint/processors/import_analyzer.py +8 -8
  71. pyopenapi_gen/visit/endpoint/processors/parameter_processor.py +13 -13
  72. pyopenapi_gen/visit/exception_visitor.py +54 -16
  73. pyopenapi_gen/visit/model/alias_generator.py +1 -4
  74. pyopenapi_gen/visit/model/dataclass_generator.py +139 -10
  75. pyopenapi_gen/visit/model/model_visitor.py +2 -3
  76. pyopenapi_gen/visit/visitor.py +3 -3
  77. {pyopenapi_gen-0.13.0.dist-info → pyopenapi_gen-0.14.1.dist-info}/METADATA +1 -1
  78. pyopenapi_gen-0.14.1.dist-info/RECORD +132 -0
  79. pyopenapi_gen-0.13.0.dist-info/RECORD +0 -131
  80. {pyopenapi_gen-0.13.0.dist-info → pyopenapi_gen-0.14.1.dist-info}/WHEEL +0 -0
  81. {pyopenapi_gen-0.13.0.dist-info → pyopenapi_gen-0.14.1.dist-info}/entry_points.txt +0 -0
  82. {pyopenapi_gen-0.13.0.dist-info → pyopenapi_gen-0.14.1.dist-info}/licenses/LICENSE +0 -0
@@ -10,7 +10,7 @@ from __future__ import annotations
10
10
  import logging
11
11
  from dataclasses import dataclass, field
12
12
  from enum import Enum
13
- from typing import Dict, List, Optional, Set
13
+ from typing import List, Set
14
14
 
15
15
  from pyopenapi_gen import IRSchema
16
16
  from pyopenapi_gen.core.utils import NameSanitizer
@@ -62,10 +62,10 @@ class CycleDetectionResult:
62
62
  """Result of cycle detection check."""
63
63
 
64
64
  is_cycle: bool
65
- cycle_type: Optional[CycleType]
65
+ cycle_type: CycleType | None
66
66
  action: CycleAction
67
- cycle_info: Optional[CycleInfo] = None
68
- placeholder_schema: Optional[IRSchema] = None
67
+ cycle_info: CycleInfo | None = None
68
+ placeholder_schema: IRSchema | None = None
69
69
 
70
70
 
71
71
  @dataclass
@@ -74,8 +74,8 @@ class UnifiedCycleContext:
74
74
 
75
75
  # Core tracking
76
76
  schema_stack: List[str] = field(default_factory=list)
77
- schema_states: Dict[str, SchemaState] = field(default_factory=dict)
78
- parsed_schemas: Dict[str, IRSchema] = field(default_factory=dict)
77
+ schema_states: dict[str, SchemaState] = field(default_factory=dict)
78
+ parsed_schemas: dict[str, IRSchema] = field(default_factory=dict)
79
79
  recursion_depth: int = 0
80
80
 
81
81
  # Detection results
@@ -155,7 +155,7 @@ def create_depth_placeholder(schema_name: str, depth: int) -> IRSchema:
155
155
  )
156
156
 
157
157
 
158
- def unified_cycle_check(schema_name: Optional[str], context: UnifiedCycleContext) -> CycleDetectionResult:
158
+ def unified_cycle_check(schema_name: str | None, context: UnifiedCycleContext) -> CycleDetectionResult:
159
159
  """Unified cycle detection that handles all cases."""
160
160
 
161
161
  if schema_name is None:
@@ -262,7 +262,7 @@ def unified_cycle_check(schema_name: Optional[str], context: UnifiedCycleContext
262
262
  return CycleDetectionResult(False, None, CycleAction.CONTINUE_PARSING)
263
263
 
264
264
 
265
- def unified_enter_schema(schema_name: Optional[str], context: UnifiedCycleContext) -> CycleDetectionResult:
265
+ def unified_enter_schema(schema_name: str | None, context: UnifiedCycleContext) -> CycleDetectionResult:
266
266
  """Unified entry point that always maintains consistent state."""
267
267
  context.recursion_depth += 1
268
268
 
@@ -275,7 +275,7 @@ def unified_enter_schema(schema_name: Optional[str], context: UnifiedCycleContex
275
275
  return result
276
276
 
277
277
 
278
- def unified_exit_schema(schema_name: Optional[str], context: UnifiedCycleContext) -> None:
278
+ def unified_exit_schema(schema_name: str | None, context: UnifiedCycleContext) -> None:
279
279
  """Unified exit that always maintains consistent state."""
280
280
  if context.recursion_depth > 0:
281
281
  context.recursion_depth -= 1
@@ -288,6 +288,6 @@ def unified_exit_schema(schema_name: Optional[str], context: UnifiedCycleContext
288
288
  context.schema_states[schema_name] = SchemaState.COMPLETED
289
289
 
290
290
 
291
- def get_schema_or_placeholder(schema_name: str, context: UnifiedCycleContext) -> Optional[IRSchema]:
291
+ def get_schema_or_placeholder(schema_name: str, context: UnifiedCycleContext) -> IRSchema | None:
292
292
  """Get an existing schema or placeholder from the context."""
293
293
  return context.parsed_schemas.get(schema_name)
@@ -32,13 +32,15 @@ class PostprocessManager:
32
32
  # Ensure all targets are Path objects
33
33
  target_paths = [Path(t) for t in targets]
34
34
 
35
- # --- RE-ENABLE RUFF CHECKS ---
36
- for target_path in target_paths:
37
- if target_path.is_file() and target_path.suffix == ".py":
38
- self.remove_unused_imports(target_path)
39
- self.sort_imports(target_path)
40
- self.format_code(target_path)
41
- # --- END RE-ENABLE ---
35
+ # OPTIMISED: Run Ruff once on all files instead of per-file
36
+ # Collect all Python files
37
+ python_files = [p for p in target_paths if p.is_file() and p.suffix == ".py"]
38
+
39
+ if python_files:
40
+ # Run Ruff checks once on all files (much faster than per-file)
41
+ self.remove_unused_imports_bulk(python_files)
42
+ self.sort_imports_bulk(python_files)
43
+ self.format_code_bulk(python_files)
42
44
 
43
45
  # Determine the package root directory(s) for Mypy
44
46
  package_roots = set()
@@ -58,11 +60,82 @@ class PostprocessManager:
58
60
  package_roots.add(target_path)
59
61
 
60
62
  # Run Mypy on each identified package root
61
- if package_roots:
62
- print(f"Running Mypy on package root(s): {package_roots}")
63
- for root_dir in package_roots:
64
- print(f"Running mypy on {root_dir}...")
65
- self.type_check(root_dir)
63
+ # TEMPORARILY DISABLED: Mypy is slow on large specs, disabled for faster iteration
64
+ # if package_roots:
65
+ # print(f"Running Mypy on package root(s): {package_roots}")
66
+ # for root_dir in package_roots:
67
+ # print(f"Running mypy on {root_dir}...")
68
+ # self.type_check(root_dir)
69
+
70
+ def remove_unused_imports_bulk(self, targets: List[Path]) -> None:
71
+ """Remove unused imports from multiple targets using Ruff (bulk operation)."""
72
+ if not targets:
73
+ return
74
+ result = subprocess.run(
75
+ [
76
+ sys.executable,
77
+ "-m",
78
+ "ruff",
79
+ "check",
80
+ "--select=F401",
81
+ "--fix",
82
+ ]
83
+ + [str(t) for t in targets],
84
+ stdout=subprocess.PIPE,
85
+ stderr=subprocess.PIPE,
86
+ text=True,
87
+ )
88
+ if result.returncode != 0 or result.stderr:
89
+ if result.stdout:
90
+ _print_filtered_stdout(result.stdout)
91
+ if result.stderr:
92
+ print(result.stderr, file=sys.stderr)
93
+
94
+ def sort_imports_bulk(self, targets: List[Path]) -> None:
95
+ """Sort imports in multiple targets using Ruff (bulk operation)."""
96
+ if not targets:
97
+ return
98
+ result = subprocess.run(
99
+ [
100
+ sys.executable,
101
+ "-m",
102
+ "ruff",
103
+ "check",
104
+ "--select=I",
105
+ "--fix",
106
+ ]
107
+ + [str(t) for t in targets],
108
+ stdout=subprocess.PIPE,
109
+ stderr=subprocess.PIPE,
110
+ text=True,
111
+ )
112
+ if result.returncode != 0 or result.stderr:
113
+ if result.stdout:
114
+ _print_filtered_stdout(result.stdout)
115
+ if result.stderr:
116
+ print(result.stderr, file=sys.stderr)
117
+
118
+ def format_code_bulk(self, targets: List[Path]) -> None:
119
+ """Format code in multiple targets using Ruff (bulk operation)."""
120
+ if not targets:
121
+ return
122
+ result = subprocess.run(
123
+ [
124
+ sys.executable,
125
+ "-m",
126
+ "ruff",
127
+ "format",
128
+ ]
129
+ + [str(t) for t in targets],
130
+ stdout=subprocess.PIPE,
131
+ stderr=subprocess.PIPE,
132
+ text=True,
133
+ )
134
+ if result.returncode != 0 or result.stderr:
135
+ if result.stdout:
136
+ _print_filtered_stdout(result.stdout)
137
+ if result.stderr:
138
+ print(result.stderr, file=sys.stderr)
66
139
 
67
140
  def remove_unused_imports(self, target: Union[str, Path]) -> None:
68
141
  """Remove unused imports from the target using Ruff."""
@@ -1,7 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from dataclasses import MISSING, dataclass, fields
4
- from typing import Any, Dict, Type, TypeVar, Union, get_args, get_origin, get_type_hints
4
+ from typing import Any, Type, TypeVar, Union, get_args, get_origin, get_type_hints
5
5
 
6
6
  T = TypeVar("T", bound="BaseSchema")
7
7
 
@@ -10,7 +10,7 @@ def _extract_base_type(field_type: Any) -> Any:
10
10
  """Extract the base type from Optional/Union types."""
11
11
  origin = get_origin(field_type)
12
12
  if origin is Union:
13
- # For Optional[T] or Union[T, None], get the non-None type
13
+ # For T | None or Union[T, None], get the non-None type
14
14
  args = get_args(field_type)
15
15
  non_none_args = [arg for arg in args if arg is not type(None)]
16
16
  if len(non_none_args) == 1:
@@ -23,26 +23,26 @@ class BaseSchema:
23
23
  """Base class for all generated models, providing validation, dict conversion, and field mapping."""
24
24
 
25
25
  @classmethod
26
- def _get_field_mappings(cls) -> Dict[str, str]:
26
+ def _get_field_mappings(cls) -> dict[str, str]:
27
27
  """Get field mappings from Meta class if defined. Returns API field -> Python field mappings."""
28
28
  if hasattr(cls, "Meta") and hasattr(cls.Meta, "key_transform_with_load"):
29
29
  return cls.Meta.key_transform_with_load # type: ignore[no-any-return]
30
30
  return {}
31
31
 
32
32
  @classmethod
33
- def _get_reverse_field_mappings(cls) -> Dict[str, str]:
33
+ def _get_reverse_field_mappings(cls) -> dict[str, str]:
34
34
  """Get reverse field mappings. Returns Python field -> API field mappings."""
35
35
  mappings = cls._get_field_mappings()
36
36
  return {python_field: api_field for api_field, python_field in mappings.items()}
37
37
 
38
38
  @classmethod
39
- def from_dict(cls: Type[T], data: Dict[str, Any]) -> T:
39
+ def from_dict(cls: Type[T], data: dict[str, Any]) -> T:
40
40
  """Create an instance from a dictionary with automatic field name mapping."""
41
41
  if not isinstance(data, dict):
42
42
  raise TypeError(f"Input must be a dictionary, got {type(data).__name__}")
43
43
 
44
44
  field_mappings = cls._get_field_mappings() # API -> Python
45
- kwargs: Dict[str, Any] = {}
45
+ kwargs: dict[str, Any] = {}
46
46
  cls_fields = {f.name: f for f in fields(cls)}
47
47
 
48
48
  # Process each field in the data
@@ -64,7 +64,7 @@ class BaseSchema:
64
64
  # Fall back to raw annotation if get_type_hints fails
65
65
  pass
66
66
 
67
- # Extract base type (handles Optional[Type] -> Type)
67
+ # Extract base type (handles Type | None -> Type)
68
68
  base_type = _extract_base_type(field_type)
69
69
 
70
70
  if base_type is not None and hasattr(base_type, "from_dict") and isinstance(value, dict):
@@ -90,7 +90,7 @@ class BaseSchema:
90
90
 
91
91
  return cls(**kwargs)
92
92
 
93
- def to_dict(self, exclude_none: bool = False) -> Dict[str, Any]:
93
+ def to_dict(self, exclude_none: bool = False) -> dict[str, Any]:
94
94
  """Convert the model instance to a dictionary with reverse field name mapping."""
95
95
  reverse_mappings = self._get_reverse_field_mappings() # Python -> API
96
96
  result = {}
@@ -116,10 +116,10 @@ class BaseSchema:
116
116
 
117
117
  # Legacy aliases for backward compatibility
118
118
  @classmethod
119
- def model_validate(cls: Type[T], data: Dict[str, Any]) -> T:
119
+ def model_validate(cls: Type[T], data: dict[str, Any]) -> T:
120
120
  """Legacy alias for from_dict."""
121
121
  return cls.from_dict(data)
122
122
 
123
- def model_dump(self, exclude_none: bool = False) -> Dict[str, Any]:
123
+ def model_dump(self, exclude_none: bool = False) -> dict[str, Any]:
124
124
  """Legacy alias for to_dict."""
125
125
  return self.to_dict(exclude_none=exclude_none)
@@ -1,17 +1,15 @@
1
1
  import json
2
- from typing import Any, AsyncIterator, List, Optional
2
+ from typing import Any, AsyncIterator, List
3
3
 
4
4
  import httpx
5
5
 
6
6
 
7
7
  class SSEEvent:
8
- def __init__(
9
- self, data: str, event: Optional[str] = None, id: Optional[str] = None, retry: Optional[int] = None
10
- ) -> None:
8
+ def __init__(self, data: str, event: str | None = None, id: str | None = None, retry: int | None = None) -> None:
11
9
  self.data: str = data
12
- self.event: Optional[str] = event
13
- self.id: Optional[str] = id
14
- self.retry: Optional[int] = retry
10
+ self.event: str | None = event
11
+ self.id: str | None = id
12
+ self.retry: int | None = retry
15
13
 
16
14
  def __repr__(self) -> str:
17
15
  return f"SSEEvent(data={self.data!r}, event={self.event!r}, id={self.id!r}, retry={self.retry!r})"
@@ -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
  """
@@ -312,7 +312,11 @@ class PythonConstructRenderer:
312
312
  has_content = True
313
313
  if body_lines:
314
314
  for line in body_lines:
315
- writer.write_line(line)
315
+ # Handle empty lines without adding indentation (Ruff W293)
316
+ if line == "":
317
+ writer.writer.newline() # Just add a newline, no indent
318
+ else:
319
+ writer.write_line(line)
316
320
  has_content = True
317
321
 
318
322
  if not has_content:
@@ -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"