diff options
| author | Gustav Olsson <gustav.olsson@hey.com> | 2022-05-18 00:39:24 +0200 |
|---|---|---|
| committer | Gustav Olsson <gustav.olsson@hey.com> | 2022-05-18 02:03:36 +0200 |
| commit | ea8a628c2e5b83ba4717aae7012230a23ed1566f (patch) | |
| tree | 1c831504f4be8c535b85ccb46d826f0a517699b6 /bindgen/gen_nim.py | |
| parent | 173da5b453a54721fa5a9b312b99de656e885c45 (diff) | |
update nim bindgen
* follow nim code style
* avoid keyword clashes
* remove redundant functions
* use nim bitfields for some consts
* generate bindings for sokol_glue.h
Diffstat (limited to 'bindgen/gen_nim.py')
| -rw-r--r-- | bindgen/gen_nim.py | 420 |
1 files changed, 188 insertions, 232 deletions
diff --git a/bindgen/gen_nim.py b/bindgen/gen_nim.py index fb6ba450..df8758ee 100644 --- a/bindgen/gen_nim.py +++ b/bindgen/gen_nim.py @@ -2,8 +2,8 @@ # Read output of gen_json.py and generate Zig language bindings. # # Nim coding style: -# - types and constants are PascalCase -# - functions, parameters, and fields are camelCase +# - type identifiers are PascalCase, everything else is camelCase +# - reference: https://nim-lang.org/docs/nep1.html #------------------------------------------------------------------------------- import gen_ir import json, re, os, shutil @@ -11,6 +11,7 @@ import json, re, os, shutil module_names = { 'sg_': 'gfx', 'sapp_': 'app', + 'sapp_sg': 'glue', 'stm_': 'time', 'saudio_': 'audio', 'sgl_': 'gl', @@ -19,13 +20,14 @@ module_names = { } c_source_paths = { - 'sg_': 'sokol-nim/src/sokol/c/sokol_gfx.c', - 'sapp_': 'sokol-nim/src/sokol/c/sokol_app.c', - 'stm_': 'sokol-nim/src/sokol/c/sokol_time.c', - 'saudio_': 'sokol-nim/src/sokol/c/sokol_audio.c', - 'sgl_': 'sokol-nim/src/sokol/c/sokol_gl.c', - 'sdtx_': 'sokol-nim/src/sokol/c/sokol_debugtext.c', - 'sshape_': 'sokol-nim/src/sokol/c/sokol_shape.c', + 'sg_': 'sokol-nim/src/sokol/gen/sokol_gfx.c', + 'sapp_': 'sokol-nim/src/sokol/gen/sokol_app.c', + 'sapp_sg': 'sokol-nim/src/sokol/gen/sokol_glue.c', + 'stm_': 'sokol-nim/src/sokol/gen/sokol_time.c', + 'saudio_': 'sokol-nim/src/sokol/gen/sokol_audio.c', + 'sgl_': 'sokol-nim/src/sokol/gen/sokol_gl.c', + 'sdtx_': 'sokol-nim/src/sokol/gen/sokol_debugtext.c', + 'sshape_': 'sokol-nim/src/sokol/gen/sokol_shape.c', } func_name_ignores = [ @@ -39,15 +41,29 @@ func_name_overrides = { 'sgl_rad': 'sgl_as_radians', } +# consts that should be converted to Nim enum bitfields, values mimic C type declarations +const_bitfield_overrides = { + 'SAPP_MODIFIER_': 'sapp_event_modifier', +} + struct_field_type_overrides = { - 'sg_context_desc.color_format': 'int', - 'sg_context_desc.depth_format': 'int', + 'sapp_event.modifiers': 'sapp_event_modifiers', # note the extra 's' at the end + 'sapp_allocator.alloc': 'void * (*)(size_t, void *)', + 'sapp_allocator.free': 'void (*)(void *, void *)', + 'sg_allocator.alloc': 'void * (*)(size_t, void *)', + 'sg_allocator.free': 'void (*)(void *, void *)', + 'sgl_allocator_t.alloc': 'void * (*)(size_t, void *)', + 'sgl_allocator_t.free': 'void (*)(void *, void *)', + 'sdtx_allocator_t.alloc': 'void * (*)(size_t, void *)', + 'sdtx_allocator_t.free': 'void (*)(void *, void *)', + 'saudio_allocator.alloc': 'void * (*)(size_t, void *)', + 'saudio_allocator.free': 'void (*)(void *, void *)', } prim_types = { - 'int': 'int32', + 'int': 'cint', 'bool': 'bool', - 'char': 'char', + 'char': 'cchar', 'int8_t': 'int8', 'uint8_t': 'uint8', 'int16_t': 'int16', @@ -56,11 +72,11 @@ prim_types = { 'uint32_t': 'uint32', 'int64_t': 'int64', 'uint64_t': 'uint64', - 'float': 'float32', - 'double': 'float64', + 'float': 'cfloat', + 'double': 'cdouble', 'uintptr_t': 'uint', 'intptr_t': 'int', - 'size_t': 'int', + 'size_t': 'csize_t', } prim_defaults = { @@ -74,13 +90,49 @@ prim_defaults = { 'uint32_t': '0', 'int64_t': '0', 'uint64_t': '0', - 'float': '0.0', + 'float': '0.0f', 'double': '0.0', 'uintptr_t': '0', 'intptr_t': '0', 'size_t': '0' } +common_prim_types = """ +untyped typed void +bool byte char +int int8 int16 int32 int64 +uint uint8 uint16 uint32 uint64 +float float32 float64 +string +cchar cint csize_t +cfloat cdouble +cstring +pointer +""".split() + +keywords = """ +addr and as asm +bind block break +case cast concept const continue converter +defer discard distinct div do +elif else end enum except export +finally for from func +if import in include interface is isnot iterator +let +macro method mixin mod +nil not notin +object of or out +proc ptr +raise ref return +shl shr static +template try tuple type +using +var +when while +xor +yield +""".split() + common_prim_types + struct_types = [] enum_types = [] enum_items = {} @@ -96,8 +148,7 @@ def reset_globals(): enum_items = {} out_lines = '' -re_1d_array = re.compile("^(?:const )?\w*\s\*?\[\d*\]$") -re_2d_array = re.compile("^(?:const )?\w*\s\*?\[\d*\]\[\d*\]$") +re_array = re.compile("([a-z_\d\s\*]+)\s*\[(\d+)\]") def l(s): global out_lines @@ -107,27 +158,12 @@ def as_nim_prim_type(s): return prim_types[s] # prefix_bla_blub(_t) => (dep.)BlaBlub -def as_nim_struct_type(s, prefix): +def as_nim_type_name(s, prefix): parts = s.lower().split('_') - outp = '' if s.startswith(prefix) else f'{parts[0]}.' - for part in parts[1:]: - if (part != 't'): - outp += part.capitalize() - return outp - -# prefix_bla_blub(_t) => (dep.)BlaBlub -def as_nim_enum_type(s, prefix): - parts = s.lower().split('_') - outp = '' if s.startswith(prefix) else f'{parts[0]}.' - for part in parts[1:]: - if (part != 't'): - outp += part.capitalize() - return outp - -# prefix_bla_blub(_t) => (dep.)BlaBlub -def as_nim_const_type(s, prefix): - parts = s.lower().split('_') - outp = '' if s.startswith(prefix) else f'{parts[0]}.' + dep = parts[0] + '_' + outp = '' + if not s.startswith(prefix) and dep in module_names: + outp = module_names[dep] + '.' for part in parts[1:]: if (part != 't'): outp += part.capitalize() @@ -149,15 +185,28 @@ def check_func_name_override(func_name): else: return func_name +def is_power_of_two(val): + return val == 0 or val & (val - 1) == 0 + +def check_consts_bitfield_override(decl): + for override in const_bitfield_overrides: + if all(override in item['name'] for item in decl['items']): + if any(not is_power_of_two(int(item['value'])) for item in decl['items']): + print(f"warning: bitfield override '{override}' encountered non-power-of-two value") + return const_bitfield_overrides[override] + return None + def trim_prefix(s, prefix): outp = s; if outp.lower().startswith(prefix.lower()): outp = outp[len(prefix):] return outp -# PREFIX_BLA_BLUB to bla_blub -def as_snake_case(s, prefix = ""): - return trim_prefix(s, prefix).lower() +def wrap_keywords(s): + if s in keywords: + return f'`{s}`' + else: + return s # prefix_bla_blub => blaBlub def as_camel_case(s, prefix = ""): @@ -165,7 +214,7 @@ def as_camel_case(s, prefix = ""): outp = parts[0] for part in parts[1:]: outp += part.capitalize() - return outp + return wrap_keywords(outp) # prefix_bla_blub => BlaBlub def as_pascal_case(s, prefix): @@ -173,20 +222,20 @@ def as_pascal_case(s, prefix): outp = "" for part in parts: outp += part.capitalize() - return outp + return wrap_keywords(outp) -# PREFIX_ENUM_BLA => Bla, _PREFIX_ENUM_BLA => Bla +# PREFIX_ENUM_BLA_BLO => Bla, _PREFIX_ENUM_BLA_BLO => blaBlo def as_enum_item_name(s): outp = s if outp.startswith('_'): outp = outp[1:] parts = outp.lower().split('_')[2:] - outp = "" - for part in parts: + outp = parts[0] + for part in parts[1:]: outp += part.capitalize() if outp[0].isdigit(): - outp = 'N' + outp - return outp + outp = 'n' + outp + return wrap_keywords(outp) def enum_default_item(enum_name): return enum_items[enum_name][0] @@ -230,20 +279,17 @@ def is_const_struct_ptr(s): def is_func_ptr(s): return '(*)' in s -def is_1d_array_type(s): - return re_1d_array.match(s) - -def is_2d_array_type(s): - return re_2d_array.match(s) +def is_array_type(s): + return re_array.match(s) is not None def type_default_value(s): return prim_defaults[s] def extract_array_type(s): - return s[:s.index('[')].strip() + return s[:s.find('[')].strip() + s[s.find(']')+1:].strip() def extract_array_nums(s): - return s[s.index('['):].replace('[', ' ').replace(']', ' ').split() + return s[s.find('[')+1:s.find(']')].strip() def extract_ptr_type(s): tokens = s.split() @@ -252,239 +298,141 @@ def extract_ptr_type(s): else: return tokens[0] -def as_extern_c_arg_type(arg_type, prefix): - if arg_type == "void": - return "void" - elif is_prim_type(arg_type): - return as_nim_prim_type(arg_type) - elif is_struct_type(arg_type): - return as_nim_struct_type(arg_type, prefix) - elif is_enum_type(arg_type): - return as_nim_enum_type(arg_type, prefix) - elif is_void_ptr(arg_type): - return "pointer" - elif is_const_void_ptr(arg_type): - return "pointer" - elif is_string_ptr(arg_type): +def as_nim_type(ctype, prefix): + if ctype == "void": + return "" + elif is_prim_type(ctype): + return as_nim_prim_type(ctype) + elif is_struct_type(ctype): + return as_nim_type_name(ctype, prefix) + elif is_enum_type(ctype): + return as_nim_type_name(ctype, prefix) + elif is_string_ptr(ctype): return "cstring" - elif is_const_struct_ptr(arg_type): - return f"ptr {as_nim_struct_type(extract_ptr_type(arg_type), prefix)}" - elif is_prim_ptr(arg_type): - return f"[*c] {as_nim_prim_type(extract_ptr_type(arg_type))}" - elif is_const_prim_ptr(arg_type): - return f"ptr {as_nim_prim_type(extract_ptr_type(arg_type))}" - else: - return '??? (as_extern_c_arg_type)' - -def as_nim_arg_type(arg_prefix, arg_type, prefix): - # NOTE: if arg_prefix is None, the result is used as return value - pre = "" if arg_prefix is None else arg_prefix - if arg_type == "void": - if arg_prefix is None: - return "void" - else: - return "" - elif is_prim_type(arg_type): - return pre + as_nim_prim_type(arg_type) - elif is_struct_type(arg_type): - return pre + as_nim_struct_type(arg_type, prefix) - elif is_enum_type(arg_type): - return pre + as_nim_enum_type(arg_type, prefix) - elif is_void_ptr(arg_type): - return pre + "pointer" - elif is_const_void_ptr(arg_type): - return pre + "pointer" - elif is_string_ptr(arg_type): - return pre + "cstring" - elif is_const_struct_ptr(arg_type): - return pre + f"ptr {as_nim_struct_type(extract_ptr_type(arg_type), prefix)}" - elif is_prim_ptr(arg_type): - return pre + f"ptr {as_nim_prim_type(extract_ptr_type(arg_type))}" - elif is_const_prim_ptr(arg_type): - return pre + f"ptr {as_nim_prim_type(extract_ptr_type(arg_type))}" + elif is_void_ptr(ctype) or is_const_void_ptr(ctype): + return "pointer" + elif is_const_struct_ptr(ctype): + return f"ptr {as_nim_type(extract_ptr_type(ctype), prefix)}" + elif is_prim_ptr(ctype) or is_const_prim_ptr(ctype): + return f"ptr {as_nim_type(extract_ptr_type(ctype), prefix)}" + elif is_func_ptr(ctype): + args = funcptr_args(ctype, prefix) + res = funcptr_res(ctype, prefix) + if res != "": + res = ":" + res + return f"proc({args}){res} {{.cdecl.}}" + elif is_array_type(ctype): + array_ctype = extract_array_type(ctype) + array_nums = extract_array_nums(ctype) + return f"array[{array_nums}, {as_nim_type(array_ctype, prefix)}]" else: - return arg_prefix + "??? (as_nim_arg_type)" + l(f"// FIXME: {ctype};") -# get C-style arguments of a function pointer as string -def funcptr_args_c(field_type, prefix): +def funcptr_args(field_type, prefix): tokens = field_type[field_type.index('(*)')+4:-1].split(',') s = "" n = 0 for token in tokens: n += 1 - arg_type = token.strip() + arg_ctype = token.strip() if s != "": s += ", " - c_arg = f"a{n}:" + as_extern_c_arg_type(arg_type, prefix) - if (c_arg == "void"): - return "" - else: - s += c_arg + arg_nimtype = as_nim_type(arg_ctype, prefix) + if arg_nimtype == "": + return "" # fun(void) + s += f"a{n}:{arg_nimtype}" if s == "a1:void": s = "" return s -# get C-style result of a function pointer as string -def funcptr_res_c(field_type): - res_type = field_type[:field_type.index('(*)')].strip() - if res_type == 'void': - return '' - elif is_const_void_ptr(res_type): - return ':pointer' - else: - return '???' +def funcptr_res(field_type, prefix): + ctype = field_type[:field_type.index('(*)')].strip() + return as_nim_type(ctype, prefix) -def funcdecl_args_c(decl, prefix): - s = "" - for param_decl in decl['params']: - if s != "": - s += ", " - arg_type = param_decl['type'] - s += as_extern_c_arg_type(arg_type, prefix) - return s - -def funcdecl_args_nim(decl, prefix): +def funcdecl_args(decl, prefix): s = "" for param_decl in decl['params']: if s != "": s += ", " arg_name = param_decl['name'] arg_type = param_decl['type'] - s += f"{as_nim_arg_type(f'{arg_name}:', arg_type, prefix)}" + s += f"{arg_name}:{as_nim_type(arg_type, prefix)}" return s -def funcdecl_res_c(decl, prefix): - decl_type = decl['type'] - res_type = decl_type[:decl_type.index('(')].strip() - return as_extern_c_arg_type(res_type, prefix) - -def funcdecl_res_nim(decl, prefix): +def funcdecl_res(decl, prefix): decl_type = decl['type'] res_type = decl_type[:decl_type.index('(')].strip() - nim_res_type = as_nim_arg_type(None, res_type, prefix) + nim_res_type = as_nim_type(res_type, prefix) if nim_res_type == "": nim_res_type = "void" return nim_res_type def gen_struct(decl, prefix, use_raw_name=False): struct_name = decl['name'] - nim_type = struct_name if use_raw_name else as_nim_struct_type(struct_name, prefix) + nim_type = struct_name if use_raw_name else as_nim_type_name(struct_name, prefix) l(f"type {nim_type}* = object") - isPublic = True + is_public = True for field in decl['fields']: field_name = field['name'] if field_name == "__pad": # FIXME: these should be guarded by SOKOL_ZIG_BINDINGS, but aren't? continue - isPublic = not field_name.startswith("_") + is_public = not field_name.startswith("_") field_name = as_camel_case(field_name, "_") - if field_name == "ptr": - field_name = "source" - if field_name == "ref": - field_name = "`ref`" - if field_name == "type": - field_name = "`type`" - if isPublic: + if is_public: field_name += "*" - field_type = field['type'] - field_type = check_struct_field_type_override(struct_name, field_name, field_type) - if is_prim_type(field_type): - l(f" {field_name}:{as_nim_prim_type(field_type)}") - elif is_struct_type(field_type): - l(f" {field_name}:{as_nim_struct_type(field_type, prefix)}") - elif is_enum_type(field_type): - l(f" {field_name}:{as_nim_enum_type(field_type, prefix)}") - elif is_string_ptr(field_type): - l(f" {field_name}:cstring") - elif is_const_void_ptr(field_type): - l(f" {field_name}:pointer") - elif is_void_ptr(field_type): - l(f" {field_name}:pointer") - elif is_const_prim_ptr(field_type): - l(f" {field_name}:ptr {as_nim_prim_type(extract_ptr_type(field_type))}") - elif is_func_ptr(field_type): - l(f" {field_name}:proc({funcptr_args_c(field_type, prefix)}){funcptr_res_c(field_type)} {{.cdecl.}}") - elif is_1d_array_type(field_type): - array_type = extract_array_type(field_type) - array_nums = extract_array_nums(field_type) - if is_prim_type(array_type) or is_struct_type(array_type): - if is_prim_type(array_type): - nim_type = as_nim_prim_type(array_type) - elif is_struct_type(array_type): - nim_type = as_nim_struct_type(array_type, prefix) - elif is_enum_type(array_type): - nim_type = as_nim_enum_type(array_type, prefix) - else: - nim_type = '??? (array type)' - t0 = f"array[{array_nums[0]}, {nim_type}]" - t0_slice = f"[]const {nim_type}" - t1 = f"[_]{nim_type}" - l(f" {field_name}:{t0}") - elif is_const_void_ptr(array_type): - l(f" {field_name}:array[{array_nums[0]}, pointer]") - else: - l(f"// FIXME: ??? array {field_name}:{field_type} => {array_type} [{array_nums[0]}]") - elif is_2d_array_type(field_type): - array_type = extract_array_type(field_type) - array_nums = extract_array_nums(field_type) - if is_prim_type(array_type): - nim_type = as_nim_prim_type(array_type) - def_val = type_default_value(array_type) - elif is_struct_type(array_type): - nim_type = as_nim_struct_type(array_type, prefix) - def_val = ".{ }" - else: - nim_type = "???" - def_val = "???" - t0 = f"array[{array_nums[0]}, array[{array_nums[1]}, {nim_type}]]" - l(f" {field_name}:{t0}") - else: - l(f"// FIXME: {field_name}:{field_type};") + field_type = check_struct_field_type_override(decl['name'], field['name'], field['type']) + l(f" {field_name}:{as_nim_type(field_type, prefix)}") l("") def gen_consts(decl, prefix): l("const") for item in decl['items']: - l(f" {trim_prefix(item['name'], prefix)}* = {item['value']}") + l(f" {as_camel_case(trim_prefix(item['name'], prefix), prefix)}* = {item['value']}") l("") -def gen_enum(decl, prefix): +def gen_enum(decl, prefix, bitfield=None): item_names_by_value = {} value = -1 - hasForceU32 = False - hasExplicitValues = False + has_force_u32 = False + has_explicit_values = False for item in decl['items']: itemName = item['name'] if itemName.endswith("_FORCE_U32"): - hasForceU32 = True + has_force_u32 = True elif itemName.endswith("_NUM"): continue else: if 'value' in item: - hasExplicitValues = True + has_explicit_values = True value = int(item['value']) else: value += 1 item_names_by_value[value] = as_enum_item_name(item['name']); - if hasForceU32: - l(f"type {as_nim_enum_type(decl['name'], prefix)}* {{.pure, size:4.}} = enum") + enum_name = bitfield if bitfield is not None else decl['name'] + enum_name_nim = as_nim_type_name(enum_name, prefix) + l('type') + if has_force_u32: + l(f" {enum_name_nim}* {{.pure, size:sizeof(cint).}} = enum") else: - l(f"type {as_nim_enum_type(decl['name'], prefix)}* {{.pure.}} = enum") - if hasExplicitValues: + l(f" {enum_name_nim}* {{.pure.}} = enum") + if has_explicit_values: # Nim requires explicit enum values to be declared in ascending order for value in sorted(item_names_by_value): name = item_names_by_value[value] - l(f" {name} = {value},") + l(f" {name} = {value},") else: for name in item_names_by_value.values(): - l(f" {name},") + l(f" {name},") + if bitfield is not None: + l(f" {enum_name_nim}s = set[{enum_name_nim}]") l("") def gen_func_nim(decl, prefix): c_func_name = decl['name'] nim_func_name = as_camel_case(decl['name'], prefix) - nim_res_type = funcdecl_res_nim(decl, prefix) - l(f"proc {nim_func_name}*({funcdecl_args_nim(decl, prefix)}):{funcdecl_res_nim(decl, prefix)} {{.cdecl, importc:\"{decl['name']}\".}}") + nim_res_type = funcdecl_res(decl, prefix) + l(f"proc {nim_func_name}*({funcdecl_args(decl, prefix)}):{funcdecl_res(decl, prefix)} {{.cdecl, importc:\"{decl['name']}\".}}") l("") def pre_parse(inp): @@ -500,6 +448,9 @@ def pre_parse(inp): enum_items[enum_name] = [] for item in decl['items']: enum_items[enum_name].append(as_enum_item_name(item['name'])) + for bitfield in const_bitfield_overrides.values(): + enum_types.append(bitfield) + enum_types.append(bitfield + 's') def gen_imports(inp, dep_prefixes): for dep_prefix in dep_prefixes: @@ -517,7 +468,11 @@ def gen_module(inp, dep_prefixes): if not decl['is_dep']: kind = decl['kind'] if kind == 'consts': - gen_consts(decl, prefix) + bitfield = check_consts_bitfield_override(decl) + if bitfield is not None: + gen_enum(decl, prefix, bitfield=bitfield) + else: + gen_consts(decl, prefix) elif kind == 'enum': gen_enum(decl, prefix) elif kind == 'struct': @@ -530,33 +485,34 @@ def prepare(): print('Generating nim bindings:') if not os.path.isdir('sokol-nim/src/sokol'): os.makedirs('sokol-nim/src/sokol') - if not os.path.isdir('sokol-nim/src/sokol/c'): - os.makedirs('sokol-nim/src/sokol/c') + if not os.path.isdir('sokol-nim/src/sokol/gen'): + os.makedirs('sokol-nim/src/sokol/gen') def gen(c_header_path, c_prefix, dep_c_prefixes): + if not c_prefix in module_names: + print(f'warning: skipping generation for {c_prefix} prefix...') + return global out_lines module_name = module_names[c_prefix] c_source_path = c_source_paths[c_prefix] print(f' {c_header_path} => {module_name}') reset_globals() - shutil.copyfile(c_header_path, f'sokol-nim/src/sokol/c/{os.path.basename(c_header_path)}') + shutil.copyfile(c_header_path, f'sokol-nim/src/sokol/gen/{os.path.basename(c_header_path)}') ir = gen_ir.gen(c_header_path, c_source_path, module_name, c_prefix, dep_c_prefixes) gen_module(ir, dep_c_prefixes) output_path = f"sokol-nim/src/sokol/{ir['module']}.nim" ## some changes for readability - out_lines = out_lines.replace("PixelformatInfo", "PixelFormatInfo") - out_lines = out_lines.replace(" Dontcare,", " DontCare,") - out_lines = out_lines.replace(" Vertexbuffer,", " VertexBuffer,") - out_lines = out_lines.replace(" Indexbuffer,", " IndexBuffer,") - out_lines = out_lines.replace(" N2d,", " Plane,") - out_lines = out_lines.replace(" N3d,", " Volume,") - out_lines = out_lines.replace(" Vs,", " Vertex,") - out_lines = out_lines.replace(" Fs,", " Fragment,") + out_lines = out_lines.replace("pixelformatInfo", "pixelFormatInfo") + out_lines = out_lines.replace(" dontcare,", " dontCare,") + out_lines = out_lines.replace(" vertexbuffer,", " vertexBuffer,") + out_lines = out_lines.replace(" indexbuffer,", " indexBuffer,") + out_lines = out_lines.replace(" n2d,", " plane,") + out_lines = out_lines.replace(" n3d,", " volume,") ## include extensions in generated code l("# Nim-specific API extensions") - l(f"include nim/{ir['module']}") + l(f"include extra/{ir['module']}") with open(output_path, 'w', newline='\n') as f_outp: f_outp.write(out_lines) |