fork of hey-api/openapi-ts because I need some additional things
1import type { RenderContext, Renderer } from '@hey-api/codegen-core';
2import type { BaseOutput } from '@hey-api/shared';
3import type { MaybeArray, MaybeFunc } from '@hey-api/types';
4
5import { py } from '../../py-compiler';
6import type { PyDsl } from '../../py-dsl';
7import type { ModuleExport, ModuleImport, SortGroup, SortKey, SortModule } from './render-utils';
8import { astToString, moduleSortKey } from './render-utils';
9
10type Exports = ReadonlyArray<ReadonlyArray<ModuleExport>>;
11type ExportsOptions = {
12 preferExportAll?: boolean;
13};
14type Header = MaybeArray<string> | null | undefined;
15type HeaderArg = MaybeFunc<(ctx: RenderContext<PyDsl>) => Header>;
16type Imports = Array<ReadonlyArray<ModuleImport>>;
17
18function headerToLines(header: Header): ReadonlyArray<string> {
19 if (!header) return [];
20 const lines: Array<string> = [];
21 if (typeof header === 'string') {
22 lines.push(...header.split(/\r?\n/));
23 return lines;
24 }
25 for (const line of header) {
26 lines.push(...line.split(/\r?\n/));
27 }
28 return lines;
29}
30
31export class PythonRenderer implements Renderer {
32 /**
33 * Function to generate a file header.
34 *
35 * @private
36 */
37 private _header?: HeaderArg;
38 /**
39 * Options for module specifier resolution.
40 *
41 * @private
42 */
43 private _module?: Partial<BaseOutput>['module'];
44 /**
45 * Whether `export * from 'module'` should be used when possible instead of named exports.
46 *
47 * @private
48 */
49 private _preferExportAll: boolean;
50
51 constructor(
52 args: Pick<Partial<BaseOutput>, 'module'> & {
53 header?: HeaderArg;
54 preferExportAll?: boolean;
55 } = {},
56 ) {
57 this._header = args.header;
58 this._module = args.module;
59 this._preferExportAll = args.preferExportAll ?? false;
60 }
61
62 render(ctx: RenderContext<PyDsl>): string {
63 const header = typeof this._header === 'function' ? this._header(ctx) : this._header;
64 return PythonRenderer.astToString({
65 exports: this.getExports(ctx),
66 exportsOptions: {
67 preferExportAll: this._preferExportAll,
68 },
69 header,
70 imports: this.getImports(ctx),
71 nodes: ctx.file.nodes,
72 });
73 }
74
75 supports(ctx: RenderContext): boolean {
76 return ctx.file.language === 'python';
77 }
78
79 static astToString(args: {
80 exports?: Exports;
81 exportsOptions?: ExportsOptions;
82 header?: Header;
83 imports?: Imports;
84 nodes?: ReadonlyArray<PyDsl>;
85 /**
86 * Whether to include a trailing newline at the end of the file.
87 *
88 * @default true
89 */
90 trailingNewline?: boolean;
91 }): string {
92 let text = '';
93 for (const header of headerToLines(args.header)) {
94 text += `${header}\n`;
95 }
96
97 const argsImports = args.imports ?? [];
98
99 for (const group of args.exports ?? []) {
100 for (const exp of group) {
101 let found = false;
102 for (const impGroup of argsImports) {
103 if (found) break;
104 for (const imp of impGroup) {
105 if (imp.modulePath === exp.modulePath) {
106 // TODO: merge imports and exports from the same module
107 found = true;
108 break;
109 }
110 }
111 }
112 if (!found) {
113 argsImports.push([
114 {
115 imports: exp.exports.map((exp) => ({
116 isTypeOnly: exp.isTypeOnly,
117 localName: exp.exportedName,
118 sourceName: exp.exportedName,
119 })),
120 isTypeOnly: false,
121 kind: 'named',
122 modulePath: exp.modulePath,
123 },
124 ]);
125 }
126 }
127 }
128
129 let imports = '';
130 for (const group of argsImports) {
131 if (imports) imports += '\n';
132 for (const imp of group) {
133 imports += `${astToString(PythonRenderer.toImportAst(imp))}`;
134 }
135 }
136 text = `${text}${text && imports ? '\n' : ''}${imports}`;
137
138 let exports = '';
139 for (const group of args.exports ?? []) {
140 if (exports) exports += '\n';
141 for (const exp of group) {
142 exports += `${astToString(PythonRenderer.toExportAst(exp, args.exportsOptions))}`;
143 }
144 }
145 text = `${text}${text && exports ? '\n' : ''}${exports}`;
146
147 let nodes = '';
148 for (const node of args.nodes ?? []) {
149 if (nodes) nodes += '\n\n';
150 nodes += `${astToString(node.toAst())}`;
151 }
152 text = `${text}${text && nodes ? '\n\n' : ''}${nodes}`;
153
154 if (args.trailingNewline === false && text.endsWith('\n')) {
155 text = text.slice(0, -1);
156 }
157
158 return text;
159 }
160
161 // eslint-disable-next-line @typescript-eslint/no-unused-vars
162 static toExportAst(group: ModuleExport, options?: ExportsOptions): py.Assignment {
163 const specifiers = group.exports.map((exp) => py.factory.createLiteral(exp.exportedName));
164 return py.factory.createAssignment(
165 py.factory.createIdentifier('__all__'),
166 undefined,
167 py.factory.createListExpression(specifiers),
168 );
169 }
170
171 static toImportAst(group: ModuleImport): py.ImportStatement {
172 const names: Array<{
173 alias?: string;
174 name: string;
175 }> = group.imports.map((imp) => ({
176 alias: imp.localName !== imp.sourceName ? imp.localName : undefined,
177 name: imp.sourceName,
178 }));
179 return py.factory.createImportStatement(group.modulePath, names, Boolean(group.imports.length));
180 }
181
182 private getExports(ctx: RenderContext): Exports {
183 type ModuleEntry = {
184 group: ModuleExport;
185 sortKey: SortKey;
186 };
187
188 const groups = new Map<SortGroup, Map<SortModule, ModuleEntry>>();
189
190 for (const exp of ctx.file.exports) {
191 const sortKey = moduleSortKey({
192 file: ctx.file,
193 fromFile: exp.from,
194 preferFileExtension: this._module?.extension || '',
195 root: ctx.project.root,
196 });
197 const modulePath = this._module?.resolve?.(sortKey[2], ctx) ?? sortKey[2];
198 const [groupIndex] = sortKey;
199
200 if (!groups.has(groupIndex)) groups.set(groupIndex, new Map());
201 const moduleMap = groups.get(groupIndex)!;
202
203 if (!moduleMap.has(modulePath)) {
204 moduleMap.set(modulePath, {
205 group: {
206 canExportAll: exp.canExportAll,
207 exports: exp.exports,
208 isTypeOnly: exp.isTypeOnly,
209 modulePath,
210 namespaceExport: exp.namespaceExport,
211 },
212 sortKey,
213 });
214 }
215 }
216
217 const exports: Array<Array<ModuleExport>> = Array.from(groups.entries())
218 .sort((a, b) => a[0] - b[0])
219 .map(([, moduleMap]) => {
220 const entries = Array.from(moduleMap.values());
221
222 entries.sort((a, b) => {
223 const d = a.sortKey[1] - b.sortKey[1];
224 return d !== 0 ? d : a.group.modulePath.localeCompare(b.group.modulePath);
225 });
226
227 return entries.map((e) => {
228 const group = e.group;
229 if (group.namespaceExport) {
230 group.exports = [];
231 } else {
232 const isTypeOnly = !group.exports.find((exp) => !exp.isTypeOnly);
233 if (isTypeOnly) {
234 group.isTypeOnly = true;
235 for (const exp of group.exports) {
236 exp.isTypeOnly = false;
237 }
238 }
239 group.exports.sort((a, b) => a.exportedName.localeCompare(b.exportedName));
240 }
241 return group;
242 });
243 });
244
245 return exports;
246 }
247
248 private getImports(ctx: RenderContext): Imports {
249 type ModuleEntry = {
250 group: ModuleImport;
251 sortKey: SortKey;
252 };
253
254 const groups = new Map<SortGroup, Map<SortModule, ModuleEntry>>();
255
256 for (const imp of ctx.file.imports) {
257 const sortKey = moduleSortKey({
258 file: ctx.file,
259 fromFile: imp.from,
260 preferFileExtension: this._module?.extension || '',
261 root: ctx.project.root,
262 });
263 const modulePath = this._module?.resolve?.(sortKey[2], ctx) ?? sortKey[2];
264 const [groupIndex] = sortKey;
265
266 if (!groups.has(groupIndex)) groups.set(groupIndex, new Map());
267 const moduleMap = groups.get(groupIndex)!;
268
269 if (!moduleMap.has(modulePath)) {
270 moduleMap.set(modulePath, {
271 group: {
272 imports: [],
273 isTypeOnly: false,
274 kind: imp.kind,
275 modulePath,
276 },
277 sortKey,
278 });
279 }
280
281 const entry = moduleMap.get(modulePath)!;
282 const group = entry.group;
283
284 if (imp.kind !== 'named') {
285 group.isTypeOnly = imp.isTypeOnly;
286 group.kind = imp.kind;
287 group.localName = imp.localName;
288 } else {
289 group.imports.push(...imp.imports);
290 }
291 }
292
293 const imports: Array<Array<ModuleImport>> = Array.from(groups.entries())
294 .sort((a, b) => a[0] - b[0])
295 .map(([, moduleMap]) => {
296 const entries = Array.from(moduleMap.values());
297
298 entries.sort((a, b) => {
299 const d = a.sortKey[1] - b.sortKey[1];
300 return d !== 0 ? d : a.group.modulePath.localeCompare(b.group.modulePath);
301 });
302
303 return entries.map((e) => {
304 const group = e.group;
305 if (group.kind === 'namespace') {
306 group.imports = [];
307 } else {
308 const isTypeOnly = !group.imports.find((imp) => !imp.isTypeOnly);
309 if (isTypeOnly) {
310 group.isTypeOnly = true;
311 for (const imp of group.imports) {
312 imp.isTypeOnly = false;
313 }
314 }
315 group.imports.sort((a, b) => a.localName.localeCompare(b.localName));
316 }
317 return group;
318 });
319 });
320
321 return imports;
322 }
323}