0
0
Fork 0

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) <noreply@anthropic.com>
This commit is contained in:
Karstein Kvistad 2026-04-26 17:23:26 +02:00
parent 32e612000d
commit c87f3a9179

View file

@ -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 '<no strategies available>'))
if new_folder:
new_folder_name = getattr(new_folder, 'get_name', lambda: FOLDER_NAME)()