diff --git a/package/build_force.sh b/package/build_force.sh index a1b505fb..12b09640 100755 --- a/package/build_force.sh +++ b/package/build_force.sh @@ -3,18 +3,18 @@ # Have users point to the location of their conda installation so we can properly activate the # conda environment that is being made. Use the "--conda-defs" option to specify this path. while [[ $# -gt 0 ]]; do - key="$1" - case $key in - --conda-defs) - CONDA_DEFS="$2" - shift - shift - ;; - *) - echo "Unknown option: $1" - exit 1 - ;; - esac + key="$1" + case $key in + --conda-defs) + CONDA_DEFS="$2" + shift + shift + ;; + *) + echo "Unknown option: $1" + exit 1 + ;; + esac done # Establish conda environment @@ -24,18 +24,18 @@ conda activate force_build_310 # Check that the conda environment is active. If not, exit. if [[ $CONDA_DEFAULT_ENV != "force_build_310" ]]; then - echo "Conda environment not activated. Maybe the path to the conda installation is incorrect?" - echo "Provided conda path: $CONDA_DEFS" - exit 1 + echo "Conda environment not activated. Maybe the path to the conda installation is incorrect?" + echo "Provided conda path: $CONDA_DEFS" + exit 1 fi pip install cx_Freeze pip install raven-framework heron-ravenframework teal-ravenframework # If on macOS, use conda to install ipopt if [[ "$OSTYPE" == "darwin"* ]]; then - # Note: The PyPI version of ipopt is not maintained and is severl major version - # behind the conda-forge distribution. - conda install -c conda-forge ipopt -y + # Note: The PyPI version of ipopt is not maintained and is severl major version + # behind the conda-forge distribution. + conda install -c conda-forge ipopt -y fi # Build the FORCE executables diff --git a/package/build_mac_app.sh b/package/build_mac_app.sh index 53b4f553..b7349192 100755 --- a/package/build_mac_app.sh +++ b/package/build_mac_app.sh @@ -37,14 +37,14 @@ cp -Rp docs /Volumes/FORCE/ # Move the "examples" and "docs" directories from the FORCE app bundle to the top level of the disk # image to make them more accessible. if [ -d FORCE.app/Contents/Resources/examples ]; then - mv /Volumes/FORCE/FORCE.app/Contents/Resources/examples /Volumes/FORCE/ + mv /Volumes/FORCE/FORCE.app/Contents/Resources/examples /Volumes/FORCE/ else - echo "WARNING: No examples directory found in FORCE.app bundle" + echo "WARNING: No examples directory found in FORCE.app bundle" fi if [ -d FORCE.app/Contents/Resources/docs ]; then - mv FORCE.app/Contents/Resources/docs /Volumes/FORCE/ + mv FORCE.app/Contents/Resources/docs /Volumes/FORCE/ else - echo "WARNING: No docs directory found in FORCE.app bundle" + echo "WARNING: No docs directory found in FORCE.app bundle" fi # Add .son file to Workbench app to provide a default HERON configuration @@ -59,7 +59,7 @@ hdiutil detach /Volumes/FORCE # Convert to read-only compressed image if [ -f FORCE.dmg ]; then - rm FORCE.dmg + rm FORCE.dmg fi hdiutil convert force_build.dmg -format UDZO -o FORCE.dmg diff --git a/package/copy_examples.sh b/package/copy_examples.sh index 6bf2ca06..562bf08d 100755 --- a/package/copy_examples.sh +++ b/package/copy_examples.sh @@ -10,28 +10,28 @@ EXAMPLES_DIR="$SCRIPT_DIR/examples" while [[ $# -gt 0 ]] do - key="$1" - case $key in - --raven-dir) - RAVEN_DIR="$2" - shift - shift - ;; - --heron-dir) - HERON_DIR="$2" - shift - shift - ;; - --dest) - EXAMPLES_DIR="$2" - shift - shift - ;; - *) - echo "Unknown option: $1" - exit 1 - ;; - esac + key="$1" + case $key in + --raven-dir) + RAVEN_DIR="$2" + shift + shift + ;; + --heron-dir) + HERON_DIR="$2" + shift + shift + ;; + --dest) + EXAMPLES_DIR="$2" + shift + shift + ;; + *) + echo "Unknown option: $1" + exit 1 + ;; + esac done # The examples we want to copy are the RAVEN user_guide tests, the HERON workshop tests, and the @@ -40,17 +40,17 @@ EXAMPLES=($RAVEN_DIR/tests/framework/user_guide $HERON_DIR/data $HERON_DIR/tests mkdir -p $EXAMPLES_DIR for ex in ${EXAMPLES[@]}; do - cp -R "$ex" "$EXAMPLES_DIR" + cp -R "$ex" "$EXAMPLES_DIR" done # Clean up the copied examples, removing files and directories created when running the tests. DIRS_TO_REMOVE=("__pycache__" "gold" "*_o") for dirname in ${DIRS_TO_REMOVE[@]}; do - find $EXAMPLES_DIR -type d -name $dirname -exec rm -r {} \; 2>/dev/null + find $EXAMPLES_DIR -type d -name $dirname -exec rm -r {} \; 2>/dev/null done FILES_TO_REMOVE=("tests" "moped_input.xml" "outer.xml" "inner.xml" "cash.xml" "*.lib" "write_inner.py" "*.heron" "*.heron.xml") for filename in ${FILES_TO_REMOVE[@]}; do - find $EXAMPLES_DIR -name $filename -exec rm {} \; 2>/dev/null + find $EXAMPLES_DIR -name $filename -exec rm {} \; 2>/dev/null done # If building on Mac, replace the %HERON_DATA% magic string with a relative path to the data diff --git a/package/heron.py b/package/heron.py index 300152d5..e8481e62 100755 --- a/package/heron.py +++ b/package/heron.py @@ -20,27 +20,27 @@ if __name__ == '__main__': - # Adds the "local/bin" directory to the system path in order to find ipopt and other executables - add_local_bin_to_path() + # Adds the "local/bin" directory to the system path in order to find ipopt and other executables + add_local_bin_to_path() - # Parse the command line arguments - import argparse - sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) - parser = argparse.ArgumentParser(description='HERON') - parser.add_argument('-w', action='store_true', default=False, required=False, help='Run in the GUI') - parser.add_argument('--definition', action="store_true", dest="definition", help='HERON input file definition compatible with the NEAMS Workbench') - parser.add_argument('input', nargs='?', help='HERON input file') - args, unknown = parser.parse_known_args() + # Parse the command line arguments + import argparse + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + parser = argparse.ArgumentParser(description='HERON') + parser.add_argument('-w', action='store_true', default=False, required=False, help='Run in the GUI') + parser.add_argument('--definition', action="store_true", dest="definition", help='HERON input file definition compatible with the NEAMS Workbench') + parser.add_argument('input', nargs='?', help='HERON input file') + args, unknown = parser.parse_known_args() - # if the input file is not an xml file, assume it's an unknown argument - if args.input and not args.input.endswith('.xml'): - unknown.insert(0, args.input) - args.input = None - # remove the -w argument from sys.argv so it doesn't interfere with HERON's argument parsing - if args.w: - sys.argv.remove('-w') + # if the input file is not an xml file, assume it's an unknown argument + if args.input and not args.input.endswith('.xml'): + unknown.insert(0, args.input) + args.input = None + # remove the -w argument from sys.argv so it doesn't interfere with HERON's argument parsing + if args.w: + sys.argv.remove('-w') - if (args.w or not args.input) and not args.definition: # if asked to or if no file is passed, run the GUI - run_from_gui(main) - else: - main() + if (args.w or not args.input) and not args.definition: # if asked to or if no file is passed, run the GUI + run_from_gui(main) + else: + main() diff --git a/package/make_docs.sh b/package/make_docs.sh index dc43c52c..ae482412 100755 --- a/package/make_docs.sh +++ b/package/make_docs.sh @@ -17,34 +17,34 @@ NO_BUILD=0 while [[ $# -gt 0 ]]; do key="$1" case $key in - --raven-dir) - RAVEN_DIR="$2" - shift - shift - ;; - --heron-dir) - HERON_DIR="$2" - shift - shift - ;; - --teal-dir) - TEAL_DIR="$2" - shift - shift - ;; - --no-build) - NO_BUILD=1 - shift - ;; - --dest) - DOC_DIR="$2" - shift - shift - ;; - *) - echo "Unknown option: $1" - exit 1 - ;; + --raven-dir) + RAVEN_DIR="$2" + shift + shift + ;; + --heron-dir) + HERON_DIR="$2" + shift + shift + ;; + --teal-dir) + TEAL_DIR="$2" + shift + shift + ;; + --no-build) + NO_BUILD=1 + shift + ;; + --dest) + DOC_DIR="$2" + shift + shift + ;; + *) + echo "Unknown option: $1" + exit 1 + ;; esac done @@ -80,27 +80,27 @@ for loc in RAVEN_DIR HERON_DIR TEAL_DIR; do # If the build flag is set, build the documentation. if [ $NO_BUILD -eq 0 ]; then - echo "Building documentation for $(basename ${!loc})" - if [[ -f "Makefile" ]] && command -v "make" >/dev/null 2>&1; then - make - elif [[ -f "make_docs.bat" ]] && [[ $OSTYPE == "msys" ]]; then - ./make_docs.bat - elif [[ -f "make_docs.sh" ]]; then - bash make_docs.sh - else - echo "ERROR: No Makefile or make_docs.sh script found in $(basename ${!loc}) doc directory." - exit 1 - fi + echo "Building documentation for $(basename ${!loc})" + if [[ -f "Makefile" ]] && command -v "make" >/dev/null 2>&1; then + make + elif [[ -f "make_docs.bat" ]] && [[ $OSTYPE == "msys" ]]; then + ./make_docs.bat + elif [[ -f "make_docs.sh" ]]; then + bash make_docs.sh + else + echo "ERROR: No Makefile or make_docs.sh script found in $(basename ${!loc}) doc directory." + exit 1 + fi fi # The PDFs that are generated are located in either a "pdfs" or "pdf" directory if [ -d pdfs ]; then - cp pdfs/*.pdf $DOC_DIR + cp pdfs/*.pdf $DOC_DIR elif [ -d pdf ]; then - cp pdf/*.pdf $DOC_DIR + cp pdf/*.pdf $DOC_DIR else - echo "ERROR: No PDFs found in $(basename ${!loc}) doc directory." - exit 1 + echo "ERROR: No PDFs found in $(basename ${!loc}) doc directory." + exit 1 fi popd > /dev/null diff --git a/package/raven_framework.py b/package/raven_framework.py index ba44fbd0..825e273d 100755 --- a/package/raven_framework.py +++ b/package/raven_framework.py @@ -27,36 +27,36 @@ if __name__ == '__main__': - # For Windows, this is required to avoid an infinite loop when running a multiprocessing script from a frozen executable. - # cx_Freeze provides a hook for this that is supposed to be called automatically to fix this issue on all platforms, - # but for now, it doesn't seem to resolve the issue on macOS. - multiprocessing.freeze_support() - - # Adds the "local/bin" directory to the system path in order to find ipopt and other executables - add_local_bin_to_path() - - # Parse the command line arguments - import argparse - parser = argparse.ArgumentParser(description='RAVEN') - parser.add_argument('-w', action='store_true', default=False, required=False,help='Run in the GUI') - parser.add_argument('input', nargs='*', help='RAVEN input file') - args, unknown = parser.parse_known_args() - - # More than one argument may be parsed for "input". Move any arguments that aren't an XML file to - # the unknown arguments list. - args_to_move = [] - for arg in args.input: - if not arg.endswith('.xml'): - args_to_move.append(arg) - for arg in args_to_move: - args.input.remove(arg) - unknown.insert(0, arg) - - # sys.argv is used by the main function, so we need to remove the -w argument - if args.w: - sys.argv.remove('-w') - - if args.w or not args.input: # run the GUI if asked to (-w) or if no input file is given - run_from_gui(main, checkLibraries=True) - else: - sys.exit(main(True)) + # For Windows, this is required to avoid an infinite loop when running a multiprocessing script from a frozen executable. + # cx_Freeze provides a hook for this that is supposed to be called automatically to fix this issue on all platforms, + # but for now, it doesn't seem to resolve the issue on macOS. + multiprocessing.freeze_support() + + # Adds the "local/bin" directory to the system path in order to find ipopt and other executables + add_local_bin_to_path() + + # Parse the command line arguments + import argparse + parser = argparse.ArgumentParser(description='RAVEN') + parser.add_argument('-w', action='store_true', default=False, required=False,help='Run in the GUI') + parser.add_argument('input', nargs='*', help='RAVEN input file') + args, unknown = parser.parse_known_args() + + # More than one argument may be parsed for "input". Move any arguments that aren't an XML file to + # the unknown arguments list. + args_to_move = [] + for arg in args.input: + if not arg.endswith('.xml'): + args_to_move.append(arg) + for arg in args_to_move: + args.input.remove(arg) + unknown.insert(0, arg) + + # sys.argv is used by the main function, so we need to remove the -w argument + if args.w: + sys.argv.remove('-w') + + if args.w or not args.input: # run the GUI if asked to (-w) or if no input file is given + run_from_gui(main, checkLibraries=True) + else: + sys.exit(main(True)) diff --git a/package/setup.py b/package/setup.py index 1f0075bf..f39f554d 100644 --- a/package/setup.py +++ b/package/setup.py @@ -8,41 +8,41 @@ build_exe_options = { - "packages": ["ravenframework","msgpack","ray","crow_modules","AMSC","sklearn","pyomo","HERON","TEAL","pyarrow","netCDF4","cftime","distributed","dask","tensorflow"], - "includes": ["ray.thirdparty_files.colorama","ray.autoscaler._private","pyomo.common.plugins","HERON.templates.template_driver","dask.distributed","imageio.plugins.pillow","imageio.plugins.pillowmulti","imageio.plugins.pillow_info"], - "include_files": [(HERON.templates.write_inner.__file__,"lib/HERON/templates/write_inner.py")], + "packages": ["ravenframework","msgpack","ray","crow_modules","AMSC","sklearn","pyomo","HERON","TEAL","pyarrow","netCDF4","cftime","distributed","dask","tensorflow"], + "includes": ["ray.thirdparty_files.colorama","ray.autoscaler._private","pyomo.common.plugins","HERON.templates.template_driver","dask.distributed","imageio.plugins.pillow","imageio.plugins.pillowmulti","imageio.plugins.pillow_info"], + "include_files": [(HERON.templates.write_inner.__file__,"lib/HERON/templates/write_inner.py")], } # Add all of the HERON template XML files to the build write_inner_path = pathlib.Path(HERON.templates.write_inner.__file__) for xml_file in os.listdir(write_inner_path.parent): - if xml_file.endswith(".xml"): - build_exe_options["include_files"].append((write_inner_path.parent / xml_file, f"lib/HERON/templates/{xml_file}")) + if xml_file.endswith(".xml"): + build_exe_options["include_files"].append((write_inner_path.parent / xml_file, f"lib/HERON/templates/{xml_file}")) # Some files must be included manually for the Windows build if platform.system().lower() == "windows": - # netCDF4 .dll files get missed by cx_Freeze - # ipopt executable must be included manually - netCDF4_libs_path = os.path.join(os.path.dirname(sys.executable), "lib", "site-packages", "netCDF4.libs") - build_exe_options["include_files"] += [ - #("Ipopt-3.14.13-win64-msvs2019-md/","local/bin/Ipopt-3.14.13-win64-msvs2019-md"), # FIXME: Point to the correct location for ipopt executable - (netCDF4_libs_path,"lib/netCDF4") - ] - # Include the Microsoft Visual C++ Runtime - build_exe_options["include_msvcr"] = True + # netCDF4 .dll files get missed by cx_Freeze + # ipopt executable must be included manually + netCDF4_libs_path = os.path.join(os.path.dirname(sys.executable), "lib", "site-packages", "netCDF4.libs") + build_exe_options["include_files"] += [ + #("Ipopt-3.14.13-win64-msvs2019-md/","local/bin/Ipopt-3.14.13-win64-msvs2019-md"), # FIXME: Point to the correct location for ipopt executable + (netCDF4_libs_path,"lib/netCDF4") + ] + # Include the Microsoft Visual C++ Runtime + build_exe_options["include_msvcr"] = True else: - ipopt_path = os.path.join(os.path.dirname(sys.executable), "ipopt") - build_exe_options["include_files"] += [ - # (ipopt_path, "local/bin/ipopt") - (ipopt_path, "ipopt") # put it in main directory - ] + ipopt_path = os.path.join(os.path.dirname(sys.executable), "ipopt") + build_exe_options["include_files"] += [ + # (ipopt_path, "local/bin/ipopt") + (ipopt_path, "ipopt") # put it in main directory + ] setup( - name="force", - version="0.1", - description="FORCE package", - executables=[Executable(script="raven_framework.py",icon="icons/raven_64.ico"), - Executable(script="heron.py",icon="icons/heron_64.ico"), - Executable(script="teal.py",icon="icons/teal_64.ico")], - options={"build_exe": build_exe_options}, + name="force", + version="0.1", + description="FORCE package", + executables=[Executable(script="raven_framework.py",icon="icons/raven_64.ico"), + Executable(script="heron.py",icon="icons/heron_64.ico"), + Executable(script="teal.py",icon="icons/teal_64.ico")], + options={"build_exe": build_exe_options}, ) diff --git a/package/teal.py b/package/teal.py index 4015a833..402b89c5 100644 --- a/package/teal.py +++ b/package/teal.py @@ -25,22 +25,22 @@ if __name__ == '__main__': - import argparse - parser = argparse.ArgumentParser(description='RAVEN') - parser.add_argument('-w', action='store_true', default=False, required=False,help='Run in the GUI') - parser.add_argument('-iXML', nargs=1, required=False, help='XML CashFlow input file name', metavar='inp_file') - parser.add_argument('-iINP', nargs=1, required=False, help='CashFlow input file name with the input variable list', metavar='inp_file') - parser.add_argument('-o', nargs=1, required=False, help='Output file name', metavar='out_file') - args = parser.parse_args() + import argparse + parser = argparse.ArgumentParser(description='RAVEN') + parser.add_argument('-w', action='store_true', default=False, required=False,help='Run in the GUI') + parser.add_argument('-iXML', nargs=1, required=False, help='XML CashFlow input file name', metavar='inp_file') + parser.add_argument('-iINP', nargs=1, required=False, help='CashFlow input file name with the input variable list', metavar='inp_file') + parser.add_argument('-o', nargs=1, required=False, help='Output file name', metavar='out_file') + args = parser.parse_args() - # Remove the -w argument from sys.argv so it doesn't interfere with TEAL's argument parsing - if args.w: - sys.argv.remove('-w') + # Remove the -w argument from sys.argv so it doesn't interfere with TEAL's argument parsing + if args.w: + sys.argv.remove('-w') - # If the -w argument is present or any of the other arguments are missing, run the GUI - if args.w or not args.iXML or not args.iINP or not args.o: - print('Running TEAL in GUI mode') - run_from_gui(TEALmain) - else: - print('Running TEAL in command line mode') - sys.exit(TEALmain()) + # If the -w argument is present or any of the other arguments are missing, run the GUI + if args.w or not args.iXML or not args.iINP or not args.o: + print('Running TEAL in GUI mode') + run_from_gui(TEALmain) + else: + print('Running TEAL in command line mode') + sys.exit(TEALmain()) diff --git a/package/ui/controllers/file_dialog.py b/package/ui/controllers/file_dialog.py index a1c178cc..b52d414b 100644 --- a/package/ui/controllers/file_dialog.py +++ b/package/ui/controllers/file_dialog.py @@ -5,70 +5,70 @@ class FileDialogController: - def __init__(self, view, file_type=None, is_output=False, persistence=None): - """ - Constructor - @In, view, tk.Frame, the view - @In, file_type, str, optional, the file type - @In, is_output, bool, optional, whether the file is an output file - @In, persistence, FileLocationPersistence, optional, the file location persistence - @Out, None - """ - self.view = view + def __init__(self, view, file_type=None, is_output=False, persistence=None): + """ + Constructor + @In, view, tk.Frame, the view + @In, file_type, str, optional, the file type + @In, is_output, bool, optional, whether the file is an output file + @In, persistence, FileLocationPersistence, optional, the file location persistence + @Out, None + """ + self.view = view - self.filename = None - self.file_type = file_type - self.persistence = persistence + self.filename = None + self.file_type = file_type + self.persistence = persistence - if is_output: - if not self.file_type: - self.file_type = 'out' - self.view.browse_button.config(command=self.open_save_dialog) - else: - self.view.browse_button.config(command=self.open_selection_dialog) + if is_output: + if not self.file_type: + self.file_type = 'out' + self.view.browse_button.config(command=self.open_save_dialog) + else: + self.view.browse_button.config(command=self.open_selection_dialog) - def get_filename(self): - """ - filename getter - @In, None - @Out, filename, str, the filename - """ - if not self.filename: # None or empty string - return None - return self.filename + def get_filename(self): + """ + filename getter + @In, None + @Out, filename, str, the filename + """ + if not self.filename: # None or empty string + return None + return self.filename - def set_filename(self, value): - """ - filename setter - @In, value, str, the filename - @Out, None - """ - if not os.path.exists(value): - raise FileNotFoundError(f'File {value} does not exist') - self.filename = os.path.abspath(value) - self.view.filename.set(os.path.basename(value)) - if self.persistence: - self.persistence.set_location(value) + def set_filename(self, value): + """ + filename setter + @In, value, str, the filename + @Out, None + """ + if not os.path.exists(value): + raise FileNotFoundError(f'File {value} does not exist') + self.filename = os.path.abspath(value) + self.view.filename.set(os.path.basename(value)) + if self.persistence: + self.persistence.set_location(value) - def open_selection_dialog(self): - """ - Open a file dialog to select an existing file - @In, None - @Out, None - """ - initial_dir = self.persistence.get_location() if self.persistence else os.getcwd() - filetypes = [(self.file_type.upper(), f'*.{self.file_type.strip().lower()}') if self.file_type else ('All Files', '*.*')] - filename = filedialog.askopenfilename(initialdir=initial_dir, filetypes=filetypes) - if filename: - self.set_filename(filename) + def open_selection_dialog(self): + """ + Open a file dialog to select an existing file + @In, None + @Out, None + """ + initial_dir = self.persistence.get_location() if self.persistence else os.getcwd() + filetypes = [(self.file_type.upper(), f'*.{self.file_type.strip().lower()}') if self.file_type else ('All Files', '*.*')] + filename = filedialog.askopenfilename(initialdir=initial_dir, filetypes=filetypes) + if filename: + self.set_filename(filename) - def open_save_dialog(self): - """ - Open a file dialog to save a new file - @In, None - @Out, None - """ - initial_dir = self.persistence.get_location() if self.persistence else os.getcwd() - filename = filedialog.asksaveasfilename(initialdir=initial_dir, defaultextension=f'.{self.file_type}') - if filename: - self.set_filename(filename) + def open_save_dialog(self): + """ + Open a file dialog to save a new file + @In, None + @Out, None + """ + initial_dir = self.persistence.get_location() if self.persistence else os.getcwd() + filename = filedialog.asksaveasfilename(initialdir=initial_dir, defaultextension=f'.{self.file_type}') + if filename: + self.set_filename(filename) diff --git a/package/ui/controllers/file_location_persistence.py b/package/ui/controllers/file_location_persistence.py index c16d1565..8c4840cf 100644 --- a/package/ui/controllers/file_location_persistence.py +++ b/package/ui/controllers/file_location_persistence.py @@ -2,72 +2,72 @@ class RCFile(dict): - """ Class for handling reading and writing to a dotfile for remembering config data between runs. """ - def __init__(self, path: str): - """ - Constructor - @In, path, str, the path to the dotfile - @Out, None - """ - super().__init__() - self.path = path - if os.path.exists(self.path): - self.read() + """ Class for handling reading and writing to a dotfile for remembering config data between runs. """ + def __init__(self, path: str): + """ + Constructor + @In, path, str, the path to the dotfile + @Out, None + """ + super().__init__() + self.path = path + if os.path.exists(self.path): + self.read() - def close(self): - """ - Save and close the dotfile - @In, None - @Out, None - """ - # Write the entries to the file when the object is deleted - with open(self.path, 'w') as f: - for key, value in self.items(): - f.write(f"{key}={value}\n") + def close(self): + """ + Save and close the dotfile + @In, None + @Out, None + """ + # Write the entries to the file when the object is deleted + with open(self.path, 'w') as f: + for key, value in self.items(): + f.write(f"{key}={value}\n") - def read(self): - """ - Reads the dotfile - @In, None - @Out, None - """ - with open(self.path, 'r') as f: - for line in f.readlines(): - key, value = line.strip().split('=') - self |= {key: value} + def read(self): + """ + Reads the dotfile + @In, None + @Out, None + """ + with open(self.path, 'r') as f: + for line in f.readlines(): + key, value = line.strip().split('=') + self |= {key: value} class FileLocationPersistence: - """ A class for remembering where the case file that was last selected is located. """ - def __init__(self): - """ - Constructor - @In, None - @Out, None - """ - # A file with the location of the last selected case file - self.rcfile = RCFile(os.path.join(os.path.dirname(__file__), '..', '.forceuirc')) + """ A class for remembering where the case file that was last selected is located. """ + def __init__(self): + """ + Constructor + @In, None + @Out, None + """ + # A file with the location of the last selected case file + self.rcfile = RCFile(os.path.join(os.path.dirname(__file__), '..', '.forceuirc')) - def get_location(self): - """ - Getter for the last file location - @In, None - @Out, last_location, str, the last file location - """ - return self.rcfile.get('DEFAULT_DIR', os.path.expanduser('~')) + def get_location(self): + """ + Getter for the last file location + @In, None + @Out, last_location, str, the last file location + """ + return self.rcfile.get('DEFAULT_DIR', os.path.expanduser('~')) - def set_location(self, value): - """ - Setter for the last file location - @In, value, str, the last file location (file path or directory) - @Out, None - """ - self.rcfile['DEFAULT_DIR'] = os.path.abspath(os.path.dirname(value)) + def set_location(self, value): + """ + Setter for the last file location + @In, value, str, the last file location (file path or directory) + @Out, None + """ + self.rcfile['DEFAULT_DIR'] = os.path.abspath(os.path.dirname(value)) - def close(self): - """ - Closes the file location persistence - @In, None - @Out, None - """ - self.rcfile.close() + def close(self): + """ + Closes the file location persistence + @In, None + @Out, None + """ + self.rcfile.close() diff --git a/package/ui/controllers/file_selection.py b/package/ui/controllers/file_selection.py index f44d12d1..e088b19d 100644 --- a/package/ui/controllers/file_selection.py +++ b/package/ui/controllers/file_selection.py @@ -7,148 +7,148 @@ from ui.utils import run_in_workbench class FileSpec: - """ Input/output file specification for a package. """ - def __init__(self, - arg_name: str, - description: str, - is_output: bool = False, - file_type: Optional[str] = None): - """ - Constructor - @In, arg_name, str, optional, the argument flag for the file - @In, description, str, the description of the file - @In, is_output, bool, optional, whether the file is an output file - @In, file_type, str, optional, the type of file - @Out, None - """ - self.arg_name = arg_name - self.description = description - self.is_output = is_output - self.file_type = file_type + """ Input/output file specification for a package. """ + def __init__(self, + arg_name: str, + description: str, + is_output: bool = False, + file_type: Optional[str] = None): + """ + Constructor + @In, arg_name, str, optional, the argument flag for the file + @In, description, str, the description of the file + @In, is_output, bool, optional, whether the file is an output file + @In, file_type, str, optional, the type of file + @Out, None + """ + self.arg_name = arg_name + self.description = description + self.is_output = is_output + self.file_type = file_type - def add_to_parser(self, parser: argparse.ArgumentParser) -> argparse.ArgumentParser: - """ - Adds the file specification to the parser. - @In, parser, argparse.ArgumentParser, the parser - @Out, parser, argparse.ArgumentParser, the parser with the file specification added - """ - # if self.arg_name.startswith('-'): - # parser.add_argument(self.arg_name, nargs=1, required=False, help=self.description) - # else: # positional argument - # parser.add_argument(self.arg_name, nargs='?', help=self.description) - parser.add_argument(self.arg_name, nargs='?', help=self.description) - return parser + def add_to_parser(self, parser: argparse.ArgumentParser) -> argparse.ArgumentParser: + """ + Adds the file specification to the parser. + @In, parser, argparse.ArgumentParser, the parser + @Out, parser, argparse.ArgumentParser, the parser with the file specification added + """ + # if self.arg_name.startswith('-'): + # parser.add_argument(self.arg_name, nargs=1, required=False, help=self.description) + # else: # positional argument + # parser.add_argument(self.arg_name, nargs='?', help=self.description) + parser.add_argument(self.arg_name, nargs='?', help=self.description) + return parser class FileSelectionController: - """ Controller for the file selection widget. """ - # Specification for the files that are required for each package. These are used to tie any files - # passed from the command line to the file selection widget and helps define what file extensions - # are allowed for each file. - _file_selection_specs = { - 'teal': [FileSpec('-iXML', 'TEAL XML File', file_type='xml'), - FileSpec('-iINP','Variable Inputs File', file_type='txt'), - FileSpec('-o', 'Output File', is_output=True)], - 'ravenframework': [FileSpec('filename', 'RAVEN XML File', file_type='xml')], - 'heron': [FileSpec('filename', 'HERON Input File', file_type='xml')] - } + """ Controller for the file selection widget. """ + # Specification for the files that are required for each package. These are used to tie any files + # passed from the command line to the file selection widget and helps define what file extensions + # are allowed for each file. + _file_selection_specs = { + 'teal': [FileSpec('-iXML', 'TEAL XML File', file_type='xml'), + FileSpec('-iINP','Variable Inputs File', file_type='txt'), + FileSpec('-o', 'Output File', is_output=True)], + 'ravenframework': [FileSpec('filename', 'RAVEN XML File', file_type='xml')], + 'heron': [FileSpec('filename', 'HERON Input File', file_type='xml')] + } - def __init__(self, model, view): - """ - Constructor - @In, model, Model, the model - @In, view, FileSelection, the view - @Out, None - """ - self.file_selection = view - self.file_dialog_controllers = {} + def __init__(self, model, view): + """ + Constructor + @In, model, Model, the model + @In, view, FileSelection, the view + @Out, None + """ + self.file_selection = view + self.file_dialog_controllers = {} - # Remember the file locations for the user - self.persistence = FileLocationPersistence() + # Remember the file locations for the user + self.persistence = FileLocationPersistence() - # Create the file selectors, adding any files specified from the command line - model_package_name = model.get_package_name().strip().lower() - self._file_specs = self._file_selection_specs[model_package_name] - args, self.unknown_args = self._parse_cli_args() - for spec in self._file_specs: - # Create a new file selector view based on the file spec - self.file_selection.new_file_selector(label=spec.description) - # Create a new file dialog controller for the file selector - file_dialog_controller = FileDialogController( - view=self.file_selection.file_selectors[spec.description], - file_type=spec.file_type, - is_output=spec.is_output, - persistence=self.persistence - ) - if filename := args.get(spec.arg_name, None): - file_dialog_controller.set_filename(filename) - self.persistence.set_location(filename) - self.file_dialog_controllers[spec.description] = file_dialog_controller + # Create the file selectors, adding any files specified from the command line + model_package_name = model.get_package_name().strip().lower() + self._file_specs = self._file_selection_specs[model_package_name] + args, self.unknown_args = self._parse_cli_args() + for spec in self._file_specs: + # Create a new file selector view based on the file spec + self.file_selection.new_file_selector(label=spec.description) + # Create a new file dialog controller for the file selector + file_dialog_controller = FileDialogController( + view=self.file_selection.file_selectors[spec.description], + file_type=spec.file_type, + is_output=spec.is_output, + persistence=self.persistence + ) + if filename := args.get(spec.arg_name, None): + file_dialog_controller.set_filename(filename) + self.persistence.set_location(filename) + self.file_dialog_controllers[spec.description] = file_dialog_controller - # Set the action for the "Open in Workbench" button - if model_package_name == "heron": - workbench_func = lambda: run_in_workbench(self.file_dialog_controllers['HERON Input File'].get_filename()) - self.file_selection.add_open_in_workbench_button(workbench_func) + # Set the action for the "Open in Workbench" button + if model_package_name == "heron": + workbench_func = lambda: run_in_workbench(self.file_dialog_controllers['HERON Input File'].get_filename()) + self.file_selection.add_open_in_workbench_button(workbench_func) - def get_sys_args_from_file_selection(self): - """ - Gets the files selected by the user and returns them as a list along with their - corresponding argument flags, if any. - @In, None - @Out, args, list, a list of files and their corresponding argument flags, if any - """ - args = [] - for spec in self._file_specs: - # Get the filename from the file selector - filename = self.file_dialog_controllers[spec.description].get_filename() - # Add the filename with its corresponding argument flag to the list - if not os.path.exists(filename) and spec.arg_name != '-o': - raise FileNotFoundError(f"File {filename} not found") - if spec.arg_name.startswith('-'): # flag argument, include the flag and the argument - args.extend([spec.arg_name, filename]) - else: # positional argument, include the argument only - args.append(filename) - # Add any unknown arguments to pass along to the model - args.extend(self.unknown_args) - return args + def get_sys_args_from_file_selection(self): + """ + Gets the files selected by the user and returns them as a list along with their + corresponding argument flags, if any. + @In, None + @Out, args, list, a list of files and their corresponding argument flags, if any + """ + args = [] + for spec in self._file_specs: + # Get the filename from the file selector + filename = self.file_dialog_controllers[spec.description].get_filename() + # Add the filename with its corresponding argument flag to the list + if not os.path.exists(filename) and spec.arg_name != '-o': + raise FileNotFoundError(f"File {filename} not found") + if spec.arg_name.startswith('-'): # flag argument, include the flag and the argument + args.extend([spec.arg_name, filename]) + else: # positional argument, include the argument only + args.append(filename) + # Add any unknown arguments to pass along to the model + args.extend(self.unknown_args) + return args - def close_persistence(self): - """ - Closes the file location persistence - @In, None - @Out, None - """ - self.persistence.close() + def close_persistence(self): + """ + Closes the file location persistence + @In, None + @Out, None + """ + self.persistence.close() - def _parse_cli_args(self): - """ - Parse arguments provided from the command line - @In, None - @Out, args, dict, the parsed arguments - @Out, unknown, list, the unknown arguments - """ - parser = argparse.ArgumentParser() - for spec in self._file_specs: - parser = spec.add_to_parser(parser) - # Handling unknown arguments lets use pass additional arguments to the model while only directly - # handling the file selection arguments. - args, unknown = parser.parse_known_args() - args = vars(args) - # Positional arguments requiring a specific file type may be missing, and an unknown argument - # may have been interpreted as being that file argument. We'll check if the argument has the - # correct file extension and if it does, we'll assume it's the file argument. Otherwise, we'll - # remove it and add it to the list of unknown arguments. Finally, we'll check the unknown arguments - # and make sure the file argument didn't end up in there. - for spec in self._file_specs: - if spec.arg_name not in args: - for arg in unknown: - if arg.endswith(f'.{spec.file_type}'): - args[spec.arg_name] = arg - unknown.remove(arg) - break - # Any arguments with flags will have had the '-' stripped off. It'll be helpful to know which - # arguments were specified with flags, so we'll add the '-' back to the keys. - for key in args.keys(): - if f'-{key}' in [spec.arg_name for spec in self._file_specs]: - args[f'-{key}'] = args.pop(key) - return args, unknown + def _parse_cli_args(self): + """ + Parse arguments provided from the command line + @In, None + @Out, args, dict, the parsed arguments + @Out, unknown, list, the unknown arguments + """ + parser = argparse.ArgumentParser() + for spec in self._file_specs: + parser = spec.add_to_parser(parser) + # Handling unknown arguments lets use pass additional arguments to the model while only directly + # handling the file selection arguments. + args, unknown = parser.parse_known_args() + args = vars(args) + # Positional arguments requiring a specific file type may be missing, and an unknown argument + # may have been interpreted as being that file argument. We'll check if the argument has the + # correct file extension and if it does, we'll assume it's the file argument. Otherwise, we'll + # remove it and add it to the list of unknown arguments. Finally, we'll check the unknown arguments + # and make sure the file argument didn't end up in there. + for spec in self._file_specs: + if spec.arg_name not in args: + for arg in unknown: + if arg.endswith(f'.{spec.file_type}'): + args[spec.arg_name] = arg + unknown.remove(arg) + break + # Any arguments with flags will have had the '-' stripped off. It'll be helpful to know which + # arguments were specified with flags, so we'll add the '-' back to the keys. + for key in args.keys(): + if f'-{key}' in [spec.arg_name for spec in self._file_specs]: + args[f'-{key}'] = args.pop(key) + return args, unknown diff --git a/package/ui/controllers/main.py b/package/ui/controllers/main.py index d58fcc7a..68853d4b 100644 --- a/package/ui/controllers/main.py +++ b/package/ui/controllers/main.py @@ -6,34 +6,34 @@ class Controller: - def __init__(self, model, view): - self.model = model - self.view = view - - # Initialize controllers - self.file_selection_controller = FileSelectionController(self.model, self.view.frames["file_selection"]) - self.text_output_controller = TextOutputController(self.model, self.view.frames["text_output"]) - self.model_status_controller = ModelStatusController(self.model, self.view.frames["text_output"].model_status) - - # Bind the run button to the model - self.view.frames["run_abort"].run_button.config(command=self.run_model) - # Bind the abort button to closing the window - self.view.frames["run_abort"].abort_button.config(command=self.quit) - - def run_model(self): - # Construct sys.argv from the file selectors - sys.argv = [sys.argv[0]] + self.file_selection_controller.get_sys_args_from_file_selection() - # Start the model - self.model.start() - - def start(self): - self.view.mainloop() - - def quit(self, showdialog: bool = True): - """ - Quit the application - @In, showdialog, bool, optional, whether to show a dialog before quitting, default is True - @Out, None - """ - self.file_selection_controller.close_persistence() - self.view.quit(showdialog) + def __init__(self, model, view): + self.model = model + self.view = view + + # Initialize controllers + self.file_selection_controller = FileSelectionController(self.model, self.view.frames["file_selection"]) + self.text_output_controller = TextOutputController(self.model, self.view.frames["text_output"]) + self.model_status_controller = ModelStatusController(self.model, self.view.frames["text_output"].model_status) + + # Bind the run button to the model + self.view.frames["run_abort"].run_button.config(command=self.run_model) + # Bind the abort button to closing the window + self.view.frames["run_abort"].abort_button.config(command=self.quit) + + def run_model(self): + # Construct sys.argv from the file selectors + sys.argv = [sys.argv[0]] + self.file_selection_controller.get_sys_args_from_file_selection() + # Start the model + self.model.start() + + def start(self): + self.view.mainloop() + + def quit(self, showdialog: bool = True): + """ + Quit the application + @In, showdialog, bool, optional, whether to show a dialog before quitting, default is True + @Out, None + """ + self.file_selection_controller.close_persistence() + self.view.quit(showdialog) diff --git a/package/ui/controllers/model_status.py b/package/ui/controllers/model_status.py index e83dff41..e9567a2c 100644 --- a/package/ui/controllers/model_status.py +++ b/package/ui/controllers/model_status.py @@ -4,38 +4,38 @@ class ModelStatusController: - """ Tracks if the model is running and updates the view to reflect the status. """ - def __init__(self, model, view): - """ - Constructor - @In, model, Model, the model - @In, view, View, the view - """ - self.model = model - self.view = view - self._model_has_run = False # Flag to indicate the model has already been run - self.check_model_status() + """ Tracks if the model is running and updates the view to reflect the status. """ + def __init__(self, model, view): + """ + Constructor + @In, model, Model, the model + @In, view, View, the view + """ + self.model = model + self.view = view + self._model_has_run = False # Flag to indicate the model has already been run + self.check_model_status() - def check_model_status(self): - """ - Check the status of the model and update the view - @In, None - @Out, None - """ - def _check_status(): - """ - Local helper function for checking the status of the model in a separate thread - @In, None - @Out, None - """ - current_status = self.model.status - while True: - if current_status != self.model.status: - current_status = self.model.status - self.view.set_status(self.model.status) - time.sleep(0.5) + def check_model_status(self): + """ + Check the status of the model and update the view + @In, None + @Out, None + """ + def _check_status(): + """ + Local helper function for checking the status of the model in a separate thread + @In, None + @Out, None + """ + current_status = self.model.status + while True: + if current_status != self.model.status: + current_status = self.model.status + self.view.set_status(self.model.status) + time.sleep(0.5) - thread = threading.Thread(target=_check_status) - thread.daemon = True - thread.name = "ModelStatusChecker" - thread.start() + thread = threading.Thread(target=_check_status) + thread.daemon = True + thread.name = "ModelStatusChecker" + thread.start() diff --git a/package/ui/controllers/text_output.py b/package/ui/controllers/text_output.py index 3b936f2d..4cab648d 100644 --- a/package/ui/controllers/text_output.py +++ b/package/ui/controllers/text_output.py @@ -6,76 +6,76 @@ class TextOutputController: - def __init__(self, model, view): - """ - Constructor - @In, model, Model, the model to control - @In, view, TextOutput, the view to control - """ - self.view = view - self.redirector = StdoutRedirector(self.view.text) - self.redirector.start() - # Define show/hide button behavior - self.view.show_hide_button.config(command=self.toggle_show_text) + def __init__(self, model, view): + """ + Constructor + @In, model, Model, the model to control + @In, view, TextOutput, the view to control + """ + self.view = view + self.redirector = StdoutRedirector(self.view.text) + self.redirector.start() + # Define show/hide button behavior + self.view.show_hide_button.config(command=self.toggle_show_text) - def toggle_show_text(self): - """ - Toggle the visibility of the output text widget and resize the window to fit - @In, None - @Out, None - """ - if self.view.is_showing: # Hide output - # self.view.text.grid_forget() - self.view.hide_text_output() - self.view.show_hide_button.config(text='Show Output') - else: # Show output - # self.view.text.grid(row=1, column=0, sticky='nsew') - self.view.show_text_output() - self.view.show_hide_button.config(text='Hide Output') - # self.view.is_showing = not self.view.is_showing + def toggle_show_text(self): + """ + Toggle the visibility of the output text widget and resize the window to fit + @In, None + @Out, None + """ + if self.view.is_showing: # Hide output + # self.view.text.grid_forget() + self.view.hide_text_output() + self.view.show_hide_button.config(text='Show Output') + else: # Show output + # self.view.text.grid(row=1, column=0, sticky='nsew') + self.view.show_text_output() + self.view.show_hide_button.config(text='Hide Output') + # self.view.is_showing = not self.view.is_showing class StdoutRedirector: - """ Redirects stdout to a tkinter widget """ - def __init__(self, widget: tk.Widget): - """ - Constructor - @In, widget, tk.Widget, the widget to redirect stdout to - @Out, None - """ - self.widget = widget - self.redirect_output = io.StringIO() - sys.stdout = self.redirect_output - sys.stderr = self.redirect_output + """ Redirects stdout to a tkinter widget """ + def __init__(self, widget: tk.Widget): + """ + Constructor + @In, widget, tk.Widget, the widget to redirect stdout to + @Out, None + """ + self.widget = widget + self.redirect_output = io.StringIO() + sys.stdout = self.redirect_output + sys.stderr = self.redirect_output - def start(self): - """ - Start the redirector. Uses a daemon thread to monitor the output. - @In, None - @Out, None - """ - self.monitor_thread = threading.Thread(target=self.monitor_output) - self.monitor_thread.daemon = True - self.monitor_thread.start() + def start(self): + """ + Start the redirector. Uses a daemon thread to monitor the output. + @In, None + @Out, None + """ + self.monitor_thread = threading.Thread(target=self.monitor_output) + self.monitor_thread.daemon = True + self.monitor_thread.start() - def monitor_output(self): - """ - Monitors the output and updates the widget - @In, None - @Out, None - """ - while True: - # Get the buffer's contents, then clear it - text = self.redirect_output.getvalue() - self.redirect_output.seek(0) - self.redirect_output.truncate(0) + def monitor_output(self): + """ + Monitors the output and updates the widget + @In, None + @Out, None + """ + while True: + # Get the buffer's contents, then clear it + text = self.redirect_output.getvalue() + self.redirect_output.seek(0) + self.redirect_output.truncate(0) - # Update the widget's text - if text: - self.widget.config(state=tk.NORMAL) - self.widget.insert(tk.END, text) - self.widget.config(state=tk.DISABLED) - self.widget.see(tk.END) + # Update the widget's text + if text: + self.widget.config(state=tk.NORMAL) + self.widget.insert(tk.END, text) + self.widget.config(state=tk.DISABLED) + self.widget.see(tk.END) - # Sleep to prevent busy-waiting - time.sleep(0.1) # FIXME: expose this as a parameter + # Sleep to prevent busy-waiting + time.sleep(0.1) # FIXME: expose this as a parameter diff --git a/package/ui/main.py b/package/ui/main.py index 35d1d572..4fbdde4f 100644 --- a/package/ui/main.py +++ b/package/ui/main.py @@ -5,16 +5,16 @@ def run_from_gui(func: Callable, **kwargs): - """ - Runs the given function from the GUI. - @In, func, Callable, the function to run - @In, args, argparse.Namespace, optional, the parsed command-line arguments - @In, kwargs, dict, optional, the keyword arguments for the model - @Out, None - """ - model = Model(func, **kwargs) - view = View() - controller = Controller(model, view) - controller.start() - # Let the controller know to clean up when the view is closed - controller.quit(showdialog=False) + """ + Runs the given function from the GUI. + @In, func, Callable, the function to run + @In, args, argparse.Namespace, optional, the parsed command-line arguments + @In, kwargs, dict, optional, the keyword arguments for the model + @Out, None + """ + model = Model(func, **kwargs) + view = View() + controller = Controller(model, view) + controller.start() + # Let the controller know to clean up when the view is closed + controller.quit(showdialog=False) diff --git a/package/ui/models/main.py b/package/ui/models/main.py index 5b365309..e52a359b 100644 --- a/package/ui/models/main.py +++ b/package/ui/models/main.py @@ -5,56 +5,56 @@ class ModelStatus(Enum): - """ Enum for model status """ - NOT_STARTED = "Not yet run" - RUNNING = "Running" - FINISHED = "Finished" - ERROR = "Error" + """ Enum for model status """ + NOT_STARTED = "Not yet run" + RUNNING = "Running" + FINISHED = "Finished" + ERROR = "Error" class Model: - """ Runs a function in a separate thread """ - def __init__(self, func: Callable, **kwargs): - """ - Constructor - @In, func, Callable, the function to run - @In, kwargs, dict, keyword arguments to pass to the function - @Out, None - """ - self.func = func - self.thread = None - self.kwargs = kwargs - self.status = ModelStatus.NOT_STARTED + """ Runs a function in a separate thread """ + def __init__(self, func: Callable, **kwargs): + """ + Constructor + @In, func, Callable, the function to run + @In, kwargs, dict, keyword arguments to pass to the function + @Out, None + """ + self.func = func + self.thread = None + self.kwargs = kwargs + self.status = ModelStatus.NOT_STARTED - def start(self): - """ - Start the thread - @In, None - @Out, None - """ - def func_wrapper(): - """ - Wrapper for the function to run and set the status to FINISHED when done - @In, None - @Out, None - """ - self.status = ModelStatus.RUNNING - try: - self.func(**self.kwargs) - except: - self.status = ModelStatus.ERROR - else: - self.status = ModelStatus.FINISHED + def start(self): + """ + Start the thread + @In, None + @Out, None + """ + def func_wrapper(): + """ + Wrapper for the function to run and set the status to FINISHED when done + @In, None + @Out, None + """ + self.status = ModelStatus.RUNNING + try: + self.func(**self.kwargs) + except: + self.status = ModelStatus.ERROR + else: + self.status = ModelStatus.FINISHED - self.thread = threading.Thread(target=func_wrapper) - self.thread.daemon = True - self.thread.name = self.get_package_name() - self.thread.start() + self.thread = threading.Thread(target=func_wrapper) + self.thread.daemon = True + self.thread.name = self.get_package_name() + self.thread.start() - def get_package_name(self): - """ - Get the top-level package name of the model - @In, None - @Out, package_name, str, the package name - """ - return self.func.__module__.split('.')[0] + def get_package_name(self): + """ + Get the top-level package name of the model + @In, None + @Out, package_name, str, the package name + """ + return self.func.__module__.split('.')[0] diff --git a/package/ui/utils.py b/package/ui/utils.py index 67830bcb..af2e0aa6 100644 --- a/package/ui/utils.py +++ b/package/ui/utils.py @@ -9,338 +9,338 @@ def is_frozen_app() -> bool: - """ - Infers whether the application being run is a frozen executable or not. This is done by checking the - location and file extension of the HERON executable. - - @return, bool, True if the application is a frozen executable, False otherwise - """ - current_file_dir = pathlib.Path(__file__).parent - # If there's a "heron.py" file one directory up from this file, then we're likely running from the - # source code. Frozen executables will have "heron" or "heron.exe" as the executable name. - # FIXME: This is likely to be a bit fragile! Is there a better way to determine if we're running from - # a frozen executable? - if (current_file_dir.parent / "heron.py").exists(): - return False - else: - return True + """ + Infers whether the application being run is a frozen executable or not. This is done by checking the + location and file extension of the HERON executable. + + @return, bool, True if the application is a frozen executable, False otherwise + """ + current_file_dir = pathlib.Path(__file__).parent + # If there's a "heron.py" file one directory up from this file, then we're likely running from the + # source code. Frozen executables will have "heron" or "heron.exe" as the executable name. + # FIXME: This is likely to be a bit fragile! Is there a better way to determine if we're running from + # a frozen executable? + if (current_file_dir.parent / "heron.py").exists(): + return False + else: + return True def get_workbench_exe_path(workbench_dir: pathlib.Path) -> pathlib.Path: - """ - Returns the path to the Workbench executable, dependent on the operating system. + """ + Returns the path to the Workbench executable, dependent on the operating system. - @ In, workbench_dir, pathlib.Path, the path to the Workbench installation directory - @ Out, workbench_exe_path, pathlib.Path, the path to the Workbench executable - """ - if platform.system() == "Windows": - workbench_exe_path = workbench_dir / "bin" / "Workbench.exe" - elif platform.system() == "Darwin": - workbench_exe_path = workbench_dir / "Contents" / "MacOS" / "Workbench" - elif platform.system() == "Linux": - workbench_exe_path = workbench_dir / "bin" / "Workbench" - else: - raise ValueError(f"Platform {platform.system()} is not supported.") + @ In, workbench_dir, pathlib.Path, the path to the Workbench installation directory + @ Out, workbench_exe_path, pathlib.Path, the path to the Workbench executable + """ + if platform.system() == "Windows": + workbench_exe_path = workbench_dir / "bin" / "Workbench.exe" + elif platform.system() == "Darwin": + workbench_exe_path = workbench_dir / "Contents" / "MacOS" / "Workbench" + elif platform.system() == "Linux": + workbench_exe_path = workbench_dir / "bin" / "Workbench" + else: + raise ValueError(f"Platform {platform.system()} is not supported.") - return workbench_exe_path + return workbench_exe_path def get_workbench_dir_from_exe_path(workbench_exe_path: pathlib.Path) -> pathlib.Path: - """ - Returns the path to the Workbench installation directory from the path to the Workbench executable. + """ + Returns the path to the Workbench installation directory from the path to the Workbench executable. - @ In, workbench_exe_path, pathlib.Path, the path to the Workbench executable - @ Out, workbench_dir, pathlib.Path, the path to the Workbench installation directory - """ - # NOTE: for macOS, this returns the path to the "Contents" directory in the app bundle, not the - # app bundle's root directory. However, this keeps pathing more consistent with other platforms. - workbench_dir = workbench_exe_path.parent.parent - return workbench_dir + @ In, workbench_exe_path, pathlib.Path, the path to the Workbench executable + @ Out, workbench_dir, pathlib.Path, the path to the Workbench installation directory + """ + # NOTE: for macOS, this returns the path to the "Contents" directory in the app bundle, not the + # app bundle's root directory. However, this keeps pathing more consistent with other platforms. + workbench_dir = workbench_exe_path.parent.parent + return workbench_dir def verify_workbench_dir(workbench_dir: pathlib.Path) -> bool: - """ - Verifies that the given path is a valid NEAMS Workbench installation directory. This is done by checking for the Workbench executable, - dependent on the operating system. + """ + Verifies that the given path is a valid NEAMS Workbench installation directory. This is done by checking for the Workbench executable, + dependent on the operating system. - @ In, workbench_dir, pathlib.Path, the path to the Workbench installation directory - @ Out, valid, bool, True if the directory is a valid Workbench installation, False otherwise - """ - workbench_exe_path = get_workbench_exe_path(workbench_dir) - valid = workbench_exe_path.exists() - return valid + @ In, workbench_dir, pathlib.Path, the path to the Workbench installation directory + @ Out, valid, bool, True if the directory is a valid Workbench installation, False otherwise + """ + workbench_exe_path = get_workbench_exe_path(workbench_dir) + valid = workbench_exe_path.exists() + return valid def get_dirs(dirname: pathlib.Path, pattern: str = "*") -> list[pathlib.Path]: - """ - Finds all directories in dirname that match the given pattern. + """ + Finds all directories in dirname that match the given pattern. - @ In, dirname, pathlib.Path, the directory to search - @ In, pattern, str, optional, the pattern to match directories against - @ Out, dirs, list[pathlib.Path], the list of directories that match the pattern - """ - dirs = [p for p in dirname.iterdir() if p.is_dir() and p.match(pattern)] - return dirs + @ In, dirname, pathlib.Path, the directory to search + @ In, pattern, str, optional, the pattern to match directories against + @ Out, dirs, list[pathlib.Path], the list of directories that match the pattern + """ + dirs = [p for p in dirname.iterdir() if p.is_dir() and p.match(pattern)] + return dirs def check_workbench_file_for_dir(workbench_file: pathlib.Path) -> pathlib.Path | None: - """ - Checks the given .workbench file for the installation directory of Workbench. If file does not exist, None is returned. If - the file does exist but if the Workbench executable cannot be found there, the WORKBENCHDIR key is deleted and None is returned. - Finally, if the file exists and the Workbench executable is found, the path to the Workbench installation directory is returned. - - @ In, workbench_file, pathlib.Path, the path to the .workbench file - @ Out, workbench_dir, pathlib.Path | None, the path to the Workbench installation directory, or None if the file does not exist or the - Workbench executable cannot be found - """ - if not workbench_file.exists(): # .workbench file not found at given path - return None - - # Parse the .workbench file to get the installation directory of Workbench. - # Info is stored in the format "KEY=VALUE" on each line. - workbench_config = {} - with open(workbench_file, 'r') as f: - for line in f: - key, value = line.strip().split("=") - workbench_config[key] = value - - workbench_dir = workbench_config.get("WORKBENCHDIR", None) - - if workbench_dir is not None and not verify_workbench_dir(pathlib.Path(workbench_dir)): - workbench_dir = None - - if workbench_dir is None: # either wasn't provided or was invalid - # If the path in the .workbench file is invalid, delete the WORKBENCHDIR key so we don't keep trying - # to use it. If no other keys are present, delete the file. - workbench_config.pop("WORKBENCHDIR", None) # Remove the key if it exists - if len(workbench_config) == 0: # If no other keys are present, delete the file - workbench_file.unlink() - else: # Otherwise, write the updated config back to the file - with open(workbench_file, 'w') as f: - for key, value in workbench_config.items(): - f.write(f"{key}={value}\n") - - return workbench_dir + """ + Checks the given .workbench file for the installation directory of Workbench. If file does not exist, None is returned. If + the file does exist but if the Workbench executable cannot be found there, the WORKBENCHDIR key is deleted and None is returned. + Finally, if the file exists and the Workbench executable is found, the path to the Workbench installation directory is returned. + + @ In, workbench_file, pathlib.Path, the path to the .workbench file + @ Out, workbench_dir, pathlib.Path | None, the path to the Workbench installation directory, or None if the file does not exist or the + Workbench executable cannot be found + """ + if not workbench_file.exists(): # .workbench file not found at given path + return None + + # Parse the .workbench file to get the installation directory of Workbench. + # Info is stored in the format "KEY=VALUE" on each line. + workbench_config = {} + with open(workbench_file, 'r') as f: + for line in f: + key, value = line.strip().split("=") + workbench_config[key] = value + + workbench_dir = workbench_config.get("WORKBENCHDIR", None) + + if workbench_dir is not None and not verify_workbench_dir(pathlib.Path(workbench_dir)): + workbench_dir = None + + if workbench_dir is None: # either wasn't provided or was invalid + # If the path in the .workbench file is invalid, delete the WORKBENCHDIR key so we don't keep trying + # to use it. If no other keys are present, delete the file. + workbench_config.pop("WORKBENCHDIR", None) # Remove the key if it exists + if len(workbench_config) == 0: # If no other keys are present, delete the file + workbench_file.unlink() + else: # Otherwise, write the updated config back to the file + with open(workbench_file, 'w') as f: + for key, value in workbench_config.items(): + f.write(f"{key}={value}\n") + + return workbench_dir def find_workbench() -> pathlib.Path | None: - """ - Finds the NEAMS Workbench executable. A ".workbench" file in the FORCE app's main directory tracks - the location of the Workbench executable. If that file doesn't exist, we look in common directories - to find Workbench ourselves. If we still can't find it, we ask the user to locate it manually, if - desired. - - @ In, None - @ Out, workbench_exe_path, pathlib.Path | None, the path to the NEAMS Workbench executable, or None if it could not be found - """ - workbench_path = None - - # Check if a ".workbench" file exists in the main build directory (same directory as the heron and - # raven_framework executables). That should be 2 directories up from the directory of this file. - current_file_dir = pathlib.Path(__file__).parent - # Is this being run from a frozen executable or via the source code? Changes where the package's - # base directory is located, changing where to look for the .workbench file. - if is_frozen_app(): # Frozen executable - force_base_dir = current_file_dir.parent.parent - else: # Source code - force_base_dir = current_file_dir.parent - workbench_file = force_base_dir / ".workbench" - workbench_path = check_workbench_file_for_dir(workbench_file) # Returns None if file doesn't exist or is invalid - - # If that .workbench file doesn't exist, we can look around for the Workbench executable - if platform.system() == "Windows": - if wb_path := os.environ.get('WORKBENCH_PATH', None): - workbench_path = wb_path - elif wb_path := shutil.which('Workbench'): - workbench_path = wb_path + """ + Finds the NEAMS Workbench executable. A ".workbench" file in the FORCE app's main directory tracks + the location of the Workbench executable. If that file doesn't exist, we look in common directories + to find Workbench ourselves. If we still can't find it, we ask the user to locate it manually, if + desired. + + @ In, None + @ Out, workbench_exe_path, pathlib.Path | None, the path to the NEAMS Workbench executable, or None if it could not be found + """ + workbench_path = None + + # Check if a ".workbench" file exists in the main build directory (same directory as the heron and + # raven_framework executables). That should be 2 directories up from the directory of this file. + current_file_dir = pathlib.Path(__file__).parent + # Is this being run from a frozen executable or via the source code? Changes where the package's + # base directory is located, changing where to look for the .workbench file. + if is_frozen_app(): # Frozen executable + force_base_dir = current_file_dir.parent.parent + else: # Source code + force_base_dir = current_file_dir.parent + workbench_file = force_base_dir / ".workbench" + workbench_path = check_workbench_file_for_dir(workbench_file) # Returns None if file doesn't exist or is invalid + + # If that .workbench file doesn't exist, we can look around for the Workbench executable + if platform.system() == "Windows": + if wb_path := os.environ.get('WORKBENCH_PATH', None): + workbench_path = wb_path + elif wb_path := shutil.which('Workbench'): + workbench_path = wb_path + else: + # Manually search through a few common directories for the Workbench installation + for path in [force_base_dir.parent, force_base_dir, "$HOMEDRIVE", "$PROGRAMFILES", + "$HOME", "$APPDATA", "$LOCALAPPDATA"]: + path = pathlib.Path(os.path.expandvars(path)) + if not path.exists(): + continue + for loc in get_dirs(path, "Workbench*"): + if verify_workbench_dir(loc): + workbench_path = loc + break + elif platform.system() == "Darwin": + # The only place Workbench should be installed on Mac is in the Applications directory. + for app in get_dirs(pathlib.Path("/Applications"), "Workbench*"): + if verify_workbench_dir(app): + workbench_path = app + break + # NOTE: Workbench install on Linux is only with a source install which has no standard location. We'll rely on the user to connect + # the two tools. + + # If we still haven't found Workbench, let the user help us out. Throw up a tkinter warning dialog to + # ask the user to locate the Workbench executable. + if workbench_path is None: + root = tk.Tk() + root.withdraw() + + response = messagebox.askyesno( + title="NEAMS Workbench could not be found!", + message="The NEAMS Workbench executable could not be found. Would you like to manually find the Workbench installation directory?" + ) + if response: + dialog_title = "Select NEAMS Workbench Application" if platform.system() == "Darwin" else "Select NEAMS Workbench Directory" + workbench_path = filedialog.askdirectory(title=dialog_title) + if workbench_path: + is_valid = verify_workbench_dir(pathlib.Path(workbench_path)) + if not is_valid: + messagebox.showerror( + title="Invalid Workbench Directory", + message="The NEAMS Workbench executable was not found in the selected directory!" + ) + workbench_path = None else: - # Manually search through a few common directories for the Workbench installation - for path in [force_base_dir.parent, force_base_dir, "$HOMEDRIVE", "$PROGRAMFILES", - "$HOME", "$APPDATA", "$LOCALAPPDATA"]: - path = pathlib.Path(os.path.expandvars(path)) - if not path.exists(): - continue - for loc in get_dirs(path, "Workbench*"): - if verify_workbench_dir(loc): - workbench_path = loc - break - elif platform.system() == "Darwin": - # The only place Workbench should be installed on Mac is in the Applications directory. - for app in get_dirs(pathlib.Path("/Applications"), "Workbench*"): - if verify_workbench_dir(app): - workbench_path = app - break - # NOTE: Workbench install on Linux is only with a source install which has no standard location. We'll rely on the user to connect - # the two tools. - - # If we still haven't found Workbench, let the user help us out. Throw up a tkinter warning dialog to - # ask the user to locate the Workbench executable. - if workbench_path is None: - root = tk.Tk() - root.withdraw() - - response = messagebox.askyesno( - title="NEAMS Workbench could not be found!", - message="The NEAMS Workbench executable could not be found. Would you like to manually find the Workbench installation directory?" - ) - if response: - dialog_title = "Select NEAMS Workbench Application" if platform.system() == "Darwin" else "Select NEAMS Workbench Directory" - workbench_path = filedialog.askdirectory(title=dialog_title) - if workbench_path: - is_valid = verify_workbench_dir(pathlib.Path(workbench_path)) - if not is_valid: - messagebox.showerror( - title="Invalid Workbench Directory", - message="The NEAMS Workbench executable was not found in the selected directory!" - ) - workbench_path = None - else: - with open(workbench_file, 'w') as f: - f.write("WORKBENCHDIR=" + workbench_path) - - if workbench_path is None: # If we still don't have a valid path, just give up I guess - return None - - if isinstance(workbench_path, str): - workbench_path = pathlib.Path(workbench_path) - - workbench_exe_path = get_workbench_exe_path(workbench_path) - - return workbench_exe_path + with open(workbench_file, 'w') as f: + f.write("WORKBENCHDIR=" + workbench_path) + if workbench_path is None: # If we still don't have a valid path, just give up I guess + return None -def create_workbench_heron_default(workbench_dir: pathlib.Path): - """ - Creates a configuration file for Workbench so it knows where HERON is located. - - @ In, workbench_dir, pathlib.Path, the path to the NEAMS Workbench installation directory - @ Out, NOne - """ - # First, we need to find the HERON executable. This will be "heron.exe" on Windows - # and just "heron" on MacOS and Linux. It should be located 2 directories up from the - # directory of this file. - current_file_dir = pathlib.Path(__file__).parent - - # Is this a frozen executable or source code? Changes where the package's base directory is located. - if (current_file_dir.parent / "heron.py").exists(): # Source code - heron_path = current_file_dir.parent / "heron.py" - else: # Frozen executable - heron_path = current_file_dir.parent.parent / "heron" - # Windows executables have a ".exe" extension - if platform.system() == "Windows": - heron_path = heron_path.with_suffix(".exe") - - # If the HERON executable doesn't exist, we can't create the Workbench configuration file - if not heron_path.exists(): - return + if isinstance(workbench_path, str): + workbench_path = pathlib.Path(workbench_path) - # Create the configuration file for Workbench - workbench_config_file = workbench_dir / "default.apps.son" - if workbench_config_file.exists(): - # If the default app config file already exists, don't overwrite it. - return + workbench_exe_path = get_workbench_exe_path(workbench_path) - # If the file doesn't exist, create it and add a configuration for HERON - print("Adding HERON configuration to NEAMS Workbench", workbench_config_file, - "with HERON executable", heron_path) - with open(workbench_config_file, "w") as f: - f.write("applications {\n" - " HERON {\n" - " configurations {\n" - " default {\n" - " options {\n" - " shared {\n" - f" Executable=\"{heron_path}\"\n" - " }\n" - " }\n" - " }\n" - " }\n" - " }\n" - "}\n") + return workbench_exe_path + + +def create_workbench_heron_default(workbench_dir: pathlib.Path): + """ + Creates a configuration file for Workbench so it knows where HERON is located. + + @ In, workbench_dir, pathlib.Path, the path to the NEAMS Workbench installation directory + @ Out, NOne + """ + # First, we need to find the HERON executable. This will be "heron.exe" on Windows + # and just "heron" on MacOS and Linux. It should be located 2 directories up from the + # directory of this file. + current_file_dir = pathlib.Path(__file__).parent + + # Is this a frozen executable or source code? Changes where the package's base directory is located. + if (current_file_dir.parent / "heron.py").exists(): # Source code + heron_path = current_file_dir.parent / "heron.py" + else: # Frozen executable + heron_path = current_file_dir.parent.parent / "heron" + # Windows executables have a ".exe" extension + if platform.system() == "Windows": + heron_path = heron_path.with_suffix(".exe") + + # If the HERON executable doesn't exist, we can't create the Workbench configuration file + if not heron_path.exists(): + return + + # Create the configuration file for Workbench + workbench_config_file = workbench_dir / "default.apps.son" + if workbench_config_file.exists(): + # If the default app config file already exists, don't overwrite it. + return + + # If the file doesn't exist, create it and add a configuration for HERON + print("Adding HERON configuration to NEAMS Workbench", workbench_config_file, + "with HERON executable", heron_path) + with open(workbench_config_file, "w") as f: + f.write("applications {\n" + " HERON {\n" + " configurations {\n" + " default {\n" + " options {\n" + " shared {\n" + f" Executable=\"{heron_path}\"\n" + " }\n" + " }\n" + " }\n" + " }\n" + " }\n" + "}\n") def convert_xml_to_heron(xml_file: pathlib.Path, workbench_path: pathlib.Path) -> pathlib.Path: - """ - Converts an .xml file to a .heron file using Workbench's xml2eddi.py conversion script. - - @ In, xml_file, pathlib.Path, the path to the .xml file to convert - @ In, workbench_path, pathlib.Path, the path to the Workbench installation directory - @ Out, heron_file, pathlib.Path, the path to the converted .heron file - """ - # Find the xml2eddi.py script in the Workbench installation directory - xml2eddi_script = workbench_path / "rte" / "util" / "xml2eddi.py" - if not xml2eddi_script.exists(): - print(f"ERROR: Could not find the xml2eddi.py script in the Workbench installation directory ({str(workbench_path)}). " - f"Checked {str(xml2eddi_script)}.") - return None - - # Convert the .xml file to a .heron file by running the xml2eddi.py script with the .xml file as - # an argument and redirecting the output to a .heron file with the same name. - heron_file = xml_file.with_suffix(".heron") - print(f"Converting {xml_file} to {heron_file} using {xml2eddi_script}...") - # Use Workbench's entry.bat script (or entry.sh) to access the app's internal Python environment - # to run the script - entry_script = workbench_path / "rte" / "entry.bat" - if platform.system() != "Windows": - entry_script = entry_script.with_suffix(".sh") - if not entry_script.exists(): - print("ERROR: Could not find the entry script in the Workbench installation directory.") - return None - - with open(heron_file, "w") as f: - subprocess.run([str(entry_script), str(xml2eddi_script), str(xml_file)], stdout=f) - - return heron_file + """ + Converts an .xml file to a .heron file using Workbench's xml2eddi.py conversion script. + + @ In, xml_file, pathlib.Path, the path to the .xml file to convert + @ In, workbench_path, pathlib.Path, the path to the Workbench installation directory + @ Out, heron_file, pathlib.Path, the path to the converted .heron file + """ + # Find the xml2eddi.py script in the Workbench installation directory + xml2eddi_script = workbench_path / "rte" / "util" / "xml2eddi.py" + if not xml2eddi_script.exists(): + print(f"ERROR: Could not find the xml2eddi.py script in the Workbench installation directory ({str(workbench_path)}). " + f"Checked {str(xml2eddi_script)}.") + return None + + # Convert the .xml file to a .heron file by running the xml2eddi.py script with the .xml file as + # an argument and redirecting the output to a .heron file with the same name. + heron_file = xml_file.with_suffix(".heron") + print(f"Converting {xml_file} to {heron_file} using {xml2eddi_script}...") + # Use Workbench's entry.bat script (or entry.sh) to access the app's internal Python environment + # to run the script + entry_script = workbench_path / "rte" / "entry.bat" + if platform.system() != "Windows": + entry_script = entry_script.with_suffix(".sh") + if not entry_script.exists(): + print("ERROR: Could not find the entry script in the Workbench installation directory.") + return None + + with open(heron_file, "w") as f: + subprocess.run([str(entry_script), str(xml2eddi_script), str(xml_file)], stdout=f) + + return heron_file def run_in_workbench(file: str | None = None): - """ - Opens the given file in the NEAMS Workbench - @ In, file, str, optional, the file to open in NEAMS Workbench - """ - # Find the Workbench executable - workbench_path = find_workbench() - if workbench_path is None: - print("ERROR: Could not find the NEAMS Workbench executable. Please set the " - "WORKBENCH_PATH environment variable, add it to the system path, or specify it manually " - "with the WORKBENCHDIR key in the \".workbench\" file in the main FORCE directory.") + """ + Opens the given file in the NEAMS Workbench + @ In, file, str, optional, the file to open in NEAMS Workbench + """ + # Find the Workbench executable + workbench_path = find_workbench() + if workbench_path is None: + print("ERROR: Could not find the NEAMS Workbench executable. Please set the " + "WORKBENCH_PATH environment variable, add it to the system path, or specify it manually " + "with the WORKBENCHDIR key in the \".workbench\" file in the main FORCE directory.") + return + + # Create Workbench default configuration for HERON if a default configurations file does not exist + workbench_install_dir = get_workbench_dir_from_exe_path(workbench_path) + create_workbench_heron_default(workbench_install_dir) + + # Convert the .xml file to a .heron file if one was provided + if file is not None: + file = pathlib.Path(file) + if not file.exists(): + print(f"ERROR: The file {file} does not exist.") + return + + if file.suffix == ".xml": + heron_file = convert_xml_to_heron(file, workbench_install_dir) + if heron_file is None: return - - # Create Workbench default configuration for HERON if a default configurations file does not exist - workbench_install_dir = get_workbench_dir_from_exe_path(workbench_path) - create_workbench_heron_default(workbench_install_dir) - - # Convert the .xml file to a .heron file if one was provided - if file is not None: - file = pathlib.Path(file) - if not file.exists(): - print(f"ERROR: The file {file} does not exist.") - return - - if file.suffix == ".xml": - heron_file = convert_xml_to_heron(file, workbench_install_dir) - if heron_file is None: - return - file = heron_file - file = str(file) - - # Open the file in Workbench - # Currently, we're only able to open the MacOS version of Workbench by opening the app itself. - # This does not accept a file as an argument, so users will need to open Workbench, then open - # the desired file manually from within the app. - # if file is not None and platform.system() == "Windows": - # command += ' ' + file - - print("Opening Workbench...", file=sys.__stdout__) - print("***If this is the first time you are running Workbench, this may take a few minutes!***\n", - file=sys.__stdout__) - if platform.system() == "Windows": - if file is None: - command = str(workbench_path) - else: - command = [str(workbench_path), file] - print("using command", command) - subprocess.run(command) + file = heron_file + file = str(file) + + # Open the file in Workbench + # Currently, we're only able to open the MacOS version of Workbench by opening the app itself. + # This does not accept a file as an argument, so users will need to open Workbench, then open + # the desired file manually from within the app. + # if file is not None and platform.system() == "Windows": + # command += ' ' + file + + print("Opening Workbench...", file=sys.__stdout__) + print("***If this is the first time you are running Workbench, this may take a few minutes!***\n", + file=sys.__stdout__) + if platform.system() == "Windows": + if file is None: + command = str(workbench_path) else: - # NOTE: untested on Linux as of 2024-07-22 - subprocess.run(["/usr/bin/open", "-n", "-a", workbench_path]) + command = [str(workbench_path), file] + print("using command", command) + subprocess.run(command) + else: + # NOTE: untested on Linux as of 2024-07-22 + subprocess.run(["/usr/bin/open", "-n", "-a", workbench_path]) diff --git a/package/ui/views/file_selection.py b/package/ui/views/file_selection.py index 3b6a7844..324ef019 100644 --- a/package/ui/views/file_selection.py +++ b/package/ui/views/file_selection.py @@ -3,58 +3,58 @@ class FileSelection(tk.Frame): - """ A widget for selecting files and displaying the path after selection.""" - def __init__(self, master: tk.Widget, **kwargs): - """ - Constructor - @In, master, tk.Widget, the parent widget - @In, kwargs, dict, keyword arguments - @Out, None - """ - super().__init__(master, **kwargs) - self.file_selectors = {} - self.open_in_workbench_button = None + """ A widget for selecting files and displaying the path after selection.""" + def __init__(self, master: tk.Widget, **kwargs): + """ + Constructor + @In, master, tk.Widget, the parent widget + @In, kwargs, dict, keyword arguments + @Out, None + """ + super().__init__(master, **kwargs) + self.file_selectors = {} + self.open_in_workbench_button = None - def new_file_selector(self, label: str): - """ - Add a file selector to the widget - @In, label, str, the title of the file selector - @Out, None - """ - frame = SelectAFile(self, label) - frame.grid(row=len(self.file_selectors), column=0, sticky='w') - self.file_selectors[label] = frame + def new_file_selector(self, label: str): + """ + Add a file selector to the widget + @In, label, str, the title of the file selector + @Out, None + """ + frame = SelectAFile(self, label) + frame.grid(row=len(self.file_selectors), column=0, sticky='w') + self.file_selectors[label] = frame - def add_open_in_workbench_button(self, command: Callable): - """ - Create a button to open the file in Workbench. This button is only created once the first - file selector is added to the widget. Not every application will need this button, so its - creation is deferred until it is needed and is called by the controller. - """ - self.open_in_workbench_button = tk.Button(self, text='Open in Workbench') - self.open_in_workbench_button.grid(row=0, column=1, sticky='se') - self.grid_columnconfigure(1, weight=1) - self.open_in_workbench_button.config(command=command) + def add_open_in_workbench_button(self, command: Callable): + """ + Create a button to open the file in Workbench. This button is only created once the first + file selector is added to the widget. Not every application will need this button, so its + creation is deferred until it is needed and is called by the controller. + """ + self.open_in_workbench_button = tk.Button(self, text='Open in Workbench') + self.open_in_workbench_button.grid(row=0, column=1, sticky='se') + self.grid_columnconfigure(1, weight=1) + self.open_in_workbench_button.config(command=command) class SelectAFile(tk.Frame): - """ A widget for selecting one file and displaying the path after selection. """ - def __init__(self, - master: tk.Widget, - label: Optional[str] = None): - """ - Constructor - @In, master, tk.Widget, the parent widget - @In, label, Optional[str], the title of the file selector - @Out, None - """ - super().__init__(master) - self.file_title = tk.Label(self, text=label) - self.file_title.grid(row=0, column=0, columnspan=2, sticky='w') - self.browse_button = tk.Button(self, text='Browse', width=10, padx=5) - self.browse_button.grid(row=1, column=0, sticky='w') - self.filename = tk.StringVar() - self.filename.set("Select a file") # Default filename is "Select a file", i.e. no file selected - self.filename_label = tk.Label(self, textvariable=self.filename, bg="white", anchor='w', padx=10, pady=3) - self.filename_label.grid(row=1, column=1, sticky='w', padx=5) - self.grid_columnconfigure(1, weight=1) + """ A widget for selecting one file and displaying the path after selection. """ + def __init__(self, + master: tk.Widget, + label: Optional[str] = None): + """ + Constructor + @In, master, tk.Widget, the parent widget + @In, label, Optional[str], the title of the file selector + @Out, None + """ + super().__init__(master) + self.file_title = tk.Label(self, text=label) + self.file_title.grid(row=0, column=0, columnspan=2, sticky='w') + self.browse_button = tk.Button(self, text='Browse', width=10, padx=5) + self.browse_button.grid(row=1, column=0, sticky='w') + self.filename = tk.StringVar() + self.filename.set("Select a file") # Default filename is "Select a file", i.e. no file selected + self.filename_label = tk.Label(self, textvariable=self.filename, bg="white", anchor='w', padx=10, pady=3) + self.filename_label.grid(row=1, column=1, sticky='w', padx=5) + self.grid_columnconfigure(1, weight=1) diff --git a/package/ui/views/main.py b/package/ui/views/main.py index ae3cd3a1..07baec60 100644 --- a/package/ui/views/main.py +++ b/package/ui/views/main.py @@ -7,53 +7,53 @@ class View: - """ Main view class. """ - def __init__(self): - """ - Main view constructor. Sets up main window by adding essential frames. - @In, None - @Out, None - """ - self.root = Root() - self.frames = {} - - # add frames to the view in a grid layout - self.add_frame('file_selection', FileSelection(self.root), - row=0, column=0, sticky='nsew', padx=10, pady=5) - self.add_frame('text_output', TextOutput(self.root), - row=1, column=0, sticky='nsew', padx=10, pady=10) - self.add_frame('run_abort', RunAbort(self.root), - row=2, column=0, sticky='se', padx=10, pady=5) - - # Let the text output frame expand to fill the available space - self.root.grid_rowconfigure(1, weight=1) - self.root.grid_columnconfigure(0, weight=1) - - def add_frame(self, name, frame, **kwargs): - """ - Add a frame to the view - @In, name, str, the name of the frame - @In, frame, tk.Frame, the frame to add - @In, kwargs, dict, keyword arguments for grid - @Out, None - """ - self.frames[name] = frame - frame.grid(**kwargs) - - def mainloop(self): - """ - Run the application main loop - @In, None - @Out, None - """ - self.root.mainloop() - - def quit(self, showdialog: bool = True): - """ - Quit the application - @In, showdialog, bool, optional, whether to show a dialog before quitting, default is True - @Out, None - """ - if not showdialog or askokcancel('Abort run', 'Are you sure you want to abort? ' - 'This will close the window and any text output will be lost.'): - self.root.quit() + """ Main view class. """ + def __init__(self): + """ + Main view constructor. Sets up main window by adding essential frames. + @In, None + @Out, None + """ + self.root = Root() + self.frames = {} + + # add frames to the view in a grid layout + self.add_frame('file_selection', FileSelection(self.root), + row=0, column=0, sticky='nsew', padx=10, pady=5) + self.add_frame('text_output', TextOutput(self.root), + row=1, column=0, sticky='nsew', padx=10, pady=10) + self.add_frame('run_abort', RunAbort(self.root), + row=2, column=0, sticky='se', padx=10, pady=5) + + # Let the text output frame expand to fill the available space + self.root.grid_rowconfigure(1, weight=1) + self.root.grid_columnconfigure(0, weight=1) + + def add_frame(self, name, frame, **kwargs): + """ + Add a frame to the view + @In, name, str, the name of the frame + @In, frame, tk.Frame, the frame to add + @In, kwargs, dict, keyword arguments for grid + @Out, None + """ + self.frames[name] = frame + frame.grid(**kwargs) + + def mainloop(self): + """ + Run the application main loop + @In, None + @Out, None + """ + self.root.mainloop() + + def quit(self, showdialog: bool = True): + """ + Quit the application + @In, showdialog, bool, optional, whether to show a dialog before quitting, default is True + @Out, None + """ + if not showdialog or askokcancel('Abort run', 'Are you sure you want to abort? ' + 'This will close the window and any text output will be lost.'): + self.root.quit() diff --git a/package/ui/views/model_status.py b/package/ui/views/model_status.py index 3f44abb5..f6d60105 100644 --- a/package/ui/views/model_status.py +++ b/package/ui/views/model_status.py @@ -3,32 +3,32 @@ class ModelStatus(tk.Frame): - """ A widget for displaying the status of the model. """ - def __init__(self, master: tk.Widget, **kwargs): - """ - Constructor - @In, master, tk.Widget, the parent widget - @In, kwargs, dict, keyword arguments - @Out, None - """ - super().__init__(master, **kwargs) - self.status = tk.StringVar() - self.status.set("Model not yet run") - self.status_label = tk.Label(self, textvariable=self.status, bg="white", anchor='w', padx=10, pady=3) - self.status_label.pack(side='left') - self.grid_columnconfigure(0, weight=1) + """ A widget for displaying the status of the model. """ + def __init__(self, master: tk.Widget, **kwargs): + """ + Constructor + @In, master, tk.Widget, the parent widget + @In, kwargs, dict, keyword arguments + @Out, None + """ + super().__init__(master, **kwargs) + self.status = tk.StringVar() + self.status.set("Model not yet run") + self.status_label = tk.Label(self, textvariable=self.status, bg="white", anchor='w', padx=10, pady=3) + self.status_label.pack(side='left') + self.grid_columnconfigure(0, weight=1) - def set_status(self, new_status: ModelStatusEnum): - """ - Set the status label - @In, new_status, ModelStatusEnum, the new status - @Out, None - """ - self.status.set(new_status.value) - # Change the color of the label based on the status - if new_status == ModelStatusEnum.FINISHED: - self.status_label.config(fg='green') - elif new_status == ModelStatusEnum.ERROR: - self.status_label.config(fg='red') - else: - self.status_label.config(fg='black') + def set_status(self, new_status: ModelStatusEnum): + """ + Set the status label + @In, new_status, ModelStatusEnum, the new status + @Out, None + """ + self.status.set(new_status.value) + # Change the color of the label based on the status + if new_status == ModelStatusEnum.FINISHED: + self.status_label.config(fg='green') + elif new_status == ModelStatusEnum.ERROR: + self.status_label.config(fg='red') + else: + self.status_label.config(fg='black') diff --git a/package/ui/views/root.py b/package/ui/views/root.py index a2d3dd71..537e1201 100644 --- a/package/ui/views/root.py +++ b/package/ui/views/root.py @@ -2,14 +2,14 @@ class Root(tk.Tk): - """ The main window. """ - def __init__(self, **kwargs): - """ - Constructor - @In, kwargs, dict, keyword arguments for tkinter.Tk - @Out, None - """ - super().__init__(**kwargs) - self.title('FORCE') - self.geometry('800x600') - self.grid() + """ The main window. """ + def __init__(self, **kwargs): + """ + Constructor + @In, kwargs, dict, keyword arguments for tkinter.Tk + @Out, None + """ + super().__init__(**kwargs) + self.title('FORCE') + self.geometry('800x600') + self.grid() diff --git a/package/ui/views/run_abort.py b/package/ui/views/run_abort.py index 2d1087a2..08255748 100644 --- a/package/ui/views/run_abort.py +++ b/package/ui/views/run_abort.py @@ -2,21 +2,21 @@ class RunAbort(tk.Frame): - """ Buttons for starting and stopping the application. """ - def __init__(self, master, **kwargs): - """ - Constructor - @In, master, tk.Widget, the parent widget - @In, kwargs, dict, keyword arguments - @Out, None - """ - super().__init__(master, **kwargs) - button_width = 10 - self.abort_button = tk.Button(self, text='Abort', width=button_width) - self.abort_button.grid(row=0, column=0, sticky='w', padx=5) + """ Buttons for starting and stopping the application. """ + def __init__(self, master, **kwargs): + """ + Constructor + @In, master, tk.Widget, the parent widget + @In, kwargs, dict, keyword arguments + @Out, None + """ + super().__init__(master, **kwargs) + button_width = 10 + self.abort_button = tk.Button(self, text='Abort', width=button_width) + self.abort_button.grid(row=0, column=0, sticky='w', padx=5) - self.run_button = tk.Button(self, text='Run', width=button_width) - self.run_button.grid(row=0, column=1, sticky='w', padx=5) + self.run_button = tk.Button(self, text='Run', width=button_width) + self.run_button.grid(row=0, column=1, sticky='w', padx=5) - self.grid_columnconfigure(0, minsize=50) - self.grid_columnconfigure(1, weight=1, minsize=50) + self.grid_columnconfigure(0, minsize=50) + self.grid_columnconfigure(1, weight=1, minsize=50) diff --git a/package/ui/views/text_output.py b/package/ui/views/text_output.py index 2279e154..7339a7a1 100644 --- a/package/ui/views/text_output.py +++ b/package/ui/views/text_output.py @@ -4,48 +4,48 @@ class TextOutput(tk.Frame): - """ A widget for displaying text output. """ - def __init__(self, master, **kwargs): - """ - Constructor - @In, master, tk.Widget, the parent widget - @In, kwargs, dict, keyword arguments - @Out, None - """ - super().__init__(master, **kwargs) - self.show_hide_button = tk.Button(self, text='Hide Ouptut', pady=5, width=15) - self.show_hide_button.grid(row=0, column=0, sticky='w') - self.model_status = ModelStatus(self) - self.model_status.grid(row=0, column=1, sticky='e') - self.text = ScrolledText(self, state=tk.DISABLED) - self.is_showing = True # To use with show/hide button - self.text.grid(row=1, column=0, sticky='nsew', columnspan=2) - self.grid_rowconfigure(0, minsize=50) - self.grid_rowconfigure(1, weight=1) - self.grid_columnconfigure(0, weight=1) + """ A widget for displaying text output. """ + def __init__(self, master, **kwargs): + """ + Constructor + @In, master, tk.Widget, the parent widget + @In, kwargs, dict, keyword arguments + @Out, None + """ + super().__init__(master, **kwargs) + self.show_hide_button = tk.Button(self, text='Hide Ouptut', pady=5, width=15) + self.show_hide_button.grid(row=0, column=0, sticky='w') + self.model_status = ModelStatus(self) + self.model_status.grid(row=0, column=1, sticky='e') + self.text = ScrolledText(self, state=tk.DISABLED) + self.is_showing = True # To use with show/hide button + self.text.grid(row=1, column=0, sticky='nsew', columnspan=2) + self.grid_rowconfigure(0, minsize=50) + self.grid_rowconfigure(1, weight=1) + self.grid_columnconfigure(0, weight=1) - def show_text_output(self): - """ - Show the text output widget - @In, None - @Out, None - """ - self.text.grid(row=1, column=0, sticky='nsew', columnspan=2) - self.show_hide_button.config(text='Hide Output') - self.is_showing = True - # Set window to default size - self.master.update() - self.master.geometry("800x600") + def show_text_output(self): + """ + Show the text output widget + @In, None + @Out, None + """ + self.text.grid(row=1, column=0, sticky='nsew', columnspan=2) + self.show_hide_button.config(text='Hide Output') + self.is_showing = True + # Set window to default size + self.master.update() + self.master.geometry("800x600") - def hide_text_output(self): - """ - Hide the text output widget - @In, None - @Out, None - """ - self.text.grid_forget() - self.show_hide_button.config(text='Show Output') - self.is_showing = False - # Reduce window size - self.master.update() - self.master.geometry("350x175") + def hide_text_output(self): + """ + Hide the text output widget + @In, None + @Out, None + """ + self.text.grid_forget() + self.show_hide_button.config(text='Show Output') + self.is_showing = False + # Reduce window size + self.master.update() + self.master.geometry("350x175") diff --git a/package/utils.py b/package/utils.py index a9b4df8f..bbeee178 100644 --- a/package/utils.py +++ b/package/utils.py @@ -3,17 +3,17 @@ def add_local_bin_to_path(): - """ - Adds the local/bin directory to the system path in order to find ipopt and other executables - """ - script_path = os.path.dirname(sys.argv[0]) - local_path = os.path.join(script_path,"local","bin") - # Add script path (to get raven_framework in the path, and local/bin - # to get things like ipopt in the path. - os.environ['PATH'] += (os.pathsep+local_path+os.pathsep+script_path) - # Recursively add all additional "bin" directories in "local/bin" to the system path - if os.path.exists(local_path): - os.environ['PATH'] += (os.pathsep+local_path) - for root, dirs, files in os.walk(local_path): - if 'bin' in dirs: - os.environ['PATH'] += (os.pathsep+os.path.join(root,'bin')) + """ + Adds the local/bin directory to the system path in order to find ipopt and other executables + """ + script_path = os.path.dirname(sys.argv[0]) + local_path = os.path.join(script_path,"local","bin") + # Add script path (to get raven_framework in the path, and local/bin + # to get things like ipopt in the path. + os.environ['PATH'] += (os.pathsep+local_path+os.pathsep+script_path) + # Recursively add all additional "bin" directories in "local/bin" to the system path + if os.path.exists(local_path): + os.environ['PATH'] += (os.pathsep+local_path) + for root, dirs, files in os.walk(local_path): + if 'bin' in dirs: + os.environ['PATH'] += (os.pathsep+os.path.join(root,'bin'))