setup.py 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172
  1. # MODULE: grass.jupyter.setup
  2. #
  3. # AUTHOR(S): Caitlin Haedrich <caitlin DOT haedrich AT gmail>
  4. # Vaclav Petras <wenzeslaus gmail com>
  5. #
  6. # PURPOSE: This module contains functions for launching a GRASS session
  7. # in Jupyter Notebooks
  8. #
  9. # COPYRIGHT: (C) 2021-2022 Caitlin Haedrich, and by the GRASS Development Team
  10. #
  11. # This program is free software under the GNU General Public
  12. # License (>=v2). Read the file COPYING that comes with GRASS
  13. # for details.
  14. """Initialization GRASS GIS session and its finalization"""
  15. import os
  16. import weakref
  17. import grass.script as gs
  18. import grass.script.setup as gsetup
  19. def _set_notebook_defaults():
  20. """Set defaults appropriate for Jupyter Notebooks.
  21. This function sets several GRASS environment variables that are
  22. important for GRASS to run smoothly in Jupyter.
  23. It also allows GRASS to overwrite existing maps of the same name.
  24. """
  25. # We want functions to raise exceptions and see standard output of
  26. # the modules in the notebook.
  27. gs.set_raise_on_error(True)
  28. gs.set_capture_stderr(True)
  29. # Allow overwrite of existing maps
  30. os.environ["GRASS_OVERWRITE"] = "1"
  31. class _JupyterGlobalSession:
  32. """Represents a global GRASS session for Jupyter Notebooks.
  33. Do not create objects of this class directly. Use the standalone *init* function
  34. and an object will be returned to you, e.g.:
  35. >>> import grass.jupyter as gj
  36. >>> session = gj.init(...)
  37. An object ends the session when it is destroyed or when the *finish* method is
  38. called explicitely.
  39. Notably, only the mapset is closed, but the libraries and GRASS modules
  40. remain on path.
  41. """
  42. def __init__(self):
  43. self._finalizer = weakref.finalize(self, gsetup.finish)
  44. def switch_mapset(self, path, location=None, mapset=None):
  45. """Switch to a mapset provided as a name or path.
  46. The mapset can be provided as a name, as a path,
  47. or as database, location, and mapset.
  48. Specifically, the *path* positional-only parameter can be either
  49. name of a mapset in the current location or a full path to a mapset.
  50. When location and mapset are provided using the additional parameters,
  51. the *path* parameter is path to a database.
  52. Raises ValueError if the mapset does not exist (e.g., when the name is
  53. misspelled or the mapset is invalid).
  54. """
  55. # The method could be a function, but this is more general (would work even for
  56. # a non-global session).
  57. # pylint: disable=no-self-use
  58. # Functions needed only here.
  59. # pylint: disable=import-outside-toplevel
  60. from grass.grassdb.checks import (
  61. get_mapset_invalid_reason,
  62. is_mapset_valid,
  63. mapset_exists,
  64. )
  65. from grass.grassdb.manage import resolve_mapset_path
  66. # For only one parameter, try if it is a mapset in the current location to
  67. # support switching only by its name.
  68. gisenv = gs.gisenv()
  69. if (
  70. not location
  71. and not mapset
  72. and mapset_exists(
  73. path=gisenv["GISDBASE"], location=gisenv["LOCATION_NAME"], mapset=path
  74. )
  75. ):
  76. gs.run_command("g.gisenv", set=f"MAPSET={path}")
  77. return
  78. mapset_path = resolve_mapset_path(path=path, location=location, mapset=mapset)
  79. if not is_mapset_valid(mapset_path):
  80. raise ValueError(
  81. _("Mapset {path} is not valid: {reason}").format(
  82. path=mapset_path.path,
  83. reason=get_mapset_invalid_reason(
  84. mapset_path.directory, mapset_path.location, mapset_path.mapset
  85. ),
  86. )
  87. )
  88. # This requires direct session file modification using g.gisenv because
  89. # g.mapset locks the mapset which is not how init and finish behave.
  90. # For code simplicity, we just change all even when only mapset is changed.
  91. gs.run_command("g.gisenv", set=f"GISDBASE={mapset_path.directory}")
  92. gs.run_command("g.gisenv", set=f"LOCATION_NAME={mapset_path.location}")
  93. gs.run_command("g.gisenv", set=f"MAPSET={mapset_path.mapset}")
  94. def finish(self):
  95. """Close the session, i.e., close the open mapset.
  96. Subsequent calls to GRASS GIS modules will fail because there will be
  97. no current (open) mapset anymore.
  98. The finish procedure is done automatically when process finishes or the object
  99. is destroyed.
  100. """
  101. self._finalizer()
  102. @property
  103. def active(self):
  104. """True unless the session was finalized (e.g., with the *finish* function)"""
  105. return self._finalizer.alive
  106. # Pylint 2.12.2 identifies this a constant (although it is not), so it wants uppercase.
  107. _global_session_handle = None # pylint: disable=invalid-name
  108. def init(path, location=None, mapset=None, grass_path=None):
  109. """Initiates a GRASS session and sets GRASS environment variables.
  110. Calling this function returns an object which represents the session.
  111. >>> import grass.jupyter as gj
  112. >>> session = gj.init(...)
  113. The session is ended when `session.finish` is called or when the object is
  114. destroyed when kernel ends or restarts. This function returns a copy of an
  115. internally kept reference, so the return value can be safely ignored when not
  116. needed.
  117. The returned object can be used to switch to another mapset:
  118. >>> session.switch_mapset("mapset_name")
  119. Subsequent calls to the *init* function result in switching the mapset if
  120. a session is active and result in creation of new session if it is not active.
  121. On the other hand, if you see ``GISRC - variable not set`` after calling
  122. a GRASS module, you know you don't have an active GRASS session.
  123. :param str path: path to GRASS mapset or database
  124. :param str location: name of GRASS location within the database
  125. :param str mapset: name of mapset within location
  126. """
  127. global _global_session_handle # pylint: disable=global-statement,invalid-name
  128. if not _global_session_handle or not _global_session_handle.active:
  129. # Create a GRASS session.
  130. gsetup.init(path, location=location, mapset=mapset, grass_path=grass_path)
  131. # Set defaults for environmental variables and library.
  132. _set_notebook_defaults()
  133. _global_session_handle = _JupyterGlobalSession()
  134. else:
  135. _global_session_handle.switch_mapset(path, location=location, mapset=mapset)
  136. return _global_session_handle