pyedit.py 16 KB

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