Major refactor on post stack
This commit is contained in:
@@ -27,21 +27,17 @@ Processor entries in PathJob """
|
||||
import FreeCAD
|
||||
import FreeCADGui
|
||||
import Path
|
||||
import Path.Base.Util as PathUtil
|
||||
import Path.Main.Job as PathJob
|
||||
from PathScripts import PathUtils
|
||||
from Path.Post.Utils import FilenameGenerator
|
||||
import os
|
||||
import re
|
||||
|
||||
from Path.Post.Processor import PostProcessor
|
||||
from Path.Post.Processor import PostProcessor, PostProcessorFactory
|
||||
from PySide import QtCore, QtGui
|
||||
from datetime import datetime
|
||||
from PySide.QtCore import QT_TRANSLATE_NOOP
|
||||
|
||||
LOG_MODULE = Path.Log.thisModule()
|
||||
|
||||
debugmodule = False
|
||||
if debugmodule:
|
||||
DEBUG = False
|
||||
if DEBUG:
|
||||
Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule())
|
||||
Path.Log.trackModule(Path.Log.thisModule())
|
||||
else:
|
||||
@@ -51,372 +47,29 @@ else:
|
||||
translate = FreeCAD.Qt.translate
|
||||
|
||||
|
||||
class _TempObject:
|
||||
Path = None
|
||||
Name = "Fixture"
|
||||
InList = []
|
||||
Label = "Fixture"
|
||||
|
||||
|
||||
def processFileNameSubstitutions(
|
||||
job,
|
||||
subpartname,
|
||||
sequencenumber,
|
||||
outputpath,
|
||||
filename,
|
||||
ext,
|
||||
):
|
||||
"""Process any substitutions in the outputpath or filename."""
|
||||
|
||||
# The following section allows substitution within the path part
|
||||
Path.Log.track(f"path before substitution: {outputpath}")
|
||||
|
||||
if "%D" in outputpath: # Directory of active document
|
||||
D = FreeCAD.ActiveDocument.FileName
|
||||
if D:
|
||||
D = os.path.dirname(D)
|
||||
# in case the document is in the current working directory
|
||||
if not D:
|
||||
D = "."
|
||||
else:
|
||||
FreeCAD.Console.PrintError(
|
||||
"Please save document in order to resolve output path!\n"
|
||||
)
|
||||
return None
|
||||
outputpath = outputpath.replace("%D", D)
|
||||
|
||||
if "%M" in outputpath:
|
||||
M = FreeCAD.getUserMacroDir()
|
||||
outputpath = outputpath.replace("%M", M)
|
||||
|
||||
# Use the file label
|
||||
if "%d" in outputpath:
|
||||
d = FreeCAD.ActiveDocument.Label
|
||||
outputpath = outputpath.replace("%d", d)
|
||||
|
||||
# Use the name of the active job object
|
||||
if "%j" in outputpath:
|
||||
j = job.Label
|
||||
outputpath = outputpath.replace("%j", j)
|
||||
|
||||
Path.Log.track(f"path after substitution: {outputpath}")
|
||||
|
||||
# The following section allows substitution within the filename part
|
||||
Path.Log.track(f"filename before substitution: {filename}")
|
||||
|
||||
# Use the file label
|
||||
if "%d" in filename:
|
||||
d = FreeCAD.ActiveDocument.Label
|
||||
filename = filename.replace("%d", d)
|
||||
|
||||
# Use the name of the active job object
|
||||
if "%j" in filename:
|
||||
j = job.Label
|
||||
filename = filename.replace("%j", j)
|
||||
|
||||
# Use the sequence number if explicitly called
|
||||
if "%S" in filename:
|
||||
j = job.Label
|
||||
filename = filename.replace("%S", str(sequencenumber))
|
||||
|
||||
# This section handles unique names for splitting output
|
||||
if job.SplitOutput:
|
||||
Path.Log.track()
|
||||
if "%T" in filename and job.OrderOutputBy == "Tool":
|
||||
filename = filename.replace("%T", subpartname)
|
||||
|
||||
if "%t" in filename and job.OrderOutputBy == "Tool":
|
||||
filename = filename.replace("%t", subpartname)
|
||||
|
||||
if "%W" in filename and job.OrderOutputBy == "Fixture":
|
||||
filename = filename.replace("%W", subpartname)
|
||||
|
||||
if "%O" in filename and job.OrderOutputBy == "Operation":
|
||||
filename = filename.replace("%O", subpartname)
|
||||
|
||||
if (
|
||||
"%S" in filename
|
||||
): # We always add a sequence number but the user can say where
|
||||
filename = filename.replace("%S", str(sequencenumber))
|
||||
else:
|
||||
filename = f"{filename}-{sequencenumber}"
|
||||
|
||||
Path.Log.track(f"filename after substitution: {filename}")
|
||||
|
||||
if not ext:
|
||||
ext = ".nc"
|
||||
Path.Log.track(f"file extension: {ext}")
|
||||
|
||||
fullPath = f"{outputpath}{os.path.sep}{filename}{ext}"
|
||||
|
||||
Path.Log.track(f"full filepath: {fullPath}")
|
||||
return fullPath
|
||||
|
||||
|
||||
def resolveFileName(job, subpartname, sequencenumber):
|
||||
"""Generate the file name to use as output."""
|
||||
|
||||
Path.Log.track(subpartname, sequencenumber)
|
||||
|
||||
validPathSubstitutions = ["D", "d", "M", "j"]
|
||||
validFilenameSubstitutions = ["j", "d", "T", "t", "W", "O", "S"]
|
||||
|
||||
# Look for preference default
|
||||
outputpath, filename = os.path.split(Path.Preferences.defaultOutputFile())
|
||||
filename, ext = os.path.splitext(filename)
|
||||
|
||||
# Override with document default if it exists
|
||||
if job.PostProcessorOutputFile:
|
||||
candidateOutputPath, candidateFilename = os.path.split(
|
||||
job.PostProcessorOutputFile
|
||||
)
|
||||
|
||||
if candidateOutputPath:
|
||||
outputpath = candidateOutputPath
|
||||
|
||||
if candidateFilename:
|
||||
filename, ext = os.path.splitext(candidateFilename)
|
||||
|
||||
# Strip any invalid substitutions from the outputpath
|
||||
for match in re.findall("%(.)", outputpath):
|
||||
if match not in validPathSubstitutions:
|
||||
outputpath = outputpath.replace(f"%{match}", "")
|
||||
|
||||
# if nothing else, use current directory
|
||||
if not outputpath:
|
||||
outputpath = "."
|
||||
|
||||
# Strip any invalid substitutions from the filename
|
||||
for match in re.findall("%(.)", filename):
|
||||
if match not in validFilenameSubstitutions:
|
||||
filename = filename.replace(f"%{match}", "")
|
||||
|
||||
# if no filename, use the active document label
|
||||
if not filename:
|
||||
filename = FreeCAD.ActiveDocument.Label
|
||||
|
||||
# if no extension, use something sensible
|
||||
if not ext:
|
||||
ext = ".nc"
|
||||
|
||||
# By now we should have a sanitized path, filename and extension to work with
|
||||
Path.Log.track(f"path: {outputpath} name: {filename} ext: {ext}")
|
||||
|
||||
fullPath = processFileNameSubstitutions(
|
||||
job,
|
||||
subpartname,
|
||||
sequencenumber,
|
||||
outputpath,
|
||||
filename,
|
||||
ext,
|
||||
)
|
||||
|
||||
# This section determines whether user interaction is necessary
|
||||
policy = Path.Preferences.defaultOutputPolicy()
|
||||
|
||||
openDialog = policy == "Open File Dialog"
|
||||
# if os.path.isdir(filename) or not os.path.isdir(os.path.dirname(filename)):
|
||||
# # Either the entire filename resolves into a directory or the parent
|
||||
# # directory doesn't exist.
|
||||
# # Either way I don't know what to do - ask for help
|
||||
# openDialog = True
|
||||
|
||||
if not FreeCAD.GuiUp: # if testing, or scripting, never open dialog.
|
||||
policy = "Append Unique ID on conflict"
|
||||
openDialog = False
|
||||
|
||||
if os.path.isfile(fullPath) and not openDialog:
|
||||
if policy == "Open File Dialog on conflict":
|
||||
openDialog = True
|
||||
elif policy == "Append Unique ID on conflict":
|
||||
fn, ext = os.path.splitext(fullPath)
|
||||
nr = fn[-3:]
|
||||
n = 1
|
||||
if nr.isdigit():
|
||||
n = int(nr)
|
||||
while os.path.isfile(f"{fn}{n:03d}{ext}"):
|
||||
n = n + 1
|
||||
fullPath = f"{fn}{n:03d}{ext}"
|
||||
|
||||
if openDialog:
|
||||
requestedfile = QtGui.QFileDialog.getSaveFileName(
|
||||
QtGui.QApplication.activeWindow(), "Output File", fullPath
|
||||
)
|
||||
if requestedfile[0]:
|
||||
fullPath = requestedfile[0]
|
||||
else:
|
||||
fullPath = None
|
||||
|
||||
if fullPath:
|
||||
# remove any unused substitution strings:
|
||||
for s in validPathSubstitutions + validFilenameSubstitutions:
|
||||
fullPath = fullPath.replace(f"%{s}", "")
|
||||
fullPath = os.path.normpath(fullPath)
|
||||
Path.Log.track(fullPath)
|
||||
|
||||
return fullPath
|
||||
|
||||
|
||||
def fixtureSetup(order, fixture, job):
|
||||
"""Convert a Fixure setting to _TempObject instance with a G0 move to a
|
||||
safe height every time the fixture coordinate system change. Skip
|
||||
the move for first fixture, to avoid moving before tool and tool
|
||||
height compensation is enabled.
|
||||
|
||||
"""
|
||||
|
||||
fobj = _TempObject()
|
||||
c1 = Path.Command(fixture)
|
||||
fobj.Path = Path.Path([c1])
|
||||
# Avoid any tool move after G49 in preamble and before tool change
|
||||
# and G43 in case tool height compensation is in use, to avoid
|
||||
# dangerous move without tool compesation.
|
||||
if order != 0:
|
||||
c2 = Path.Command(
|
||||
"G0 Z"
|
||||
+ str(
|
||||
job.Stock.Shape.BoundBox.ZMax
|
||||
+ job.SetupSheet.ClearanceHeightOffset.Value
|
||||
)
|
||||
)
|
||||
fobj.Path.addCommands(c2)
|
||||
fobj.InList.append(job)
|
||||
return fobj
|
||||
|
||||
|
||||
def buildPostList(job):
|
||||
"""Takes the job and determines the specific objects and order to
|
||||
postprocess Returns a list of objects which can be passed to
|
||||
exportObjectsWith() for final posting."""
|
||||
wcslist = job.Fixtures
|
||||
orderby = job.OrderOutputBy
|
||||
|
||||
postlist = []
|
||||
|
||||
if orderby == "Fixture":
|
||||
Path.Log.debug("Ordering by Fixture")
|
||||
# Order by fixture means all operations and tool changes will be
|
||||
# completed in one fixture before moving to the next.
|
||||
|
||||
currTool = None
|
||||
for index, f in enumerate(wcslist):
|
||||
# create an object to serve as the fixture path
|
||||
sublist = [fixtureSetup(index, f, job)]
|
||||
|
||||
# Now generate the gcode
|
||||
for obj in job.Operations.Group:
|
||||
tc = PathUtil.toolControllerForOp(obj)
|
||||
if tc is not None and PathUtil.opProperty(obj, "Active"):
|
||||
if tc.ToolNumber != currTool:
|
||||
sublist.append(tc)
|
||||
Path.Log.debug(f"Appending TC: {tc.Name}")
|
||||
currTool = tc.ToolNumber
|
||||
sublist.append(obj)
|
||||
postlist.append((f, sublist))
|
||||
|
||||
elif orderby == "Tool":
|
||||
Path.Log.debug("Ordering by Tool")
|
||||
# Order by tool means tool changes are minimized.
|
||||
# all operations with the current tool are processed in the current
|
||||
# fixture before moving to the next fixture.
|
||||
|
||||
toolstring = "None"
|
||||
currTool = None
|
||||
|
||||
# Build the fixture list
|
||||
fixturelist = []
|
||||
for index, f in enumerate(wcslist):
|
||||
# create an object to serve as the fixture path
|
||||
fixturelist.append(fixtureSetup(index, f, job))
|
||||
|
||||
# Now generate the gcode
|
||||
curlist = [] # list of ops for tool, will repeat for each fixture
|
||||
sublist = [] # list of ops for output splitting
|
||||
|
||||
Path.Log.track(job.PostProcessorOutputFile)
|
||||
for idx, obj in enumerate(job.Operations.Group):
|
||||
Path.Log.track(obj.Label)
|
||||
|
||||
# check if the operation is active
|
||||
if not getattr(obj, "Active", True):
|
||||
Path.Log.track()
|
||||
continue
|
||||
|
||||
# Determine the proper string for the Op's TC
|
||||
tc = PathUtil.toolControllerForOp(obj)
|
||||
if tc is None:
|
||||
tcstring = "None"
|
||||
elif "%T" in job.PostProcessorOutputFile:
|
||||
tcstring = f"{tc.ToolNumber}"
|
||||
else:
|
||||
tcstring = re.sub(r"[^\w\d-]", "_", tc.Label)
|
||||
Path.Log.track(toolstring)
|
||||
|
||||
if tc is None or tc.ToolNumber == currTool:
|
||||
curlist.append(obj)
|
||||
elif tc.ToolNumber != currTool and currTool is None: # first TC
|
||||
sublist.append(tc)
|
||||
curlist.append(obj)
|
||||
currTool = tc.ToolNumber
|
||||
toolstring = tcstring
|
||||
|
||||
elif tc.ToolNumber != currTool and currTool is not None: # TC
|
||||
for fixture in fixturelist:
|
||||
sublist.append(fixture)
|
||||
sublist.extend(curlist)
|
||||
postlist.append((toolstring, sublist))
|
||||
sublist = [tc]
|
||||
curlist = [obj]
|
||||
currTool = tc.ToolNumber
|
||||
toolstring = tcstring
|
||||
|
||||
if idx == len(job.Operations.Group) - 1: # Last operation.
|
||||
for fixture in fixturelist:
|
||||
sublist.append(fixture)
|
||||
sublist.extend(curlist)
|
||||
|
||||
postlist.append((toolstring, sublist))
|
||||
|
||||
elif orderby == "Operation":
|
||||
Path.Log.debug("Ordering by Operation")
|
||||
# Order by operation means ops are done in each fixture in
|
||||
# sequence.
|
||||
currTool = None
|
||||
|
||||
# Now generate the gcode
|
||||
for obj in job.Operations.Group:
|
||||
|
||||
# check if the operation is active
|
||||
if not getattr(obj, "Active", True):
|
||||
continue
|
||||
|
||||
sublist = []
|
||||
Path.Log.debug(f"obj: {obj.Name}")
|
||||
|
||||
for index, f in enumerate(wcslist):
|
||||
sublist.append(fixtureSetup(index, f, job))
|
||||
tc = PathUtil.toolControllerForOp(obj)
|
||||
if tc is not None:
|
||||
if job.SplitOutput or (tc.ToolNumber != currTool):
|
||||
sublist.append(tc)
|
||||
currTool = tc.ToolNumber
|
||||
sublist.append(obj)
|
||||
postlist.append((obj.Label, sublist))
|
||||
|
||||
if job.SplitOutput:
|
||||
Path.Log.track()
|
||||
return postlist
|
||||
|
||||
Path.Log.track()
|
||||
finalpostlist = [
|
||||
("allitems", [item for slist in postlist for item in slist[1]])
|
||||
]
|
||||
return finalpostlist
|
||||
def _resolve_post_processor_name(job):
|
||||
Path.Log.debug("_resolve_post_processor_name()")
|
||||
if job.PostProcessor:
|
||||
valid_name = job.PostProcessor
|
||||
elif Path.Preferences.defaultPostProcessor():
|
||||
valid_name = Path.Preferences.defaultPostProcessor()
|
||||
elif FreeCAD.GuiUp:
|
||||
valid_name = (
|
||||
DlgSelectPostProcessor().exec_()
|
||||
) # Ensure DlgSelectPostProcessor is defined
|
||||
else:
|
||||
valid_name = None
|
||||
|
||||
if valid_name and PostProcessor.exists(valid_name):
|
||||
return valid_name
|
||||
else:
|
||||
raise ValueError(f"Post processor not identified.")
|
||||
|
||||
|
||||
class DlgSelectPostProcessor:
|
||||
"""Provide user with list of available and active post processor
|
||||
choices."""
|
||||
|
||||
def __init__(self):
|
||||
self.dialog = FreeCADGui.PySideUic.loadUi(":/panels/DlgSelectPostProcessor.ui")
|
||||
firstItem = None
|
||||
@@ -454,18 +107,6 @@ class DlgSelectPostProcessor:
|
||||
|
||||
|
||||
class CommandPathPost:
|
||||
subpart = 1
|
||||
|
||||
def resolvePostProcessor(self, job):
|
||||
if hasattr(job, "PostProcessor"):
|
||||
post = Path.Preferences.defaultPostProcessor()
|
||||
if job.PostProcessor:
|
||||
post = job.PostProcessor
|
||||
if post and PostProcessor.exists(post):
|
||||
return post
|
||||
dlg = DlgSelectPostProcessor()
|
||||
return dlg.exec_()
|
||||
|
||||
def GetResources(self):
|
||||
return {
|
||||
"Pixmap": "CAM_Post",
|
||||
@@ -475,144 +116,97 @@ class CommandPathPost:
|
||||
}
|
||||
|
||||
def IsActive(self):
|
||||
if FreeCAD.ActiveDocument is not None:
|
||||
if FreeCADGui.Selection.getCompleteSelection():
|
||||
for o in FreeCAD.ActiveDocument.Objects:
|
||||
if o.Name[:3] == "Job":
|
||||
return True
|
||||
selected = FreeCADGui.Selection.getSelectionEx()
|
||||
if len(selected) != 1:
|
||||
return False
|
||||
|
||||
return False
|
||||
selected_object = selected[0].Object
|
||||
self.candidate = PathUtils.findParentJob(selected_object)
|
||||
|
||||
def exportObjectsWith(self, objs, partname, job, sequence, extraargs=None):
|
||||
Path.Log.track(extraargs)
|
||||
# check if the user has a project and has set the default post and
|
||||
# output filename
|
||||
# extraargs can be passed in at this time
|
||||
Path.Log.track(partname, sequence)
|
||||
Path.Log.track(objs)
|
||||
return self.candidate is not None
|
||||
|
||||
# partname = objs[0]
|
||||
# slist = objs[1]
|
||||
Path.Log.track(objs, partname)
|
||||
def _write_file(self, filename, gcode, policy):
|
||||
if policy == "Open File Dialog":
|
||||
dlg = QtGui.QFileDialog()
|
||||
dlg.setFileMode(QtGui.QFileDialog.FileMode.AnyFile)
|
||||
dlg.setAcceptMode(QtGui.QFileDialog.AcceptMode.AcceptSave)
|
||||
dlg.setDirectory(os.path.dirname(filename))
|
||||
dlg.selectFile(os.path.basename(filename))
|
||||
if dlg.exec_():
|
||||
filename = dlg.selectedFiles()[0]
|
||||
Path.Log.debug(filename)
|
||||
with open(filename, "w") as f:
|
||||
f.write(gcode)
|
||||
|
||||
postArgs = Path.Preferences.defaultPostProcessorArgs()
|
||||
if hasattr(job, "PostProcessorArgs") and job.PostProcessorArgs:
|
||||
postArgs = job.PostProcessorArgs
|
||||
elif hasattr(job, "PostProcessor") and job.PostProcessor:
|
||||
postArgs = ""
|
||||
elif policy == "Append Unique ID on conflict":
|
||||
while os.path.isfile(filename):
|
||||
base, ext = os.path.splitext(filename)
|
||||
filename = f"{base}-1{ext}"
|
||||
with open(filename, "w") as f:
|
||||
f.write(gcode)
|
||||
|
||||
if extraargs is not None:
|
||||
postArgs += f" {extraargs}"
|
||||
|
||||
Path.Log.track(postArgs)
|
||||
|
||||
postname = self.resolvePostProcessor(job)
|
||||
# filename = "-"
|
||||
filename = resolveFileName(job, partname, sequence)
|
||||
# if postname and needFilename:
|
||||
# filename = resolveFileName(job)
|
||||
|
||||
if postname and filename:
|
||||
print("post: %s(%s, %s)" % (postname, filename, postArgs))
|
||||
processor = PostProcessor.load(postname)
|
||||
gcode = processor.export(objs, filename, postArgs)
|
||||
return (False, gcode, filename)
|
||||
else:
|
||||
return (True, "", filename)
|
||||
elif policy == "Open File Dialog on conflict":
|
||||
if os.path.isfile(filename):
|
||||
dlg = QtGui.QFileDialog()
|
||||
dlg.setFileMode(QtGui.QFileDialog.FileMode.AnyFile)
|
||||
dlg.setAcceptMode(QtGui.QFileDialog.AcceptSave)
|
||||
dlg.setDirectory(os.path.dirname(filename))
|
||||
dlg.selectFile(os.path.basename(filename))
|
||||
if dlg.exec_():
|
||||
filename = dlg.selectedFiles()[0]
|
||||
Path.Log.debug(filename)
|
||||
with open(filename, "w") as f:
|
||||
f.write(gcode)
|
||||
else:
|
||||
with open(filename, "w") as f:
|
||||
f.write(gcode)
|
||||
else: # Overwrite
|
||||
with open(filename, "w") as f:
|
||||
f.write(gcode)
|
||||
FreeCAD.Console.PrintMessage(f"File written to {filename}\n")
|
||||
|
||||
def Activated(self):
|
||||
Path.Log.track()
|
||||
FreeCAD.ActiveDocument.openTransaction("Post Process the Selected path(s)")
|
||||
FreeCADGui.addModule("Path.Post.Command")
|
||||
"""
|
||||
Handles the activation of post processing, initiating the process based
|
||||
on user selection and document context.
|
||||
"""
|
||||
Path.Log.debug(self.candidate.Name)
|
||||
FreeCAD.ActiveDocument.openTransaction("Post Process the Selected Job")
|
||||
|
||||
# Attempt to figure out what the user wants to post-process
|
||||
# If a job is selected, post that.
|
||||
# If there's only one job in a document, post it.
|
||||
# If a user has selected a subobject of a job, post the job.
|
||||
# If multiple jobs and can't guess, ask them.
|
||||
postprocessor_name = _resolve_post_processor_name(self.candidate)
|
||||
Path.Log.debug(f"Post Processor: {postprocessor_name}")
|
||||
|
||||
selected = FreeCADGui.Selection.getSelectionEx()
|
||||
if len(selected) > 1:
|
||||
FreeCAD.Console.PrintError(
|
||||
"Please select a single job or other path object\n"
|
||||
)
|
||||
return
|
||||
elif len(selected) == 1:
|
||||
sel = selected[0].Object
|
||||
if sel.Name[:3] == "Job":
|
||||
job = sel
|
||||
elif hasattr(sel, "Path"):
|
||||
try:
|
||||
job = PathUtils.findParentJob(sel)
|
||||
except Exception:
|
||||
job = None
|
||||
else:
|
||||
job = None
|
||||
|
||||
if job is None:
|
||||
targetlist = []
|
||||
for o in FreeCAD.ActiveDocument.Objects:
|
||||
if hasattr(o, "Proxy"):
|
||||
if isinstance(o.Proxy, PathJob.ObjectJob):
|
||||
targetlist.append(o.Label)
|
||||
Path.Log.debug(f"Possible post objects: {targetlist}")
|
||||
if len(targetlist) > 1:
|
||||
jobname, result = QtGui.QInputDialog.getItem(
|
||||
None, translate("Path", "Choose a Path Job"), None, targetlist
|
||||
)
|
||||
|
||||
if result is False:
|
||||
return
|
||||
else:
|
||||
jobname = targetlist[0]
|
||||
job = FreeCAD.ActiveDocument.getObject(jobname)
|
||||
|
||||
Path.Log.debug(f"about to postprocess job: {job.Name}")
|
||||
|
||||
postlist = buildPostList(job)
|
||||
# filename = resolveFileName(job, "allitems", 0)
|
||||
|
||||
filenames = []
|
||||
|
||||
success = True
|
||||
finalgcode = ""
|
||||
for idx, section in enumerate(postlist):
|
||||
partname = section[0]
|
||||
sublist = section[1]
|
||||
|
||||
result, gcode, name = self.exportObjectsWith(sublist, partname, job, idx)
|
||||
filenames.append(name)
|
||||
Path.Log.track(result, gcode, name)
|
||||
|
||||
if name is None:
|
||||
success = False
|
||||
else:
|
||||
finalgcode += gcode
|
||||
|
||||
# if job.SplitOutput:
|
||||
# for idx, sublist in enumerate(postlist): # name, slist in postlist:
|
||||
# result = self.exportObjectsWith(sublist[1], sublist[0], job, idx)
|
||||
|
||||
# if result is None:
|
||||
# success = False
|
||||
# else:
|
||||
# gcode += result
|
||||
|
||||
# else:
|
||||
# finalpostlist = [item for (_, slist) in postlist for item in slist]
|
||||
# gcode = self.exportObjectsWith(finalpostlist, "allitems", job, 1)
|
||||
# success = gcode is not None
|
||||
|
||||
Path.Log.track(success)
|
||||
if success:
|
||||
if hasattr(job, "LastPostProcessDate"):
|
||||
job.LastPostProcessDate = str(datetime.now())
|
||||
if hasattr(job, "LastPostProcessOutput"):
|
||||
job.LastPostProcessOutput = " \n".join(filenames)
|
||||
Path.Log.track(job.LastPostProcessOutput)
|
||||
FreeCAD.ActiveDocument.commitTransaction()
|
||||
else:
|
||||
if not postprocessor_name:
|
||||
FreeCAD.ActiveDocument.abortTransaction()
|
||||
return
|
||||
|
||||
# get a postprocessor
|
||||
postprocessor = PostProcessorFactory.get_post_processor(
|
||||
self.candidate, postprocessor_name
|
||||
)
|
||||
|
||||
post_data = postprocessor.export()
|
||||
if not post_data:
|
||||
FreeCAD.ActiveDocument.abortTransaction()
|
||||
return
|
||||
|
||||
policy = Path.Preferences.defaultOutputPolicy()
|
||||
generator = FilenameGenerator(job=self.candidate)
|
||||
generated_filename = generator.generate_filenames()
|
||||
|
||||
for item in post_data:
|
||||
subpart, gcode = item
|
||||
|
||||
# get a name for the file
|
||||
subpart = "" if subpart == "allitems" else subpart
|
||||
Path.Log.debug(subpart)
|
||||
generator.set_subpartname(subpart)
|
||||
fname = next(generated_filename)
|
||||
|
||||
# write the results to the file
|
||||
self._write_file(fname, gcode, policy)
|
||||
|
||||
FreeCAD.ActiveDocument.commitTransaction()
|
||||
FreeCAD.ActiveDocument.recompute()
|
||||
|
||||
|
||||
|
||||
@@ -19,62 +19,324 @@
|
||||
# * USA *
|
||||
# * *
|
||||
# ***************************************************************************
|
||||
|
||||
import Path
|
||||
import sys
|
||||
"""
|
||||
The base classes for post processors in CAM workbench.
|
||||
"""
|
||||
from PySide import QtCore, QtGui
|
||||
from importlib import reload
|
||||
import FreeCAD
|
||||
import Path
|
||||
import Path.Base.Util as PathUtil
|
||||
import importlib.util
|
||||
import os
|
||||
import sys
|
||||
|
||||
Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule())
|
||||
|
||||
debug = True
|
||||
if debug:
|
||||
Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule())
|
||||
Path.Log.trackModule(Path.Log.thisModule())
|
||||
else:
|
||||
Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule())
|
||||
|
||||
|
||||
class _TempObject:
|
||||
Path = None
|
||||
Name = "Fixture"
|
||||
InList = []
|
||||
Label = "Fixture"
|
||||
|
||||
|
||||
class PostProcessorFactory:
|
||||
"""Factory class for creating post processors."""
|
||||
|
||||
@staticmethod
|
||||
def get_post_processor(job, postname):
|
||||
# Log initial debug message
|
||||
Path.Log.debug("PostProcessorFactory.get_post_processor()")
|
||||
|
||||
# Posts have to be in a place we can find them
|
||||
syspath = sys.path
|
||||
paths = Path.Preferences.searchPathsPost()
|
||||
paths.extend(sys.path)
|
||||
|
||||
module_name = f"{postname}_post"
|
||||
class_name = postname.title()
|
||||
|
||||
# Iterate all the paths to find the module
|
||||
for path in paths:
|
||||
module_path = os.path.join(path, f"{module_name}.py")
|
||||
spec = importlib.util.spec_from_file_location(module_name, module_path)
|
||||
|
||||
if spec and spec.loader:
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
try:
|
||||
spec.loader.exec_module(module)
|
||||
Path.Log.debug(f"found module {module_name} at {module_path}")
|
||||
|
||||
except (FileNotFoundError, ImportError, ModuleNotFoundError):
|
||||
continue
|
||||
|
||||
try:
|
||||
PostClass = getattr(module, class_name)
|
||||
return PostClass(job)
|
||||
except AttributeError:
|
||||
# Return an instance of WrapperPost if no valid module is found
|
||||
Path.Log.debug(f"Post processor {postname} is a script")
|
||||
return WrapperPost(job, module_path)
|
||||
|
||||
|
||||
class PostProcessor:
|
||||
"""Base Class. All postprocessors should inherit from this class."""
|
||||
|
||||
def __init__(self, job, tooltip, tooltipargs, units, *args, **kwargs):
|
||||
self._tooltip = tooltip
|
||||
self._tooltipargs = tooltipargs
|
||||
self._units = units
|
||||
self._job = job
|
||||
self._args = args
|
||||
self._kwargs = kwargs
|
||||
|
||||
@classmethod
|
||||
def exists(cls, processor):
|
||||
return processor in Path.Preferences.allAvailablePostProcessors()
|
||||
|
||||
@classmethod
|
||||
def load(cls, processor):
|
||||
Path.Log.track(processor)
|
||||
syspath = sys.path
|
||||
paths = Path.Preferences.searchPathsPost()
|
||||
paths.extend(sys.path)
|
||||
sys.path = paths
|
||||
def export(self):
|
||||
raise NotImplementedError("Subclass must implement abstract method")
|
||||
|
||||
postname = processor + "_post"
|
||||
namespace = {}
|
||||
@property
|
||||
def tooltip(self):
|
||||
"""Get the tooltip text for the post processor."""
|
||||
return self._tooltip
|
||||
|
||||
# can't modify function local scope with exec in python3
|
||||
exec("import %s as current_post" % postname, namespace)
|
||||
current_post = namespace["current_post"]
|
||||
@property
|
||||
def tooltipargs(self):
|
||||
"""Get the tooltip arguments for the post processor."""
|
||||
return self._tooltipargs
|
||||
|
||||
# make sure the script is reloaded if it was previously loaded
|
||||
# should the script have been imported for the first time above
|
||||
# then the initialization code of the script gets executed twice
|
||||
# resulting in 2 load messages if the script outputs one of those.
|
||||
@property
|
||||
def units(self):
|
||||
"""Get the units used by the post processor."""
|
||||
return self._units
|
||||
|
||||
exec("reload(%s)" % "current_post")
|
||||
def _buildPostList(self):
|
||||
"""
|
||||
determines the specific objects and order to
|
||||
postprocess Returns a list of objects which can be passed to
|
||||
exportObjectsWith() for final posting."""
|
||||
|
||||
sys.path = syspath
|
||||
def __fixtureSetup(order, fixture, job):
|
||||
"""Convert a Fixure setting to _TempObject instance with a G0 move to a
|
||||
safe height every time the fixture coordinate system change. Skip
|
||||
the move for first fixture, to avoid moving before tool and tool
|
||||
height compensation is enabled.
|
||||
|
||||
instance = PostProcessor(current_post)
|
||||
if hasattr(current_post, "UNITS"):
|
||||
if current_post.UNITS == "G21":
|
||||
instance.units = "Metric"
|
||||
else:
|
||||
instance.units = "Inch"
|
||||
"""
|
||||
|
||||
if hasattr(current_post, "TOOLTIP"):
|
||||
instance.tooltip = current_post.TOOLTIP
|
||||
if hasattr(current_post, "TOOLTIP_ARGS"):
|
||||
instance.tooltipArgs = current_post.TOOLTIP_ARGS
|
||||
return instance
|
||||
fobj = _TempObject()
|
||||
c1 = Path.Command(fixture)
|
||||
fobj.Path = Path.Path([c1])
|
||||
# Avoid any tool move after G49 in preamble and before tool change
|
||||
# and G43 in case tool height compensation is in use, to avoid
|
||||
# dangerous move without toolgg compesation.
|
||||
if order != 0:
|
||||
c2 = Path.Command(
|
||||
"G0 Z"
|
||||
+ str(
|
||||
job.Stock.Shape.BoundBox.ZMax
|
||||
+ job.SetupSheet.ClearanceHeightOffset.Value
|
||||
)
|
||||
)
|
||||
fobj.Path.addCommands(c2)
|
||||
fobj.InList.append(job)
|
||||
return fobj
|
||||
|
||||
def __init__(self, script):
|
||||
self.script = script
|
||||
self.tooltip = None
|
||||
self.tooltipArgs = None
|
||||
self.units = None
|
||||
self.machineName = None
|
||||
wcslist = self._job.Fixtures
|
||||
orderby = self._job.OrderOutputBy
|
||||
Path.Log.debug(f"Ordering by {orderby}")
|
||||
|
||||
def export(self, obj, filename, args):
|
||||
return self.script.export(obj, filename, args)
|
||||
postlist = []
|
||||
|
||||
if orderby == "Fixture":
|
||||
Path.Log.debug("Ordering by Fixture")
|
||||
# Order by fixture means all operations and tool changes will be
|
||||
# completed in one fixture before moving to the next.
|
||||
|
||||
currTool = None
|
||||
for index, f in enumerate(wcslist):
|
||||
# create an object to serve as the fixture path
|
||||
sublist = [__fixtureSetup(index, f, self._job)]
|
||||
|
||||
# Now generate the gcode
|
||||
for obj in self._job.Operations.Group:
|
||||
tc = PathUtil.toolControllerForOp(obj)
|
||||
if tc is not None and PathUtil.opProperty(obj, "Active"):
|
||||
if tc.ToolNumber != currTool:
|
||||
sublist.append(tc)
|
||||
Path.Log.debug(f"Appending TC: {tc.Name}")
|
||||
currTool = tc.ToolNumber
|
||||
sublist.append(obj)
|
||||
postlist.append((f, sublist))
|
||||
|
||||
elif orderby == "Tool":
|
||||
Path.Log.debug("Ordering by Tool")
|
||||
# Order by tool means tool changes are minimized.
|
||||
# all operations with the current tool are processed in the current
|
||||
# fixture before moving to the next fixture.
|
||||
|
||||
toolstring = "None"
|
||||
currTool = None
|
||||
|
||||
# Build the fixture list
|
||||
fixturelist = []
|
||||
for index, f in enumerate(wcslist):
|
||||
# create an object to serve as the fixture path
|
||||
fixturelist.append(__fixtureSetup(index, f, self._job))
|
||||
|
||||
# Now generate the gcode
|
||||
curlist = [] # list of ops for tool, will repeat for each fixture
|
||||
sublist = [] # list of ops for output splitting
|
||||
|
||||
Path.Log.track(self._job.PostProcessorOutputFile)
|
||||
for idx, obj in enumerate(self._job.Operations.Group):
|
||||
Path.Log.track(obj.Label)
|
||||
|
||||
# check if the operation is active
|
||||
if not getattr(obj, "Active", True):
|
||||
Path.Log.track()
|
||||
continue
|
||||
|
||||
# Determine the proper string for the Op's TC
|
||||
tc = PathUtil.toolControllerForOp(obj)
|
||||
if tc is None:
|
||||
tcstring = "None"
|
||||
elif "%T" in self._job.PostProcessorOutputFile:
|
||||
tcstring = f"{tc.ToolNumber}"
|
||||
else:
|
||||
tcstring = re.sub(r"[^\w\d-]", "_", tc.Label)
|
||||
Path.Log.track(toolstring)
|
||||
|
||||
if tc is None or tc.ToolNumber == currTool:
|
||||
curlist.append(obj)
|
||||
elif tc.ToolNumber != currTool and currTool is None: # first TC
|
||||
sublist.append(tc)
|
||||
curlist.append(obj)
|
||||
currTool = tc.ToolNumber
|
||||
toolstring = tcstring
|
||||
|
||||
elif tc.ToolNumber != currTool and currTool is not None: # TC
|
||||
for fixture in fixturelist:
|
||||
sublist.append(fixture)
|
||||
sublist.extend(curlist)
|
||||
postlist.append((toolstring, sublist))
|
||||
sublist = [tc]
|
||||
curlist = [obj]
|
||||
currTool = tc.ToolNumber
|
||||
toolstring = tcstring
|
||||
|
||||
if idx == len(self._job.Operations.Group) - 1: # Last operation.
|
||||
for fixture in fixturelist:
|
||||
sublist.append(fixture)
|
||||
sublist.extend(curlist)
|
||||
|
||||
postlist.append((toolstring, sublist))
|
||||
|
||||
elif orderby == "Operation":
|
||||
Path.Log.debug("Ordering by Operation")
|
||||
# Order by operation means ops are done in each fixture in
|
||||
# sequence.
|
||||
currTool = None
|
||||
|
||||
# Now generate the gcode
|
||||
for obj in self._job.Operations.Group:
|
||||
|
||||
# check if the operation is active
|
||||
if not getattr(obj, "Active", True):
|
||||
continue
|
||||
|
||||
sublist = []
|
||||
Path.Log.debug(f"obj: {obj.Name}")
|
||||
|
||||
for index, f in enumerate(wcslist):
|
||||
sublist.append(_fixtureSetup(index, f, self._job))
|
||||
tc = PathUtil.toolControllerForOp(obj)
|
||||
if tc is not None:
|
||||
if self._job.SplitOutput or (tc.ToolNumber != currTool):
|
||||
sublist.append(tc)
|
||||
currTool = tc.ToolNumber
|
||||
sublist.append(obj)
|
||||
postlist.append((obj.Label, sublist))
|
||||
|
||||
Path.Log.debug(f"Postlist: {postlist}")
|
||||
|
||||
if self._job.SplitOutput:
|
||||
Path.Log.track()
|
||||
return postlist
|
||||
|
||||
Path.Log.track()
|
||||
finalpostlist = [
|
||||
("allitems", [item for slist in postlist for item in slist[1]])
|
||||
]
|
||||
Path.Log.debug(f"Postlist: {postlist}")
|
||||
return finalpostlist
|
||||
|
||||
|
||||
class WrapperPost(PostProcessor):
|
||||
"""Wrapper class for old post processors that are scripts."""
|
||||
|
||||
def __init__(self, job, script_path, *args, **kwargs):
|
||||
super().__init__(
|
||||
job, tooltip=None, tooltipargs=None, units=None, *args, **kwargs
|
||||
)
|
||||
self.script_path = script_path
|
||||
Path.Log.debug(f"WrapperPost.__init__({script_path})")
|
||||
self.load_script()
|
||||
|
||||
def load_script(self):
|
||||
# Dynamically load the script as a module
|
||||
try:
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
"script_module", self.script_path
|
||||
)
|
||||
self.script_module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(self.script_module)
|
||||
except Exception as e:
|
||||
raise ImportError(f"Failed to load script: {e}")
|
||||
|
||||
if not hasattr(self.script_module, "export"):
|
||||
raise AttributeError("The script does not have an 'export' function.")
|
||||
|
||||
# Set properties based on attributes of the module
|
||||
self._units = (
|
||||
"Metric" if getattr(self.script_module, "UNITS", "G21") == "G21" else "Inch"
|
||||
)
|
||||
self._tooltip = getattr(self.script_module, "TOOLTIP", "No tooltip provided")
|
||||
self._tooltipargs = getattr(self.script_module, "TOOLTIP_ARGS", [])
|
||||
|
||||
def export(self):
|
||||
# Dynamically reload the module for the export to ensure up-to-date usage
|
||||
|
||||
postables = self._buildPostList()
|
||||
Path.Log.debug(f"postables count: {len(postables)}")
|
||||
|
||||
g_code_sections = []
|
||||
for idx, section in enumerate(postables):
|
||||
partname, sublist = section
|
||||
|
||||
gcode = self.script_module.export(sublist, "-", self._job.PostProcessorArgs)
|
||||
Path.Log.debug(f"Exported {partname}")
|
||||
g_code_sections.append((partname, gcode))
|
||||
return g_code_sections
|
||||
|
||||
@property
|
||||
def tooltip(self):
|
||||
return self._tooltip
|
||||
|
||||
@property
|
||||
def tooltipargs(self):
|
||||
return self._tooltipargs
|
||||
|
||||
@property
|
||||
def units(self):
|
||||
return self._units
|
||||
|
||||
@@ -27,21 +27,155 @@
|
||||
These are common functions and classes for creating custom post processors.
|
||||
"""
|
||||
|
||||
from PySide import QtCore, QtGui
|
||||
|
||||
import FreeCAD
|
||||
|
||||
import Path
|
||||
import Part
|
||||
|
||||
from Path.Base.MachineState import MachineState
|
||||
from PySide import QtCore, QtGui
|
||||
import FreeCAD
|
||||
import Part
|
||||
import Path
|
||||
import os
|
||||
import re
|
||||
|
||||
debug = False
|
||||
if debug:
|
||||
Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule())
|
||||
Path.Log.trackModule(Path.Log.thisModule())
|
||||
else:
|
||||
Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule())
|
||||
|
||||
translate = FreeCAD.Qt.translate
|
||||
|
||||
FreeCADGui = None
|
||||
if FreeCAD.GuiUp:
|
||||
import FreeCADGui
|
||||
|
||||
|
||||
class FilenameGenerator:
|
||||
def __init__(self, job):
|
||||
self.job = job
|
||||
self.subpartname = ""
|
||||
self.sequencenumber = 0
|
||||
path, filename, ext = self.get_path_and_filename_default()
|
||||
|
||||
self.qualified_path = self._apply_path_substitutions(path)
|
||||
self.qualified_filename = self._apply_filename_substitutions(filename)
|
||||
self.extension = ext
|
||||
|
||||
def get_path_and_filename_default(self):
|
||||
outputpath = ""
|
||||
filename = ""
|
||||
ext = ".nc"
|
||||
|
||||
validPathSubstitutions = ["D", "d", "M", "j"]
|
||||
validFilenameSubstitutions = ["j", "d", "T", "t", "W", "O", "S"]
|
||||
|
||||
if self.job.PostProcessorOutputFile:
|
||||
candidateOutputPath, candidateFilename = os.path.split(
|
||||
self.job.PostProcessorOutputFile
|
||||
)
|
||||
|
||||
if candidateOutputPath:
|
||||
outputpath = candidateOutputPath
|
||||
|
||||
if candidateFilename:
|
||||
filename, ext = os.path.splitext(candidateFilename)
|
||||
else:
|
||||
outputpath, filename = os.path.split(Path.Preferences.defaultOutputFile())
|
||||
filename, ext = os.path.splitext(filename)
|
||||
|
||||
# Make sure we have something to work with
|
||||
if not filename:
|
||||
filename = FreeCAD.ActiveDocument.Label
|
||||
|
||||
if not outputpath:
|
||||
outputpath = "."
|
||||
|
||||
if not ext:
|
||||
ext = ".nc"
|
||||
|
||||
# Check for invalid matches
|
||||
for match in re.findall("%(.)", outputpath):
|
||||
Path.Log.debug(f"match: {match}")
|
||||
if match not in validPathSubstitutions:
|
||||
outputpath = outputpath.replace(f"%{match}", "")
|
||||
FreeCAD.Console.PrintWarning(
|
||||
"Invalid substitution strings will be ignored in output path: %s\n"
|
||||
% match
|
||||
)
|
||||
|
||||
for match in re.findall("%(.)", filename):
|
||||
Path.Log.debug(f"match: {match}")
|
||||
if match not in validFilenameSubstitutions:
|
||||
filename = filename.replace(f"%{match}", "")
|
||||
FreeCAD.Console.PrintWarning(
|
||||
"Invalid substitution strings will be ignored in file path: %s\n"
|
||||
% match
|
||||
)
|
||||
|
||||
Path.Log.debug(f"outputpath: {outputpath} filename: {filename} ext: {ext}")
|
||||
return outputpath, filename, ext
|
||||
|
||||
def set_subpartname(self, subpartname):
|
||||
self.subpartname = subpartname
|
||||
|
||||
def _apply_path_substitutions(self, file_path):
|
||||
"""Apply substitutions based on job settings and other parameters."""
|
||||
substitutions = {
|
||||
"%D": os.path.dirname(self.job.Document.FileName or "."),
|
||||
"%d": self.job.Document.Label,
|
||||
"%j": self.job.Label,
|
||||
"%M": os.path.dirname(FreeCAD.getUserMacroDir()),
|
||||
}
|
||||
for key, value in substitutions.items():
|
||||
file_path = file_path.replace(key, value)
|
||||
|
||||
Path.Log.debug(f"file_path: {file_path}")
|
||||
return file_path
|
||||
|
||||
def _apply_filename_substitutions(self, file_name):
|
||||
Path.Log.debug(f"file_name: {file_name}")
|
||||
"""Apply substitutions based on job settings and other parameters."""
|
||||
substitutions = {
|
||||
"%d": self.job.Document.Label,
|
||||
"%j": self.job.Label,
|
||||
"%T": self.subpartname, # Tool
|
||||
"%t": self.subpartname, # Tool
|
||||
"%W": self.subpartname, # Fixture
|
||||
"%O": self.subpartname, # Operation
|
||||
}
|
||||
for key, value in substitutions.items():
|
||||
file_name = file_name.replace(key, value)
|
||||
|
||||
Path.Log.debug(f"file_name: {file_name}")
|
||||
return file_name
|
||||
|
||||
def generate_filenames(self):
|
||||
"""Yield filenames indefinitely with proper substitutions."""
|
||||
while True:
|
||||
temp_filename = self.qualified_filename
|
||||
Path.Log.debug(f"temp_filename: {temp_filename}")
|
||||
explicit_sequence = False
|
||||
matches = re.findall("%S", temp_filename)
|
||||
if matches:
|
||||
Path.Log.debug(f"matches: {matches}")
|
||||
temp_filename = re.sub("%S", str(self.sequencenumber), temp_filename)
|
||||
explicit_sequence = True
|
||||
|
||||
subpart = f"-{self.subpartname}" if self.subpartname else ""
|
||||
sequence = (
|
||||
f"-{self.sequencenumber}"
|
||||
if not explicit_sequence and self.sequencenumber
|
||||
else ""
|
||||
)
|
||||
filename = f"{temp_filename}{subpart}{sequence}{self.extension}"
|
||||
full_path = os.path.join(self.qualified_path, filename)
|
||||
|
||||
self.sequencenumber += 1
|
||||
Path.Log.debug(f"yielding filename: {full_path}")
|
||||
yield os.path.normpath(full_path)
|
||||
|
||||
|
||||
|
||||
class GCodeHighlighter(QtGui.QSyntaxHighlighter):
|
||||
def __init__(self, parent=None):
|
||||
super(GCodeHighlighter, self).__init__(parent)
|
||||
|
||||
@@ -27,7 +27,7 @@ import PathScripts.PathUtils as PathUtils
|
||||
from builtins import open as pyopen
|
||||
|
||||
TOOLTIP = """
|
||||
Dumper is an extremely simple postprocessor file for the Path workbench. It is used
|
||||
Dumper is an extremely simple postprocessor file for the CAM workbench. It is used
|
||||
to dump the command list from one or more Path objects for simple inspection. This post
|
||||
doesn't do any manipulation of the path and doesn't write anything to disk. It just
|
||||
shows the dialog so you can see it. Useful for debugging, but not much else.
|
||||
|
||||
@@ -43,10 +43,13 @@ from Tests.TestPathHelixGenerator import TestPathHelixGenerator
|
||||
from Tests.TestPathLog import TestPathLog
|
||||
from Tests.TestPathOpUtil import TestPathOpUtil
|
||||
|
||||
# from Tests.TestPathPost import TestPathPost
|
||||
#from Tests.TestPathPost import TestPathPost
|
||||
from Tests.TestPathPost import TestPathPostUtils
|
||||
from Tests.TestPathPost import TestBuildPostList
|
||||
from Tests.TestPathPost import TestOutputNameSubstitution
|
||||
from Tests.TestPathPost import TestPostProcessorFactory
|
||||
from Tests.TestPathPost import TestResolvingPostProcessorName
|
||||
from Tests.TestPathPost import TestFileNameGenerator
|
||||
|
||||
from Tests.TestPathPreferences import TestPathPreferences
|
||||
from Tests.TestPathProfile import TestPathProfile
|
||||
@@ -81,6 +84,7 @@ False if TestApp.__name__ else True
|
||||
False if TestBuildPostList.__name__ else True
|
||||
False if TestDressupDogbone.__name__ else True
|
||||
False if TestDressupDogboneII.__name__ else True
|
||||
False if TestFileNameGenerator.__name__ else True
|
||||
False if TestGeneratorDogboneII.__name__ else True
|
||||
False if TestHoldingTags.__name__ else True
|
||||
False if TestPathLanguage.__name__ else True
|
||||
@@ -94,7 +98,9 @@ False if TestPathHelpers.__name__ else True
|
||||
# False if TestPathHelix.__name__ else True
|
||||
False if TestPathLog.__name__ else True
|
||||
False if TestPathOpUtil.__name__ else True
|
||||
# False if TestPathPost.__name__ else True
|
||||
#False if TestPathPost.__name__ else True
|
||||
False if TestPostProcessorFactory.__name__ else True
|
||||
False if TestResolvingPostProcessorName.__name__ else True
|
||||
False if TestPathPostUtils.__name__ else True
|
||||
False if TestPathPreferences.__name__ else True
|
||||
False if TestPathProfile.__name__ else True
|
||||
|
||||
@@ -24,189 +24,362 @@
|
||||
import difflib
|
||||
import os
|
||||
import unittest
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
import FreeCAD
|
||||
import Path
|
||||
|
||||
import Path.Post.Command as PathPost
|
||||
import Path.Post.Command as PathCommand
|
||||
import Path.Post.Utils as PostUtils
|
||||
|
||||
from Path.Post.Processor import PostProcessor
|
||||
from Path.Post.Processor import PostProcessor, PostProcessorFactory
|
||||
|
||||
from Path.Post.Command import processFileNameSubstitutions, DlgSelectPostProcessor
|
||||
|
||||
# If KEEP_DEBUG_OUTPUT is False, remove the gcode file after the test succeeds.
|
||||
# If KEEP_DEBUG_OUTPUT is True or the test fails leave the gcode file behind
|
||||
# so it can be looked at easily.
|
||||
KEEP_DEBUG_OUTPUT = False
|
||||
|
||||
PathPost.LOG_MODULE = Path.Log.thisModule()
|
||||
Path.Log.setLevel(Path.Log.Level.INFO, PathPost.LOG_MODULE)
|
||||
PathCommand.LOG_MODULE = Path.Log.thisModule()
|
||||
Path.Log.setLevel(Path.Log.Level.DEBUG, PathCommand.LOG_MODULE)
|
||||
|
||||
|
||||
class TestPathPost(unittest.TestCase):
|
||||
"""Test some of the output of the postprocessors.
|
||||
|
||||
So far there are three tests each for the linuxcnc
|
||||
and centroid postprocessors.
|
||||
"""
|
||||
class TestFileNameGenerator(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
"""Set up the postprocessor tests."""
|
||||
pass
|
||||
self.doc = FreeCAD.open(FreeCAD.getHomePath() + "/Mod/CAM/Tests/boxtest.fcstd")
|
||||
self.job = self.doc.getObject("Job")
|
||||
|
||||
def tearDown(self):
|
||||
"""Tear down after the postprocessor tests."""
|
||||
pass
|
||||
FreeCAD.closeDocument("boxtest")
|
||||
|
||||
#
|
||||
# You can run just this test using:
|
||||
# ./FreeCAD -c -t Tests.TestPathPost.TestPathPost.test_postprocessors
|
||||
#
|
||||
def test_postprocessors(self):
|
||||
"""Test the postprocessors."""
|
||||
#
|
||||
# The tests are performed in the order they are listed:
|
||||
# one test performed on all of the postprocessors
|
||||
# then the next test on all of the postprocessors, etc.
|
||||
# You can comment out the tuples for tests that you don't want
|
||||
# to use.
|
||||
#
|
||||
tests_to_perform = (
|
||||
# (output_file_id, freecad_document, job_name, postprocessor_arguments,
|
||||
# postprocessor_list)
|
||||
#
|
||||
# test with all of the defaults (metric mode, etc.)
|
||||
("default", "boxtest1", "Job", "--no-show-editor", ()),
|
||||
# test in Imperial mode
|
||||
("imperial", "boxtest1", "Job", "--no-show-editor --inches", ()),
|
||||
# test in metric, G55, M4, the other way around the part
|
||||
("other_way", "boxtest1", "Job001", "--no-show-editor", ()),
|
||||
# test in metric, split by fixtures, G54, G55, G56
|
||||
("split", "boxtest1", "Job002", "--no-show-editor", ()),
|
||||
# test in metric mode without the header
|
||||
("no_header", "boxtest1", "Job", "--no-header --no-show-editor", ()),
|
||||
# test translating G81, G82, and G83 to G00 and G01 commands
|
||||
(
|
||||
"drill_translate",
|
||||
"drill_test1",
|
||||
"Job",
|
||||
"--no-show-editor --translate_drill",
|
||||
("grbl", "refactored_grbl"),
|
||||
),
|
||||
)
|
||||
#
|
||||
# The postprocessors to test.
|
||||
# You can comment out any postprocessors that you don't want
|
||||
# to test.
|
||||
#
|
||||
postprocessors_to_test = (
|
||||
"centroid",
|
||||
# "fanuc",
|
||||
"grbl",
|
||||
"linuxcnc",
|
||||
"mach3_mach4",
|
||||
"refactored_centroid",
|
||||
# "refactored_fanuc",
|
||||
"refactored_grbl",
|
||||
"refactored_linuxcnc",
|
||||
"refactored_mach3_mach4",
|
||||
"refactored_test",
|
||||
)
|
||||
#
|
||||
# Enough of the path to where the tests are stored so that
|
||||
# they can be found by the python interpreter.
|
||||
#
|
||||
PATHTESTS_LOCATION = "Mod/CAM/Tests"
|
||||
#
|
||||
# The following code tries to re-use an open FreeCAD document
|
||||
# as much as possible. It compares the current document with
|
||||
# the document for the next test. If the names are different
|
||||
# then the current document is closed and the new document is
|
||||
# opened. The final document is closed at the end of the code.
|
||||
#
|
||||
current_document = ""
|
||||
for (
|
||||
output_file_id,
|
||||
freecad_document,
|
||||
job_name,
|
||||
postprocessor_arguments,
|
||||
postprocessor_list,
|
||||
) in tests_to_perform:
|
||||
if current_document != freecad_document:
|
||||
if current_document != "":
|
||||
FreeCAD.closeDocument(current_document)
|
||||
current_document = freecad_document
|
||||
current_document_path = (
|
||||
FreeCAD.getHomePath()
|
||||
+ PATHTESTS_LOCATION
|
||||
+ os.path.sep
|
||||
+ current_document
|
||||
+ ".fcstd"
|
||||
)
|
||||
FreeCAD.open(current_document_path)
|
||||
job = FreeCAD.ActiveDocument.getObject(job_name)
|
||||
# Create the objects to be written by the postprocessor.
|
||||
postlist = PathPost.buildPostList(job)
|
||||
for postprocessor_id in postprocessors_to_test:
|
||||
if postprocessor_list == () or postprocessor_id in postprocessor_list:
|
||||
print(
|
||||
"\nRunning %s test on %s postprocessor:\n"
|
||||
% (output_file_id, postprocessor_id)
|
||||
)
|
||||
processor = PostProcessor.load(postprocessor_id)
|
||||
output_file_path = FreeCAD.getHomePath() + PATHTESTS_LOCATION
|
||||
output_file_pattern = "test_%s_%s" % (
|
||||
postprocessor_id,
|
||||
output_file_id,
|
||||
)
|
||||
output_file_extension = ".ngc"
|
||||
for idx, section in enumerate(postlist):
|
||||
partname = section[0]
|
||||
sublist = section[1]
|
||||
output_filename = PathPost.processFileNameSubstitutions(
|
||||
job,
|
||||
partname,
|
||||
idx,
|
||||
output_file_path,
|
||||
output_file_pattern,
|
||||
output_file_extension,
|
||||
)
|
||||
# print("output file: " + output_filename)
|
||||
file_path, extension = os.path.splitext(output_filename)
|
||||
reference_file_name = "%s%s%s" % (file_path, "_ref", extension)
|
||||
# print("reference file: " + reference_file_name)
|
||||
gcode = processor.export(
|
||||
sublist, output_filename, postprocessor_arguments
|
||||
)
|
||||
if not gcode:
|
||||
print("no gcode")
|
||||
with open(reference_file_name, "r") as fp:
|
||||
reference_gcode = fp.read()
|
||||
if not reference_gcode:
|
||||
print("no reference gcode")
|
||||
# Remove the "Output Time:" line in the header from the
|
||||
# comparison if it is present because it changes with
|
||||
# every test.
|
||||
gcode_lines = [
|
||||
i for i in gcode.splitlines(True) if "Output Time:" not in i
|
||||
]
|
||||
reference_gcode_lines = [
|
||||
i
|
||||
for i in reference_gcode.splitlines(True)
|
||||
if "Output Time:" not in i
|
||||
]
|
||||
if gcode_lines != reference_gcode_lines:
|
||||
msg = "".join(
|
||||
difflib.ndiff(gcode_lines, reference_gcode_lines)
|
||||
)
|
||||
self.fail(
|
||||
os.path.basename(output_filename)
|
||||
+ " output doesn't match:\n"
|
||||
+ msg
|
||||
)
|
||||
if not KEEP_DEBUG_OUTPUT:
|
||||
os.remove(output_filename)
|
||||
if current_document != "":
|
||||
FreeCAD.closeDocument(current_document)
|
||||
# def test010(self):
|
||||
# # Assuming PostUtils.FilenameGenerator has been imported correctly
|
||||
# generator = PostUtils.FilenameGenerator(job=self.job, base_output_path="output",
|
||||
# filename_template="file", extension=".nc")
|
||||
|
||||
# filename_generator = generator.generate_filenames()
|
||||
# for i in range(5):
|
||||
# generated_filename = next(filename_generator)
|
||||
# expected_filename = f"output/file-{i}.nc"
|
||||
# self.assertEqual(generated_filename, expected_filename)
|
||||
|
||||
|
||||
def test010(self):
|
||||
generator = PostUtils.FilenameGenerator(job=self.job, base_output_path="output",
|
||||
filename_template="file", extension=".nc")
|
||||
filename_generator = generator.generate_filenames()
|
||||
expected_filenames = ["output/file.nc"] + [f"output/file-{i}.nc" for i in range(1, 5)]
|
||||
for expected_filename in expected_filenames:
|
||||
generated_filename = next(filename_generator)
|
||||
self.assertEqual(generated_filename, expected_filename)
|
||||
|
||||
class TestResolvingPostProcessorName(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.doc = FreeCAD.open(FreeCAD.getHomePath() + "/Mod/CAM/Tests/boxtest.fcstd")
|
||||
self.job = self.doc.getObject("Job")
|
||||
pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/CAM")
|
||||
pref.SetString("PostProcessorDefault", "")
|
||||
|
||||
def tearDown(self):
|
||||
FreeCAD.closeDocument("boxtest")
|
||||
|
||||
def test010(self):
|
||||
# Test if post is defined in job
|
||||
with patch('Path.Post.Processor.PostProcessor.exists', return_value=True):
|
||||
postname = PathCommand._resolve_post_processor_name(self.job)
|
||||
self.assertEqual(postname, "linuxcnc")
|
||||
|
||||
def test020(self):
|
||||
# Test if post is invalid
|
||||
with patch('Path.Post.Processor.PostProcessor.exists', return_value=False):
|
||||
with self.assertRaises(ValueError):
|
||||
PathCommand._resolve_post_processor_name(self.job)
|
||||
|
||||
def test030(self):
|
||||
# Test if post is defined in prefs
|
||||
pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/CAM")
|
||||
pref.SetString("PostProcessorDefault", "grbl")
|
||||
self.job.PostProcessor = ""
|
||||
with patch('Path.Post.Processor.PostProcessor.exists', return_value=True):
|
||||
postname = PathCommand._resolve_post_processor_name(self.job)
|
||||
self.assertEqual(postname, "grbl")
|
||||
|
||||
def test040(self):
|
||||
# Test if user interaction is correctly handled
|
||||
self.job.PostProcessor = ""
|
||||
if FreeCAD.GuiUp:
|
||||
with patch('Path.Post.Command.DlgSelectPostProcessor') as mock_dlg, \
|
||||
patch('Path.Post.Processor.PostProcessor.exists', return_value=True):
|
||||
mock_dlg.return_value.exec_.return_value = 'generic'
|
||||
postname = PathCommand._resolve_post_processor_name(self.job)
|
||||
self.assertEqual(postname, 'generic')
|
||||
else:
|
||||
with self.assertRaises(ValueError):
|
||||
PathCommand._resolve_post_processor_name(self.job)
|
||||
|
||||
class TestPostProcessorFactory(unittest.TestCase):
|
||||
"""Test creation of postprocessor objects."""
|
||||
|
||||
def setUp(self):
|
||||
self.doc = FreeCAD.open(FreeCAD.getHomePath() + "/Mod/CAM/Tests/boxtest.fcstd")
|
||||
self.job = self.doc.getObject("Job")
|
||||
|
||||
def tearDown(self):
|
||||
FreeCAD.closeDocument("boxtest")
|
||||
|
||||
|
||||
def test020(self):
|
||||
# test creation of postprocessor object
|
||||
post = PostProcessorFactory.get_post_processor(self.job, "generic")
|
||||
self.assertTrue(post is not None)
|
||||
self.assertTrue(hasattr(post, "export"))
|
||||
self.assertTrue(hasattr(post, "_buildPostList"))
|
||||
|
||||
def test030(self):
|
||||
# test wrapping of old school postprocessor scripts
|
||||
post = PostProcessorFactory.get_post_processor(self.job, "linuxcnc")
|
||||
self.assertTrue(post is not None)
|
||||
self.assertTrue(hasattr(post, "_buildPostList"))
|
||||
|
||||
|
||||
def test100(self):
|
||||
"""Test the processFileNameSubstitutions function."""
|
||||
|
||||
document_dir = os.path.dirname(FreeCAD.ActiveDocument.FileName)
|
||||
Path.Log.debug(f"document_dir: {document_dir}")
|
||||
|
||||
user_macro_dir = os.path.dirname(FreeCAD.getUserMacroDir())
|
||||
Path.Log.debug(f"user_macro_dir: {user_macro_dir}")
|
||||
|
||||
outputpath = "%D/output"
|
||||
filename = "outputfile-%j-%d"
|
||||
ext = ".txt"
|
||||
expected_path = f"{document_dir}{os.path.sep}output/outputfile-{self.job.Label}-{self.doc.Label}{ext}"
|
||||
|
||||
# Call the function
|
||||
result = processFileNameSubstitutions(self.job, "Subpart", 1, outputpath, filename, ext)
|
||||
|
||||
Path.Log.debug(f"result: {result}")
|
||||
|
||||
self.assertEqual(result, expected_path)
|
||||
|
||||
# test macro path substitution
|
||||
outputpath = "%M/output"
|
||||
filename = "file-%j"
|
||||
ext = ".txt"
|
||||
|
||||
result = processFileNameSubstitutions(self.job, "Subpart", 1, outputpath, filename, ext)
|
||||
expected_path = f"{user_macro_dir}/output/file-{self.job.Label}.txt"
|
||||
|
||||
self.assertEqual(result, expected_path)
|
||||
|
||||
# test job name substitution
|
||||
document_label = FreeCAD.ActiveDocument.Label
|
||||
outputpath = "output/%d"
|
||||
filename = "file-%d-%j"
|
||||
ext = ".txt"
|
||||
|
||||
result = processFileNameSubstitutions(self.job, "Subpart", 1, outputpath, filename, ext)
|
||||
expected_path = f"output/{document_label}/file-{document_label}-{self.job.Label}.txt"
|
||||
|
||||
self.assertEqual(result, expected_path)
|
||||
|
||||
# test sequence number substitution
|
||||
outputpath = "output"
|
||||
filename = "file-%S"
|
||||
ext = ".txt"
|
||||
|
||||
result = processFileNameSubstitutions(self.job, "Subpart", 42, outputpath, filename, ext)
|
||||
expected_path = f"output/file-42.txt"
|
||||
|
||||
self.assertEqual(result, expected_path)
|
||||
|
||||
# test tool number substitution
|
||||
outputpath = "output"
|
||||
filename = "file"
|
||||
ext = ""
|
||||
|
||||
result = processFileNameSubstitutions(self.job, "Subpart", 1, outputpath, filename, ext)
|
||||
expected_path = f"output/file.nc" # Expect default .nc extension
|
||||
|
||||
self.assertEqual(result, expected_path)
|
||||
|
||||
|
||||
class TestPostProcessorClass(unittest.TestCase):
|
||||
"""Test new post structure objects."""
|
||||
|
||||
def setUp(self):
|
||||
self.doc = FreeCAD.open(FreeCAD.getHomePath() + "/Mod/CAM/Tests/boxtest.fcstd")
|
||||
self.job = self.doc.getObject("Job")
|
||||
post = PostProcessorFactory.get_post_processor(self.job, "generic")
|
||||
|
||||
def tearDown(self):
|
||||
FreeCAD.closeDocument("boxtest")
|
||||
|
||||
class TestPostProcessorScript(unittest.TestCase):
|
||||
"""Test old-school posts"""
|
||||
|
||||
def setUp(self):
|
||||
self.doc = FreeCAD.open(FreeCAD.getHomePath() + "/Mod/CAM/Tests/boxtest.fcstd")
|
||||
self.job = self.doc.getObject("Job")
|
||||
post = PostProcessorFactory.get_post_processor(self.job, "linuxcnc")
|
||||
|
||||
def tearDown(self):
|
||||
FreeCAD.closeDocument("boxtest")
|
||||
|
||||
|
||||
##
|
||||
## You can run just this test using:
|
||||
## ./FreeCAD -c -t Tests.TestPathPost.TestPathPost.test_postprocessors
|
||||
##
|
||||
#def test_postprocessors(self):
|
||||
# """Test the postprocessors."""
|
||||
# #
|
||||
# # The tests are performed in the order they are listed:
|
||||
# # one test performed on all of the postprocessors
|
||||
# # then the next test on all of the postprocessors, etc.
|
||||
# # You can comment out the tuples for tests that you don't want
|
||||
# # to use.
|
||||
# #
|
||||
# tests_to_perform = (
|
||||
# # (output_file_id, freecad_document, job_name, postprocessor_arguments,
|
||||
# # postprocessor_list)
|
||||
# #
|
||||
# # test with all of the defaults (metric mode, etc.)
|
||||
# ("default", "boxtest1", "Job", "--no-show-editor", ()),
|
||||
# # test in Imperial mode
|
||||
# ("imperial", "boxtest1", "Job", "--no-show-editor --inches", ()),
|
||||
# # test in metric, G55, M4, the other way around the part
|
||||
# ("other_way", "boxtest1", "Job001", "--no-show-editor", ()),
|
||||
# # test in metric, split by fixtures, G54, G55, G56
|
||||
# ("split", "boxtest1", "Job002", "--no-show-editor", ()),
|
||||
# # test in metric mode without the header
|
||||
# ("no_header", "boxtest1", "Job", "--no-header --no-show-editor", ()),
|
||||
# # test translating G81, G82, and G83 to G00 and G01 commands
|
||||
# (
|
||||
# "drill_translate",
|
||||
# "drill_test1",
|
||||
# "Job",
|
||||
# "--no-show-editor --translate_drill",
|
||||
# ("grbl", "refactored_grbl"),
|
||||
# ),
|
||||
# )
|
||||
# #
|
||||
# # The postprocessors to test.
|
||||
# # You can comment out any postprocessors that you don't want
|
||||
# # to test.
|
||||
# #
|
||||
# postprocessors_to_test = (
|
||||
# "centroid",
|
||||
# # "fanuc",
|
||||
# "grbl",
|
||||
# "linuxcnc",
|
||||
# "mach3_mach4",
|
||||
# "refactored_centroid",
|
||||
# # "refactored_fanuc",
|
||||
# "refactored_grbl",
|
||||
# "refactored_linuxcnc",
|
||||
# "refactored_mach3_mach4",
|
||||
# "refactored_test",
|
||||
# )
|
||||
# #
|
||||
# # Enough of the path to where the tests are stored so that
|
||||
# # they can be found by the python interpreter.
|
||||
# #
|
||||
# PATHTESTS_LOCATION = "Mod/CAM/Tests"
|
||||
# #
|
||||
# # The following code tries to re-use an open FreeCAD document
|
||||
# # as much as possible. It compares the current document with
|
||||
# # the document for the next test. If the names are different
|
||||
# # then the current document is closed and the new document is
|
||||
# # opened. The final document is closed at the end of the code.
|
||||
# #
|
||||
# current_document = ""
|
||||
# for (
|
||||
# output_file_id,
|
||||
# freecad_document,
|
||||
# job_name,
|
||||
# postprocessor_arguments,
|
||||
# postprocessor_list,
|
||||
# ) in tests_to_perform:
|
||||
# if current_document != freecad_document:
|
||||
# if current_document != "":
|
||||
# FreeCAD.closeDocument(current_document)
|
||||
# current_document = freecad_document
|
||||
# current_document_path = (
|
||||
# FreeCAD.getHomePath()
|
||||
# + PATHTESTS_LOCATION
|
||||
# + os.path.sep
|
||||
# + current_document
|
||||
# + ".fcstd"
|
||||
# )
|
||||
# FreeCAD.open(current_document_path)
|
||||
# job = FreeCAD.ActiveDocument.getObject(job_name)
|
||||
# # Create the objects to be written by the postprocessor.
|
||||
# postlist = PathPost.buildPostList(job)
|
||||
# for postprocessor_id in postprocessors_to_test:
|
||||
# if postprocessor_list == () or postprocessor_id in postprocessor_list:
|
||||
# print(
|
||||
# "\nRunning %s test on %s postprocessor:\n"
|
||||
# % (output_file_id, postprocessor_id)
|
||||
# )
|
||||
# processor = PostProcessor.load(postprocessor_id)
|
||||
# output_file_path = FreeCAD.getHomePath() + PATHTESTS_LOCATION
|
||||
# output_file_pattern = "test_%s_%s" % (
|
||||
# postprocessor_id,
|
||||
# output_file_id,
|
||||
# )
|
||||
# output_file_extension = ".ngc"
|
||||
# for idx, section in enumerate(postlist):
|
||||
# partname = section[0]
|
||||
# sublist = section[1]
|
||||
# output_filename = PathPost.processFileNameSubstitutions(
|
||||
# job,
|
||||
# partname,
|
||||
# idx,
|
||||
# output_file_path,
|
||||
# output_file_pattern,
|
||||
# output_file_extension,
|
||||
# )
|
||||
# # print("output file: " + output_filename)
|
||||
# file_path, extension = os.path.splitext(output_filename)
|
||||
# reference_file_name = "%s%s%s" % (file_path, "_ref", extension)
|
||||
# # print("reference file: " + reference_file_name)
|
||||
# gcode = processor.export(
|
||||
# sublist, output_filename, postprocessor_arguments
|
||||
# )
|
||||
# if not gcode:
|
||||
# print("no gcode")
|
||||
# with open(reference_file_name, "r") as fp:
|
||||
# reference_gcode = fp.read()
|
||||
# if not reference_gcode:
|
||||
# print("no reference gcode")
|
||||
# # Remove the "Output Time:" line in the header from the
|
||||
# # comparison if it is present because it changes with
|
||||
# # every test.
|
||||
# gcode_lines = [
|
||||
# i for i in gcode.splitlines(True) if "Output Time:" not in i
|
||||
# ]
|
||||
# reference_gcode_lines = [
|
||||
# i
|
||||
# for i in reference_gcode.splitlines(True)
|
||||
# if "Output Time:" not in i
|
||||
# ]
|
||||
# if gcode_lines != reference_gcode_lines:
|
||||
# msg = "".join(
|
||||
# difflib.ndiff(gcode_lines, reference_gcode_lines)
|
||||
# )
|
||||
# self.fail(
|
||||
# os.path.basename(output_filename)
|
||||
# + " output doesn't match:\n"
|
||||
# + msg
|
||||
# )
|
||||
# if not KEEP_DEBUG_OUTPUT:
|
||||
# os.remove(output_filename)
|
||||
# if current_document != "":
|
||||
# FreeCAD.closeDocument(current_document)
|
||||
|
||||
|
||||
class TestPathPostUtils(unittest.TestCase):
|
||||
@@ -237,13 +410,13 @@ class TestPathPostUtils(unittest.TestCase):
|
||||
)
|
||||
|
||||
|
||||
def dumpgroup(group):
|
||||
print("====Dump Group======")
|
||||
for i in group:
|
||||
print(i[0])
|
||||
for j in i[1]:
|
||||
print(f"--->{j.Name}")
|
||||
print("====================")
|
||||
#def dumpgroup(group):
|
||||
#print("====Dump Group======")
|
||||
#for i in group:
|
||||
# print(i[0])
|
||||
# for j in i[1]:
|
||||
# print(f"--->{j.Name}")
|
||||
#print("====================")
|
||||
|
||||
|
||||
class TestBuildPostList(unittest.TestCase):
|
||||
|
||||
Reference in New Issue
Block a user