Repository: data-exp-lab/inheritance_explorer (language: python)

Issue:
CLI: add to or delete
`cli.py` exists from the initial cookiecutter. Should delete it or add basic functionality. Only slightly tricky part would be dynamic imports -- maybe a command line argument that is a list of packages to import before running the traversal (e.g., [this SO answer](https://stackoverflow.com/a/301146/9357244)).


Discussion / hints:

Fix patch:
diff --git a/.gitignore b/.gitignore
index c1c2fad..c727aa3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -108,3 +108,6 @@ ENV/
 
 # tempfiles 
 *.swp
+
+# build
+pip-wheel-metadata/*
diff --git a/inheritance_explorer/cli.py b/inheritance_explorer/cli.py
index dbc3736..ccd95a2 100644
--- a/inheritance_explorer/cli.py
+++ b/inheritance_explorer/cli.py
@@ -1,21 +1,59 @@
 """Console script for inheritance_explorer."""
-import argparse
-import sys
+import importlib
+import os
 
+import click
 
-def main():
-    """Console script for inheritance_explorer."""
-    parser = argparse.ArgumentParser()
-    parser.add_argument("_", nargs="*")
-    args = parser.parse_args()
+from inheritance_explorer.inheritance_explorer import ClassGraphTree
 
-    print("Arguments: " + str(args._))
-    print(
-        "Replace this message by putting your code into "
-        "inheritance_explorer.cli.main"
-    )
-    return 0
 
+@click.command()
+@click.argument("module_class", type=str)
+@click.argument("output_file", type=str)
+@click.option(
+    "--output_format",
+    default=None,
+    help="output format string (default is svg, or the file extension of output_file)",
+)
+@click.option(
+    "--import_list", default=None, help="comma-separated list of modules to import"
+)
+@click.option("--funcname", default=None, help="function name to track")
+def map_class(module_class, output_file, output_format, import_list, funcname):
+    """
+    map a class and save the graph to a file. requires graphviz installation.
+    """
+
+    # import the class of interest
+    mod_cls = module_class.split(".")
+    if len(mod_cls) > 2:
+        mod_name = ".".join(mod_cls[0:-1])
+    elif len(mod_cls) < 2:
+        raise ValueError(f"module_class be of the form module.class, found {mod_cls}")
+    else:
+        mod_name = mod_cls[0]
+
+    mod = importlib.import_module(mod_name)
+    cls = getattr(mod, mod_cls[-1])
+
+    # import any other modules that we want in scope
+    if import_list is not None:
+        import_list = [m.strip() for m in import_list.split(",")]
+        modules = map(importlib.__import__, import_list)  # noqa: F841
+
+    if funcname is not None:
+        if hasattr(cls, funcname) is False:
+            raise AttributeError(f"{funcname} is not an attribute of {cls}")
 
-if __name__ == "__main__":
-    sys.exit(main())  # pragma: no cover
+    # now build the graph
+    cgt = ClassGraphTree(cls, funcname=funcname)
+
+    # and save it
+    _, file_extension = os.path.splitext(output_file)
+    fmt = file_extension.replace(".", "")
+    if output_format is None and file_extension == "":
+        fmt = "svg"
+
+    cgt.graph.write(output_file, format=fmt)
+
+    return 0
diff --git a/setup.py b/setup.py
index c8b5053..55b2206 100644
--- a/setup.py
+++ b/setup.py
@@ -40,7 +40,7 @@
     description="A tool for exploring complex inheritance patters in python code",
     entry_points={
         "console_scripts": [
-            "inheritance_explorer=inheritance_explorer.cli:main",
+            "inheritance_explorer=inheritance_explorer.cli:map_class",
         ],
     },
     install_requires=requirements,


Test patch:
diff --git a/tests/test_cli.py b/tests/test_cli.py
new file mode 100644
index 0000000..6a5c865
--- /dev/null
+++ b/tests/test_cli.py
@@ -0,0 +1,42 @@
+import os
+
+import pytest
+from click.testing import CliRunner
+
+from inheritance_explorer.cli import map_class
+
+
+@pytest.mark.parametrize(
+    "extra_args",
+    (
+        ["--import_list", "numpy, collections"],
+        ["--funcname", "clear"],
+        ["--output_format", "png"],
+    ),
+)
+def test_map_class(tmp_path, extra_args):
+    runner = CliRunner()
+
+    if "output_format" in "_".join(extra_args):
+        outfile = str(tmp_path / "test.") + extra_args[-1]
+    else:
+        outfile = str(tmp_path / "test.svg")
+
+    result = runner.invoke(map_class, ["matplotlib.axes.Axes", outfile] + extra_args)
+    assert result.exit_code == 0
+    assert os.path.isfile(outfile)
+
+
+def test_map_class_no_format(tmp_path):
+    runner = CliRunner()
+    outfile = str(tmp_path / "test")
+    result = runner.invoke(map_class, ["matplotlib.axes.Axes", outfile])
+    assert result.exit_code == 0
+    assert os.path.isfile(outfile)
+
+
+def test_map_class_bad():
+    runner = CliRunner()
+    arg_list = ["numpy.float64", "whatever.png", "--funcname", "notafunc"]
+    result = runner.invoke(map_class, arg_list, catch_exceptions=AttributeError)
+    assert isinstance(result.exception, AttributeError)