diff --git a/docs_src/api.md b/docs_src/api.md index b66c594..de37e7d 100644 --- a/docs_src/api.md +++ b/docs_src/api.md @@ -1,19 +1,26 @@ # Python API -The Python API is contained in the *idaes_connectivity.base* module. -The basic workflow is: -* Create an instance of the [Connectivity](connectivity-class) class from a model. -* Format the connectivity information with one of the subclasses of the [Formatter](formatter-classes) class, writing to a file -or returning the value as a string. +The Python API is contained in the *idaes_connectivity.base* module. The basic +workflow is: +* Create an instance of the [Connectivity](connectivity-class) class from a + model. +* Format the connectivity information with one of the subclasses of the + [Formatter](formatter-classes) class, writing to a file or returning the value + as a string. - You can output CSV for viewing in a text editor or spreadsheet program. - - You can also output a text-based diagram specification for viewing in a tool such as Mermaid or D2. Find more details on the [diagrams](diagrams.md) page. + - You can also output a text-based diagram specification for viewing in a tool + such as Mermaid or D2. Find more details on the [diagrams](diagrams.md) + page. -The rest of this page provides some [examples](api-examples) of API usage and details of the [Connectivity](connectivity-class) and [Formatter](formatter-classes) classes. +The rest of this page provides some [examples](api-examples) of API usage and +details of the [Connectivity](connectivity-class) and +[Formatter](formatter-classes) classes. (api-examples)= ## Examples -In these examples, we will assume we have an instance of Connectivity for a given model. +In these examples, we will assume we have an instance of Connectivity for a +given model. ``` from idaes_connectivity.tests.example_flowsheet import build model = build() @@ -39,7 +46,14 @@ d2_fmt = D2(conn) d2_fmt.write("myfile.d2") ``` -Returning as a string requires `None` as the file argument (full example to show output): +You can directly create a rendered Mermaid image diagram: +``` +from idaes_connectivity.base import MermaidImage +MermaidImage(conn).write("flowsheet_diagram.png") +``` + +Returning as a string requires `None` as the file argument (full example to show +output): ``` from idaes_connectivity.base import D2, Connectivity from idaes_connectivity.tests.example_flowsheet import build @@ -58,8 +72,9 @@ Unit_B -> Unit_C Unit_C -> Unit_D ``` -A convenience method, `display_connectivity`, displays the Mermaid diagram inline in a Jupyter Notebook. -This requires JupyterLab 4.1 or Notebook 7.1, or later. +A convenience method, `display_connectivity`, displays the Mermaid diagram +inline in a Jupyter Notebook. This requires JupyterLab 4.1 or Notebook 7.1, or +later. ``` from idaes_connectivity.base import Connectivity from idaes_connectivity.jupyter import display_connectivity @@ -107,8 +122,8 @@ The basic usage is: ### CSV formatter -The CSV formatter writes out text as comma-separated values. -It ignores the `direction` argument in the `Formatter` base class constructor. +The CSV formatter writes out text as comma-separated values. It ignores the +`direction` argument in the `Formatter` base class constructor. ```{eval-rst} .. autoclass:: idaes_connectivity.base.CSV @@ -128,9 +143,10 @@ The Mermaid formatter writes out a Mermaid text description. #### Jupyter -Mermaid is supported by newer versions of Jupyter Notebooks and Jupyter Lab. -The *display_connectivity* function allows one to easily display a diagram in a Jupyter notebook. -This function is also shown in the [Jupyter Notebook example](./example.md). +Mermaid is supported by newer versions of Jupyter Notebooks and Jupyter Lab. The +*display_connectivity* function allows one to easily display a diagram in a +Jupyter notebook. This function is also shown in the +[Jupyter Notebook example](./example.md). ```{eval-rst} .. autofunction:: idaes_connectivity.jupyter.display_connectivity diff --git a/docs_src/diagrams.md b/docs_src/diagrams.md index 9bc5c9d..2407ab5 100644 --- a/docs_src/diagrams.md +++ b/docs_src/diagrams.md @@ -6,10 +6,15 @@ myst: # Making diagrams Formatting of connectivity information as a diagram relies on external tools. -While this does require an extra step to install and run these tools, it also provides flexibility and leverages the full power of the tools' user communities. +While this does require an extra step to install and run these tools, it also +provides flexibility and leverages the full power of the tools' user +communities. -Below are links an instructions for the supported tools, [](mermaid-tool) and [](d2-tool). -Both tools do roughly the same thing: create diagrams from text, with automatic layout. Mermaid, written in JavaScript, has an online editor and built-in support in GitHub and Jupyter notebooks whereas D2, written in Go, has more flexible layout options and is easier to use in the console. +Below are links an instructions for the supported tools, [](mermaid-tool) and +[](d2-tool). Both tools do roughly the same thing: create diagrams from text, +with automatic layout. Mermaid, written in JavaScript, has an online editor and +built-in support in GitHub and Jupyter notebooks whereas D2, written in Go, has +more flexible layout options and is easier to use in the console. (mermaid-tool)= ## Mermaid @@ -17,8 +22,8 @@ Both tools do roughly the same thing: create diagrams from text, with automatic The {{ proj }} can create Mermaid text definitions and also has a convenience function to help render Mermaid diagrams inside a Jupyter Notebook. ### Generate text definition -For example, to generate Mermaid text from connectivity data in the CSV file *model_conn.csv*, -you could run: +For example, to generate Mermaid text from connectivity data in the CSV file +*model_conn.csv*, you could run: ``` idaes-conn --to mermaid model_conn.csv -O "-" @@ -30,7 +35,8 @@ This will print the text definition of the diagram to the console. #### Jupyter / GitHub -You could then paste the output into a Jupyter Notebook markdown cell, or GitHub markdown page, like this: +You could then paste the output into a Jupyter Notebook markdown cell, or GitHub +markdown page, like this: :::{code} ```mermaid @@ -40,47 +46,39 @@ You could then paste the output into a Jupyter Notebook markdown cell, or GitHub #### Online Editor -You could also load these diagrams into the online [Mermaid Live Editor](https://mermaid.live/). +You could also load these diagrams into the online +[Mermaid Live Editor](https://mermaid.live/). #### Console -If you want to generate the diagram locally, and are willing and able to install NodeJS packages on your machine, then follow these instructions: +If you want to generate the diagram locally, and are willing and able to install +NodeJS packages on your machine, then follow these instructions: -First install the mermaid-cli with the [Node Package Manager](https://www.npmjs.com/) (install that first if you don't have it): +First install the mermaid-cli with the +[Node Package Manager](https://www.npmjs.com/) (install that first if you don't +have it): ``` npm install @mermaid-js/mermaid-cli ``` -This will install wherever you ran the command. Make sure you run the next commands in the same directory. -Next, paste the following into a script we will call `run-mermaid.js`: -``` -const { run } = await import("@mermaid-js/mermaid-cli"); -const input_file = process.argv[2]; -const output_file = process.argv[3]; -console.log("Generating " + output_file + " from " + input_file); -await run(input_file, output_file); -``` - -An optional but useful step to avoid some warnings: edit the *package.json* file (this was created when you did the `npm install` command) and add a line at the top. -``` -{ - "type": "module", - # .. rest of file .. -} -``` +This will install wherever you ran the command. Make sure you run the next +commands in the same directory. -Finally, you can convert a Mermaid diagram to an SVG (Scalable Vector Graphics image) file with this command: +Now, you can generate an image with the `idaes-conn` command by adding the +option `--mmdc`: ``` -node run-mermaid.js +idaes-conn --to mermaid --mmdc model_conn.csv -O model_diagram.png ``` +The `--to mermaid` is optional as Mermaid is the default. (d2-tool)= ## D2 -[Declarative Diagramming (D2)](https://d2lang.com/) describes itself as "A modern language that turns text to diagrams". -Like Mermaid, D2 generates a SVG diagram from a simple text description. +[Declarative Diagramming (D2)](https://d2lang.com/) describes itself as "A +modern language that turns text to diagrams". Like Mermaid, D2 generates a SVG +diagram from a simple text description. ### Generate text definition -For example, to generate D2 text from connectivity data in the CSV file *model_conn.csv*, -you could run: +For example, to generate D2 text from connectivity data in the CSV file +*model_conn.csv*, you could run: ``` idaes-conn --to d2 model_conn.csv -O model_conn.d2 ``` @@ -89,12 +87,17 @@ This will print the text definition of the diagram to the file *model_conn.d2* ### Generate diagram -To generate the diagram, [install D2 on your computer](https://d2lang.com/tour/install) and then run the `d2` command-line interface (CLI) with the file you generated above as input: +To generate the diagram, +[install D2 on your computer](https://d2lang.com/tour/install) and then run the +`d2` command-line interface (CLI) with the file you generated above as input: ``` d2 model_conn.d2 model_conn.svg ``` -There are numerous options to the `d2` CLI that can help modify layout and style, as well as the program behavior. For example, specifying an output file with ".png" as the suffix will generate a PNG image. Run `d2 -h` to see them and/or visit the documentation on the website. +There are numerous options to the `d2` CLI that can help modify layout and +style, as well as the program behavior. For example, specifying an output file +with ".png" as the suffix will generate a PNG image. Run `d2 -h` to see them +and/or visit the documentation on the website. ```{note} Unlike Mermaid, D2 does not have an online editor or Jupyter integration. On the other hand, generating diagrams locally is straightforward. diff --git a/idaes_connectivity/__init__.py b/idaes_connectivity/__init__.py index cb70601..0f7ec82 100644 --- a/idaes_connectivity/__init__.py +++ b/idaes_connectivity/__init__.py @@ -1,3 +1,5 @@ from idaes_connectivity import version __version__ = version.VERSION + +from idaes_connectivity.base import Mermaid, MermaidImage, Connectivity diff --git a/idaes_connectivity/base.py b/idaes_connectivity/base.py index b6d4315..fb8751f 100644 --- a/idaes_connectivity/base.py +++ b/idaes_connectivity/base.py @@ -28,8 +28,11 @@ import logging from pathlib import Path import re -import sys -from typing import TextIO, Tuple, Union, Optional, List, Dict +import shutil +import subprocess +from tempfile import NamedTemporaryFile +import time +from typing import TextIO, Tuple, Union, Optional, List, Dict, Any import warnings from PIL import Image as im @@ -37,6 +40,8 @@ import io, requests # third-party +from IPython.display import Markdown + try: import pyomo from pyomo.network import Arc @@ -623,8 +628,57 @@ class Formatter(abc.ABC): defaults = {} # extend in subclasses - def __init__(self, connectivity: Connectivity, **kwargs): - self._conn = connectivity + def __init__(self, connectivity: Connectivity | Any, **kwargs): + """Constructor. + + Arguments: + connectivity: Either a Connectivity instance or any valid value + for `input_*` arguments that could be passed to + create a Connectivity instance. + + Raises: + ValueError: Unable to construct Connectivity instance from provided arg + """ + if isinstance(connectivity, Connectivity): + self._conn = connectivity + else: + self._conn = self._connectivity_factory(connectivity) + + def _connectivity_factory(self, arg) -> Connectivity: + kwargs = {} + # a string can be many things: + # path, CSV, module name + if isinstance(arg, str): + try: + # is this a path? + path = Path(arg) + if not path.exists(): + raise ValueError() + kwargs["input_file"] = path + except ValueError: + # not a path. is it a CSV blob? + csv_data = arg.split("\n") + if len(csv_data) > 1: + hdr = csv_data[0].split(",") + if len(hdr) > 1: + kwargs["input_data"] = arg + else: + # not CSV. is it a module name? + kwargs["input_module"] = arg + # things that are specifically file paths + elif isinstance(arg, TextIO) or isinstance(arg, Path): + kwargs["input_file"] = arg + # otherwise it is probably a model + elif hasattr(arg, "component_objects"): + kwargs["input_model"] = arg + + if not kwargs: # nothing matched! + raise ValueError( + "Argument is not an input file, Pyomo/IDAES model, " + "module name, or CSV text data" + ) + + return Connectivity(**kwargs) @staticmethod def _parse_direction(d): @@ -776,6 +830,11 @@ def _start_image_server(self, component_image_dir: Path | str) -> bool: return started + def _repr_markdown_(self): + """Display using Markdown in a Jupyter Notebook.""" + graph_str = self.write(None) + return f"```mermaid\n{graph_str}\n```" + def write(self, output_file: Union[str, TextIO, None]) -> Optional[str]: """Write Mermaid text description.""" f = self._get_output_stream(output_file) @@ -901,6 +960,76 @@ def _clean_stream_label(label): return label +class MermaidImage(Formatter): + """Formatter that calls mermaid-cli command line program (mmdc) in order to + generate the diagram as an aimage file. + + For more information about mermaid-cli, see https://github.com/mermaid-js/mermaid-cli + + Example usage:: + + from idaes_connectivity.base import Connectivity, MermaidImage + # somehow create connectivity object, e.g. from a CSV file + conn = Connectivity(input_file="idaes_connectivity/tests/example_flowsheet.csv") + # create an image + MermaidImage(conn).write("flowsheet_diagram.png") + """ + + def __init__(self, conn: Connectivity, **kwargs): + """Constructor. + + Args: + conn: Connectivity to graph + kwargs: Same as for the `Mermaid` class, except for an additional + section for (optional) keywords related to the mmdc program:: + { "mmdc": + "bin": "", # path to the binary + "options": ["", "", ..] # extra CLI opts + } + """ + if "mmdc" in kwargs: + self._bin = kwargs["mmdc"].get("bin", self.find_mmdc()) + self._opt = kwargs["mmdc"].get("options", []) + del kwargs["mmdc"] + else: + self._bin = self.find_mmdc() + self._opt = [] + self._formatter = Mermaid(conn, **kwargs) + + def write(self, output_file: Path | str): + """Write to image file. + + Arguments: + output_file: Image file name. Extension determines image type, as decided + by the mermaid-cli program. + """ + _log.info(f"Use 'mmdc' to create output in '{output_file}'") + # write mermaid output to a named temporary file + tmpfile = NamedTemporaryFile(mode="w") + self._formatter.write(tmpfile) + tmpfile.flush() + if _log.isEnabledFor(logging.DEBUG): + with open(tmpfile.name, "r") as f: + buf = f.read() + _log.debug(f"Contents of temporary file ({tmpfile.name}):\n{buf}") + time.sleep(1) # lame, but safer + # run mmdc on temporary file, writing its image output to user-provided file + args = [self._bin, "-i", tmpfile.name] + if not hasattr(output_file, "close"): # e.g. stdout + args.extend(["-o", output_file]) + args += self._opt + _log.info(f"running: {' '.join(args)}") + try: + subprocess.check_call(args, stderr=subprocess.DEVNULL) + except (subprocess.CalledProcessError, FileNotFoundError) as err: + raise RuntimeError(err) + + @staticmethod + def find_mmdc() -> str | None: + """Find CLI program for mermaid-cli (mmdc).""" + return shutil.which("mmdc") + + class D2(Formatter): """Create output in Terraform D2 syntax. diff --git a/idaes_connectivity/cli.py b/idaes_connectivity/cli.py index f5b193f..7a478c7 100644 --- a/idaes_connectivity/cli.py +++ b/idaes_connectivity/cli.py @@ -17,6 +17,7 @@ import logging from pathlib import Path import re +import shutil import sys # package @@ -33,6 +34,10 @@ _log = logging.getLogger(SCRIPT_NAME) +class MainError(Exception): + pass + + class MermaidHtml(ic.Formatter): def __init__(self, conn, **mmd_opt): self._mmd = ic.Mermaid(conn, **mmd_opt) @@ -69,14 +74,21 @@ def _write_html(self, f): _log.debug(f"_end_ write MermaidJS HTML to '{filename}'") -def infer_output_file(ifile: str, to_, input_file=None): +def infer_output_file(ifile: str, to_, source_type, mermaid_image_fmt=None): to_fmt = OutputFormats(to_) # arg checked already - ext = { - OutputFormats.MERMAID: "mmd", - OutputFormats.CSV: "csv", - OutputFormats.D2: "d2", - }[to_fmt] - i = ifile.rfind(".") + if mermaid_image_fmt: + ext = mermaid_image_fmt + else: + ext = { + OutputFormats.MERMAID: "mmd", + OutputFormats.CSV: "csv", + OutputFormats.D2: "d2", + }[to_fmt] + if source_type == "module": + i = len(ifile) # use whole thing + else: # py or csv + ifile = Path(ifile).name # filename only + i = ifile.rfind(".") if i > 0: filename = ifile[:i] + "." + ext else: @@ -96,25 +108,18 @@ def csv_main(args) -> int: """ _log.info(f"_begin_ create from matrix. args={args}") - if args.ofile is None: - args.ofile = infer_output_file(args.source, args.to) - print(f"Output in: {args.ofile}") - elif args.ofile == CONSOLE: - args.ofile = sys.stdout - fmt_opt = {"stream_labels": args.labels, "direction": args.direction} try: conn = ic.Connectivity(input_file=args.source) - formatter = get_formatter(conn, args.to) + formatter = get_formatter(conn, args.to, args) formatter.write(args.ofile) except (RuntimeError, ic.DataLoadError) as err: - _log.info("_end_ create from matrix (1)") - _log.error(f"{err}") - return 1 + _log.info("_end_ create from matrix status=error") + raise MainError(str(err)) _log.info("_end_ create from matrix") - return 0 + return args.ofile def module_main(args) -> int: @@ -130,15 +135,14 @@ def module_main(args) -> int: options, conn_kw = _code_main(args) try: conn = ic.Connectivity(input_module=args.source, **conn_kw) - formatter = get_formatter(conn, args.to, options) + formatter = get_formatter(conn, args.to, args, options=options) formatter.write(args.ofile) except (RuntimeError, ic.ModelLoadError) as err: - _log.info("_end_ create from Python model (1)") - _log.error(f"{err}") - return 1 + _log.info("_end_ create from Python model status=error") + raise MainError(str(err)) _log.info("_end_ create from Python model") - return 0 + return args.ofile def py_main(args) -> int: @@ -149,39 +153,41 @@ def py_main(args) -> int: Returns: int: Code for sys.exit() """ - _log.info("_begin_ create from Python script") + _log.info( + f"_begin_ create from Python script. source={args.source} ofile={args.ofile}" + ) options, conn_kw = _code_main(args) script_globals = {} + old_argv = sys.argv + sys.argv = [args.source] # , *args.extra_args] try: - with open(args.source, "r") as f: - exec(f.read(), script_globals) - except Exception as err: - _log.info("_end_ create from Python module (1)") - _log.error("Error executing Python file: %s", str(err)) - return -1 - try: - model = eval(args.build, script_globals)() - except Exception as err: - _log.info("_end_ create from Python module (1)") - _log.error("Error evaluating Python file: %s", str(err)) - return -1 - try: - conn = ic.Connectivity(input_model=model, **conn_kw) - formatter = get_formatter(conn, args.to, options) - formatter.write(args.ofile) - except (RuntimeError, ic.ModelLoadError) as err: - _log.info("_end_ create from Python model (1)") - _log.error(f"{err}") - return 1 + try: + with open(args.source, "r") as f: + exec(f.read(), script_globals) + except Exception as err: + _log.info("_end_ create from Python module status=error") + raise MainError(f"Executing Python file {err}") + return -1 + try: + model = eval(args.build, script_globals)() + except Exception as err: + _log.info("_end_ create from Python module status=error") + raise MainError(f"Error evaluating Python file: {err}") + try: + conn = ic.Connectivity(input_model=model, **conn_kw) + formatter = get_formatter(conn, args.to, args, options=options) + formatter.write(args.ofile) + except (RuntimeError, ic.ModelLoadError) as err: + _log.info("_end_ create from Python model status=error") + raise MainError(str(err)) + finally: + sys.argv = old_argv _log.info("_end_ create from Python script") - return 0 + return args.ofile def _code_main(args): - if args.ofile is None or args.ofile == CONSOLE: - args.ofile = sys.stdout - options = {"stream_labels": args.labels, "direction": args.direction} conn_kw = {} if args.fs is not None: @@ -191,7 +197,7 @@ def _code_main(args): return options, conn_kw -def get_formatter(conn: object, fmt: str, options=None) -> ic.Formatter: +def get_formatter(conn: object, fmt: str, args, options=None) -> ic.Formatter: options = {} if options is None else options fmt = fmt.lower().strip() if fmt == OutputFormats.CSV.value: @@ -199,11 +205,22 @@ def get_formatter(conn: object, fmt: str, options=None) -> ic.Formatter: elif fmt == OutputFormats.D2.value: clazz = ic.D2 elif fmt == OutputFormats.MERMAID.value: - clazz = ic.Mermaid + if args.mmdc: + clazz = ic.MermaidImage + mmdc_extra = [] + for key in ("scale",): # allow others later + value = getattr(args, f"image_{key}", None) + if value: + mmdc_extra.append(f"--{key}") + mmdc_extra.append(str(value)) + options["mmdc"] = {"bin": args.mmdc_bin, "options": mmdc_extra} + else: + clazz = ic.Mermaid elif fmt == OutputFormats.HTML.value: clazz = MermaidHtml else: raise ValueError(f"Unrecognized output format: {fmt}") + return clazz(conn, **options) @@ -296,9 +313,6 @@ def _process_log_options(module_name: str, args: argparse.Namespace) -> logging. return log -import shutil - - def _copy_images(): # use importlib so this works on installed wheels, too image_path = imp_files("idaes_connectivity.images") @@ -387,9 +401,26 @@ def main(command_line=None): p.add_argument( "--version", help="Print version number and quit", action="store_true" ) + p.add_argument( + "--png", + action="store_true", + help="Use mermaid-cli tool (mmdc) to generate a PNG image file directly", + ) + p.add_argument( + "--svg", + action="store_true", + help="Use mermaid-cli tool (mmdc) to generate a SVG image file directly", + ) + p.add_argument( + "--image-scale", + help="For Mermaid images, scale diagram size by an integer factor", + default=1, + type=int, + ) _add_log_options(p) + if command_line: - args = p.parse_args(args=command_line) + args = p.parse_args(command_line) else: args = p.parse_args() @@ -415,7 +446,17 @@ def main(command_line=None): server.kill_all() return 0 - # Generate the mermaid diagram + # Process Mermaid image options + args.mmdc, mmd_img_fmt = None, None + if args.png and args.svg: + p.error("Arguments --svg and --png conflict") + if args.png: + # set pseudo-arg 'mmdc' and also image format + args.mmdc, mmd_img_fmt = True, "png" + elif args.svg: + args.mmdc, mmd_img_fmt = True, "svg" + + # Generate the diagram if args.source is None: print("File or module source is required. Try --usage for details.\n") p.print_help() @@ -430,7 +471,7 @@ def main(command_line=None): f"{args.source}" ) return 2 - main_method = csv_main + main_method, source_type = csv_main, "csv" elif args.source.lower().endswith(".py"): path = Path(args.source) if not path.exists(): @@ -439,14 +480,14 @@ def main(command_line=None): f"{args.source}" ) return 2 - main_method = py_main + main_method, source_type = py_main, "py" elif "/" in args.source: path = Path(args.source) if path.exists(): _log.warning( "File path given, but suffix is not .csv; assuming CSV mode" ) - main_method = csv_main + main_method, source_type = csv_main, "csv" else: print(f"Source looks like file path, but does not exist: {args.source}") return 2 @@ -459,7 +500,7 @@ def main(command_line=None): f"Source looks like a module name, but is not valid: {args.source}" ) return 2 - main_method = module_main + main_method, source_type = module_main, "module" else: if args.type == "csv": if not Path(args.source).exists(): @@ -470,14 +511,51 @@ def main(command_line=None): main_method = py_main elif args.type == "module": main_method = module_main + source_type = args.type if args.to is None: - if main_method is csv_main: + if main_method is csv_main or args.mmdc: args.to = OutputFormats.MERMAID.value else: args.to = OutputFormats.CSV.value + if args.mmdc: + if args.to == OutputFormats.MERMAID.value: + mmdc_bin = ic.MermaidImage.find_mmdc() + if mmdc_bin is None: + p.error( + "Could not find command-line program 'mmdc' from mermaid-cli.\n" + "This program can be installed with the node package manager: npm install -g @mermaid-js/mermaid-cli\n" + "For more information, see https://github.com/mermaid-js/mermaid-cli\n" + "For more information about the node package manager (NPM) see https://www.npmjs.com/" + ) - return main_method(args) + else: + p.error( + f"Argument --mmdc only works for MermaidJS output, selected format is '{args.to}'" + ) + # we are doing Mermaid, we have a mmdc_bin, so stash it in the 'args' and carry on + args.mmdc_bin = mmdc_bin + + # figure out output file if not given + if args.ofile is None: + if args.mmdc: + args.ofile = infer_output_file( + args.source, args.to, source_type, mermaid_image_fmt=mmd_img_fmt + ) + else: + args.ofile = sys.stdout + elif args.ofile == CONSOLE: + args.ofile = sys.stdout + + try: + output_file = main_method(args) + except MainError as err: + print(f"Error creating output: {err}") + return 1 + + if output_file is not sys.stdout: + print(f"Output in: {output_file}") + return 0 if __name__ == "__main__": diff --git a/idaes_connectivity/tests/test_cli.py b/idaes_connectivity/tests/test_cli.py index 538d71c..2d47da5 100644 --- a/idaes_connectivity/tests/test_cli.py +++ b/idaes_connectivity/tests/test_cli.py @@ -189,8 +189,11 @@ def test_cli( args.append("--labels") if direction is not None: args.extend(["--direction", direction]) - if verbosity is not None: - args.append(f"-{verbosity}") + + # if verbosity is not None: + # args.append(f"-{verbosity}") + args.append(f"-vv") # XXX + print(f"run cli.main with args: {args}") if return_code == -1: with pytest.raises(SystemExit):