jack2 codebase
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

849 lines
20KB

  1. #!/usr/bin/env python
  2. # encoding: utf-8
  3. # Thomas Nagy, 2005-2010 (ita)
  4. """
  5. Node: filesystem structure, contains lists of nodes
  6. #. Each file/folder is represented by exactly one node.
  7. #. Some potential class properties are stored on :py:class:`waflib.Build.BuildContext` : nodes to depend on, etc.
  8. Unused class members can increase the `.wafpickle` file size sensibly.
  9. #. Node objects should never be created directly, use
  10. the methods :py:func:`Node.make_node` or :py:func:`Node.find_node`
  11. #. The methods :py:func:`Node.find_resource`, :py:func:`Node.find_dir` :py:func:`Node.find_or_declare` should be
  12. used when a build context is present
  13. #. Each instance of :py:class:`waflib.Context.Context` has a unique :py:class:`Node` subclass.
  14. (:py:class:`waflib.Node.Nod3`, see the :py:class:`waflib.Context.Context` initializer). A reference to the context owning a node is held as self.ctx
  15. """
  16. import os, re, sys, shutil
  17. from waflib import Utils, Errors
  18. exclude_regs = '''
  19. **/*~
  20. **/#*#
  21. **/.#*
  22. **/%*%
  23. **/._*
  24. **/CVS
  25. **/CVS/**
  26. **/.cvsignore
  27. **/SCCS
  28. **/SCCS/**
  29. **/vssver.scc
  30. **/.svn
  31. **/.svn/**
  32. **/BitKeeper
  33. **/.git
  34. **/.git/**
  35. **/.gitignore
  36. **/.bzr
  37. **/.bzrignore
  38. **/.bzr/**
  39. **/.hg
  40. **/.hg/**
  41. **/_MTN
  42. **/_MTN/**
  43. **/.arch-ids
  44. **/{arch}
  45. **/_darcs
  46. **/_darcs/**
  47. **/.intlcache
  48. **/.DS_Store'''
  49. """
  50. Ant patterns for files and folders to exclude while doing the
  51. recursive traversal in :py:meth:`waflib.Node.Node.ant_glob`
  52. """
  53. # TODO remove in waf 1.9
  54. split_path = Utils.split_path
  55. split_path_unix = Utils.split_path_unix
  56. split_path_cygwin = Utils.split_path_cygwin
  57. split_path_win32 = Utils.split_path_win32
  58. class Node(object):
  59. """
  60. This class is organized in two parts
  61. * The basic methods meant for filesystem access (compute paths, create folders, etc)
  62. * The methods bound to a :py:class:`waflib.Build.BuildContext` (require ``bld.srcnode`` and ``bld.bldnode``)
  63. The Node objects are not thread safe in any way.
  64. """
  65. dict_class = dict
  66. __slots__ = ('name', 'sig', 'children', 'parent', 'cache_abspath', 'cache_isdir', 'cache_sig')
  67. def __init__(self, name, parent):
  68. self.name = name
  69. self.parent = parent
  70. if parent:
  71. if name in parent.children:
  72. raise Errors.WafError('node %s exists in the parent files %r already' % (name, parent))
  73. parent.children[name] = self
  74. def __setstate__(self, data):
  75. "Deserializes from data"
  76. self.name = data[0]
  77. self.parent = data[1]
  78. if data[2] is not None:
  79. # Issue 1480
  80. self.children = self.dict_class(data[2])
  81. if data[3] is not None:
  82. self.sig = data[3]
  83. def __getstate__(self):
  84. "Serialize the node info"
  85. return (self.name, self.parent, getattr(self, 'children', None), getattr(self, 'sig', None))
  86. def __str__(self):
  87. "String representation (name), for debugging purposes"
  88. return self.name
  89. def __repr__(self):
  90. "String representation (abspath), for debugging purposes"
  91. return self.abspath()
  92. def __hash__(self):
  93. "Node hash, used for storage in dicts. This hash is not persistent."
  94. return id(self)
  95. def __eq__(self, node):
  96. "Node comparison, based on the IDs"
  97. return id(self) == id(node)
  98. def __copy__(self):
  99. "Implemented to prevent nodes from being copied (raises an exception)"
  100. raise Errors.WafError('nodes are not supposed to be copied')
  101. def read(self, flags='r', encoding='ISO8859-1'):
  102. """
  103. Return the contents of the file represented by this node::
  104. def build(bld):
  105. bld.path.find_node('wscript').read()
  106. :type fname: string
  107. :param fname: Path to file
  108. :type m: string
  109. :param m: Open mode
  110. :rtype: string
  111. :return: File contents
  112. """
  113. return Utils.readf(self.abspath(), flags, encoding)
  114. def write(self, data, flags='w', encoding='ISO8859-1'):
  115. """
  116. Write some text to the physical file represented by this node::
  117. def build(bld):
  118. bld.path.make_node('foo.txt').write('Hello, world!')
  119. :type data: string
  120. :param data: data to write
  121. :type flags: string
  122. :param flags: Write mode
  123. """
  124. Utils.writef(self.abspath(), data, flags, encoding)
  125. def read_json(self, convert=True, encoding='utf-8'):
  126. """
  127. Read and parse the contents of this node as JSON::
  128. def build(bld):
  129. bld.path.find_node('abc.json').read_json()
  130. Note that this by default automatically decodes unicode strings on Python2, unlike what the Python JSON module does.
  131. :type convert: boolean
  132. :param convert: Prevents decoding of unicode strings on Python2
  133. :type encoding: string
  134. :param encoding: The encoding of the file to read. This default to UTF8 as per the JSON standard
  135. :rtype: object
  136. :return: Parsed file contents
  137. """
  138. import json # Python 2.6 and up
  139. object_pairs_hook = None
  140. if convert and sys.hexversion < 0x3000000:
  141. try:
  142. _type = unicode
  143. except NameError:
  144. _type = str
  145. def convert(value):
  146. if isinstance(value, list):
  147. return [convert(element) for element in value]
  148. elif isinstance(value, _type):
  149. return str(value)
  150. else:
  151. return value
  152. def object_pairs(pairs):
  153. return dict((str(pair[0]), convert(pair[1])) for pair in pairs)
  154. object_pairs_hook = object_pairs
  155. return json.loads(self.read(encoding=encoding), object_pairs_hook=object_pairs_hook)
  156. def write_json(self, data, pretty=True):
  157. """
  158. Writes a python object as JSON to disk. Files are always written as UTF8 as per the JSON standard::
  159. def build(bld):
  160. bld.path.find_node('xyz.json').write_json(199)
  161. :type data: object
  162. :param data: The data to write to disk
  163. :type pretty: boolean
  164. :param pretty: Determines if the JSON will be nicely space separated
  165. """
  166. import json # Python 2.6 and up
  167. indent = 2
  168. separators = (',', ': ')
  169. sort_keys = pretty
  170. newline = os.linesep
  171. if not pretty:
  172. indent = None
  173. separators = (',', ':')
  174. newline = ''
  175. output = json.dumps(data, indent=indent, separators=separators, sort_keys=sort_keys) + newline
  176. self.write(output, encoding='utf-8')
  177. def chmod(self, val):
  178. """
  179. Change file/dir permissions::
  180. def build(bld):
  181. bld.path.chmod(493) # 0755
  182. """
  183. os.chmod(self.abspath(), val)
  184. def delete(self):
  185. """Delete the file/folder, and remove this node from the tree. Do not use this object after calling this method."""
  186. try:
  187. try:
  188. if hasattr(self, 'children'):
  189. shutil.rmtree(self.abspath())
  190. else:
  191. os.remove(self.abspath())
  192. except OSError as e:
  193. if os.path.exists(self.abspath()):
  194. raise e
  195. finally:
  196. self.evict()
  197. def evict(self):
  198. """Internal - called when a node is removed"""
  199. del self.parent.children[self.name]
  200. def suffix(self):
  201. """Return the file extension"""
  202. k = max(0, self.name.rfind('.'))
  203. return self.name[k:]
  204. def height(self):
  205. """Depth in the folder hierarchy from the filesystem root or from all the file drives"""
  206. d = self
  207. val = -1
  208. while d:
  209. d = d.parent
  210. val += 1
  211. return val
  212. def listdir(self):
  213. """List the folder contents"""
  214. lst = Utils.listdir(self.abspath())
  215. lst.sort()
  216. return lst
  217. def mkdir(self):
  218. """
  219. Create a folder represented by this node, creating intermediate nodes as needed
  220. An exception will be raised only when the folder cannot possibly exist there
  221. """
  222. if getattr(self, 'cache_isdir', None):
  223. return
  224. try:
  225. self.parent.mkdir()
  226. except OSError:
  227. pass
  228. if self.name:
  229. try:
  230. os.makedirs(self.abspath())
  231. except OSError:
  232. pass
  233. if not os.path.isdir(self.abspath()):
  234. raise Errors.WafError('Could not create the directory %s' % self.abspath())
  235. try:
  236. self.children
  237. except AttributeError:
  238. self.children = self.dict_class()
  239. self.cache_isdir = True
  240. def find_node(self, lst):
  241. """
  242. Find a node on the file system (files or folders), create intermediate nodes as needed
  243. :param lst: path
  244. :type lst: string or list of string
  245. """
  246. if isinstance(lst, str):
  247. lst = [x for x in split_path(lst) if x and x != '.']
  248. cur = self
  249. for x in lst:
  250. if x == '..':
  251. cur = cur.parent or cur
  252. continue
  253. try:
  254. ch = cur.children
  255. except AttributeError:
  256. cur.children = self.dict_class()
  257. else:
  258. try:
  259. cur = ch[x]
  260. continue
  261. except KeyError:
  262. pass
  263. # optimistic: create the node first then look if it was correct to do so
  264. cur = self.__class__(x, cur)
  265. try:
  266. os.stat(cur.abspath())
  267. except OSError:
  268. cur.evict()
  269. return None
  270. ret = cur
  271. try:
  272. os.stat(ret.abspath())
  273. except OSError:
  274. ret.evict()
  275. return None
  276. try:
  277. while not getattr(cur.parent, 'cache_isdir', None):
  278. cur = cur.parent
  279. cur.cache_isdir = True
  280. except AttributeError:
  281. pass
  282. return ret
  283. def make_node(self, lst):
  284. """
  285. Find or create a node without looking on the filesystem
  286. :param lst: path
  287. :type lst: string or list of string
  288. """
  289. if isinstance(lst, str):
  290. lst = [x for x in split_path(lst) if x and x != '.']
  291. cur = self
  292. for x in lst:
  293. if x == '..':
  294. cur = cur.parent or cur
  295. continue
  296. if getattr(cur, 'children', {}):
  297. if x in cur.children:
  298. cur = cur.children[x]
  299. continue
  300. else:
  301. cur.children = self.dict_class()
  302. cur = self.__class__(x, cur)
  303. return cur
  304. def search_node(self, lst):
  305. """
  306. Search for a node without looking on the filesystem
  307. :param lst: path
  308. :type lst: string or list of string
  309. """
  310. if isinstance(lst, str):
  311. lst = [x for x in split_path(lst) if x and x != '.']
  312. cur = self
  313. for x in lst:
  314. if x == '..':
  315. cur = cur.parent or cur
  316. else:
  317. try:
  318. cur = cur.children[x]
  319. except (AttributeError, KeyError):
  320. return None
  321. return cur
  322. def path_from(self, node):
  323. """
  324. Path of this node seen from the other::
  325. def build(bld):
  326. n1 = bld.path.find_node('foo/bar/xyz.txt')
  327. n2 = bld.path.find_node('foo/stuff/')
  328. n1.path_from(n2) # '../bar/xyz.txt'
  329. :param node: path to use as a reference
  330. :type node: :py:class:`waflib.Node.Node`
  331. """
  332. c1 = self
  333. c2 = node
  334. c1h = c1.height()
  335. c2h = c2.height()
  336. lst = []
  337. up = 0
  338. while c1h > c2h:
  339. lst.append(c1.name)
  340. c1 = c1.parent
  341. c1h -= 1
  342. while c2h > c1h:
  343. up += 1
  344. c2 = c2.parent
  345. c2h -= 1
  346. while id(c1) != id(c2):
  347. lst.append(c1.name)
  348. up += 1
  349. c1 = c1.parent
  350. c2 = c2.parent
  351. if c1.parent:
  352. for i in range(up):
  353. lst.append('..')
  354. else:
  355. if lst and not Utils.is_win32:
  356. lst.append('')
  357. lst.reverse()
  358. return os.sep.join(lst) or '.'
  359. def abspath(self):
  360. """
  361. Absolute path. A cache is kept in the context as ``cache_node_abspath``
  362. """
  363. try:
  364. return self.cache_abspath
  365. except AttributeError:
  366. pass
  367. # think twice before touching this (performance + complexity + correctness)
  368. if not self.parent:
  369. val = os.sep
  370. elif not self.parent.name:
  371. val = os.sep + self.name
  372. else:
  373. val = self.parent.abspath() + os.sep + self.name
  374. self.cache_abspath = val
  375. return val
  376. if Utils.is_win32:
  377. def abspath(self):
  378. try:
  379. return self.cache_abspath
  380. except AttributeError:
  381. pass
  382. if not self.parent:
  383. val = ''
  384. elif not self.parent.name:
  385. val = self.name + os.sep
  386. else:
  387. val = self.parent.abspath().rstrip(os.sep) + os.sep + self.name
  388. self.cache_abspath = val
  389. return val
  390. def is_child_of(self, node):
  391. """
  392. Does this node belong to the subtree node?::
  393. def build(bld):
  394. node = bld.path.find_node('wscript')
  395. node.is_child_of(bld.path) # True
  396. :param node: path to use as a reference
  397. :type node: :py:class:`waflib.Node.Node`
  398. """
  399. p = self
  400. diff = self.height() - node.height()
  401. while diff > 0:
  402. diff -= 1
  403. p = p.parent
  404. return id(p) == id(node)
  405. def ant_iter(self, accept=None, maxdepth=25, pats=[], dir=False, src=True, remove=True):
  406. """
  407. Semi-private and recursive method used by ant_glob.
  408. :param accept: function used for accepting/rejecting a node, returns the patterns that can be still accepted in recursion
  409. :type accept: function
  410. :param maxdepth: maximum depth in the filesystem (25)
  411. :type maxdepth: int
  412. :param pats: list of patterns to accept and list of patterns to exclude
  413. :type pats: tuple
  414. :param dir: return folders too (False by default)
  415. :type dir: bool
  416. :param src: return files (True by default)
  417. :type src: bool
  418. :param remove: remove files/folders that do not exist (True by default)
  419. :type remove: bool
  420. """
  421. dircont = self.listdir()
  422. dircont.sort()
  423. try:
  424. lst = set(self.children.keys())
  425. except AttributeError:
  426. self.children = self.dict_class()
  427. else:
  428. if remove:
  429. for x in lst - set(dircont):
  430. self.children[x].evict()
  431. for name in dircont:
  432. npats = accept(name, pats)
  433. if npats and npats[0]:
  434. accepted = [] in npats[0]
  435. node = self.make_node([name])
  436. isdir = os.path.isdir(node.abspath())
  437. if accepted:
  438. if isdir:
  439. if dir:
  440. yield node
  441. else:
  442. if src:
  443. yield node
  444. if getattr(node, 'cache_isdir', None) or isdir:
  445. node.cache_isdir = True
  446. if maxdepth:
  447. for k in node.ant_iter(accept=accept, maxdepth=maxdepth - 1, pats=npats, dir=dir, src=src, remove=remove):
  448. yield k
  449. raise StopIteration
  450. def ant_glob(self, *k, **kw):
  451. """
  452. This method is used for finding files across folders. It behaves like ant patterns:
  453. * ``**/*`` find all files recursively
  454. * ``**/*.class`` find all files ending by .class
  455. * ``..`` find files having two dot characters
  456. For example::
  457. def configure(cfg):
  458. cfg.path.ant_glob('**/*.cpp') # find all .cpp files
  459. cfg.root.ant_glob('etc/*.txt') # using the filesystem root can be slow
  460. cfg.path.ant_glob('*.cpp', excl=['*.c'], src=True, dir=False)
  461. For more information see http://ant.apache.org/manual/dirtasks.html
  462. The nodes that correspond to files and folders that do not exist will be removed. To prevent this
  463. behaviour, pass 'remove=False'
  464. :param incl: ant patterns or list of patterns to include
  465. :type incl: string or list of strings
  466. :param excl: ant patterns or list of patterns to exclude
  467. :type excl: string or list of strings
  468. :param dir: return folders too (False by default)
  469. :type dir: bool
  470. :param src: return files (True by default)
  471. :type src: bool
  472. :param remove: remove files/folders that do not exist (True by default)
  473. :type remove: bool
  474. :param maxdepth: maximum depth of recursion
  475. :type maxdepth: int
  476. :param ignorecase: ignore case while matching (False by default)
  477. :type ignorecase: bool
  478. """
  479. src = kw.get('src', True)
  480. dir = kw.get('dir', False)
  481. excl = kw.get('excl', exclude_regs)
  482. incl = k and k[0] or kw.get('incl', '**')
  483. reflags = kw.get('ignorecase', 0) and re.I
  484. def to_pat(s):
  485. lst = Utils.to_list(s)
  486. ret = []
  487. for x in lst:
  488. x = x.replace('\\', '/').replace('//', '/')
  489. if x.endswith('/'):
  490. x += '**'
  491. lst2 = x.split('/')
  492. accu = []
  493. for k in lst2:
  494. if k == '**':
  495. accu.append(k)
  496. else:
  497. k = k.replace('.', '[.]').replace('*','.*').replace('?', '.').replace('+', '\\+')
  498. k = '^%s$' % k
  499. try:
  500. #print "pattern", k
  501. accu.append(re.compile(k, flags=reflags))
  502. except Exception as e:
  503. raise Errors.WafError("Invalid pattern: %s" % k, e)
  504. ret.append(accu)
  505. return ret
  506. def filtre(name, nn):
  507. ret = []
  508. for lst in nn:
  509. if not lst:
  510. pass
  511. elif lst[0] == '**':
  512. ret.append(lst)
  513. if len(lst) > 1:
  514. if lst[1].match(name):
  515. ret.append(lst[2:])
  516. else:
  517. ret.append([])
  518. elif lst[0].match(name):
  519. ret.append(lst[1:])
  520. return ret
  521. def accept(name, pats):
  522. nacc = filtre(name, pats[0])
  523. nrej = filtre(name, pats[1])
  524. if [] in nrej:
  525. nacc = []
  526. return [nacc, nrej]
  527. ret = [x for x in self.ant_iter(accept=accept, pats=[to_pat(incl), to_pat(excl)], maxdepth=kw.get('maxdepth', 25), dir=dir, src=src, remove=kw.get('remove', True))]
  528. if kw.get('flat', False):
  529. return ' '.join([x.path_from(self) for x in ret])
  530. return ret
  531. # --------------------------------------------------------------------------------
  532. # the following methods require the source/build folders (bld.srcnode/bld.bldnode)
  533. # using a subclass is a possibility, but is that really necessary?
  534. # --------------------------------------------------------------------------------
  535. def is_src(self):
  536. """
  537. True if the node is below the source directory
  538. note: !is_src does not imply is_bld()
  539. :rtype: bool
  540. """
  541. cur = self
  542. x = id(self.ctx.srcnode)
  543. y = id(self.ctx.bldnode)
  544. while cur.parent:
  545. if id(cur) == y:
  546. return False
  547. if id(cur) == x:
  548. return True
  549. cur = cur.parent
  550. return False
  551. def is_bld(self):
  552. """
  553. True if the node is below the build directory
  554. note: !is_bld does not imply is_src
  555. :rtype: bool
  556. """
  557. cur = self
  558. y = id(self.ctx.bldnode)
  559. while cur.parent:
  560. if id(cur) == y:
  561. return True
  562. cur = cur.parent
  563. return False
  564. def get_src(self):
  565. """
  566. Return the equivalent src node (or self if not possible)
  567. :rtype: :py:class:`waflib.Node.Node`
  568. """
  569. cur = self
  570. x = id(self.ctx.srcnode)
  571. y = id(self.ctx.bldnode)
  572. lst = []
  573. while cur.parent:
  574. if id(cur) == y:
  575. lst.reverse()
  576. return self.ctx.srcnode.make_node(lst)
  577. if id(cur) == x:
  578. return self
  579. lst.append(cur.name)
  580. cur = cur.parent
  581. return self
  582. def get_bld(self):
  583. """
  584. Return the equivalent bld node (or self if not possible)
  585. :rtype: :py:class:`waflib.Node.Node`
  586. """
  587. cur = self
  588. x = id(self.ctx.srcnode)
  589. y = id(self.ctx.bldnode)
  590. lst = []
  591. while cur.parent:
  592. if id(cur) == y:
  593. return self
  594. if id(cur) == x:
  595. lst.reverse()
  596. return self.ctx.bldnode.make_node(lst)
  597. lst.append(cur.name)
  598. cur = cur.parent
  599. # the file is external to the current project, make a fake root in the current build directory
  600. lst.reverse()
  601. if lst and Utils.is_win32 and len(lst[0]) == 2 and lst[0].endswith(':'):
  602. lst[0] = lst[0][0]
  603. return self.ctx.bldnode.make_node(['__root__'] + lst)
  604. def find_resource(self, lst):
  605. """
  606. Try to find a declared build node or a source file
  607. :param lst: path
  608. :type lst: string or list of string
  609. """
  610. if isinstance(lst, str):
  611. lst = [x for x in split_path(lst) if x and x != '.']
  612. node = self.get_bld().search_node(lst)
  613. if not node:
  614. self = self.get_src()
  615. node = self.find_node(lst)
  616. if node:
  617. if os.path.isdir(node.abspath()):
  618. return None
  619. return node
  620. def find_or_declare(self, lst):
  621. """
  622. if 'self' is in build directory, try to return an existing node
  623. if no node is found, go to the source directory
  624. try to find an existing node in the source directory
  625. if no node is found, create it in the build directory
  626. :param lst: path
  627. :type lst: string or list of string
  628. """
  629. if isinstance(lst, str):
  630. lst = [x for x in split_path(lst) if x and x != '.']
  631. node = self.get_bld().search_node(lst)
  632. if node:
  633. if not os.path.isfile(node.abspath()):
  634. node.sig = None
  635. node.parent.mkdir()
  636. return node
  637. self = self.get_src()
  638. node = self.find_node(lst)
  639. if node:
  640. if not os.path.isfile(node.abspath()):
  641. node.sig = None
  642. node.parent.mkdir()
  643. return node
  644. node = self.get_bld().make_node(lst)
  645. node.parent.mkdir()
  646. return node
  647. def find_dir(self, lst):
  648. """
  649. Search for a folder in the filesystem
  650. :param lst: path
  651. :type lst: string or list of string
  652. """
  653. if isinstance(lst, str):
  654. lst = [x for x in split_path(lst) if x and x != '.']
  655. node = self.find_node(lst)
  656. try:
  657. if not os.path.isdir(node.abspath()):
  658. return None
  659. except (OSError, AttributeError):
  660. # the node might be None, and raise an AttributeError
  661. return None
  662. return node
  663. # helpers for building things
  664. def change_ext(self, ext, ext_in=None):
  665. """
  666. :return: A build node of the same path, but with a different extension
  667. :rtype: :py:class:`waflib.Node.Node`
  668. """
  669. name = self.name
  670. if ext_in is None:
  671. k = name.rfind('.')
  672. if k >= 0:
  673. name = name[:k] + ext
  674. else:
  675. name = name + ext
  676. else:
  677. name = name[:- len(ext_in)] + ext
  678. return self.parent.find_or_declare([name])
  679. def bldpath(self):
  680. "Path seen from the build directory default/src/foo.cpp"
  681. return self.path_from(self.ctx.bldnode)
  682. def srcpath(self):
  683. "Path seen from the source directory ../src/foo.cpp"
  684. return self.path_from(self.ctx.srcnode)
  685. def relpath(self):
  686. "If a file in the build directory, bldpath, else srcpath"
  687. cur = self
  688. x = id(self.ctx.bldnode)
  689. while cur.parent:
  690. if id(cur) == x:
  691. return self.bldpath()
  692. cur = cur.parent
  693. return self.srcpath()
  694. def bld_dir(self):
  695. "Build path without the file name"
  696. return self.parent.bldpath()
  697. def get_bld_sig(self):
  698. """
  699. Node signature, assuming the file is in the build directory
  700. """
  701. try:
  702. return self.cache_sig
  703. except AttributeError:
  704. pass
  705. if not self.is_bld() or self.ctx.bldnode is self.ctx.srcnode:
  706. self.sig = Utils.h_file(self.abspath())
  707. self.cache_sig = ret = self.sig
  708. return ret
  709. pickle_lock = Utils.threading.Lock()
  710. """Lock mandatory for thread-safe node serialization"""
  711. class Nod3(Node):
  712. """Mandatory subclass for thread-safe node serialization"""
  713. pass # do not remove