diff --git a/pyproject.toml b/pyproject.toml index c2e118f..ee736c7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,8 +21,8 @@ dependencies = [ "cadquery-ocp>=7.7.0a0,<7.8", "ezdxf", "multimethod>=1.7,<2.0", + "numpy<2.0.0", "nlopt", - "nptyping==2.0.1", "typish", "casadi", "path", diff --git a/src/cq_cli/main.py b/src/cq_cli/main.py index 52ff02f..c58a3a6 100755 --- a/src/cq_cli/main.py +++ b/src/cq_cli/main.py @@ -141,8 +141,11 @@ def get_params_from_file(param_json_path, errfile): def main(): outfile = None + outfiles = None errfile = None codec_module = None + codecs = None + active_codecs = None params = {} output_opts = {} @@ -155,7 +158,7 @@ def main(): ) parser.add_argument( "--codec", - help="The codec to use when converting the CadQuery output. Must match the name of a codec file in the cqcodecs directory.", + help="(REQUIRED) The codec to use when converting the CadQuery output. Must match the name of a codec file in the cqcodecs directory. Multiple codecs can be specified, separated by the colon (;) character. The number of codecs must match the number of output files (outfile parameter).", ) parser.add_argument( "--getparams", @@ -164,7 +167,7 @@ def main(): parser.add_argument("--infile", help="The input CadQuery script to convert.") parser.add_argument( "--outfile", - help="File to write the converted CadQuery output to. Prints to stdout if not specified.", + help="File to write the converted CadQuery output to. Prints to stdout if not specified. Multiple output files can be specified, separated by the colon (;) character. The number of codecs (codec parameter) must match the number of output files.", ) parser.add_argument( "--errfile", @@ -210,6 +213,11 @@ def main(): if args.outfile != None: outfile = args.outfile + # Handle the case of multiple outfiles + if ";" in outfile: + outfiles = outfile.split(";") + outfile = outfiles[0] + # # Errfile handling # @@ -321,6 +329,11 @@ def main(): # Save the requested codec for later codec = args.codec + # Handle multiple output files + if codec != None and ";" in codec: + codecs = codec.split(";") + codec = codecs[0] + # Attempt to auto-detect the codec if the user has not set the option if args.outfile != None and args.codec == None: # Determine the codec from the file extension @@ -330,6 +343,24 @@ def main(): if codec_temp in loaded_codecs: codec = codec_temp + # If there are multiple output files, make sure to set the codecs for all of them + if outfiles != None and codecs == None: + codecs = [] + for i in range(len(outfiles)): + codec_temp = outfiles[i].split(".")[-1] + if codec_temp != None: + # Construct the codec module name + codec_temp = "cq_codec_" + codec_temp + + if codec_temp in loaded_codecs: + # The codecs array needs just the short name, not the full module name + codecs.append(codec_temp.replace("cq_codec_", "")) + + # Keep track of the codes that are being actively used + if active_codecs == None: + active_codecs = [] + active_codecs.append(loaded_codecs[codec_temp]) + # If the user has not supplied a codec, they need to be validating the script if (codec == None and args.outfile == None) and ( args.validate == None or args.validate == "false" @@ -351,6 +382,16 @@ def main(): if codec in key: codec_module = loaded_codecs[key] + # Handle there being multiple codecs + if codecs != None: + for cur_codec in codecs: + for key in loaded_codecs: + # Check to make sure that the requested codec exists + if cur_codec in key: + if active_codecs == None: + active_codecs = [] + active_codecs.append(loaded_codecs["cq_codec_" + cur_codec]) + # # Infile handling # @@ -451,26 +492,38 @@ def main(): # # Build, parse and let the selected codec convert the CQ output try: - # Use the codec plugin to do the conversion - converted = codec_module.convert(build_result, outfile, errfile, output_opts) - - # If converted is None, assume that the output was written to file directly by the codec - if converted != None: - # Write the converted output to the appropriate place based on the command line arguments - if outfile == None: - print(converted) - else: - if isinstance(converted, str): - with open(outfile, "w") as file: - file.write(converted) - elif isinstance(converted, (bytes, bytearray)): - with open(outfile, "wb") as file: - file.write(converted) + # Handle the case of multiple output files + if outfiles == None: + outfiles = [outfile] + + # Step through all of the potential output files + for i in range(len(outfiles)): + outfile = outfiles[i] + if len(outfiles) > 1: + codec_module = active_codecs[i] + + # Use the codec plugin to do the conversion + converted = codec_module.convert( + build_result, outfile, errfile, output_opts + ) + + # If converted is None, assume that the output was written to file directly by the codec + if converted != None: + # Write the converted output to the appropriate place based on the command line arguments + if outfile == None: + print(converted) else: - raise TypeError( - "Expected converted output to be str, bytes, or bytearray. Got '%s'" - % type(converted).__name__ - ) + if isinstance(converted, str): + with open(outfile, "w") as file: + file.write(converted) + elif isinstance(converted, (bytes, bytearray)): + with open(outfile, "wb") as file: + file.write(converted) + else: + raise TypeError( + "Expected converted output to be str, bytes, or bytearray. Got '%s'" + % type(converted).__name__ + ) except Exception: out_tb = traceback.format_exc() diff --git a/tests/test_cli.py b/tests/test_cli.py index fe77199..6fb4779 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -117,6 +117,68 @@ def test_codec_infile_outfile_errfile_arguments(): assert err_str == "Argument error: infile does not exist." +def test_no_codec_parameter(): + """ + Tests the CLI's ability to infer the codec from the outfile extension. + """ + test_file = helpers.get_test_file_location("cube.py") + + # Get a temporary output file location + temp_dir = tempfile.gettempdir() + temp_file = os.path.join(temp_dir, "temp_test_12.step") + + command = [ + "python", + "src/cq_cli/main.py", + "--infile", + test_file, + "--outfile", + temp_file, + ] + out, err, exitcode = helpers.cli_call(command) + + # Read the STEP output back from the outfile + with open(temp_file, "r") as file: + step_str = file.read() + + assert step_str.startswith("ISO-10303-21;") + + +def test_no_codec_parameter_multiple_infiles(): + """ + Tests the CLI's ability to infer the codecs from multiple infile extensions. + """ + test_file = helpers.get_test_file_location("cube.py") + + # Get a temporary output file location + temp_dir = tempfile.gettempdir() + temp_file_step = os.path.join(temp_dir, "temp_test_13.step") + temp_file_stl = os.path.join(temp_dir, "temp_test_13.stl") + temp_paths = temp_file_step + ";" + temp_file_stl + + command = [ + "python", + "src/cq_cli/main.py", + "--infile", + test_file, + "--outfile", + temp_paths, + ] + out, err, exitcode = helpers.cli_call(command) + + # Read the STEP output back from the outfile to make sure it has the correct content + with open(temp_file_step, "r") as file: + step_str = file.read() + assert step_str.startswith("ISO-10303-21;") + + # Read the STL output back from the outfile to make sure it has the correct content + with open(temp_file_stl, "r") as file: + stl_str = file.read() + assert stl_str.startswith("solid") + + assert exitcode == 0 + + def test_parameter_file(): """ Tests the CLI's ability to load JSON parameters from a file. @@ -456,3 +518,29 @@ def test_expression_argument(): # cq-cli invocation should fail assert exitcode == 200 + + +def test_multiple_outfiles(): + """ + Tests the CLI with multiple output files specified. + """ + test_file = helpers.get_test_file_location("cube.py") + + # Get a temporary output file location + temp_dir = tempfile.gettempdir() + temp_file_step = os.path.join(temp_dir, "temp_test_11.step") + temp_file_stl = os.path.join(temp_dir, "temp_test_11.stl") + temp_paths = temp_file_step + ";" + temp_file_stl + + command = [ + "python", + "src/cq_cli/main.py", + "--codec", + "step;stl", + "--infile", + test_file, + "--outfile", + temp_paths, + ] + out, err, exitcode = helpers.cli_call(command) + assert exitcode == 0