module.py 16 KB

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