module.py 16 KB

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