diff --git a/.gitignore b/.gitignore index 867c51d15..efd8a71bb 100644 --- a/.gitignore +++ b/.gitignore @@ -69,3 +69,7 @@ src/PyMca5/PyMcaMath/mva/_cython_kmeans/*.c # Vim *.swp + +# Profiling +profile_output +test.profile diff --git a/src/PyMca5/PyMcaCore/LegacyStackROIBatch.py b/src/PyMca5/PyMcaCore/LegacyStackROIBatch.py index a836169d8..52250942e 100644 --- a/src/PyMca5/PyMcaCore/LegacyStackROIBatch.py +++ b/src/PyMca5/PyMcaCore/LegacyStackROIBatch.py @@ -33,11 +33,17 @@ __doc__ = """ Module to calculate a set of ROIs on a stack of data. """ + import os +import sys +import logging + import numpy + from PyMca5.PyMcaIO import ConfigDict -import time -import logging +from PyMca5.PyMca import EDFStack +from PyMca5.PyMca import ArraySave +from PyMca5.PyMcaMisc import CliUtils _logger = logging.getLogger(__name__) @@ -274,113 +280,111 @@ def getFileListFromPattern(pattern, begin, end, increment=None): raise ValueError("Cannot handle more than three indices.") return fileList -if __name__ == "__main__": - import glob - import sys - from PyMca5.PyMca import EDFStack - from PyMca5.PyMca import ArraySave - import getopt - _logger.setLevel(logging.DEBUG) - options = '' - longoptions = ['cfg=', 'outdir=', - 'tif=', #'listfile=', - 'filepattern=', 'begin=', 'end=', 'increment=', - "outfileroot="] - try: - opts, args = getopt.getopt( - sys.argv[1:], - options, - longoptions) - except Exception: - _logger.error(sys.exc_info()[1]) - sys.exit(1) - fileRoot = "" - outputDir = None - fileindex = 0 - filepattern=None - begin = None - end = None - increment=None - tif=0 - for opt, arg in opts: - if opt in ('--cfg'): - configurationFile = arg - elif opt in '--begin': - if "," in arg: - begin = [int(x) for x in arg.split(",")] - else: - begin = [int(arg)] - elif opt in '--end': - if "," in arg: - end = [int(x) for x in arg.split(",")] - else: - end = int(arg) - elif opt in '--increment': - if "," in arg: - increment = [int(x) for x in arg.split(",")] - else: - increment = int(arg) - elif opt in '--filepattern': - filepattern = arg.replace('"', '') - filepattern = filepattern.replace("'", "") - elif opt in '--outdir': - outputDir = arg - elif opt in '--outfileroot': - fileRoot = arg - elif opt in ['--tif', '--tiff']: - tif = int(arg) - if filepattern is not None: - if (begin is None) or (end is None): - raise ValueError(\ - "A file pattern needs at least a set of begin and end indices") - if filepattern is not None: - fileList = getFileListFromPattern(filepattern, begin, end, increment=increment) - else: - fileList = args - if len(fileList): - dataStack = EDFStack.EDFStack(fileList, dtype=numpy.float32) + +def main(args): + if args.filepattern is not None: + if args.begin is None or args.end is None: + raise ValueError( + "A file pattern needs at least a set of begin and end indices" + ) + fileList = getFileListFromPattern( + args.filepattern, + args.begin, + args.end, + increment=args.increment, + ) else: - print("OPTIONS:", longoptions) - sys.exit(0) - if outputDir is None: + fileList = args.files + + if not fileList: + print("No input files provided") + return 0 + + dataStack = EDFStack.EDFStack(fileList, dtype=numpy.float32) + + if args.outdir is None: print("RESULTS WILL NOT BE SAVED: No output directory specified") - t0 = time.time() + worker = StackROIBatch() - worker.setConfigurationFile(configurationFile) + worker.setConfigurationFile(args.configurationFile) result = worker.batchROIMultipleSpectra(y=dataStack) - if outputDir is not None: - imageNames = result['names'] - images = result['images'] - nImages = images.shape[0] - - if fileRoot in [None, ""]: - fileRoot = "images" - if not os.path.exists(outputDir): - os.mkdir(outputDir) - imagesDir = os.path.join(outputDir, "IMAGES") - if not os.path.exists(imagesDir): - os.mkdir(imagesDir) - imageList = [None] * (nImages) - fileImageNames = [None] * (nImages) - j = 0 - for i in range(nImages): - name = imageNames[i].replace(" ", "-") - fileImageNames[j] = name - imageList[j] = images[i] - j += 1 - fileName = os.path.join(imagesDir, fileRoot+".edf") - ArraySave.save2DArrayListAsEDF(imageList, fileName, - labels=fileImageNames) - fileName = os.path.join(imagesDir, fileRoot+".csv") - ArraySave.save2DArrayListAsASCII(imageList, fileName, csv=True, - labels=fileImageNames) - if tif: - i = 0 - for i in range(len(fileImageNames)): - label = fileImageNames[i] - fileName = os.path.join(imagesDir, - fileRoot + fileImageNames[i] + ".tif") - ArraySave.save2DArrayListAsMonochromaticTiff([imageList[i]], - fileName, - labels=[label], - dtype=numpy.float32) + + if args.outdir is None: + print("No output directory provided") + return 0 + + imageNames = result["names"] + images = result["images"] + nImages = images.shape[0] + + if not fileRoot: + fileRoot = "images" + + os.makedirs(args.outdir, exist_ok=True) + imagesDir = os.path.join(args.outdir, "IMAGES") + os.makedirs(imagesDir, exist_ok=True) + + imageList = [] + fileImageNames = [] + + for i in range(nImages): + name = imageNames[i].replace(" ", "-") + fileImageNames.append(name) + imageList.append(images[i]) + + # Save EDF + fileName = os.path.join(imagesDir, fileRoot + ".edf") + ArraySave.save2DArrayListAsEDF( + imageList, + fileName, + labels=fileImageNames, + ) + + # Save CSV + fileName = os.path.join(imagesDir, fileRoot + ".csv") + ArraySave.save2DArrayListAsASCII( + imageList, + fileName, + csv=True, + labels=fileImageNames, + ) + + # Optional TIFF + if args.tif: + for i, label in enumerate(fileImageNames): + fileName = os.path.join( + imagesDir, + fileRoot + label + ".tif" + ) + ArraySave.save2DArrayListAsMonochromaticTiff( + [imageList[i]], + fileName, + labels=[label], + dtype=numpy.float32, + ) + + return 0 + + +def build_parser(): + parser = CliUtils.create_parser(description="Stack ROI Batch Processing") + + parser.add_argument("--cfg", type=str, dest="configurationFile") + parser.add_argument("--outdir", type=str, default=None) + parser.add_argument("--outfileroot", type=str, dest="fileRoot", default=None) + + parser.add_argument("--filepattern", type=str, default=None, help="File pattern") + parser.add_argument("--begin", type=CliUtils.int_or_list, default=None, help="Begin index/indices, comma-separated") + parser.add_argument("--end", type=CliUtils.int_or_list, default=None, help="End index/indices, comma-separated") + parser.add_argument("--increment", type=CliUtils.int_or_list, default=None, help="Increment(s), comma-separated") + + parser.add_argument("--tif", type=int, default=0) + + parser.add_argument("files", nargs="*") + + return parser + + +if __name__ == "__main__": + exit_code = CliUtils.cli_main(main, build_parser(), loggers=(_logger,)) + sys.exit(exit_code) diff --git a/src/PyMca5/PyMcaCore/LoggingLevel.py b/src/PyMca5/PyMcaCore/LoggingLevel.py deleted file mode 100644 index 4f47f9833..000000000 --- a/src/PyMca5/PyMcaCore/LoggingLevel.py +++ /dev/null @@ -1,89 +0,0 @@ - -#!/usr/bin/env python -#/*########################################################################## -# Copyright (C) 2018-2022 European Synchrotron Radiation Facility -# -# This file is part of the PyMca X-ray Fluorescence Toolkit developed at -# the ESRF. -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -#############################################################################*/ -"""Module for parsing command line options related to the logging level.""" -__author__ = "P. Knobel" -__license__ = "MIT" -__copyright__ = "European Synchrotron Radiation Facility, Grenoble, France" - -import logging - - -DEFAULT_LOGGING_LEVEL = logging.WARNING - - -def getLoggingLevel(opts): - """Find logging level from the output of `getopt.getopt()`. - This level can be specified via one of two long options: - --debug or --logging. If both are specified, --logging overrules - --debug. - - When specifying the level with --logging, the level can be - specified explicitly as a string (debug, info, warning, error, critical), - or as an integer in the range 0--4, in increasing order of verbosity - (0 is "critical", 4 is "debug"). - - The option --debug only allows to chose between the default logging level - (--debug=0) or debugging mode with maximum verbosity (--debug=1). - - :param opts: Command line options as a list of 2-tuples of strings - (e.g. ``[('--logging', 'debug'), ('--cfg', 'config.ini')]``). - :returns: logging level - :rtype: int""" - logging_level = None - for opt, arg in opts: - if opt == '--logging': - levels_dict = { - # Explicit args - 'debug': logging.DEBUG, - 'info': logging.INFO, - 'warning': logging.WARNING, - 'error': logging.ERROR, - 'critical': logging.CRITICAL, - # int args sorted by increasing verbosity - '0': logging.CRITICAL, - '1': logging.ERROR, - '2': logging.WARNING, - '3': logging.INFO, - '4': logging.DEBUG} - - logging_level = levels_dict.get(arg.lower()) - if logging_level is None: - raise ValueError("Unknown logging level <%s>" % arg) - # if --logging is specified, ignore --debug - return logging_level - if opt == '--debug': - # simpler option to choose between the default logging or DEBUG - if arg.lower() in ["0", 0, "false"]: - logging_level = DEFAULT_LOGGING_LEVEL - elif arg.lower() in ["1", 1, "true"]: - logging_level = logging.DEBUG - else: - raise ValueError("Incorrect debug parameter <%s> (should be 0 or 1)" % arg) - if logging_level is None: - return DEFAULT_LOGGING_LEVEL - return logging_level diff --git a/src/PyMca5/PyMcaCore/McaStackView.py b/src/PyMca5/PyMcaCore/McaStackView.py index b869a5cb5..33afba9aa 100644 --- a/src/PyMca5/PyMcaCore/McaStackView.py +++ b/src/PyMca5/PyMcaCore/McaStackView.py @@ -27,7 +27,6 @@ # #############################################################################*/ __author__ = "Wout De Nolf" -__contact__ = "wout.de_nolf@esrf.eu" __license__ = "MIT" __copyright__ = "European Synchrotron Radiation Facility, Grenoble, France" diff --git a/src/PyMca5/PyMcaCore/StackROIBatch.py b/src/PyMca5/PyMcaCore/StackROIBatch.py index 2b88f5229..6635bd900 100644 --- a/src/PyMca5/PyMcaCore/StackROIBatch.py +++ b/src/PyMca5/PyMcaCore/StackROIBatch.py @@ -33,14 +33,18 @@ __doc__ = """ Module to calculate a set of ROIs on a stack of data. """ + import os -import numpy +import sys import logging import copy + +import numpy + from PyMca5.PyMcaIO import ConfigDict from PyMca5.PyMcaIO.OutputBuffer import OutputBuffer as OutputBufferBase from PyMca5.PyMcaCore import McaStackView - +from PyMca5.PyMcaMisc import CliUtils _logger = logging.getLogger(__name__) @@ -347,113 +351,85 @@ def prepareDataStack(fileList): return dataStack -def main(): - import glob - import sys - import getopt - _logger.setLevel(logging.DEBUG) - options = '' - longoptions = ['cfg=', 'outdir=', - 'tif=', 'edf=', 'csv=', 'h5=', 'dat=', - 'filepattern=', 'begin=', 'end=', 'increment=', - 'outroot=', 'outentry=', 'outprocess=', - 'overwrite=', 'multipage='] - try: - opts, args = getopt.getopt( - sys.argv[1:], - options, - longoptions) - except Exception: - _logger.error(sys.exc_info()[1]) - sys.exit(1) - outputDir = None - outputRoot = "" - fileEntry = "" - fileProcess = "" - filepattern = None - begin = None - end = None - increment = None - tif = 0 - edf = 0 - csv = 0 - h5 = 1 - dat = 0 - overwrite = 1 - multipage = 0 - for opt, arg in opts: - if opt in ('--cfg'): - configurationFile = arg - elif opt in '--begin': - if "," in arg: - begin = [int(x) for x in arg.split(",")] - else: - begin = [int(arg)] - elif opt in '--end': - if "," in arg: - end = [int(x) for x in arg.split(",")] - else: - end = int(arg) - elif opt in '--increment': - if "," in arg: - increment = [int(x) for x in arg.split(",")] - else: - increment = int(arg) - elif opt in '--filepattern': - filepattern = arg.replace('"', '') - filepattern = filepattern.replace("'", "") - elif opt in '--outdir': - outputDir = arg - elif opt == '--outroot': - outputRoot = arg - elif opt == '--outentry': - fileEntry = arg - elif opt == '--outprocess': - fileProcess = arg - elif opt in ('--tif', '--tiff'): - tif = int(arg) - elif opt == '--edf': - edf = int(arg) - elif opt == '--csv': - csv = int(arg) - elif opt == '--h5': - h5 = int(arg) - elif opt == '--dat': - dat = int(arg) - elif opt == '--overwrite': - overwrite = int(arg) - elif opt == '--multipage': - multipage = int(arg) - if filepattern is not None: - if (begin is None) or (end is None): +def main(args): + if args.filepattern is not None: + if args.begin is None or args.end is None: raise ValueError( - "A file pattern needs at least a set of begin and end indices") - if filepattern is not None: - fileList = getFileListFromPattern(filepattern, begin, end, - increment=increment) - else: - fileList = args - if len(fileList): - dataStack = prepareDataStack(fileList) + "A file pattern needs at least a set of begin and end indices" + ) + fileList = getFileListFromPattern( + args.filepattern, + args.begin, + args.end, + increment=args.increment + ) else: - print("OPTIONS:", longoptions) - sys.exit(0) - if outputDir is None: + fileList = args.files + + if not fileList: + print("No input files provided") + return 0 + + dataStack = prepareDataStack(fileList) + + if args.outdir is None: print("RESULTS WILL NOT BE SAVED: No output directory specified") + worker = StackROIBatch() - worker.setConfigurationFile(configurationFile) - outbuffer = OutputBuffer(outputDir=outputDir, - outputRoot=outputRoot, - fileEntry=fileEntry, - fileProcess=fileProcess, - tif=tif, edf=edf, csv=csv, - h5=h5, dat=dat, - multipage=multipage, - overwrite=overwrite) + worker.setConfigurationFile(args.configurationFile) + + outbuffer = OutputBuffer( + outputDir=args.outdir, + outputRoot=args.outroot, + fileEntry=args.outentry, + fileProcess=args.outprocess, + tif=args.tif, + edf=args.edf, + csv=args.csv, + h5=args.h5, + dat=args.dat, + multipage=args.multipage, + overwrite=args.overwrite, + ) + with outbuffer.saveContext(): - worker.batchROIMultipleSpectra(y=dataStack, - outbuffer=outbuffer) + worker.batchROIMultipleSpectra( + y=dataStack, + outbuffer=outbuffer + ) + + return 0 + + +def build_parser(): + parser = CliUtils.create_parser(description="Stack ROI Batch Processing") + + parser.add_argument("--cfg", type=str, dest="configurationFile") + + parser.add_argument("--outdir", type=str, default=None) + parser.add_argument("--outroot", type=str, default=None) + parser.add_argument("--outentry", type=str, default=None) + parser.add_argument("--outprocess", type=str, default=None) + + parser.add_argument("--filepattern", type=str, default=None, help="File pattern") + parser.add_argument("--begin", type=CliUtils.int_or_list, default=None, help="Begin index/indices, comma-separated") + parser.add_argument("--end", type=CliUtils.int_or_list, help="End index/indices, comma-separated") + parser.add_argument("--increment", type=CliUtils.int_or_list, help="Increment(s), comma-separated") + + parser.add_argument("--tif", type=int, default=0) + parser.add_argument("--edf", type=int, default=0) + parser.add_argument("--csv", type=int, default=0) + parser.add_argument("--h5", type=int, default=1) + parser.add_argument("--dat", type=int, default=0) + + parser.add_argument("--overwrite", type=int, default=1) + parser.add_argument("--multipage", type=int, default=0) + + parser.add_argument("files", nargs="*", help="Input files") + + return parser + if __name__ == "__main__": - logging.basicConfig(level=logging.INFO) - main() + exit_code = CliUtils.cli_main(main, build_parser(), loggers=(_logger,)) + sys.exit(exit_code) diff --git a/src/PyMca5/PyMcaCore/XiaCorrect.py b/src/PyMca5/PyMcaCore/XiaCorrect.py index 855853f83..fe481129b 100644 --- a/src/PyMca5/PyMcaCore/XiaCorrect.py +++ b/src/PyMca5/PyMcaCore/XiaCorrect.py @@ -27,11 +27,22 @@ __contact__ = "sole@esrf.fr" __license__ = "MIT" __copyright__ = "European Synchrotron Radiation Facility, Grenoble, France" -from . import XiaEdf -import sys + +from PyMca5.PyMcaGui import PyMcaAppInit + +if __name__== '__main__': + PyMcaAppInit.init_before_app_import() + import os +import sys import time +from PyMca5.PyMcaGui import PyMcaQt as qt +from PyMca5.PyMcaGui.pymca import XiaCorrectWizard +from PyMca5.PyMcaMisc import CliUtils +from . import XiaEdf + + __version__="$Revision: 1.11 $" def defaultErrorCB(message): @@ -147,7 +158,7 @@ def correctFiles(xiafiles, deadtime=1, livetime=0, sums=None, avgflag=0, outdir= file.appendPrefix(outname) name= file.get() - if sums is not None: + if sums: err= xia.sum(sums, deadtime, livetime, avgflag) file.setType("sum", -1) else: @@ -182,7 +193,7 @@ def correctFiles(xiafiles, deadtime=1, livetime=0, sums=None, avgflag=0, outdir= file.setDirectory(outdir) file.appendPrefix(outname) - if sums is None: + if not sums: for file in group[:-1]: det= file.getDetector() @@ -240,181 +251,148 @@ def correctFiles(xiafiles, deadtime=1, livetime=0, sums=None, avgflag=0, outdir= log_cb("\n") -def parseArguments(): - import getopt, os.path - - prog= os.path.basename(sys.argv[0]) - - long = ["help", "input=", "output=", "force", "verbose", "deadtime", "livetime", "sum=", "avg", "name=", "parsing"] - short= ["h", "i:", "o:", "f", "v", "d", "l", "s:", "a", "n:", "p"] - - try: - opts, args= getopt.getopt(sys.argv[1:], " ".join(short), long) - except getopt.error: - print("XiaCorrect ERROR: Cannot parse command line arguments") - print("\t%s" % sys.exc_info()[1]) - sys.exit(0) - - parsing= 0 - options= {"input": [], "files": [], "output": None, "force": 0, "name": "corr", - "verbose": 0, "deadtime": 0, "livetime": 0, "sums": None, "avgflag": 0, "parsing": 0} - - for opt, arg in opts: - if opt in ("-h", "--help"): - printHelp() - sys.exit(0) - if opt in ("-i", "--input"): - options["input"].append(os.path.normpath(arg)) - if opt in ("-o", "--output"): - options["output"]= os.path.normpath(arg) - if opt in ("-f", "--force"): - options["force"]= 1 - if opt in ("-v", "--verbose"): - options["verbose"]= 1 - if opt in ("-d", "--deadtime"): - options["deadtime"]= 1 - if opt in ("-l", "--livetime"): - options["livetime"]= 1 - if opt in ("-n", "--name"): - options["name"]= str(arg) - if opt in ("-s", "--sum"): - if options["sums"] is None: - options["sums"]= [] + +def main(args): + # If no CLI arguments -> GUI mode + if ( + not args.input + and not args.files + and not args.deadtime + and not args.livetime + and not args.sums + ): + app = qt.QApplication([]) + PyMcaAppInit.init_before_app_start(qt_app=app, cli_args=args) + + wid = XiaCorrectWizard.XiaCorrectWizard() + + # Auto-close Qt for tests + if args.cli_test: + qt.QTimer.singleShot(0, wid.close) + + ret = wid.exec() + + if ret == qt.QDialog.Accepted: + options = wid.get() + files = parseFiles(options["files"], options["verbose"]) + if files is not None: + correctFiles( + files, + options["deadtime"], + options["livetime"], + options["sums"], + options["avgflag"], + options["output"], + options["name"], + options["force"], + options["verbose"], + ) + + return 0 + + options = { + "input": args.input or [], + "files": [], + "output": args.output, + "force": int(args.force), + "name": args.name, + "verbose": int(args.verbose), + "deadtime": int(args.deadtime), + "livetime": int(args.livetime), + "sums": [], + "avgflag": int(args.avgflag), + "parsing": int(args.parsing), + } + + # Handle sums + if args.sums: + for s in args.sums: try: - ssum= [ int(det) for det in arg.split(",") ] - if ssum[0]==-1: - ssum= [] + ssum = [int(det) for det in s.split(",")] + if ssum and ssum[0] == -1: + ssum = [] options["sums"].append(ssum) except Exception: print("XiaCorrect ERROR: Cannot parse sum detectors") - print("\t%s"%arg) - sys.exit(0) - if opt in ("-a", "--avg"): - options["avgflag"]= 1 - if opt in ("-p", "--parsing"): - options["parsing"]= 1 - + print("\t%s" % s) + return 0 + # Expand input directories for iinput in options["input"]: if not os.path.isdir(iinput): - print("XiaCorrect WARNING: Input directory <%s> is not valid"%\ - iinput) + print(f"XiaCorrect WARNING: Input directory <{iinput}> is not valid") + continue - files= [ os.path.join(iinput, file) for file in os.listdir(iinput) ] - if not len(files): - print("XiaCorrect WARNING: Input directory <%s> is empty"%\ - (iinput, prog)) + files = [os.path.join(iinput, f) for f in os.listdir(iinput)] + if not files: + print(f"XiaCorrect WARNING: Input directory <{iinput}> is empty") else: - options["files"]+= files + options["files"] += files - if len(args): - options["files"]+= args + # Add explicit files + options["files"] += args.files - if not len(options["files"]): + if not options["files"]: print("XiaCorrect ERROR: No input datafiles") - sys.exit(0) + return 0 + # Validation if not options["parsing"]: - if not options["deadtime"] and not options["livetime"] and options["sums"] is None: + if not options["deadtime"] and not options["livetime"] and not options["sums"]: print("XiaCorrect ERROR: Must have at least deadtime, livetime or sum options") - sys.exit(0) - - if options["output"] is not None: - if not os.path.isdir(options["output"]): - print("XiaCorrect ERROR: output directory is not valid") - sys.exit(0) - - return options - -def printHelp(): - prog= os.path.basename(sys.argv[0]) - msg= """ - -%s [-h] [-v] [-f] [-d] [-l] [-a] [-s ] [-i ] [-o ] [] - -Options: - [-h]/[--help] - Print help message - [-v]/[--verbose] - Switch ON verbose mode - [-f]/[--force] - Force writing output files if they already exists - [-d]/[--deadtime] - Perform deadtime correction - [-l]/[--livetime] - Perform livetime normalization - [-s]/[--sum] - Sum given detectors. if detector list is set to (-1), - all detectors are used: - %s -s 2,4,8 --> will sum detectors 2,4 and 8 - %s -s -1 --> will sum ALL detectors - Several sums can be added: - -s 2,4,6,7 -s 8,9,10,11 - [-a]/[--avg] - Sum(s) are averaged. - Need <-s> to specify list of detectors: - -s 2,3,4 -a --> will average detectors 2,3 and 4 - [-i]/[--input] - Specify input directory: all files in this directory - which appears to be xia edf files are processed. - Several [-i] options can be added: - %s -d -i /tmp -i /data/opidXX - [-o]/[--output] - Specify output directories. If not specified, output - files are saved in the same place as input file. - [-n]/[--name] - String to be appended to prefix for output filename. - Default is \"corr\". - [] - Specify one or several input files. Wildcards can be used: - %s -l file1.edf file2.edf /tmp/test*.edf - -Minimum options to work: - [-l] , [-d] or [-s] - [-i input] or - -"""%(prog, prog, prog, prog, prog) - print(msg) - - -def mainCommandLine(): - options= parseArguments() - files= parseFiles(options["files"], options["verbose"]) - if files is not None: - if options["parsing"]: - for group in files: - print("FileGroup:") - for file in group: - print(" - ", file.get()) - else: - correctFiles(files, options["deadtime"], options["livetime"], options["sums"], options["avgflag"], \ - options["output"], options["name"], options["force"], options["verbose"]) - -def mainGUI(app=None): - from PyMca5.PyMcaGui import PyMcaQt as qt - from PyMca5.PyMcaGui.pymca import XiaCorrectWizard - - if app is None: - app= qt.QApplication(sys.argv) + return 0 - wid= XiaCorrectWizard.XiaCorrectWizard() - ret= wid.exec() + if options["output"] is not None and not os.path.isdir(options["output"]): + print("XiaCorrect ERROR: output directory is not valid") + return 0 - if ret==qt.QDialog.Accepted: - options= wid.get() - files= parseFiles(options["files"], options["verbose"]) - if files is not None: - correctFiles(files, options["deadtime"], options["livetime"], options["sums"], options["avgflag"], \ - options["output"], options["name"], options["force"], options["verbose"]) + # Execute + files = parseFiles(options["files"], options["verbose"]) + if files is None: + return 0 - - -if __name__=="__main__": - import sys - - if len(sys.argv)==1: - mainGUI() + if options["parsing"]: + for group in files: + print("FileGroup:") + for file in group: + print(" - ", file.get()) else: - mainCommandLine() - + correctFiles( + files, + options["deadtime"], + options["livetime"], + options["sums"], + options["avgflag"], + options["output"], + options["name"], + options["force"], + options["verbose"], + ) + + return 0 + + +def build_parser(): + parser = CliUtils.create_parser(description="Xia Correct Tool", add_qt_options=True) + + parser.add_argument("-i", "--input", action="append", help="Input directory (can be used multiple times)") + parser.add_argument("-o", "--output", help="Output directory") + parser.add_argument("-f", "--force", action="store_true", help="Force writing output files if they already exists") + parser.add_argument("-v", "--verbose", action="store_true") + parser.add_argument("-d", "--deadtime", action="store_true", help="Perform deadtime correction") + parser.add_argument("-l", "--livetime", action="store_true", help="Perform livetime normalization") + parser.add_argument("-s", "--sum", action="append", dest="sums", help="Comma separated detector list") + parser.add_argument("-a", "--avg", dest="avgflag", action="store_true") + parser.add_argument("-n", "--name", default="corr", help="String to be appended to prefix for output filename") + parser.add_argument("-p", "--parsing", action="store_true") + + parser.add_argument("files", nargs="*", help="Input files") + + return parser + + +if __name__ == "__main__": + PyMcaAppInit.init_before_app_create() + exit_code = CliUtils.cli_main(main, build_parser()) + sys.exit(exit_code) diff --git a/src/PyMca5/PyMcaGui/PyMcaAppInit.py b/src/PyMca5/PyMcaGui/PyMcaAppInit.py new file mode 100644 index 000000000..a47bfa686 --- /dev/null +++ b/src/PyMca5/PyMcaGui/PyMcaAppInit.py @@ -0,0 +1,257 @@ +# /*########################################################################## +# +# The PyMca X-Ray Fluorescence Toolkit +# +# Copyright (c) 2004-2023 European Synchrotron Radiation Facility +# +# This file is part of the PyMca X-ray Fluorescence Toolkit developed at +# the ESRF. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +#############################################################################*/ +"""Common PyMca Appliction utilities to be used like this + +.. code-block:: python + + # Top of the file + from PyMca5.PyMcaGui import PyMcaAppInit + + if __name__== '__main__': + PyMcaAppInit.init_before_app_import() + + ... # Other imports and definitions + + + def main(args): + app = qt.QApplication([]) + PyMcaAppInit.init_before_app_start(qt_app=app, cli_args=args) + + ... # Main widget + + widget.show() + + # Auto-close Qt application for tests + if args.cli_test: + qt.QTimer.singleShot(0, app.quit) + + return app.exec() + + + if __name__ == "__main__": + PyMcaAppInit.init_before_app_create() + ... +""" + +__author__ = "Wout De Nolf" +__license__ = "MIT" +__copyright__ = "European Synchrotron Radiation Facility, Grenoble, France" + +import os +import sys +import argparse +import logging + +from PyMca5.PyMcaMisc.LoggingUtils import parse_log_level + +_logger = logging.getLogger(__name__) + + +def init_before_app_import(qt=True, mp=True, mpl=True, logging=True): + """ + Call this before importing application dependencies. + """ + cli_args = _silent_pre_cli_app() + + if mp: + _init_multiprocessing() + + if logging: + _init_logging_from_cli(cli_args) + + if qt: + _init_qt_binding_from_cli(cli_args) + + if mpl: + _init_matplotlib() + + +def init_before_app_create(qt=True, hdf5=True): + """ + Call this after importing application dependencies and before instantiating the application. + """ + if hdf5: + _init_hdf5() + + if qt: + _init_qt_before() + + +def init_before_app_start(qt_app=None, cli_args=None): + """ + Call this after instantiating and before starting the application. + """ + if qt_app: + _init_qt_after(qt_app, cli_args) + + +def _silent_pre_cli_app(): + """ + CLI argument needed before creating the CLI parser. + """ + try: + parser = argparse.ArgumentParser(description="PyMca pre-import CLI", add_help=False) + parser.add_argument("--binding", type=str, default=None) + parser.add_argument("--qt", type=str, default=None) + parser.add_argument("--logging", dest="log_level", type=parse_log_level, default=None) + args, _ = parser.parse_known_args() + except Exception: + args = argparse.Namespace(binding=None, qt=None, logging=None) + return args + + +def _init_logging_from_cli(cli_args): + """ + Call this to log imports. + """ + if cli_args.log_level: + logging.basicConfig(level=cli_args.log_level) + + +def _init_qt_binding_from_cli(cli_args): + """ + Call this before importing PyMcaQt. + """ + if cli_args.binding: + _init_qt_binding(cli_args.binding) + elif cli_args.qt: + _init_qt_version(cli_args.qt) + + +def _init_qt_binding(binding): + """ + Call this before importing PyMcaQt. + """ + binding = binding.lower() + if binding == "pyqt5": + import PyQt5.QtCore + elif binding == "pyside2": + import PySide2.QtCore + elif binding == "pyside6": + import PySide6.QtCore + elif binding == "pyqt6": + import PyQt6.QtCore + else: + raise ValueError(f"Unsupported Qt binding {binding!r}") + + +def _init_qt_version(qtversion): + if qtversion == "3": + raise NotImplementedError("Qt3 is no longer supported") + elif qtversion == "4": + raise NotImplementedError("Qt4 is no longer supported") + elif qtversion == "5": + try: + import PyQt5.QtCore + except ImportError: + import PySide2.QtCore + elif qtversion == "6": + import PySide6.QtCore + else: + raise ValueError(f"Unsupported Qt version {qtversion!r}") + + +def _init_multiprocessing(): + """ + Call this as soon as possible. + """ + if getattr(sys, "frozen", False): + try: + import multiprocessing + + multiprocessing.freeze_support() + except Exception: + _logger.debug("Failed to import multiprocessing or enable freeze support") + + +def _init_matplotlib(): + """ + Call this before importing matplotlib. + """ + try: + # try to import silx prior to importing matplotlib to prevent + # unnecessary warning + import silx.gui.plot + except Exception: + _logger.debug("Failed to import silx.gui.plot") + + +def _init_hdf5(): + """ + Call this before importing h5py. + """ + os.environ["HDF5_USE_FILE_LOCKING"] = "FALSE" + _logger.info("HDF5_USE_FILE_LOCKING set to %s", os.environ["HDF5_USE_FILE_LOCKING"]) + + try: + import hdf5plugin + except Exception: + _logger.info("Failed to import hdf5plugin") + + +def _init_qt_before(): + """ + Call this before instantiating the Qt application. + """ + pass + + +def _init_qt_after(app, cli_args): + """ + Call this after instantiating the Qt application. + """ + from PyMca5.PyMcaGui import PyMcaQt as qt + + if cli_args and cli_args.binding: + if cli_args.binding.lower() != qt.BINDING.lower(): + _logger.warning("Qt binding is %r instead of %r", qt.BINDING, cli_args.binding) + + if sys.platform not in ["win32", "darwin"]: + # Some themes of Ubuntu 16.04 give black tool tips on black background + try: + _ttp = app.palette() + _ttText = _ttp.color(qt.QPalette.ToolTipText).name() + _ttBorder = _ttText + _ttBase = _ttp.color(qt.QPalette.ToolTipBase).name() + app.setStyleSheet("QToolTip { color: %s; background-color: %s; border: 1px solid %s; }" % (_ttText, _ttBase, _ttBorder)) + except Exception: + app.setStyleSheet("QToolTip { color: #000000; background-color: #fff0cd; border: 1px solid black; }") + + if cli_args.nativefiledialogs is not None: + from PyMca5.PyMcaCore import PyMcaDirs + + PyMcaDirs.nativeFileDialogs = bool(cli_args.nativefiledialogs) + + # This is the default behavior unless app.setQuitOnLastWindowClosed(False). + # So do we need this explicitly? + app.lastWindowClosed.connect(app.quit) + + if not cli_args.cli_test: + # From now on errors are shown in Qt dialogs. + sys.excepthook = qt.exceptionHandler diff --git a/src/PyMca5/PyMcaGui/physics/xrf/ConcentrationsWidget.py b/src/PyMca5/PyMcaGui/physics/xrf/ConcentrationsWidget.py index da86d7b18..fdd262957 100644 --- a/src/PyMca5/PyMcaGui/physics/xrf/ConcentrationsWidget.py +++ b/src/PyMca5/PyMcaGui/physics/xrf/ConcentrationsWidget.py @@ -30,10 +30,15 @@ __contact__ = "sole@esrf.fr" __license__ = "MIT" __copyright__ = "European Synchrotron Radiation Facility, Grenoble, France" + +from PyMca5.PyMcaGui import PyMcaAppInit + +if __name__== '__main__': + PyMcaAppInit.init_before_app_import() + import sys +import copy import logging -_logger = logging.getLogger(__name__) -_logger.debug("ConcentrationsWidget is in debug mode") from PyMca5.PyMcaGui import PyMcaQt as qt @@ -46,6 +51,8 @@ XRFMC_FLAG = True +_logger = logging.getLogger(__name__) + try: from PyMca5.PyMcaGui.misc.TableWidget import TableWidget QTable = TableWidget @@ -58,7 +65,9 @@ from PyMca5.PyMcaGui.misc import CalculationThread from PyMca5.PyMcaPhysics.xrf import ConcentrationsTool from PyMca5.PyMcaPhysics.xrf import Elements -import time +from PyMca5.PyMcaIO import ConfigDict +from PyMca5.PyMcaMisc import CliUtils + class Concentrations(qt.QWidget): sigConcentrationsSignal = qt.pyqtSignal(object) @@ -878,50 +887,69 @@ def _mySlot(self, ddict): self.setCurrentText(current) +def main(args): + app = qt.QApplication([]) + PyMcaAppInit.init_before_app_start(qt_app=app, cli_args=args) + + demo = Concentrations() + config = demo.getParameters() + + # Update configuration from CLI arguments + if args.flux is not None: + config["flux"] = args.flux + if args.area is not None: + config["area"] = args.area + if args.time is not None: + config["time"] = args.time + if args.distance is not None: + config["distance"] = args.distance + if args.attenuators is not None: + config["useattenuators"] = int(args.attenuators) + if args.usematrix is not None: + config["usematrix"] = int(args.usematrix) + + demo.setParameters(config) + + # Process fit result files + for filename in args.files: + d = ConfigDict.ConfigDict() + d.read(filename) + + for material in d["result"]["config"]["materials"].keys(): + Elements.Material[material] = copy.deepcopy( + d["result"]["config"]["materials"][material] + ) + + demo.processFitResult( + fitresult=d, + elementsfrommatrix=False + ) + + demo.show() + + # Auto-close Qt for tests + if args.cli_test: + qt.QTimer.singleShot(0, app.quit) + + return app.exec() + + +def build_parser(): + parser = CliUtils.create_parser(description="Concentrations tool", add_qt_options=True) + + parser.add_argument("--flux", type=float) + parser.add_argument("--time", type=float) + parser.add_argument("--area", type=float) + parser.add_argument("--distance", type=float) + parser.add_argument("--attenuators", type=float) + parser.add_argument("--usematrix", type=float) + + parser.add_argument("files", nargs="*", help="Fit result files") + + return parser + + if __name__ == "__main__": - import getopt - import copy - # import sys - # from PyMca5 import ConcentrationsTool - from PyMca5.PyMcaIO import ConfigDict - if len(sys.argv) > 1: - options = '' - longoptions = ['flux=', 'time=', 'area=', 'distance=', 'attenuators=', - 'usematrix='] - - opts, args = getopt.getopt( - sys.argv[1:], - options, - longoptions) - app = qt.QApplication([]) - app.lastWindowClosed.connect(app.quit) - demo = Concentrations() - config = demo.getParameters() - for opt, arg in opts: - if opt in ('--flux'): - config['flux'] = float(arg) - elif opt in ('--area'): - config['area'] = float(arg) - elif opt in ('--time'): - config['time'] = float(arg) - elif opt in ('--distance'): - config['distance'] = float(arg) - elif opt in ('--attenuators'): - config['useattenuators'] = int(float(arg)) - elif opt in ('--usematrix'): - config['usematrix'] = int(float(arg)) - demo.setParameters(config) - filelist = args - for file in filelist: - d = ConfigDict.ConfigDict() - d.read(file) - for material in d['result']['config']['materials'].keys(): - Elements.Material[material] = copy.deepcopy(d['result']['config']['materials'][material]) - demo.processFitResult(fitresult=d, elementsfrommatrix=False) - demo.show() - ret = app.exec() - app = None - sys.exit(ret) - else: - print("Usage:") - print("ConcentrationsWidget.py [--flux=xxxx --area=xxxx] fitresultfile") + PyMcaAppInit.init_before_app_create() + exit_code = CliUtils.cli_main(main, build_parser(), loggers=(_logger,)) + sys.exit(exit_code) diff --git a/src/PyMca5/PyMcaGui/physics/xrf/ElementsInfo.py b/src/PyMca5/PyMcaGui/physics/xrf/ElementsInfo.py index 77d29f621..9da5cff9e 100644 --- a/src/PyMca5/PyMcaGui/physics/xrf/ElementsInfo.py +++ b/src/PyMca5/PyMcaGui/physics/xrf/ElementsInfo.py @@ -30,20 +30,20 @@ __contact__ = "sole@esrf.fr" __license__ = "MIT" __copyright__ = "European Synchrotron Radiation Facility, Grenoble, France" + +from PyMca5.PyMcaGui import PyMcaAppInit + +if __name__== '__main__': + PyMcaAppInit.init_before_app_import() + import sys import logging -if __name__== '__main__': - # avoid issues if some module or dependency tries to use multiprocessing in frozen binaries - if getattr(sys, "frozen", False): - try: - import multiprocessing - multiprocessing.freeze_support() - except Exception: - pass + from PyMca5.PyMcaGui import PyMcaQt as qt from PyMca5.PyMcaPhysics.xrf import ElementHtml from PyMca5.PyMcaPhysics.xrf import Elements from PyMca5.PyMcaGui.physics.xrf.QPeriodicTable import QPeriodicTable +from PyMca5.PyMcaMisc import CliUtils _logger = logging.getLogger(__name__) @@ -286,12 +286,28 @@ def focusOutEvent(self,event): qt.QApplication.instance().palette().color(qt.QPalette.Base)) self.sigFocusOut.emit() -def main(): - logging.basicConfig(level=logging.INFO) + +def main(args): app = qt.QApplication([]) - w= ElementsInfo() + PyMcaAppInit.init_before_app_start(qt_app=app, cli_args=args) + + w = ElementsInfo() w.show() - app.exec() + + # Auto-close Qt for tests + if args.cli_test: + qt.QTimer.singleShot(0, app.quit) + + return app.exec() + + +def build_parser(): + parser = CliUtils.create_parser(description="Elements Tool", add_qt_options=True) + + return parser + if __name__ == "__main__": - main() + PyMcaAppInit.init_before_app_create() + exit_code = CliUtils.cli_main(main, build_parser(), loggers=(_logger,)) + sys.exit(exit_code) diff --git a/src/PyMca5/PyMcaGui/physics/xrf/McaCalWidget.py b/src/PyMca5/PyMcaGui/physics/xrf/McaCalWidget.py index 5a17dfa80..9caad42c5 100644 --- a/src/PyMca5/PyMcaGui/physics/xrf/McaCalWidget.py +++ b/src/PyMca5/PyMcaGui/physics/xrf/McaCalWidget.py @@ -31,12 +31,19 @@ __license__ = "MIT" __copyright__ = "European Synchrotron Radiation Facility, Grenoble, France" +from PyMca5.PyMcaGui import PyMcaAppInit + +if __name__== '__main__': + PyMcaAppInit.init_before_app_import() + +import os import sys -import numpy -from numpy.linalg import inv as inverse import copy import logging +import numpy +from numpy.linalg import inv as inverse + from PyMca5.PyMcaGui import PyMcaQt as qt from PyMca5.PyMcaGui.plotting.PlotWidget import PlotWidget @@ -51,8 +58,12 @@ from PyMca5.PyMcaGui.plotting import PyMca_Icons IconDict = PyMca_Icons.IconDict from . import PeakTableWidget -if 0: - from PyMca5 import XRDPeakTableWidget +# from PyMca5 import XRDPeakTableWidget +from PyMca5.PyMcaIO import specfilewrapper as specfile +from PyMca5 import PyMcaDataDir +from PyMca5.PyMcaMisc import CliUtils + + _logger = logging.getLogger(__name__) LOW_HEIGHT_THRESHOLD = 660 @@ -1563,68 +1574,78 @@ def setOptions(self,options=['1','2','3']): def getCurrent(self): return self.currentIndex(),str(self.currentText()) -def test(x,y,legend): - app = qt.QApplication(args) - demo = McaCalWidget(x=x,y=y,modal=1,legend=legend) - ret=demo.exec() + +def main(args): + # Resolve input file + inputfile = args.inputfile + if inputfile is None: + inputfile = os.path.join( + PyMcaDataDir.PYMCA_DATA_DIR, + "XRFSpectrum.mca" + ) + + # Read spec file + sf = specfile.Specfile(inputfile) + + if args.scan is None: + scan = sf[len(sf) - 1] + else: + scan = sf.select(args.scan) + + nbmca = scan.nbmca() + mcadata = scan.mca(nbmca) + + y = numpy.array(mcadata).astype(numpy.float64) + x = numpy.arange(len(y)).astype(numpy.float64) + + # Qt application + app = qt.QApplication([]) + PyMcaAppInit.init_before_app_start(qt_app=app, cli_args=args) + + demo = McaCalWidget( + x=x, + y=y, + modal=1, + legend=inputfile + ) + + if args.cli_test: + qt.QTimer.singleShot(0, demo.close) + + ret = demo.exec() + if ret == qt.QDialog.Accepted: - ddict=demo.getDict() + ddict = demo.getDict() else: - ddict={} + ddict = {} + print(" output = ", ddict) + demo.close() del demo - #app.exec_loop() - -if __name__ == '__main__': - import os - import getopt - from PyMca5.PyMcaIO import specfilewrapper as specfile - options = 'f:s:o' - longoptions = ['file=','scan=','pkm=', - 'output=','linear=','strip=', - 'maxiter=','sumflag=','plotflag='] - opts, args = getopt.getopt( - sys.argv[1:], - options, - longoptions) - inputfile = None - scan = None - pkm = None - scankey = None - plotflag = 0 - strip = 1 - linear = 0 - for opt,arg in opts: - if opt in ('-f','--file'): - inputfile = arg - if opt in ('-s','--scan'): - scan = arg - if opt in ('--pkm'): - pkm = arg - if opt in ('--linear'): - linear = int(float(arg)) - if opt in ('--strip'): - strip = int(float(arg)) - if opt in ('--maxiter'): - maxiter = int(float(arg)) - if opt in ('--sum'): - sumflag = int(float(arg)) - if opt in ('--plotflag'): - plotflag = int(float(arg)) - if len(sys.argv) > 1: - inputfile = sys.argv[1] - if inputfile is None: - from PyMca5 import PyMcaDataDir - inputfile = os.path.join(PyMcaDataDir.PYMCA_DATA_DIR, - 'XRFSpectrum.mca') - sf=specfile.Specfile(inputfile) - if scankey is None: - scan=sf[len(sf) - 1] - else: - scan=sf.select(scankey) - nbmca=scan.nbmca() - mcadata=scan.mca(nbmca) - y=numpy.array(mcadata).astype(numpy.float64) - x=numpy.arange(len(y)).astype(numpy.float64) - test(x,y,inputfile) + + if args.cli_test: + qt.QTimer.singleShot(0, app.quit) + + return ret + + +def build_parser(): + parser = CliUtils.create_parser(description="MCA Calibration Tool", add_qt_options=True) + + parser.add_argument("-f", "--file", dest="inputfile", help="Input spec file") + parser.add_argument("-s", "--scan", help="Scan key to select") + parser.add_argument("--linear", type=float, default=0) + parser.add_argument("--strip", type=float, default=1) + parser.add_argument("--maxiter", type=float) + parser.add_argument("--sumflag", "--sum", dest="sumflag", type=float) + parser.add_argument("--plotflag", type=float, default=0) + parser.add_argument("--pkm", type=str) + + return parser + + +if __name__ == "__main__": + PyMcaAppInit.init_before_app_create() + exit_code = CliUtils.cli_main(main, build_parser(), loggers=(_logger,)) + sys.exit(exit_code) diff --git a/src/PyMca5/PyMcaGui/physics/xrf/PeakIdentifier.py b/src/PyMca5/PyMcaGui/physics/xrf/PeakIdentifier.py index d8c0c83ff..5bb39e7bb 100644 --- a/src/PyMca5/PyMcaGui/physics/xrf/PeakIdentifier.py +++ b/src/PyMca5/PyMcaGui/physics/xrf/PeakIdentifier.py @@ -30,19 +30,20 @@ __contact__ = "sole@esrf.fr" __license__ = "MIT" __copyright__ = "European Synchrotron Radiation Facility, Grenoble, France" + +from PyMca5.PyMcaGui import PyMcaAppInit + +if __name__== '__main__': + PyMcaAppInit.init_before_app_import() + import sys import logging -if __name__== '__main__': - # avoid issues if some module or dependency tries to use multiprocessing in frozen binaries - if getattr(sys, "frozen", False): - try: - import multiprocessing - multiprocessing.freeze_support() - except Exception: - pass + from PyMca5.PyMcaGui import PyMcaQt as qt from PyMca5.PyMcaPhysics import Elements from PyMca5.PyMcaGui.plotting import PyMca_Icons +from PyMca5.PyMcaMisc import CliUtils + IconDict = PyMca_Icons.IconDict QTVERSION = qt.qVersion() @@ -293,21 +294,36 @@ def focusOutEvent(self,event): qt.QApplication.instance().palette().color(qt.QPalette.Base)) qt.QLineEdit.focusOutEvent(self, event) -def main(): - logging.basicConfig(level=logging.INFO) - app = qt.QApplication(sys.argv) - if len(sys.argv) > 1: - ene = float(sys.argv[1]) - else: - ene = 5.9 + +def main(args): + app = qt.QApplication([]) + PyMcaAppInit.init_before_app_start(qt_app=app, cli_args=args) + mw = qt.QWidget() l = qt.QVBoxLayout(mw) l.setSpacing(0) - w= PeakIdentifier(mw,energy=ene,useviewer=1) + + w= PeakIdentifier(mw ,energy=args.energy or 5.9, useviewer=1) l.addWidget(w) mw.setWindowTitle("Peak Identifier") mw.show() - app.exec() + + # Auto-close Qt for tests + if args.cli_test: + qt.QTimer.singleShot(0, app.quit) + + return app.exec() + + +def build_parser(): + parser = CliUtils.create_parser(description="Peak Identifier Tool", add_qt_options=True) + + parser.add_argument("energy", nargs="?", type=float, default=None) + + return parser + if __name__ == "__main__": - main() + PyMcaAppInit.init_before_app_create() + exit_code = CliUtils.cli_main(main, build_parser(), loggers=(_logger,)) + sys.exit(exit_code) diff --git a/src/PyMca5/PyMcaGui/plotting/ImageView.py b/src/PyMca5/PyMcaGui/plotting/ImageView.py index 1a77bd990..abd9b991f 100644 --- a/src/PyMca5/PyMcaGui/plotting/ImageView.py +++ b/src/PyMca5/PyMcaGui/plotting/ImageView.py @@ -57,9 +57,15 @@ ``python -m PyMca5.PyMcaGui.plotting.ImageView -h`` """ +from PyMca5.PyMcaGui import PyMcaAppInit + +if __name__== '__main__': + PyMcaAppInit.init_before_app_import() # import ###################################################################### +import os +import sys import numpy as np try: @@ -71,7 +77,8 @@ from .Toolbars import ProfileToolBar, LimitsToolBar from PyMca5.PyMcaGraph import Plot - +from PyMca5.PyMcaMisc import CliUtils +from PyMca5.PyMcaIO.EdfFile import EdfFile # utils ####################################################################### @@ -934,36 +941,7 @@ def setImage(self, image, *args, **kwargs): # main ######################################################################## -if __name__ == "__main__": - import argparse - import os.path - import sys - - from PyMca5.PyMcaIO.EdfFile import EdfFile - - # Command-line arguments - parser = argparse.ArgumentParser( - description='Browse the images of an EDF file.') - parser.add_argument( - '-b', '--backend', - choices=('mpl', 'opengl', 'osmesa'), - help="""The plot backend to use: Matplotlib (mpl, the default), - OpenGL 2.1 (opengl, requires appropriate OpenGL drivers) or - Off-screen Mesa OpenGL software pipeline (osmesa, - requires appropriate OSMesa library).""") - parser.add_argument( - '-o', '--origin', nargs=2, - type=float, default=(0., 0.), - help="""Coordinates of the origin of the image: (x, y). - Default: 0., 0.""") - parser.add_argument( - '-s', '--scale', nargs=2, - type=float, default=(1., 1.), - help="""Scale factors applied to the image: (sx, sy). - Default: 1., 1.""") - parser.add_argument('filename', help='EDF filename of the image to open') - args = parser.parse_args() - +def main(args): # Open the input file if not os.path.isfile(args.filename): raise RuntimeError('No input file: %s' % args.filename) @@ -976,6 +954,7 @@ def setImage(self, image, *args, **kwargs): # Set-up Qt application and main window app = qt.QApplication([]) + PyMcaAppInit.init_before_app_start(qt_app=app, cli_args=args) mainWindow = ImageViewMainWindow(backend=args.backend) mainWindow.setImage(edfFile.GetData(0), @@ -1003,4 +982,34 @@ def updateImage(index): mainWindow.show() - sys.exit(app.exec()) + # Auto-close Qt for tests + if args.cli_test: + qt.QTimer.singleShot(0, app.quit) + + return app.exec() + + +def build_parser(): + parser = CliUtils.create_parser( + description="Browse the images of an EDF file.", add_qt_options=True, add_backend_options=True + ) + + parser.add_argument( + '-o', '--origin', nargs=2, + type=float, default=(0., 0.), + help="""Coordinates of the origin of the image: (x, y). + Default: 0., 0.""") + parser.add_argument( + '-s', '--scale', nargs=2, + type=float, default=(1., 1.), + help="""Scale factors applied to the image: (sx, sy). + Default: 1., 1.""") + parser.add_argument('filename', help='EDF filename of the image to open') + + return parser + + +if __name__ == "__main__": + PyMcaAppInit.init_before_app_create() + exit_code = CliUtils.cli_main(main, build_parser()) + sys.exit(exit_code) diff --git a/src/PyMca5/PyMcaGui/plotting/MaskImageWidget.py b/src/PyMca5/PyMcaGui/plotting/MaskImageWidget.py index 2a4c6e018..d9420ff1c 100644 --- a/src/PyMca5/PyMcaGui/plotting/MaskImageWidget.py +++ b/src/PyMca5/PyMcaGui/plotting/MaskImageWidget.py @@ -27,6 +27,12 @@ __contact__ = "sole@esrf.fr" __license__ = "MIT" __copyright__ = "European Synchrotron Radiation Facility, Grenoble, France" + +from PyMca5.PyMcaGui import PyMcaAppInit + +if __name__== '__main__': + PyMcaAppInit.init_before_app_import() + import sys import os import numpy @@ -55,6 +61,7 @@ from PyMca5.PyMcaIO import ArraySave from . import ProfileScanWidget from PyMca5.PyMcaMath.fitting import SpecfitFuns +from PyMca5.PyMcaMisc import CliUtils COLORMAPLIST = [spslut.GREYSCALE, spslut.REVERSEGREY, spslut.TEMP, spslut.RED, spslut.GREEN, spslut.BLUE, spslut.MANY] @@ -2178,23 +2185,25 @@ def getImageMask(image, mask=None): del(w) return mask -def test(filename=None, backend=None): + +def main(args): app = qt.QApplication([]) - app.lastWindowClosed.connect(app.quit) - if filename: - container = MaskImageWidget(backend=backend, + PyMcaAppInit.init_before_app_start(qt_app=app, cli_args=args) + + if args.filename: + container = MaskImageWidget(backend=args.backend, selection=True, aspect=True, imageicons=True, profileselection=True, maxNRois=2) - if filename.endswith('edf') or\ - filename.endswith('cbf') or\ - filename.endswith('ccd') or\ - filename.endswith('spe') or\ - filename.endswith('tif') or\ - filename.endswith('tiff'): + if args.filename.endswith('edf') or\ + args.filename.endswith('cbf') or\ + args.filename.endswith('ccd') or\ + args.filename.endswith('spe') or\ + args.filename.endswith('tif') or\ + args.filename.endswith('tiff'): from PyMca5.PyMcaIO import EdfFile edf = EdfFile.EdfFile(sys.argv[1]) data = edf.GetData(0) @@ -2202,12 +2211,12 @@ def test(filename=None, backend=None): else: - image = qt.QImage(filename) + image = qt.QImage(args.filename) #container.setQImage(image, image.width(),image.height()) container.setQImage(image, 200, 200) else: - container = MaskImageWidget(backend=backend, + container = MaskImageWidget(backend=args.backend, aspect=True, profileselection=True, maxNRois=2) @@ -2237,25 +2246,35 @@ def test(filename=None, backend=None): #data.shape = 100, 400 #container.setImageData(None) #container.setImageData(data) + container.show() def theSlot(ddict): print(ddict['event']) container.sigMaskImageWidgetSignal.connect(theSlot) - app.exec() + + # Auto-close Qt for tests + if args.cli_test: + qt.QTimer.singleShot(0, app.quit) + + exit_code = app.exec() + print(container.getSelectionMask()) + return exit_code + + +def build_parser(): + parser = CliUtils.create_parser( + description="PyMca image mask authoring tool.", add_qt_options=True, add_backend_options=True + ) -if __name__ == "__main__": - import argparse - - parser = argparse.ArgumentParser( - description='PyMca image mask authoring tool.') - parser.add_argument( - '-b', '--backend', - choices=('mpl', 'opengl'), - help="""The plot backend to use: Matplotlib (mpl, the default), - OpenGL 2.1 (opengl, requires appropriate OpenGL drivers).""") parser.add_argument('filename', default='', nargs='?', help='Image filename to open') - args = parser.parse_args() - test(args.filename, args.backend) + + return parser + + +if __name__ == "__main__": + PyMcaAppInit.init_before_app_create() + exit_code = CliUtils.cli_main(main, build_parser()) + sys.exit(exit_code) diff --git a/src/PyMca5/PyMcaGui/pymca/EdfFileSimpleViewer.py b/src/PyMca5/PyMcaGui/pymca/EdfFileSimpleViewer.py index d00548c6c..4466ad1d0 100644 --- a/src/PyMca5/PyMcaGui/pymca/EdfFileSimpleViewer.py +++ b/src/PyMca5/PyMcaGui/pymca/EdfFileSimpleViewer.py @@ -28,16 +28,16 @@ __contact__ = "sole@esrf.fr" __license__ = "MIT" __copyright__ = "European Synchrotron Radiation Facility, Grenoble, France" + +from PyMca5.PyMcaGui import PyMcaAppInit + +if __name__== '__main__': + PyMcaAppInit.init_before_app_import() + import sys +import glob import logging -if __name__== '__main__': - # avoid issues if some module or dependency tries to use multiprocessing in frozen binaries - if getattr(sys, "frozen", False): - try: - import multiprocessing - multiprocessing.freeze_support() - except Exception: - pass + from PyMca5.PyMcaGui import PyMcaQt as qt QTVERSION = qt.qVersion() @@ -46,6 +46,8 @@ from PyMca5.PyMcaGui.io import QSourceSelector from PyMca5.PyMcaGui.pymca import QDataSource from PyMca5.PyMcaGui.io import QEdfFileWidget +from PyMca5.PyMcaMisc import CliUtils + class EdfFileSimpleViewer(qt.QWidget): def __init__(self, parent=None): @@ -116,31 +118,40 @@ def setFileList(self, filelist): self.sourceSelector.openFile(ffile, justloaded = 1) -def main(): - import sys - import getopt - import glob - from PyMca5.PyMcaCore.LoggingLevel import getLoggingLevel - app=qt.QApplication(sys.argv) - options='' - longoptions=['logging=', 'debug='] - opts, args = getopt.getopt( - sys.argv[1:], - options, - longoptions) - logging.basicConfig(level=getLoggingLevel(opts)) - _logger.setLevel(getLoggingLevel(opts)) - filelist = args - if len(filelist) == 1: - if sys.platform.startswith("win") and '*' in filelist[0]: - filelist = glob.glob(filelist[0]) - app.lastWindowClosed.connect(app.quit) - w=EdfFileSimpleViewer() - if len(filelist): +def main(args): + # Qt application + app = qt.QApplication([]) + PyMcaAppInit.init_before_app_start(qt_app=app, cli_args=args) + + # File list (expand wildcards on Windows) + filelist = args.files + if len(filelist) == 1 and sys.platform.startswith("win") and "*" in filelist[0]: + filelist = glob.glob(filelist[0]) + + # Launch viewer + w = EdfFileSimpleViewer() + if filelist: w.setFileList(filelist) w.show() - app.exec() + + # Auto-close Qt for tests + if args.cli_test: + qt.QTimer.singleShot(0, app.quit) + + return app.exec() + + +def build_parser(): + parser = CliUtils.create_parser(description="Simple EDF file viewer", add_qt_options=True) + + parser.add_argument( + "files", nargs="*", help="EDF files to open (supports wildcards on Windows)" + ) + + return parser if __name__ == "__main__": - main() + PyMcaAppInit.init_before_app_create() + exit_code = CliUtils.cli_main(main, build_parser(), loggers=(_logger,)) + sys.exit(exit_code) diff --git a/src/PyMca5/PyMcaGui/pymca/ExternalImagesStackPluginBase.py b/src/PyMca5/PyMcaGui/pymca/ExternalImagesStackPluginBase.py index b5dbbf903..6e799e12e 100644 --- a/src/PyMca5/PyMcaGui/pymca/ExternalImagesStackPluginBase.py +++ b/src/PyMca5/PyMcaGui/pymca/ExternalImagesStackPluginBase.py @@ -27,7 +27,6 @@ # #############################################################################*/ __author__ = "Wout De Nolf" -__contact__ = "wout.de_nolf@esrf.eu" __license__ = "MIT" __copyright__ = "European Synchrotron Radiation Facility, Grenoble, France" diff --git a/src/PyMca5/PyMcaGui/pymca/Fit2Spec.py b/src/PyMca5/PyMcaGui/pymca/Fit2Spec.py index 8d014afe3..398aab74d 100644 --- a/src/PyMca5/PyMcaGui/pymca/Fit2Spec.py +++ b/src/PyMca5/PyMcaGui/pymca/Fit2Spec.py @@ -1,6 +1,6 @@ #!/usr/bin/env python #/*########################################################################## -# Copyright (C) 2004-2016 V.A. Sole, European Synchrotron Radiation Facility +# Copyright (C) 2004-2026 V.A. Sole, European Synchrotron Radiation Facility # # This file is part of the PyMca X-ray Fluorescence Toolkit developed at # the ESRF by the Software group. @@ -28,325 +28,358 @@ __contact__ = "sole@esrf.fr" __license__ = "MIT" __copyright__ = "European Synchrotron Radiation Facility, Grenoble, France" + +from PyMca5.PyMcaGui import PyMcaAppInit + +if __name__== '__main__': + PyMcaAppInit.init_before_app_import() + import os import sys import time -from . import McaCustomEvent + +from PyMca5.PyMcaGui import PyMcaQt as qt from PyMca5.PyMcaIO import ConfigDict +from PyMca5.PyMcaMisc import CliUtils + +from . import McaCustomEvent + ROIWIDTH = 250. -from PyMca5.PyMcaGui import PyMcaQt as qt class Fit2SpecGUI(qt.QWidget): - def __init__(self,parent=None,name="Fit to Spec Conversion", - filelist=None,outputdir=None, actions=0): - qt.QWidget.__init__(self,parent,name) - layout = qt.QVBoxLayout(self) - layout.setAutoAdd(1) - self.setCaption(name) - self.__build(actions) - if filelist is None: filelist = [] - self.outputDir = None - self.setFileList(filelist) - self.setOutputDir(outputdir) - - def __build(self,actions): - self.__grid= qt.QWidget(self) - #self.__grid.setGeometry(qt.QRect(30,30,288,156)) - grid = qt.QGridLayout(self.__grid,2,3,11,6) - grid.setColStretch(0,0) - grid.setColStretch(1,1) - grid.setColStretch(2,0) - #input list - listrow = 0 - listlabel = qt.QLabel(self.__grid) - listlabel.setText("Input File list:") - listlabel.setAlignment(qt.QLabel.WordBreak | qt.QLabel.AlignVCenter) - self.__listView = qt.QTextView(self.__grid) - self.__listView.setMaximumHeight(30*listlabel.sizeHint().height()) - self.__listButton = qt.QPushButton(self.__grid) - self.__listButton.setText('Browse') + def __init__(self, parent=None, name="Fit to Spec Conversion", + filelist=None, outputdir=None, actions=0): + super().__init__(parent) + self.setWindowTitle(name) + + # Main vertical layout + main_layout = qt.QVBoxLayout(self) + main_layout.setContentsMargins(15, 15, 15, 15) + main_layout.setSpacing(10) + + # Form grid (input files + output dir) + self.__grid = qt.QWidget(self) + grid = qt.QGridLayout(self.__grid) + grid.setHorizontalSpacing(10) + grid.setVerticalSpacing(10) + grid.setColumnStretch(0, 0) + grid.setRowStretch(0, 1) + grid.setColumnStretch(1, 1) + grid.setColumnStretch(2, 0) + + # Input file list + list_label = qt.QLabel("Input File list:", self.__grid) + list_label.setAlignment(qt.Qt.AlignVCenter | qt.Qt.AlignLeft) + list_label.setWordWrap(True) + + self.__listView = qt.QTextEdit(self.__grid) + self.__listView.setReadOnly(True) + self.__listView.setMinimumHeight(120) + self.__listView.setSizePolicy(qt.QSizePolicy.Expanding, qt.QSizePolicy.Expanding) + + self.__listButton = qt.QPushButton("Browse", self.__grid) self.__listButton.clicked.connect(self.browseList) - grid.addWidget(listlabel, listrow, 0, qt.Qt.AlignTop|qt.Qt.AlignLeft) - grid.addWidget(self.__listView, listrow, 1) - grid.addWidget(self.__listButton,listrow, 2, qt.Qt.AlignTop|qt.Qt.AlignRight) - - #output dir - outrow = 1 - outlabel = qt.QLabel(self.__grid) - outlabel.setText("Output dir:") - outlabel.setAlignment(qt.QLabel.WordBreak | qt.QLabel.AlignVCenter) + + grid.addWidget(list_label, 0, 0, alignment=qt.Qt.AlignTop | qt.Qt.AlignLeft) + grid.addWidget(self.__listView, 0, 1) + grid.addWidget(self.__listButton, 0, 2, alignment=qt.Qt.AlignTop | qt.Qt.AlignRight) + + # Output directory + out_label = qt.QLabel("Output dir:", self.__grid) + out_label.setAlignment(qt.Qt.AlignLeft | qt.Qt.AlignVCenter) + out_label.setWordWrap(True) + self.__outLine = qt.QLineEdit(self.__grid) self.__outLine.setReadOnly(True) - #self.__outLine.setSizePolicy(qt.QSizePolicy(qt.QSizePolicy.Maximum, qt.QSizePolicy.Fixed)) - self.__outButton = qt.QPushButton(self.__grid) - self.__outButton.setText('Browse') + self.__outLine.setSizePolicy(qt.QSizePolicy.Expanding, qt.QSizePolicy.Fixed) + + self.__outButton = qt.QPushButton("Browse", self.__grid) self.__outButton.clicked.connect(self.browseOutputDir) - grid.addWidget(outlabel, outrow, 0, qt.Qt.AlignLeft) - grid.addWidget(self.__outLine, outrow, 1) - grid.addWidget(self.__outButton, outrow, 2, qt.Qt.AlignLeft) + grid.addWidget(out_label, 1, 0, alignment=qt.Qt.AlignLeft) + grid.addWidget(self.__outLine, 1, 1) + grid.addWidget(self.__outButton, 1, 2, alignment=qt.Qt.AlignLeft) + + main_layout.addWidget(self.__grid) - if actions: self.__buildActions() + # Action buttons + if actions: + self.__buildActions() + + # Initialize file list & output dir + self.outputDir = None + self.setFileList(filelist or []) + self.setOutputDir(outputdir) def __buildActions(self): - box = qt.QHBox(self) - qt.HorizontalSpacer(box) - self.__dismissButton = qt.QPushButton(box) - qt.HorizontalSpacer(box) - self.__dismissButton.setText("Close") - self.__startButton = qt.QPushButton(box) - qt.HorizontalSpacer(box) - self.__startButton.setText("Start") + box = qt.QHBoxLayout() + box.addStretch(1) + + self.__dismissButton = qt.QPushButton("Close") + self.__startButton = qt.QPushButton("Start") + + box.addWidget(self.__dismissButton) + box.addSpacing(20) + box.addWidget(self.__startButton) + box.addStretch(1) + self.__dismissButton.clicked.connect(self.close) self.__startButton.clicked.connect(self.start) - def setFileList(self,filelist=None): - if filelist is None: - filelist = [] + container = qt.QWidget(self) + container.setLayout(box) + self.layout().addWidget(container) + + def setFileList(self, filelist=None): + filelist = filelist or [] if True or self.__goodFileList(filelist): - text = "" - filelist.sort() - for ffile in filelist: - text += "%s\n" % ffile + filelist = sorted(filelist) + text = "\n".join(filelist) self.fileList = filelist self.__listView.setText(text) - def setOutputDir(self,outputdir=None): - if outputdir is None:return + def setOutputDir(self, outputdir=None): + if not outputdir: + return if self.__goodOutputDir(outputdir): self.outputDir = outputdir self.__outLine.setText(outputdir) else: - qt.QMessageBox.critical(self, "ERROR", - "Cannot use output directory:\n%s"% (outputdir)) + qt.QMessageBox.critical(self, "ERROR", f"Cannot use output directory:\n{outputdir}") - def __goodFileList(self,filelist): - if not len(filelist):return True + def __goodFileList(self, filelist): for file in filelist: if not os.path.exists(file): - qt.QMessageBox.critical(self, "ERROR",'File %s\ndoes not exists' % file) + qt.QMessageBox.critical(self, "ERROR", f'File {file}\ndoes not exist') self.raiseW() return False return True - def __goodOutputDir(self,outputdir): - if os.path.isdir(outputdir):return True - else:return False + def __goodOutputDir(self, outputdir): + return os.path.isdir(outputdir) def browseList(self): - filedialog = qt.QFileDialog(self,"Open a set of files",1) - filedialog.setMode(filedialog.ExistingFiles) - if hasattr(filedialog, "setFilters"): - filedialog.setFilters("Fit Files (*.fit)\n") - else: - filedialog.setNameFilters("Fit Files (*.fit)\n") - if filedialog.exec_loop() == qt.QDialog.Accepted: - filelist0= filedialog.selectedFiles() + filedialog = qt.QFileDialog(self, "Open a set of files") + filedialog.setFileMode(qt.QFileDialog.ExistingFiles) + filedialog.setNameFilters(["Fit Files (*.fit)"]) + + if filedialog.exec() == qt.QDialog.Accepted: + filelist0 = filedialog.selectedFiles() else: self.raiseW() return - filelist = [] - for f in filelist0: - filelist.append(qt.safe_str(f)) - if len(filelist):self.setFileList(filelist) + + filelist = [qt.safe_str(f) for f in filelist0] + if filelist: + self.setFileList(filelist) self.raiseW() def browseConfig(self): - filename= qt.QFileDialog(self,"Open a new fit config file",1) - filename.setMode(filename.ExistingFiles) - filename.setFilters("Config Files (*.cfg)\nAll files (*)") - if filename.exec_loop() == qt.QDialog.Accepted: - filename = filename.selectedFile() + dialog = qt.QFileDialog(self, "Open a new fit config file") + dialog.setFileMode(qt.QFileDialog.ExistingFile) + dialog.setNameFilters(["Config Files (*.cfg)", "All files (*)"]) + + if dialog.exec() == qt.QDialog.Accepted: + filename = dialog.selectedFiles()[0] else: self.raiseW() return + filename = qt.safe_str(filename) - if len(filename): + if filename: self.setConfigFile(filename) self.raiseW() def browseOutputDir(self): - outfile = qt.QFileDialog(self,"Output Directory Selection",1) - outfile.setMode(outfile.DirectoryOnly) - ret = outfile.exec_loop() - if ret: - outdir = qt.safe_str(outfile.selectedFile()) - outfile.close() - del outfile + dialog = qt.QFileDialog(self, "Output Directory Selection") + dialog.setFileMode(qt.QFileDialog.Directory) + dialog.setOption(qt.QFileDialog.ShowDirsOnly, True) + + if dialog.exec() == qt.QDialog.Accepted: + outdir = qt.safe_str(dialog.selectedFiles()[0]) self.setOutputDir(outdir) - else: - outfile.close() - del outfile self.raiseW() def start(self): - if not len(self.fileList): - qt.QMessageBox.critical(self, "ERROR",'Empty file list') + if not getattr(self, "fileList", []): + qt.QMessageBox.critical(self, "ERROR", 'Empty file list') self.raiseW() return if (self.outputDir is None) or (not self.__goodOutputDir(self.outputDir)): - qt.QMessageBox.critical(self, "ERROR",'Invalid output directory') + qt.QMessageBox.critical(self, "ERROR", 'Invalid output directory') self.raiseW() return - name = "Batch from %s to %s " % (os.path.basename(self.fileList[ 0]), - os.path.basename(self.fileList[-1])) - window = Fit2SpecWindow(name="Fit 2 Spec "+name,actions=1) - b = Fit2SpecBatch(window,self.fileList,self.outputDir) + name = f"Batch from {os.path.basename(self.fileList[0])} to {os.path.basename(self.fileList[-1])}" + window = Fit2SpecWindow(name="Fit 2 Spec " + name, actions=1) + b = Fit2SpecBatch(window, self.fileList, self.outputDir) + def cleanup(): b.pleasePause = 0 b.pleaseBreak = 1 - b.wait() - qApp = qt.QApplication.instance() - qApp.processEvents() + if hasattr(b, "wait"): + b.wait() + qt.QApplication.instance().processEvents() def pause(): if b.pleasePause: - b.pleasePause=0 + b.pleasePause = 0 window.pauseButton.setText("Pause") else: - b.pleasePause=1 + b.pleasePause = 1 window.pauseButton.setText("Continue") + window.pauseButton.clicked.connect(pause) window.abortButton.clicked.connect(window.close) - qApp = qt.QApplication.instance() - qApp.aboutToQuit.connect(cleanup) + qt.QApplication.instance().aboutToQuit.connect(cleanup) + self.__window = window - self.__b = b + self.__b = b window.show() b.start() + def raiseW(self): + self.raise_() + self.activateWindow() + class Fit2SpecBatch(qt.QThread): - def __init__(self, parent, filelist=None, outputdir = None): - self._filelist = filelist - self.outputdir = outputdir - qt.QThread.__init__(self) + def __init__(self, parent, filelist=None, outputdir=None): + super().__init__(parent) self.parent = parent + self._filelist = filelist or [] + self.outputdir = outputdir self.pleasePause = 0 + self.pleaseBreak = 0 + + def _postEvent(self, event): + qt.QApplication.postEvent(self.parent, event) def processList(self): for fitfile in self._filelist: + if self.pleaseBreak: + break self.onNewFile(fitfile, self._filelist) + d = ConfigDict.ConfigDict() d.read(fitfile) - f = open(os.path.join(self.outputdir,os.path.basename(fitfile)+".dat"),'w+') - npoints = len(d['result']['xdata']) - f.write("\n") - f.write("#S 1 %s\n" % fitfile) - i=0 - for parameter in d['result']['parameters']: - f.write("#U%d %s %.6g +/- %.3g\n" % (i, parameter, - d['result']['fittedpar'][i], - d['result']['sigmapar'][i])) - i+=1 - f.write("#N 6\n") - f.write("#L Energy channel counts fit continuum pileup\n") - for i in range(npoints): - f.write("%.6g %.6g %.6g %.6g %.6g %.6g\n" % (d['result']['energy'][i], - d['result']['xdata'][i], - d['result']['ydata'][i], - d['result']['yfit'][i], - d['result']['continuum'][i], - d['result']['pileup'][i])) - f.close() + + outfile = os.path.join(self.outputdir, os.path.basename(fitfile) + ".dat") + with open(outfile, "w") as f: + npoints = len(d['result']['xdata']) + f.write("\n") + f.write(f"#S 1 {fitfile}\n") + for i, parameter in enumerate(d['result']['parameters']): + f.write(f"#U{i} {parameter} {d['result']['fittedpar'][i]:.6g} +/- {d['result']['sigmapar'][i]:.3g}\n") + f.write("#N 6\n") + f.write("#L Energy channel counts fit continuum pileup\n") + for i in range(npoints): + f.write(f"{d['result']['energy'][i]:.6g} {d['result']['xdata'][i]:.6g} " + f"{d['result']['ydata'][i]:.6g} {d['result']['yfit'][i]:.6g} " + f"{d['result']['continuum'][i]:.6g} {d['result']['pileup'][i]:.6g}\n") self.onEnd() def run(self): self.processList() def onNewFile(self, file, filelist): - self.postEvent(self.parent, McaCustomEvent.McaCustomEvent({'file':file, - 'filelist':filelist, - 'event':'onNewFile'})) - if self.pleasePause:self.__pauseMethod() + self._postEvent(McaCustomEvent.McaCustomEvent({'file': file, + 'filelist': filelist, + 'event': 'onNewFile'})) + if self.pleasePause: + self.__pauseMethod() def onEnd(self): - self.postEvent(self.parent, McaCustomEvent.McaCustomEvent({'event':'onEnd'})) - if self.pleasePause:self.__pauseMethod() - + self._postEvent(McaCustomEvent.McaCustomEvent({'event': 'onEnd'})) + if self.pleasePause: + self.__pauseMethod() def __pauseMethod(self): - self.postEvent(self.parent, McaCustomEvent.McaCustomEvent({'event':'batchPaused'})) - while(self.pleasePause): + self._postEvent(McaCustomEvent.McaCustomEvent({'event': 'batchPaused'})) + while self.pleasePause: time.sleep(1) - self.postEvent(self.parent, McaCustomEvent.McaCustomEvent({'event':'batchResumed'})) + self._postEvent(McaCustomEvent.McaCustomEvent({'event': 'batchResumed'})) class Fit2SpecWindow(qt.QWidget): - def __init__(self,parent=None, name="BatchWindow", fl=0, actions = 0): - qt.QWidget.__init__(self, parent, name, fl) - self.setCaption(name) + def __init__(self, parent=None, name="BatchWindow", actions=0): + super().__init__(parent) + + self.setObjectName(name) + self.setWindowTitle(name) + self.l = qt.QVBoxLayout(self) - self.l.setAutoAdd(1) - self.bars =qt.QWidget(self) - self.barsLayout = qt.QGridLayout(self.bars,2,3) - self.progressBar = qt.QProgressBar(self.bars) - self.progressLabel = qt.QLabel(self.bars) - self.progressLabel.setText('File Progress:') - - self.barsLayout.addWidget(self.progressLabel,0,0) - self.barsLayout.addWidget(self.progressBar,0,1) - self.status = qt.QLabel(self) - self.status.setText(" ") - self.timeLeft = qt.QLabel(self) - self.timeLeft.setText("Estimated time left = ???? min") + + # Progress section + self.bars = qt.QWidget(self) + barsLayout = qt.QGridLayout(self.bars) + self.progressLabel = qt.QLabel("File Progress:", self.bars) + self.progressBar = qt.QProgressBar(self.bars) + barsLayout.addWidget(self.progressLabel, 0, 0) + barsLayout.addWidget(self.progressBar, 0, 1) + self.l.addWidget(self.bars) + + # Status labels + self.status = qt.QLabel(" ", self) + self.timeLeft = qt.QLabel("Estimated time left = ???? min", self) + self.l.addWidget(self.status) + self.l.addWidget(self.timeLeft) + self.time0 = None - if actions: self.addButtons() + self.actions = actions + if actions: + self.addButtons() + self.show() self.raiseW() - def addButtons(self): - self.actions = 1 self.buttonsBox = qt.QWidget(self) l = qt.QHBoxLayout(self.buttonsBox) - l.setAutoAdd(1) - qt.HorizontalSpacer(self.buttonsBox) - self.pauseButton = qt.QPushButton(self.buttonsBox) - qt.HorizontalSpacer(self.buttonsBox) - self.pauseButton.setText("Pause") - self.abortButton = qt.QPushButton(self.buttonsBox) - qt.HorizontalSpacer(self.buttonsBox) - self.abortButton.setText("Abort") - self.update() - - def customEvent(self,event): - if event.dict['event'] == 'onNewFile':self.onNewFile(event.dict['file'], - event.dict['filelist']) - elif event.dict['event'] == 'onEnd': self.onEnd(event.dict) - - elif event.dict['event'] == 'batchPaused': self.onPause() - - elif event.dict['event'] == 'batchResumed':self.onResume() - + l.addStretch(1) + self.pauseButton = qt.QPushButton("Pause", self.buttonsBox) + l.addWidget(self.pauseButton) + l.addSpacing(10) + self.abortButton = qt.QPushButton("Abort", self.buttonsBox) + l.addWidget(self.abortButton) + l.addStretch(1) + self.l.addWidget(self.buttonsBox) + + def customEvent(self, event): + if event.dict['event'] == 'onNewFile': + self.onNewFile(event.dict['file'], event.dict['filelist']) + elif event.dict['event'] == 'onEnd': + self.onEnd(event.dict) + elif event.dict['event'] == 'batchPaused': + self.onPause() + elif event.dict['event'] == 'batchResumed': + self.onResume() else: - print("Unhandled event",event) - + print("Unhandled event", event) def onNewFile(self, file, filelist): - indexlist = range(0,len(filelist)) - index = indexlist.index(filelist.index(file)) - nfiles = len(indexlist) - self.status.setText("Processing file %s" % file) - e = time.time() - self.progressBar.setTotalSteps(nfiles) - self.progressBar.setProgress(index) + index = filelist.index(file) + nfiles = len(filelist) + self.status.setText(f"Processing file {file}") + self.progressBar.setMaximum(nfiles) + self.progressBar.setValue(index) + + now = time.time() if self.time0 is not None: - t = (e - self.time0) * (nfiles - index) - self.time0 =e + t = (now - self.time0) * (nfiles - index) + self.time0 = now if t < 120: - self.timeLeft.setText("Estimated time left = %d sec" % (t)) + self.timeLeft.setText(f"Estimated time left = {int(t)} sec") else: - self.timeLeft.setText("Estimated time left = %d min" % (int(t / 60.))) + self.timeLeft.setText(f"Estimated time left = {int(t / 60)} min") else: - self.time0 = e + self.time0 = now - def onEnd(self,dict): - n = self.progressBar.progress() - self.progressBar.setProgress(n+1) - self.status.setText ("Batch Finished") + def onEnd(self, dict=None): + n = self.progressBar.value() + self.progressBar.setValue(n + 1) + self.status.setText("Batch Finished") self.timeLeft.setText("Estimated time left = 0 sec") if self.actions: self.pauseButton.hide() @@ -358,67 +391,107 @@ def onPause(self): def onResume(self): pass -if __name__ == "__main__": - import getopt - options = 'f' - longoptions = ['outdir=', 'listfile='] - filelist = None - outdir = None - listfile = None - opts, args = getopt.getopt( - sys.argv[1:], - options, - longoptions) - for opt,arg in opts: - if opt in ('--outdir'): - outdir = arg - elif opt in ('--listfile'): - listfile = arg - if listfile is None: - filelist=[] - for item in args: - filelist.append(item) + def raiseW(self): + self.raise_() + self.activateWindow() + + +def main(args): + # Prepare file list + if args.listfile is None: + filelist = args.files or [] else: - fd = open(listfile) - filelist = fd.readlines() - fd.close() - for i in range(len(filelist)): - filelist[i]=filelist[i].replace('\n','') - - app=qt.QApplication(sys.argv) - app.lastWindowClosed.conenct(app.quit) - if len(filelist) == 0: + with open(args.listfile, 'r') as fd: + filelist = [line.strip() for line in fd.readlines()] + + # Qt application + app = qt.QApplication([]) + PyMcaAppInit.init_before_app_start(qt_app=app, cli_args=args) + + # Launch GUI if no files provided + if not filelist: w = Fit2SpecGUI(actions=1) - app.setMainWidget(w) w.show() - app.exec() else: - text = "Batch from %s to %s" % (os.path.basename(filelist[0]), os.path.basename(filelist[-1])) - window = Fit2SpecWindow(name=text,actions=1) - b = Fit2SpecBatch(window,filelist,outdir) + text = f"Batch from {os.path.basename(filelist[0])} to {os.path.basename(filelist[-1])}" + window = Fit2SpecWindow(name=text, actions=1) + b = Fit2SpecBatch(window, filelist, args.outdir) + + # Cleanup and pause handling def cleanup(): b.pleasePause = 0 b.pleaseBreak = 1 - b.wait() - qApp = qt.QApplication.instance() - qApp.processEvents() + if hasattr(b, "wait"): + b.wait() + qt.QApplication.instance().processEvents() def pause(): if b.pleasePause: - b.pleasePause=0 + b.pleasePause = 0 window.pauseButton.setText("Pause") else: - b.pleasePause=1 + b.pleasePause = 1 window.pauseButton.setText("Continue") + window.pauseButton.clicked.connect(pause) window.abortButton.clicked.connect(window.close) app.aboutToQuit.connect(cleanup) + window.show() b.start() - app.setMainWidget(window) - app.exec() + # Auto-close Qt for tests + if args.cli_test: + qt.QTimer.singleShot(0, app.quit) + + return app.exec() + + +def build_parser(): + parser = CliUtils.create_parser(description="Fit2Spec GUI launcher", add_qt_options=True) + parser.add_argument("--outdir", type=str, default=None, help="Output directory") + parser.add_argument("--listfile", type=str, default=None, help="File containing list of input files") -# PyMcaBatch.py --cfg=/mntdirect/_bliss/users/sole/COTTE/WithLead.cfg --outdir=/tmp/ /mntdirect/_bliss/users/sole/COTTE/ch09/ch09__mca_0003_0000_0007.edf /mntdirect/_bliss/users/sole/COTTE/ch09/ch09__mca_0003_0000_0008.edf /mntdirect/_bliss/users/sole/COTTE/ch09/ch09__mca_0003_0000_0009.edf /mntdirect/_bliss/users/sole/COTTE/ch09/ch09__mca_0003_0000_0010.edf /mntdirect/_bliss/users/sole/COTTE/ch09/ch09__mca_0003_0000_0011.edf /mntdirect/_bliss/users/sole/COTTE/ch09/ch09__mca_0003_0000_0012.edf /mntdirect/_bliss/users/sole/COTTE/ch09/ch09__mca_0003_0000_0013.edf & -# PyMcaBatch.exe --cfg=E:/COTTE/WithLead.cfg --outdir=C:/tmp/ E:/COTTE/ch09/ch09__mca_0003_0000_0007.edf E:/COTTE/ch09/ch09__mca_0003_0000_0008.edf + parser.add_argument("files", nargs="*", help="Files to process if --listfile not provided") + + return parser + + +if __name__ == "__main__": + PyMcaAppInit.init_before_app_create() + exit_code = CliUtils.cli_main(main, build_parser()) + sys.exit(exit_code) + + +# Example FIT file: +# +# [result] +# parameters = [100.0 5.89 0.12] +# fittedpar = [98.0 5.87 0.13] +# sigmapar = [5.0 0.02 0.01] +# xdata = [1 2 3 4 5 6 7 8 9 10] +# ydata = [10 12 15 18 25 35 30 20 12 5] +# yfit = [9.8 11.9 14.7 17.9 24.8 34.9 30.2 19.8 12.1 4.9] +# energy = [1.0 2.0 3.0 4.0 5.0 6.0 7.0 8.0 9.0 10.0] +# continuum = [0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5 0.5] +# pileup = [0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0] +# +# The resulting SPEC file: +# +# #S 1 example.fit +# #U0 100.0 98 +/- 5 +# #U1 5.89 5.87 +/- 0.02 +# #U2 0.12 0.13 +/- 0.01 +# #N 6 +# #L Energy channel counts fit continuum pileup +# 1 1 10 9.8 0.5 0 +# 2 2 12 11.9 0.5 0 +# 3 3 15 14.7 0.5 0 +# 4 4 18 17.9 0.5 0 +# 5 5 25 24.8 0.5 0 +# 6 6 35 34.9 0.5 0 +# 7 7 30 30.2 0.5 0 +# 8 8 20 19.8 0.5 0 +# 9 9 12 12.1 0.5 0 +# 10 10 5 4.9 0.5 0 diff --git a/src/PyMca5/PyMcaGui/pymca/LegacyPyMcaBatch.py b/src/PyMca5/PyMcaGui/pymca/LegacyPyMcaBatch.py index 73859819f..592a5a2c4 100644 --- a/src/PyMca5/PyMcaGui/pymca/LegacyPyMcaBatch.py +++ b/src/PyMca5/PyMcaGui/pymca/LegacyPyMcaBatch.py @@ -28,6 +28,12 @@ __contact__ = "sole@esrf.fr" __license__ = "MIT" __copyright__ = "European Synchrotron Radiation Facility, Grenoble, France" + +from PyMca5.PyMcaGui import PyMcaAppInit + +if __name__== '__main__': + PyMcaAppInit.init_before_app_import() + import sys import os import time @@ -62,6 +68,7 @@ from PyMca5.PyMcaCore import HtmlIndex from PyMca5.PyMcaCore import PyMcaDirs from PyMca5.PyMcaCore import LegacyPyMcaBatchBuildOutput as PyMcaBatchBuildOutput +from PyMca5.PyMcaMisc import CliUtils ROIWIDTH = 100. _logger = logging.getLogger(__name__) @@ -1557,186 +1564,163 @@ def plotImages(self,imagelist): _logger.debug("cmd = %s", cmd) os.system(cmd) -def main(): - sys.excepthook = qt.exceptionHandler - import getopt - from PyMca5.PyMcaCore.LoggingLevel import getLoggingLevel - options = 'f' - longoptions = ['cfg=','outdir=','roifit=','roi=','roiwidth=', - 'overwrite=', 'filestep=', 'mcastep=', 'html=','htmlindex=', - 'listfile=','cfglistfile=', 'concentrations=', 'table=', 'fitfiles=', - 'filebeginoffset=','fileendoffset=','mcaoffset=', 'chunk=', - 'nativefiledialogs=','selection=', 'exitonend=', - 'logging=', 'debug=', 'showresult='] - filelist = None - outdir = None - cfg = None - listfile = None - cfglistfile = None - selection = False - roifit = 0 - roiwidth = ROIWIDTH - overwrite= 1 - filestep = 1 - html = 0 - htmlindex= None - mcastep = 1 - table = 2 - fitfiles = 1 - concentrations = 0 - filebeginoffset = 0 - fileendoffset = 0 - mcaoffset = 0 - chunk = None - exitonend = False - showresult = True - opts, args = getopt.getopt( - sys.argv[1:], - options, - longoptions) - for opt,arg in opts: - if opt in ('--cfg'): - cfg = arg - elif opt in ('--outdir'): - outdir = arg - elif opt in ('--roi','--roifit'): - roifit = int(arg) - elif opt in ('--roiwidth'): - roiwidth = float(arg) - elif opt in ('--overwrite'): - overwrite= int(arg) - elif opt in ('--filestep'): - filestep = int(arg) - elif opt in ('--mcastep'): - mcastep = int(arg) - elif opt in ('--html'): - html = int(arg) - elif opt in ('--htmlindex'): - htmlindex = arg - elif opt in ('--listfile'): - listfile = arg - elif opt in ('--cfglistfile'): - cfglistfile = arg - elif opt in ('--concentrations'): - concentrations = int(arg) - elif opt in ('--table'): - table = int(arg) - elif opt in ('--fitfiles'): - fitfiles = int(arg) - elif opt in ('--filebeginoffset'): - filebeginoffset = int(arg) - elif opt in ('--fileendoffset'): - fileendoffset = int(arg) - elif opt in ('--mcaoffset'): - mcaoffset = int(arg) - elif opt in ('--chunk'): - chunk = int(arg) - elif opt in ('--selection'): - selection = int(arg) - if selection: - selection = True - else: - selection = False - elif opt in ('--nativefiledialogs'): - if int(arg): - PyMcaDirs.nativeFileDialogs = True - else: - PyMcaDirs.nativeFileDialogs = False - elif opt in ('--exitonend'): - exitonend = int(arg) - elif opt in ('--showresult'): - showresult = int(arg) - - logging.basicConfig(level=getLoggingLevel(opts)) - - if listfile is None: - filelist=[] - for item in args: - filelist.append(item) + +def main(args): + # Prepare file list + if args.listfile is None: + filelist = args.files or [] selection = None else: - if selection: + if args.selection: tmpDict = ConfigDict.ConfigDict() - tmpDict.read(listfile) + tmpDict.read(args.listfile) tmpDict = tmpDict['PyMcaBatch'] filelist = tmpDict['filelist'] - if type(filelist) == type(""): + if isinstance(filelist, str): filelist = [filelist] selection = tmpDict['selection'] else: - fd = open(listfile, 'rb') - filelist = fd.readlines() - fd.close() - for i in range(len(filelist)): - filelist[i]=filelist[i].decode(sys.getfilesystemencoding()).replace('\n','') + with open(args.listfile, 'rb') as fd: + filelist = [line.decode(sys.getfilesystemencoding()).strip() for line in fd.readlines()] selection = None - if cfglistfile is not None: - fd = open(cfglistfile, 'rb') - cfg = fd.readlines() - fd.close() - for i in range(len(cfg)): - cfg[i]=cfg[i].decode(sys.getfilesystemencoding()).replace('\n','') - app=qt.QApplication(sys.argv) + + # Prepare configurations + cfg = args.cfg + if args.cfglistfile is not None: + with open(args.cfglistfile, 'rb') as fd: + cfg = [line.decode(sys.getfilesystemencoding()).strip() for line in fd.readlines()] + + # Qt application + app = qt.QApplication([]) + PyMcaAppInit.init_before_app_start(qt_app=app, cli_args=args) + if len(filelist) == 0: - app.lastWindowClosed.connect(app.quit) + # No files -> launch GUI w = McaBatchGUI(actions=1) w.show() w.raise_() - app.exec() else: - app.lastWindowClosed.connect(app.quit) - text = "LegacyBatch from %s to %s" % (os.path.basename(filelist[0]), os.path.basename(filelist[-1])) - window = McaBatchWindow(name=text,actions=1, - outputdir=outdir,html=html, htmlindex=htmlindex, table=table, - chunk=chunk, exitonend=exitonend, showresult=showresult) + # Otherwise launch batch processing + text = f"LegacyBatch from {os.path.basename(filelist[0])} to {os.path.basename(filelist[-1])}" + window = McaBatchWindow( + name=text, + actions=1, + outputdir=args.outdir, + html=args.html, + htmlindex=args.htmlindex, + table=args.table, + chunk=args.chunk, + exitonend=args.exitonend, + showresult=args.showresult, + ) + + if args.html: + fitfiles = 1 + else: + fitfiles = args.fitfiles - if html:fitfiles=1 try: - b = McaBatch(window,cfg,filelist,outdir,roifit=roifit,roiwidth=roiwidth, - overwrite = overwrite, filestep=filestep, mcastep=mcastep, - concentrations=concentrations, fitfiles=fitfiles, - filebeginoffset=filebeginoffset,fileendoffset=fileendoffset, - mcaoffset=mcaoffset, chunk=chunk, selection=selection) + b = McaBatch( + window, + cfg, + filelist, + args.outdir, + roifit=args.roifit, + roiwidth=args.roiwidth, + overwrite=args.overwrite, + filestep=args.filestep, + mcastep=args.mcastep, + concentrations=args.concentrations, + fitfiles=fitfiles, + filebeginoffset=args.filebeginoffset, + fileendoffset=args.fileendoffset, + mcaoffset=args.mcaoffset, + chunk=args.chunk, + selection=selection, + ) except Exception: - if exitonend: - _logger.warning("Error: ", sys.exc_info()[1]) + b = None + if args.exitonend: + _logger.warning("Error: %s", sys.exc_info()[1]) _logger.warning("Quitting as requested") qt.QApplication.instance().quit() else: msg = qt.QMessageBox() msg.setIcon(qt.QMessageBox.Critical) - msg.setText("%s" % sys.exc_info()[1]) + msg.setText(f"{sys.exc_info()[1]}") msg.exec() return - + # Cleanup and pause handling def cleanup(): + if b is None: + return b.pleasePause = 0 b.pleaseBreak = 1 if hasattr(b, "wait"): b.wait() - qApp = qt.QApplication.instance() - qApp.processEvents() + qt.QApplication.instance().processEvents() def pause(): + if b is None: + return if b.pleasePause: - b.pleasePause=0 + b.pleasePause = 0 window.pauseButton.setText("Pause") else: - b.pleasePause=1 + b.pleasePause = 1 window.pauseButton.setText("Continue") + window.pauseButton.clicked.connect(pause) window.abortButton.clicked.connect(window.close) - app.aboutToQuit[()].connect(cleanup) - window._rootname = "%s"% b._rootname - window.show() - b.start() - app.exec() - app = None + app.aboutToQuit.connect(cleanup) -if __name__ == "__main__": - main() + if b is not None: + window._rootname = str(b._rootname) + window.show() + b.start() + + # Auto-close for tests + if args.cli_test: + qt.QTimer.singleShot(0, app.quit) + + return app.exec() +def build_parser(): + parser = CliUtils.create_parser(description="Legacy PyMca Batch Processing GUI", add_qt_options=True) + + parser.add_argument("--cfg", type=str) + parser.add_argument("--cfglistfile", type=str) + parser.add_argument("--outdir", type=str) + parser.add_argument("--listfile", type=str) + parser.add_argument("--roifit", "--roi", type=int, default=0) + parser.add_argument("--roiwidth", type=float, default=ROIWIDTH) + parser.add_argument("--overwrite", type=int, default=1) + parser.add_argument("--filestep", type=int, default=1) + parser.add_argument("--mcastep", type=int, default=1) + parser.add_argument("--html", type=int, default=0) + parser.add_argument("--htmlindex", type=str) + parser.add_argument("--concentrations", type=int, default=0) + parser.add_argument("--table", type=int, default=2) + parser.add_argument("--fitfiles", type=int, default=1) + parser.add_argument("--filebeginoffset", type=int, default=0) + parser.add_argument("--fileendoffset", type=int, default=0) + parser.add_argument("--mcaoffset", type=int, default=0) + parser.add_argument("--chunk", type=int) + parser.add_argument("--selection", type=int, default=0) + parser.add_argument("--exitonend", type=int, default=0) + parser.add_argument("--showresult", type=int, default=1) + + parser.add_argument("files", nargs="*", help="Files to process if --listfile is not provided") + + return parser + + +if __name__ == "__main__": + PyMcaAppInit.init_before_app_create() + exit_code = CliUtils.cli_main(main, build_parser(), loggers=(_logger,)) + sys.exit(exit_code) + # LegacyPyMcaBatch.py --cfg=/mntdirect/_bliss/users/sole/COTTE/WithLead.cfg --outdir=/tmp/ /mntdirect/_bliss/users/sole/COTTE/ch09/ch09__mca_0003_0000_0007.edf /mntdirect/_bliss/users/sole/COTTE/ch09/ch09__mca_0003_0000_0008.edf /mntdirect/_bliss/users/sole/COTTE/ch09/ch09__mca_0003_0000_0009.edf /mntdirect/_bliss/users/sole/COTTE/ch09/ch09__mca_0003_0000_0010.edf /mntdirect/_bliss/users/sole/COTTE/ch09/ch09__mca_0003_0000_0011.edf /mntdirect/_bliss/users/sole/COTTE/ch09/ch09__mca_0003_0000_0012.edf /mntdirect/_bliss/users/sole/COTTE/ch09/ch09__mca_0003_0000_0013.edf & # LegacyPyMcaBatch.exe --cfg=E:/COTTE/WithLead.cfg --outdir=C:/tmp/ E:/COTTE/ch09/ch09__mca_0003_0000_0007.edf E:/COTTE/ch09/ch09__mca_0003_0000_0008.edf diff --git a/src/PyMca5/PyMcaGui/pymca/Mca2Edf.py b/src/PyMca5/PyMcaGui/pymca/Mca2Edf.py index 09f9a31d0..26d6a94fc 100644 --- a/src/PyMca5/PyMcaGui/pymca/Mca2Edf.py +++ b/src/PyMca5/PyMcaGui/pymca/Mca2Edf.py @@ -28,18 +28,17 @@ __contact__ = "sole@esrf.fr" __license__ = "MIT" __copyright__ = "European Synchrotron Radiation Facility, Grenoble, France" + +from PyMca5.PyMcaGui import PyMcaAppInit + +if __name__== '__main__': + PyMcaAppInit.init_before_app_import() + import sys import os import numpy import time -if __name__== '__main__': - # avoid issues if some module or dependency tries to use multiprocessing in frozen binaries - if getattr(sys, "frozen", False): - try: - import multiprocessing - multiprocessing.freeze_support() - except Exception: - pass + from PyMca5.PyMcaGui import PyMcaQt as qt QTVERSION = qt.qVersion() qt.Qt.WDestructiveClose = "TO BE DONE" @@ -49,6 +48,7 @@ from PyMca5.PyMcaIO import EdfFile from PyMca5.PyMcaCore import SpecFileLayer from PyMca5 import PyMcaDirs +from PyMca5.PyMcaMisc import CliUtils if sys.platform.startswith("darwin"): import threading @@ -56,6 +56,7 @@ else: QThread = qt.QThread + class Mca2EdfGUI(qt.QWidget): def __init__(self,parent=None,name="Mca to Edf Conversion",fl=qt.Qt.WDestructiveClose, filelist=None,outputdir=None, actions=0): @@ -442,76 +443,75 @@ def onPause(self): def onResume(self): pass -def main(): - import logging - from PyMca5.PyMcaCore.LoggingLevel import getLoggingLevel - import getopt - options = 'f' - longoptions = ['outdir=', 'listfile=', 'mcastep=', - 'logging=', 'debug='] - filelist = None - outdir = None - listfile = None - mcastep = 1 - opts, args = getopt.getopt( - sys.argv[1:], - options, - longoptions) - for opt, arg in opts: - if opt in ('--outdir'): - outdir = arg - elif opt in ('--listfile'): - listfile = arg - elif opt in ('--mcastep'): - mcastep = int(arg) - - logging.basicConfig(level=getLoggingLevel(opts)) - if listfile is None: - filelist=[] - for item in args: - filelist.append(item) + +def main(args): + # Prepare file list + if args.listfile is None: + filelist = args.files or [] else: - fd = open(listfile) - filelist = fd.readlines() - fd.close() - for i in range(len(filelist)): - filelist[i]=filelist[i].replace('\n','') - app=qt.QApplication(sys.argv) - app.lastWindowClosed.connect(app.quit) + with open(args.listfile, 'r') as fd: + filelist = [line.strip() for line in fd.readlines()] + + # Qt application + app = qt.QApplication([]) + PyMcaAppInit.init_before_app_start(qt_app=app, cli_args=args) + if len(filelist) == 0: + # GUI-only mode w = Mca2EdfGUI(actions=1) w.show() - sys.exit(app.exec()) else: - text = "Batch from %s to %s" % (os.path.basename(filelist[0]), \ - os.path.basename(filelist[-1])) - window = Mca2EdfWindow(name=text,actions=1) - b = Mca2EdfBatch(window,filelist,outdir,mcastep) + # Batch mode + text = f"Batch from {os.path.basename(filelist[0])} to {os.path.basename(filelist[-1])}" + window = Mca2EdfWindow(name=text, actions=1) + batch = Mca2EdfBatch(window, filelist, args.outdir, args.mcastep) + + # Cleanup function for application exit def cleanup(): - b.pleasePause = 0 - b.pleaseBreak = 1 - b.wait() + batch.pleasePause = 0 + batch.pleaseBreak = 1 + batch.wait() qApp = qt.QApplication.instance() qApp.processEvents() + # Pause toggle def pause(): - if b.pleasePause: - b.pleasePause=0 + if batch.pleasePause: + batch.pleasePause = 0 window.pauseButton.setText("Pause") else: - b.pleasePause=1 + batch.pleasePause = 1 window.pauseButton.setText("Continue") + window.pauseButton.clicked.connect(pause) window.abortButton.clicked.connect(window.close) app.aboutToQuit.connect(cleanup) + window.show() - b.start() - sys.exit(app.exec()) + batch.start() + + # Auto-close Qt for tests + if args.cli_test: + qt.QTimer.singleShot(0, app.quit) + + return app.exec() + + +def build_parser(): + parser = CliUtils.create_parser(description="Mca2Edf batch/GUI converter", add_qt_options=True) + + parser.add_argument("--outdir", type=str, help="Output directory") + parser.add_argument("--listfile", type=str, help="List of input files") + parser.add_argument("--mcastep", type=int, default=1) + + parser.add_argument("files", nargs="*", help="Files to process if --listfile is not provided") + + return parser + if __name__ == "__main__": - main() + PyMcaAppInit.init_before_app_create() + exit_code = CliUtils.cli_main(main, build_parser()) + sys.exit(exit_code) # Mca2Edf.py --outdir=/tmp --mcastep=1 *.mca - -# PyMcaBatch.py --cfg=/mntdirect/_bliss/users/sole/COTTE/WithLead.cfg --outdir=/tmp/ /mntdirect/_bliss/users/sole/COTTE/ch09/ch09__mca_0003_0000_0007.edf /mntdirect/_bliss/users/sole/COTTE/ch09/ch09__mca_0003_0000_0008.edf /mntdirect/_bliss/users/sole/COTTE/ch09/ch09__mca_0003_0000_0009.edf /mntdirect/_bliss/users/sole/COTTE/ch09/ch09__mca_0003_0000_0010.edf /mntdirect/_bliss/users/sole/COTTE/ch09/ch09__mca_0003_0000_0011.edf /mntdirect/_bliss/users/sole/COTTE/ch09/ch09__mca_0003_0000_0012.edf /mntdirect/_bliss/users/sole/COTTE/ch09/ch09__mca_0003_0000_0013.edf & -# PyMcaBatch.exe --cfg=E:/COTTE/WithLead.cfg --outdir=C:/tmp/ E:/COTTE/ch09/ch09__mca_0003_0000_0007.edf E:/COTTE/ch09/ch09__mca_0003_0000_0008.edf diff --git a/src/PyMca5/PyMcaGui/pymca/PyMcaBatch.py b/src/PyMca5/PyMcaGui/pymca/PyMcaBatch.py index 6bf690d57..ce3f8a067 100644 --- a/src/PyMca5/PyMcaGui/pymca/PyMcaBatch.py +++ b/src/PyMca5/PyMcaGui/pymca/PyMcaBatch.py @@ -28,6 +28,12 @@ __contact__ = "sole@esrf.fr" __license__ = "MIT" __copyright__ = "European Synchrotron Radiation Facility, Grenoble, France" + +from PyMca5.PyMcaGui import PyMcaAppInit + +if __name__== '__main__': + PyMcaAppInit.init_before_app_import() + import sys import os import time @@ -36,16 +42,8 @@ import atexit import logging import traceback -if __name__== '__main__': - # avoid issues if some module or dependency tries to use multiprocessing in frozen binaries - if getattr(sys, "frozen", False): - try: - import multiprocessing - multiprocessing.freeze_support() - except Exception: - pass + from glob import glob -from contextlib import contextmanager try: from collections.abc import MutableMapping except ImportError: @@ -80,6 +78,7 @@ from PyMca5.PyMcaCore import HtmlIndex from PyMca5.PyMcaCore import PyMcaDirs from PyMca5.PyMcaCore import PyMcaBatchBuildOutput +from PyMca5.PyMcaMisc import CliUtils ROIWIDTH = 100. @@ -1927,213 +1926,179 @@ def plotImages(self,imagelist): launchProcess(cmd, independent=True) -def main(): - sys.excepthook = qt.exceptionHandler - import getopt - from PyMca5.PyMcaCore.LoggingLevel import getLoggingLevel - options = 'f' - longoptions = ['cfg=','outdir=','roifit=','roi=','roiwidth=', - 'overwrite=', 'filestep=', 'mcastep=', 'html=','htmlindex=', - 'listfile=','cfglistfile=', 'concentrations=', 'table=', 'fitfiles=', - 'filebeginoffset=','fileendoffset=','mcaoffset=', 'chunk=', - 'nativefiledialogs=','selection=', 'exitonend=', - 'edf=', 'h5=', 'csv=', 'tif=', 'dat=', 'diagnostics=', - 'logging=', 'debug=', 'gui=', 'multipage=', 'nproc=', - 'showresult='] - filelist = None - outdir = None - cfg = None - listfile = None - cfglistfile = None - selection = False - roifit = 0 - roiwidth = ROIWIDTH - overwrite= 1 - filestep = 1 - html = 0 - htmlindex= None - mcastep = 1 - table = 2 - fitfiles = 0 - concentrations = 0 - filebeginoffset = 0 - fileendoffset = 0 - mcaoffset = 0 - chunk = None - exitonend = False - showresult = True - gui = 0 - diagnostics = 0 - tif = 0 - edf = 1 - csv = 0 - h5 = 1 - dat = 1 - multipage = 0 - nproc = 1 - opts, args = getopt.getopt( - sys.argv[1:], - options, - longoptions) - for opt,arg in opts: - if opt in ('--cfg'): - cfg = arg - elif opt in ('--outdir'): - outdir = arg - elif opt in ('--roi','--roifit'): - roifit = int(arg) - elif opt in ('--roiwidth'): - roiwidth = float(arg) - elif opt in ('--overwrite'): - overwrite= int(arg) - elif opt in ('--filestep'): - filestep = int(arg) - elif opt in ('--mcastep'): - mcastep = int(arg) - elif opt in ('--html'): - html = int(arg) - elif opt in ('--htmlindex'): - htmlindex = arg - elif opt in ('--listfile'): - listfile = arg - elif opt in ('--cfglistfile'): - cfglistfile = arg - elif opt in ('--concentrations'): - concentrations = int(arg) - elif opt in ('--table'): - table = int(arg) - elif opt in ('--fitfiles'): - fitfiles = int(arg) - elif opt in ('--filebeginoffset'): - filebeginoffset = int(arg) - elif opt in ('--fileendoffset'): - fileendoffset = int(arg) - elif opt in ('--mcaoffset'): - mcaoffset = int(arg) - elif opt in ('--chunk'): - chunk = int(arg) - elif opt in ('--gui'): - gui = int(arg) - elif opt in ('--selection'): - selection = int(arg) - if selection: - selection = True - else: - selection = False - elif opt in ('--nativefiledialogs'): - if int(arg): - PyMcaDirs.nativeFileDialogs = True - else: - PyMcaDirs.nativeFileDialogs = False - elif opt in ('--exitonend'): - exitonend = int(arg) - elif opt in ('--showresult'): - showresult = int(arg) - elif opt == '--diagnostics': - diagnostics = int(arg) - elif opt == '--edf': - edf = int(arg) - elif opt == '--csv': - csv = int(arg) - elif opt == '--h5': - h5 = int(arg) - elif opt == '--dat': - dat = int(arg) - elif opt == '--tif': - tif = int(arg) - elif opt == '--multipage': - multipage = int(arg) - elif opt == '--nproc': - nproc = max(int(arg), 0) - level = getLoggingLevel(opts) - logging.basicConfig(level=level) - _logger.setLevel(level) - - # Files to fit: - if listfile is None: - filelist=[] - for item in args: - filelist.append(item) +def main(args): + if args.cli_test and not args.listfile: + print("No input files provided.") + return 0 + + # Prepare file list + if args.listfile is None: + filelist = args.files or [] selection = None else: - if selection: + if args.selection: tmpDict = ConfigDict.ConfigDict() - tmpDict.read(listfile) + tmpDict.read(args.listfile) tmpDict = tmpDict['PyMcaBatch'] filelist = tmpDict['filelist'] - if type(filelist) == type(""): + if isinstance(filelist, str): filelist = [filelist] selection = tmpDict['selection'] else: - fd = open(listfile, 'rb') - filelist = fd.readlines() - fd.close() - for i in range(len(filelist)): - filelist[i]=filelist[i].decode(sys.getfilesystemencoding()).replace('\n','') + with open(args.listfile, 'rb') as fd: + filelist = [line.decode(sys.getfilesystemencoding()).strip() for line in fd.readlines()] selection = None - - # Configurations to use: - if cfglistfile is not None: - fd = open(cfglistfile, 'rb') - cfg = fd.readlines() - fd.close() - for i in range(len(cfg)): - cfg[i]=cfg[i].decode(sys.getfilesystemencoding()).replace('\n','') - - # Launch + + # Prepare configurations + cfg = args.cfg + if args.cfglistfile is not None: + with open(args.cfglistfile, 'rb') as fd: + cfg = [line.decode(sys.getfilesystemencoding()).strip() for line in fd.readlines()] + app = qt.QApplication([]) - if html: - fitfiles=1 - if len(filelist) == 0 or gui: - # Launch GUI when no files are provided - app.lastWindowClosed.connect(app.quit) - w = McaBatchGUI(actions=1,filelist=filelist,config=cfg,outputdir=outdir, - roifit=roifit,roiwidth=roiwidth,overwrite=overwrite, - concentrations=concentrations, fitfiles=fitfiles, - diagnostics=diagnostics, multipage=multipage, - tif=tif, edf=edf, csv=csv, h5=h5, dat=dat, nproc=nproc, - table=table, html=html) + PyMcaAppInit.init_before_app_start(qt_app=app, cli_args=args) + + if args.html: + fitfiles = 1 + else: + fitfiles = args.fitfiles + + if not filelist or args.gui: + # Launch GUI + w = McaBatchGUI( + actions=1, + filelist=filelist, + config=cfg, + outputdir=args.outdir, + roifit=args.roifit, + roiwidth=args.roiwidth, + overwrite=args.overwrite, + concentrations=args.concentrations, + fitfiles=fitfiles, + diagnostics=args.diagnostics, + multipage=args.multipage, + tif=args.tif, + edf=args.edf, + csv=args.csv, + h5=args.h5, + dat=args.dat, + nproc=args.nproc, + table=args.table, + html=args.html + ) w.show() w.raise_() else: - # Launch processing thread when files are provided - app.lastWindowClosed.connect(app.quit) - text = "Batch from %s to %s" % (os.path.basename(filelist[0]), os.path.basename(filelist[-1])) - window = McaBatchWindow(name=text,actions=1, - outputdir=outdir,html=html, htmlindex=htmlindex, table=table, - chunk=chunk, exitonend=exitonend, showresult=showresult) + # Launch processing thread + text = f"Batch from {os.path.basename(filelist[0])} to {os.path.basename(filelist[-1])}" + window = McaBatchWindow( + name=text, + actions=1, + outputdir=args.outdir, + html=args.html, + htmlindex=args.htmlindex, + table=args.table, + chunk=args.chunk, + exitonend=args.exitonend, + showresult=args.showresult + ) try: - thread = McaBatch(window,cfg,filelist=filelist,outputdir=outdir,roifit=roifit,roiwidth=roiwidth, - overwrite=overwrite, filestep=filestep, mcastep=mcastep, - concentrations=concentrations, fitfiles=fitfiles, - filebeginoffset=filebeginoffset,fileendoffset=fileendoffset, - mcaoffset=mcaoffset, chunk=chunk, selection=selection, - diagnostics=diagnostics, multipage=multipage, - tif=tif, edf=edf, csv=csv, h5=h5, dat=dat) - except Exception: - if exitonend: - _logger.warning("Error: ", sys.exc_info()[1]) + thread = McaBatch( + window, + cfg, + filelist=filelist, + outputdir=args.outdir, + roifit=args.roifit, + roiwidth=args.roiwidth, + overwrite=args.overwrite, + filestep=args.filestep, + mcastep=args.mcastep, + concentrations=args.concentrations, + fitfiles=fitfiles, + filebeginoffset=args.filebeginoffset, + fileendoffset=args.fileendoffset, + mcaoffset=args.mcaoffset, + chunk=args.chunk, + selection=selection, + diagnostics=args.diagnostics, + multipage=args.multipage, + tif=args.tif, + edf=args.edf, + csv=args.csv, + h5=args.h5, + dat=args.dat + ) + except Exception as ex: + thread = None + if args.exitonend: + _logger.warning("Error: %s", ex) _logger.warning("Quitting as requested") qt.QApplication.instance().quit() else: msg = qt.QMessageBox() msg.setIcon(qt.QMessageBox.Critical) - msg.setText("%s" % sys.exc_info()[1]) + msg.setText(str(ex)) msg.exec() return - window._rootname = "%s"% thread._rootname - launchThread(thread, window) + if thread is not None: + window._rootname = f"{thread._rootname}" + launchThread(thread, window) + + # Auto-close Qt application for tests + if args.cli_test: + qt.QTimer.singleShot(0, app.quit) + + return app.exec() + + +def build_parser(): + parser = CliUtils.create_parser(description="PyMca Batch Processing GUI", add_qt_options=True) + + parser.add_argument("--cfg", type=str, help="Configuration file") + parser.add_argument("--cfglistfile", type=str, help="Configuration list file") + parser.add_argument("--outdir", type=str, help="Output directory") + parser.add_argument("--listfile", type=str, help="List of input files") + parser.add_argument("--roifit", "--roi", type=int, default=0) + parser.add_argument("--roiwidth", type=float, default=ROIWIDTH) + parser.add_argument("--overwrite", type=int, default=1) + parser.add_argument("--filestep", type=int, default=1) + parser.add_argument("--mcastep", type=int, default=1) + parser.add_argument("--html", type=int, default=0) + parser.add_argument("--htmlindex", type=str, default=None) + parser.add_argument("--concentrations", type=int, default=0) + parser.add_argument("--table", type=int, default=2) + parser.add_argument("--fitfiles", type=int, default=0) + parser.add_argument("--filebeginoffset", type=int, default=0) + parser.add_argument("--fileendoffset", type=int, default=0) + parser.add_argument("--mcaoffset", type=int, default=0) + parser.add_argument("--chunk", type=int, default=None) + parser.add_argument("--selection", type=int, default=0) + parser.add_argument("--exitonend", type=int, default=0) + parser.add_argument("--showresult", type=int, default=1) + + # Perhaps this flag was originally intended to be equivalent to --fitfiles=1 + parser.add_argument("-f", action="store_true", help="UNUSED") + + parser.add_argument("--gui", type=int, default=0) + parser.add_argument("--diagnostics", type=int, default=0) + parser.add_argument("--tif", type=int, default=0) + parser.add_argument("--edf", type=int, default=1) + parser.add_argument("--csv", type=int, default=0) + parser.add_argument("--h5", type=int, default=1) + parser.add_argument("--dat", type=int, default=1) + parser.add_argument("--multipage", type=int, default=0) + parser.add_argument("--nproc", type=int, default=1) + + parser.add_argument("files", nargs="*", help="Files to process if --listfile is not provided") + + return parser - app.exec() - app = None if __name__ == "__main__": - # We are going to read. Disable file locking. - os.environ["HDF5_USE_FILE_LOCKING"] = "FALSE" - _logger.info("%s set to %s" % ("HDF5_USE_FILE_LOCKING", - os.environ["HDF5_USE_FILE_LOCKING"])) - main() - -# PyMcaBatch.py --cfg=/mntdirect/_bliss/users/sole/COTTE/WithLead.cfg --outdir=/tmp/ /mntdirect/_bliss/users/sole/COTTE/ch09/ch09__mca_0003_0000_0007.edf /mntdirect/_bliss/users/sole/COTTE/ch09/ch09__mca_0003_0000_0008.edf /mntdirect/_bliss/users/sole/COTTE/ch09/ch09__mca_0003_0000_0009.edf /mntdirect/_bliss/users/sole/COTTE/ch09/ch09__mca_0003_0000_0010.edf /mntdirect/_bliss/users/sole/COTTE/ch09/ch09__mca_0003_0000_0011.edf /mntdirect/_bliss/users/sole/COTTE/ch09/ch09__mca_0003_0000_0012.edf /mntdirect/_bliss/users/sole/COTTE/ch09/ch09__mca_0003_0000_0013.edf & -# PyMcaBatch.exe --cfg=E:/COTTE/WithLead.cfg --outdir=C:/tmp/ E:/COTTE/ch09/ch09__mca_0003_0000_0007.edf E:/COTTE/ch09/ch09__mca_0003_0000_0008.edf + PyMcaAppInit.init_before_app_create() + exit_code = CliUtils.cli_main(main, build_parser(), loggers=(_logger,)) + sys.exit(exit_code) + + # PyMcaBatch.py --cfg=/mntdirect/_bliss/users/sole/COTTE/WithLead.cfg --outdir=/tmp/ /mntdirect/_bliss/users/sole/COTTE/ch09/ch09__mca_0003_0000_0007.edf /mntdirect/_bliss/users/sole/COTTE/ch09/ch09__mca_0003_0000_0008.edf /mntdirect/_bliss/users/sole/COTTE/ch09/ch09__mca_0003_0000_0009.edf /mntdirect/_bliss/users/sole/COTTE/ch09/ch09__mca_0003_0000_0010.edf /mntdirect/_bliss/users/sole/COTTE/ch09/ch09__mca_0003_0000_0011.edf /mntdirect/_bliss/users/sole/COTTE/ch09/ch09__mca_0003_0000_0012.edf /mntdirect/_bliss/users/sole/COTTE/ch09/ch09__mca_0003_0000_0013.edf & + # PyMcaBatch.exe --cfg=E:/COTTE/WithLead.cfg --outdir=C:/tmp/ E:/COTTE/ch09/ch09__mca_0003_0000_0007.edf E:/COTTE/ch09/ch09__mca_0003_0000_0008.edf diff --git a/src/PyMca5/PyMcaGui/pymca/PyMcaMain.py b/src/PyMca5/PyMcaGui/pymca/PyMcaMain.py index 909673f0e..e2fc6a685 100644 --- a/src/PyMca5/PyMcaGui/pymca/PyMcaMain.py +++ b/src/PyMca5/PyMcaGui/pymca/PyMcaMain.py @@ -28,133 +28,29 @@ __contact__ = "sole@esrf.fr" __license__ = "MIT" __copyright__ = "European Synchrotron Radiation Facility, Grenoble, France" + +from PyMca5.PyMcaGui import PyMcaAppInit + +if __name__== '__main__': + PyMcaAppInit.init_before_app_import() + import os -import sys, getopt +import sys +import profile +import pstats import traceback import logging -if __name__== '__main__': - # avoid issues if some module or dependency tries to use multiprocessing in frozen binaries - if getattr(sys, "frozen", False): - try: - import multiprocessing - multiprocessing.freeze_support() - except Exception: - pass + if sys.platform == 'win32': import ctypes from ctypes.wintypes import MAX_PATH -nativeFileDialogs = None -_logger = logging.getLogger(__name__) -backend=None -if __name__ == '__main__': - options = '-f' - longoptions = ['spec=', - 'shm=', - 'debug=', - 'qt=', - 'backend=', - 'nativefiledialogs=', - 'PySide=', - 'binding=', - 'logging=', - 'test'] - try: - opts, args = getopt.getopt( - sys.argv[1:], - options, - longoptions) - except getopt.error: - print("%s" % sys.exc_info()[1]) - sys.exit(1) - - keywords={} - debugreport = 0 - qtversion = None - binding = None - for opt, arg in opts: - if opt in ('--spec'): - keywords['spec'] = arg - elif opt in ('--shm'): - keywords['shm'] = arg - elif opt in ('--debug'): - if arg.lower() not in ['0', 'false']: - debugreport = 1 - _logger.setLevel(logging.DEBUG) - # --debug is also parsed later for the global logging level - elif opt in ('-f'): - keywords['fresh'] = 1 - elif opt in ('--qt'): - qtversion = arg - elif opt in ('--backend'): - backend = arg - elif opt in ('--nativefiledialogs'): - if int(arg): - nativeFileDialogs = True - else: - nativeFileDialogs = False - elif opt in ('--PySide'): - print("Please use --binding=PySide6") - import PySide6.QtCore - elif opt in ('--binding'): - binding = arg.lower() - if binding == "pyqt5": - import PyQt5.QtCore - elif binding == "pyside2": - import PySide2.QtCore - elif binding == "pyside6": - import PySide6.QtCore - elif binding == "pyqt6": - import PyQt6.QtCore - else: - raise ValueError("Unknown Qt binding <%s>" % binding) - elif opt in ('--test'): - try: - from PyMca5.tests import TestAll - print("Running PyMca Unit Tests...") - result = TestAll.main() - exit_code = 0 if result.wasSuccessful() else 1 - print('exit code: ', exit_code) - sys.exit(exit_code) - except Exception as e: - import traceback - print("Failed to run tests:", e) - traceback.print_exc() - sys.exit(1) - - from PyMca5.PyMcaCore.LoggingLevel import getLoggingLevel - logging.basicConfig(level=getLoggingLevel(opts)) - # We are going to read. Disable file locking. - os.environ["HDF5_USE_FILE_LOCKING"] = "FALSE" - _logger.info("%s set to %s" % ("HDF5_USE_FILE_LOCKING", - os.environ["HDF5_USE_FILE_LOCKING"])) - if binding is None: - if qtversion in ('3', '4'): - raise NotImplementedError("Qt%d is no longer supported" % int(qtversion)) - elif qtversion == '5': - try: - import PyQt5.QtCore - except ImportError: - import PySide2.QtCore - elif qtversion == '6': - import PySide6.QtCore - try: - # make sure hdf5plugins are imported - import hdf5plugin - except Exception: - _logger.info("Failed to import hdf5plugin") +from PyMca5.PyMcaMisc import CliUtils from PyMca5.PyMcaGui import PyMcaQt as qt from PyMca5.PyMcaGui.io import PyMcaFileDialogs QTVERSION = qt.qVersion() -try: - import silx - # try to import silx prior to importing matplotlib to prevent - # unnecessary warning - import silx.gui.plot -except Exception: - pass from PyMca5.PyMcaGui.pymca import PyMcaMdi IconDict = PyMcaMdi.IconDict IconDict0 = PyMcaMdi.IconDict0 @@ -182,59 +78,7 @@ from PyMca5 import PyMcaDataDir __version__ = PyMca5.version() -if __name__ == "__main__": - sys.excepthook = qt.exceptionHandler - - app = qt.QApplication(sys.argv) - - if sys.platform not in ["win32", "darwin"]: - # some themes of Ubuntu 16.04 give black tool tips on black background - try: - _ttp = qt.QApplication.instance().palette() - _ttText = _ttp.color(qt.QPalette.ToolTipText).name() - _ttBase = _ttp.color(qt.QPalette.ToolTipBase).name() - app.setStyleSheet("QToolTip { color: %s; background-color: %s; border: 1px solid %s; }" % (_ttText, _ttBase, _ttText)) - except Exception: - app.setStyleSheet("QToolTip { color: #000000; background-color: #fff0cd; border: 1px solid black; }") - - mpath = PyMcaDataDir.PYMCA_DATA_DIR - if mpath[-3:] == "exe": - mpath = os.path.dirname(mpath) - - fname = os.path.join(mpath, 'PyMcaSplashImage.png') - if not os.path.exists(fname): - while len(mpath) > 3: - fname = os.path.join(mpath, 'PyMcaSplashImage.png') - if not os.path.exists(fname): - mpath = os.path.dirname(mpath) - else: - break - if os.path.exists(fname): - pixmap = qt.QPixmap(QString(fname)) - splash = qt.QSplashScreen(pixmap) - else: - splash = qt.QSplashScreen() - splash.show() - splash.raise_() - from PyMca5.PyMcaGui.pymca import ChangeLog - font = splash.font() - font.setBold(1) - splash.setFont(font) - try: - # there is a deprecation warning in Python 3.8 when - # dealing with the alignement flags. - alignment = int(qt.Qt.AlignLeft|qt.Qt.AlignBottom) - splash.showMessage( 'PyMca %s' % __version__, - alignment, - qt.Qt.white) - except Exception: - # fall back to original implementation in case of troubles - splash.showMessage( 'PyMca %s' % __version__, - qt.Qt.AlignLeft|qt.Qt.AlignBottom, - qt.Qt.white) - if sys.platform == "darwin": - qApp = qt.QApplication.instance() - qApp.processEvents() +_logger = logging.getLogger(__name__) from PyMca5.PyMcaGraph.Plot import Plot from PyMca5.PyMcaGui.pymca import ScanWindow @@ -308,7 +152,6 @@ from PyMca5.PyMcaGui.physics.xrf import ElementsInfo from PyMca5.PyMcaGui.physics.xrf import PeakIdentifier from PyMca5.PyMcaGui.pymca import PyMcaBatch -###########import Fit2Spec from PyMca5.PyMcaGui.pymca import Mca2Edf try: from PyMca5.PyMcaGui.pymca import QStackWidget @@ -319,6 +162,7 @@ from PyMca5.PyMcaGui.pymca import PyMcaPostBatch from PyMca5.PyMcaGui.pymca import RGBCorrelator from PyMca5.PyMcaGui.physics.xrf import MaterialEditor +from PyMca5.PyMcaGui.pymca import ChangeLog from PyMca5.PyMcaIO import ConfigDict from PyMca5 import PyMcaDirs @@ -333,7 +177,9 @@ SOURCESLIST = QDispatcher.QDataSource.source_types.keys() class PyMcaMain(PyMcaMdi.PyMcaMdi): - def __init__(self, parent=None, name="PyMca", fl=None,**kw): + def __init__( + self, parent=None, name="PyMca", fl=None, spec=None, shm=None, fresh=None, backend=None + ): if fl is None: fl = qt.Qt.WA_DeleteOnClose PyMcaMdi.PyMcaMdi.__init__(self, parent, name, fl) @@ -418,9 +264,9 @@ def __init__(self, parent=None, name="PyMca", fl=None,**kw): self.sourceWidget.sigOtherSignals.connect( \ self.dispatcherOtherSignalsSlot) if 0: - if 'shm' in kw: - if len(kw['shm']) >= 8: - if kw['shm'][0:8] in ['MCA_DATA', 'XIA_DATA']: + if shm: + if len(shm) >= 8: + if shm[0:8] in ['MCA_DATA', 'XIA_DATA']: self.mcaWindow.showMaximized() self.toggleSource() else: @@ -430,25 +276,25 @@ def __init__(self, parent=None, name="PyMca", fl=None,**kw): defaultFileName = PyMca5.getDefaultSettingsFile() self.configDir = os.path.dirname(defaultFileName) except Exception: - if not ('fresh' in kw): + if not fresh: raise - if not ('fresh' in kw): + if not fresh: if os.path.exists(defaultFileName): currentConfigDict.read(defaultFileName) self.setConfig(currentConfigDict) - if ('spec' in kw) and ('shm' in kw): - if len(kw['shm']) >= 8: - #if kw['shm'][0:8] in ['MCA_DATA', 'XIA_DATA']: - if kw['shm'][0:8] in ['MCA_DATA']: + if spec and shm: + if len(shm) >= 8: + #if shm[0:8] in ['MCA_DATA', 'XIA_DATA']: + if shm[0:8] in ['MCA_DATA']: #self.mcaWindow.showMaximized() self.toggleSource() - self._startupSelection(source=kw['spec'], - selection=kw['shm']) + self._startupSelection(source=spec, + selection=shm) else: - self._startupSelection(source=kw['spec'], + self._startupSelection(source=spec, selection=None) else: - self._startupSelection(source=kw['spec'], + self._startupSelection(source=spec, selection=None) def connectDispatcher(self, viewer, dispatcher=None): @@ -1321,6 +1167,8 @@ def __mca2EdfConversion(self): self.__mca2Edf.raise_() def __fit2SpecConversion(self): + from PyMca5.PyMcaGui.pymca import Fit2Spec + if self.__fit2Spec is None: self.__fit2Spec = Fit2Spec.Fit2SpecGUI(fl=0,actions=1) if self.__fit2Spec.isHidden(): @@ -1854,42 +1702,133 @@ def mousePressEvent(self,event): ddict['data'] = event self.sigPixmapLabelMousePressEvent.emit(ddict) -if __name__ == '__main__': - PROFILING = 0 - if PROFILING: - import profile - import pstats - PyMcaMainWidgetInstance = PyMcaMain(**keywords) - if nativeFileDialogs is not None: - PyMcaDirs.nativeFileDialogs = nativeFileDialogs - if debugreport: - PyMcaMainWidgetInstance.onDebug() - app.lastWindowClosed.connect(app.quit) - - splash.finish(PyMcaMainWidgetInstance) - PyMcaMainWidgetInstance.show() - PyMcaMainWidgetInstance.raise_() - PyMcaMainWidgetInstance.mcaWindow.replot() - - #try to interpret rest of command line arguments as data sources + +def run_tests(): + """Run PyMca unit tests.""" try: - for source in args: - PyMcaMainWidgetInstance.sourceWidget.sourceSelector.openSource(source) + from PyMca5.tests import TestAll + print("Running PyMca Unit Tests...") + result = TestAll.main() + exit_code = 0 if result.wasSuccessful() else 1 + print("exit code:", exit_code) + return exit_code + except Exception as e: + print("Failed to run tests:", e) + traceback.print_exc() + return 1 + + +def _splash_screen(): + mpath = PyMcaDataDir.PYMCA_DATA_DIR + if mpath[-3:] == "exe": + mpath = os.path.dirname(mpath) + + fname = os.path.join(mpath, 'PyMcaSplashImage.png') + if not os.path.exists(fname): + while len(mpath) > 3: + fname = os.path.join(mpath, 'PyMcaSplashImage.png') + if not os.path.exists(fname): + mpath = os.path.dirname(mpath) + else: + break + + if os.path.exists(fname): + pixmap = qt.QPixmap(QString(fname)) + splash = qt.QSplashScreen(pixmap) + else: + splash = qt.QSplashScreen() + + splash.show() + splash.raise_() + + # Add PyMca version to splash screen + font = splash.font() + font.setBold(1) + splash.setFont(font) + try: + # there is a deprecation warning in Python 3.8 when + # dealing with the alignment flags. + splash.showMessage(f'PyMca {__version__}', + int(qt.Qt.AlignLeft|qt.Qt.AlignBottom), + qt.Qt.white) except Exception: - msg = qt.QMessageBox(PyMcaMainWidgetInstance) - msg.setIcon(qt.QMessageBox.Critical) - msg.setWindowTitle("Error opening data source") - msg.setText("Cannot open data source %s" % source) - msg.setInformativeText(str(sys.exc_info()[1])) - msg.setDetailedText(traceback.format_exc()) - msg.exec() - - if PROFILING: - profile.run('sys.exit(app.exec())',"test") - p=pstats.Stats("test") + # fall back to original implementation in case of troubles + splash.showMessage(f'PyMca {__version__}', + qt.Qt.AlignLeft|qt.Qt.AlignBottom, + qt.Qt.white) + + # Ensure the splash screen is rendered + if sys.platform == "darwin": + qApp = qt.QApplication.instance() + qApp.processEvents() + + return splash + + +def main(args): + # Handle test mode immediately + if args.test: + return run_tests() + + # Initialize Qt application + app = qt.QApplication([]) + PyMcaAppInit.init_before_app_start(qt_app=app, cli_args=args) + + # Splash screen + splash = _splash_screen() + + # Launch main PyMca GUI + main_widget = PyMcaMain(spec=args.spec, shm=args.shm, fresh=args.fresh, backend=args.backend) + if args.debug: + main_widget.onDebug() + + # Close splash screen and show main window + splash.finish(main_widget) + main_widget.show() + main_widget.raise_() + main_widget.mcaWindow.replot() + + # Attempt to open files + for source in args.files: + try: + main_widget.sourceWidget.sourceSelector.openSource(source) + except Exception: + msg = qt.QMessageBox(main_widget) + msg.setIcon(qt.QMessageBox.Critical) + msg.setWindowTitle("Error opening data source") + msg.setText(f"Cannot open data source {source}") + msg.setInformativeText(str(sys.exc_info()[1])) + msg.setDetailedText(traceback.format_exc()) + msg.exec() + + # If running in test mode, quit immediately + if args.cli_test: + qt.QTimer.singleShot(0, app.quit) + + if args.profiling: + profile.runctx('app.exec()', globals=dict(), locals=dict(app=app), filename='profile_output') + p = pstats.Stats('profile_output') p.strip_dirs().sort_stats(-1).print_stats() else: - ret = app.exec() - app = None - sys.exit(ret) + return app.exec() + +def build_parser(): + parser = CliUtils.create_parser(description="Main PyMca GUI", add_qt_options=True, add_backend_options=True) + + parser.add_argument("--spec", type=str, default=None, help="Spec file") + parser.add_argument("--shm", type=str, default=None, help="Shared memory spec") + + parser.add_argument("--fresh", "-f", action="store_true", help="Clear configuration") + parser.add_argument("--profiling", action="store_true", help="Run main loop under profiler") + parser.add_argument("--test", action="store_true", help="Run unit tests") + + parser.add_argument("files", nargs="*", help="Optional list of data files to open") + + return parser + + +if __name__ == "__main__": + PyMcaAppInit.init_before_app_create() + exit_code = CliUtils.cli_main(main, build_parser(), loggers=(_logger,)) + sys.exit(exit_code) diff --git a/src/PyMca5/PyMcaGui/pymca/PyMcaMdi.py b/src/PyMca5/PyMcaGui/pymca/PyMcaMdi.py index dcc34f40b..100e4f420 100644 --- a/src/PyMca5/PyMcaGui/pymca/PyMcaMdi.py +++ b/src/PyMca5/PyMcaGui/pymca/PyMcaMdi.py @@ -27,8 +27,15 @@ __contact__ = "sole@esrf.fr" __license__ = "MIT" __copyright__ = "European Synchrotron Radiation Facility, Grenoble, France" -import sys, getopt, string + +from PyMca5.PyMcaGui import PyMcaAppInit + +if __name__== '__main__': + PyMcaAppInit.init_before_app_import() + +import sys import logging + from PyMca5.PyMcaGui import PyMcaQt as qt if hasattr(qt, "QString"): QString = qt.QString @@ -40,11 +47,14 @@ from PyMca5.PyMcaGui.plotting import PyMca_Icons IconDict = PyMca_Icons.IconDict IconDict0 = PyMca_Icons.IconDict0 -from .PyMca_help import HelpDict + +from PyMca5.PyMcaMisc import CliUtils + _logger = logging.getLogger(__name__) __version__ = "1.5" + class PyMcaMdi(qt.QMainWindow): def __init__(self, parent=None, name="PyMca Mdi", fl=None, options={}): qt.QMainWindow.__init__(self, parent) @@ -419,32 +429,40 @@ def onSaveAs(self): def onPrint(self): qt.QMessageBox.about(self, "Print", "Not implemented") + def main(args): - app = qt.QApplication(args) - - options = '' - longoptions = ['spec=','shm='] - try: - opts, args = getopt.getopt( - sys.argv[1:], - options, - longoptions) - except getopt.error: - _logger.error(sys.exc_info()[1]) - sys.exit(1) - # --- waiting widget - kw={} - for opt, arg in opts: - if opt in ('--spec'): - kw['spec'] = arg - elif opt in ('--shm'): - kw['shm'] = arg - #demo = McaWindow.McaWidget(**kw) - demo = PyMcaMdi() - app.lastWindowClosed.connect(app.quit) + # Qt application + app = qt.QApplication([]) + PyMcaAppInit.init_before_app_start(qt_app=app, cli_args=args) + + # Extract optional keyword args + kw = {} + if args.spec: + kw["spec"] = args.spec + if args.shm: + kw["shm"] = args.shm + + # Launch PyMcaMdi + demo = PyMcaMdi(**kw) demo.show() - app.exec() -if __name__ == '__main__': - main(sys.argv) + # If running in test mode, quit immediately + if args.cli_test: + qt.QTimer.singleShot(0, app.quit) + + return app.exec() + + +def build_parser(): + parser = CliUtils.create_parser(description="PyMcaMdi launcher", add_qt_options=True) + + parser.add_argument("--spec", type=str, default=None, help="Optional spec file") + parser.add_argument("--shm", type=str, default=None, help="Optional shared memory") + + return parser + +if __name__ == "__main__": + PyMcaAppInit.init_before_app_create() + exit_code = CliUtils.cli_main(main, build_parser(), loggers=(_logger,)) + sys.exit(exit_code) diff --git a/src/PyMca5/PyMcaGui/pymca/PyMcaPostBatch.py b/src/PyMca5/PyMcaGui/pymca/PyMcaPostBatch.py index 0e79ae0df..98fdad630 100644 --- a/src/PyMca5/PyMcaGui/pymca/PyMcaPostBatch.py +++ b/src/PyMca5/PyMcaGui/pymca/PyMcaPostBatch.py @@ -28,33 +28,22 @@ __contact__ = "sole@esrf.fr" __license__ = "MIT" __copyright__ = "European Synchrotron Radiation Facility, Grenoble, France" + +from PyMca5.PyMcaGui import PyMcaAppInit + +if __name__== '__main__': + PyMcaAppInit.init_before_app_import() + import sys import os import logging -if __name__== '__main__': - # avoid issues if some module or dependency tries to use multiprocessing in frozen binaries - if getattr(sys, "frozen", False): - try: - import multiprocessing - multiprocessing.freeze_support() - except Exception: - pass -_logger = logging.getLogger(__name__) -if __name__ == "__main__": - # We are going to read. Disable file locking. - os.environ["HDF5_USE_FILE_LOCKING"] = "FALSE" - _logger.info("%s set to %s" % ("HDF5_USE_FILE_LOCKING", - os.environ["HDF5_USE_FILE_LOCKING"])) - try: - # make sure hdf5plugins are imported - import hdf5plugin - except Exception: - _logger.info("Failed to import hdf5plugin") -from PyMca5 import PyMcaDirs from PyMca5.PyMcaGui import PyMcaQt as qt +from PyMca5 import PyMcaDirs from PyMca5.PyMcaGui.io import PyMcaFileDialogs from PyMca5.PyMcaGui.pymca import RGBCorrelator +from PyMca5.PyMcaMisc import CliUtils + if hasattr(qt, "QString"): QString = qt.QString QStringList = qt.QStringList @@ -63,6 +52,8 @@ QStringList = list QTVERSION = qt.qVersion() +_logger = logging.getLogger(__name__) + class PyMcaPostBatch(RGBCorrelator.RGBCorrelator): @@ -98,58 +89,55 @@ def _getStackOfFiles(self): return [] -def main(): - from PyMca5.PyMcaCore.LoggingLevel import getLoggingLevel - sys.excepthook = qt.exceptionHandler +def main(args): app = qt.QApplication([]) - app.lastWindowClosed.connect(app.quit) - - import getopt - options = '' - longoptions = ["nativefiledialogs=", "transpose=", "fileindex=", - "logging=", "debug=", "shape="] - opts, args = getopt.getopt( - sys.argv[1:], - options, - longoptions) - transpose = False - image_shape = None - for opt, arg in opts: - if opt in '--nativefiledialogs': - if int(arg): - PyMcaDirs.nativeFileDialogs = True - else: - PyMcaDirs.nativeFileDialogs = False - elif opt in '--transpose': - if int(arg): - transpose = True - elif opt in '--fileindex': - if int(arg): - transpose = True - elif opt in '--shape': - if 'x' in arg: - split_on = "x" - else: - split_on = "," - image_shape = tuple(int(n) for n in arg.split(split_on)) - - logging.basicConfig(level=getLoggingLevel(opts)) - - filelist = args + PyMcaAppInit.init_before_app_start(qt_app=app, cli_args=args) + + if args.shape: + split_on = "x" if "x" in args.shape else "," + image_shape = tuple(int(n) for n in args.shape.split(split_on)) + else: + image_shape = None + + # Create the widget w = PyMcaPostBatch(image_shape=image_shape) w.layout().setContentsMargins(11, 11, 11, 11) - if not filelist: - filelist = w._getStackOfFiles() + + # Handle files + filelist = args.files + if not filelist and not args.cli_test: + w._getStackOfFiles() + if filelist: w.addFileList(filelist) else: - print("Usage:") - print("python PyMcaPostBatch.py PyMCA_BATCH_RESULT_DOT_DAT_FILE") - if transpose: + print("Usage: python PyMcaPostBatch.py PyMCA_BATCH_RESULT_DOT_DAT_FILE") + + # Optional behaviors + if args.transpose: w.transposeImages() + w.show() - app.exec() + + # Auto-close Qt application for tests + if args.cli_test: + qt.QTimer.singleShot(0, app.quit) + + return app.exec() + + +def build_parser(): + parser = CliUtils.create_parser(description="PyMca Post-Batch Processing GUI", add_qt_options=True) + + parser.add_argument("--transpose", "--fileindex", type=int, default=0, help="Transpose all images") + parser.add_argument("--shape", type=str, default=None, help="Image shape as WxH or W,H") + + parser.add_argument("files", nargs="*", help="Optional list of data files to open") + + return parser if __name__ == "__main__": - main() + PyMcaAppInit.init_before_app_create() + exit_code = CliUtils.cli_main(main, build_parser(), loggers=(_logger,)) + sys.exit(exit_code) diff --git a/src/PyMca5/PyMcaGui/pymca/QStackWidget.py b/src/PyMca5/PyMcaGui/pymca/QStackWidget.py index d22519303..0c73d52f6 100644 --- a/src/PyMca5/PyMcaGui/pymca/QStackWidget.py +++ b/src/PyMca5/PyMcaGui/pymca/QStackWidget.py @@ -28,6 +28,12 @@ __contact__ = "sole@esrf.fr" __license__ = "MIT" __copyright__ = "European Synchrotron Radiation Facility, Grenoble, France" + +from PyMca5.PyMcaGui import PyMcaAppInit + +if __name__== '__main__': + PyMcaAppInit.init_before_app_import() + import sys import os import copy @@ -35,60 +41,9 @@ import numpy import weakref import logging -if __name__== '__main__': - # avoid issues if some module or dependency tries to use multiprocessing in frozen binaries - if getattr(sys, "frozen", False): - try: - import multiprocessing - multiprocessing.freeze_support() - except Exception: - pass -_logger = logging.getLogger(__name__) - -if __name__ == "__main__": - # We are going to read. Disable file locking. - os.environ["HDF5_USE_FILE_LOCKING"] = "FALSE" - _logger.info("%s set to %s" % ("HDF5_USE_FILE_LOCKING", - os.environ["HDF5_USE_FILE_LOCKING"])) - try: - # make sure hdf5plugins are imported - import hdf5plugin - except Exception: - _logger.info("Failed to import hdf5plugin") - # we have to get the Qt binding prior to import PyMcaQt - import getopt - options = '' - longoptions = ["fileindex=","old", - "filepattern=", "begin=", "end=", "increment=", - "nativefiledialogs=", "imagestack=", "image=", - "backend=", "binding=", "logging=", "debug="] - opts, args = getopt.getopt( - sys.argv[1:], - options, - longoptions) - binding = None - for opt, arg in opts: - if opt in ('--debug'): - if arg.lower() not in ['0', 'false']: - debugreport = 1 - _logger.setLevel(logging.DEBUG) - # --debug is also parsed later for the global logging level - elif opt in ('--binding'): - binding = arg.lower() - if binding == "pyqt5": - import PyQt5.QtCore - elif binding == "pyside2": - import PySide2.QtCore - elif binding == "pyside6": - import PySide6.QtCore - elif binding == "pyqt6": - import PyQt6.QtCore - else: - raise ValueError("Unsupported Qt binding <%s>" % binding) - from PyMca5.PyMcaCore.LoggingLevel import getLoggingLevel - logging.basicConfig(level=getLoggingLevel(opts)) from PyMca5.PyMcaGui import PyMcaQt as qt + if hasattr(qt, "QString"): QString = qt.QString else: @@ -110,17 +65,18 @@ convertToRowAndColumn = MaskImageWidget.convertToRowAndColumn from PyMca5.PyMcaGui.pymca import RGBCorrelator +from PyMca5.PyMcaGui.pymca import QStackWidget +from PyMca5.PyMcaGui.pymca import StackSelector from PyMca5.PyMcaGui.pymca.RGBCorrelatorWidget import ImageShapeDialog from PyMca5.PyMcaGui.plotting.PyMca_Icons import IconDict from PyMca5.PyMcaGui.pymca import StackSelector from PyMca5 import PyMcaDirs from PyMca5.PyMcaIO import ArraySave +from PyMca5.PyMcaMisc import CliUtils + HDF5 = ArraySave.HDF5 -# _logger.setLevel(logging.DEBUG) QTVERSION = qt.qVersion() -if _logger.getEffectiveLevel() == logging.DEBUG: - StackBase.logger.setLevel(logging.DEBUG) class QStackWidget(StackBase.StackBase, @@ -1463,116 +1419,79 @@ def closeEvent(self, event): PyMcaPrintPreview.resetSingletonPrintPreview() -def test(): - #create a dummy stack - nrows = 100 - ncols = 200 - nchannels = 1024 - a = numpy.ones((nrows, ncols), numpy.float64) - stackData = numpy.zeros((nrows, ncols, nchannels), numpy.float64) - for i in range(nchannels): - stackData[:, :, i] = a * i - stackData[0:10, :, :] = 0 - w = QStackWidget() - w.setStack(stackData, mcaindex=2) - w.show() - return w +def main(args): + if args.cli_test and args.filepattern is None and not args.files: + print("No input files provided.") + return 0 + if args.filepattern is not None: + if (args.begin is None) or (args.end is None): + raise ValueError( + "A file pattern needs at least a set of begin and end indices" + ) -if __name__ == "__main__": - sys.excepthook = qt.exceptionHandler - try: - opts, args = getopt.getopt( - sys.argv[1:], - options, - longoptions) - except Exception: - print("%s" % sys.exc_info()[1]) - sys.exit(1) - fileindex = 0 - filepattern=None - begin = None - end = None - imagestack=None - increment=None - backend=None - PyMcaDirs.nativeFileDialogs=True - - for opt, arg in opts: - if opt in '--begin': - if "," in arg: - begin = [int(x) for x in arg.split(",")] - else: - begin = [int(arg)] - elif opt in '--end': - if "," in arg: - end = [int(x) for x in arg.split(",")] - else: - end = int(arg) - elif opt in '--increment': - if "," in arg: - increment = [int(x) for x in arg.split(",")] - else: - increment = int(arg) - elif opt in '--filepattern': - filepattern = arg.replace('"', '') - filepattern = filepattern.replace("'", "") - elif opt in '--fileindex': - fileindex = int(arg) - elif opt in ['--imagestack', "--image"]: - imagestack = int(arg) - elif opt in '--nativefiledialogs': - if int(arg): - PyMcaDirs.nativeFileDialogs = True - else: - PyMcaDirs.nativeFileDialogs = False - elif opt in '--backend': - backend = arg - #elif opt in '--old': - # import QEDFStackWidget - # sys.exit(QEDFStackWidget.runAsMain()) - if filepattern is not None: - if (begin is None) or (end is None): - raise ValueError("A file pattern needs at least a set of begin and end indices") - app = qt.QApplication([]) - if sys.platform not in ["win32", "darwin"]: - # some themes of Ubuntu 16.04 give black tool tips on black background - try: - _ttp = qt.QApplication.instance().palette() - _ttText = _ttp.color(qt.QPalette.ToolTipText).name() - _ttBase = _ttp.color(qt.QPalette.ToolTipBase).name() - app.setStyleSheet("QToolTip { color: %s; background-color: %s; border: 1px solid %s; }" % (_ttText, _ttBase, _ttText)) - except Exception: - app.setStyleSheet("QToolTip { color: #000000; background-color: #fff0cd; border: 1px solid black; }") - if backend is not None: - # set the default backend + if args.backend is not None: try: from PyMca5.PyMcaGraph.Plot import Plot - Plot.defaultBackend = backend + Plot.defaultBackend = args.backend except Exception: - _logger.warning("WARNING: Cannot set backend to %s", backend) + _logger.warning("Cannot set backend to %s", args.backend) + + app = qt.QApplication([]) + PyMcaAppInit.init_before_app_start(qt_app=app, cli_args=args) + widget = QStackWidget() w = StackSelector.StackSelector(widget) - if filepattern is not None: - #ignore the args even if present - stack = w.getStackFromPattern(filepattern, begin, end, increment=increment, - imagestack=imagestack) + + # Load stack from file pattern or positional arguments + if args.filepattern: + stack = w.getStackFromPattern( + args.filepattern, args.begin, args.end, increment=args.increment, imagestack=args.imagestack + ) else: - stack = w.getStack(args, imagestack=imagestack) - if (type(stack) == type([])) or (isinstance(stack, list)): - #aifira like, two stacks + stack = w.getStack(args.files, imagestack=args.imagestack) + + if stack is None: + print("No input files provided.") + return 0 + + # Handle multiple stacks (AIFIRA style) + if isinstance(stack, list): widget.setStack(stack[0]) - if len(stack) > 1: - for i in range(1, len(stack)): - if stack[i] is not None: - secondary = QStackWidget(primary=False, - rgbwidget=widget.rgbWidget) - secondary.setStack(stack[i]) - widget.addSecondary(secondary) + for secondary_stack in stack[1:]: + if secondary_stack is not None: + secondary = QStackWidget(primary=False, rgbwidget=widget.rgbWidget) + secondary.setStack(secondary_stack) + widget.addSecondary(secondary) stack = None else: widget.setStack(stack) + widget.show() - app.exec() - app = None + # Auto-close Qt application for tests + if args.cli_test: + qt.QTimer.singleShot(0, app.quit) + + return app.exec() + + +def build_parser(): + parser = CliUtils.create_parser(description="PyMca StackSelector GUI", add_qt_options=True, add_backend_options=True) + + parser.add_argument("--fileindex", type=int, default=0, help="Index of stack to display") + parser.add_argument("--filepattern", type=str, default=None, help="Pattern to match stack files") + parser.add_argument("--begin", type=CliUtils.int_or_list, default=None, help="Begin index/indices, comma-separated") + parser.add_argument("--end", type=CliUtils.int_or_list, default=None, help="End index/indices, comma-separated") + parser.add_argument("--increment", type=CliUtils.int_or_list, default=None, help="Increment(s), comma-separated") + parser.add_argument("--imagestack", "--image", type=int, default=0, help="Load image stack") + + parser.add_argument("files", nargs="*", help="Positional files if not using filepattern") + + return parser + + +if __name__ == "__main__": + PyMcaAppInit.init_before_app_create() + exit_code = CliUtils.cli_main(main, build_parser(), loggers=(StackBase.logger,)) + sys.exit(exit_code) diff --git a/src/PyMca5/PyMcaGui/pymca/RGBCorrelator.py b/src/PyMca5/PyMcaGui/pymca/RGBCorrelator.py index 52c5329e3..9badde288 100644 --- a/src/PyMca5/PyMcaGui/pymca/RGBCorrelator.py +++ b/src/PyMca5/PyMcaGui/pymca/RGBCorrelator.py @@ -28,8 +28,16 @@ __contact__ = "sole@esrf.fr" __license__ = "MIT" __copyright__ = "European Synchrotron Radiation Facility, Grenoble, France" + +from PyMca5.PyMcaGui import PyMcaAppInit + +if __name__== '__main__': + PyMcaAppInit.init_before_app_import() + import sys import os + + import numpy from PyMca5.PyMcaGui.pymca import RGBCorrelatorWidget qt = RGBCorrelatorWidget.qt @@ -39,6 +47,8 @@ QString = str from PyMca5.PyMcaGui.plotting import RGBCorrelatorGraph from PyMca5.PyMcaGui.pymca import QPyMcaMatplotlibSave +from PyMca5.PyMcaMisc import CliUtils + USE_MASK_WIDGET = False if USE_MASK_WIDGET: from PyMca5.PyMcaGui.plotting import MaskImageWidget @@ -167,54 +177,67 @@ def show(self): self.controller.show() qt.QWidget.show(self) -def test(): - import logging + +def main(args): app = qt.QApplication([]) - app.lastWindowClosed.connect(app.quit) - if 0: + PyMcaAppInit.init_before_app_start(qt_app=app, cli_args=args) + + # Create the widget + if args.image: + w = RGBCorrelator() + w.resize(800, 600) + else: graphWidget = RGBCorrelatorGraph.RGBCorrelatorGraph() graph = graphWidget.graph w = RGBCorrelator(graph=graph) - else: - w = RGBCorrelator() - w.resize(800, 600) - import getopt - from PyMca5.PyMcaCore.LoggingLevel import getLoggingLevel - options = '' - longoptions = ["logging=", "debug="] - opts, args = getopt.getopt( - sys.argv[1:], - options, - longoptions) - - logging.basicConfig(level=getLoggingLevel(opts)) - filelist=args - if len(filelist): + + # Load files if any + filelist = args.files or [] + if filelist: try: import DataSource DataReader = DataSource.DataSource except Exception: import EdfFileDataSource DataReader = EdfFileDataSource.EdfFileDataSource + for fname in filelist: source = DataReader(fname) - for key in source.getSourceInfo()['KeyList']: + for key in source.getSourceInfo()["KeyList"]: dataObject = source.getDataObject(key) - w.addImage(dataObject.data, os.path.basename(fname)+" "+key) + w.addImage(dataObject.data, os.path.basename(fname) + " " + key) else: + # Default test data print("This is a just test method using 100 x 100 matrices.") print("Run PyMcaPostBatch to have file loading capabilities.") array1 = numpy.arange(10000) - array2 = numpy.resize(numpy.arange(10000), (100, 100)) - array2 = numpy.transpose(array2) + array2 = numpy.resize(numpy.arange(10000), (100, 100)).T array3 = array1 * 1 w.addImage(array1) w.addImage(array2) w.addImage(array3) w.setImageShape([100, 100]) + w.show() - app.exec() -if __name__ == "__main__": - test() + # Auto-close Qt application for tests + if args.cli_test: + qt.QTimer.singleShot(0, app.quit) + + return app.exec() + +def build_parser(): + parser = CliUtils.create_parser(description="RGBCorrelator GUI", add_qt_options=True) + + parser.add_argument("--image", action="store_true", help="Show image") + + parser.add_argument("files", nargs="*", help="Files to load (optional)") + + return parser + + +if __name__ == "__main__": + PyMcaAppInit.init_before_app_create() + exit_code = CliUtils.cli_main(main, build_parser()) + sys.exit(exit_code) diff --git a/src/PyMca5/PyMcaGui/pymca/RGBCorrelatorWidget.py b/src/PyMca5/PyMcaGui/pymca/RGBCorrelatorWidget.py index 269428195..476634890 100644 --- a/src/PyMca5/PyMcaGui/pymca/RGBCorrelatorWidget.py +++ b/src/PyMca5/PyMcaGui/pymca/RGBCorrelatorWidget.py @@ -27,6 +27,12 @@ __contact__ = "sole@esrf.fr" __license__ = "MIT" __copyright__ = "European Synchrotron Radiation Facility, Grenoble, France" + +from PyMca5.PyMcaGui import PyMcaAppInit + +if __name__== '__main__': + PyMcaAppInit.init_before_app_import() + import sys import os import numpy @@ -35,8 +41,7 @@ import warnings import re from typing import Optional -from . import RGBCorrelatorSlider -from . import RGBCorrelatorTable + from PyMca5.PyMcaGui.pymca import RGBImageCalculator from PyMca5 import spslut from PyMca5.PyMcaGui.plotting.PyMca_Icons import IconDict @@ -48,6 +53,11 @@ from PyMca5.PyMcaIO import TiffIO from PyMca5.PyMcaGui.io import PyMcaFileDialogs from PyMca5.PyMcaGui.plotting import ScatterPlotCorrelatorWidget +from PyMca5.PyMcaGui.plotting import RGBCorrelatorGraph +from PyMca5.PyMcaMisc import CliUtils + +from . import RGBCorrelatorSlider +from . import RGBCorrelatorTable DataReader = EdfFileDataSource.EdfFileDataSource USE_STRING = False @@ -1777,14 +1787,12 @@ def drawContents(self, painter): painter.font().setBold(0) -def main(): - from PyMca5.PyMcaGui.plotting import RGBCorrelatorGraph - +def main(args): app = qt.QApplication([]) - app.lastWindowClosed.connect(app.quit) + PyMcaAppInit.init_before_app_start(qt_app=app, cli_args=args) container = qt.QSplitter() - # containerLayout = qt.QHBoxLayout(container) + w = RGBCorrelatorWidget(container) graph = RGBCorrelatorGraph.RGBCorrelatorGraph(container) @@ -1797,23 +1805,17 @@ def slot(ddict): graph.graph.replot() w.sigRGBCorrelatorWidgetSignal.connect(slot) - import getopt - - options = "" - longoptions = [] - opts, args = getopt.getopt(sys.argv[1:], options, longoptions) - for opt, arg in opts: - pass - filelist = args - if len(filelist): + + filelist = args.files + + if filelist: try: import DataSource - DataReader = DataSource.DataSource except Exception: from PyMca5.PyMcaCore import EdfFileDataSource - DataReader = EdfFileDataSource.EdfFileDataSource + for fname in filelist: source = DataReader(fname) for key in source.getSourceInfo()["KeyList"]: @@ -1824,15 +1826,32 @@ def slot(ddict): array2 = numpy.resize(numpy.arange(10000), (100, 100)) array2 = numpy.transpose(array2) array3 = array1 * 1 + w.addImage(array1) w.addImage(array2) w.addImage(array3) w.setImageShape([100, 100]) + # containerLayout.addWidget(w) # containerLayout.addWidget(graph) container.show() - app.exec() + + # Auto-close Qt application for tests + if args.cli_test: + qt.QTimer.singleShot(0, app.quit) + + return app.exec() + + +def build_parser(): + parser = CliUtils.create_parser(description="RGB Correlator Viewer", add_qt_options=True) + + parser.add_argument("files", nargs="*", help="Input files") + + return parser if __name__ == "__main__": - main() + PyMcaAppInit.init_before_app_create() + exit_code = CliUtils.cli_main(main, build_parser(), loggers=(_logger,)) + sys.exit(exit_code) diff --git a/src/PyMca5/PyMcaGui/pymca/StackSelector.py b/src/PyMca5/PyMcaGui/pymca/StackSelector.py index acb9acca2..2ba3c8864 100644 --- a/src/PyMca5/PyMcaGui/pymca/StackSelector.py +++ b/src/PyMca5/PyMcaGui/pymca/StackSelector.py @@ -28,11 +28,18 @@ __contact__ = "sole@esrf.fr" __license__ = "MIT" __copyright__ = "European Synchrotron Radiation Facility, Grenoble, France" + +from PyMca5.PyMcaGui import PyMcaAppInit + +if __name__== '__main__': + PyMcaAppInit.init_before_app_import() + import sys import os import copy import traceback import logging + from PyMca5.PyMcaGui import PyMcaQt as qt from PyMca5 import PyMcaDirs from PyMca5 import DataObject @@ -53,6 +60,9 @@ from PyMca5.PyMcaIO import FsmMap from PyMca5.PyMcaIO import LabSpec6TxtMap from PyMca5.PyMcaIO import BrukerBCF +from PyMca5.PyMcaMisc import CliUtils + + from .QStack import QStack, QSpecFileStack try: from PyMca5.PyMcaGui.pymca import QHDF5Stack1D @@ -369,11 +379,11 @@ def _getFileList(self, fileTypeList, message=None, getfilter=None): getfilter=False, single=False, native=True) - if not(len(filelist)): - return [] - PyMcaDirs.inputDir = os.path.dirname(filelist[0]) - if PyMcaDirs.outputDir is None: - PyMcaDirs.outputDir = os.path.dirname(filelist[0]) + + if len(filelist): + PyMcaDirs.inputDir = os.path.dirname(filelist[0]) + if PyMcaDirs.outputDir is None: + PyMcaDirs.outputDir = os.path.dirname(filelist[0]) if getfilter: return filelist, filterused @@ -447,78 +457,78 @@ def getFileListFromPattern(self, pattern, begin, end, increment=None): return fileList -if __name__ == "__main__": - from PyMca5 import QStackWidget - import getopt - options = '' - longoptions = ["fileindex=", - "filepattern=", "begin=", "end=", "increment=", - "nativefiledialogs=", "imagestack="] - try: - opts, args = getopt.getopt( - sys.argv[1:], - options, - longoptions) - except Exception: - _logger.error(sys.exc_info()[1]) - sys.exit(1) - fileindex = 0 - filepattern = None - begin = None - end = None - imagestack = False - increment = None - for opt, arg in opts: - if opt in '--begin': - if "," in arg: - begin = [int(x) for x in arg.split(",")] - else: - begin = [int(arg)] - elif opt in '--end': - if "," in arg: - end = [int(x) for x in arg.split(",")] - else: - end = int(arg) - elif opt in '--increment': - if "," in arg: - increment = [int(x) for x in arg.split(",")] - else: - increment = int(arg) - elif opt in '--filepattern': - filepattern = arg.replace('"', '') - filepattern = filepattern.replace("'", "") - elif opt in '--fileindex': - fileindex = int(arg) - elif opt in '--imagestack': - imagestack = int(arg) - elif opt in '--nativefiledialogs': - if int(arg): - PyMcaDirs.nativeFileDialogs = True - else: - PyMcaDirs.nativeFileDialogs = False - if filepattern is not None: - if (begin is None) or (end is None): - raise ValueError(\ - "A file pattern needs at least a set of begin and end indices") +def main(args): + if args.cli_test and args.filepattern is None and not args.files: + print("No input files provided.") + return 0 + + from PyMca5.PyMcaGui.pymca import QStackWidget + + if args.filepattern is not None: + if (args.begin is None) or (args.end is None): + raise ValueError( + "A file pattern needs at least a set of begin and end indices" + ) + app = qt.QApplication([]) + PyMcaAppInit.init_before_app_start(qt_app=app, cli_args=args) + widget = QStackWidget.QStackWidget() w = StackSelector(widget) - if filepattern is not None: - #ignore the args even if present - stack = w.getStackFromPattern(filepattern, begin, end, - increment=increment, - imagestack=imagestack) + + if args.filepattern is not None: + stack = w.getStackFromPattern( + args.filepattern, + args.begin, + args.end, + increment=args.increment, + imagestack=args.imagestack, + ) else: - stack = w.getStack(args, imagestack=imagestack) - if type(stack) == type([]): - #aifira like, two stacks + stack = w.getStack(args.files, imagestack=args.imagestack) + + if stack is None: + print("No files selected.") + return 0 + + if isinstance(stack, list): widget.setStack(stack[0]) - secondary = QStackWidget.QStackWidget(primary=False, - rgbwidget=widget.rgbWidget) + secondary = QStackWidget.QStackWidget( + primary=False, + rgbwidget=widget.rgbWidget, + ) secondary.setStack(stack[1]) widget.setSecondary(secondary) - stack = None else: widget.setStack(stack) + widget.show() - app.exec() + + # Auto-close Qt application for tests + if args.cli_test: + qt.QTimer.singleShot(0, app.quit) + + return app.exec() + + +def build_parser(): + parser = CliUtils.create_parser(description="PyMca Stack Viewer", add_qt_options=True) + + parser.add_argument("--fileindex", type=int, default=0) + + parser.add_argument("--filepattern", type=str, default=None, help="File pattern") + parser.add_argument("--begin", type=CliUtils.int_or_list, default=None, help="Begin index/indices, comma-separated") + parser.add_argument("--end", type=CliUtils.int_or_list, default=None, help="End index/indices, comma-separated") + parser.add_argument("--increment", type=CliUtils.int_or_list, default=None, help="Increment(s), comma-separated") + + parser.add_argument("--imagestack", type=int, default=0) + + parser.add_argument("files", nargs="*", help="Input files if not using filepattern") + + return parser + + +if __name__ == "__main__": + PyMcaAppInit.init_before_app_create() + exit_code = CliUtils.cli_main(main, build_parser(), loggers=(_logger,)) + sys.exit(exit_code) diff --git a/src/PyMca5/PyMcaIO/NexusUtils.py b/src/PyMca5/PyMcaIO/NexusUtils.py index 72fa85131..acdc6d064 100644 --- a/src/PyMca5/PyMcaIO/NexusUtils.py +++ b/src/PyMca5/PyMcaIO/NexusUtils.py @@ -27,7 +27,6 @@ # #############################################################################*/ __author__ = "Wout De Nolf" -__contact__ = "wout.de_nolf@esrf.eu" __license__ = "MIT" __copyright__ = "European Synchrotron Radiation Facility, Grenoble, France" diff --git a/src/PyMca5/PyMcaIO/OutputBuffer.py b/src/PyMca5/PyMcaIO/OutputBuffer.py index edb9d64ef..61bd237f9 100644 --- a/src/PyMca5/PyMcaIO/OutputBuffer.py +++ b/src/PyMca5/PyMcaIO/OutputBuffer.py @@ -27,7 +27,6 @@ # #############################################################################*/ __author__ = "Wout De Nolf" -__contact__ = "wout.de_nolf@esrf.eu" __license__ = "MIT" __copyright__ = "European Synchrotron Radiation Facility, Grenoble, France" diff --git a/src/PyMca5/PyMcaMisc/CliUtils.py b/src/PyMca5/PyMcaMisc/CliUtils.py new file mode 100644 index 000000000..211cb2b32 --- /dev/null +++ b/src/PyMca5/PyMcaMisc/CliUtils.py @@ -0,0 +1,209 @@ +# /*########################################################################## +# +# The PyMca X-Ray Fluorescence Toolkit +# +# Copyright (c) 2004-2023 European Synchrotron Radiation Facility +# +# This file is part of the PyMca X-ray Fluorescence Toolkit developed at +# the ESRF. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +#############################################################################*/ +"""Common CLI utilities to be used like this + +.. code-block:: python + + import sys + import logging + from PyMca5.PyMcaMisc import CliUtils + + _logger = logging.getLogger(__name__) + + def main(args): + ... + return 0 + + def build_parser(): + parser = CliUtils.create_parser(description="...") + ... + return parser + + if __name__ == "__main__": + exit_code = CliUtils.cli_main(main, build_parser(), loggers=(_logger,)) + sys.exit(exit_code) +""" + +__author__ = "Wout De Nolf" +__license__ = "MIT" +__copyright__ = "European Synchrotron Radiation Facility, Grenoble, France" + +import argparse +import logging +from typing import List, Union + +from PyMca5.PyMcaMisc.LoggingUtils import parse_log_level + +LOGGING_FORMAT = "%(levelname)s: %(message)s" + + +def cli_main(main_func, parser, args=None, loggers=tuple()): + """ + Standard CLI entry point wrapper. + """ + args = parser.parse_args(args) + + # Apply common arguments; handle early exit + early_exit = _apply_common_arguments(args, loggers=loggers) + if early_exit is not None: + return early_exit + + # Run main + exit_code = main_func(args) + + # Ensure exit code is an int + if not isinstance(exit_code, int): + return int(bool(exit_code)) + return exit_code + + +def create_parser( + add_common_options=True, + add_qt_options=False, + add_backend_options=False, + default_log_level="WARNING", + **parser_options +): + """ + Standard CLI parser with common features. + """ + parser_options.setdefault( + "formatter_class", argparse.ArgumentDefaultsHelpFormatter + ) + parser = argparse.ArgumentParser(**parser_options) + + if add_common_options: + _add_common_arguments(parser, default_log_level=default_log_level) + + if add_qt_options: + _add_qt_arguments(parser) + + if add_backend_options: + _add_backend_argument(parser) + + parser.add_argument("--cli-test", action="store_true", help=argparse.SUPPRESS) + + return parser + + +def int_or_list(value:Union[str, None]) -> Union[List[int], int, None]: + """Parse comma-separated string.""" + if value is None: + return None + parts = [int(s) for s in value.split(",")] + if len(parts) == 1: + return parts[0] + return parts + + +def _add_common_arguments(parser, default_log_level): + """ + Common CLI arguments. + """ + parser.add_argument( + "--debug", + type=int, + default=0, + help="DEBUG log level for relevant application loggers" + ) + + parser.add_argument( + "--logging", + dest="log_level", + default=default_log_level.lower(), + type=parse_log_level, + help="Global log level (debug/info/warning/error/critical or 0-4)", + ) + + parser.add_argument("--version", action="store_true", help="Show version and exit") + + +def _add_backend_argument(parser): + """ + Plotting backends accepted by `PyMca5.PyMcaGraph.Plot.Plot` + """ + backend_choices = [ + "matplotlib", "mpl", + "gl", "opengl", + "glut", + "osmesa", "mesa", + "silx", "silx-mpl", "silxmpl", + "silx-gl", "silxgl" + ] + + help = """The plot backend to use:\n + Matplotlib, + OpenGL 2.1 (requires appropriate OpenGL drivers), or + Off-screen Mesa OpenGL software pipeline (requires OSMesa library). + """ + + parser.add_argument( + "-b", "--backend", type=str, choices=backend_choices, default="mpl", help=help + ) + + +def _add_qt_arguments(parser): + """ + Common Qt arguments. + """ + parser.add_argument("--qt", type=str, default=None, choices=["5","6"], help="Force Qt version") + parser.add_argument( + "--binding", type=str, default=None, choices=["pyqt5","pyqt6","pyside2","pyside6"], help="Qt binding" + ) + parser.add_argument("--nativefiledialogs", type=int, default=None, help="Use native file dialogs") + + +def _apply_common_arguments(args, loggers=None): + """ + Apply common arguments and handle early exit. + """ + if getattr(args, "version", False): + from pymca import __version__ + + print(__version__) + return 0 + + _configure_logging(args, loggers=loggers) + return None + + +def _configure_logging(args, loggers=tuple()): + """ + Local and global logging configuration. + """ + debug = getattr(args, "debug", None) + if loggers: + for logger in loggers: + if debug: + logger.setLevel(logging.DEBUG) + else: + logger.setLevel(logging.INFO) + + level = getattr(args, "log_level", logging.WARNING) + logging.basicConfig(level=level, format=LOGGING_FORMAT) diff --git a/src/PyMca5/PyMcaMisc/LoggingUtils.py b/src/PyMca5/PyMcaMisc/LoggingUtils.py new file mode 100644 index 000000000..fb46898a8 --- /dev/null +++ b/src/PyMca5/PyMcaMisc/LoggingUtils.py @@ -0,0 +1,27 @@ +import logging +import argparse + +_LOG_LEVELS_DICT = { + # Explicit args + "debug": logging.DEBUG, + "info": logging.INFO, + "warning": logging.WARNING, + "error": logging.ERROR, + "critical": logging.CRITICAL, + # int args sorted by increasing verbosity + "0": logging.CRITICAL, + "1": logging.ERROR, + "2": logging.WARNING, + "3": logging.INFO, + "4": logging.DEBUG, +} + + +def parse_log_level(value: str) -> int: + """Convert CLI logging argument to logging level.""" + key = value.lower() + if key in _LOG_LEVELS_DICT: + return _LOG_LEVELS_DICT[key] + raise argparse.ArgumentTypeError( + f"Invalid log level '{value}'. Use debug/info/warning/error/critical or verbosity 0-4." + ) diff --git a/src/PyMca5/PyMcaMisc/ProfilingUtils.py b/src/PyMca5/PyMcaMisc/ProfilingUtils.py index 09ad1ff78..f3e5102b3 100644 --- a/src/PyMca5/PyMcaMisc/ProfilingUtils.py +++ b/src/PyMca5/PyMcaMisc/ProfilingUtils.py @@ -27,7 +27,6 @@ # #############################################################################*/ __author__ = "Wout De Nolf" -__contact__ = "wout.de_nolf@esrf.eu" __license__ = "MIT" __copyright__ = "European Synchrotron Radiation Facility, Grenoble, France" try: diff --git a/src/PyMca5/PyMcaPhysics/xrf/ClassMcaTheory.py b/src/PyMca5/PyMcaPhysics/xrf/ClassMcaTheory.py index 989ca16d9..90c4d6ad2 100644 --- a/src/PyMca5/PyMcaPhysics/xrf/ClassMcaTheory.py +++ b/src/PyMca5/PyMcaPhysics/xrf/ClassMcaTheory.py @@ -30,26 +30,38 @@ __contact__ = "sole@esrf.fr" __license__ = "MIT" __copyright__ = "European Synchrotron Radiation Facility, Grenoble, France" + import os import sys -import numpy import copy +import time +import profile +import pstats import logging + +import numpy + +from PyMca5.PyMcaMath.fitting import SpecfitFuns +from PyMca5.PyMcaIO import ConfigDict +from PyMca5.PyMcaMath.fitting import Gefit +from PyMca5 import PyMcaDataDir +from PyMca5.PyMcaMisc import CliUtils + from .Strategies import STRATEGIES from . import ConcentrationsTool FISX = ConcentrationsTool.FISX if FISX: FisxHelper = ConcentrationsTool.FisxHelper from . import Elements -from PyMca5.PyMcaMath.fitting import SpecfitFuns -from PyMca5.PyMcaIO import ConfigDict -from PyMca5.PyMcaMath.fitting import Gefit -from PyMca5 import PyMcaDataDir + + _logger = logging.getLogger(__name__) #"python ClassMcaTheory.py -s1.1 --file=03novs060sum.mca --pkm=McaTheory.dat --continuum=0 --strip=1 --sumflag=1 --maxiter=4" CONTINUUM_LIST = [None,'Constant','Linear','Parabolic','Linear Polynomial','Exp. Polynomial'] OLDESCAPE = 0 MAX_ATTENUATION = 1.0E-300 + + class McaTheory(object): def __init__(self, initdict=None, filelist=None, **kw): self.ydata0 = None @@ -2899,8 +2911,8 @@ def test(inputfile=None,scankey=None,pkm=None, if inputfile is None: print("USAGE") print("python -m PyMca5.PyMcaPhysics.xrf.ClassMcaTheory.py -s1.1 --file=filename --cfg=cfgfile [--plotflag=1]") - #python ClassMcaTheory.py -s2.1 --file=ch09__mca_0005.mca --pkm=TEST.cfg --continuum=0 --stripflag=1 --sumflag=1 --maxiter=4 - sys.exit(0) + return + print("assuming is a specfile ...") sf=specfile.Specfile(inputfile) if scankey is None: @@ -2956,72 +2968,66 @@ def test(inputfile=None,scankey=None,pkm=None, "Summing") graph.addCurve(mcafitresult['energy'],mcafitresult['continuum'],"Continuum") graph.show() - app.exec() - -PROFILING = 0 -if __name__ == "__main__": - import time - t0=time.time() - if PROFILING: - import profile - import pstats - profile.run('test()',"test") - p=pstats.Stats("test") + return app.exec() + + +def main(args): + t0 = time.time() + + kwargs = dict( + inputfile=args.file, + scankey=args.scan, + pkm=args.pkm, + maxiter=args.maxiter, + continuum=args.continuum, + stripflag=args.stripflag, + sumflag=args.sumflag, + hypermetflag=args.hypermetflag, + escapeflag=args.escapeflag, + plotflag=args.plotflag, + attenuatorsflag=args.attenuatorsflag, + outfile=args.outfile, + ) + + if args.profiling: + profile.runctx( + "test(**kwargs)", globals=dict(), locals=dict(kwargs=kwargs), filename="test.profile" + ) + p = pstats.Stats("test.profile") p.strip_dirs().sort_stats(-1).print_stats() + exit_code = None else: - import getopt - if 1: - #try: - options = 'f:s:o' - longoptions = ['file=','scan=','pkm=','cfg=', - 'output=','continuum=','stripflag=', - 'maxiter=','sumflag=','escapeflag=','hypermetflag=','plotflag=', - 'attenuatorsflag=','outfile='] - opts, args = getopt.getopt( - sys.argv[1:], - options, - longoptions) - inputfile = None - outfile = None - scan = None - pkm = None - maxiter = 100 - sumflag = 0 - hypermetflag = 1 - plotflag = 0 - stripflag = 1 - escapeflag= 1 - continuum = 0 - attenuatorsflag = 1 - for opt,arg in opts: - if opt in ('-f','--file'): - inputfile = arg - if opt in ('-s','--scan'): - scan = arg - if opt in ('--pkm','--cfg'): - pkm = arg - if opt in ('--continuum'): - continuum = int(float(arg)) - if opt in ('--strip'): - strip = int(float(arg)) - if opt in ('--maxiter'): - maxiter = int(float(arg)) - if opt in ('--sumflag'): - sumflag = int(float(arg)) - if opt in ('--escapeflag'): - escapeflag = int(float(arg)) - if opt in ('--stripflag'): - stripflag = int(float(arg)) - if opt in ('--plotflag'): - plotflag = int(float(arg)) - if opt in ('--hypermetflag'): - hypermetflag = int(float(arg)) - if opt in ('--attenuatorsflag'): - attenuatorsflag = int(float(arg)) - if opt in ('--outfile'): - outfile = arg - test(inputfile=inputfile,scankey=scan,pkm=pkm, - maxiter=maxiter,continuum=continuum,stripflag=stripflag,sumflag=sumflag, - hypermetflag=hypermetflag,escapeflag=escapeflag,plotflag=plotflag, - attenuatorsflag=attenuatorsflag,outfile=outfile) - print("TIME = ",time.time()-t0) + exit_code = test(**kwargs) + + print("TIME = ", time.time() - t0) + return exit_code + + +def build_parser(): + parser = CliUtils.create_parser(description="Run test fitting procedure") + + parser.add_argument("-f", "--file", type=str, help="Input file") + parser.add_argument("-s", "--scan", type=str, help="Scan key") + parser.add_argument("--pkm", "--cfg", dest="pkm", required=True, type=str, help="PKM/CFG file") + + parser.add_argument("--continuum", type=int, default=0) + parser.add_argument("--stripflag", type=int, default=1) + parser.add_argument("--maxiter", type=int, default=100) + parser.add_argument("--sumflag", type=int, default=0) + parser.add_argument("--escapeflag", type=int, default=1) + parser.add_argument("--plotflag", type=int, default=0) + parser.add_argument("--hypermetflag", type=int, default=1) + parser.add_argument("--attenuatorsflag", type=int, default=1) + + parser.add_argument("--outfile", type=str, help="Output file") + parser.add_argument("--profiling", action="store_true", help="Enable profiling") + + return parser + + +if __name__ == "__main__": + exit_code = CliUtils.cli_main(main, build_parser()) + sys.exit(exit_code) + + # python -m PyMca5.PyMcaPhysics.xrf.ClassMcaTheory --cfg ./src/PyMca5/PyMcaData/Steel.cfg --file ./src/PyMca5/PyMcaData/Steel.spe + # python ClassMcaTheory.py -s2.1 --file=ch09__mca_0005.mca --pkm=TEST.cfg --continuum=0 --stripflag=1 --sumflag=1 --maxiter=4 diff --git a/src/PyMca5/PyMcaPhysics/xrf/ConcentrationsTool.py b/src/PyMca5/PyMcaPhysics/xrf/ConcentrationsTool.py index bcaea889e..ed2e5f3b4 100644 --- a/src/PyMca5/PyMcaPhysics/xrf/ConcentrationsTool.py +++ b/src/PyMca5/PyMcaPhysics/xrf/ConcentrationsTool.py @@ -26,15 +26,21 @@ # THE SOFTWARE. # #############################################################################*/ + __author__ = "V.A. Sole" __contact__ = "sole@esrf.fr" __license__ = "MIT" __copyright__ = "European Synchrotron Radiation Facility, Grenoble, France" + import sys import copy import numpy + +from PyMca5.PyMcaIO import ConfigDict +from PyMca5.PyMcaMisc import CliUtils from . import Elements from .XRFMC import XRFMCHelper + FISX = False try: from . import FisxHelper @@ -797,47 +803,69 @@ def _figureOfMerit(self, element, fluo, fitresult): weight = weightHelp return weight -def main(): - import sys - import getopt - - from PyMca5.PyMcaIO import ConfigDict - - if len(sys.argv) > 1: - options = '' - longoptions = ['flux=', 'time=', 'area=', 'distance=', - 'attenuators=', 'usematrix='] - tool = ConcentrationsTool() - opts, args = getopt.getopt( - sys.argv[1:], - options, - longoptions) - config = tool.configure() - for opt, arg in opts: - if opt in ('--flux'): - config['flux'] = float(arg) - elif opt in ('--area'): - config['area'] = float(arg) - elif opt in ('--time'): - config['time'] = float(arg) - elif opt in ('--distance'): - config['distance'] = float(arg) - elif opt in ('--attenuators'): - config['useattenuators'] = int(float(arg)) - elif opt in ('--usematrix'): - config['usematrix'] = int(float(arg)) - tool.configure(config) - filelist = args - for filename in filelist: - d = ConfigDict.ConfigDict() - d.read(filename) - for material in d['result']['config']['materials'].keys(): - Elements.Material[material] =\ - copy.deepcopy(d['result']['config']['materials'][material]) - print(tool.processFitResult(fitresult=d, elementsfrommatrix=True)) - else: - print("Usage:") - print("ConcentrationsTool [--flux=xxxx --area=xxxx] fitresultfile") + +def main(args): + tool = ConcentrationsTool() + + # Get default configuration + config = tool.configure() + + # Override config from CLI arguments + if args.flux is not None: + config['flux'] = args.flux + + if args.area is not None: + config['area'] = args.area + + if args.time is not None: + config['time'] = args.time + + if args.distance is not None: + config['distance'] = args.distance + + if args.attenuators is not None: + config['useattenuators'] = int(args.attenuators) + + if args.usematrix is not None: + config['usematrix'] = int(args.usematrix) + + tool.configure(config) + + if not args.files: + print("No input files provided.") + return 0 + + for filename in args.files: + d = ConfigDict.ConfigDict() + d.read(filename) + + # Restore materials into global Elements registry + for material in d['result']['config']['materials'].keys(): + Elements.Material[material] = copy.deepcopy( + d['result']['config']['materials'][material] + ) + + print(tool.processFitResult(fitresult=d, elementsfrommatrix=True)) + + return 0 + + +def build_parser(): + parser = CliUtils.create_parser(description="Process fit result files and compute concentrations") + + parser.add_argument("--flux", type=float, default=None, help="Incident flux") + parser.add_argument("--time", type=float, default=None, help="Acquisition time") + parser.add_argument("--area", type=float, default=None, help="Detector area") + parser.add_argument("--distance", type=float, default=None, help="Sample-detector distance") + parser.add_argument("--attenuators", type=float, default=None, help="Use attenuators (0/1)") + parser.add_argument("--usematrix", type=float, default=None, help="Use matrix correction (0/1)") + + # Positional input files + parser.add_argument("files", nargs="*", help="Fit result files") + + return parser + if __name__ == "__main__": - main() + exit_code = CliUtils.cli_main(main, build_parser()) + sys.exit(exit_code) diff --git a/src/PyMca5/PyMcaPhysics/xrf/FastXRFLinearFit.py b/src/PyMca5/PyMcaPhysics/xrf/FastXRFLinearFit.py index 48039d524..796b574a5 100644 --- a/src/PyMca5/PyMcaPhysics/xrf/FastXRFLinearFit.py +++ b/src/PyMca5/PyMcaPhysics/xrf/FastXRFLinearFit.py @@ -33,20 +33,24 @@ __doc__ = """ Module to perform a fast linear fit on a stack of fluorescence spectra. """ + import os -import numpy -import logging +import sys import time -import h5py -import collections -from . import ClassMcaTheory -from . import ConcentrationsTool +import logging + +import numpy + from PyMca5.PyMcaMath.linalg import lstsq from PyMca5.PyMcaMath.fitting import Gefit from PyMca5.PyMcaMath.fitting import SpecfitFuns from PyMca5.PyMcaIO import ConfigDict -from .XRFBatchFitOutput import OutputBuffer from PyMca5.PyMcaCore import McaStackView +from PyMca5.PyMcaMisc import CliUtils +from PyMca5.PyMcaMisc import ProfilingUtils +from . import ClassMcaTheory +from . import ConcentrationsTool +from .XRFBatchFitOutput import OutputBuffer _logger = logging.getLogger(__name__) @@ -1046,148 +1050,90 @@ def prepareDataStack(fileList): return dataStack -def main(): - import sys - import getopt - options = '' - longoptions = ['cfg=', 'outdir=', 'concentrations=', 'weight=', 'refit=', - 'tif=', 'edf=', 'csv=', 'h5=', 'dat=', - 'filepattern=', 'begin=', 'end=', 'increment=', - 'outroot=', 'outentry=', 'outprocess=', - 'diagnostics=', 'debug=', 'overwrite=', 'multipage='] - try: - opts, args = getopt.getopt( - sys.argv[1:], - options, - longoptions) - except Exception: - print(sys.exc_info()[1]) - sys.exit(1) - outputDir = None - outputRoot = "" - fileEntry = "" - fileProcess = "" - refit = None - filepattern = None - begin = None - end = None - increment = None - backend = None - weight = 0 - tif = 0 - edf = 0 - csv = 0 - h5 = 1 - dat = 0 - concentrations = 0 - diagnostics = 0 - debug = 0 - overwrite = 1 - multipage = 0 - for opt, arg in opts: - if opt == '--cfg': - configurationFile = arg - elif opt == '--begin': - if "," in arg: - begin = [int(x) for x in arg.split(",")] - else: - begin = [int(arg)] - elif opt == '--end': - if "," in arg: - end = [int(x) for x in arg.split(",")] - else: - end = int(arg) - elif opt == '--increment': - if "," in arg: - increment = [int(x) for x in arg.split(",")] - else: - increment = int(arg) - elif opt == '--filepattern': - filepattern = arg.replace('"', '') - filepattern = filepattern.replace("'", "") - elif opt == '--outdir': - outputDir = arg - elif opt == '--weight': - weight = int(arg) - elif opt == '--refit': - refit = int(arg) - elif opt == '--concentrations': - concentrations = int(arg) - elif opt == '--diagnostics': - diagnostics = int(arg) - elif opt == '--outroot': - outputRoot = arg - elif opt == '--outentry': - fileEntry = arg - elif opt == '--outprocess': - fileProcess = arg - elif opt in ('--tif', '--tiff'): - tif = int(arg) - elif opt == '--edf': - edf = int(arg) - elif opt == '--csv': - csv = int(arg) - elif opt == '--h5': - h5 = int(arg) - elif opt == '--dat': - dat = int(arg) - elif opt == '--debug': - debug = int(arg) - elif opt == '--overwrite': - overwrite = int(arg) - elif opt == '--multipage': - multipage = int(arg) - - logging.basicConfig() - if debug: - _logger.setLevel(logging.DEBUG) - else: - _logger.setLevel(logging.INFO) - if filepattern is not None: - if (begin is None) or (end is None): - raise ValueError(\ - "A file pattern needs at least a set of begin and end indices") - if filepattern is not None: - fileList = getFileListFromPattern(filepattern, begin, end, increment=increment) - else: - fileList = args - if refit is None: - refit = 0 - _logger.warning("--refit=%d taken as default" % refit) - if len(fileList): - dataStack = prepareDataStack(fileList) +def main(args): + """ + Main entry point for the FastXRFLinearFit CLI. + """ + # Validate file pattern arguments + if args.filepattern is not None: + if args.begin is None or args.end is None: + raise ValueError("A file pattern needs at least a set of begin and end indices") + filelist = getFileListFromPattern(args.filepattern, args.begin, args.end, increment=args.increment) else: - print("OPTIONS:", longoptions) - sys.exit(0) - if outputDir is None: - print("RESULTS WILL NOT BE SAVED: No output directory specified") + filelist = args.filelist + + if not filelist: + _logger.warning("No input files provided") + return 0 + + dataStack = prepareDataStack(filelist) + + if args.outdir is None: + _logger.warning("RESULTS WILL NOT BE SAVED: No output directory specified") t0 = time.time() fastFit = FastXRFLinearFit() - fastFit.setFitConfigurationFile(configurationFile) - print("Main configuring Elapsed = % s " % (time.time() - t0)) - - outbuffer = OutputBuffer(outputDir=outputDir, - outputRoot=outputRoot, - fileEntry=fileEntry, - fileProcess=fileProcess, - diagnostics=diagnostics, - tif=tif, edf=edf, csv=csv, - h5=h5, dat=dat, - multipage=multipage, - overwrite=overwrite) - - from PyMca5.PyMcaMisc import ProfilingUtils - with ProfilingUtils.profile(memory=debug, time=debug): + fastFit.setFitConfigurationFile(args.cfg) + _logger.info("Main configuring Elapsed = %.3f s", time.time() - t0) + + outbuffer = OutputBuffer( + outputDir=args.outdir, + outputRoot=args.outroot, + fileEntry=args.outentry, + fileProcess=args.outprocess, + diagnostics=args.diagnostics, + tif=args.tif, + edf=args.edf, + csv=args.csv, + h5=args.h5, + dat=args.dat, + multipage=args.multipage, + overwrite=args.overwrite + ) + + with ProfilingUtils.profile(memory=args.debug, time=args.debug): with outbuffer.saveContext(): - outbuffer = fastFit.fitMultipleSpectra(y=dataStack, - weight=weight, - refit=refit, - concentrations=concentrations, - outbuffer=outbuffer) - print("Total Elapsed = % s " % (time.time() - t0)) + outbuffer = fastFit.fitMultipleSpectra( + y=dataStack, + weight=args.weight, + refit=args.refit, + concentrations=args.concentrations, + outbuffer=outbuffer + ) + + _logger.info("Total Elapsed = %.3f s", time.time() - t0) + return 0 + + +def build_parser(): + parser = CliUtils.create_parser(description="Batch fast XRF fit") + + parser.add_argument("--cfg", required=True, type=str, help="Configuration file") + parser.add_argument("--filepattern", default=None, type=str, help="File pattern") + parser.add_argument("--begin", type=CliUtils.int_or_list, default=None, help="Begin index/indices, comma-separated") + parser.add_argument("--end", type=CliUtils.int_or_list, default=None, help="End index/indices, comma-separated") + parser.add_argument("--increment", type=CliUtils.int_or_list, default=None, help="Increment(s), comma-separated") + parser.add_argument("--outdir", default=None, type=str, help="Output directory") + parser.add_argument("--outroot", default=None, type=str, help="Output root name") + parser.add_argument("--outentry", default=None, type=str, help="File entry name") + parser.add_argument("--outprocess", default=None, type=str, help="Process name") + parser.add_argument("--weight", default=0, type=int, help="Weight") + parser.add_argument("--refit", type=int, default=0, help="Refit flag") + parser.add_argument("--concentrations", type=int, default=0, help="Concentration flag") + parser.add_argument("--diagnostics", type=int, default=0, help="Diagnostics flag") + parser.add_argument("--tif", type=int, default=0, help="TIF output flag") + parser.add_argument("--edf", type=int, default=0, help="EDF output flag") + parser.add_argument("--csv", type=int, default=0, help="CSV output flag") + parser.add_argument("--h5", type=int, default=1, help="H5 output flag") + parser.add_argument("--dat", type=int, default=0, help="DAT output flag") + parser.add_argument("--overwrite", type=int, default=1, help="Overwrite existing files") + parser.add_argument("--multipage", type=int, default=0, help="Multipage flag") + + parser.add_argument("filelist", nargs="*", help="Input files if not using filepattern") + + return parser if __name__ == "__main__": - logging.basicConfig(level=logging.INFO) - main() + exit_code = CliUtils.cli_main(main, build_parser(), loggers=(_logger,)) + sys.exit(exit_code) diff --git a/src/PyMca5/PyMcaPhysics/xrf/LegacyFastXRFLinearFit.py b/src/PyMca5/PyMcaPhysics/xrf/LegacyFastXRFLinearFit.py index 563ccdf99..a3be563ac 100644 --- a/src/PyMca5/PyMcaPhysics/xrf/LegacyFastXRFLinearFit.py +++ b/src/PyMca5/PyMcaPhysics/xrf/LegacyFastXRFLinearFit.py @@ -33,16 +33,24 @@ __doc__ = """ Module to perform a fast linear fit on a stack of fluorescence spectra. """ + import os -import numpy +import sys +import time import logging + +import numpy + from PyMca5.PyMcaMath.linalg import lstsq -from . import ClassMcaTheory from PyMca5.PyMcaMath.fitting import Gefit -from . import ConcentrationsTool from PyMca5.PyMcaMath.fitting import SpecfitFuns from PyMca5.PyMcaIO import ConfigDict -import time +from PyMca5.PyMca import EDFStack +from PyMca5.PyMcaIO import HDF5Stack1D +from PyMca5.PyMcaMisc import CliUtils + +from . import ClassMcaTheory +from . import ConcentrationsTool _logger = logging.getLogger(__name__) @@ -771,127 +779,97 @@ def save(result, outputDir, fileRoot=None, tif=False, csv=True): dtype=numpy.float32) -if __name__ == "__main__": - logging.basicConfig(level=logging.INFO) - _logger.setLevel(logging.DEBUG) - import glob - import sys - import getopt - from PyMca5.PyMca import EDFStack - options = '' - longoptions = ['cfg=', 'outdir=', 'concentrations=', 'weight=', 'refit=', - 'tif=', #'listfile=', - 'filepattern=', 'begin=', 'end=', 'increment=', - "outfileroot="] - try: - opts, args = getopt.getopt( - sys.argv[1:], - options, - longoptions) - except Exception: - print(sys.exc_info()[1]) - sys.exit(1) - fileRoot = "" - outputDir = None - refit = None - fileindex = 0 - filepattern=None - begin = None - end = None - increment=None - backend=None - weight=0 - tif=0 - concentrations=0 - for opt, arg in opts: - if opt in ('--cfg'): - configurationFile = arg - elif opt in '--begin': - if "," in arg: - begin = [int(x) for x in arg.split(",")] - else: - begin = [int(arg)] - elif opt in '--end': - if "," in arg: - end = [int(x) for x in arg.split(",")] - else: - end = int(arg) - elif opt in '--increment': - if "," in arg: - increment = [int(x) for x in arg.split(",")] - else: - increment = int(arg) - elif opt in '--filepattern': - filepattern = arg.replace('"', '') - filepattern = filepattern.replace("'", "") - elif opt in '--outdir': - outputDir = arg - elif opt in '--weight': - weight = int(arg) - elif opt in '--refit': - refit = int(arg) - elif opt in '--concentrations': - concentrations = int(arg) - elif opt in '--outfileroot': - fileRoot = arg - elif opt in ['--tif', '--tiff']: - tif = int(arg) - if filepattern is not None: - if (begin is None) or (end is None): - raise ValueError(\ - "A file pattern needs at least a set of begin and end indices") +def main(args): + """ + Main entry point for the FastXRFLinearFit CLI. + """ + # Build file list + filepattern = args.filepattern if filepattern is not None: + if begin is None or end is None: + raise ValueError("A file pattern needs at least a set of begin and end indices") fileList = getFileListFromPattern(filepattern, begin, end, increment=increment) else: - fileList = args - if refit is None: - refit = 0 - print("WARNING: --refit=%d taken as default" % refit) - if len(fileList): - if (not os.path.exists(fileList[0])) and \ - os.path.exists(fileList[0].split("::")[0]): - # odo convention to get a dataset form an HDF5 - fname, dataPath = fileList[0].split("::") - # compared to the ROI imaging tool, this way of reading puts data - # into memory while with the ROI imaging tool, there is a check. - if 0: - import h5py - h5 = h5py.File(fname, "r") - dataStack = h5[dataPath][:] - h5.close() - else: - from PyMca5.PyMcaIO import HDF5Stack1D - # this way reads information associated to the dataset (if present) - if dataPath.startswith("/"): - pathItems = dataPath[1:].split("/") - else: - pathItems = dataPath.split("/") - if len(pathItems) > 1: - scanlist = ["/" + pathItems[0]] - selection = {"y":"/" + "/".join(pathItems[1:])} - else: - selection = {"y":dataPath} - scanlist = None - print(selection) - print("scanlist = ", scanlist) - dataStack = HDF5Stack1D.HDF5Stack1D([fname], - selection, - scanlist=scanlist) + fileList = args.filelist + + if not fileList: + _logger.warning("No input files provided.") + return 0 + + # Parse comma-separated lists + def parse_index_list(s): + if s is None: + return None + parts = s.split(",") + return [int(x) for x in parts] if len(parts) > 1 else int(parts[0]) + + begin = parse_index_list(args.begin) + end = parse_index_list(args.end) + increment = parse_index_list(args.increment) + + # Handle HDF5 stack convention if first file exists as "file::dataset" + first = fileList[0] + if (not os.path.exists(first)) and os.path.exists(first.split("::")[0]): + fname, dataPath = first.split("::") + if dataPath.startswith("/"): + pathItems = dataPath[1:].split("/") else: - dataStack = EDFStack.EDFStack(fileList, dtype=numpy.float32) + pathItems = dataPath.split("/") + + if len(pathItems) > 1: + scanlist = ["/" + pathItems[0]] + selection = {"y": "/" + "/".join(pathItems[1:])} + else: + scanlist = None + selection = {"y": dataPath} + + _logger.debug("selection = %s, scanlist = %s", selection, scanlist) + dataStack = HDF5Stack1D.HDF5Stack1D([fname], selection, scanlist=scanlist) else: - print("OPTIONS:", longoptions) - sys.exit(0) - if outputDir is None: + dataStack = EDFStack.EDFStack(fileList, dtype=numpy.float32) + + if args.outdir is None: print("RESULTS WILL NOT BE SAVED: No output directory specified") + + # Main fitting t0 = time.time() fastFit = FastXRFLinearFit() - fastFit.setFitConfigurationFile(configurationFile) - print("Main configuring Elapsed = % s " % (time.time() - t0)) - result = fastFit.fitMultipleSpectra(y=dataStack, - weight=weight, - refit=refit, - concentrations=concentrations) - print("Total Elapsed = % s " % (time.time() - t0)) - if outputDir is not None: - save(result, outputDir, fileRoot=fileRoot, tif=False) + fastFit.setFitConfigurationFile(args.cfg) + print("Main configuring Elapsed = %s" % (time.time() - t0)) + + result = fastFit.fitMultipleSpectra( + y=dataStack, + weight=args.weight, + refit=args.refit, + concentrations=args.concentrations + ) + + print("Total Elapsed = %s" % (time.time() - t0)) + + if args.outdir is not None: + save(result, args.outdir, fileRoot=args.outfileroot, tif=args.tif) + + +def build_parser(): + parser = CliUtils.create_parser(description="Batch fast XRF fit") + + parser.add_argument("--cfg", required=True, type=str, help="Configuration file") + parser.add_argument("--outdir", default=None, type=str, help="Output directory") + parser.add_argument("--concentrations", default=0, type=int, help="Compute concentrations") + parser.add_argument("--weight", default=0, type=int, help="Weight for fitting") + parser.add_argument("--refit", default=0, type=int, help="Refit flag") + parser.add_argument("--tif", "--tiff", default=0, type=int, help="Write TIFF files") + parser.add_argument("--filepattern", default=None, type=str, help="File pattern") + parser.add_argument("--begin", default=None, type=CliUtils.int_or_list, help="Begin index/indices, comma-separated") + parser.add_argument("--end", default=None, type=CliUtils.int_or_list, help="End index/indices, comma-separated") + parser.add_argument("--increment", default=None, type=CliUtils.int_or_list, help="Increment(s), comma-separated") + parser.add_argument("--outfileroot", default=None, type=str, help="Output root file name") + + parser.add_argument("filelist", nargs="*", help="Input files if not using filepattern") + + return parser + + +if __name__ == "__main__": + exit_code = CliUtils.cli_main(main, build_parser()) + sys.exit(exit_code) diff --git a/src/PyMca5/PyMcaPhysics/xrf/LegacyMcaAdvancedFitBatch.py b/src/PyMca5/PyMcaPhysics/xrf/LegacyMcaAdvancedFitBatch.py index cf116eb3a..40e9fbc4e 100644 --- a/src/PyMca5/PyMcaPhysics/xrf/LegacyMcaAdvancedFitBatch.py +++ b/src/PyMca5/PyMcaPhysics/xrf/LegacyMcaAdvancedFitBatch.py @@ -49,6 +49,7 @@ except ImportError: HDF5SUPPORT = False from PyMca5.PyMcaIO import ConfigDict +from PyMca5.PyMcaMisc import CliUtils from . import ConcentrationsTool @@ -959,32 +960,36 @@ def saveImage(self,ffile=None): i=1 -if __name__ == "__main__": - import getopt - options = 'f' - longoptions = ['cfg=','pkm=','outdir=','roifit=','roi=','roiwidth='] - filelist = None - outdir = None - cfg = None - roifit = 0 - roiwidth = 250. - opts, args = getopt.getopt( - sys.argv[1:], - options, - longoptions) - for opt,arg in opts: - if opt in ('--pkm','--cfg'): - cfg = arg - elif opt in ('--outdir'): - outdir = arg - elif opt in ('--roi','--roifit'): - roifit = int(arg) - elif opt in ('--roiwidth'): - roiwidth = float(arg) - filelist=args - if len(filelist) == 0: - print("No input files, run GUI") - sys.exit(0) - - b = McaAdvancedFitBatch(cfg,filelist,outdir,roifit,roiwidth) +def main(args): + if not args.filelist: + print("No input files provided.") + return 0 + + # Create batch object and process + b = McaAdvancedFitBatch( + args.cfg, + args.filelist, + args.outdir, + args.roi, + args.roiwidth + ) b.processList() + + +def build_parser(): + parser = CliUtils.create_parser(description="Batch MCA advanced fit (simple)") + + parser.add_argument("--cfg", "--pkm", dest="cfg", required=True, type=str, help="Configuration file") + parser.add_argument("--outdir", default=None, type=str, help="Output directory") + parser.add_argument("--roi", "--roifit", default=0, type=int, help="ROI fit") + parser.add_argument("--roiwidth", default=250.0, type=float, help="ROI width") + + # Positional argument: list of input files + parser.add_argument("filelist", nargs="*", help="Input files") + + return parser + + +if __name__ == "__main__": + exit_code = CliUtils.cli_main(main, build_parser()) + sys.exit(exit_code) diff --git a/src/PyMca5/PyMcaPhysics/xrf/McaAdvancedFitBatch.py b/src/PyMca5/PyMcaPhysics/xrf/McaAdvancedFitBatch.py index 2956b696f..8b9f7f20c 100644 --- a/src/PyMca5/PyMcaPhysics/xrf/McaAdvancedFitBatch.py +++ b/src/PyMca5/PyMcaPhysics/xrf/McaAdvancedFitBatch.py @@ -45,6 +45,8 @@ from PyMca5.PyMcaIO import LispixMap from PyMca5.PyMcaIO import NumpyStack from PyMca5.PyMcaIO import BrukerBCF +from PyMca5.PyMcaMisc import ProfilingUtils +from PyMca5.PyMcaMisc import CliUtils try: import h5py @@ -1112,115 +1114,73 @@ def _storeRoiFitResult(self, result): output[i, self.__row, self.__col] = result[group][roi+' ROI'] -def main(): - import getopt - options = 'f' - longoptions = ['cfg=', 'pkm=', 'outdir=', 'roifit=', 'roi=', - 'roiwidth=', 'concentrations=', 'overwrite=', - 'outroot=', 'outentry=', 'outprocess=', - 'edf=', 'h5=', 'csv=', 'tif=', 'dat=', - 'diagnostics=', 'debug=', 'multipage='] - filelist = None - cfg = None - roifit = 0 - roiwidth = 250. - tif = 0 - edf = 1 - csv = 0 - h5 = 1 - dat = 0 - multipage = 0 - debug = 0 - outputDir = None - concentrations = 0 - diagnostics = 0 - overwrite = 1 - outputRoot = "" - fileEntry = "" - fileProcess = "" - opts, args = getopt.getopt( - sys.argv[1:], - options, - longoptions) - for opt,arg in opts: - if opt in ('--pkm','--cfg'): - cfg = arg - elif opt in ('--outdir'): - outputDir = arg - elif opt in ('--roi','--roifit'): - roifit = int(arg) - elif opt in ('--roiwidth'): - roiwidth = float(arg) - elif opt in ('--tif', '--tiff'): - tif = int(arg) - elif opt == '--edf': - edf = int(arg) - elif opt == '--csv': - csv = int(arg) - elif opt == '--dat': - dat = int(arg) - elif opt == '--h5': - h5 = int(arg) - elif opt == '--overwrite': - overwrite = int(arg) - elif opt == '--concentrations': - concentrations = int(arg) - elif opt == '--outroot': - outputRoot = arg - elif opt == '--outentry': - fileEntry = arg - elif opt == '--outprocess': - fileProcess = arg - elif opt == '--debug': - debug = int(arg) - elif opt == '--diagnostics': - diagnostics = int(arg) - elif opt == '--edf': - edf = int(arg) - elif opt == '--csv': - csv = int(arg) - elif opt == '--h5': - h5 = int(arg) - elif opt == '--dat': - dat = int(arg) - elif opt == '--multipage': - multipage = int(arg) - - logging.basicConfig() - if debug: - _logger.setLevel(logging.DEBUG) - else: - _logger.setLevel(logging.INFO) +def main(args): + filelist = args.filelist + if not filelist: + _logger.warning("No input files provided.") + return 0 - filelist=args - if len(filelist) == 0: - _logger.error("No input files, run GUI") - sys.exit(0) t0 = time.time() - outbuffer = OutputBuffer(outputDir=outputDir, - outputRoot=outputRoot, - fileEntry=fileEntry, - fileProcess=fileProcess, - diagnostics=diagnostics, - tif=tif, edf=edf, csv=csv, - h5=h5, dat=dat, - multipage=multipage, - overwrite=overwrite) - - from PyMca5.PyMcaMisc import ProfilingUtils - with ProfilingUtils.profile(memory=debug, time=debug): - b = McaAdvancedFitBatch(cfg,filelist=filelist, - fitfiles=False, - outputdir=outputDir, - roifit=roifit, - roiwidth=roiwidth, - concentrations=concentrations, - outbuffer=outbuffer, - overwrite=overwrite) + # Setup output buffer + outbuffer = OutputBuffer( + outputDir=args.outdir, + outputRoot=args.outroot, + fileEntry=args.outentry, + fileProcess=args.outprocess, + diagnostics=args.diagnostics, + tif=args.tif, + edf=args.edf, + csv=args.csv, + h5=args.h5, + dat=args.dat, + multipage=args.multipage, + overwrite=args.overwrite, + ) + + # Profiling context + with ProfilingUtils.profile(memory=args.debug, time=args.debug): + b = McaAdvancedFitBatch( + args.cfg, + filelist=filelist, + fitfiles=False, + outputdir=args.outdir, + roifit=args.roi, + roiwidth=args.roiwidth, + concentrations=args.concentrations, + outbuffer=outbuffer, + overwrite=args.overwrite, + ) b.processList() - print("Total Elapsed = % s " % (time.time() - t0)) + print("Total Elapsed = %s " % (time.time() - t0)) + + +def build_parser(): + parser = CliUtils.create_parser(description="Batch MCA advanced fit") + + parser.add_argument("--cfg", "--pkm", dest="cfg", required=True, type=str, help="Configuration file") + parser.add_argument("--outdir", default=None, type=str, help="Output directory") + parser.add_argument("--roi", "--roifit", default=0, type=int, help="ROI fit") + parser.add_argument("--roiwidth", default=250.0, type=float, help="ROI width") + parser.add_argument("--tif", "--tiff", default=0, type=int, help="Write TIFF files") + parser.add_argument("--edf", default=1, type=int, help="Write EDF files") + parser.add_argument("--csv", default=0, type=int, help="Write CSV files") + parser.add_argument("--h5", default=1, type=int, help="Write HDF5 files") + parser.add_argument("--dat", default=0, type=int, help="Write DAT files") + parser.add_argument("--multipage", default=0, type=int, help="Multipage output") + parser.add_argument("--diagnostics", default=0, type=int, help="Enable diagnostics") + parser.add_argument("--concentrations", default=0, type=int, help="Compute concentrations") + parser.add_argument("--outroot", default="", type=str, help="Output root name") + parser.add_argument("--outentry", default="", type=str, help="File entry") + parser.add_argument("--outprocess", default="", type=str, help="File process") + parser.add_argument("--overwrite", type=int, default=1, help="Overwrite existing files") + + # Positional arguments: list of input files + parser.add_argument("filelist", nargs="*", help="Input files") + + return parser + if __name__ == "__main__": - logging.basicConfig(level=logging.INFO) - main() + exit_code = CliUtils.cli_main(main, build_parser(), loggers=(_logger,)) + sys.exit(exit_code) diff --git a/src/PyMca5/PyMcaPhysics/xrf/XRFBatchFitOutput.py b/src/PyMca5/PyMcaPhysics/xrf/XRFBatchFitOutput.py index 62d82ae75..d8e45ffd4 100644 --- a/src/PyMca5/PyMcaPhysics/xrf/XRFBatchFitOutput.py +++ b/src/PyMca5/PyMcaPhysics/xrf/XRFBatchFitOutput.py @@ -27,7 +27,6 @@ # #############################################################################*/ __author__ = "Wout De Nolf" -__contact__ = "wout.de_nolf@esrf.eu" __license__ = "MIT" __copyright__ = "European Synchrotron Radiation Facility, Grenoble, France" diff --git a/src/PyMca5/PyMcaPhysics/xrf/XRayTubeEbel.py b/src/PyMca5/PyMcaPhysics/xrf/XRayTubeEbel.py index 79d9da6a8..65afc64d0 100644 --- a/src/PyMca5/PyMcaPhysics/xrf/XRayTubeEbel.py +++ b/src/PyMca5/PyMcaPhysics/xrf/XRayTubeEbel.py @@ -30,10 +30,16 @@ __contact__ = "sole@esrf.fr" __license__ = "MIT" __copyright__ = "European Synchrotron Radiation Facility, Grenoble, France" -from . import Elements + +import sys import math import numpy +import sys +from PyMca5.PyMcaMisc import CliUtils +from . import Elements + + def continuumEbel(target, e0, e=None, window=None, alphae=None, alphax=None, transmission=None, targetthickness=None, @@ -508,111 +514,80 @@ def generateLists(target, e0, window=None, return finalenergy, finalweight, scatterflag +def main(args): + # Energy array + e = numpy.arange(args.voltage * 10 + 1)[1:] / 10 + + # Compute spectra + _ = continuumEbel( + args.target, + args.voltage, + e, + [args.wele, Elements.Element[args.wele]['density'], args.wthickness], + alphae=args.anglee, + alphax=args.anglex, + transmission=args.transmission, + targetthickness=args.tthickness, + filterlist=None + ) + + fllines = characteristicEbel( + args.target, + args.voltage, + [args.wele, Elements.Element[args.wele]['density'], args.wthickness], + alphae=args.anglee, + alphax=args.anglex, + transmission=args.transmission, + targetthickness=args.tthickness, + filterlist=None + ) + + # Print characteristic lines + fsum = 0.0 + for l in fllines: + print("%s %.4f %.3e" % (l[2], l[0], l[1])) + fsum += l[1] + + energy, weight, scatter = generateLists( + args.target, + args.voltage, + [args.wele, Elements.Element[args.wele]['density'], args.wthickness], + alphae=args.anglee, + alphax=args.anglex, + transmission=args.transmission, + targetthickness=args.tthickness, + filterlist=None + ) + + # Write output file + fname = "Tube_%s_%.1f_%s_%.5f_ae%.1f_ax%.1f.txt" % ( + args.target, args.voltage, args.wele, args.wthickness, args.anglee, args.anglex + ) + + with open(fname, "w") as f: + f.write("energyweight= " + ", ".join(str(w) for w in weight) + "\n") + f.write("energy= " + ", ".join(str(e) for e in energy) + "\n") + f.write("energyflag= " + ", ".join("1" for _ in energy) + "\n") + f.write("energyscatter= " + ", ".join(str(s) for s in scatter) + "\n") + + +def build_parser(): + parser = CliUtils.create_parser(description="Generate Tube Spectrum Files") + + parser.add_argument("--target", default="Ag", type=str, help="Target element") + parser.add_argument("--voltage", default=40, type=float, help="Tube voltage (kV)") + parser.add_argument("--wele", "--window", default="Be", type=str, help="Window element") + parser.add_argument("--wthickness", default=0.0125, type=float, help="Window thickness (cm)") + parser.add_argument("--anglee", "--alphae", default=70, type=float, help="Emission angle (deg)") + parser.add_argument("--anglex", "--alphax", default=50, type=float, help="X-ray angle (deg)") + parser.add_argument("--cfg", default=None, type=str, help="Config file") + parser.add_argument("--deltae", default=None, type=float, help="Delta E (optional)") + parser.add_argument("--transmission", default=None, type=int, help="Transmission factor") + parser.add_argument("--tthickness", default=None, type=float, help="Target thickness") + + return parser + + if __name__ == "__main__": - import sys - import getopt - options = '' - longoptions = ['target=', 'voltage=', 'wele=', 'window=', 'wthickness=', - 'anglee=', 'anglex=', - 'cfg=', 'deltae=', 'transmission=', 'tthickness='] - opts, args = getopt.getopt( - sys.argv[1:], - options, - longoptions) - target = 'Ag' - voltage = 40 - wele = 'Be' - wthickness = 0.0125 - anglee = 70 - anglex = 50 - cfgfile = None - transmission = None - ttarget = None - filterlist = None - for opt, arg in opts: - if opt in ('--target'): - target = arg - elif opt in ('--tthickness'): - ttarget = float(arg) - if opt in ('--cfg'): - cfgfile = arg - if opt in ('--voltage'): - voltage = float(arg) - if opt in ('--wthickness'): - wthickness = float(arg) - if opt in ('--wele', 'window'): - wele = arg - if opt in ('--transmission'): - transmission = int(arg) - if opt in ('--anglee', '--alphae'): - anglee = float(arg) - if opt in ('--anglex', '--alphax'): - anglex = float(arg) - try: - e = numpy.arange(voltage * 10 + 1)[1:] / 10 - y = continuumEbel(target, voltage, e, - [wele, Elements.Element[wele]['density'], - wthickness], - alphae=anglee, alphax=anglex, - transmission=transmission, - targetthickness=ttarget, - filterlist=filterlist) - fllines = characteristicEbel(target, voltage, - [wele, Elements.Element[wele]['density'], - wthickness], - alphae=anglee, alphax=anglex, - transmission=transmission, - targetthickness=ttarget, - filterlist=filterlist) - fsum = 0.0 - for l in fllines: - print("%s %.4f %.3e" % (l[2], l[0], l[1])) - fsum += l[1] - energy, weight, scatter = \ - generateLists(target, voltage, - [wele, Elements.Element[wele]['density'], wthickness], - alphae=anglee, alphax=anglex, - transmission=transmission, targetthickness=ttarget, - filterlist=filterlist) - - f = open("Tube_%s_%.1f_%s_%.5f_ae%.1f_ax%.1f.txt" % (target, voltage, - wele, wthickness, - anglee, anglex), - "w+") - text = "energyweight=" - for i in range(len(energy)): - if i == 0: - text += " %f" % weight[i] - else: - text += ", %f" % weight[i] - text += "\n" - f.write(text) - text = "energy=" - for i in range(len(energy)): - if i == 0: - text += " %f" % energy[i] - else: - text += ", %f" % energy[i] - text += "\n" - f.write(text) - text = "energyflag=" - for i in range(len(energy)): - if i == 0: - text += " %f" % 1 - else: - text += ", %f" % 1 - text += "\n" - f.write(text) - text = "energyscatter=" - for i in range(len(energy)): - if i == 0: - text += " %f" % scatter[i] - else: - text += ", %f" % scatter[i] - text += "\n" - f.write(text) - f.close() - except Exception: - print("Usage:") - print("options = ", longoptions) - sys.exit(0) + exit_code = CliUtils.cli_main(main, build_parser()) + sys.exit(exit_code) diff --git a/src/PyMca5/tests/CliTest.py b/src/PyMca5/tests/CliTest.py new file mode 100644 index 000000000..b95b117fe --- /dev/null +++ b/src/PyMca5/tests/CliTest.py @@ -0,0 +1,408 @@ +# /*########################################################################## +# +# The PyMca X-Ray Fluorescence Toolkit +# +# Copyright (c) 2004-2026 European Synchrotron Radiation Facility +# +# This file is part of the PyMca X-ray Fluorescence Toolkit developed at +# the ESRF. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +#############################################################################*/ +__author__ = "Wout De Nolf" +__license__ = "MIT" +__copyright__ = "European Synchrotron Radiation Facility, Grenoble, France" + +import contextlib +import importlib +import io +import logging +import os +import subprocess +import sys +import tempfile +import unittest +from dataclasses import dataclass, field +from pathlib import Path +from typing import Callable, List, Optional, Type + +import numpy + +from PyMca5.PyMcaIO.EdfFile import EdfFile +from PyMca5.PyMcaMisc import CliUtils + +_logger = logging.getLogger(__name__) + + +@dataclass +class CliScenario: + name: str + args: List[str] + return_code: int = 0 + expect_files: List[str] = field(default_factory=list) + forbid_files: List[str] = field(default_factory=list) + use_data_dir: bool = False + qt_app: bool = False + post_check: Optional[Callable] = None + + +CLI_SPECS = { + "PyMca5.PyMcaPhysics.XRayTubeEbel": [ + CliScenario("help", ["--help"]), + CliScenario("default_generates_txt", [], expect_files=["Tube_*.txt"]), + ], + "PyMca5.PyMcaPhysics.xrf.ConcentrationsTool": [ + CliScenario("help", ["--help"]), + CliScenario("noargs", []), + ], + "PyMca5.PyMcaGui.physics.xrf.ConcentrationsWidget": [ + CliScenario("help", ["--help"]), + CliScenario("noargs", ["--cli-test"], qt_app=True), + ], + "PyMca5.PyMcaGui.physics.xrf.ElementsInfo": [ + CliScenario("help", ["--help"]), + CliScenario("noargs", ["--cli-test"], qt_app=True), + ], + "PyMca5.PyMcaGui.physics.xrf.PeakIdentifier": [ + CliScenario("help", ["--help"]), + CliScenario("noargs", ["--cli-test"], qt_app=True), + ], + "PyMca5.PyMcaGui.pymca.StackSelector": [ + CliScenario("help", ["--help"]), + CliScenario("qt_noargs", ["--cli-test"], qt_app=True), + ], + "PyMca5.PyMcaGui.pymca.QStackWidget": [ + CliScenario("help", ["--help"]), + CliScenario("qt_noargs", ["--cli-test"], qt_app=True), + ], + "PyMca5.PyMcaGui.pymca.RGBCorrelatorWidget": [ + CliScenario("help", ["--help"]), + CliScenario("qt_noargs", ["--cli-test"], qt_app=True), + ], + "PyMca5.PyMcaGui.pymca.RGBCorrelator": [ + CliScenario("help", ["--help"]), + CliScenario("qt_noargs", ["--cli-test"], qt_app=True), + ], + "PyMca5.PyMcaGui.pymca.PyMcaPostBatch": [ + CliScenario("help", ["--help"]), + CliScenario("qt_noargs", ["--cli-test"], qt_app=True), + ], + "PyMca5.PyMcaGui.pymca.PyMcaBatch": [ + CliScenario("help", ["--help"]), + CliScenario("qt_noargs", ["--cli-test"], qt_app=True), + ], + "PyMca5.PyMcaGui.pymca.PyMcaMdi": [ + CliScenario("help", ["--help"]), + CliScenario("qt_noargs", ["--cli-test"], qt_app=True), + ], + "PyMca5.PyMcaGui.pymca.PyMcaMain": [ + CliScenario("help", ["--help"]), + CliScenario("qt_noargs", ["--cli-test"], qt_app=True), + ], + "PyMca5.PyMcaGui.pymca.Mca2Edf": [ + CliScenario("help", ["--help"]), + CliScenario("qt_noargs", ["--cli-test"], qt_app=True), + ], + "PyMca5.PyMcaGui.pymca.LegacyPyMcaBatch": [ + CliScenario("help", ["--help"]), + CliScenario("qt_noargs", ["--cli-test"], qt_app=True), + ], + "PyMca5.PyMcaGui.pymca.Fit2Spec": [ + CliScenario("help", ["--help"]), + CliScenario("qt_noargs", ["--cli-test"], qt_app=True), + ], + "PyMca5.PyMcaGui.pymca.EdfFileSimpleViewer": [ + CliScenario("help", ["--help"]), + CliScenario("qt_noargs", ["--cli-test"], qt_app=True), + ], + "PyMca5.PyMcaGui.plotting.ImageView": [ + CliScenario("help", ["--help"]), + CliScenario("qt_noargs", ["--cli-test", "test.edf"], qt_app=True), + ], + "PyMca5.PyMcaGui.plotting.MaskImageWidget": [ + CliScenario("help", ["--help"]), + CliScenario("qt_noargs", ["--cli-test"], qt_app=True), + ], + "PyMca5.PyMcaCore.XiaCorrect": [ + CliScenario("help", ["--help"]), + CliScenario("qt_noargs", ["--cli-test"], qt_app=True), + ], + "PyMca5.PyMcaCore.StackROIBatch": [ + CliScenario("help", ["--help"]), + CliScenario("qt_noargs", ["--cli-test"], qt_app=True), + ], + "PyMca5.PyMcaCore.LegacyStackROIBatch": [ + CliScenario("help", ["--help"]), + CliScenario("noargs", []), + ], + "PyMca5.PyMcaGui.physics.xrf.McaCalWidget": [ + CliScenario("help", ["--help"]), + CliScenario("qt_noargs", ["--cli-test"], qt_app=True), + ], + "PyMca5.PyMcaPhysics.McaAdvancedFitBatch": [ + CliScenario("help", ["--help"]), + CliScenario("requires_cfg", ["--cfg", "dummy.cfg"]), + ], + "PyMca5.PyMcaPhysics.FastXRFLinearFit": [ + CliScenario("help", ["--help"]), + CliScenario("requires_cfg", ["--cfg", "dummy.cfg"]), + ], + "PyMca5.PyMcaPhysics.LegacyMcaAdvancedFitBatch": [ + CliScenario("help", ["--help"]), + CliScenario("requires_cfg", ["--cfg", "dummy.cfg"]), + ], + "PyMca5.PyMcaPhysics.LegacyFastXRFLinearFit": [ + CliScenario("help", ["--help"]), + CliScenario("requires_cfg", ["--cfg", "dummy.cfg"]), + ], + "PyMca5.PyMcaPhysics.xrf.ClassMcaTheory": [ + CliScenario("help", ["--help"]), + CliScenario( + "requires_existing_data", + ["--cfg", "__STEEL_CFG__", "--file", "__STEEL_SPE__"], + use_data_dir=True, + ), + ], +} + + +class TestCliModules(unittest.TestCase): + + def setUp(self): + self._orig_cwd = None + self._temporary_cwd = None + self._original_data_dir = None + self._current_data_dir = None + + super().setUp() + self._setup_pymca_data_dir() + self._setup_cwd() + + def tearDown(self): + self._restore_cwd() + self._restore_pymca_data_dir() + super().tearDown() + + def _setup_cwd(self): + """Ensure all CLI tests run in a temporary directory + so the current working directory does not get cluttered + with files. + """ + self._temporary_cwd = tempfile.TemporaryDirectory() + self._orig_cwd = os.getcwd() + os.chdir(self._temporary_cwd.name) + self._create_files() + + def _restore_cwd(self): + if self._orig_cwd: + os.chdir(self._orig_cwd) + if self._temporary_cwd: + self._temporary_cwd.cleanup() + self._temporary_cwd = None + + def _setup_pymca_data_dir(self): + """Ensure PYMCA_DATA_DIR is an absolute path before + we change the current working directory. + """ + try: + from PyMca5 import PyMcaDataDir + except Exception: + self._original_data_dir = None + self._current_data_dir = None + return + + self._original_data_dir = PyMcaDataDir.PYMCA_DATA_DIR + self._current_data_dir = os.path.abspath(self._original_data_dir) + PyMcaDataDir.PYMCA_DATA_DIR = self._current_data_dir + + def _restore_pymca_data_dir(self): + try: + from PyMca5 import PyMcaDataDir + except Exception: + return + if self._original_data_dir is None: + return + PyMcaDataDir.PYMCA_DATA_DIR = self._original_data_dir + self._original_data_dir = None + self._current_data_dir = None + + def _create_files(self): + """Create files in the current working directory.""" + image = numpy.arange(10 * 20).reshape((10, 20)) + edf = EdfFile("test.edf", "wb+") + edf.WriteImage({}, image) + del edf + + def _run_scenario(self, module: Type, scenario: CliScenario): + args = list(scenario.args) + + # Replace placeholders + if scenario.use_data_dir: + for i, value in enumerate(args): + if value == "__STEEL_CFG__": + args[i] = self._get_data_file("Steel.cfg") + elif value == "__STEEL_SPE__": + args[i] = self._get_data_file("Steel.spe") + + # Check execution + return_code = self._call_cli(module, args, scenario) + self.assertEqual(return_code, scenario.return_code) + + # Check expected files + for pattern in scenario.expect_files: + self.assertTrue( + self._glob_count(pattern) > 0, + f"Expected file pattern '{pattern}' not created", + ) + + # Check forbidden files + for pattern in scenario.forbid_files: + self.assertEqual( + self._glob_count(pattern), + 0, + f"Unexpected file pattern '{pattern}' created", + ) + + if scenario.post_check: + scenario.post_check(self) + + def _call_cli(self, module, args, scenario): + # Call the CLI in a sub-process. + cmd = self._subprocess_cmd(module, args) + if cmd: + return self._run_cli_main_subprocess(cmd) + + # Do not test Qt CLI's in the current process + # to avoid the need to handle the Qt application + # life-cycle. + if scenario.qt_app: + self.skipTest("Qt CLI only tested in a subprocess") + + # Test the CLI in the current process. + return self._run_cli_main(module, args) + + def _run_cli_main_subprocess(self, cmd): + """Call CLI in a sub-process.""" + _logger.info("Execute command: %s", " ".join(cmd)) + + completed = subprocess.run( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + ) + + if completed.returncode != 0: + print("\n--- Subprocess Output ---") + print(completed.stdout) + print("--- End Subprocess Output ---\n") + + return completed.returncode + + def _run_cli_main(self, module, args): + """Call CLI in the current process.""" + _logger.info( + "Execute CLI main from %s with arguments %s", + module.__name__, + args, + ) + + parser = module.build_parser() + + buffer = io.StringIO() + + with contextlib.redirect_stdout(buffer), contextlib.redirect_stderr(buffer): + try: + return_code = CliUtils.cli_main(module.main, parser, args=args) + except SystemExit as ex: + return_code = ex.code + + self.assertIsInstance(return_code, int) + + if return_code != 0: + print("\n--- CLI Output ---") + print(buffer.getvalue()) + print("--- End CLI Output ---\n") + + return return_code + + def _subprocess_cmd(self, module, args): + """Sub-process command to call the CLI.""" + # Python CLI subprocess command if available. + frozen = getattr(sys, "frozen", False) + if not frozen: + return [sys.executable, "-m", module.__name__, *args] + + # Frozen binary subprocess command if available. + name = module.__name__.split(".")[-1] + exe_dir = Path(sys.executable).parent + executables = {exe.stem: exe for exe in exe_dir.iterdir() if exe.is_file()} + if name in executables: + return [str(executables[name]), *args] + + # No subprocess command available. + return None + + def _glob(self, pattern): + return list(Path(self._temporary_cwd.name).glob(pattern)) + + def _glob_count(self, pattern): + return len(self._glob(pattern)) + + def _get_data_file(self, *parts): + if self._current_data_dir is None: + self.skipTest("PyMca Data Directory cannot be found") + return str(Path(self._current_data_dir).joinpath(*parts)) + + +# Add tests dynamically on import because `subTest` does not print each test +# separately which is important to see what is skipped and why and to run individual tests. +def _make_test(module_path, scenario): + def test(self): + try: + module = importlib.import_module(module_path) + except Exception as ex: + self.skipTest(f"Cannot import {module_path}: {ex}") + + self._run_scenario(module, scenario) + + module_name = module_path.split(".")[-1] + test.__name__ = f"test_{module_name}_{scenario.name}".replace(" ", "_") + return test + + +for module_path, scenarios in CLI_SPECS.items(): + for scenario in scenarios: + test_method = _make_test(module_path, scenario) + setattr(TestCliModules, test_method.__name__, test_method) + + +def getSuite(auto=True): + suite = unittest.TestSuite() + suite.addTest(unittest.TestLoader().loadTestsFromTestCase(TestCliModules)) + return suite + + +def test(auto=False): + unittest.TextTestRunner(verbosity=2).run(getSuite(auto=auto)) + + +if __name__ == "__main__": + test() diff --git a/src/PyMca5/tests/HDF5UtilsTest.py b/src/PyMca5/tests/HDF5UtilsTest.py index 9b9046489..b6f2f064e 100644 --- a/src/PyMca5/tests/HDF5UtilsTest.py +++ b/src/PyMca5/tests/HDF5UtilsTest.py @@ -36,7 +36,7 @@ def setUp(self): def tearDown(self): shutil.rmtree(self.path) - @unittest.skipIf(hasattr(sys, 'frozen'), "skipped running as frozen binary") + @unittest.skipIf(getattr(sys, 'frozen', False), "skipped running as frozen binary") def testHdf5GroupKeys(self): filename = os.path.join(self.path, "test.h5") with h5py.File(filename, "w", track_order=True) as f: @@ -148,7 +148,7 @@ def getSuite(auto=True): testSuite.addTest(unittest.TestLoader().loadTestsFromTestCase(testHDF5Utils)) else: # use a predefined order - if not hasattr(sys, 'frozen'): + if not getattr(sys, 'frozen', False): testSuite.addTest(testHDF5Utils("testHdf5GroupKeys")) testSuite.addTest(testHDF5Utils("testSegFault")) return testSuite diff --git a/src/PyMca5/tests/McaStackViewTest.py b/src/PyMca5/tests/McaStackViewTest.py index 156a161e1..bc12bc3fa 100644 --- a/src/PyMca5/tests/McaStackViewTest.py +++ b/src/PyMca5/tests/McaStackViewTest.py @@ -27,9 +27,9 @@ # #############################################################################*/ __author__ = "Wout De Nolf" -__contact__ = "wout.de_nolf@esrf.eu" __license__ = "MIT" __copyright__ = "European Synchrotron Radiation Facility, Grenoble, France" + import unittest import tempfile import shutil diff --git a/src/PyMca5/tests/NexusUtilsTest.py b/src/PyMca5/tests/NexusUtilsTest.py index c3b58e637..075ade2f5 100644 --- a/src/PyMca5/tests/NexusUtilsTest.py +++ b/src/PyMca5/tests/NexusUtilsTest.py @@ -27,9 +27,9 @@ # #############################################################################*/ __author__ = "Wout De Nolf" -__contact__ = "wout.de_nolf@esrf.eu" __license__ = "MIT" __copyright__ = "European Synchrotron Radiation Facility, Grenoble, France" + import unittest import tempfile import shutil diff --git a/src/PyMca5/tests/PyMcaBatchTest.py b/src/PyMca5/tests/PyMcaBatchTest.py index 58fdbc8d8..ff0426b4c 100644 --- a/src/PyMca5/tests/PyMcaBatchTest.py +++ b/src/PyMca5/tests/PyMcaBatchTest.py @@ -27,9 +27,9 @@ # #############################################################################*/ __author__ = "Wout De Nolf" -__contact__ = "wout.de_nolf@esrf.eu" __license__ = "MIT" __copyright__ = "European Synchrotron Radiation Facility, Grenoble, France" + import unittest import sys import os @@ -153,7 +153,7 @@ def testSlowRoiFitEdfMap(self): self._assertSlowFitMap('edf', roiwidth=100, outputdir='fitresulta') self._assertSlowGuiFitMap('edf', roiwidth=100, outputdir='fitresultb') - @unittest.skipIf(hasattr(sys, 'frozen') or numpy.version.version == '1.17.0', "skipped running as frozen binary or numpy issue 13715") + @unittest.skipIf(getattr(sys, 'frozen', False) or numpy.version.version == '1.17.0', "skipped running as frozen binary or numpy issue 13715") def testSlowMultiFitEdfMap(self): self._assertSlowMultiFitMap('edf') @@ -167,7 +167,7 @@ def testSlowRoiFitSpecMap(self): self._assertSlowFitMap('specmesh', roiwidth=100, outputdir='fitresulta') self._assertSlowGuiFitMap('specmesh', roiwidth=100, outputdir='fitresultb') - @unittest.skipIf(hasattr(sys, 'frozen') or numpy.version.version == '1.17.0', "skipped running as frozen binary or numpy issue 13715") + @unittest.skipIf(getattr(sys, 'frozen', False) or numpy.version.version == '1.17.0', "skipped running as frozen binary or numpy issue 13715") def testSlowMultiFitSpecMap(self): self._assertSlowMultiFitMap('specmesh') @@ -205,10 +205,12 @@ def _assertSlowFitMap(self, typ, outputdir='fitresults', **kwargs): def _assertSlowMultiFitMap(self, typ, outputdir='fitresults', **kwargs): from PyMca5.PyMcaGui.pymca.PyMcaBatch import ranAsBootstrap info = self._generateData(typ=typ) + # Compare single vs. multi processing result1 = self._fitMap(info, nBatches=2, outputdir=outputdir+'1', **kwargs) result2 = self._fitMap(info, nBatches=1, outputdir=outputdir+'2', **kwargs) self._assertEqualFitResults(result1, result2, rtol=0) + if not ranAsBootstrap() and typ != 'hdf5': # REMARK: not supported by legacy code # - testing from source @@ -222,14 +224,17 @@ def _assertSlowMultiFitMap(self, typ, outputdir='fitresults', **kwargs): outputdir=outputdir+'4', **kwargs) if typ != 'specmesh': self._assertEqualFitResults(result3, result4, rtol=0) + # Compare with legacy PyMcaBatch if typ != 'specmesh': self._assertEqualFitResults(result1, result3, rtol=self._rtolLegacy) self._assertEqualFitResults(result2, result4, rtol=self._rtolLegacy) + # Compare thread vs. process result5 = self._fitMap(info, nBatches=0, outputdir=outputdir+'5', **kwargs) self._assertEqualFitResults(result2, result5, rtol=0) + # Compare blocking vs. non-blocking process result6 = self._fitMap(info, nBatches=1, blocking=True, outputdir=outputdir+'6', **kwargs) @@ -239,6 +244,7 @@ def _assertSlowGuiFitMap(self, typ, outputdir='fitresults', **kwargs): from PyMca5.PyMcaGui.pymca.PyMcaBatch import ranAsBootstrap info = self._generateData(typ=typ) result1 = self._fitMap(info, nBatches=1, outputdir=outputdir+'1', **kwargs) + if not ranAsBootstrap() and typ != 'hdf5': # Compare with legacy PyMcaBatch result2 = self._fitMap(info, nBatches=1, legacy=True, @@ -669,7 +675,7 @@ def getSuite(auto=True): testSuite.addTest(testPyMcaBatch("testFastFitEdfMap")) testSuite.addTest(testPyMcaBatch("testSlowFitEdfMap")) testSuite.addTest(testPyMcaBatch("testSlowRoiFitEdfMap")) - if not hasattr(sys, 'frozen'): + if not getattr(sys, 'frozen', False): testSuite.addTest(testPyMcaBatch("testSlowMultiFitEdfMap")) testSuite.addTest(testPyMcaBatch("testFastFitHdf5Map")) testSuite.addTest(testPyMcaBatch("testSlowFitHdf5Map")) @@ -678,7 +684,7 @@ def getSuite(auto=True): testSuite.addTest(testPyMcaBatch("testFastFitSpecMap")) testSuite.addTest(testPyMcaBatch("testSlowFitSpecMap")) testSuite.addTest(testPyMcaBatch("testSlowRoiFitSpecMap")) - if not hasattr(sys, 'frozen'): + if not getattr(sys, 'frozen', False): testSuite.addTest(testPyMcaBatch("testSlowMultiFitSpecMap")) return testSuite diff --git a/src/PyMca5/tests/TestAll.py b/src/PyMca5/tests/TestAll.py index fc18cc0fc..371863cc6 100644 --- a/src/PyMca5/tests/TestAll.py +++ b/src/PyMca5/tests/TestAll.py @@ -45,8 +45,8 @@ def getSuite(auto=True): modName = os.path.splitext(os.path.basename(fname))[0] try: module = __import__(modName) - except ImportError: - print("Failed to import %s" % fname) + except ImportError as ex: + print("Failed to import %s: %s" % (fname, ex)) continue if hasattr(module, "getSuite"): testSuite.addTest(module.getSuite(auto)) diff --git a/src/PyMca5/tests/XRFBatchFitOutputTest.py b/src/PyMca5/tests/XRFBatchFitOutputTest.py index 3aae36fa2..93999b6d9 100644 --- a/src/PyMca5/tests/XRFBatchFitOutputTest.py +++ b/src/PyMca5/tests/XRFBatchFitOutputTest.py @@ -27,9 +27,9 @@ # #############################################################################*/ __author__ = "Wout De Nolf" -__contact__ = "wout.de_nolf@esrf.eu" __license__ = "MIT" __copyright__ = "European Synchrotron Radiation Facility, Grenoble, France" + import unittest import tempfile import shutil diff --git a/src/PyMca5/tests/XrfData.py b/src/PyMca5/tests/XrfData.py index c4dcbd89d..ea3915293 100644 --- a/src/PyMca5/tests/XrfData.py +++ b/src/PyMca5/tests/XrfData.py @@ -27,9 +27,9 @@ # #############################################################################*/ __author__ = "Wout De Nolf" -__contact__ = "wout.de_nolf@esrf.eu" __license__ = "MIT" __copyright__ = "European Synchrotron Radiation Facility, Grenoble, France" + import numpy import time import os diff --git a/src/PyMca5/tests/XrfDataTest.py b/src/PyMca5/tests/XrfDataTest.py index 2a29252bd..02b55a5d2 100644 --- a/src/PyMca5/tests/XrfDataTest.py +++ b/src/PyMca5/tests/XrfDataTest.py @@ -27,9 +27,9 @@ # #############################################################################*/ __author__ = "Wout De Nolf" -__contact__ = "wout.de_nolf@esrf.eu" __license__ = "MIT" __copyright__ = "European Synchrotron Radiation Facility, Grenoble, France" + import unittest import tempfile import shutil