浏览代码

jupyter: Add `__getattr__` shortcut for calling GRASS display modules (#1723)

Shortcut allowing users to call GRASS display modules (d.*) with method .d_*.
Method name must start with d and underscore and it needs to be an executable on path,
other error handling is left to the underlying functions. Example is in the Jupyter integration notebook.
Caitlin H 3 年之前
父节点
当前提交
fd19e91c70
共有 2 个文件被更改,包括 64 次插入6 次删除
  1. 12 1
      doc/notebooks/jupyter_integration.ipynb
  2. 52 5
      python/grass/jupyter/display.py

+ 12 - 1
doc/notebooks/jupyter_integration.ipynb

@@ -90,11 +90,19 @@
    "source": [
     "# Demonstration of GrassRenderer for non-interactive map display\n",
     "\n",
+    "# Create an instance of GrassRenderer\n",
     "r = gj.GrassRenderer(height=540, filename = \"streams_maps.png\")\n",
     "\n",
+    "# Add a raster and vector to the map\n",
     "r.run(\"d.rast\", map=\"elevation\")\n",
     "r.run(\"d.vect\", map=\"streams\")\n",
     "\n",
+    "# We can also call 'd.*' display modules with a shortcut\n",
+    "# Shortcut methods must be in the form '.d_{name_of_module}'\n",
+    "# For example, let's add a legend using a shortcut\n",
+    "r.d_legend(raster=\"elevation\", at=(55, 95, 2, 6), flags=\"b\") # shortcut for calling \"d.legend\"\n",
+    "\n",
+    "# Display map\n",
     "r.show()"
    ]
   },
@@ -102,7 +110,9 @@
    "cell_type": "markdown",
    "metadata": {},
    "source": [
-    "#### We can have two instances of GrassRenderer"
+    "#### Multiple instances of GrassRenderer\n",
+    "\n",
+    "We can have multiple instances of GrassRenderer."
    ]
   },
   {
@@ -128,6 +138,7 @@
    "outputs": [],
    "source": [
     "# Then, we return to the first instance and continue to modify and display it\n",
+    "# Notice that layers a drawn in the order they are added\n",
     "\n",
     "r.run(\"d.vect\", map = \"zipcodes\", color=\"red\", fill_color=\"none\")\n",
     "\n",

+ 52 - 5
python/grass/jupyter/display.py

@@ -12,19 +12,46 @@
 #           for details.
 
 import os
+import shutil
 from pathlib import Path
 from IPython.display import Image
 import grass.script as gs
 
 
 class GrassRenderer:
-    """The grassRenderer class creates and displays GRASS maps in
-    Jupyter Notebooks."""
+    """GrassRenderer creates and displays GRASS maps in
+    Jupyter Notebooks.
+
+    Elements are added to the display by calling GRASS display modules.
+
+    Basic usage::
+    >>> m = GrassRenderer()
+    >>> m.run("d.rast", map="elevation")
+    >>> m.run("d.legend", raster="elevation")
+    >>> m.show()
+
+    GRASS display modules can also be called by using the name of module
+    as a class method and replacing "." with "_" in the name.
+
+    Shortcut usage::
+    >>> m = GrassRenderer()
+    >>> m.d_rast(map="elevation")
+    >>> m.d_legend(raster="elevation")
+    >>> m.show()
+    """
 
     def __init__(
         self, env=None, width=600, height=400, filename="map.png", text_size=12
     ):
-        """Initiates an instance of the GrassRenderer class."""
+        """Creates an instance of the GrassRenderer class.
+
+        :param int height: height of map in pixels
+        :param int width: width of map in pixels
+        :param str filename: filename or path to save a PNG of map
+        :param str env: environment
+        :param int text_size: default text size, overwritten by most display modules
+        :param renderer: GRASS renderer driver (options: cairo, png, ps, html)
+        """
 
         if env:
             self._env = env.copy()
@@ -33,7 +60,7 @@ class GrassRenderer:
 
         self._env["GRASS_RENDER_WIDTH"] = str(width)
         self._env["GRASS_RENDER_HEIGHT"] = str(height)
-        self._env["GRASS_TEXT_SIZE"] = str(text_size)
+        self._env["GRASS_RENDER_TEXT_SIZE"] = str(text_size)
         self._env["GRASS_RENDER_IMMEDIATE"] = "cairo"
         self._env["GRASS_RENDER_FILE"] = str(filename)
         self._env["GRASS_RENDER_FILE_READ"] = "TRUE"
@@ -53,6 +80,26 @@ class GrassRenderer:
         else:
             raise ValueError("Module must begin with letter 'd'.")
 
+    def __getattr__(self, name):
+        """Parse attribute to GRASS display module. Attribute should be in
+        the form 'd_module_name'. For example, 'd.rast' is called with 'd_rast'.
+        """
+
+        # Check to make sure format is correct
+        if not name.startswith("d_"):
+            raise AttributeError(_("Module must begin with 'd_'"))
+        # Reformat string
+        grass_module = name.replace("_", ".")
+        # Assert module exists
+        if not shutil.which(grass_module):
+            raise AttributeError(_("Cannot find GRASS module {}").format(grass_module))
+
+        def wrapper(**kwargs):
+            # Run module
+            self.run(grass_module, **kwargs)
+
+        return wrapper
+
     def show(self):
-        """Displays a PNG image of the map (non-interactive)"""
+        """Displays a PNG image of the map"""
         return Image(self._filename)