module.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483
  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 as e:
  195. print("OSError error({0}): {1}".format(e.errno, e.strerror))
  196. str_err = "Error running: `%s --interface-description`."
  197. raise GrassError(str_err % self.name)
  198. # get the xml of the module
  199. self.xml = get_cmd_xml.communicate()[0]
  200. # transform and parse the xml into an Element class:
  201. # http://docs.python.org/library/xml.etree.elementtree.html
  202. tree = fromstring(self.xml)
  203. for e in tree:
  204. if e.tag not in ('parameter', 'flag'):
  205. self.__setattr__(e.tag, GETFROMTAG[e.tag](e))
  206. #
  207. # extract parameters from the xml
  208. #
  209. self.params_list = [Parameter(p) for p in tree.findall("parameter")]
  210. self.inputs = TypeDict(Parameter)
  211. self.outputs = TypeDict(Parameter)
  212. self.required = []
  213. # Insert parameters into input/output and required
  214. for par in self.params_list:
  215. if par.input:
  216. self.inputs[par.name] = par
  217. else:
  218. self.outputs[par.name] = par
  219. if par.required:
  220. self.required.append(par.name)
  221. #
  222. # extract flags from the xml
  223. #
  224. flags_list = [Flag(f) for f in tree.findall("flag")]
  225. self.flags = TypeDict(Flag)
  226. for flag in flags_list:
  227. self.flags[flag.name] = flag
  228. #
  229. # Add new attributes to the class
  230. #
  231. self.run_ = True
  232. self.finish_ = True
  233. self.env_ = None
  234. self.stdin_ = None
  235. self.stdin = None
  236. self.stdout_ = None
  237. self.stderr_ = None
  238. diz = {'name': 'stdin', 'required': False,
  239. 'multiple': False, 'type': 'all',
  240. 'value': None}
  241. self.inputs['stdin'] = Parameter(diz=diz)
  242. diz['name'] = 'stdout'
  243. self.outputs['stdout'] = Parameter(diz=diz)
  244. diz['name'] = 'stderr'
  245. self.outputs['stderr'] = Parameter(diz=diz)
  246. self.popen = None
  247. self.time = None
  248. if args or kargs:
  249. self.__call__(*args, **kargs)
  250. def __call__(self, *args, **kargs):
  251. if not args and not kargs:
  252. self.run()
  253. return
  254. #
  255. # check for extra kargs, set attribute and remove from dictionary
  256. #
  257. if 'flags' in kargs:
  258. for flg in kargs['flags']:
  259. self.flags[flg].value = True
  260. del(kargs['flags'])
  261. if 'run_' in kargs:
  262. self.run_ = kargs['run_']
  263. del(kargs['run_'])
  264. if 'stdin_' in kargs:
  265. self.inputs['stdin'].value = kargs['stdin_']
  266. del(kargs['stdin_'])
  267. if 'stdout_' in kargs:
  268. self.outputs['stdout'].value = kargs['stdout_']
  269. del(kargs['stdout_'])
  270. if 'stderr_' in kargs:
  271. self.outputs['stderr'].value = kargs['stderr_']
  272. del(kargs['stderr_'])
  273. if 'env_' in kargs:
  274. self.env_ = kargs['env_']
  275. del(kargs['env_'])
  276. if 'finish_' in kargs:
  277. self.finish_ = kargs['finish_']
  278. del(kargs['finish_'])
  279. #
  280. # check args
  281. #
  282. for param, arg in zip(self.params_list, args):
  283. param.value = arg
  284. for key, val in kargs.items():
  285. if key in self.inputs:
  286. self.inputs[key].value = val
  287. elif key in self.outputs:
  288. self.outputs[key].value = val
  289. elif key in self.flags:
  290. # we need to add this, because some parameters (overwrite,
  291. # verbose and quiet) work like parameters
  292. self.flags[key].value = val
  293. else:
  294. raise ParameterError('%s is not a valid parameter.' % key)
  295. #
  296. # check if execute
  297. #
  298. if self.run_:
  299. #
  300. # check reqire parameters
  301. #
  302. for k in self.required:
  303. if ((k in self.inputs and self.inputs[k].value is None) or
  304. (k in self.outputs and self.outputs[k].value is None)):
  305. msg = "Required parameter <%s> not set."
  306. raise ParameterError(msg % key)
  307. self.run()
  308. def get_bash(self):
  309. return ' '.join(self.make_cmd())
  310. def get_python(self):
  311. prefix = self.name.split('.')[0]
  312. name = '_'.join(self.name.split('.')[1:])
  313. params = ', '.join([par.get_python() for par in self.params_list
  314. if par.get_python() != ''])
  315. special = ', '.join([flg.get_python()
  316. for flg in self.flags.values()
  317. if flg.special and flg.get_python() != ''])
  318. # pre name par flg special
  319. if self.flags and special:
  320. return "%s.%s(%s, flags=%r, %s)" % (prefix, name, params,
  321. self.flags, special)
  322. elif self.flags:
  323. return "%s.%s(%s, flags=%r)" % (prefix, name, params, self.flags)
  324. elif special:
  325. return "%s.%s(%s, %s)" % (prefix, name, params, special)
  326. else:
  327. return "%s.%s(%s)" % (prefix, name, params)
  328. def __str__(self):
  329. """!Return the command string that can be executed in a shell
  330. """
  331. return ' '.join(self.make_cmd())
  332. def __repr__(self):
  333. return "Module(%r)" % self.name
  334. @property
  335. def __doc__(self):
  336. """{cmd_name}({cmd_params})
  337. """
  338. head = DOC['head'].format(cmd_name=self.name,
  339. cmd_params=('\n' + # go to a new line
  340. # give space under the function name
  341. (' ' * (len(self.name) + 1))).join([', '.join(
  342. # transform each parameter in string
  343. [str(param) for param in line if param is not None])
  344. # make a list of parameters with only 3 param per line
  345. for line in izip_longest(*[iter(self.params_list)] * 3)]),)
  346. params = '\n'.join([par.__doc__ for par in self.params_list])
  347. flags = self.flags.__doc__
  348. return '\n'.join([head, params, DOC['flag_head'], flags, DOC['foot']])
  349. def get_dict(self):
  350. """!Return a dictionary that includes the name, all valid
  351. inputs, outputs and flags
  352. """
  353. dic = {}
  354. dic['name'] = self.name
  355. dic['inputs'] = [(k, v.value) for k, v in self.inputs.items()
  356. if v.value]
  357. dic['outputs'] = [(k, v.value) for k, v in self.outputs.items()
  358. if v.value]
  359. dic['flags'] = [flg for flg in self.flags if self.flags[flg].value]
  360. return dic
  361. def make_cmd(self):
  362. """!Create the command string that can be executed in a shell
  363. @return The command string
  364. """
  365. skip = ['stdin', 'stdout', 'stderr']
  366. args = [self.name, ]
  367. for key in self.inputs:
  368. if key not in skip and self.inputs[key].value:
  369. args.append(self.inputs[key].get_bash())
  370. for key in self.outputs:
  371. if key not in skip and self.outputs[key].value:
  372. args.append(self.outputs[key].get_bash())
  373. for flg in self.flags:
  374. if self.flags[flg].value:
  375. args.append(str(self.flags[flg]))
  376. return args
  377. def run(self, node=None):
  378. """!Run the module
  379. This function will wait for the process to terminate
  380. in case finish_==True and sets up stdout and stderr.
  381. If finish_==False this function will return after starting
  382. the process. Use self.popen.communicate() of self.popen.wait()
  383. to wait for the process termination. The handling
  384. of stdout and stderr must then be done outside of this
  385. function.
  386. """
  387. if self.inputs['stdin'].value:
  388. self.stdin = self.inputs['stdin'].value
  389. self.stdin_ = subprocess.PIPE
  390. if self.outputs['stdout'].value:
  391. self.stdout_ = self.outputs['stdout'].value
  392. if self.outputs['stderr'].value:
  393. self.stderr_ = self.outputs['stderr'].value
  394. cmd = self.make_cmd()
  395. start = time.time()
  396. self.popen = subprocess.Popen(cmd,
  397. stdin=self.stdin_,
  398. stdout=self.stdout_,
  399. stderr=self.stderr_,
  400. env=self.env_)
  401. if self.finish_:
  402. stdout, stderr = self.popen.communicate(input=self.stdin)
  403. self.outputs['stdout'].value = stdout if stdout else ''
  404. self.outputs['stderr'].value = stderr if stderr else ''
  405. self.time = time.time() - start
  406. ###############################################################################
  407. if __name__ == "__main__":
  408. import doctest
  409. doctest.testmod()