pyedit.py 19 KB

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