3 # Copyright (c) 2010 The SCons Foundation
5 # Permission is hereby granted, free of charge, to any person obtaining
6 # a copy of this software and associated documentation files (the
7 # "Software"), to deal in the Software without restriction, including
8 # without limitation the rights to use, copy, modify, merge, publish,
9 # distribute, sublicense, and/or sell copies of the Software, and to
10 # permit persons to whom the Software is furnished to do so, subject to
11 # the following conditions:
13 # The above copyright notice and this permission notice shall be included
14 # in all copies or substantial portions of the Software.
16 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
17 # KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
18 # WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20 # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21 # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22 # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
25 # Module for handling SCons documentation processing.
29 This module parses home-brew XML files that document various things
30 in SCons. Right now, it handles Builders, functions, construction
31 variables, and Tools, but we expect it to get extended in the future.
33 In general, you can use any DocBook tag in the input, and this module
34 just adds processing various home-brew tags to try to make life a
39 <builder name="BUILDER">
41 <para>This is the summary description of an SCons Builder.
42 It will get placed in the man page,
43 and in the appropriate User's Guide appendix.
44 The name of this builder may be interpolated
45 anywhere in the document by specifying the
46 &b-BUILDER; element. A link to this definition may be
47 interpolated by specifying the &b-link-BUILDER; element.
50 Unlike normal XML, blank lines are significant in these
51 descriptions and serve to separate paragraphs.
52 They'll get replaced in DocBook output with appropriate tags
53 to indicate a new paragraph.
56 print("this is example code, it will be offset and indented")
63 <scons_function name="FUNCTION">
64 <arguments signature="SIGTYPE">
65 (arg1, arg2, key=value)
68 <para>This is the summary description of an SCons function.
69 It will get placed in the man page,
70 and in the appropriate User's Guide appendix.
71 If the "signature" attribute is specified, SIGTYPE may be one
72 of "global", "env" or "both" (the default if omitted is "both"),
73 to indicate the signature applies to the global form or the
74 environment form, or to generate both with the same signature
75 (excepting the insertion of "env.").
76 This allows for the cases of
77 describing that only one signature should be generated,
78 or both signatures should be generated and they differ,
79 or both signatures should be generated and they are the same.
80 The name of this function may be interpolated
81 anywhere in the document by specifying the
82 &f-FUNCTION; element or the &f-env-FUNCTION; element.
83 Links to this definition may be interpolated by specifying
84 the &f-link-FUNCTION: or &f-link-env-FUNCTION; element.
88 print("this is example code, it will be offset and indented")
93 Construction variable example:
95 <cvar name="VARIABLE">
97 <para>This is the summary description of a construction variable.
98 It will get placed in the man page,
99 and in the appropriate User's Guide appendix.
100 The name of this construction variable may be interpolated
101 anywhere in the document by specifying the
102 &cv-VARIABLE; element. A link to this definition may be
103 interpolated by specifying the &cv-link-VARIABLE; element.
107 print("this is example code, it will be offset and indented")
116 <para>This is the summary description of an SCons Tool.
117 It will get placed in the man page,
118 and in the appropriate User's Guide appendix.
119 The name of this tool may be interpolated
120 anywhere in the document by specifying the
121 &t-TOOL; element. A link to this definition may be
122 interpolated by specifying the &t-link-TOOL; element.
126 print("this is example code, it will be offset and indented")
139 from lxml
import etree
142 import xml
.etree
.ElementTree
as etree
144 raise ImportError("Failed to import ElementTree from any known place")
146 # patterns to help trim XML passed in as strings
147 re_entity
= re
.compile(r
"&([^;]+);")
148 re_entity_header
= re
.compile(r
"<!DOCTYPE\s+sconsdoc\s+[^\]]+\]>")
150 # Namespace for the SCons Docbook XSD
151 dbxsd
= "http://www.scons.org/dbxsd/v1.0"
152 # Namsespace pattern to help identify an scons-xml file read as bytes
153 dbxsdpat
= b
'xmlns="%s"' % dbxsd
.encode('utf-8')
154 # Namespace map identifier for the SCons Docbook XSD
156 # Namespace for schema instances
157 xsi
= "http://www.w3.org/2001/XMLSchema-instance"
159 # Header comment with copyright
160 copyright_comment
= """
163 This file is processed by the bin/SConsDoc.py module.
164 See its docstring for a discussion of the format.
167 def isSConsXml(fpath
):
168 """ Check whether the given file is an SCons XML file.
170 It is SCons XML if it contains the default target namespace definition
171 described by dbxsdpat
175 with
open(fpath
, 'rb') as f
:
177 if content
.find(dbxsdpat
) >= 0:
184 def remove_entities(content
):
185 # Cut out entity inclusions
186 content
= re_entity_header
.sub("", content
, re
.M
)
187 # Cut out entities themselves
188 content
= re_entity
.sub(lambda match
: match
.group(1), content
)
192 default_xsd
= os
.path
.join('doc', 'xsd', 'scons.xsd')
197 class Libxml2ValidityHandler
:
203 def error(self
, msg
, data
):
205 raise Exception("Error handler did not receive correct argument")
206 self
.errors
.append(msg
)
208 def warning(self
, msg
, data
):
210 raise Exception("Warning handler did not receive correct argument")
211 self
.warnings
.append(msg
)
215 def __init__(self
, name_
, uri_
):
219 def getEntityString(self
):
220 txt
= """ <!ENTITY %(perc)s %(name)s SYSTEM "%(uri)s">
222 """ % {'perc': perc
, 'name': self
.name
, 'uri': self
.uri
}
227 class DoctypeDeclaration
:
228 def __init__(self
, name_
=None):
231 if self
.name
is None:
232 # Add default entries
233 self
.name
= "sconsdoc"
234 self
.addEntity("scons", "../scons.mod")
235 self
.addEntity("builders-mod", "builders.mod")
236 self
.addEntity("functions-mod", "functions.mod")
237 self
.addEntity("tools-mod", "tools.mod")
238 self
.addEntity("variables-mod", "variables.mod")
240 def addEntity(self
, name
, uri
):
241 self
.entries
.append(DoctypeEntity(name
, uri
))
243 def createDoctype(self
):
244 content
= '<!DOCTYPE %s [\n' % self
.name
245 for e
in self
.entries
:
246 content
+= e
.getEntityString()
256 def newNode(tag
, **kwargs
):
257 return etree
.Element(tag
, **kwargs
)
260 def newSubNode(parent
, tag
, **kwargs
):
261 return etree
.SubElement(parent
, tag
, **kwargs
)
264 def newEtreeNode(tag
, init_ns
=False, **kwargs
):
266 NSMAP
= {None: dbxsd
,
268 return etree
.Element(tag
, nsmap
=NSMAP
, **kwargs
)
270 return etree
.Element(tag
, **kwargs
)
274 return copy
.deepcopy(node
)
277 def appendNode(parent
, child
):
281 def hasAttribute(node
, att
):
282 return att
in node
.attrib
285 def getAttribute(node
, att
):
286 return node
.attrib
[att
]
289 def setAttribute(node
, att
, value
):
290 node
.attrib
[att
] = value
297 def appendCvLink(root
, key
, lntail
):
298 linknode
= etree
.Entity('cv-link-' + key
)
299 linknode
.tail
= lntail
300 root
.append(linknode
)
303 def setText(root
, txt
):
311 def setTail(root
, txt
):
315 def writeGenTree(root
, fp
):
316 dt
= DoctypeDeclaration()
317 fp
.write(etree
.tostring(root
, encoding
="utf-8",
319 doctype
=dt
.createDoctype()).decode('utf-8'))
322 def writeTree(root
, fpath
):
323 with
open(fpath
, 'wb') as fp
:
324 fp
.write(etree
.tostring(root
, encoding
="utf-8",
328 def prettyPrintFile(fpath
):
329 with
open(fpath
,'rb') as fin
:
330 tree
= etree
.parse(fin
)
331 pretty_content
= etree
.tostring(tree
, encoding
="utf-8",
334 with
open(fpath
,'wb') as fout
:
335 fout
.write(pretty_content
)
338 def decorateWithHeader(root
):
339 root
.attrib
["{"+xsi
+"}schemaLocation"] = "%s %s/scons.xsd" % (dbxsd
, dbxsd
)
342 def newXmlTree(self
, root
):
343 """ Return a XML file tree with the correct namespaces set,
344 the element root as top entry and the given header comment.
346 NSMAP
= {None: dbxsd
, 'xsi' : xsi
}
347 t
= etree
.Element(root
, nsmap
=NSMAP
)
348 return self
.decorateWithHeader(t
)
350 # singleton to cache parsed xmlschema..
354 def validateXml(fpath
, xmlschema_context
):
356 if TreeFactory
.xmlschema
is None:
357 TreeFactory
.xmlschema
= etree
.XMLSchema(xmlschema_context
)
359 doc
= etree
.parse(fpath
)
360 except Exception as e
:
361 print("ERROR: %s fails to parse:"%fpath
)
366 TreeFactory
.xmlschema
.assertValid(doc
)
367 except etree
.XMLSchemaValidateError
as e
:
368 print("ERROR: %s fails to validate:" % fpath
)
370 print(e
.error_log
.last_error
.message
)
371 print("In file: [%s]" % e
.error_log
.last_error
.filename
)
372 print("Line : %d" % e
.error_log
.last_error
.line
)
375 except Exception as e
:
376 print("ERROR: %s fails to validate:" % fpath
)
383 def findAll(root
, tag
, ns
=None, xp_ctxt
=None, nsmap
=None):
384 expression
= ".//{%s}%s" % (nsmap
[ns
], tag
)
385 if not ns
or not nsmap
:
386 expression
= ".//%s" % tag
387 return root
.findall(expression
)
390 def findAllChildrenOf(root
, tag
, ns
=None, xp_ctxt
=None, nsmap
=None):
391 expression
= "./{%s}%s/*" % (nsmap
[ns
], tag
)
392 if not ns
or not nsmap
:
393 expression
= "./%s/*" % tag
394 return root
.findall(expression
)
397 def convertElementTree(root
):
398 """ Convert the given tree of etree.Element
399 entries to a list of tree nodes for the
409 self
.nsmap
= {'dbx': dbxsd
}
412 self
.xpath_context
= None
414 def parseContent(self
, content
, include_entities
=True):
415 """ Parses the given text content as XML
417 This is the setup portion, called from parseContent in
418 an SConsDocHandler instance - see the notes there.
420 if not include_entities
:
421 content
= remove_entities(content
)
422 # Create domtree from given content string
423 self
.root
= etree
.fromstring(content
)
425 def parseXmlFile(self
, fpath
):
426 # Create domtree from file
427 parser
= etree
.XMLParser(load_dtd
=True, resolve_entities
=False)
428 domtree
= etree
.parse(fpath
, parser
)
429 self
.root
= domtree
.getroot()
432 if self
.doc
is not None:
434 if self
.xpath_context
is not None:
435 self
.xpath_context
.xpathFreeContext()
439 def validate_all_xml(dpaths
, xsdfile
=default_xsd
):
440 xmlschema_context
= etree
.parse(xsdfile
)
444 if dp
.endswith('.xml') and isSConsXml(dp
):
448 for path
, dirs
, files
in os
.walk(dp
):
450 if f
.endswith('.xml'):
451 fp
= os
.path
.join(path
, f
)
456 fpaths
= sorted(fpaths
)
457 for idx
, fp
in enumerate(fpaths
):
458 fpath
= os
.path
.join(path
, fp
)
459 print("%.2f%s (%d/%d) %s" % (float(idx
+ 1) * 100.0 /float(len(fpaths
)),
460 perc
, idx
+ 1, len(fpaths
), fp
))
462 if not tf
.validateXml(fp
, xmlschema_context
):
473 def __init__(self
, name
):
475 self
.sort_name
= name
.lower()
476 if self
.sort_name
[0] == '_':
477 self
.sort_name
= self
.sort_name
[1:]
481 self
.arguments
= None
482 def cmp_name(self
, name
):
486 def __eq__(self
, other
):
487 return self
.sort_name
== other
.sort_name
488 def __lt__(self
, other
):
489 return self
.sort_name
< other
.sort_name
496 class Function(Item
):
501 def __init__(self
, name
):
502 super().__init
__(name
)
503 self
.entity
= self
.name
.replace('+', 'X')
506 class ConstructionVariable(Item
):
511 def __init__(self
, signature
, body
=None):
515 self
.signature
= signature
517 s
= ''.join(self
.body
).strip()
519 for m
in re
.findall(r
'([a-zA-Z/_]+|[^a-zA-Z/_]+)', s
):
523 return ' '.join(result
)
524 def append(self
, data
):
525 self
.body
.append(data
)
528 class SConsDocHandler
:
535 def parseItems(self
, domelem
, xpath_context
, nsmap
):
538 for i
in tf
.findAll(domelem
, "item", dbxid
, xpath_context
, nsmap
):
543 items
.append(txt
.strip())
547 def parseUsesSets(self
, domelem
, xpath_context
, nsmap
):
551 for u
in tf
.findAll(domelem
, "uses", dbxid
, xpath_context
, nsmap
):
552 uses
.extend(self
.parseItems(u
, xpath_context
, nsmap
))
553 for s
in tf
.findAll(domelem
, "sets", dbxid
, xpath_context
, nsmap
):
554 sets
.extend(self
.parseItems(s
, xpath_context
, nsmap
))
556 return sorted(uses
), sorted(sets
)
558 def parseInstance(self
, domelem
, map, Class
,
559 xpath_context
, nsmap
, include_entities
=True):
561 if tf
.hasAttribute(domelem
, 'name'):
562 name
= tf
.getAttribute(domelem
, 'name')
566 instance
= Class(name
)
568 uses
, sets
= self
.parseUsesSets(domelem
, xpath_context
, nsmap
)
569 instance
.uses
.extend(uses
)
570 instance
.sets
.extend(sets
)
572 # Parse summary and function arguments
573 for s
in tf
.findAllChildrenOf(domelem
, "summary", dbxid
, xpath_context
, nsmap
):
574 if instance
.summary
is None:
575 instance
.summary
= []
576 instance
.summary
.append(tf
.copyNode(s
))
577 for a
in tf
.findAll(domelem
, "arguments", dbxid
, xpath_context
, nsmap
):
578 if instance
.arguments
is None:
579 instance
.arguments
= []
580 instance
.arguments
.append(tf
.copyNode(a
))
582 def parseDomtree(self
, root
, xpath_context
=None, nsmap
=None, include_entities
=True):
584 for b
in tf
.findAll(root
, "builder", dbxid
, xpath_context
, nsmap
):
585 self
.parseInstance(b
, self
.builders
, Builder
,
586 xpath_context
, nsmap
, include_entities
)
588 for f
in tf
.findAll(root
, "scons_function", dbxid
, xpath_context
, nsmap
):
589 self
.parseInstance(f
, self
.functions
, Function
,
590 xpath_context
, nsmap
, include_entities
)
592 for t
in tf
.findAll(root
, "tool", dbxid
, xpath_context
, nsmap
):
593 self
.parseInstance(t
, self
.tools
, Tool
,
594 xpath_context
, nsmap
, include_entities
)
596 for c
in tf
.findAll(root
, "cvar", dbxid
, xpath_context
, nsmap
):
597 self
.parseInstance(c
, self
.cvars
, ConstructionVariable
,
598 xpath_context
, nsmap
, include_entities
)
600 def parseContent(self
, content
, include_entities
=True):
601 """Parse the given content as XML.
603 This method is used when we generate the basic lists of entities
604 for the builders, tools and functions. So we usually don't
605 bother about namespaces and resolving entities here...
606 this is handled in parseXmlFile below (step 2 of the overall process).
610 t
.parseContent(content
, include_entities
)
612 self
.parseDomtree(t
.root
, t
.xpath_context
, t
.nsmap
, include_entities
)
614 def parseXmlFile(self
, fpath
):
617 t
.parseXmlFile(fpath
)
619 self
.parseDomtree(t
.root
, t
.xpath_context
, t
.nsmap
)
621 def importfile(path
):
622 """Import a Python source file or compiled file given its path."""
623 from importlib
.util
import MAGIC_NUMBER
624 with
open(path
, 'rb') as ifp
:
625 is_bytecode
= MAGIC_NUMBER
== ifp
.read(len(MAGIC_NUMBER
))
626 filename
= os
.path
.basename(path
)
627 name
, ext
= os
.path
.splitext(filename
)
629 loader
= importlib
._bootstrap
_external
.SourcelessFileLoader(name
, path
)
631 loader
= importlib
._bootstrap
_external
.SourceFileLoader(name
, path
)
632 # XXX We probably don't need to pass in the loader here.
633 spec
= importlib
.util
.spec_from_file_location(name
, path
, loader
=loader
)
635 return importlib
._bootstrap
._load
(spec
)
637 raise Exception(path
, sys
.exc_info())
641 # indent-tabs-mode:nil
643 # vim: set expandtab tabstop=4 shiftwidth=4: