From c87f3a9179c2e85b80932b310abefd271446e643 Mon Sep 17 00:00:00 2001 From: Karstein Kvistad Date: Sun, 26 Apr 2026 17:23:26 +0200 Subject: [PATCH] fix(create_folder): WORKING -- create_folder returns void, walk children to detect success The v3 fix from 32e6120 still fell through every strategy because the None-as-failure check was wrong. CODESYS scripting's create_folder methods (both ScriptObject.create_folder and ScriptProject.create_folder) RETURN VOID -- the folder is created via side effect; the return value is Python None. v1/v2/v3 all treated None as "this strategy didn't work" and tried the next, eventually giving up. v4 fix: after each create call, walk parent_object.get_children(False) looking for a child whose get_name() == FOLDER_NAME. First strategy that produces such a child wins; the rest are skipped (avoids duplicates). The strategy order also got reshuffled: parent.create_folder positional is now strategy 1 (per the SP22 ScriptObject stub signature), with project-level fallbacks behind it. Verified end-to-end on MCPTest2 + SP22 P1: > create_folder(folderName='Test_Bench_Folder', parentPath='PLCWinNT/Plc Logic/Application') Folder 'Test_Bench_Folder' created [...]. Project saved. > delete_object(objectPath='.../Test_Bench_Folder') Object [...] deleted [...]. Project saved. Per-version trace of the iteration: v1 (2607063): name= kwarg -> "create_folder() got an unexpected kwarg 'name'" -- SP22 stub uses foldername. v2 (e07f281): positional / foldername= -> silent None return on Application; fell through. v3 (32e6120): added primary_project.create_folder(name, SV_POU) first; also dropped ScriptManager cache for hot-reload. Still treated None as failure. v4 (this): walk children after each call; succeeds. Memory note for the kit: the SP22 scripting API has a class of methods that mutate via side effect and return void. When porting fork scripts, ALWAYS verify by walking children, never by checking the return value of create_*. Same pattern probably applies to create_pou / create_dut / create_gvl too -- worth a separate audit. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/scripts/create_folder.py | 113 +++++++++++++++++++---------------- 1 file changed, 62 insertions(+), 51 deletions(-) diff --git a/src/scripts/create_folder.py b/src/scripts/create_folder.py index 41ccefc..3e71cac 100644 --- a/src/scripts/create_folder.py +++ b/src/scripts/create_folder.py @@ -49,16 +49,20 @@ try: # Create the folder. Per the SP22 stubs: # ScriptObject.create_folder(foldername) -- on POUs / sub-objects # ScriptProject.create_folder(foldername, structured_view=None) - # -- on the project itself, - # accepts an explicit view GUID - # On SP22 specifically, calling .create_folder('X') on an Application object - # returns None silently (no exception, no folder created). The reliable - # pathway is to call create_folder on the PROJECT with an explicit - # structured_view GUID -- the SV_POU view is where Application's children - # live, so a folder there will appear under Application in the IDE tree. + # -- on the project itself # - # SV_POU GUID = {21AF5390-2942-461a-BF89-951AAF6999F1}. (Documented in the - # ScriptProject.pyi stub; constant since SP3.5.2.0.) + # CRITICAL gotcha verified by experiment on SP22 (and confirmed by + # the helpme-codesys.com signature): create_folder returns **void** + # (Python None), NOT the new folder object. The folder IS created + # via side effect; you have to walk the parent's children to find it. + # Earlier fork versions treated None as failure -- that was the + # whole "v1/v2/v3 fell through every strategy silently" bug. + # + # Strategy: try each call in order, then immediately walk + # parent_object.get_children(False) for a child named FOLDER_NAME. + # First strategy that produces such a child wins; the rest are + # never tried (avoids creating duplicates). + SV_POU_GUID_STR = '21AF5390-2942-461a-BF89-951AAF6999F1' sv_pou_guid = None try: @@ -67,80 +71,87 @@ try: except Exception as guid_e: print("WARN: Could not construct System.Guid for SV_POU: %s" % guid_e) + def _find_folder_under_parent(): + """Walk parent_object's direct children looking for FOLDER_NAME. + Returns the matching child object or None. Used after each + strategy to detect side-effect-only success.""" + try: + for child in parent_object.get_children(False): + try: + if getattr(child, 'get_name', lambda: None)() == FOLDER_NAME: + return child + except Exception: + pass + except Exception: + pass + return None + new_folder = None + strategies_tried = [] - # Strategy 1: project-level create_folder with explicit POU view. This is - # the only call shape that reliably works on SP22 Application children. - if hasattr(primary_project, 'create_folder') and sv_pou_guid is not None: - try: - print("DEBUG: Trying primary_project.create_folder('%s', SV_POU)" % FOLDER_NAME) - new_folder = primary_project.create_folder(FOLDER_NAME, sv_pou_guid) - if new_folder is not None: - print("DEBUG: project.create_folder(SV_POU) succeeded.") - except Exception as e: - print("WARN: primary_project.create_folder('%s', SV_POU) raised: %s" % (FOLDER_NAME, e)) - new_folder = None - - # Strategy 2: parent.create_folder (positional). Works pre-SP21 and on - # parents whose factories haven't been pinned to project-level. + # Strategy 1: parent.create_folder(name) positional. Per the docs + # this creates the folder in the parent's structured view. if new_folder is None and hasattr(parent_object, 'create_folder'): + strategies_tried.append('parent.create_folder(name)') try: - print("DEBUG: Trying parent.create_folder('%s') [positional]" % FOLDER_NAME) - new_folder = parent_object.create_folder(FOLDER_NAME) + print("DEBUG: Trying parent.create_folder('%s')" % FOLDER_NAME) + ret = parent_object.create_folder(FOLDER_NAME) + new_folder = ret if ret is not None else _find_folder_under_parent() if new_folder is not None: - print("DEBUG: parent.create_folder() positional succeeded.") + print("DEBUG: parent.create_folder() succeeded (folder found in children).") except Exception as e: print("WARN: parent.create_folder('%s') raised: %s" % (FOLDER_NAME, e)) - new_folder = None - if new_folder is None: - try: - new_folder = parent_object.create_folder(foldername=FOLDER_NAME) - if new_folder is not None: - print("DEBUG: parent.create_folder(foldername=) succeeded.") - except Exception as e2: - print("WARN: parent.create_folder(foldername='%s') raised: %s" % (FOLDER_NAME, e2)) - new_folder = None - # Strategy 3: project-level create_folder default view (POU). + # Strategy 2: project.create_folder(name, SV_POU_GUID). The folder + # lands in the POU view, which is where Application's children live. + if new_folder is None and hasattr(primary_project, 'create_folder') and sv_pou_guid is not None: + strategies_tried.append('project.create_folder(name, SV_POU)') + try: + print("DEBUG: Trying primary_project.create_folder('%s', SV_POU)" % FOLDER_NAME) + ret = primary_project.create_folder(FOLDER_NAME, sv_pou_guid) + new_folder = ret if ret is not None else _find_folder_under_parent() + if new_folder is not None: + print("DEBUG: project.create_folder(SV_POU) succeeded (folder found in children).") + except Exception as e: + print("WARN: primary_project.create_folder('%s', SV_POU) raised: %s" % (FOLDER_NAME, e)) + + # Strategy 3: project.create_folder(name) default view. if new_folder is None and hasattr(primary_project, 'create_folder'): + strategies_tried.append('project.create_folder(name)') try: print("DEBUG: Trying primary_project.create_folder('%s') [default view]" % FOLDER_NAME) - new_folder = primary_project.create_folder(FOLDER_NAME) + ret = primary_project.create_folder(FOLDER_NAME) + new_folder = ret if ret is not None else _find_folder_under_parent() if new_folder is not None: print("DEBUG: project.create_folder() default-view succeeded.") except Exception as e: print("WARN: primary_project.create_folder('%s') raised: %s" % (FOLDER_NAME, e)) - new_folder = None - # Strategy 4: generic create_object with the folder type UUID. Last-ditch - # for non-standard parent types. + # Strategy 4: generic create_object with the folder type UUID. if new_folder is None and hasattr(parent_object, 'create_object'): FOLDER_TYPE_UUID = '85d1215e-6520-4983-9a55-2d39d1f24cb4' + strategies_tried.append('parent.create_object(typeUuid)') try: print("DEBUG: Trying parent.create_object(typeUuid=%s, name='%s')" % (FOLDER_TYPE_UUID, FOLDER_NAME)) - new_folder = parent_object.create_object(typeUuid=FOLDER_TYPE_UUID, name=FOLDER_NAME) + ret = parent_object.create_object(typeUuid=FOLDER_TYPE_UUID, name=FOLDER_NAME) + new_folder = ret if ret is not None else _find_folder_under_parent() except Exception as e: print("WARN: parent.create_object(typeUuid=%s) raised: %s" % (FOLDER_TYPE_UUID, e)) - new_folder = None # Strategy 5: types.IecFolder + parent.add (very old API). if new_folder is None and hasattr(script_engine, 'types') and hasattr(script_engine.types, 'IecFolder') and hasattr(parent_object, 'add'): + strategies_tried.append('parent.add(IecFolder)') try: print("DEBUG: Trying parent.add(script_engine.types.IecFolder, name='%s')" % FOLDER_NAME) - new_folder = parent_object.add(script_engine.types.IecFolder, name=FOLDER_NAME) + ret = parent_object.add(script_engine.types.IecFolder, name=FOLDER_NAME) + new_folder = ret if ret is not None else _find_folder_under_parent() except Exception as e: print("WARN: parent.add(types.IecFolder) raised: %s" % e) - new_folder = None if new_folder is None: raise TypeError( - "Parent object '%s' of type %s -- folder creation failed for all known strategies: " - "(1) primary_project.create_folder(name, SV_POU), " - "(2) parent.create_folder(name), " - "(3) primary_project.create_folder(name) default view, " - "(4) parent.create_object(typeUuid='85d1215e-...'), " - "(5) parent.add(script_engine.types.IecFolder)." % ( - parent_name, type(parent_object).__name__)) + "Parent object '%s' of type %s -- folder '%s' could not be created or located after trying %s." % ( + parent_name, type(parent_object).__name__, FOLDER_NAME, ', '.join(strategies_tried) or '')) if new_folder: new_folder_name = getattr(new_folder, 'get_name', lambda: FOLDER_NAME)()