module.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486
  1. # -*- coding: utf-8 -*-
  2. """
  3. Created on Tue Apr 2 18:41:27 2013
  4. @author: pietro
  5. @code
  6. >>> import grass.pygrass.modules as pymod
  7. >>> import copy
  8. >>> region = pymod.Module("g.region")
  9. >>> region.flags["p"].value = True
  10. >>> region.flags["3"].value = True
  11. >>> region.get_bash()
  12. 'g.region -p -3'
  13. >>> new_region = copy.deepcopy(region)
  14. >>> new_region.inputs["res"].value = "10"
  15. >>> new_region.get_bash()
  16. 'g.region res=10 -p -3'
  17. >>> neighbors = pymod.Module("r.neighbors")
  18. >>> neighbors.inputs["input"].value = "mapA"
  19. >>> neighbors.outputs["output"].value = "mapB"
  20. >>> neighbors.inputs["size"].value = 5
  21. >>> neighbors.get_bash()
  22. 'r.neighbors input=mapA method=average size=5 quantile=0.5 output=mapB'
  23. >>> new_neighbors1 = copy.deepcopy(neighbors)
  24. >>> new_neighbors1.inputs["input"].value = "mapD"
  25. >>> new_neighbors1.inputs["size"].value = 3
  26. >>> new_neighbors1.get_bash()
  27. 'r.neighbors input=mapD method=average size=3 quantile=0.5 output=mapB'
  28. >>> new_neighbors2 = copy.deepcopy(neighbors)
  29. >>> new_neighbors2(input="mapD", size=3, run_=False)
  30. >>> new_neighbors2.get_bash()
  31. 'r.neighbors input=mapD method=average size=3 quantile=0.5 output=mapB'
  32. >>> neighbors = pymod.Module("r.neighbors")
  33. >>> neighbors.get_bash()
  34. 'r.neighbors method=average size=3 quantile=0.5'
  35. >>> new_neighbors3 = copy.deepcopy(neighbors)
  36. >>> new_neighbors3(input="mapA", size=3, output="mapB", run_=False)
  37. >>> new_neighbors3.get_bash()
  38. 'r.neighbors input=mapA method=average size=3 quantile=0.5 output=mapB'
  39. @endcode
  40. """
  41. from __future__ import print_function
  42. import subprocess
  43. from itertools import izip_longest
  44. from xml.etree.ElementTree import fromstring
  45. import time
  46. import sys
  47. from grass.pygrass.errors import GrassError, ParameterError
  48. from parameter import Parameter
  49. from flag import Flag
  50. from typedict import TypeDict
  51. from read import GETFROMTAG, DOC
  52. class ParallelModuleQueue(object):
  53. """!This class is designed to run an arbitrary number of pygrass Module
  54. processes in parallel.
  55. Objects of type grass.pygrass.modules.Module can be put into the
  56. queue using put() method. When the queue is full with the maximum
  57. number of parallel processes it will wait for all processes to finish,
  58. sets the stdout and stderr of the Module object and removes it
  59. from the queue when its finished.
  60. This class will raise a GrassError in case a Module process exits
  61. with a return code other than 0.
  62. Usage:
  63. @code
  64. >>> import copy
  65. >>> import grass.pygrass.modules as pymod
  66. >>> mapcalc_list = []
  67. >>> mapcalc = pymod.Module("r.mapcalc",
  68. ... overwrite=True,
  69. ... run_=False,
  70. ... finish_=False)
  71. >>> queue = pymod.ParallelModuleQueue(max_num_procs=3)
  72. >>> for i in xrange(5):
  73. ... new_mapcalc = copy.deepcopy(mapcalc)
  74. ... mapcalc_list.append(new_mapcalc)
  75. ... new_mapcalc(expression="test_pygrass_%i = %i"%(i, i))
  76. ... queue.put(new_mapcalc)
  77. >>> queue.wait()
  78. >>> for mapcalc in mapcalc_list:
  79. ... print(mapcalc.popen.returncode)
  80. 0
  81. 0
  82. 0
  83. 0
  84. 0
  85. @endcode
  86. """
  87. def __init__(self, max_num_procs=1):
  88. """!Constructor
  89. @param max_num_procs The maximum number of Module processes that
  90. can be run in parallel
  91. """
  92. self._num_procs = int(max_num_procs)
  93. self._list = int(max_num_procs) * [None]
  94. self._proc_count = 0
  95. def put(self, module):
  96. """!Put the next Module object in the queue
  97. To run the Module objects in parallel the run_ and finish_ options
  98. of the Module must be set to False.
  99. @param module A preconfigured Module object with run_ and finish_
  100. set to False
  101. """
  102. self._list[self._proc_count] = module
  103. self._list[self._proc_count].run()
  104. self._proc_count += 1
  105. if self._proc_count == self._num_procs:
  106. self.wait()
  107. def get(self, num):
  108. """!Get a Module object from the queue
  109. @param num The number of the object in queue
  110. @return The Module object or None if num is not in the queue
  111. """
  112. if num < self._num_procs:
  113. return self._list[num]
  114. return None
  115. def get_num_run_procs(self):
  116. """!Get the number of Module processes that are in the queue running
  117. or finished
  118. @return The maximum number fo Module processes running/finished in
  119. the queue
  120. """
  121. return len(self._list)
  122. def get_max_num_procs(self):
  123. """!Return the maximum number of parallel Module processes
  124. """
  125. return self._num_procs
  126. def set_max_num_procs(self, max_num_procs):
  127. """!Set the maximum number of Module processes that should run
  128. in parallel
  129. """
  130. self._num_procs = int(max_num_procs)
  131. self.wait()
  132. def wait(self):
  133. """!Wait for all Module processes that are in the list to finish
  134. and set the modules stdout and stderr output options
  135. """
  136. for proc in self._list:
  137. if proc:
  138. stdout, stderr = proc.popen.communicate(input=proc.stdin)
  139. proc.outputs['stdout'].value = stdout if stdout else ''
  140. proc.outputs['stderr'].value = stderr if stderr else ''
  141. if proc.popen.returncode != 0:
  142. GrassError(("Error running module %s") % (proc.name))
  143. self._list = self._num_procs * [None]
  144. self._proc_count = 0
  145. class Module(object):
  146. """
  147. Python allow developers to not specify all the arguments and
  148. keyword arguments of a method or function.
  149. ::
  150. def f(*args):
  151. for arg in args:
  152. print arg
  153. therefore if we call the function like: ::
  154. >>> f('grass', 'gis', 'modules')
  155. grass
  156. gis
  157. modules
  158. or we can define a new list: ::
  159. >>> words = ['grass', 'gis', 'modules']
  160. >>> f(*words)
  161. grass
  162. gis
  163. modules
  164. we can do the same with keyword arguments, rewrite the above function: ::
  165. def f(*args, **kargs):
  166. for arg in args:
  167. print arg
  168. for key, value in kargs.items():
  169. print "%s = %r" % (key, value)
  170. now we can use the new function, with: ::
  171. >>> f('grass', 'gis', 'modules', os = 'linux', language = 'python')
  172. grass
  173. gis
  174. modules
  175. os = 'linux'
  176. language = 'python'
  177. or, as before we can, define a dictionary and give the dictionary to
  178. the function, like: ::
  179. >>> keywords = {'os' : 'linux', 'language' : 'python'}
  180. >>> f(*words, **keywords)
  181. grass
  182. gis
  183. modules
  184. os = 'linux'
  185. language = 'python'
  186. In the Module class we heavily use this language feature to pass arguments
  187. and keyword arguments to the grass module.
  188. """
  189. def __init__(self, cmd, *args, **kargs):
  190. self.name = cmd
  191. try:
  192. # call the command with --interface-description
  193. get_cmd_xml = subprocess.Popen([cmd, "--interface-description"],
  194. stdout=subprocess.PIPE,
  195. shell=(sys.platform == "win32"))
  196. except OSError as e:
  197. print("OSError error({0}): {1}".format(e.errno, e.strerror))
  198. str_err = "Error running: `%s --interface-description`."
  199. raise GrassError(str_err % self.name)
  200. # get the xml of the module
  201. self.xml = get_cmd_xml.communicate()[0]
  202. # transform and parse the xml into an Element class:
  203. # http://docs.python.org/library/xml.etree.elementtree.html
  204. tree = fromstring(self.xml)
  205. for e in tree:
  206. if e.tag not in ('parameter', 'flag'):
  207. self.__setattr__(e.tag, GETFROMTAG[e.tag](e))
  208. #
  209. # extract parameters from the xml
  210. #
  211. self.params_list = [Parameter(p) for p in tree.findall("parameter")]
  212. self.inputs = TypeDict(Parameter)
  213. self.outputs = TypeDict(Parameter)
  214. self.required = []
  215. # Insert parameters into input/output and required
  216. for par in self.params_list:
  217. if par.input:
  218. self.inputs[par.name] = par
  219. else:
  220. self.outputs[par.name] = par
  221. if par.required:
  222. self.required.append(par.name)
  223. #
  224. # extract flags from the xml
  225. #
  226. flags_list = [Flag(f) for f in tree.findall("flag")]
  227. self.flags = TypeDict(Flag)
  228. for flag in flags_list:
  229. self.flags[flag.name] = flag
  230. #
  231. # Add new attributes to the class
  232. #
  233. self.run_ = True
  234. self.finish_ = True
  235. self.env_ = None
  236. self.stdin_ = None
  237. self.stdin = None
  238. self.stdout_ = None
  239. self.stderr_ = None
  240. diz = {'name': 'stdin', 'required': False,
  241. 'multiple': False, 'type': 'all',
  242. 'value': None}
  243. self.inputs['stdin'] = Parameter(diz=diz)
  244. diz['name'] = 'stdout'
  245. self.outputs['stdout'] = Parameter(diz=diz)
  246. diz['name'] = 'stderr'
  247. self.outputs['stderr'] = Parameter(diz=diz)
  248. self.popen = None
  249. self.time = None
  250. if args or kargs:
  251. self.__call__(*args, **kargs)
  252. def __call__(self, *args, **kargs):
  253. if not args and not kargs:
  254. self.run()
  255. return
  256. #
  257. # check for extra kargs, set attribute and remove from dictionary
  258. #
  259. if 'flags' in kargs:
  260. for flg in kargs['flags']:
  261. self.flags[flg].value = True
  262. del(kargs['flags'])
  263. if 'run_' in kargs:
  264. self.run_ = kargs['run_']
  265. del(kargs['run_'])
  266. if 'stdin_' in kargs:
  267. self.inputs['stdin'].value = kargs['stdin_']
  268. del(kargs['stdin_'])
  269. if 'stdout_' in kargs:
  270. self.outputs['stdout'].value = kargs['stdout_']
  271. del(kargs['stdout_'])
  272. if 'stderr_' in kargs:
  273. self.outputs['stderr'].value = kargs['stderr_']
  274. del(kargs['stderr_'])
  275. if 'env_' in kargs:
  276. self.env_ = kargs['env_']
  277. del(kargs['env_'])
  278. if 'finish_' in kargs:
  279. self.finish_ = kargs['finish_']
  280. del(kargs['finish_'])
  281. #
  282. # check args
  283. #
  284. for param, arg in zip(self.params_list, args):
  285. param.value = arg
  286. for key, val in kargs.items():
  287. if key in self.inputs:
  288. self.inputs[key].value = val
  289. elif key in self.outputs:
  290. self.outputs[key].value = val
  291. elif key in self.flags:
  292. # we need to add this, because some parameters (overwrite,
  293. # verbose and quiet) work like parameters
  294. self.flags[key].value = val
  295. else:
  296. raise ParameterError('%s is not a valid parameter.' % key)
  297. #
  298. # check if execute
  299. #
  300. if self.run_:
  301. #
  302. # check reqire parameters
  303. #
  304. for k in self.required:
  305. if ((k in self.inputs and self.inputs[k].value is None) or
  306. (k in self.outputs and self.outputs[k].value is None)):
  307. msg = "Required parameter <%s> not set."
  308. raise ParameterError(msg % key)
  309. self.run()
  310. def get_bash(self):
  311. return ' '.join(self.make_cmd())
  312. def get_python(self):
  313. prefix = self.name.split('.')[0]
  314. name = '_'.join(self.name.split('.')[1:])
  315. params = ', '.join([par.get_python() for par in self.params_list
  316. if par.get_python() != ''])
  317. special = ', '.join([flg.get_python()
  318. for flg in self.flags.values()
  319. if flg.special and flg.get_python() != ''])
  320. # pre name par flg special
  321. if self.flags and special:
  322. return "%s.%s(%s, flags=%r, %s)" % (prefix, name, params,
  323. self.flags, special)
  324. elif self.flags:
  325. return "%s.%s(%s, flags=%r)" % (prefix, name, params, self.flags)
  326. elif special:
  327. return "%s.%s(%s, %s)" % (prefix, name, params, special)
  328. else:
  329. return "%s.%s(%s)" % (prefix, name, params)
  330. def __str__(self):
  331. """!Return the command string that can be executed in a shell
  332. """
  333. return ' '.join(self.make_cmd())
  334. def __repr__(self):
  335. return "Module(%r)" % self.name
  336. @property
  337. def __doc__(self):
  338. """{cmd_name}({cmd_params})
  339. """
  340. head = DOC['head'].format(cmd_name=self.name,
  341. cmd_params=('\n' + # go to a new line
  342. # give space under the function name
  343. (' ' * (len(self.name) + 1))).join([', '.join(
  344. # transform each parameter in string
  345. [str(param) for param in line if param is not None])
  346. # make a list of parameters with only 3 param per line
  347. for line in izip_longest(*[iter(self.params_list)] * 3)]),)
  348. params = '\n'.join([par.__doc__ for par in self.params_list])
  349. flags = self.flags.__doc__
  350. return '\n'.join([head, params, DOC['flag_head'], flags, DOC['foot']])
  351. def get_dict(self):
  352. """!Return a dictionary that includes the name, all valid
  353. inputs, outputs and flags
  354. """
  355. dic = {}
  356. dic['name'] = self.name
  357. dic['inputs'] = [(k, v.value) for k, v in self.inputs.items()
  358. if v.value]
  359. dic['outputs'] = [(k, v.value) for k, v in self.outputs.items()
  360. if v.value]
  361. dic['flags'] = [flg for flg in self.flags if self.flags[flg].value]
  362. return dic
  363. def make_cmd(self):
  364. """!Create the command string that can be executed in a shell
  365. @return The command string
  366. """
  367. skip = ['stdin', 'stdout', 'stderr']
  368. args = [self.name, ]
  369. for key in self.inputs:
  370. if key not in skip and self.inputs[key].value:
  371. args.append(self.inputs[key].get_bash())
  372. for key in self.outputs:
  373. if key not in skip and self.outputs[key].value:
  374. args.append(self.outputs[key].get_bash())
  375. for flg in self.flags:
  376. if self.flags[flg].value:
  377. args.append(str(self.flags[flg]))
  378. return args
  379. def run(self, node=None):
  380. """!Run the module
  381. This function will wait for the process to terminate
  382. in case finish_==True and sets up stdout and stderr.
  383. If finish_==False this function will return after starting
  384. the process. Use self.popen.communicate() of self.popen.wait()
  385. to wait for the process termination. The handling
  386. of stdout and stderr must then be done outside of this
  387. function.
  388. """
  389. if self.inputs['stdin'].value:
  390. self.stdin = self.inputs['stdin'].value
  391. self.stdin_ = subprocess.PIPE
  392. if self.outputs['stdout'].value:
  393. self.stdout_ = self.outputs['stdout'].value
  394. if self.outputs['stderr'].value:
  395. self.stderr_ = self.outputs['stderr'].value
  396. cmd = self.make_cmd()
  397. start = time.time()
  398. self.popen = subprocess.Popen(cmd,
  399. stdin=self.stdin_,
  400. stdout=self.stdout_,
  401. stderr=self.stderr_,
  402. env=self.env_,
  403. shell=(sys.platform == "win32"))
  404. if self.finish_:
  405. stdout, stderr = self.popen.communicate(input=self.stdin)
  406. self.outputs['stdout'].value = stdout if stdout else ''
  407. self.outputs['stderr'].value = stderr if stderr else ''
  408. self.time = time.time() - start
  409. ###############################################################################
  410. if __name__ == "__main__":
  411. import doctest
  412. doctest.testmod()