gooderp18绿色标准版
您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

680 行
26KB

  1. # -*- coding: utf-8 -*-
  2. # Part of Odoo. See LICENSE file for full copyright and licensing details.
  3. __all__ = [
  4. 'convert_file', 'convert_sql_import',
  5. 'convert_csv_import', 'convert_xml_import'
  6. ]
  7. import base64
  8. import csv
  9. import io
  10. import logging
  11. import os.path
  12. import pprint
  13. import re
  14. import subprocess
  15. from datetime import datetime, timedelta
  16. from dateutil.relativedelta import relativedelta
  17. from lxml import etree, builder
  18. try:
  19. import jingtrang
  20. except ImportError:
  21. jingtrang = None
  22. import odoo
  23. from .config import config
  24. from .misc import file_open, file_path, SKIPPED_ELEMENT_TYPES
  25. from odoo.exceptions import ValidationError
  26. from .safe_eval import safe_eval as s_eval, pytz, time
  27. _logger = logging.getLogger(__name__)
  28. def safe_eval(expr, ctx={}):
  29. return s_eval(expr, ctx, nocopy=True)
  30. class ParseError(Exception):
  31. ...
  32. def _get_idref(self, env, model_str, idref):
  33. idref2 = dict(idref,
  34. Command=odoo.fields.Command,
  35. time=time,
  36. DateTime=datetime,
  37. datetime=datetime,
  38. timedelta=timedelta,
  39. relativedelta=relativedelta,
  40. version=odoo.release.major_version,
  41. ref=self.id_get,
  42. pytz=pytz)
  43. if model_str:
  44. idref2['obj'] = env[model_str].browse
  45. return idref2
  46. def _fix_multiple_roots(node):
  47. """
  48. Surround the children of the ``node`` element of an XML field with a
  49. single root "data" element, to prevent having a document with multiple
  50. roots once parsed separately.
  51. XML nodes should have one root only, but we'd like to support
  52. direct multiple roots in our partial documents (like inherited view architectures).
  53. As a convention we'll surround multiple root with a container "data" element, to be
  54. ignored later when parsing.
  55. """
  56. real_nodes = [x for x in node if not isinstance(x, SKIPPED_ELEMENT_TYPES)]
  57. if len(real_nodes) > 1:
  58. data_node = etree.Element("data")
  59. for child in node:
  60. data_node.append(child)
  61. node.append(data_node)
  62. def _eval_xml(self, node, env):
  63. if node.tag in ('field','value'):
  64. t = node.get('type','char')
  65. f_model = node.get('model')
  66. if f_search := node.get('search'):
  67. f_use = node.get("use",'id')
  68. f_name = node.get("name")
  69. idref2 = {}
  70. if f_search:
  71. idref2 = _get_idref(self, env, f_model, self.idref)
  72. q = safe_eval(f_search, idref2)
  73. ids = env[f_model].search(q).ids
  74. if f_use != 'id':
  75. ids = [x[f_use] for x in env[f_model].browse(ids).read([f_use])]
  76. _fields = env[f_model]._fields
  77. if (f_name in _fields) and _fields[f_name].type == 'many2many':
  78. return ids
  79. f_val = False
  80. if len(ids):
  81. f_val = ids[0]
  82. if isinstance(f_val, tuple):
  83. f_val = f_val[0]
  84. return f_val
  85. if a_eval := node.get('eval'):
  86. idref2 = _get_idref(self, env, f_model, self.idref)
  87. try:
  88. return safe_eval(a_eval, idref2)
  89. except Exception:
  90. logging.getLogger('odoo.tools.convert.init').error(
  91. 'Could not eval(%s) for %s in %s', a_eval, node.get('name'), env.context)
  92. raise
  93. def _process(s):
  94. matches = re.finditer(br'[^%]%\((.*?)\)[ds]'.decode('utf-8'), s)
  95. done = set()
  96. for m in matches:
  97. found = m.group()[1:]
  98. if found in done:
  99. continue
  100. done.add(found)
  101. id = m.groups()[0]
  102. if not id in self.idref:
  103. self.idref[id] = self.id_get(id)
  104. # So funny story: in Python 3, bytes(n: int) returns a
  105. # bytestring of n nuls. In Python 2 it obviously returns the
  106. # stringified number, which is what we're expecting here
  107. s = s.replace(found, str(self.idref[id]))
  108. s = s.replace('%%', '%') # Quite weird but it's for (somewhat) backward compatibility sake
  109. return s
  110. if t == 'xml':
  111. _fix_multiple_roots(node)
  112. return '<?xml version="1.0"?>\n'\
  113. +_process("".join(etree.tostring(n, encoding='unicode') for n in node))
  114. if t == 'html':
  115. return _process("".join(etree.tostring(n, method='html', encoding='unicode') for n in node))
  116. if node.get('file'):
  117. if t == 'base64':
  118. with file_open(node.get('file'), 'rb', env=env) as f:
  119. return base64.b64encode(f.read())
  120. with file_open(node.get('file'), env=env) as f:
  121. data = f.read()
  122. else:
  123. data = node.text or ''
  124. match t:
  125. case 'file':
  126. path = data.strip()
  127. try:
  128. file_path(os.path.join(self.module, path))
  129. except FileNotFoundError:
  130. raise FileNotFoundError(
  131. f"No such file or directory: {path!r} in {self.module}"
  132. ) from None
  133. return '%s,%s' % (self.module, path)
  134. case 'char':
  135. return data
  136. case 'int':
  137. d = data.strip()
  138. if d == 'None':
  139. return None
  140. return int(d)
  141. case 'float':
  142. return float(data.strip())
  143. case 'list':
  144. return [_eval_xml(self, n, env) for n in node.iterchildren('value')]
  145. case 'tuple':
  146. return tuple(_eval_xml(self, n, env) for n in node.iterchildren('value'))
  147. case 'base64':
  148. raise ValueError("base64 type is only compatible with file data")
  149. case t:
  150. raise ValueError(f"Unknown type {t!r}")
  151. elif node.tag == "function":
  152. model_str = node.get('model')
  153. model = env[model_str]
  154. method_name = node.get('name')
  155. # determine arguments
  156. args = []
  157. kwargs = {}
  158. if a_eval := node.get('eval'):
  159. idref2 = _get_idref(self, env, model_str, self.idref)
  160. args = list(safe_eval(a_eval, idref2))
  161. for child in node:
  162. if child.tag == 'value' and child.get('name'):
  163. kwargs[child.get('name')] = _eval_xml(self, child, env)
  164. else:
  165. args.append(_eval_xml(self, child, env))
  166. # merge current context with context in kwargs
  167. kwargs['context'] = {**env.context, **kwargs.get('context', {})}
  168. # invoke method
  169. return odoo.api.call_kw(model, method_name, args, kwargs)
  170. elif node.tag == "test":
  171. return node.text
  172. def str2bool(value):
  173. return value.lower() not in ('0', 'false', 'off')
  174. def nodeattr2bool(node, attr, default=False):
  175. if not node.get(attr):
  176. return default
  177. val = node.get(attr).strip()
  178. if not val:
  179. return default
  180. return str2bool(val)
  181. class xml_import(object):
  182. def get_env(self, node, eval_context=None):
  183. uid = node.get('uid')
  184. context = node.get('context')
  185. if uid or context:
  186. return self.env(
  187. user=uid and self.id_get(uid),
  188. context=context and {
  189. **self.env.context,
  190. **safe_eval(context, {
  191. 'ref': self.id_get,
  192. **(eval_context or {})
  193. }),
  194. }
  195. )
  196. return self.env
  197. def make_xml_id(self, xml_id):
  198. if not xml_id or '.' in xml_id:
  199. return xml_id
  200. return "%s.%s" % (self.module, xml_id)
  201. def _test_xml_id(self, xml_id):
  202. if '.' in xml_id:
  203. module, id = xml_id.split('.', 1)
  204. assert '.' not in id, """The ID reference "%s" must contain
  205. maximum one dot. They are used to refer to other modules ID, in the
  206. form: module.record_id""" % (xml_id,)
  207. if module != self.module:
  208. modcnt = self.env['ir.module.module'].search_count([('name', '=', module), ('state', '=', 'installed')])
  209. assert modcnt == 1, """The ID "%s" refers to an uninstalled module""" % (xml_id,)
  210. def _tag_delete(self, rec):
  211. d_model = rec.get("model")
  212. records = self.env[d_model]
  213. if d_search := rec.get("search"):
  214. idref = _get_idref(self, self.env, d_model, {})
  215. try:
  216. records = records.search(safe_eval(d_search, idref))
  217. except ValueError:
  218. _logger.warning('Skipping deletion for failed search `%r`', d_search, exc_info=True)
  219. if d_id := rec.get("id"):
  220. try:
  221. records += records.browse(self.id_get(d_id))
  222. except ValueError:
  223. # d_id cannot be found. doesn't matter in this case
  224. _logger.warning('Skipping deletion for missing XML ID `%r`', d_id, exc_info=True)
  225. if records:
  226. records.unlink()
  227. def _tag_function(self, rec):
  228. if self.noupdate and self.mode != 'init':
  229. return
  230. env = self.get_env(rec)
  231. _eval_xml(self, rec, env)
  232. def _tag_menuitem(self, rec, parent=None):
  233. rec_id = rec.attrib["id"]
  234. self._test_xml_id(rec_id)
  235. # The parent attribute was specified, if non-empty determine its ID, otherwise
  236. # explicitly make a top-level menu
  237. values = {
  238. 'parent_id': False,
  239. 'active': nodeattr2bool(rec, 'active', default=True),
  240. }
  241. if rec.get('sequence'):
  242. values['sequence'] = int(rec.get('sequence'))
  243. if parent is not None:
  244. values['parent_id'] = parent
  245. elif rec.get('parent'):
  246. values['parent_id'] = self.id_get(rec.attrib['parent'])
  247. elif rec.get('web_icon'):
  248. values['web_icon'] = rec.attrib['web_icon']
  249. if rec.get('name'):
  250. values['name'] = rec.attrib['name']
  251. if rec.get('action'):
  252. a_action = rec.attrib['action']
  253. if '.' not in a_action:
  254. a_action = '%s.%s' % (self.module, a_action)
  255. act = self.env.ref(a_action).sudo()
  256. values['action'] = "%s,%d" % (act.type, act.id)
  257. if not values.get('name') and act.type.endswith(('act_window', 'wizard', 'url', 'client', 'server')) and act.name:
  258. values['name'] = act.name
  259. if not values.get('name'):
  260. values['name'] = rec_id or '?'
  261. groups = []
  262. for group in rec.get('groups', '').split(','):
  263. if group.startswith('-'):
  264. group_id = self.id_get(group[1:])
  265. groups.append(odoo.Command.unlink(group_id))
  266. elif group:
  267. group_id = self.id_get(group)
  268. groups.append(odoo.Command.link(group_id))
  269. if groups:
  270. values['groups_id'] = groups
  271. data = {
  272. 'xml_id': self.make_xml_id(rec_id),
  273. 'values': values,
  274. 'noupdate': self.noupdate,
  275. }
  276. menu = self.env['ir.ui.menu']._load_records([data], self.mode == 'update')
  277. for child in rec.iterchildren('menuitem'):
  278. self._tag_menuitem(child, parent=menu.id)
  279. def _tag_record(self, rec, extra_vals=None):
  280. rec_model = rec.get("model")
  281. env = self.get_env(rec)
  282. rec_id = rec.get("id", '')
  283. model = env[rec_model]
  284. if self.xml_filename and rec_id:
  285. model = model.with_context(
  286. install_module=self.module,
  287. install_filename=self.xml_filename,
  288. install_xmlid=rec_id,
  289. )
  290. self._test_xml_id(rec_id)
  291. xid = self.make_xml_id(rec_id)
  292. # in update mode, the record won't be updated if the data node explicitly
  293. # opt-out using @noupdate="1". A second check will be performed in
  294. # model._load_records() using the record's ir.model.data `noupdate` field.
  295. if self.noupdate and self.mode != 'init':
  296. # check if the xml record has no id, skip
  297. if not rec_id:
  298. return None
  299. if record := env['ir.model.data']._load_xmlid(xid):
  300. # if the resource already exists, don't update it but store
  301. # its database id (can be useful)
  302. self.idref[rec_id] = record.id
  303. return None
  304. elif not nodeattr2bool(rec, 'forcecreate', True):
  305. # if it doesn't exist and we shouldn't create it, skip it
  306. return None
  307. # else create it normally
  308. foreign_record_to_create = False
  309. if xid and xid.partition('.')[0] != self.module:
  310. # updating a record created by another module
  311. record = self.env['ir.model.data']._load_xmlid(xid)
  312. if not record and not (foreign_record_to_create := nodeattr2bool(rec, 'forcecreate')): # Allow foreign records if explicitely stated
  313. if self.noupdate and not nodeattr2bool(rec, 'forcecreate', True):
  314. # if it doesn't exist and we shouldn't create it, skip it
  315. return None
  316. raise Exception("Cannot update missing record %r" % xid)
  317. res = {}
  318. sub_records = []
  319. for field in rec.iterchildren('field'):
  320. #TODO: most of this code is duplicated above (in _eval_xml)...
  321. f_name = field.get("name")
  322. f_model = field.get("model")
  323. if not f_model and f_name in model._fields:
  324. f_model = model._fields[f_name].comodel_name
  325. f_use = field.get("use",'') or 'id'
  326. f_val = False
  327. if f_search := field.get("search"):
  328. idref2 = _get_idref(self, env, f_model, self.idref)
  329. q = safe_eval(f_search, idref2)
  330. assert f_model, 'Define an attribute model="..." in your .XML file!'
  331. # browse the objects searched
  332. s = env[f_model].search(q)
  333. # column definitions of the "local" object
  334. _fields = env[rec_model]._fields
  335. # if the current field is many2many
  336. if (f_name in _fields) and _fields[f_name].type == 'many2many':
  337. f_val = [odoo.Command.set([x[f_use] for x in s])]
  338. elif len(s):
  339. # otherwise (we are probably in a many2one field),
  340. # take the first element of the search
  341. f_val = s[0][f_use]
  342. elif f_ref := field.get("ref"):
  343. if f_name in model._fields and model._fields[f_name].type == 'reference':
  344. val = self.model_id_get(f_ref)
  345. f_val = val[0] + ',' + str(val[1])
  346. else:
  347. f_val = self.id_get(f_ref, raise_if_not_found=nodeattr2bool(rec, 'forcecreate', True))
  348. if not f_val:
  349. _logger.warning("Skipping creation of %r because %s=%r could not be resolved", xid, f_name, f_ref)
  350. return None
  351. else:
  352. f_val = _eval_xml(self, field, env)
  353. if f_name in model._fields:
  354. field_type = model._fields[f_name].type
  355. if field_type == 'many2one':
  356. f_val = int(f_val) if f_val else False
  357. elif field_type == 'integer':
  358. f_val = int(f_val)
  359. elif field_type in ('float', 'monetary'):
  360. f_val = float(f_val)
  361. elif field_type == 'boolean' and isinstance(f_val, str):
  362. f_val = str2bool(f_val)
  363. elif field_type == 'one2many':
  364. for child in field.iterchildren('record'):
  365. sub_records.append((child, model._fields[f_name].inverse_name))
  366. if isinstance(f_val, str):
  367. # We do not want to write on the field since we will write
  368. # on the childrens' parents later
  369. continue
  370. elif field_type == 'html':
  371. if field.get('type') == 'xml':
  372. _logger.warning('HTML field %r is declared as `type="xml"`', f_name)
  373. res[f_name] = f_val
  374. if extra_vals:
  375. res.update(extra_vals)
  376. if 'sequence' not in res and 'sequence' in model._fields:
  377. sequence = self.next_sequence()
  378. if sequence:
  379. res['sequence'] = sequence
  380. data = dict(xml_id=xid, values=res, noupdate=self.noupdate)
  381. if foreign_record_to_create:
  382. model = model.with_context(foreign_record_to_create=foreign_record_to_create)
  383. record = model._load_records([data], self.mode == 'update')
  384. if rec_id:
  385. self.idref[rec_id] = record.id
  386. if config.get('import_partial'):
  387. env.cr.commit()
  388. for child_rec, inverse_name in sub_records:
  389. self._tag_record(child_rec, extra_vals={inverse_name: record.id})
  390. return rec_model, record.id
  391. def _tag_template(self, el):
  392. # This helper transforms a <template> element into a <record> and forwards it
  393. tpl_id = el.get('id', el.get('t-name'))
  394. full_tpl_id = tpl_id
  395. if '.' not in full_tpl_id:
  396. full_tpl_id = '%s.%s' % (self.module, tpl_id)
  397. # set the full template name for qweb <module>.<id>
  398. if not el.get('inherit_id'):
  399. el.set('t-name', full_tpl_id)
  400. el.tag = 't'
  401. else:
  402. el.tag = 'data'
  403. el.attrib.pop('id', None)
  404. if self.module.startswith('theme_'):
  405. model = 'theme.ir.ui.view'
  406. else:
  407. model = 'ir.ui.view'
  408. record_attrs = {
  409. 'id': tpl_id,
  410. 'model': model,
  411. }
  412. for att in ['forcecreate', 'context']:
  413. if att in el.attrib:
  414. record_attrs[att] = el.attrib.pop(att)
  415. Field = builder.E.field
  416. name = el.get('name', tpl_id)
  417. record = etree.Element('record', attrib=record_attrs)
  418. record.append(Field(name, name='name'))
  419. record.append(Field(full_tpl_id, name='key'))
  420. record.append(Field("qweb", name='type'))
  421. if 'track' in el.attrib:
  422. record.append(Field(el.get('track'), name='track'))
  423. if 'priority' in el.attrib:
  424. record.append(Field(el.get('priority'), name='priority'))
  425. if 'inherit_id' in el.attrib:
  426. record.append(Field(name='inherit_id', ref=el.get('inherit_id')))
  427. if 'website_id' in el.attrib:
  428. record.append(Field(name='website_id', ref=el.get('website_id')))
  429. if 'key' in el.attrib:
  430. record.append(Field(el.get('key'), name='key'))
  431. if el.get('active') in ("True", "False"):
  432. view_id = self.id_get(tpl_id, raise_if_not_found=False)
  433. if self.mode != "update" or not view_id:
  434. record.append(Field(name='active', eval=el.get('active')))
  435. if el.get('customize_show') in ("True", "False"):
  436. record.append(Field(name='customize_show', eval=el.get('customize_show')))
  437. groups = el.attrib.pop('groups', None)
  438. if groups:
  439. grp_lst = [("ref('%s')" % x) for x in groups.split(',')]
  440. record.append(Field(name="groups_id", eval="[Command.set(["+', '.join(grp_lst)+"])]"))
  441. if el.get('primary') == 'True':
  442. # Pseudo clone mode, we'll set the t-name to the full canonical xmlid
  443. el.append(
  444. builder.E.xpath(
  445. builder.E.attribute(full_tpl_id, name='t-name'),
  446. expr=".",
  447. position="attributes",
  448. )
  449. )
  450. record.append(Field('primary', name='mode'))
  451. # inject complete <template> element (after changing node name) into
  452. # the ``arch`` field
  453. record.append(Field(el, name="arch", type="xml"))
  454. return self._tag_record(record)
  455. def id_get(self, id_str, raise_if_not_found=True):
  456. if id_str in self.idref:
  457. return self.idref[id_str]
  458. res = self.model_id_get(id_str, raise_if_not_found)
  459. return res and res[1]
  460. def model_id_get(self, id_str, raise_if_not_found=True):
  461. if '.' not in id_str:
  462. id_str = '%s.%s' % (self.module, id_str)
  463. return self.env['ir.model.data']._xmlid_to_res_model_res_id(id_str, raise_if_not_found=raise_if_not_found)
  464. def _tag_root(self, el):
  465. for rec in el:
  466. f = self._tags.get(rec.tag)
  467. if f is None:
  468. continue
  469. self.envs.append(self.get_env(el))
  470. self._noupdate.append(nodeattr2bool(el, 'noupdate', self.noupdate))
  471. self._sequences.append(0 if nodeattr2bool(el, 'auto_sequence', False) else None)
  472. try:
  473. f(rec)
  474. except ParseError:
  475. raise
  476. except ValidationError as err:
  477. msg = "while parsing {file}:{viewline}\n{err}\n\nView error context:\n{context}\n".format(
  478. file=rec.getroottree().docinfo.URL,
  479. viewline=rec.sourceline,
  480. context=pprint.pformat(getattr(err, 'context', None) or '-no context-'),
  481. err=err.args[0],
  482. )
  483. _logger.debug(msg, exc_info=True)
  484. raise ParseError(msg) from None # Restart with "--log-handler odoo.tools.convert:DEBUG" for complete traceback
  485. except Exception as e:
  486. raise ParseError('while parsing %s:%s, somewhere inside\n%s' % (
  487. rec.getroottree().docinfo.URL,
  488. rec.sourceline,
  489. etree.tostring(rec, encoding='unicode').rstrip()
  490. )) from e
  491. finally:
  492. self._noupdate.pop()
  493. self.envs.pop()
  494. self._sequences.pop()
  495. @property
  496. def env(self):
  497. return self.envs[-1]
  498. @property
  499. def noupdate(self):
  500. return self._noupdate[-1]
  501. def next_sequence(self):
  502. value = self._sequences[-1]
  503. if value is not None:
  504. value = self._sequences[-1] = value + 10
  505. return value
  506. def __init__(self, env, module, idref, mode, noupdate=False, xml_filename=None):
  507. self.mode = mode
  508. self.module = module
  509. self.envs = [env(context=dict(env.context, lang=None))]
  510. self.idref = {} if idref is None else idref
  511. self._noupdate = [noupdate]
  512. self._sequences = []
  513. self.xml_filename = xml_filename
  514. self._tags = {
  515. 'record': self._tag_record,
  516. 'delete': self._tag_delete,
  517. 'function': self._tag_function,
  518. 'menuitem': self._tag_menuitem,
  519. 'template': self._tag_template,
  520. **dict.fromkeys(self.DATA_ROOTS, self._tag_root)
  521. }
  522. def parse(self, de):
  523. assert de.tag in self.DATA_ROOTS, "Root xml tag must be <openerp>, <odoo> or <data>."
  524. self._tag_root(de)
  525. DATA_ROOTS = ['odoo', 'data', 'openerp']
  526. def convert_file(env, module, filename, idref, mode='update', noupdate=False, kind=None, pathname=None):
  527. if pathname is None:
  528. pathname = os.path.join(module, filename)
  529. ext = os.path.splitext(filename)[1].lower()
  530. with file_open(pathname, 'rb') as fp:
  531. if ext == '.csv':
  532. convert_csv_import(env, module, pathname, fp.read(), idref, mode, noupdate)
  533. elif ext == '.sql':
  534. convert_sql_import(env, fp)
  535. elif ext == '.xml':
  536. convert_xml_import(env, module, fp, idref, mode, noupdate)
  537. elif ext == '.js':
  538. pass # .js files are valid but ignored here.
  539. else:
  540. raise ValueError("Can't load unknown file type %s.", filename)
  541. def convert_sql_import(env, fp):
  542. env.cr.execute(fp.read()) # pylint: disable=sql-injection
  543. def convert_csv_import(env, module, fname, csvcontent, idref=None, mode='init',
  544. noupdate=False):
  545. '''Import csv file :
  546. quote: "
  547. delimiter: ,
  548. encoding: utf-8'''
  549. env = env(context=dict(env.context, lang=None))
  550. filename, _ext = os.path.splitext(os.path.basename(fname))
  551. model = filename.split('-')[0]
  552. reader = csv.reader(io.StringIO(csvcontent.decode()), quotechar='"', delimiter=',')
  553. fields = next(reader)
  554. if not (mode == 'init' or 'id' in fields):
  555. _logger.error("Import specification does not contain 'id' and we are in init mode, Cannot continue.")
  556. return
  557. # filter out empty lines (any([]) == False) and lines containing only empty cells
  558. datas = [
  559. line for line in reader
  560. if any(line)
  561. ]
  562. context = {
  563. 'mode': mode,
  564. 'module': module,
  565. 'install_module': module,
  566. 'install_filename': fname,
  567. 'noupdate': noupdate,
  568. }
  569. result = env[model].with_context(**context).load(fields, datas)
  570. if any(msg['type'] == 'error' for msg in result['messages']):
  571. # Report failed import and abort module install
  572. warning_msg = "\n".join(msg['message'] for msg in result['messages'])
  573. raise Exception(env._(
  574. "Module loading %(module)s failed: file %(file)s could not be processed:\n%(message)s",
  575. module=module,
  576. file=fname,
  577. message=warning_msg,
  578. ))
  579. def convert_xml_import(env, module, xmlfile, idref=None, mode='init', noupdate=False, report=None):
  580. doc = etree.parse(xmlfile)
  581. schema = os.path.join(config['root_path'], 'import_xml.rng')
  582. relaxng = etree.RelaxNG(etree.parse(schema))
  583. try:
  584. relaxng.assert_(doc)
  585. except Exception:
  586. _logger.exception("The XML file '%s' does not fit the required schema!", xmlfile.name)
  587. if jingtrang:
  588. p = subprocess.run(['pyjing', schema, xmlfile.name], stdout=subprocess.PIPE)
  589. _logger.warning(p.stdout.decode())
  590. else:
  591. for e in relaxng.error_log:
  592. _logger.warning(e)
  593. _logger.info("Install 'jingtrang' for more precise and useful validation messages.")
  594. raise
  595. if isinstance(xmlfile, str):
  596. xml_filename = xmlfile
  597. else:
  598. xml_filename = xmlfile.name
  599. obj = xml_import(env, module, idref, mode, noupdate=noupdate, xml_filename=xml_filename)
  600. obj.parse(doc.getroot())
上海开阖软件有限公司 沪ICP备12045867号-1