pythonalgorithm.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525
  1. from __future__ import absolute_import, print_function
  2. from functools import wraps
  3. from xml.dom.minidom import parseString
  4. from xml.parsers.expat import ExpatError
  5. from inspect import getargspec
  6. import sys
  7. def _count(values):
  8. """internal: returns the `number-of-elements` for the argument.
  9. if argument is a list or type, it returns its `len`, else returns 0 for None
  10. and 1 for non-None."""
  11. if type(values) == list or type(values) == tuple:
  12. return len(values)
  13. elif values is None:
  14. return 0
  15. else:
  16. return 1
  17. def _stringify(values):
  18. """internal method: used to convert values to a string suitable for an xml attribute"""
  19. if type(values) == list or type(values) == tuple:
  20. return " ".join([str(x) for x in values])
  21. elif type(values) == type(True):
  22. return "1" if values else "0"
  23. else:
  24. return str(values)
  25. def _generate_xml(attrs, nested_xmls=[]):
  26. """internal: used to generate an XML string from the arguments.
  27. `attrs` is a dict with attributes specified using (key, value) for the dict.
  28. `type` key in `attrs` is treated as the XML tag name.
  29. `nested_xmls` is a list of strings that get nested in the resulting xmlstring
  30. returned by this function.
  31. """
  32. d = {}
  33. d["type"] = attrs.pop("type")
  34. attr_items = filter(lambda item : item[1] is not None, attrs.items())
  35. d["attrs"] = "\n".join(["%s=\"%s\"" % (x, _stringify(y)) for x,y in attr_items])
  36. d["nested_xmls"] = "\n".join(nested_xmls)
  37. xml = """<{type} {attrs}> {nested_xmls} </{type}>"""
  38. return xml.format(**d)
  39. def _undecorate(func):
  40. """internal: Traverses through nested decorated objects to return the original
  41. object. This is not a general mechanism and only supports decorator chains created
  42. via `_create_decorator`."""
  43. if hasattr(func, "_pv_original_func"):
  44. return _undecorate(func._pv_original_func)
  45. return func
  46. def _create_decorator(kwargs={}, update_func=None, generate_xml_func=None):
  47. """internal: used to create decorator for class or function objects.
  48. `kwargs`: these are typically the keyword arguments passed to the decorator itself.
  49. must be a `dict`. The decorator will often update the dict to have
  50. defaults or overrides to the parameters passed to the decorator, as appropriate.
  51. `update_func`: if non-None, must be a callable that takes 2 arguments `(decoratedobj, kwargs)`.
  52. The purpose of this callback is to update the kwargs (and return updated version)
  53. for required-yet-missing attributes that can be deduced by introspecting
  54. the `decoratedobj`.
  55. `generate_xml_func`: must be non-None and must be a callable that takes 2 arguments `(decoratedobj, kwargs)`.
  56. The kwargs can be expected to have been updated by calling `update_func`, if non-None.
  57. The callback typically adds an attribute on the decoratedobj for further processing later.
  58. """
  59. attrs = kwargs.copy()
  60. def decorator(func):
  61. original_func = _undecorate(func)
  62. if update_func is not None:
  63. updated_attrs = update_func(original_func, attrs)
  64. else:
  65. updated_attrs = attrs
  66. generate_xml_func(original_func, updated_attrs)
  67. @wraps(func)
  68. def wrapper(*args, **kwargs):
  69. return func(*args, **kwargs)
  70. setattr(wrapper, "_pv_original_func", original_func)
  71. return wrapper
  72. return decorator
  73. class smproperty(object):
  74. """
  75. Provides decorators for class methods that are to be exposed as
  76. server-manager properties in ParaView. Only methods that are decorated
  77. using one of the available decorators will be exposed be accessible to ParaView
  78. UI or client-side Python scripting API.
  79. """
  80. @staticmethod
  81. def _append_xml(func, xml):
  82. pxmls = []
  83. if hasattr(func, "_pvsm_property_xmls"):
  84. pxmls = func._pvsm_property_xmls
  85. pxmls.append(xml)
  86. setattr(func, "_pvsm_property_xmls", pxmls)
  87. @staticmethod
  88. def _generate_xml(func, attrs):
  89. nested_xmls = []
  90. if hasattr(func, "_pvsm_domain_xmls"):
  91. for d in func._pvsm_domain_xmls:
  92. nested_xmls.append(d)
  93. delattr(func, "_pvsm_domain_xmls")
  94. if hasattr(func, "_pvsm_hints_xmls"):
  95. hints = []
  96. for h in func._pvsm_hints_xmls:
  97. hints.append(h)
  98. nested_xmls.append(_generate_xml({"type":"Hints"}, hints))
  99. delattr(func, "_pvsm_hints_xmls")
  100. pxml = _generate_xml(attrs, nested_xmls)
  101. smproperty._append_xml(func, pxml)
  102. @staticmethod
  103. def _update_property_defaults(func, attrs):
  104. """Function used to populate default attribute values for missing attributes
  105. on a for a property"""
  106. # determine unspecified attributes based on the `func`
  107. if attrs.get("name", None) is None:
  108. attrs["name"] = func.__name__
  109. if attrs.get("command", None) is None:
  110. attrs["command"] = func.__name__
  111. return attrs
  112. @staticmethod
  113. def _update_vectorproperty_defaults(func, attrs):
  114. """Function used to populate default attribute values for missing attributes
  115. on a vector property"""
  116. attrs = smproperty._update_property_defaults(func, attrs)
  117. if attrs.get("number_of_elements", None) is None:
  118. attrs["number_of_elements"] = len(getargspec(func).args) - 1
  119. if attrs.get("default_values", None) is None:
  120. attrs["default_values"] = "None"
  121. else:
  122. # confirm number_of_elements == len(default_values)
  123. assert attrs["number_of_elements"] == _count(attrs["default_values"])
  124. # if repeat_command, set number_of_elements_per_command
  125. # if not set.
  126. if attrs.get("repeat_command", None) is not None and \
  127. attrs.get("number_of_elements_per_command", None) is None:
  128. attrs["number_of_elements_per_command"] = len(getargspec(func).args) - 1
  129. return attrs
  130. @staticmethod
  131. def _update_proxyproperty_attrs(func, attrs):
  132. return _update_property_defaults(func, attrs)
  133. @staticmethod
  134. def xml(xmlstr):
  135. """Decorator that be used to directly add a ServerManager property XML for a method."""
  136. def generate(func, attrs):
  137. smproperty._append_xml(func, xmlstr)
  138. return _create_decorator(generate_xml_func=generate)
  139. @staticmethod
  140. def intvector(**kwargs):
  141. attrs = { "type" : "IntVectorProperty"}
  142. attrs.update(kwargs)
  143. return _create_decorator(attrs,
  144. update_func=smproperty._update_vectorproperty_defaults,
  145. generate_xml_func=smproperty._generate_xml)
  146. @staticmethod
  147. def doublevector(**kwargs):
  148. attrs = { "type" : "DoubleVectorProperty"}
  149. attrs.update(kwargs)
  150. return _create_decorator(attrs,
  151. update_func=smproperty._update_vectorproperty_defaults,
  152. generate_xml_func=smproperty._generate_xml)
  153. @staticmethod
  154. def idtypevector(**kwargs):
  155. attrs = { "type" : "IdTypeVectorProperty"}
  156. attrs.update(kwargs)
  157. return _create_decorator(attrs,
  158. update_func=smproperty._update_vectorproperty_defaults,
  159. generate_xml_func=smproperty._generate_xml)
  160. @staticmethod
  161. def stringvector(**kwargs):
  162. attrs = { "type" : "StringVectorProperty" }
  163. attrs.update(kwargs)
  164. return _create_decorator(attrs,
  165. update_func=smproperty._update_vectorproperty_defaults,
  166. generate_xml_func=smproperty._generate_xml)
  167. @staticmethod
  168. def proxy(**kwargs):
  169. attrs = { "type" : "ProxyProperty" }
  170. attrs.update(kwargs)
  171. return _create_decorator(attrs,
  172. update_func=smproperty._update_proxyproperty_attrs,
  173. generate_xml_func=smproperty._generate_xml)
  174. @staticmethod
  175. def input(**kwargs):
  176. attrs = { "type" : "InputProperty" }
  177. if kwargs.get("multiple_input", False) or kwargs.get("repeat_command", False):
  178. attrs["command"] = "AddInputConnection"
  179. # FIXME: input property doesn't support cleaning port connections alone :(
  180. attrs["clean_command"] = "RemoveAllInputs"
  181. else:
  182. attrs["command"] = "SetInputConnection"
  183. # todo: handle inputType
  184. attrs.update(kwargs)
  185. return _create_decorator(attrs,
  186. update_func=smproperty._update_property_defaults,
  187. generate_xml_func=smproperty._generate_xml)
  188. @staticmethod
  189. def dataarrayselection(name=None):
  190. def generate(func, attrs):
  191. xml="""<StringVectorProperty name="{name}Info"
  192. command="{command}"
  193. number_of_elements_per_command="2"
  194. information_only="1"
  195. si_class="vtkSIDataArraySelectionProperty" />
  196. <StringVectorProperty name="{name}"
  197. information_property="{name}Info"
  198. command="{command}"
  199. number_of_elements_per_command="2"
  200. element_types="2 0"
  201. repeat_command="1"
  202. si_class="vtkSIDataArraySelectionProperty">
  203. <ArraySelectionDomain name="array_list">
  204. <RequiredProperties>
  205. <Property function="ArrayList" name="{name}Info" />
  206. </RequiredProperties>
  207. </ArraySelectionDomain>
  208. </StringVectorProperty>
  209. """.format(**attrs)
  210. smproperty._append_xml(func, xml)
  211. return _create_decorator({"name" : name},
  212. update_func=smproperty._update_property_defaults,
  213. generate_xml_func=generate)
  214. class smdomain(object):
  215. """
  216. Provides decorators that add domains to properties.
  217. """
  218. @staticmethod
  219. def _append_xml(func, xml):
  220. domains = []
  221. if hasattr(func, "_pvsm_domain_xmls"):
  222. domains = func._pvsm_domain_xmls
  223. domains.append(xml)
  224. setattr(func, "_pvsm_domain_xmls", domains)
  225. @staticmethod
  226. def _generate_xml(func, attrs):
  227. smdomain._append_xml(func, _generate_xml(attrs, []))
  228. @staticmethod
  229. def xml(xmlstr):
  230. def generate(func, attrs):
  231. smdomain._append_xml(func, xmlstr)
  232. return _create_decorator({},
  233. generate_xml_func=generate)
  234. @staticmethod
  235. def doublerange(**kwargs):
  236. attrs = { "type" : "DoubleRangeDomain" , "name" : "range" }
  237. attrs.update(kwargs)
  238. return _create_decorator(attrs,
  239. generate_xml_func=smdomain._generate_xml)
  240. @staticmethod
  241. def intrange(**kwargs):
  242. attrs = { "type" : "IntRangeDomain" , "name" : "range" }
  243. attrs.update(kwargs)
  244. return _create_decorator(attrs,
  245. generate_xml_func=smdomain._generate_xml)
  246. @staticmethod
  247. def filelist(**kwargs):
  248. attrs = { "type" : "FileListDomain", "name" : "files" }
  249. attrs.update(kwargs)
  250. return _create_decorator(attrs,
  251. generate_xml_func=smdomain._generate_xml)
  252. @staticmethod
  253. def datatype(dataTypes, **kwargs):
  254. attrs = {"type" : "DataTypeDomain", "name": "input_type"}
  255. attrs.update(kwargs)
  256. def generate(func, attrs):
  257. type_xmls = []
  258. for atype in dataTypes:
  259. type_xmls.append(_generate_xml({"type" : "DataType", "value" : atype}, []))
  260. smdomain._append_xml(func, _generate_xml(attrs, type_xmls))
  261. return _create_decorator(attrs,
  262. generate_xml_func=generate)
  263. class smhint(object):
  264. """Provides decorators that add hints to proxies and properties."""
  265. @staticmethod
  266. def _generate_xml(func, attrs):
  267. lhints = []
  268. if hasattr(func, "_pvsm_hints_xmls"):
  269. lhints = func._pvsm_hints_xmls
  270. lhints.append(_generate_xml(attrs, []))
  271. setattr(func, "_pvsm_hints_xmls", lhints)
  272. @staticmethod
  273. def xml(xmlstr):
  274. def generate(func, attrs):
  275. lhints = []
  276. if hasattr(func, "_pvsm_hints_xmls"):
  277. lhints = func._pvsm_hints_xmls
  278. lhints.append(xmlstr)
  279. setattr(func, "_pvsm_hints_xmls", lhints)
  280. return _create_decorator({},
  281. generate_xml_func=generate)
  282. @staticmethod
  283. def filechooser(extensions, file_description):
  284. attrs = {}
  285. attrs["type"] = "FileChooser"
  286. attrs["extensions"] = extensions
  287. attrs["file_description"] = file_description
  288. return _create_decorator(attrs,
  289. generate_xml_func=smhint._generate_xml)
  290. def get_qualified_classname(classobj):
  291. if classobj.__module__ == "__main__":
  292. return classobj.__name__
  293. else:
  294. return "%s.%s" % (classobj.__module__, classobj.__name__)
  295. class smproxy(object):
  296. """
  297. Provides decorators for class objects that should be exposed to
  298. ParaView.
  299. """
  300. @staticmethod
  301. def _update_proxy_defaults(classobj, attrs):
  302. if attrs.get("name", None) is None:
  303. attrs["name"] = classobj.__name__
  304. if attrs.get("class", None) is None:
  305. attrs["class"] = get_qualified_classname(classobj)
  306. if attrs.get("label", None) is None:
  307. attrs["label"] = attrs["name"]
  308. return attrs
  309. @staticmethod
  310. def _generate_xml(classobj, attrs):
  311. nested_xmls = []
  312. classobj = _undecorate(classobj)
  313. if hasattr(classobj, "_pvsm_property_xmls"):
  314. val = getattr(classobj, "_pvsm_property_xmls")
  315. if type(val) == type([]):
  316. nested_xmls += val
  317. else:
  318. nested_xmls.append(val)
  319. prop_xmls_dict = {}
  320. for pname, val in classobj.__dict__.items():
  321. val = _undecorate(val)
  322. if callable(val) and hasattr(val, "_pvsm_property_xmls"):
  323. pxmls = getattr(val, "_pvsm_property_xmls")
  324. if len(pxmls) > 1:
  325. raise RuntimeError("Multiple property definitions on the same"\
  326. "method are not supported.")
  327. prop_xmls_dict[pname] = pxmls[0]
  328. # since the order of the properties keeps on changing between invocations,
  329. # let's sort them by the name for consistency. In future, we may put in
  330. # extra logic to preserve order they were defined in the class
  331. nested_xmls += [prop_xmls_dict[key] for key in sorted(prop_xmls_dict.keys())]
  332. if attrs.get("support_reload", True):
  333. nested_xmls.insert(0, """
  334. <Property name="Reload Python Module" panel_widget="command_button">
  335. <Documentation>Reload the Python module.</Documentation>
  336. </Property>""")
  337. if hasattr(classobj, "_pvsm_hints_xmls"):
  338. hints = [h for h in classobj._pvsm_hints_xmls]
  339. nested_xmls.append(_generate_xml({"type":"Hints"}, hints))
  340. proxyxml = _generate_xml(attrs, nested_xmls)
  341. groupxml = _generate_xml({"type":"ProxyGroup", "name":attrs.get("group")},
  342. [proxyxml])
  343. smconfig = _generate_xml({"type":"ServerManagerConfiguration"}, [groupxml])
  344. setattr(classobj, "_pvsm_proxy_xml", smconfig)
  345. @staticmethod
  346. def source(**kwargs):
  347. attrs = {}
  348. attrs["type"] = "SourceProxy"
  349. attrs["group"] = "sources"
  350. attrs["si_class"] = "vtkSIPythonSourceProxy"
  351. attrs.update(kwargs)
  352. return _create_decorator(attrs,
  353. update_func=smproxy._update_proxy_defaults,
  354. generate_xml_func=smproxy._generate_xml)
  355. @staticmethod
  356. def filter(**kwargs):
  357. attrs = { "group" : "filters" }
  358. attrs.update(kwargs)
  359. return smproxy.source(**attrs)
  360. @staticmethod
  361. def reader(file_description, extensions=None, filename_patterns=None, is_directory=False, **kwargs):
  362. """
  363. Decorates a reader. Either `filename_patterns` or `extensions` must be
  364. provided.
  365. """
  366. if extensions is None and filename_patterns is None:
  367. raise RuntimeError("Either `filename_patterns` or `extensions` must be provided for a reader.")
  368. attrs = { "type" : "ReaderFactory" }
  369. attrs["file_description"] = file_description
  370. attrs["extensions"] = extensions
  371. attrs["filename_patterns"] = filename_patterns
  372. attrs["is_directory"] = "1" if is_directory else None
  373. _xml = _generate_xml(attrs, [])
  374. def decorator(func):
  375. f = smhint.xml(_xml)(func)
  376. return smproxy.source(**kwargs)(f)
  377. return decorator
  378. @staticmethod
  379. def writer(file_description, extensions, **kwargs):
  380. """
  381. Decorates a writer.
  382. """
  383. assert file_description is not None and extensions is not None
  384. attrs = { "type" : "WriterFactory" }
  385. attrs["file_description"] = file_description
  386. attrs["extensions"] = extensions
  387. _xml = _generate_xml(attrs, [])
  388. def decorator(func):
  389. f = smhint.xml(_xml)(func)
  390. return smproxy.source(group="writers", type="WriterProxy", **kwargs)(f)
  391. return decorator
  392. def get_plugin_xmls(module_or_package):
  393. """helper function called by vtkPVPythonAlgorithmPlugin to discover
  394. all "proxy" decorated classes in the module or package. We don't recurse
  395. into the package, on simply needs to export all classes that form the
  396. ParaView plugin in the __init__.py for the package."""
  397. from inspect import ismodule, isclass
  398. items = []
  399. if ismodule(module_or_package):
  400. items = module_or_package.__dict__.items()
  401. elif hasattr(module_or_package, "items"):
  402. items = module_or_package.items()
  403. xmls = []
  404. for (k,v) in items:
  405. v = _undecorate(v)
  406. if hasattr(v, "_pvsm_proxy_xml"):
  407. xmls.append(getattr(v, "_pvsm_proxy_xml"))
  408. return xmls
  409. def get_plugin_name(module_or_package):
  410. """helper function called by vtkPVPythonAlgorithmPlugin to discover
  411. ParaView plugin name, if any."""
  412. from inspect import ismodule, isclass
  413. if ismodule(module_or_package) and hasattr(module_or_package, "paraview_plugin_name"):
  414. return str(getattr(module_or_package, "paraview_plugin_name"))
  415. else:
  416. return module_or_package.__name__
  417. def get_plugin_version(module_or_package):
  418. """helper function called by vtkPVPythonAlgorithmPlugin to discover
  419. ParaView plugin version, if any."""
  420. from inspect import ismodule, isclass
  421. if ismodule(module_or_package) and hasattr(module_or_package, "paraview_plugin_version"):
  422. return str(getattr(module_or_package, "paraview_plugin_version"))
  423. else:
  424. return "(unknown)"
  425. def load_plugin(filepath, default_modulename=None):
  426. """helper function called by vtkPVPythonAlgorithmPlugin to load
  427. a python file."""
  428. # should we scope these under a plugins namespace?
  429. if default_modulename:
  430. modulename = default_modulename
  431. else:
  432. import os.path
  433. modulename = "%s" % os.path.splitext(os.path.basename(filepath))[0]
  434. try:
  435. # for Python 3.5+
  436. from importlib.util import spec_from_file_location, module_from_spec
  437. spec = spec_from_file_location(modulename, filepath)
  438. module = module_from_spec(spec)
  439. spec.loader.exec_module(module)
  440. except ImportError:
  441. # for Python 3.3 and 3.4
  442. import imp
  443. module = imp.load_source(modulename, filepath)
  444. import sys
  445. sys.modules[modulename] = module
  446. return module
  447. def reload_plugin_module(module):
  448. """helper function to reload a plugin module previously loaded via
  449. load_plugin"""
  450. from inspect import getsourcefile, ismodule
  451. if ismodule(module) and getsourcefile(module):
  452. return load_plugin(getsourcefile(module), module.__name__)
  453. return module