pyedit.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555
  1. import sys
  2. import os
  3. import stat
  4. from StringIO import StringIO
  5. import time
  6. import wx
  7. import grass.script as gscript
  8. from grass.script.utils import try_remove
  9. # just for testing
  10. if __name__ == '__main__':
  11. from grass.script.setup import set_gui_path
  12. set_gui_path()
  13. from core.utils import _
  14. from core.gcmd import EncodeString, GError
  15. from core.giface import StandaloneGrassInterface
  16. from gui_core.pystc import PyStc
  17. from core import globalvar
  18. from core.menutree import MenuTreeModelBuilder
  19. from gui_core.menu import Menu
  20. from gui_core.toolbars import BaseToolbar, BaseIcons
  21. from icons.icon import MetaIcon
  22. # TODO: add validation: call/import pep8 (error message if not available)
  23. # TODO: run with parameters
  24. # TODO: run with overwrite (in process env, not os.environ)
  25. # TODO: add more examples (separate file)
  26. # TODO: add test for templates and examples
  27. # TODO: add pep8 test for templates and examples
  28. # TODO: add snippets?
  29. def script_template():
  30. """The most simple script which runs and gives something"""
  31. return r"""#!/usr/bin/env python
  32. import grass.script as gscript
  33. def main():
  34. gscript.run_command('g.region', flags='p')
  35. if __name__ == '__main__':
  36. main()
  37. """
  38. def module_template():
  39. """Template from which to start writing GRASS module"""
  40. import getpass
  41. author = getpass.getuser()
  42. properties = {}
  43. properties['name'] = 'module name'
  44. properties['author'] = author
  45. properties['description'] = 'Module description'
  46. output = StringIO()
  47. # header
  48. output.write(
  49. r"""#!/usr/bin/env python
  50. #
  51. #%s
  52. #
  53. # MODULE: %s
  54. #
  55. # AUTHOR(S): %s
  56. #
  57. # PURPOSE: %s
  58. #
  59. # DATE: %s
  60. #
  61. #%s
  62. """ % ('#' * 72,
  63. EncodeString(properties['name']),
  64. EncodeString(properties['author']),
  65. EncodeString('\n# '.join(properties['description'].splitlines())),
  66. time.asctime(),
  67. '#' * 72))
  68. # UI
  69. output.write(
  70. r"""
  71. #%%module
  72. #%% description: %s
  73. #%%end
  74. """ % (EncodeString(' '.join(properties['description'].splitlines()))))
  75. # import modules
  76. output.write(
  77. r"""
  78. import sys
  79. import os
  80. import atexit
  81. import grass.script as gscript
  82. """)
  83. # cleanup()
  84. output.write(
  85. r"""
  86. RAST_REMOVE = []
  87. def cleanup():
  88. """)
  89. output.write(
  90. r""" gscript.run_command('g.remove', flags='f', type='raster',
  91. name=RAST_REMOVE)
  92. """)
  93. output.write("\ndef main():\n")
  94. output.write(
  95. r""" options, flags = gscript.parser()
  96. gscript.run_command('g.remove', flags='f', type='raster',
  97. name=RAST_REMOVE)
  98. """)
  99. output.write("\n return 0\n")
  100. output.write(
  101. r"""
  102. if __name__ == "__main__":
  103. atexit.register(cleanup)
  104. sys.exit(main())
  105. """)
  106. return output.getvalue()
  107. def script_example():
  108. """Example of a simple script"""
  109. return r"""#!/usr/bin/env python
  110. import grass.script as gscript
  111. def main():
  112. input_raster = 'elevation'
  113. output_raster = 'high_areas'
  114. stats = gscript.parse_command('r.univar', map='elevation', flags='g')
  115. raster_mean = float(stats['mean'])
  116. raster_stddev = float(stats['stddev'])
  117. raster_high = raster_mean + raster_stddev
  118. gscript.mapcalc('{r} = {a} > {m}'.format(r=output_raster, a=input_raster,
  119. m=raster_high))
  120. if __name__ == "__main__":
  121. main()
  122. """
  123. def module_example():
  124. """Example of a GRASS module"""
  125. return r"""#!/usr/bin/env python
  126. #%module
  127. #% description: Adds the values of two rasters (A + B)
  128. #% keyword: raster
  129. #% keyword: algebra
  130. #% keyword: sum
  131. #%end
  132. #%option G_OPT_R_INPUT
  133. #% key: araster
  134. #% description: Name of input raster A in an expression A + B
  135. #%end
  136. #%option G_OPT_R_INPUT
  137. #% key: braster
  138. #% description: Name of input raster B in an expression A + B
  139. #%end
  140. #%option G_OPT_R_OUTPUT
  141. #%end
  142. import sys
  143. import grass.script as gscript
  144. def main():
  145. options, flags = gscript.parser()
  146. araster = options['araster']
  147. braster = options['braster']
  148. output = options['output']
  149. gscript.mapcalc('{r} = {a} + {b}'.format(r=output, a=araster, b=braster))
  150. return 0
  151. if __name__ == "__main__":
  152. sys.exit(main())
  153. """
  154. class PyEditController(object):
  155. # using the naming GUI convention, change for controller?
  156. # pylint: disable=invalid-name
  157. def __init__(self, panel, guiparent, giface):
  158. """Simple editor, this class could be a pure controller"""
  159. self.guiparent = guiparent
  160. self.giface = giface
  161. self.body = panel
  162. self.filename = None
  163. self.tempfile = None # bool, make them strings for better code
  164. self.running = False
  165. def OnRun(self, event):
  166. """Run Python script"""
  167. if self.running:
  168. # ignore when already running
  169. return
  170. if not self.filename:
  171. self.filename = gscript.tempfile()
  172. self.tempfile = True
  173. try:
  174. fd = open(self.filename, "w")
  175. fd.write(self.body.GetText())
  176. except IOError as e:
  177. GError(_("Unable to launch Python script. %s") % e,
  178. parent=self.guiparent)
  179. return
  180. finally:
  181. fd.close()
  182. mode = stat.S_IMODE(os.lstat(self.filename)[stat.ST_MODE])
  183. os.chmod(self.filename, mode | stat.S_IXUSR)
  184. else:
  185. fd = open(self.filename, "w")
  186. try:
  187. fd.write(self.body.GetText())
  188. finally:
  189. fd.close()
  190. # set executable file
  191. # (not sure if needed every time but useful for opened files)
  192. os.chmod(self.filename, stat.S_IRWXU | stat.S_IWUSR)
  193. # TODO: add overwrite to toolbar, needs env in GConsole
  194. # run in console as other modules, avoid Python shell which
  195. # carries variables over to the next execution
  196. self.giface.RunCmd([fd.name], skipInterface=True, onDone=self.OnDone)
  197. self.running = True
  198. def OnDone(self, event):
  199. """Python script finished"""
  200. if self.tempfile:
  201. try_remove(self.filename)
  202. self.filename = None
  203. self.running = False
  204. def SaveAs(self):
  205. """Save python script to file"""
  206. filename = ''
  207. dlg = wx.FileDialog(parent=self.guiparent,
  208. message=_("Choose file to save"),
  209. defaultDir=os.getcwd(),
  210. wildcard=_("Python script (*.py)|*.py"),
  211. style=wx.FD_SAVE)
  212. if dlg.ShowModal() == wx.ID_OK:
  213. filename = dlg.GetPath()
  214. if not filename:
  215. return
  216. # check for extension
  217. if filename[-3:] != ".py":
  218. filename += ".py"
  219. if os.path.exists(filename):
  220. dlg = wx.MessageDialog(
  221. parent=self.guiparent,
  222. message=_("File <%s> already exists. "
  223. "Do you want to overwrite this file?") % filename,
  224. caption=_("Save file"),
  225. style=wx.YES_NO | wx.YES_DEFAULT | wx.ICON_QUESTION)
  226. if dlg.ShowModal() == wx.ID_NO:
  227. dlg.Destroy()
  228. return
  229. dlg.Destroy()
  230. self.filename = filename
  231. self.tempfile = False
  232. self.Save()
  233. def Save(self):
  234. """Save current content to a file and set executable permissions"""
  235. assert self.filename
  236. fd = open(self.filename, "w")
  237. try:
  238. fd.write(self.body.GetText())
  239. finally:
  240. fd.close()
  241. # executable file
  242. os.chmod(self.filename, stat.S_IRWXU | stat.S_IWUSR)
  243. def OnSave(self, event):
  244. """Save python script to file
  245. Just save if file already specified, save as action otherwise.
  246. """
  247. if self.filename:
  248. self.Save()
  249. else:
  250. self.SaveAs()
  251. # TODO: it should be probably used with replacing, when this gives what we want?
  252. def IsModified(self):
  253. """Check if python script has been modified"""
  254. return self.body.modified
  255. def Open(self):
  256. """Ask for a filename and load its content"""
  257. filename = ''
  258. dlg = wx.FileDialog(parent=self.guiparent,
  259. message=_("Open file"),
  260. defaultDir=os.getcwd(),
  261. wildcard=_("Python script (*.py)|*.py"),
  262. style=wx.OPEN)
  263. if dlg.ShowModal() == wx.ID_OK:
  264. filename = dlg.GetPath()
  265. if not filename:
  266. return
  267. fd = open(filename, "r")
  268. try:
  269. self.body.SetText(fd.read())
  270. finally:
  271. fd.close()
  272. self.filename = filename
  273. self.tempfile = False
  274. def OnOpen(self, event):
  275. if self.CanReplaceContent('file'):
  276. self.Open()
  277. def IsEmpty(self):
  278. """Check if python script is empty"""
  279. return len(self.body.GetText()) == 0
  280. def SetScriptTemplate(self, event):
  281. if self.CanReplaceContent('template'):
  282. self.body.SetText(script_template())
  283. def SetModuleTemplate(self, event):
  284. if self.CanReplaceContent('template'):
  285. self.body.SetText(module_template())
  286. def SetScriptExample(self, event):
  287. if self.CanReplaceContent('template'):
  288. self.body.SetText(script_example())
  289. def SetModuleExample(self, event):
  290. if self.CanReplaceContent('template'):
  291. self.body.SetText(module_example())
  292. def CanReplaceContent(self, by_message):
  293. if by_message == 'template':
  294. message = _("Replace the content by the template?")
  295. elif by_message == 'file':
  296. message = _("Replace the current content by the file content?")
  297. else:
  298. message = by_message
  299. if not self.IsEmpty():
  300. dlg = wx.MessageDialog(
  301. parent=self.guiparent, message=message,
  302. caption=_("Replace content"),
  303. style=wx.YES_NO | wx.YES_DEFAULT | wx.ICON_QUESTION)
  304. if dlg.ShowModal() == wx.ID_NO:
  305. dlg.Destroy()
  306. return False
  307. dlg.Destroy()
  308. return True
  309. def OnHelp(self, event):
  310. import webbrowser
  311. # inspired by g.manual but simple not using GRASS_HTML_BROWSER
  312. # not using g.manual because it does not show
  313. entry = 'libpython/script_intro.html'
  314. major, minor, patch = gscript.version()['version'].split('.')
  315. url = 'http://grass.osgeo.org/grass%s%s/manuals/%s' % (
  316. major, minor, entry)
  317. webbrowser.open(url)
  318. def OnPythonHelp(self, event):
  319. import webbrowser
  320. url = 'https://docs.python.org/%s/tutorial/' % sys.version_info[0]
  321. webbrowser.open(url)
  322. def OnModulesHelp(self, event):
  323. self.giface.Help('full_index')
  324. def OnAddonsHelp(self, event):
  325. import webbrowser
  326. url = 'https://grass.osgeo.org/development/code-submission/'
  327. webbrowser.open(url)
  328. def OnSupport(self, event):
  329. import webbrowser
  330. url = 'https://grass.osgeo.org/support/'
  331. webbrowser.open(url)
  332. class PyEditToolbar(BaseToolbar):
  333. # GUI class
  334. # pylint: disable=too-many-ancestors
  335. # pylint: disable=too-many-public-methods
  336. """PyEdit toolbar"""
  337. def __init__(self, parent):
  338. BaseToolbar.__init__(self, parent)
  339. # workaround for http://trac.wxwidgets.org/ticket/13888
  340. if sys.platform == 'darwin':
  341. parent.SetToolBar(self)
  342. self.InitToolbar(self._toolbarData())
  343. # realize the toolbar
  344. self.Realize()
  345. def _toolbarData(self):
  346. """Toolbar data"""
  347. icons = {
  348. 'open': MetaIcon(img='open',
  349. label=_('Open (Ctrl+O)')),
  350. 'save': MetaIcon(img='save',
  351. label=_('Save (Ctrl+S)')),
  352. 'run': MetaIcon(img='execute',
  353. label=_('Run (Ctrl+R')),
  354. }
  355. return self._getToolbarData((('open', icons['open'],
  356. self.parent.OnOpen),
  357. ('save', icons['save'],
  358. self.parent.OnSave),
  359. (None, ),
  360. ('run', icons['run'],
  361. self.parent.OnRun),
  362. (None, ),
  363. ("help", BaseIcons['help'],
  364. self.parent.OnHelp),
  365. ))
  366. class PyEditFrame(wx.Frame):
  367. # GUI class and a lot of trampoline methods
  368. # pylint: disable=missing-docstring
  369. # pylint: disable=too-many-public-methods
  370. # pylint: disable=invalid-name
  371. def __init__(self, parent, giface, id=wx.ID_ANY,
  372. title=_("GRASS GIS Simple Python Editor"),
  373. **kwargs):
  374. wx.Frame.__init__(self, parent=parent, id=id, title=title, **kwargs)
  375. self.parent = parent
  376. filename = os.path.join(
  377. globalvar.WXGUIDIR, 'xml', 'menudata_pyedit.xml')
  378. self.menubar = Menu(
  379. parent=self,
  380. model=MenuTreeModelBuilder(filename).GetModel(separators=True))
  381. self.SetMenuBar(self.menubar)
  382. self.toolbar = PyEditToolbar(parent=self)
  383. # workaround for http://trac.wxwidgets.org/ticket/13888
  384. # TODO: toolbar is set in toolbar and here
  385. if sys.platform != 'darwin':
  386. self.SetToolBar(self.toolbar)
  387. self.panel = PyStc(parent=self)
  388. self.controller = PyEditController(
  389. panel=self.panel, guiparent=self, giface=giface)
  390. # don't start with an empty page
  391. self.panel.SetText(script_template())
  392. sizer = wx.BoxSizer(wx.VERTICAL)
  393. sizer.Add(item=self.panel, proportion=1,
  394. flag=wx.EXPAND)
  395. sizer.Fit(self)
  396. sizer.SetSizeHints(self)
  397. self.SetSizer(sizer)
  398. self.Fit()
  399. self.SetAutoLayout(True)
  400. self.Layout()
  401. # TODO: it would be nice if we can pass the controller to the menu
  402. # might not be possible on the side of menu
  403. # here we use self and self.controller which might make it harder
  404. def OnOpen(self, *args, **kwargs):
  405. self.controller.OnOpen(*args, **kwargs)
  406. def OnSave(self, *args, **kwargs):
  407. self.controller.OnSave(*args, **kwargs)
  408. def OnClose(self, *args, **kwargs):
  409. # saves without asking if we have an open file
  410. self.controller.OnSave(*args, **kwargs)
  411. self.Destroy()
  412. def OnRun(self, *args, **kwargs):
  413. # save without asking
  414. self.controller.OnRun(*args, **kwargs)
  415. def OnHelp(self, *args, **kwargs):
  416. # save without asking
  417. self.controller.OnHelp(*args, **kwargs)
  418. def OnSimpleScriptTemplate(self, *args, **kwargs):
  419. self.controller.SetScriptTemplate(*args, **kwargs)
  420. def OnGrassModuleTemplate(self, *args, **kwargs):
  421. self.controller.SetModuleTemplate(*args, **kwargs)
  422. def OnSimpleScriptExample(self, *args, **kwargs):
  423. self.controller.SetScriptExample(*args, **kwargs)
  424. def OnGrassModuleExample(self, *args, **kwargs):
  425. self.controller.SetModuleExample(*args, **kwargs)
  426. def OnPythonHelp(self, *args, **kwargs):
  427. self.controller.OnPythonHelp(*args, **kwargs)
  428. def OnModulesHelp(self, *args, **kwargs):
  429. self.controller.OnModulesHelp(*args, **kwargs)
  430. def OnAddonsHelp(self, *args, **kwargs):
  431. self.controller.OnAddonsHelp(*args, **kwargs)
  432. def OnSupport(self, *args, **kwargs):
  433. self.controller.OnSupport(*args, **kwargs)
  434. def main():
  435. """Test application (potentially useful as g.gui.pyedit)"""
  436. app = wx.App()
  437. giface = StandaloneGrassInterface()
  438. simple_editor = PyEditFrame(parent=None, giface=giface)
  439. simple_editor.SetSize((600, 800))
  440. simple_editor.Show()
  441. app.MainLoop()
  442. if __name__ == '__main__':
  443. main()