Ticket #4138: sdist-support-4138.4.patch
File sdist-support-4138.4.patch, 141.0 KB (added by , 12 years ago) |
---|
-
setup.py
diff --git a/setup.py b/setup.py index c8b5877..529fda5 100755
a b def getExtensions(): 31 31 execfile(setup_py, ns, ns) 32 32 if "extensions" in ns: 33 33 extensions.extend(ns["extensions"]) 34 34 35 35 return extensions 36 36 37 37 … … def main(args): 43 43 if os.path.exists('twisted'): 44 44 sys.path.insert(0, '.') 45 45 from twisted import copyright 46 from twisted.python.dist import getDataFiles, getScripts, getPackages, setup 46 from twisted.python.dist import getDataFiles, getScripts, getPackages 47 from twisted.python.dist import setup, _SDistTwisted 47 48 48 49 # "" is included because core scripts are directly in bin/ 49 50 projects = [''] + [x for x in os.listdir('bin') … … on event-based network programming and multiprotocol integration. 72 73 packages = getPackages('twisted'), 73 74 conditionalExtensions = getExtensions(), 74 75 scripts = scripts, 75 data_files=getDataFiles('twisted'), 76 data_files=getDataFiles('twisted'), 77 cmdclass = {'sdist': _SDistTwisted}, 76 78 ) 77 79 78 80 if 'setuptools' in sys.modules: -
new file twisted/python/_dist.py
diff --git a/twisted/python/_dist.py b/twisted/python/_dist.py new file mode 100644 index 0000000..847473f
- + 1 # -*- test-case-name: twisted.python.test.test__dist -*- 2 # Copyright (c) 2010 Twisted Matrix Laboratories. 3 # See LICENSE for details. 4 """ 5 Code to support distutils making a distribution of a release. 6 7 Much of this code used to live in L{twisted.python._release}, but there is 8 a distinction between a "distribution" and a "release". Only Twisted devs can 9 make a release (using the code in C{t.p._release} to build API documentation, 10 change version numbers, package tarballs and so forth), but anybody anywhere 11 can use distutils to make a distribution of the files of a particular release. 12 13 Because Twisted's release code is only designed to work in a POSIX environment, 14 it's not appropriate for the generic distutils code to depend on it; therefore 15 this module contains code for bundling the files of a release into 16 a distribution, and both C{setup.py} and C{t.p._release} depend on it. 17 """ 18 19 import os, fnmatch, tarfile, errno, shutil 20 from twisted.lore.scripts import lore 21 22 23 twisted_subprojects = ["conch", "lore", "mail", "names", 24 "news", "pair", "runner", "web", "web2", 25 "words", "vfs"] 26 27 28 29 # Files and directories matching these patterns will be excluded from Twisted 30 # releases. 31 EXCLUDE_PATTERNS = ["{arch}", "_darcs", "*.py[cdo]", "*.s[ol]", ".*", "*~"] 32 33 34 35 def isDistributable(filepath): 36 """ 37 Determine if the given item should be included in Twisted distributions. 38 39 This function is useful for filtering out files and directories in the 40 Twisted directory that aren't supposed to be part of the official Twisted 41 package - things like version control system metadata, editor backup files, 42 and various other detritus. 43 44 @type filepath: L{FilePath} 45 @param filepath: The file or directory that is a candidate for packaging. 46 47 @rtype: C{bool} 48 @return: True if the file should be included, False otherwise. 49 """ 50 for pattern in EXCLUDE_PATTERNS: 51 if fnmatch.fnmatch(filepath.basename(), pattern): 52 return False 53 return True 54 55 56 57 class NoDocumentsFound(Exception): 58 """ 59 Raised when no input documents are found. 60 """ 61 62 63 64 class LoreBuilderMixin(object): 65 """ 66 Base class for builders which invoke lore. 67 """ 68 def lore(self, arguments): 69 """ 70 Run lore with the given arguments. 71 72 @param arguments: A C{list} of C{str} giving command line arguments to 73 lore which should be used. 74 """ 75 options = lore.Options() 76 options.parseOptions(["--null"] + arguments) 77 lore.runGivenOptions(options) 78 79 80 81 class DocBuilder(LoreBuilderMixin): 82 """ 83 Generate HTML documentation for projects. 84 """ 85 86 def build(self, version, resourceDir, docDir, template, apiBaseURL=None, 87 deleteInput=False): 88 """ 89 Build the documentation in C{docDir} with Lore. 90 91 Input files ending in .xhtml will be considered. Output will written as 92 .html files. 93 94 @param version: the version of the documentation to pass to lore. 95 @type version: C{str} 96 97 @param resourceDir: The directory which contains the toplevel index and 98 stylesheet file for this section of documentation. 99 @type resourceDir: L{twisted.python.filepath.FilePath} 100 101 @param docDir: The directory of the documentation. 102 @type docDir: L{twisted.python.filepath.FilePath} 103 104 @param template: The template used to generate the documentation. 105 @type template: L{twisted.python.filepath.FilePath} 106 107 @type apiBaseURL: C{str} or C{NoneType} 108 @param apiBaseURL: A format string which will be interpolated with the 109 fully-qualified Python name for each API link. For example, to 110 generate the Twisted 8.0.0 documentation, pass 111 C{"http://twistedmatrix.com/documents/8.0.0/api/%s.html"}. 112 113 @param deleteInput: If True, the input documents will be deleted after 114 their output is generated. 115 @type deleteInput: C{bool} 116 117 @raise NoDocumentsFound: When there are no .xhtml files in the given 118 C{docDir}. 119 """ 120 linkrel = self.getLinkrel(resourceDir, docDir) 121 inputFiles = docDir.globChildren("*.xhtml") 122 filenames = [x.path for x in inputFiles] 123 if not filenames: 124 raise NoDocumentsFound("No input documents found in %s" % (docDir,)) 125 if apiBaseURL is not None: 126 arguments = ["--config", "baseurl=" + apiBaseURL] 127 else: 128 arguments = [] 129 arguments.extend(["--config", "template=%s" % (template.path,), 130 "--config", "ext=.html", 131 "--config", "version=%s" % (version,), 132 "--linkrel", linkrel] + filenames) 133 self.lore(arguments) 134 if deleteInput: 135 for inputFile in inputFiles: 136 inputFile.remove() 137 138 139 def getLinkrel(self, resourceDir, docDir): 140 """ 141 Calculate a value appropriate for Lore's --linkrel option. 142 143 Lore's --linkrel option defines how to 'find' documents that are 144 linked to from TEMPLATE files (NOT document bodies). That is, it's a 145 prefix for links ('a' and 'link') in the template. 146 147 @param resourceDir: The directory which contains the toplevel index and 148 stylesheet file for this section of documentation. 149 @type resourceDir: L{twisted.python.filepath.FilePath} 150 151 @param docDir: The directory containing documents that must link to 152 C{resourceDir}. 153 @type docDir: L{twisted.python.filepath.FilePath} 154 """ 155 if resourceDir != docDir: 156 return '/'.join(filePathDelta(docDir, resourceDir)) + "/" 157 else: 158 return "" 159 160 161 162 class ManBuilder(LoreBuilderMixin): 163 """ 164 Generate man pages of the different existing scripts. 165 """ 166 167 def build(self, manDir): 168 """ 169 Generate Lore input files from the man pages in C{manDir}. 170 171 Input files ending in .1 will be considered. Output will written as 172 -man.xhtml files. 173 174 @param manDir: The directory of the man pages. 175 @type manDir: L{twisted.python.filepath.FilePath} 176 177 @raise NoDocumentsFound: When there are no .1 files in the given 178 C{manDir}. 179 """ 180 inputFiles = manDir.globChildren("*.1") 181 filenames = [x.path for x in inputFiles] 182 if not filenames: 183 raise NoDocumentsFound("No manual pages found in %s" % (manDir,)) 184 arguments = ["--input", "man", 185 "--output", "lore", 186 "--config", "ext=-man.xhtml"] + filenames 187 self.lore(arguments) 188 189 190 191 def _stageFile(src, dest): 192 """ 193 Stages src at the destination path. 194 195 "Staging", in this case, means "creating a temporary copy to be archived". 196 In particular, we want to preserve all the metadata of the original file, 197 but we don't care about whether edits to the file propagate back and forth 198 (since the staged version should never be edited). We hard-link the file if 199 we can, otherwise we copy it and preserve metadata. 200 201 @type src: L{twisted.python.filepath.FilePath} 202 @param src: The file or path to be staged. 203 204 @type dest: L{twisted.python.filepath.FilePath} 205 @param dest: The path at which the source file should be staged. Any 206 missing directories in this path will be created. 207 208 @raise OSError: If the source is a file, and the destination already 209 exists, C{OSError} will be raised with the C{errno} attribute set to 210 C{EEXIST}. 211 """ 212 213 if not isDistributable(src): 214 # Not a file we care about, quietly skip it. 215 return 216 217 if src.isfile(): 218 # Make sure the directory's parent exists. 219 destParent = dest.parent() 220 if not destParent.exists(): 221 destParent.makedirs() 222 223 # If the file already exists, raise an exception. 224 # os.link raises OSError, shutil.copy (sometimes) raises IOError or 225 # overwrites the destination, so let's do the checking ourselves and 226 # raise our own error. 227 if dest.exists(): 228 raise OSError(errno.EEXIST, "File exists: %s" % (dest.path,)) 229 230 # If we can create a hard link, that's faster than trying to copy 231 # things around. 232 if hasattr(os, "link"): 233 copyfunc = os.link 234 else: 235 copyfunc = shutil.copy2 236 237 try: 238 copyfunc(src.path, dest.path) 239 except OSError, e: 240 if e.errno == errno.EXDEV: 241 shutil.copy2(src.path, dest.path) 242 else: 243 raise 244 245 elif src.isdir(): 246 if not dest.exists(): 247 dest.makedirs() 248 249 for child in src.children(): 250 _stageFile(child, dest.child(child.basename())) 251 252 else: 253 raise NotImplementedError("Can only stage files or directories") 254 255 256 257 class DistributionBuilder(object): 258 """ 259 A builder of Twisted distributions. 260 261 This knows how to build tarballs for Twisted and all of its subprojects. 262 263 @type blacklist: C{list} of C{str} 264 @cvar blacklist: The list subproject names to exclude from the main Twisted 265 tarball and for which no individual project tarballs will be built. 266 """ 267 268 blacklist = ["vfs", "web2"] 269 270 def __init__(self, rootDirectory, outputDirectory, apiBaseURL=None): 271 """ 272 Create a distribution builder. 273 274 @param rootDirectory: root of a Twisted export which will populate 275 subsequent tarballs. 276 @type rootDirectory: L{FilePath}. 277 278 @param outputDirectory: The directory in which to create the tarballs. 279 @type outputDirectory: L{FilePath} 280 281 @type apiBaseURL: C{str} or C{NoneType} 282 @param apiBaseURL: A format string which will be interpolated with the 283 fully-qualified Python name for each API link. For example, to 284 generate the Twisted 8.0.0 documentation, pass 285 C{"http://twistedmatrix.com/documents/8.0.0/api/%s.html"}. 286 """ 287 self.rootDirectory = rootDirectory 288 self.outputDirectory = outputDirectory 289 self.apiBaseURL = apiBaseURL 290 self.manBuilder = ManBuilder() 291 self.docBuilder = DocBuilder() 292 293 294 def _buildDocInDir(self, path, version, howtoPath): 295 """ 296 Generate documentation in the given path, building man pages first if 297 necessary and swallowing errors (so that directories without lore 298 documentation in them are ignored). 299 300 @param path: The path containing documentation to build. 301 @type path: L{FilePath} 302 @param version: The version of the project to include in all generated 303 pages. 304 @type version: C{str} 305 @param howtoPath: The "resource path" as L{DocBuilder} describes it. 306 @type howtoPath: L{FilePath} 307 """ 308 templatePath = self.rootDirectory.child("doc").child("core" 309 ).child("howto").child("template.tpl") 310 if path.basename() == "man": 311 self.manBuilder.build(path) 312 if path.isdir(): 313 try: 314 self.docBuilder.build(version, howtoPath, path, 315 templatePath, self.apiBaseURL, True) 316 except NoDocumentsFound: 317 pass 318 319 320 def buildTwistedFiles(self, version, releaseName): 321 """ 322 Build a directory containing the main Twisted distribution. 323 """ 324 # Make all the directories we'll need for copying things to. 325 distDirectory = self.outputDirectory.child(releaseName) 326 distBin = distDirectory.child("bin") 327 distTwisted = distDirectory.child("twisted") 328 distPlugins = distTwisted.child("plugins") 329 distDoc = distDirectory.child("doc") 330 331 for dir in (distDirectory, distBin, distTwisted, distPlugins, distDoc): 332 dir.makedirs() 333 334 # Now, this part is nasty. We need to exclude blacklisted subprojects 335 # from the main Twisted distribution. This means we need to exclude 336 # their bin directories, their documentation directories, their 337 # plugins, and their python packages. Given that there's no "add all 338 # but exclude these particular paths" functionality in tarfile, we have 339 # to walk through all these directories and add things that *aren't* 340 # part of the blacklisted projects. 341 342 for binthing in self.rootDirectory.child("bin").children(): 343 # bin/admin should also not be included. 344 if binthing.basename() not in self.blacklist + ["admin"]: 345 _stageFile(binthing, distBin.child(binthing.basename())) 346 347 bad_plugins = ["twisted_%s.py" % (blacklisted,) 348 for blacklisted in self.blacklist] 349 350 for submodule in self.rootDirectory.child("twisted").children(): 351 if submodule.basename() == "plugins": 352 for plugin in submodule.children(): 353 if plugin.basename() not in bad_plugins: 354 _stageFile(plugin, 355 distPlugins.child(plugin.basename())) 356 elif submodule.basename() not in self.blacklist: 357 _stageFile(submodule, distTwisted.child(submodule.basename())) 358 359 for docDir in self.rootDirectory.child("doc").children(): 360 if docDir.basename() not in self.blacklist: 361 _stageFile(docDir, distDoc.child(docDir.basename())) 362 363 for toplevel in self.rootDirectory.children(): 364 if not toplevel.isdir(): 365 _stageFile(toplevel, distDirectory.child(toplevel.basename())) 366 367 # Generate docs in the distribution directory. 368 docPath = distDirectory.child("doc") 369 if docPath.isdir(): 370 for subProjectDir in docPath.children(): 371 if (subProjectDir.isdir() 372 and subProjectDir.basename() not in self.blacklist): 373 for child in subProjectDir.walk(): 374 self._buildDocInDir(child, version, 375 subProjectDir.child("howto")) 376 377 378 def buildTwisted(self, version): 379 """ 380 Build the main Twisted distribution in C{Twisted-<version>.tar.bz2}. 381 382 Projects listed in in L{blacklist} will not have their plugins, code, 383 documentation, or bin directories included. 384 385 bin/admin is also excluded. 386 387 @type version: C{str} 388 @param version: The version of Twisted to build. 389 390 @return: The tarball file. 391 @rtype: L{FilePath}. 392 """ 393 releaseName = "Twisted-%s" % (version,) 394 395 outputTree = self.outputDirectory.child(releaseName) 396 outputFile = self.outputDirectory.child(releaseName + ".tar.bz2") 397 398 tarball = tarfile.TarFile.open(outputFile.path, 'w:bz2') 399 self.buildTwistedFiles(version, releaseName) 400 tarball.add(outputTree.path, releaseName) 401 tarball.close() 402 403 outputTree.remove() 404 405 return outputFile 406 407 408 def buildCore(self, version): 409 """ 410 Build a core distribution in C{TwistedCore-<version>.tar.bz2}. 411 412 This is very similar to L{buildSubProject}, but core tarballs and the 413 input are laid out slightly differently. 414 415 - scripts are in the top level of the C{bin} directory. 416 - code is included directly from the C{twisted} directory, excluding 417 subprojects. 418 - all plugins except the subproject plugins are included. 419 420 @type version: C{str} 421 @param version: The version of Twisted to build. 422 423 @return: The tarball file. 424 @rtype: L{FilePath}. 425 """ 426 releaseName = "TwistedCore-%s" % (version,) 427 outputFile = self.outputDirectory.child(releaseName + ".tar.bz2") 428 buildPath = lambda *args: '/'.join((releaseName,) + args) 429 tarball = self._createBasicSubprojectTarball( 430 "core", version, outputFile) 431 432 # Include the bin directory for the subproject. 433 for path in self.rootDirectory.child("bin").children(): 434 if not path.isdir(): 435 tarball.add(path.path, buildPath("bin", path.basename())) 436 437 # Include all files within twisted/ that aren't part of a subproject. 438 for path in self.rootDirectory.child("twisted").children(): 439 if path.basename() == "plugins": 440 for plugin in path.children(): 441 for subproject in twisted_subprojects: 442 if plugin.basename() == "twisted_%s.py" % (subproject,): 443 break 444 else: 445 tarball.add(plugin.path, 446 buildPath("twisted", "plugins", 447 plugin.basename())) 448 elif not path.basename() in twisted_subprojects + ["topfiles"]: 449 tarball.add(path.path, buildPath("twisted", path.basename())) 450 451 tarball.add(self.rootDirectory.child("twisted").child("topfiles").path, 452 releaseName) 453 tarball.close() 454 455 return outputFile 456 457 458 def buildSubProject(self, projectName, version): 459 """ 460 Build a subproject distribution in 461 C{Twisted<Projectname>-<version>.tar.bz2}. 462 463 @type projectName: C{str} 464 @param projectName: The lowercase name of the subproject to build. 465 @type version: C{str} 466 @param version: The version of Twisted to build. 467 468 @return: The tarball file. 469 @rtype: L{FilePath}. 470 """ 471 releaseName = "Twisted%s-%s" % (projectName.capitalize(), version) 472 outputFile = self.outputDirectory.child(releaseName + ".tar.bz2") 473 buildPath = lambda *args: '/'.join((releaseName,) + args) 474 subProjectDir = self.rootDirectory.child("twisted").child(projectName) 475 476 tarball = self._createBasicSubprojectTarball(projectName, version, 477 outputFile) 478 479 tarball.add(subProjectDir.child("topfiles").path, releaseName) 480 481 # Include all files in the subproject package except for topfiles. 482 for child in subProjectDir.children(): 483 name = child.basename() 484 if name != "topfiles": 485 tarball.add( 486 child.path, 487 buildPath("twisted", projectName, name)) 488 489 pluginsDir = self.rootDirectory.child("twisted").child("plugins") 490 # Include the plugin for the subproject. 491 pluginFileName = "twisted_%s.py" % (projectName,) 492 pluginFile = pluginsDir.child(pluginFileName) 493 if pluginFile.exists(): 494 tarball.add(pluginFile.path, 495 buildPath("twisted", "plugins", pluginFileName)) 496 497 # Include the bin directory for the subproject. 498 binPath = self.rootDirectory.child("bin").child(projectName) 499 if binPath.isdir(): 500 tarball.add(binPath.path, buildPath("bin")) 501 tarball.close() 502 503 return outputFile 504 505 506 def _createBasicSubprojectTarball(self, projectName, version, outputFile): 507 """ 508 Helper method to create and fill a tarball with things common between 509 subprojects and core. 510 511 @param projectName: The subproject's name. 512 @type projectName: C{str} 513 @param version: The version of the release. 514 @type version: C{str} 515 @param outputFile: The location of the tar file to create. 516 @type outputFile: L{FilePath} 517 """ 518 releaseName = "Twisted%s-%s" % (projectName.capitalize(), version) 519 buildPath = lambda *args: '/'.join((releaseName,) + args) 520 521 tarball = tarfile.TarFile.open(outputFile.path, 'w:bz2') 522 523 tarball.add(self.rootDirectory.child("LICENSE").path, 524 buildPath("LICENSE")) 525 526 docPath = self.rootDirectory.child("doc").child(projectName) 527 528 if docPath.isdir(): 529 for child in docPath.walk(): 530 self._buildDocInDir(child, version, docPath.child("howto")) 531 tarball.add(docPath.path, buildPath("doc")) 532 533 return tarball 534 535 536 537 def makeAPIBaseURL(version): 538 """ 539 Guess where the Twisted API docs for a given version will live. 540 541 @type version: C{str} 542 @param version: A URL-safe string containing a version number, such as 543 "10.0.0". 544 @rtype: C{str} 545 @return: A URL template pointing to the Twisted API docs for the given 546 version, ready to have the class, module or function name substituted 547 in. 548 """ 549 return "http://twistedmatrix.com/documents/%s/api/%%s.html" % (version,) 550 551 552 553 def filePathDelta(origin, destination): 554 """ 555 Return a list of strings that represent C{destination} as a path relative 556 to C{origin}. 557 558 It is assumed that both paths represent directories, not files. That is to 559 say, the delta of L{twisted.python.filepath.FilePath} /foo/bar to 560 L{twisted.python.filepath.FilePath} /foo/baz will be C{../baz}, 561 not C{baz}. 562 563 @type origin: L{twisted.python.filepath.FilePath} 564 @param origin: The origin of the relative path. 565 566 @type destination: L{twisted.python.filepath.FilePath} 567 @param destination: The destination of the relative path. 568 """ 569 commonItems = 0 570 path1 = origin.path.split(os.sep) 571 path2 = destination.path.split(os.sep) 572 for elem1, elem2 in zip(path1, path2): 573 if elem1 == elem2: 574 commonItems += 1 575 else: 576 break 577 path = [".."] * (len(path1) - commonItems) 578 return path + path2[commonItems:] 579 580 581 -
twisted/python/_release.py
diff --git a/twisted/python/_release.py b/twisted/python/_release.py index 10dc03c..fbaf835 100644
a b import re 18 18 import sys 19 19 import os 20 20 from tempfile import mkdtemp 21 import tarfile22 21 23 22 from subprocess import PIPE, STDOUT, Popen 24 23 25 24 from twisted.python.versions import Version 26 25 from twisted.python.filepath import FilePath 27 from twisted.python.dist import twisted_subprojects 28 29 # This import is an example of why you shouldn't use this module unless you're 30 # radix 31 try: 32 from twisted.lore.scripts import lore 33 except ImportError: 34 pass 26 from twisted.python._dist import LoreBuilderMixin, DistributionBuilder 27 from twisted.python._dist import makeAPIBaseURL, twisted_subprojects 35 28 36 29 # The offset between a year and the corresponding major version number. 37 30 VERSION_OFFSET = 2000 … … def changeAllProjectVersions(root, versionTemplate, today=None): 164 157 165 158 166 159 167 168 160 class Project(object): 169 161 """ 170 162 A representation of a project that has a version. … … def updateTwistedVersionInformation(baseDirectory, now): 238 230 project.updateVersion(getNextVersion(project.getVersion(), now=now)) 239 231 240 232 233 241 234 def generateVersionFileData(version): 242 235 """ 243 236 Generate the data to be placed into a _version.py file. … … version = versions.Version(%r, %s, %s, %s%s) 256 249 return data 257 250 258 251 252 259 253 def replaceProjectVersion(filename, newversion): 260 254 """ 261 255 Write version specification code into the given filename, which … … def replaceInFile(filename, oldToNew): 291 285 292 286 293 287 294 class NoDocumentsFound(Exception):295 """296 Raised when no input documents are found.297 """298 299 300 301 class LoreBuilderMixin(object):302 """303 Base class for builders which invoke lore.304 """305 def lore(self, arguments):306 """307 Run lore with the given arguments.308 309 @param arguments: A C{list} of C{str} giving command line arguments to310 lore which should be used.311 """312 options = lore.Options()313 options.parseOptions(["--null"] + arguments)314 lore.runGivenOptions(options)315 316 317 318 class DocBuilder(LoreBuilderMixin):319 """320 Generate HTML documentation for projects.321 """322 323 def build(self, version, resourceDir, docDir, template, apiBaseURL=None,324 deleteInput=False):325 """326 Build the documentation in C{docDir} with Lore.327 328 Input files ending in .xhtml will be considered. Output will written as329 .html files.330 331 @param version: the version of the documentation to pass to lore.332 @type version: C{str}333 334 @param resourceDir: The directory which contains the toplevel index and335 stylesheet file for this section of documentation.336 @type resourceDir: L{twisted.python.filepath.FilePath}337 338 @param docDir: The directory of the documentation.339 @type docDir: L{twisted.python.filepath.FilePath}340 341 @param template: The template used to generate the documentation.342 @type template: L{twisted.python.filepath.FilePath}343 344 @type apiBaseURL: C{str} or C{NoneType}345 @param apiBaseURL: A format string which will be interpolated with the346 fully-qualified Python name for each API link. For example, to347 generate the Twisted 8.0.0 documentation, pass348 C{"http://twistedmatrix.com/documents/8.0.0/api/%s.html"}.349 350 @param deleteInput: If True, the input documents will be deleted after351 their output is generated.352 @type deleteInput: C{bool}353 354 @raise NoDocumentsFound: When there are no .xhtml files in the given355 C{docDir}.356 """357 linkrel = self.getLinkrel(resourceDir, docDir)358 inputFiles = docDir.globChildren("*.xhtml")359 filenames = [x.path for x in inputFiles]360 if not filenames:361 raise NoDocumentsFound("No input documents found in %s" % (docDir,))362 if apiBaseURL is not None:363 arguments = ["--config", "baseurl=" + apiBaseURL]364 else:365 arguments = []366 arguments.extend(["--config", "template=%s" % (template.path,),367 "--config", "ext=.html",368 "--config", "version=%s" % (version,),369 "--linkrel", linkrel] + filenames)370 self.lore(arguments)371 if deleteInput:372 for inputFile in inputFiles:373 inputFile.remove()374 375 376 def getLinkrel(self, resourceDir, docDir):377 """378 Calculate a value appropriate for Lore's --linkrel option.379 380 Lore's --linkrel option defines how to 'find' documents that are381 linked to from TEMPLATE files (NOT document bodies). That is, it's a382 prefix for links ('a' and 'link') in the template.383 384 @param resourceDir: The directory which contains the toplevel index and385 stylesheet file for this section of documentation.386 @type resourceDir: L{twisted.python.filepath.FilePath}387 388 @param docDir: The directory containing documents that must link to389 C{resourceDir}.390 @type docDir: L{twisted.python.filepath.FilePath}391 """392 if resourceDir != docDir:393 return '/'.join(filePathDelta(docDir, resourceDir)) + "/"394 else:395 return ""396 397 398 399 class ManBuilder(LoreBuilderMixin):400 """401 Generate man pages of the different existing scripts.402 """403 404 def build(self, manDir):405 """406 Generate Lore input files from the man pages in C{manDir}.407 408 Input files ending in .1 will be considered. Output will written as409 -man.xhtml files.410 411 @param manDir: The directory of the man pages.412 @type manDir: L{twisted.python.filepath.FilePath}413 414 @raise NoDocumentsFound: When there are no .1 files in the given415 C{manDir}.416 """417 inputFiles = manDir.globChildren("*.1")418 filenames = [x.path for x in inputFiles]419 if not filenames:420 raise NoDocumentsFound("No manual pages found in %s" % (manDir,))421 arguments = ["--input", "man",422 "--output", "lore",423 "--config", "ext=-man.xhtml"] + filenames424 self.lore(arguments)425 426 427 428 288 class APIBuilder(object): 429 289 """ 430 290 Generate API documentation from source files using … … class NewsBuilder(object): 924 784 925 785 926 786 927 def filePathDelta(origin, destination):928 """929 Return a list of strings that represent C{destination} as a path relative930 to C{origin}.931 932 It is assumed that both paths represent directories, not files. That is to933 say, the delta of L{twisted.python.filepath.FilePath} /foo/bar to934 L{twisted.python.filepath.FilePath} /foo/baz will be C{../baz},935 not C{baz}.936 937 @type origin: L{twisted.python.filepath.FilePath}938 @param origin: The origin of the relative path.939 940 @type destination: L{twisted.python.filepath.FilePath}941 @param destination: The destination of the relative path.942 """943 commonItems = 0944 path1 = origin.path.split(os.sep)945 path2 = destination.path.split(os.sep)946 for elem1, elem2 in zip(path1, path2):947 if elem1 == elem2:948 commonItems += 1949 else:950 break951 path = [".."] * (len(path1) - commonItems)952 return path + path2[commonItems:]953 954 955 956 class DistributionBuilder(object):957 """958 A builder of Twisted distributions.959 960 This knows how to build tarballs for Twisted and all of its subprojects.961 962 @type blacklist: C{list} of C{str}963 @cvar blacklist: The list of subproject names to exclude from the main964 Twisted tarball and for which no individual project tarballs will be965 built. The same list as C{PROJECT_BLACKLIST}.966 """967 968 from twisted.python.dist import twisted_subprojects as subprojects969 blacklist = PROJECT_BLACKLIST970 971 def __init__(self, rootDirectory, outputDirectory, apiBaseURL=None):972 """973 Create a distribution builder.974 975 @param rootDirectory: root of a Twisted export which will populate976 subsequent tarballs.977 @type rootDirectory: L{FilePath}.978 979 @param outputDirectory: The directory in which to create the tarballs.980 @type outputDirectory: L{FilePath}981 982 @type apiBaseURL: C{str} or C{NoneType}983 @param apiBaseURL: A format string which will be interpolated with the984 fully-qualified Python name for each API link. For example, to985 generate the Twisted 8.0.0 documentation, pass986 C{"http://twistedmatrix.com/documents/8.0.0/api/%s.html"}.987 """988 self.rootDirectory = rootDirectory989 self.outputDirectory = outputDirectory990 self.apiBaseURL = apiBaseURL991 self.manBuilder = ManBuilder()992 self.docBuilder = DocBuilder()993 994 995 def _buildDocInDir(self, path, version, howtoPath):996 """997 Generate documentation in the given path, building man pages first if998 necessary and swallowing errors (so that directories without lore999 documentation in them are ignored).1000 1001 @param path: The path containing documentation to build.1002 @type path: L{FilePath}1003 @param version: The version of the project to include in all generated1004 pages.1005 @type version: C{str}1006 @param howtoPath: The "resource path" as L{DocBuilder} describes it.1007 @type howtoPath: L{FilePath}1008 """1009 templatePath = self.rootDirectory.child("doc").child("core"1010 ).child("howto").child("template.tpl")1011 if path.basename() == "man":1012 self.manBuilder.build(path)1013 if path.isdir():1014 try:1015 self.docBuilder.build(version, howtoPath, path,1016 templatePath, self.apiBaseURL, True)1017 except NoDocumentsFound:1018 pass1019 1020 1021 def buildTwisted(self, version):1022 """1023 Build the main Twisted distribution in C{Twisted-<version>.tar.bz2}.1024 1025 Projects listed in in L{blacklist} will not have their plugins, code,1026 documentation, or bin directories included.1027 1028 bin/admin is also excluded.1029 1030 @type version: C{str}1031 @param version: The version of Twisted to build.1032 1033 @return: The tarball file.1034 @rtype: L{FilePath}.1035 """1036 releaseName = "Twisted-%s" % (version,)1037 buildPath = lambda *args: '/'.join((releaseName,) + args)1038 1039 outputFile = self.outputDirectory.child(releaseName + ".tar.bz2")1040 tarball = tarfile.TarFile.open(outputFile.path, 'w:bz2')1041 1042 docPath = self.rootDirectory.child("doc")1043 1044 # Generate docs!1045 if docPath.isdir():1046 for subProjectDir in docPath.children():1047 if (subProjectDir.isdir()1048 and subProjectDir.basename() not in self.blacklist):1049 for child in subProjectDir.walk():1050 self._buildDocInDir(child, version,1051 subProjectDir.child("howto"))1052 1053 # Now, this part is nasty. We need to exclude blacklisted subprojects1054 # from the main Twisted distribution. This means we need to exclude1055 # their bin directories, their documentation directories, their1056 # plugins, and their python packages. Given that there's no "add all1057 # but exclude these particular paths" functionality in tarfile, we have1058 # to walk through all these directories and add things that *aren't*1059 # part of the blacklisted projects.1060 1061 for binthing in self.rootDirectory.child("bin").children():1062 # bin/admin should also not be included.1063 if binthing.basename() not in self.blacklist + ["admin"]:1064 tarball.add(binthing.path,1065 buildPath("bin", binthing.basename()))1066 1067 bad_plugins = ["twisted_%s.py" % (blacklisted,)1068 for blacklisted in self.blacklist]1069 1070 for submodule in self.rootDirectory.child("twisted").children():1071 if submodule.basename() == "plugins":1072 for plugin in submodule.children():1073 if plugin.basename() not in bad_plugins:1074 tarball.add(plugin.path, buildPath("twisted", "plugins",1075 plugin.basename()))1076 elif submodule.basename() not in self.blacklist:1077 tarball.add(submodule.path, buildPath("twisted",1078 submodule.basename()))1079 1080 for docDir in self.rootDirectory.child("doc").children():1081 if docDir.basename() not in self.blacklist:1082 tarball.add(docDir.path, buildPath("doc", docDir.basename()))1083 1084 for toplevel in self.rootDirectory.children():1085 if not toplevel.isdir():1086 tarball.add(toplevel.path, buildPath(toplevel.basename()))1087 1088 tarball.close()1089 1090 return outputFile1091 1092 1093 def buildCore(self, version):1094 """1095 Build a core distribution in C{TwistedCore-<version>.tar.bz2}.1096 1097 This is very similar to L{buildSubProject}, but core tarballs and the1098 input are laid out slightly differently.1099 1100 - scripts are in the top level of the C{bin} directory.1101 - code is included directly from the C{twisted} directory, excluding1102 subprojects.1103 - all plugins except the subproject plugins are included.1104 1105 @type version: C{str}1106 @param version: The version of Twisted to build.1107 1108 @return: The tarball file.1109 @rtype: L{FilePath}.1110 """1111 releaseName = "TwistedCore-%s" % (version,)1112 outputFile = self.outputDirectory.child(releaseName + ".tar.bz2")1113 buildPath = lambda *args: '/'.join((releaseName,) + args)1114 tarball = self._createBasicSubprojectTarball(1115 "core", version, outputFile)1116 1117 # Include the bin directory for the subproject.1118 for path in self.rootDirectory.child("bin").children():1119 if not path.isdir():1120 tarball.add(path.path, buildPath("bin", path.basename()))1121 1122 # Include all files within twisted/ that aren't part of a subproject.1123 for path in self.rootDirectory.child("twisted").children():1124 if path.basename() == "plugins":1125 for plugin in path.children():1126 for subproject in self.subprojects:1127 if plugin.basename() == "twisted_%s.py" % (subproject,):1128 break1129 else:1130 tarball.add(plugin.path,1131 buildPath("twisted", "plugins",1132 plugin.basename()))1133 elif not path.basename() in self.subprojects + ["topfiles"]:1134 tarball.add(path.path, buildPath("twisted", path.basename()))1135 1136 tarball.add(self.rootDirectory.child("twisted").child("topfiles").path,1137 releaseName)1138 tarball.close()1139 1140 return outputFile1141 1142 1143 def buildSubProject(self, projectName, version):1144 """1145 Build a subproject distribution in1146 C{Twisted<Projectname>-<version>.tar.bz2}.1147 1148 @type projectName: C{str}1149 @param projectName: The lowercase name of the subproject to build.1150 @type version: C{str}1151 @param version: The version of Twisted to build.1152 1153 @return: The tarball file.1154 @rtype: L{FilePath}.1155 """1156 releaseName = "Twisted%s-%s" % (projectName.capitalize(), version)1157 outputFile = self.outputDirectory.child(releaseName + ".tar.bz2")1158 buildPath = lambda *args: '/'.join((releaseName,) + args)1159 subProjectDir = self.rootDirectory.child("twisted").child(projectName)1160 1161 tarball = self._createBasicSubprojectTarball(projectName, version,1162 outputFile)1163 1164 tarball.add(subProjectDir.child("topfiles").path, releaseName)1165 1166 # Include all files in the subproject package except for topfiles.1167 for child in subProjectDir.children():1168 name = child.basename()1169 if name != "topfiles":1170 tarball.add(1171 child.path,1172 buildPath("twisted", projectName, name))1173 1174 pluginsDir = self.rootDirectory.child("twisted").child("plugins")1175 # Include the plugin for the subproject.1176 pluginFileName = "twisted_%s.py" % (projectName,)1177 pluginFile = pluginsDir.child(pluginFileName)1178 if pluginFile.exists():1179 tarball.add(pluginFile.path,1180 buildPath("twisted", "plugins", pluginFileName))1181 1182 # Include the bin directory for the subproject.1183 binPath = self.rootDirectory.child("bin").child(projectName)1184 if binPath.isdir():1185 tarball.add(binPath.path, buildPath("bin"))1186 tarball.close()1187 1188 return outputFile1189 1190 1191 def _createBasicSubprojectTarball(self, projectName, version, outputFile):1192 """1193 Helper method to create and fill a tarball with things common between1194 subprojects and core.1195 1196 @param projectName: The subproject's name.1197 @type projectName: C{str}1198 @param version: The version of the release.1199 @type version: C{str}1200 @param outputFile: The location of the tar file to create.1201 @type outputFile: L{FilePath}1202 """1203 releaseName = "Twisted%s-%s" % (projectName.capitalize(), version)1204 buildPath = lambda *args: '/'.join((releaseName,) + args)1205 1206 tarball = tarfile.TarFile.open(outputFile.path, 'w:bz2')1207 1208 tarball.add(self.rootDirectory.child("LICENSE").path,1209 buildPath("LICENSE"))1210 1211 docPath = self.rootDirectory.child("doc").child(projectName)1212 1213 if docPath.isdir():1214 for child in docPath.walk():1215 self._buildDocInDir(child, version, docPath.child("howto"))1216 tarball.add(docPath.path, buildPath("doc"))1217 1218 return tarball1219 1220 1221 1222 787 class UncleanWorkingDirectory(Exception): 1223 788 """ 1224 789 Raised when the working directory of an SVN checkout is unclean. 1225 790 """ 1226 791 1227 792 793 1228 794 class NotWorkingDirectory(Exception): 1229 795 """ 1230 796 Raised when a directory does not appear to be an SVN working directory. 1231 797 """ 1232 798 1233 799 800 1234 801 def buildAllTarballs(checkout, destination): 1235 802 """ 1236 803 Build complete tarballs (including documentation) for Twisted and all … … def buildAllTarballs(checkout, destination): 1265 832 version = Project(twistedPath).getVersion() 1266 833 versionString = version.base() 1267 834 1268 apiBaseURL = "http://twistedmatrix.com/documents/%s/api/%%s.html" % (1269 versionString)1270 835 if not destination.exists(): 1271 836 destination.createDirectory() 1272 db = DistributionBuilder(export, destination, apiBaseURL=apiBaseURL) 837 db = DistributionBuilder(export, destination, 838 apiBaseURL=makeAPIBaseURL(versionString)) 1273 839 1274 840 db.buildCore(versionString) 1275 841 for subproject in twisted_subprojects: … … def buildAllTarballs(checkout, destination): 1281 847 workPath.remove() 1282 848 1283 849 850 1284 851 class ChangeVersionsScript(object): 1285 852 """ 1286 853 A thing for changing version numbers. See L{main}. -
twisted/python/dist.py
diff --git a/twisted/python/dist.py b/twisted/python/dist.py index 5727065..6d91b52 100644
a b Don't use this outside of Twisted. 6 6 Maintainer: Christopher Armstrong 7 7 """ 8 8 9 import sys, os 10 from distutils.command import build_scripts, install_data, build_ext, build_py 9 import os 10 from distutils.command import (build_scripts, install_data, build_ext, 11 sdist) 11 12 from distutils.errors import CompileError 12 13 from distutils import core 13 14 from distutils.core import Extension 15 from twisted.python.filepath import FilePath 16 from twisted.python._dist import DistributionBuilder, makeAPIBaseURL 17 from twisted.python._dist import isDistributable 14 18 15 twisted_subprojects = ["conch", "lore", "mail", "names",16 "news", "pair", "runner", "web", "web2",17 "words", "vfs"]18 19 19 20 20 21 class ConditionalExtension(Extension): … … def setup(**kw): 46 47 """ 47 48 return core.setup(**get_setup_args(**kw)) 48 49 50 51 49 52 def get_setup_args(**kw): 50 53 if 'twisted_subproject' in kw: 51 54 if 'twisted' not in os.listdir('.'): … … def get_setup_args(**kw): 73 76 kw.setdefault('py_modules', []).extend(py_modules) 74 77 del kw['plugins'] 75 78 76 if 'cmdclass' not in kw: 77 kw['cmdclass'] = { 79 defaultCmdClasses = { 78 80 'install_data': install_data_twisted, 79 81 'build_scripts': build_scripts_twisted} 80 if sys.version_info[:3] < (2, 3, 0): 81 kw['cmdclass']['build_py'] = build_py_twisted 82 83 if 'cmdclass' in kw: 84 # Override our defaults with setup.py's custom cmdclasses 85 defaultCmdClasses.update(kw['cmdclass']) 86 kw['cmdclass'] = defaultCmdClasses 82 87 83 88 if "conditionalExtensions" in kw: 84 89 extensions = kw["conditionalExtensions"] … … def get_setup_args(**kw): 100 105 kw.setdefault('cmdclass', {})['build_ext'] = my_build_ext 101 106 return kw 102 107 108 109 103 110 def getVersion(proj, base="twisted"): 104 111 """ 105 112 Extract the version number for a given project. … … def getVersion(proj, base="twisted"): 120 127 return ns['version'].base() 121 128 122 129 123 # Names that are exluded from globbing results:124 EXCLUDE_NAMES = ["{arch}", "CVS", ".cvsignore", "_darcs",125 "RCS", "SCCS", ".svn"]126 EXCLUDE_PATTERNS = ["*.py[cdo]", "*.s[ol]", ".#*", "*~", "*.py"]127 128 import fnmatch129 130 def _filterNames(names):131 """Given a list of file names, return those names that should be copied.132 """133 names = [n for n in names134 if n not in EXCLUDE_NAMES]135 # This is needed when building a distro from a working136 # copy (likely a checkout) rather than a pristine export:137 for pattern in EXCLUDE_PATTERNS:138 names = [n for n in names139 if (not fnmatch.fnmatch(n, pattern))140 and (not n.endswith('.py'))]141 return names142 130 143 131 def relativeTo(base, relativee): 144 132 """ 145 Gets 'relativee' relative to 'basepath'. 146 147 i.e., 148 149 >>> relativeTo('/home/', '/home/radix/') 150 'radix' 151 >>> relativeTo('.', '/home/radix/Projects/Twisted') # curdir is /home/radix 152 'Projects/Twisted' 153 154 The 'relativee' must be a child of 'basepath'. 133 Converts the path to C{base} to a path to {relativee}. 134 135 >>> relativeTo('/home', '/home/radix') 136 '/home/radix' 137 >>> relativeTo('../radix/', '/home/radix/foo') 138 '../radix/foo' 139 140 @type base: C{str} 141 @param base: A filesystem path. 142 @type relativee: C{str} 143 @param relativee: A filesystem path that is a child of C{base}. 144 @rtype: C{str} 145 @return: A filesystem path to relativee. 155 146 """ 156 147 basepath = os.path.abspath(base) 157 148 relativee = os.path.abspath(relativee) … … def relativeTo(base, relativee): 163 154 raise ValueError("%s is not a subpath of %s" % (relativee, basepath)) 164 155 165 156 157 166 158 def getDataFiles(dname, ignore=None, parent=None): 167 159 """ 168 160 Get all the data files that should be included in this distutils Project. … … def getDataFiles(dname, ignore=None, parent=None): 186 178 result = [] 187 179 for directory, subdirectories, filenames in os.walk(dname): 188 180 resultfiles = [] 189 for exname in EXCLUDE_NAMES: 190 if exname in subdirectories: 191 subdirectories.remove(exname) 192 for ig in ignore: 193 if ig in subdirectories: 194 subdirectories.remove(ig) 195 for filename in _filterNames(filenames): 196 resultfiles.append(filename) 181 basePath = FilePath(os.path.join(dname, directory)) 182 183 for subdir in subdirectories: 184 if subdir in ignore or not isDistributable(basePath.child(subdir)): 185 subdirectories.remove(subdir) 186 187 for filename in filenames: 188 if (isDistributable(basePath.child(filename)) 189 and not filename.endswith(".py")): 190 resultfiles.append(filename) 191 197 192 if resultfiles: 193 # Sort our results so that tests pass regardless of the underlying 194 # filesystem's file order. 195 resultfiles.sort() 198 196 result.append((relativeTo(parent, directory), 199 197 [relativeTo(parent, 200 198 os.path.join(directory, filename)) 201 199 for filename in resultfiles])) 202 200 return result 203 201 202 203 204 204 def getPackages(dname, pkgname=None, results=None, ignore=None, parent=None): 205 205 """ 206 206 Get all packages which are under dname. This is necessary for … … def getScripts(projname, basedir=''): 251 251 [os.path.join(scriptdir, x) for x in thingies]) 252 252 253 253 254 ## Helpers and distutil tweaks255 256 class build_py_twisted(build_py.build_py):257 """258 Changes behavior in Python 2.2 to support simultaneous specification of259 `packages' and `py_modules'.260 """261 def run(self):262 if self.py_modules:263 self.build_modules()264 if self.packages:265 self.build_packages()266 self.byte_compile(self.get_outputs(include_bytecode=0))267 268 254 255 ## Helpers and distutil tweaks 269 256 270 257 class build_scripts_twisted(build_scripts.build_scripts): 271 258 """Renames scripts so they end with '.py' on Windows.""" … … class build_ext_twisted(build_ext.build_ext): 359 346 self.compiler.announce("checking for %s ..." % header_name, 0) 360 347 return self._compile_helper("#include <%s>\n" % header_name) 361 348 349 350 class _SDistTwisted(sdist.sdist): 351 """ 352 Build a Twisted source distribution like the official release scripts do. 353 """ 354 355 def get_file_list(self): 356 """ 357 Overridden to do nothing. 358 359 Twisted does not use a MANIFEST file. 360 """ 361 362 def make_release_tree(self, basedir, _): 363 """ 364 Overridden to call the official release scripts' functionality. 365 366 Builds a Twisted source distribution in the given directory. 367 """ 368 if 'twisted' not in os.listdir('.'): 369 raise RuntimeError("Sorry, you need to run setup.py from the " 370 "toplevel source directory.") 371 372 rootDirectory = FilePath(".") 373 outputDirectory = FilePath(".") 374 version = self.distribution.get_version() 375 builder = DistributionBuilder(rootDirectory, outputDirectory, 376 apiBaseURL=makeAPIBaseURL(version)) 377 builder.buildTwistedFiles(version, basedir) 378 379 self.distribution.metadata.write_pkg_info(basedir) -
new file twisted/python/test/test__dist.py
diff --git a/twisted/python/test/test__dist.py b/twisted/python/test/test__dist.py new file mode 100644 index 0000000..e84f385
- + 1 # Copyright (c) 2010 Twisted Matrix Laboratories. 2 # See LICENSE for details. 3 4 import os, stat, errno, tarfile 5 from xml.dom import minidom as dom 6 from twisted.trial.unittest import TestCase 7 from twisted.python.filepath import FilePath 8 9 from twisted.python._dist import DocBuilder, ManBuilder, isDistributable 10 from twisted.python._dist import makeAPIBaseURL, DistributionBuilder 11 from twisted.python._dist import NoDocumentsFound, filePathDelta 12 from twisted.python._dist import _stageFile 13 14 15 16 # When we test that scripts are installed with the "correct" permissions, we 17 # expect the "correct" permissions to be rwxr-xr-x 18 SCRIPT_PERMS = ( 19 stat.S_IRWXU # rwx 20 | stat.S_IRGRP | stat.S_IXGRP # r-x 21 | stat.S_IROTH | stat.S_IXOTH) # r-x 22 23 24 25 # Check a bunch of dependencies to skip tests if necessary. 26 try: 27 from twisted.lore.scripts import lore 28 except ImportError: 29 loreSkip = "Lore is not present." 30 else: 31 loreSkip = None 32 33 34 35 class StructureAssertingMixin(object): 36 """ 37 A mixin for L{TestCase} subclasses which provides some methods for asserting 38 the structure and contents of directories and files on the filesystem. 39 """ 40 def createStructure(self, parent, dirDict, origRoot=None): 41 """ 42 Create a set of directories and files given a dict defining their 43 structure. 44 45 @param parent: The directory in which to create the structure. It must 46 already exist. 47 @type parent: L{FilePath} 48 49 @param dirDict: The dict defining the structure. Keys should be strings 50 naming files, values should be strings describing file contents OR 51 dicts describing subdirectories. All files are written in binary 52 mode. Any string values are assumed to describe text files and 53 will have their newlines replaced with the platform-native newline 54 convention. For example:: 55 56 {"foofile": "foocontents", 57 "bardir": {"barfile": "bar\ncontents"}} 58 @type dirDict: C{dict} 59 60 @param origRoot: The directory provided as C{parent} in the original 61 invocation of C{createStructure}. Leave this as C{None}, it's used 62 in recursion. 63 @type origRoot: L{FilePath} or C{None} 64 """ 65 if origRoot is None: 66 origRoot = parent 67 68 for x in dirDict: 69 child = parent.child(x) 70 if isinstance(dirDict[x], dict): 71 child.createDirectory() 72 self.createStructure(child, dirDict[x], origRoot=origRoot) 73 74 # If x is in a bin directory, make sure children 75 # representing files have the executable bit set. 76 if "bin" in child.segmentsFrom(origRoot): 77 for script in [k for (k,v) in dirDict[x].items() 78 if isinstance(v, basestring)]: 79 scriptPath = child.child(script) 80 scriptPath.chmod(SCRIPT_PERMS) 81 82 else: 83 child.setContent(dirDict[x].replace('\n', os.linesep)) 84 85 def assertStructure(self, root, dirDict): 86 """ 87 Assert that a directory is equivalent to one described by a dict. 88 89 @param root: The filesystem directory to compare. 90 @type root: L{FilePath} 91 @param dirDict: The dict that should describe the contents of the 92 directory. It should be the same structure as the C{dirDict} 93 parameter to L{createStructure}. 94 @type dirDict: C{dict} 95 """ 96 children = [x.basename() for x in root.children()] 97 for x in dirDict: 98 child = root.child(x) 99 if isinstance(dirDict[x], dict): 100 self.assertTrue(child.isdir(), "%s is not a dir!" 101 % (child.path,)) 102 self.assertStructure(child, dirDict[x]) 103 104 # If x is in a bin directory, make sure children 105 # representing files have the executable bit set. 106 if "/bin" in child.path: 107 for script in [k for (k,v) in dirDict[x].items() 108 if isinstance(v, basestring)]: 109 scriptPath = child.child(script) 110 scriptPath.restat() 111 # What with SVN and umask and all that jazz, all we can 112 # really check is that these scripts have at least one 113 # executable bit set. 114 self.assertTrue(scriptPath.statinfo.st_mode & 115 (stat.S_IXUSR|stat.S_IXGRP|stat.S_IXOTH), 116 "File %r should be executable" 117 % (scriptPath.path,)) 118 else: 119 a = child.getContent().replace(os.linesep, '\n') 120 self.assertEquals(a, dirDict[x], child.path) 121 children.remove(x) 122 if children: 123 self.fail("There were extra children in %s: %s" 124 % (root.path, children)) 125 126 127 def assertExtractedStructure(self, outputFile, dirDict): 128 """ 129 Assert that a tarfile content is equivalent to one described by a dict. 130 131 @param outputFile: The tar file built by L{DistributionBuilder}. 132 @type outputFile: L{FilePath}. 133 @param dirDict: The dict that should describe the contents of the 134 directory. It should be the same structure as the C{dirDict} 135 parameter to L{createStructure}. 136 @type dirDict: C{dict} 137 """ 138 tarFile = tarfile.TarFile.open(outputFile.path, "r:bz2") 139 extracted = FilePath(self.mktemp()) 140 extracted.createDirectory() 141 for info in tarFile: 142 tarFile.extract(info, path=extracted.path) 143 self.assertStructure(extracted.children()[0], dirDict) 144 145 146 147 class BuilderTestsMixin(object): 148 """ 149 A mixin class which provides various methods for creating sample Lore input 150 and output. 151 152 @cvar template: The lore template that will be used to prepare sample 153 output. 154 @type template: C{str} 155 156 @ivar docCounter: A counter which is incremented every time input is 157 generated and which is included in the documents. 158 @type docCounter: C{int} 159 """ 160 template = ''' 161 <html> 162 <head><title>Yo:</title></head> 163 <body> 164 <div class="body" /> 165 <a href="index.html">Index</a> 166 <span class="version">Version: </span> 167 </body> 168 </html> 169 ''' 170 171 def setUp(self): 172 """ 173 Initialize the doc counter which ensures documents are unique. 174 """ 175 self.docCounter = 0 176 177 178 def assertXMLEqual(self, first, second): 179 """ 180 Verify that two strings represent the same XML document. 181 """ 182 self.assertEqual( 183 dom.parseString(first).toxml(), 184 dom.parseString(second).toxml()) 185 186 187 def getArbitraryOutput(self, version, counter, prefix="", apiBaseURL="%s"): 188 """ 189 Get the correct HTML output for the arbitrary input returned by 190 L{getArbitraryLoreInput} for the given parameters. 191 192 @param version: The version string to include in the output. 193 @type version: C{str} 194 @param counter: A counter to include in the output. 195 @type counter: C{int} 196 """ 197 document = """\ 198 <?xml version="1.0"?><html> 199 <head><title>Yo:Hi! Title: %(count)d</title></head> 200 <body> 201 <div class="content">Hi! %(count)d<div class="API"><a href="%(foobarLink)s" title="foobar">foobar</a></div></div> 202 <a href="%(prefix)sindex.html">Index</a> 203 <span class="version">Version: %(version)s</span> 204 </body> 205 </html>""" 206 # Try to normalize irrelevant whitespace. 207 return dom.parseString( 208 document % {"count": counter, "prefix": prefix, 209 "version": version, 210 "foobarLink": apiBaseURL % ("foobar",)}).toxml('utf-8') 211 212 213 def getArbitraryLoreInput(self, counter): 214 """ 215 Get an arbitrary, unique (for this test case) string of lore input. 216 217 @param counter: A counter to include in the input. 218 @type counter: C{int} 219 """ 220 template = ( 221 '<html>' 222 '<head><title>Hi! Title: %(count)s</title></head>' 223 '<body>' 224 'Hi! %(count)s' 225 '<div class="API">foobar</div>' 226 '</body>' 227 '</html>') 228 return template % {"count": counter} 229 230 231 def getArbitraryLoreInputAndOutput(self, version, prefix="", 232 apiBaseURL="%s"): 233 """ 234 Get an input document along with expected output for lore run on that 235 output document, assuming an appropriately-specified C{self.template}. 236 237 @param version: A version string to include in the input and output. 238 @type version: C{str} 239 @param prefix: The prefix to include in the link to the index. 240 @type prefix: C{str} 241 242 @return: A two-tuple of input and expected output. 243 @rtype: C{(str, str)}. 244 """ 245 self.docCounter += 1 246 return (self.getArbitraryLoreInput(self.docCounter), 247 self.getArbitraryOutput(version, self.docCounter, 248 prefix=prefix, apiBaseURL=apiBaseURL)) 249 250 251 def getArbitraryManInput(self): 252 """ 253 Get an arbitrary man page content. 254 """ 255 return """.TH MANHOLE "1" "August 2001" "" "" 256 .SH NAME 257 manhole \- Connect to a Twisted Manhole service 258 .SH SYNOPSIS 259 .B manhole 260 .SH DESCRIPTION 261 manhole is a GTK interface to Twisted Manhole services. You can execute python 262 code as if at an interactive Python console inside a running Twisted process 263 with this.""" 264 265 266 def getArbitraryManLoreOutput(self): 267 """ 268 Get an arbitrary lore input document which represents man-to-lore 269 output based on the man page returned from L{getArbitraryManInput} 270 """ 271 return """\ 272 <?xml version="1.0"?> 273 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" 274 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> 275 <html><head> 276 <title>MANHOLE.1</title></head> 277 <body> 278 279 <h1>MANHOLE.1</h1> 280 281 <h2>NAME</h2> 282 283 <p>manhole - Connect to a Twisted Manhole service 284 </p> 285 286 <h2>SYNOPSIS</h2> 287 288 <p><strong>manhole</strong> </p> 289 290 <h2>DESCRIPTION</h2> 291 292 <p>manhole is a GTK interface to Twisted Manhole services. You can execute python 293 code as if at an interactive Python console inside a running Twisted process 294 with this.</p> 295 296 </body> 297 </html> 298 """ 299 300 def getArbitraryManHTMLOutput(self, version, prefix=""): 301 """ 302 Get an arbitrary lore output document which represents the lore HTML 303 output based on the input document returned from 304 L{getArbitraryManLoreOutput}. 305 306 @param version: A version string to include in the document. 307 @type version: C{str} 308 @param prefix: The prefix to include in the link to the index. 309 @type prefix: C{str} 310 """ 311 # Try to normalize the XML a little bit. 312 return dom.parseString("""\ 313 <?xml version="1.0" ?><html> 314 <head><title>Yo:MANHOLE.1</title></head> 315 <body> 316 <div class="content"> 317 318 <span/> 319 320 <h2>NAME<a name="auto0"/></h2> 321 322 <p>manhole - Connect to a Twisted Manhole service 323 </p> 324 325 <h2>SYNOPSIS<a name="auto1"/></h2> 326 327 <p><strong>manhole</strong> </p> 328 329 <h2>DESCRIPTION<a name="auto2"/></h2> 330 331 <p>manhole is a GTK interface to Twisted Manhole services. You can execute python 332 code as if at an interactive Python console inside a running Twisted process 333 with this.</p> 334 335 </div> 336 <a href="%(prefix)sindex.html">Index</a> 337 <span class="version">Version: %(version)s</span> 338 </body> 339 </html>""" % { 340 'prefix': prefix, 'version': version}).toxml("utf-8") 341 342 343 344 class DocBuilderTestCase(TestCase, BuilderTestsMixin): 345 """ 346 Tests for L{DocBuilder}. 347 348 Note for future maintainers: The exact byte equality assertions throughout 349 this suite may need to be updated due to minor differences in lore. They 350 should not be taken to mean that Lore must maintain the same byte format 351 forever. Feel free to update the tests when Lore changes, but please be 352 careful. 353 """ 354 skip = loreSkip 355 356 def setUp(self): 357 """ 358 Set up a few instance variables that will be useful. 359 360 @ivar builder: A plain L{DocBuilder}. 361 @ivar docCounter: An integer to be used as a counter by the 362 C{getArbitrary...} methods. 363 @ivar howtoDir: A L{FilePath} representing a directory to be used for 364 containing Lore documents. 365 @ivar templateFile: A L{FilePath} representing a file with 366 C{self.template} as its content. 367 """ 368 BuilderTestsMixin.setUp(self) 369 self.builder = DocBuilder() 370 self.howtoDir = FilePath(self.mktemp()) 371 self.howtoDir.createDirectory() 372 self.templateFile = self.howtoDir.child("template.tpl") 373 self.templateFile.setContent(self.template) 374 375 376 def test_build(self): 377 """ 378 The L{DocBuilder} runs lore on all .xhtml files within a directory. 379 """ 380 version = "1.2.3" 381 input1, output1 = self.getArbitraryLoreInputAndOutput(version) 382 input2, output2 = self.getArbitraryLoreInputAndOutput(version) 383 384 self.howtoDir.child("one.xhtml").setContent(input1) 385 self.howtoDir.child("two.xhtml").setContent(input2) 386 387 self.builder.build(version, self.howtoDir, self.howtoDir, 388 self.templateFile) 389 out1 = self.howtoDir.child('one.html') 390 out2 = self.howtoDir.child('two.html') 391 self.assertXMLEqual(out1.getContent(), output1) 392 self.assertXMLEqual(out2.getContent(), output2) 393 394 395 def test_noDocumentsFound(self): 396 """ 397 The C{build} method raises L{NoDocumentsFound} if there are no 398 .xhtml files in the given directory. 399 """ 400 self.assertRaises( 401 NoDocumentsFound, 402 self.builder.build, "1.2.3", self.howtoDir, self.howtoDir, 403 self.templateFile) 404 405 406 def test_parentDocumentLinking(self): 407 """ 408 The L{DocBuilder} generates correct links from documents to 409 template-generated links like stylesheets and index backreferences. 410 """ 411 input = self.getArbitraryLoreInput(0) 412 tutoDir = self.howtoDir.child("tutorial") 413 tutoDir.createDirectory() 414 tutoDir.child("child.xhtml").setContent(input) 415 self.builder.build("1.2.3", self.howtoDir, tutoDir, self.templateFile) 416 outFile = tutoDir.child('child.html') 417 self.assertIn('<a href="../index.html">Index</a>', 418 outFile.getContent()) 419 420 421 def test_siblingDirectoryDocumentLinking(self): 422 """ 423 It is necessary to generate documentation in a directory foo/bar where 424 stylesheet and indexes are located in foo/baz. Such resources should be 425 appropriately linked to. 426 """ 427 input = self.getArbitraryLoreInput(0) 428 resourceDir = self.howtoDir.child("resources") 429 docDir = self.howtoDir.child("docs") 430 docDir.createDirectory() 431 docDir.child("child.xhtml").setContent(input) 432 self.builder.build("1.2.3", resourceDir, docDir, self.templateFile) 433 outFile = docDir.child('child.html') 434 self.assertIn('<a href="../resources/index.html">Index</a>', 435 outFile.getContent()) 436 437 438 def test_apiLinking(self): 439 """ 440 The L{DocBuilder} generates correct links from documents to API 441 documentation. 442 """ 443 version = "1.2.3" 444 input, output = self.getArbitraryLoreInputAndOutput(version) 445 self.howtoDir.child("one.xhtml").setContent(input) 446 447 self.builder.build(version, self.howtoDir, self.howtoDir, 448 self.templateFile, "scheme:apilinks/%s.ext") 449 out = self.howtoDir.child('one.html') 450 self.assertIn( 451 '<a href="scheme:apilinks/foobar.ext" title="foobar">foobar</a>', 452 out.getContent()) 453 454 455 def test_deleteInput(self): 456 """ 457 L{DocBuilder.build} can be instructed to delete the input files after 458 generating the output based on them. 459 """ 460 input1 = self.getArbitraryLoreInput(0) 461 self.howtoDir.child("one.xhtml").setContent(input1) 462 self.builder.build("whatever", self.howtoDir, self.howtoDir, 463 self.templateFile, deleteInput=True) 464 self.assertTrue(self.howtoDir.child('one.html').exists()) 465 self.assertFalse(self.howtoDir.child('one.xhtml').exists()) 466 467 468 def test_doNotDeleteInput(self): 469 """ 470 Input will not be deleted by default. 471 """ 472 input1 = self.getArbitraryLoreInput(0) 473 self.howtoDir.child("one.xhtml").setContent(input1) 474 self.builder.build("whatever", self.howtoDir, self.howtoDir, 475 self.templateFile) 476 self.assertTrue(self.howtoDir.child('one.html').exists()) 477 self.assertTrue(self.howtoDir.child('one.xhtml').exists()) 478 479 480 def test_getLinkrelToSameDirectory(self): 481 """ 482 If the doc and resource directories are the same, the linkrel should be 483 an empty string. 484 """ 485 linkrel = self.builder.getLinkrel(FilePath("/foo/bar"), 486 FilePath("/foo/bar")) 487 self.assertEquals(linkrel, "") 488 489 490 def test_getLinkrelToParentDirectory(self): 491 """ 492 If the doc directory is a child of the resource directory, the linkrel 493 should make use of '..'. 494 """ 495 linkrel = self.builder.getLinkrel(FilePath("/foo"), 496 FilePath("/foo/bar")) 497 self.assertEquals(linkrel, "../") 498 499 500 def test_getLinkrelToSibling(self): 501 """ 502 If the doc directory is a sibling of the resource directory, the 503 linkrel should make use of '..' and a named segment. 504 """ 505 linkrel = self.builder.getLinkrel(FilePath("/foo/howto"), 506 FilePath("/foo/examples")) 507 self.assertEquals(linkrel, "../howto/") 508 509 510 def test_getLinkrelToUncle(self): 511 """ 512 If the doc directory is a sibling of the parent of the resource 513 directory, the linkrel should make use of multiple '..'s and a named 514 segment. 515 """ 516 linkrel = self.builder.getLinkrel(FilePath("/foo/howto"), 517 FilePath("/foo/examples/quotes")) 518 self.assertEquals(linkrel, "../../howto/") 519 520 521 522 class ManBuilderTestCase(TestCase, BuilderTestsMixin): 523 """ 524 Tests for L{ManBuilder}. 525 """ 526 skip = loreSkip 527 528 def setUp(self): 529 """ 530 Set up a few instance variables that will be useful. 531 532 @ivar builder: A plain L{ManBuilder}. 533 @ivar manDir: A L{FilePath} representing a directory to be used for 534 containing man pages. 535 """ 536 BuilderTestsMixin.setUp(self) 537 self.builder = ManBuilder() 538 self.manDir = FilePath(self.mktemp()) 539 self.manDir.createDirectory() 540 541 542 def test_noDocumentsFound(self): 543 """ 544 L{ManBuilder.build} raises L{NoDocumentsFound} if there are no 545 .1 files in the given directory. 546 """ 547 self.assertRaises(NoDocumentsFound, self.builder.build, self.manDir) 548 549 550 def test_build(self): 551 """ 552 Check that L{ManBuilder.build} find the man page in the directory, and 553 successfully produce a Lore content. 554 """ 555 manContent = self.getArbitraryManInput() 556 self.manDir.child('test1.1').setContent(manContent) 557 self.builder.build(self.manDir) 558 output = self.manDir.child('test1-man.xhtml').getContent() 559 expected = self.getArbitraryManLoreOutput() 560 # No-op on *nix, fix for windows 561 expected = expected.replace('\n', os.linesep) 562 self.assertEquals(output, expected) 563 564 565 def test_toHTML(self): 566 """ 567 Check that the content output by C{build} is compatible as input of 568 L{DocBuilder.build}. 569 """ 570 manContent = self.getArbitraryManInput() 571 self.manDir.child('test1.1').setContent(manContent) 572 self.builder.build(self.manDir) 573 574 templateFile = self.manDir.child("template.tpl") 575 templateFile.setContent(DocBuilderTestCase.template) 576 docBuilder = DocBuilder() 577 docBuilder.build("1.2.3", self.manDir, self.manDir, 578 templateFile) 579 output = self.manDir.child('test1-man.html').getContent() 580 581 self.assertXMLEqual( 582 output, 583 """\ 584 <?xml version="1.0" ?><html> 585 <head><title>Yo:MANHOLE.1</title></head> 586 <body> 587 <div class="content"> 588 589 <span/> 590 591 <h2>NAME<a name="auto0"/></h2> 592 593 <p>manhole - Connect to a Twisted Manhole service 594 </p> 595 596 <h2>SYNOPSIS<a name="auto1"/></h2> 597 598 <p><strong>manhole</strong> </p> 599 600 <h2>DESCRIPTION<a name="auto2"/></h2> 601 602 <p>manhole is a GTK interface to Twisted Manhole services. You can execute python 603 code as if at an interactive Python console inside a running Twisted process 604 with this.</p> 605 606 </div> 607 <a href="index.html">Index</a> 608 <span class="version">Version: 1.2.3</span> 609 </body> 610 </html>""") 611 612 613 614 class DistributionBuilderTestBase(BuilderTestsMixin, StructureAssertingMixin, 615 TestCase): 616 """ 617 Base for tests of L{DistributionBuilder}. 618 """ 619 skip = loreSkip 620 621 def setUp(self): 622 BuilderTestsMixin.setUp(self) 623 624 self.rootDir = FilePath(self.mktemp()) 625 self.rootDir.createDirectory() 626 627 self.outputDir = FilePath(self.mktemp()) 628 self.outputDir.createDirectory() 629 self.builder = DistributionBuilder(self.rootDir, self.outputDir) 630 631 632 633 class DistributionBuilderTest(DistributionBuilderTestBase): 634 635 def test_twistedDistribution(self): 636 """ 637 The Twisted tarball contains everything in the source checkout, with 638 built documentation. 639 """ 640 loreInput, loreOutput = self.getArbitraryLoreInputAndOutput("10.0.0") 641 manInput1 = self.getArbitraryManInput() 642 manOutput1 = self.getArbitraryManHTMLOutput("10.0.0", "../howto/") 643 manInput2 = self.getArbitraryManInput() 644 manOutput2 = self.getArbitraryManHTMLOutput("10.0.0", "../howto/") 645 coreIndexInput, coreIndexOutput = self.getArbitraryLoreInputAndOutput( 646 "10.0.0", prefix="howto/") 647 648 structure = { 649 "README": "Twisted", 650 "unrelated": "x", 651 "LICENSE": "copyright!", 652 "setup.py": "import toplevel", 653 "bin": {"web": {"websetroot": "SET ROOT"}, 654 "twistd": "TWISTD"}, 655 "twisted": 656 {"web": 657 {"__init__.py": "import WEB", 658 "topfiles": {"setup.py": "import WEBINSTALL", 659 "README": "WEB!"}}, 660 "words": {"__init__.py": "import WORDS"}, 661 "plugins": {"twisted_web.py": "import WEBPLUG", 662 "twisted_words.py": "import WORDPLUG"}}, 663 "doc": {"web": {"howto": {"index.xhtml": loreInput}, 664 "man": {"websetroot.1": manInput2}}, 665 "core": {"howto": {"template.tpl": self.template}, 666 "man": {"twistd.1": manInput1}, 667 "index.xhtml": coreIndexInput}}} 668 669 outStructure = { 670 "README": "Twisted", 671 "unrelated": "x", 672 "LICENSE": "copyright!", 673 "setup.py": "import toplevel", 674 "bin": {"web": {"websetroot": "SET ROOT"}, 675 "twistd": "TWISTD"}, 676 "twisted": 677 {"web": {"__init__.py": "import WEB", 678 "topfiles": {"setup.py": "import WEBINSTALL", 679 "README": "WEB!"}}, 680 "words": {"__init__.py": "import WORDS"}, 681 "plugins": {"twisted_web.py": "import WEBPLUG", 682 "twisted_words.py": "import WORDPLUG"}}, 683 "doc": {"web": {"howto": {"index.html": loreOutput}, 684 "man": {"websetroot.1": manInput2, 685 "websetroot-man.html": manOutput2}}, 686 "core": {"howto": {"template.tpl": self.template}, 687 "man": {"twistd.1": manInput1, 688 "twistd-man.html": manOutput1}, 689 "index.html": coreIndexOutput}}} 690 691 self.createStructure(self.rootDir, structure) 692 693 outputFile = self.builder.buildTwisted("10.0.0") 694 695 self.assertExtractedStructure(outputFile, outStructure) 696 697 698 def test_stageFileStagesFiles(self): 699 """ 700 L{_stageFile} duplicates the content and metadata of the given file. 701 """ 702 # Make a test file 703 inputFile = self.rootDir.child("foo") 704 705 # Put some content in it. 706 inputFile.setContent("bar") 707 708 # Put a funny mode on it. 709 modeReadOnly = stat.S_IRUSR|stat.S_IRGRP|stat.S_IROTH 710 inputFile.chmod(modeReadOnly) 711 712 # Test the first: stage the file into an existing directory. 713 outputFile1 = self.outputDir.child("foo") 714 715 # Test the second: stage the file into a new directory. 716 outputFile2 = self.outputDir.preauthChild("sub/dir/foo") 717 718 for outputFile in [outputFile1, outputFile2]: 719 _stageFile(inputFile, outputFile) 720 721 # Check the contents of the staged file 722 self.failUnlessEqual(outputFile.open("r").read(), "bar") 723 724 # Check the mode of the staged file 725 outputFile.restat() 726 self.assertEqual(outputFile.statinfo.st_mode, 727 (modeReadOnly | stat.S_IFREG)) 728 729 730 def test_stageFileWillNotOverwrite(self): 731 """ 732 L{_stageFile} raises an exception if asked to overwrite an output file. 733 """ 734 # Make a test file 735 inputFile = self.rootDir.child("foo") 736 inputFile.setContent("bar") 737 738 # Make an output file. 739 outputFile = self.outputDir.child("foo") 740 741 # First attempt should work fine. 742 _stageFile(inputFile, outputFile) 743 744 # Second attempt should raise OSError with EEXIST. 745 exception = self.failUnlessRaises(OSError, _stageFile, inputFile, 746 outputFile) 747 748 self.failUnlessEqual(exception.errno, errno.EEXIST) 749 750 751 def test_stageFileStagesDirectories(self): 752 """ 753 L{_stageFile} duplicates the content of the given directory. 754 """ 755 # Make a test directory with stuff in it. 756 structure = { 757 "somedirectory": { 758 "somefile": "some content", 759 "anotherdirectory": { 760 "anotherfile": "other content"}}} 761 self.createStructure(self.rootDir, structure) 762 inputDirectory = self.rootDir.child("somedirectory") 763 764 # Stage this directory structure 765 outputDirectory = self.outputDir.child("somedirectory") 766 _stageFile(inputDirectory, outputDirectory) 767 768 # Check that everything was copied across properly. 769 self.assertStructure(self.outputDir, structure) 770 771 772 def test_stageFileFiltersBytecode(self): 773 """ 774 L{_stageFile} ignores Python bytecode files. 775 """ 776 # Make a test directory with stuff in it. 777 inputStructure = { 778 "somepackage": { 779 "__init__.py": "", 780 "__init__.pyc": "gibberish", 781 "__init__.pyo": "more gibberish", 782 "module.py": "import this", 783 "module.pyc": "extra gibberish", 784 "module.pyo": "bonus gibberish", 785 "datafile.xqz": "A file with an unknown extension"}, 786 "somemodule.py": "import that", 787 "somemodule.pyc": "surplus gibberish", 788 "somemodule.pyo": "sundry gibberish"} 789 self.createStructure(self.rootDir, inputStructure) 790 791 # Stage this directory structure 792 for child in self.rootDir.children(): 793 dest = self.outputDir.child(child.basename()) 794 _stageFile(child, dest) 795 796 # Check that everything but bytecode files has been copied across. 797 outputStructure = { 798 "somepackage": { 799 # Ordinary Python files should be copied. 800 "__init__.py": "", 801 "module.py": "import this", 802 803 # .pyc and .pyc files should be ignored. 804 805 # Other unknown files should be copied too. 806 "datafile.xqz": "A file with an unknown extension"}, 807 # Individually staged files should be copied, unless they're 808 # bytecode files. 809 "somemodule.py": "import that"} 810 self.assertStructure(self.outputDir, outputStructure) 811 812 813 def test_stageFileFiltersVCSMetadata(self): 814 """ 815 L{_stageFile} ignores common VCS directories. 816 """ 817 # Make a test directory with stuff in it. 818 inputStructure = { 819 # Twisted's official repository is Subversion. 820 ".svn": { 821 "svn-data": "some Subversion data"}, 822 # Twisted has a semi-official bzr mirror of the svn repository. 823 ".bzr": { 824 "bzr-data": "some Bazaar data"}, 825 # git-svn is a popular way for git users to deal with svn 826 # repositories. 827 ".git": { 828 "git-data": "some Git data"}, 829 "somepackage": { 830 # Subversion litters its .svn directories everywhere, not just 831 # the top-level. 832 ".svn": { 833 "svn-data": "more Subversion data"}, 834 "__init__.py": "", 835 "module.py": "import this"}, 836 "somemodule.py": "import that"} 837 self.createStructure(self.rootDir, inputStructure) 838 839 # Stage this directory structure 840 for child in self.rootDir.children(): 841 dest = self.outputDir.child(child.basename()) 842 _stageFile(child, dest) 843 844 # Check that everything but VCS files has been copied across. 845 outputStructure = { 846 # No VCS files in the root. 847 "somepackage": { 848 # Ordinary Python files should be copied. 849 "__init__.py": "", 850 "module.py": "import this", 851 852 # No VCS files in the package, either. 853 }, 854 855 # Individually staged files should be copied, unless they're 856 # bytecode files. 857 "somemodule.py": "import that"} 858 self.assertStructure(self.outputDir, outputStructure) 859 860 861 def test_stageFileHandlesEXDEV(self): 862 """ 863 L{_stageFile} should fall back to copying if os.link raises EXDEV. 864 """ 865 def mock_link(src, dst): 866 raise OSError(errno.EXDEV, "dummy error") 867 868 # Mock out os.link so that it always fails with EXDEV. 869 self.patch(os, "link", mock_link) 870 871 # Staging a file should still work properly. 872 873 # Make a test file 874 inputFile = self.rootDir.child("foo") 875 inputFile.setContent("bar") 876 modeReadOnly = stat.S_IRUSR|stat.S_IRGRP|stat.S_IROTH 877 inputFile.chmod(modeReadOnly) 878 879 # Stage the file into an existing directory. 880 outputFile = self.outputDir.child("foo") 881 _stageFile(inputFile, outputFile) 882 883 # Check the contents of the staged file 884 self.failUnlessEqual(outputFile.open("r").read(), "bar") 885 886 # Check the mode of the staged file 887 outputFile.restat() 888 self.assertEqual(outputFile.statinfo.st_mode, 889 (modeReadOnly | stat.S_IFREG)) 890 891 if not getattr(os, "link", None): 892 test_stageFileHandlesEXDEV.skip = "OS does not support hard-links" 893 894 895 def test_twistedDistributionExcludesWeb2AndVFSAndAdmin(self): 896 """ 897 The main Twisted distribution does not include web2 or vfs, or the 898 bin/admin directory. 899 """ 900 loreInput, loreOutput = self.getArbitraryLoreInputAndOutput("10.0.0") 901 coreIndexInput, coreIndexOutput = self.getArbitraryLoreInputAndOutput( 902 "10.0.0", prefix="howto/") 903 904 structure = { 905 "README": "Twisted", 906 "unrelated": "x", 907 "LICENSE": "copyright!", 908 "setup.py": "import toplevel", 909 "bin": {"web2": {"websetroot": "SET ROOT"}, 910 "vfs": {"vfsitup": "hee hee"}, 911 "twistd": "TWISTD", 912 "admin": {"build-a-thing": "yay"}}, 913 "twisted": 914 {"web2": 915 {"__init__.py": "import WEB", 916 "topfiles": {"setup.py": "import WEBINSTALL", 917 "README": "WEB!"}}, 918 "vfs": 919 {"__init__.py": "import VFS", 920 "blah blah": "blah blah"}, 921 "words": {"__init__.py": "import WORDS"}, 922 "plugins": {"twisted_web.py": "import WEBPLUG", 923 "twisted_words.py": "import WORDPLUG", 924 "twisted_web2.py": "import WEB2", 925 "twisted_vfs.py": "import VFS"}}, 926 "doc": {"web2": {"excluded!": "yay"}, 927 "vfs": {"unrelated": "whatever"}, 928 "core": {"howto": {"template.tpl": self.template}, 929 "index.xhtml": coreIndexInput}}} 930 931 outStructure = { 932 "README": "Twisted", 933 "unrelated": "x", 934 "LICENSE": "copyright!", 935 "setup.py": "import toplevel", 936 "bin": {"twistd": "TWISTD"}, 937 "twisted": 938 {"words": {"__init__.py": "import WORDS"}, 939 "plugins": {"twisted_web.py": "import WEBPLUG", 940 "twisted_words.py": "import WORDPLUG"}}, 941 "doc": {"core": {"howto": {"template.tpl": self.template}, 942 "index.html": coreIndexOutput}}} 943 self.createStructure(self.rootDir, structure) 944 945 outputFile = self.builder.buildTwisted("10.0.0") 946 947 self.assertExtractedStructure(outputFile, outStructure) 948 949 950 def test_subProjectLayout(self): 951 """ 952 The subproject tarball includes files like so: 953 954 1. twisted/<subproject>/topfiles defines the files that will be in the 955 top level in the tarball, except LICENSE, which comes from the real 956 top-level directory. 957 2. twisted/<subproject> is included, but without the topfiles entry 958 in that directory. No other twisted subpackages are included. 959 3. twisted/plugins/twisted_<subproject>.py is included, but nothing 960 else in plugins is. 961 """ 962 structure = { 963 "README": "HI!@", 964 "unrelated": "x", 965 "LICENSE": "copyright!", 966 "setup.py": "import toplevel", 967 "bin": {"web": {"websetroot": "SET ROOT"}, 968 "words": {"im": "#!im"}}, 969 "twisted": 970 {"web": 971 {"__init__.py": "import WEB", 972 "topfiles": {"setup.py": "import WEBINSTALL", 973 "README": "WEB!"}}, 974 "words": {"__init__.py": "import WORDS"}, 975 "plugins": {"twisted_web.py": "import WEBPLUG", 976 "twisted_words.py": "import WORDPLUG"}}} 977 978 outStructure = { 979 "README": "WEB!", 980 "LICENSE": "copyright!", 981 "setup.py": "import WEBINSTALL", 982 "bin": {"websetroot": "SET ROOT"}, 983 "twisted": {"web": {"__init__.py": "import WEB"}, 984 "plugins": {"twisted_web.py": "import WEBPLUG"}}} 985 986 self.createStructure(self.rootDir, structure) 987 988 outputFile = self.builder.buildSubProject("web", "0.3.0") 989 990 self.assertExtractedStructure(outputFile, outStructure) 991 992 993 def test_minimalSubProjectLayout(self): 994 """ 995 L{DistributionBuilder.buildSubProject} works with minimal subprojects. 996 """ 997 structure = { 998 "LICENSE": "copyright!", 999 "bin": {}, 1000 "twisted": 1001 {"web": {"__init__.py": "import WEB", 1002 "topfiles": {"setup.py": "import WEBINSTALL"}}, 1003 "plugins": {}}} 1004 1005 outStructure = { 1006 "setup.py": "import WEBINSTALL", 1007 "LICENSE": "copyright!", 1008 "twisted": {"web": {"__init__.py": "import WEB"}}} 1009 1010 self.createStructure(self.rootDir, structure) 1011 1012 outputFile = self.builder.buildSubProject("web", "0.3.0") 1013 1014 self.assertExtractedStructure(outputFile, outStructure) 1015 1016 1017 def test_subProjectDocBuilding(self): 1018 """ 1019 When building a subproject release, documentation should be built with 1020 lore. 1021 """ 1022 loreInput, loreOutput = self.getArbitraryLoreInputAndOutput("0.3.0") 1023 manInput = self.getArbitraryManInput() 1024 manOutput = self.getArbitraryManHTMLOutput("0.3.0", "../howto/") 1025 structure = { 1026 "LICENSE": "copyright!", 1027 "twisted": {"web": {"__init__.py": "import WEB", 1028 "topfiles": {"setup.py": "import WEBINST"}}}, 1029 "doc": {"web": {"howto": {"index.xhtml": loreInput}, 1030 "man": {"twistd.1": manInput}}, 1031 "core": {"howto": {"template.tpl": self.template}} 1032 } 1033 } 1034 1035 outStructure = { 1036 "LICENSE": "copyright!", 1037 "setup.py": "import WEBINST", 1038 "twisted": {"web": {"__init__.py": "import WEB"}}, 1039 "doc": {"howto": {"index.html": loreOutput}, 1040 "man": {"twistd.1": manInput, 1041 "twistd-man.html": manOutput}}} 1042 1043 self.createStructure(self.rootDir, structure) 1044 1045 outputFile = self.builder.buildSubProject("web", "0.3.0") 1046 1047 self.assertExtractedStructure(outputFile, outStructure) 1048 1049 1050 def test_coreProjectLayout(self): 1051 """ 1052 The core tarball looks a lot like a subproject tarball, except it 1053 doesn't include: 1054 1055 - Python packages from other subprojects 1056 - plugins from other subprojects 1057 - scripts from other subprojects 1058 """ 1059 indexInput, indexOutput = self.getArbitraryLoreInputAndOutput( 1060 "8.0.0", prefix="howto/") 1061 howtoInput, howtoOutput = self.getArbitraryLoreInputAndOutput("8.0.0") 1062 specInput, specOutput = self.getArbitraryLoreInputAndOutput( 1063 "8.0.0", prefix="../howto/") 1064 upgradeInput, upgradeOutput = self.getArbitraryLoreInputAndOutput( 1065 "8.0.0", prefix="../howto/") 1066 tutorialInput, tutorialOutput = self.getArbitraryLoreInputAndOutput( 1067 "8.0.0", prefix="../") 1068 1069 structure = { 1070 "LICENSE": "copyright!", 1071 "twisted": {"__init__.py": "twisted", 1072 "python": {"__init__.py": "python", 1073 "roots.py": "roots!"}, 1074 "conch": {"__init__.py": "conch", 1075 "unrelated.py": "import conch"}, 1076 "plugin.py": "plugin", 1077 "plugins": {"twisted_web.py": "webplug", 1078 "twisted_whatever.py": "include!", 1079 "cred.py": "include!"}, 1080 "topfiles": {"setup.py": "import CORE", 1081 "README": "core readme"}}, 1082 "doc": {"core": {"howto": {"template.tpl": self.template, 1083 "index.xhtml": howtoInput, 1084 "tutorial": 1085 {"index.xhtml": tutorialInput}}, 1086 "specifications": {"index.xhtml": specInput}, 1087 "upgrades": {"index.xhtml": upgradeInput}, 1088 "examples": {"foo.py": "foo.py"}, 1089 "index.xhtml": indexInput}, 1090 "web": {"howto": {"index.xhtml": "webindex"}}}, 1091 "bin": {"twistd": "TWISTD", 1092 "web": {"websetroot": "websetroot"}} 1093 } 1094 1095 outStructure = { 1096 "LICENSE": "copyright!", 1097 "setup.py": "import CORE", 1098 "README": "core readme", 1099 "twisted": {"__init__.py": "twisted", 1100 "python": {"__init__.py": "python", 1101 "roots.py": "roots!"}, 1102 "plugin.py": "plugin", 1103 "plugins": {"twisted_whatever.py": "include!", 1104 "cred.py": "include!"}}, 1105 "doc": {"howto": {"template.tpl": self.template, 1106 "index.html": howtoOutput, 1107 "tutorial": {"index.html": tutorialOutput}}, 1108 "specifications": {"index.html": specOutput}, 1109 "upgrades": {"index.html": upgradeOutput}, 1110 "examples": {"foo.py": "foo.py"}, 1111 "index.html": indexOutput}, 1112 "bin": {"twistd": "TWISTD"}, 1113 } 1114 1115 self.createStructure(self.rootDir, structure) 1116 outputFile = self.builder.buildCore("8.0.0") 1117 self.assertExtractedStructure(outputFile, outStructure) 1118 1119 1120 def test_apiBaseURL(self): 1121 """ 1122 L{DistributionBuilder} builds documentation with the specified 1123 API base URL. 1124 """ 1125 apiBaseURL = "http://%s" 1126 builder = DistributionBuilder(self.rootDir, self.outputDir, 1127 apiBaseURL=apiBaseURL) 1128 loreInput, loreOutput = self.getArbitraryLoreInputAndOutput( 1129 "0.3.0", apiBaseURL=apiBaseURL) 1130 structure = { 1131 "LICENSE": "copyright!", 1132 "twisted": {"web": {"__init__.py": "import WEB", 1133 "topfiles": {"setup.py": "import WEBINST"}}}, 1134 "doc": {"web": {"howto": {"index.xhtml": loreInput}}, 1135 "core": {"howto": {"template.tpl": self.template}} 1136 } 1137 } 1138 1139 outStructure = { 1140 "LICENSE": "copyright!", 1141 "setup.py": "import WEBINST", 1142 "twisted": {"web": {"__init__.py": "import WEB"}}, 1143 "doc": {"howto": {"index.html": loreOutput}}} 1144 1145 self.createStructure(self.rootDir, structure) 1146 outputFile = builder.buildSubProject("web", "0.3.0") 1147 self.assertExtractedStructure(outputFile, outStructure) 1148 1149 1150 1151 class IsDistributableTest(TestCase): 1152 """ 1153 Tests for L{isDistributable}. 1154 """ 1155 1156 1157 def test_fixedNamesExcluded(self): 1158 """ 1159 L{isDistributable} denies certain fixed names from being packaged. 1160 """ 1161 self.failUnlessEqual(isDistributable(FilePath("foo/_darcs")), False) 1162 1163 1164 def test_hiddenFilesExcluded(self): 1165 """ 1166 L{isDistributable} denies names beginning with a ".". 1167 """ 1168 self.failUnlessEqual(isDistributable(FilePath("foo/.svn")), False) 1169 1170 1171 def test_byteCodeFilesExcluded(self): 1172 """ 1173 L{isDistributable} denies Python bytecode files. 1174 """ 1175 self.failUnlessEqual(isDistributable(FilePath("foo/bar.pyc")), False) 1176 self.failUnlessEqual(isDistributable(FilePath("foo/bar.pyo")), False) 1177 1178 1179 def test_otherFilesIncluded(self): 1180 """ 1181 L{isDistributable} allows files with other names. 1182 """ 1183 self.failUnlessEqual(isDistributable(FilePath("foo/bar.py")), True) 1184 self.failUnlessEqual(isDistributable(FilePath("foo/README")), True) 1185 self.failUnlessEqual(isDistributable(FilePath("foo/twisted")), True) 1186 1187 1188 1189 class MakeAPIBaseURLTest(TestCase): 1190 """ 1191 Tests for L{makeAPIBaseURL}. 1192 """ 1193 1194 1195 def test_makeAPIBaseURLIsSubstitutable(self): 1196 """ 1197 L{makeAPIBaseURL} has a place to subtitute an API name. 1198 """ 1199 template = makeAPIBaseURL("12.34") 1200 1201 # Substitute in an API name. 1202 url = template % ("sasquatch",) 1203 1204 self.assertEqual(url, 1205 "http://twistedmatrix.com/documents/12.34/api/sasquatch.html") 1206 1207 1208 1209 class FilePathDeltaTest(TestCase): 1210 """ 1211 Tests for L{filePathDelta}. 1212 """ 1213 1214 def test_filePathDeltaSubdir(self): 1215 """ 1216 L{filePathDelta} can create a simple relative path to a child path. 1217 """ 1218 self.assertEquals(filePathDelta(FilePath("/foo/bar"), 1219 FilePath("/foo/bar/baz")), 1220 ["baz"]) 1221 1222 1223 def test_filePathDeltaSiblingDir(self): 1224 """ 1225 L{filePathDelta} can traverse upwards to create relative paths to 1226 siblings. 1227 """ 1228 self.assertEquals(filePathDelta(FilePath("/foo/bar"), 1229 FilePath("/foo/baz")), 1230 ["..", "baz"]) 1231 1232 1233 def test_filePathNoCommonElements(self): 1234 """ 1235 L{filePathDelta} can create relative paths to totally unrelated paths 1236 for maximum portability. 1237 """ 1238 self.assertEquals(filePathDelta(FilePath("/foo/bar"), 1239 FilePath("/baz/quux")), 1240 ["..", "..", "baz", "quux"]) 1241 1242 1243 def test_filePathDeltaSimilarEndElements(self): 1244 """ 1245 L{filePathDelta} doesn't take into account final elements when 1246 comparing 2 paths, but stops at the first difference. 1247 """ 1248 self.assertEquals(filePathDelta(FilePath("/foo/bar/bar/spam"), 1249 FilePath("/foo/bar/baz/spam")), 1250 ["..", "..", "baz", "spam"]) -
twisted/python/test/test_dist.py
diff --git a/twisted/python/test/test_dist.py b/twisted/python/test/test_dist.py index c69717d..00b819d 100644
a b from distutils.core import Distribution 13 13 from twisted.trial.unittest import TestCase 14 14 15 15 from twisted.python import dist 16 from twisted.python.dist import get_setup_args, ConditionalExtension 16 from twisted.python.dist import (get_setup_args, ConditionalExtension, 17 install_data_twisted, build_scripts_twisted, 18 getDataFiles) 17 19 from twisted.python.filepath import FilePath 18 20 19 21 … … class SetupTest(TestCase): 25 27 """ 26 28 Passing C{conditionalExtensions} as a list of L{ConditionalExtension} 27 29 objects to get_setup_args inserts a custom build_ext into the result 28 which knows how to check whether they should be 30 which knows how to check whether they should be. 29 31 """ 30 32 good_ext = ConditionalExtension("whatever", ["whatever.c"], 31 33 condition=lambda b: True) … … class SetupTest(TestCase): 55 57 self.assertEquals(ext.define_macros, [("whatever", 2), ("WIN32", 1)]) 56 58 57 59 60 def test_defaultCmdClasses(self): 61 """ 62 get_setup_args supplies default values for the cmdclass keyword. 63 """ 64 args = get_setup_args() 65 self.assertIn('cmdclass', args) 66 cmdclass = args['cmdclass'] 67 self.assertIn('install_data', cmdclass) 68 self.assertEquals(cmdclass['install_data'], install_data_twisted) 69 self.assertIn('build_scripts', cmdclass) 70 self.assertEquals(cmdclass['build_scripts'], build_scripts_twisted) 71 72 73 def test_settingCmdClasses(self): 74 """ 75 get_setup_args allows new cmdclasses to be added. 76 """ 77 args = get_setup_args(cmdclass={'foo': 'bar'}) 78 self.assertEquals(args['cmdclass']['foo'], 'bar') 79 80 81 def test_overridingCmdClasses(self): 82 """ 83 get_setup_args allows choosing which defaults to override. 84 """ 85 args = get_setup_args(cmdclass={'install_data': 'baz'}) 86 87 # Overridden cmdclass should be overridden 88 self.assertEquals(args['cmdclass']['install_data'], 'baz') 89 90 # Non-overridden cmdclasses should still be set to defaults. 91 self.assertEquals(args['cmdclass']['build_scripts'], 92 build_scripts_twisted) 93 94 58 95 59 96 class GetVersionTest(TestCase): 60 97 """ … … class GetScriptsTest(TestCase): 171 208 os.mkdir(basedir) 172 209 scripts = dist.getScripts('noscripts', basedir=basedir) 173 210 self.assertEquals(scripts, []) 211 212 213 214 class GetDataFilesTests(TestCase): 215 """ 216 Tests for L{getDataFiles}. 217 """ 218 219 def _makeBaseDir(self): 220 """ 221 Make a directory for getDataFiles to search. 222 """ 223 rawBaseDir = os.path.join(".", self.mktemp()) 224 baseDir = FilePath(rawBaseDir) 225 baseDir.makedirs() 226 227 return rawBaseDir, baseDir 228 229 230 def test_basicOperation(self): 231 """ 232 L{getDataFiles} finds a single data file in a given directory. 233 """ 234 # The directory where we'll put our data file. 235 rawBaseDir, baseDir = self._makeBaseDir() 236 237 # A data file to be found. 238 baseDir.child("foo.txt").touch() 239 240 results = getDataFiles(baseDir.path) 241 self.assertEquals( 242 results, 243 [(rawBaseDir, [os.path.join(rawBaseDir, "foo.txt")])]) 244 245 246 def test_directoryRecursion(self): 247 """ 248 L{getDataFiles} searches for data files inside subdirectories. 249 """ 250 rawBaseDir, baseDir = self._makeBaseDir() 251 252 subDir = baseDir.child("foo") 253 subDir.makedirs() 254 255 subDir.child("bar.txt").touch() 256 257 subSubDir = subDir.child("baz") 258 subSubDir.makedirs() 259 260 subSubDir.child("qux.txt").touch() 261 262 results = getDataFiles(baseDir.path) 263 self.assertEquals( 264 results, 265 [(os.path.join(rawBaseDir, "foo"), 266 [os.path.join(rawBaseDir, "foo", "bar.txt")]), 267 (os.path.join(rawBaseDir, "foo", "baz"), 268 [os.path.join(rawBaseDir, "foo", "baz", "qux.txt")])]) 269 270 271 def test_ignoreVCSMetadata(self): 272 """ 273 L{getDataFiles} ignores Subversion metadata files. 274 """ 275 rawBaseDir, baseDir = self._makeBaseDir() 276 277 # Top-level directory contains a VCS dir, containing ignorable data. 278 vcsDir = baseDir.child(".svn") 279 vcsDir.makedirs() 280 vcsDir.child("data.txt").touch() 281 282 # Subdirectory contains a valid data file. 283 subDir = baseDir.child("foo") 284 subDir.makedirs() 285 subDir.child("bar.txt").touch() 286 287 # Subdirectory contains another VCS dir, with more ignorable data. 288 subVcsDir = subDir.child("_darcs") 289 subVcsDir.makedirs() 290 subVcsDir.child("data.txt").touch() 291 292 # Subdirectory contains an ignorable VCS file. 293 subDir.child(".cvsignore").touch() 294 295 results = getDataFiles(baseDir.path) 296 self.assertEquals( 297 results, 298 [(os.path.join(rawBaseDir, "foo"), 299 [os.path.join(rawBaseDir, "foo", "bar.txt")])]) 300 301 302 def test_ignoreArbitrarySubdirectories(self): 303 """ 304 L{getDataFiles} ignores any filenames it's asked to ignore. 305 """ 306 rawBaseDir, baseDir = self._makeBaseDir() 307 308 subDir = baseDir.child("foo") 309 subDir.makedirs() 310 311 # Make an ordinary subdirectory with some data files. 312 subDir.child("bar.txt").touch() 313 subDir.child("ignorable").touch() # not a dir, won't be ignored 314 315 # Make a subdirectory with an ignorable name, and some data files. 316 ignorableSubDir = baseDir.child("ignorable") 317 ignorableSubDir.makedirs() 318 ignorableSubDir.child("bar.txt").touch() 319 320 results = getDataFiles(baseDir.path, ignore=["ignorable"]) 321 self.assertEquals( 322 results, 323 [(os.path.join(rawBaseDir, "foo"), 324 [os.path.join(rawBaseDir, "foo", "bar.txt"), 325 os.path.join(rawBaseDir, "foo", "ignorable")])]) 326 327 328 def test_ignoreNonDataFiles(self): 329 """ 330 L{getDataFiles} ignores Python code, backup files and bytecode. 331 """ 332 rawBaseDir, baseDir = self._makeBaseDir() 333 334 # All these are not data files, and should be ignored. 335 baseDir.child("module.py").touch() 336 baseDir.child("module.pyc").touch() 337 baseDir.child("module.pyo").touch() 338 339 subDir = baseDir.child("foo") 340 subDir.makedirs() 341 342 subDir.child("bar.txt").touch() 343 344 # An editor-made backup of bar.txt should be ignored. 345 subDir.child("bar.txt~").touch() 346 347 results = getDataFiles(baseDir.path) 348 self.assertEquals( 349 results, 350 [(os.path.join(rawBaseDir, "foo"), 351 [os.path.join(rawBaseDir, "foo", "bar.txt")])]) 352 353 354 def test_pathsRelativeToParent(self): 355 """ 356 L{getDataFiles} returns paths relative to the parent parameter. 357 """ 358 rawBaseDir, baseDir = self._makeBaseDir() 359 360 # munge rawBaseDir in a way that we can recognise later. 361 mungedBaseDir = os.path.join(rawBaseDir, "foo/../") 362 363 subDir = baseDir.child("foo") 364 subDir.makedirs() 365 366 subDir.child("bar.txt").touch() 367 368 results = getDataFiles(subDir.path, parent=mungedBaseDir) 369 self.assertEquals( 370 results, 371 [(os.path.join(mungedBaseDir, "foo"), 372 [os.path.join(mungedBaseDir, "foo", "bar.txt")])]) -
twisted/python/test/test_release.py
diff --git a/twisted/python/test/test_release.py b/twisted/python/test/test_release.py index a9eb872..84d1302 100644
a b import warnings 13 13 import operator 14 14 import os, sys, signal 15 15 from StringIO import StringIO 16 import tarfile17 from xml.dom import minidom as dom18 16 19 17 from datetime import date 20 18 … … from twisted.python._release import replaceProjectVersion 31 29 from twisted.python._release import updateTwistedVersionInformation, Project 32 30 from twisted.python._release import generateVersionFileData 33 31 from twisted.python._release import changeAllProjectVersions 34 from twisted.python._release import VERSION_OFFSET, DocBuilder, ManBuilder 35 from twisted.python._release import NoDocumentsFound, filePathDelta 32 from twisted.python._release import VERSION_OFFSET 36 33 from twisted.python._release import CommandFailed, BookBuilder 37 from twisted.python._release import DistributionBuilder,APIBuilder34 from twisted.python._release import APIBuilder 38 35 from twisted.python._release import BuildAPIDocsScript 39 36 from twisted.python._release import buildAllTarballs, runCommand 40 37 from twisted.python._release import UncleanWorkingDirectory, NotWorkingDirectory 41 38 from twisted.python._release import ChangeVersionsScript, BuildTarballsScript 42 39 from twisted.python._release import NewsBuilder 43 40 41 from twisted.python.test.test__dist import loreSkip, StructureAssertingMixin 42 from twisted.python.test.test__dist import BuilderTestsMixin 43 from twisted.python.test.test__dist import DistributionBuilderTestBase 44 44 45 if os.name != 'posix': 45 46 skip = "Release toolchain only supported on POSIX." 46 47 else: … … def genVersion(*args, **kwargs): 92 93 93 94 94 95 95 class StructureAssertingMixin(object):96 """97 A mixin for L{TestCase} subclasses which provides some methods for asserting98 the structure and contents of directories and files on the filesystem.99 """100 def createStructure(self, root, dirDict):101 """102 Create a set of directories and files given a dict defining their103 structure.104 105 @param root: The directory in which to create the structure. It must106 already exist.107 @type root: L{FilePath}108 109 @param dirDict: The dict defining the structure. Keys should be strings110 naming files, values should be strings describing file contents OR111 dicts describing subdirectories. All files are written in binary112 mode. Any string values are assumed to describe text files and113 will have their newlines replaced with the platform-native newline114 convention. For example::115 116 {"foofile": "foocontents",117 "bardir": {"barfile": "bar\ncontents"}}118 @type dirDict: C{dict}119 """120 for x in dirDict:121 child = root.child(x)122 if isinstance(dirDict[x], dict):123 child.createDirectory()124 self.createStructure(child, dirDict[x])125 else:126 child.setContent(dirDict[x].replace('\n', os.linesep))127 128 def assertStructure(self, root, dirDict):129 """130 Assert that a directory is equivalent to one described by a dict.131 132 @param root: The filesystem directory to compare.133 @type root: L{FilePath}134 @param dirDict: The dict that should describe the contents of the135 directory. It should be the same structure as the C{dirDict}136 parameter to L{createStructure}.137 @type dirDict: C{dict}138 """139 children = [x.basename() for x in root.children()]140 for x in dirDict:141 child = root.child(x)142 if isinstance(dirDict[x], dict):143 self.assertTrue(child.isdir(), "%s is not a dir!"144 % (child.path,))145 self.assertStructure(child, dirDict[x])146 else:147 a = child.getContent().replace(os.linesep, '\n')148 self.assertEquals(a, dirDict[x], child.path)149 children.remove(x)150 if children:151 self.fail("There were extra children in %s: %s"152 % (root.path, children))153 154 155 def assertExtractedStructure(self, outputFile, dirDict):156 """157 Assert that a tarfile content is equivalent to one described by a dict.158 159 @param outputFile: The tar file built by L{DistributionBuilder}.160 @type outputFile: L{FilePath}.161 @param dirDict: The dict that should describe the contents of the162 directory. It should be the same structure as the C{dirDict}163 parameter to L{createStructure}.164 @type dirDict: C{dict}165 """166 tarFile = tarfile.TarFile.open(outputFile.path, "r:bz2")167 extracted = FilePath(self.mktemp())168 extracted.createDirectory()169 for info in tarFile:170 tarFile.extract(info, path=extracted.path)171 self.assertStructure(extracted.children()[0], dirDict)172 173 174 175 96 class ChangeVersionTest(TestCase, StructureAssertingMixin): 176 97 """ 177 98 Twisted has the ability to change versions. … … class VersionWritingTest(TestCase): 537 458 538 459 539 460 540 class BuilderTestsMixin(object):541 """542 A mixin class which provides various methods for creating sample Lore input543 and output.544 545 @cvar template: The lore template that will be used to prepare sample546 output.547 @type template: C{str}548 549 @ivar docCounter: A counter which is incremented every time input is550 generated and which is included in the documents.551 @type docCounter: C{int}552 """553 template = '''554 <html>555 <head><title>Yo:</title></head>556 <body>557 <div class="body" />558 <a href="index.html">Index</a>559 <span class="version">Version: </span>560 </body>561 </html>562 '''563 564 def setUp(self):565 """566 Initialize the doc counter which ensures documents are unique.567 """568 self.docCounter = 0569 570 571 def assertXMLEqual(self, first, second):572 """573 Verify that two strings represent the same XML document.574 """575 self.assertEqual(576 dom.parseString(first).toxml(),577 dom.parseString(second).toxml())578 579 580 def getArbitraryOutput(self, version, counter, prefix="", apiBaseURL="%s"):581 """582 Get the correct HTML output for the arbitrary input returned by583 L{getArbitraryLoreInput} for the given parameters.584 585 @param version: The version string to include in the output.586 @type version: C{str}587 @param counter: A counter to include in the output.588 @type counter: C{int}589 """590 document = """\591 <?xml version="1.0"?><html>592 <head><title>Yo:Hi! Title: %(count)d</title></head>593 <body>594 <div class="content">Hi! %(count)d<div class="API"><a href="%(foobarLink)s" title="foobar">foobar</a></div></div>595 <a href="%(prefix)sindex.html">Index</a>596 <span class="version">Version: %(version)s</span>597 </body>598 </html>"""599 # Try to normalize irrelevant whitespace.600 return dom.parseString(601 document % {"count": counter, "prefix": prefix,602 "version": version,603 "foobarLink": apiBaseURL % ("foobar",)}).toxml('utf-8')604 605 606 def getArbitraryLoreInput(self, counter):607 """608 Get an arbitrary, unique (for this test case) string of lore input.609 610 @param counter: A counter to include in the input.611 @type counter: C{int}612 """613 template = (614 '<html>'615 '<head><title>Hi! Title: %(count)s</title></head>'616 '<body>'617 'Hi! %(count)s'618 '<div class="API">foobar</div>'619 '</body>'620 '</html>')621 return template % {"count": counter}622 623 624 def getArbitraryLoreInputAndOutput(self, version, prefix="",625 apiBaseURL="%s"):626 """627 Get an input document along with expected output for lore run on that628 output document, assuming an appropriately-specified C{self.template}.629 630 @param version: A version string to include in the input and output.631 @type version: C{str}632 @param prefix: The prefix to include in the link to the index.633 @type prefix: C{str}634 635 @return: A two-tuple of input and expected output.636 @rtype: C{(str, str)}.637 """638 self.docCounter += 1639 return (self.getArbitraryLoreInput(self.docCounter),640 self.getArbitraryOutput(version, self.docCounter,641 prefix=prefix, apiBaseURL=apiBaseURL))642 643 644 def getArbitraryManInput(self):645 """646 Get an arbitrary man page content.647 """648 return """.TH MANHOLE "1" "August 2001" "" ""649 .SH NAME650 manhole \- Connect to a Twisted Manhole service651 .SH SYNOPSIS652 .B manhole653 .SH DESCRIPTION654 manhole is a GTK interface to Twisted Manhole services. You can execute python655 code as if at an interactive Python console inside a running Twisted process656 with this."""657 658 659 def getArbitraryManLoreOutput(self):660 """661 Get an arbitrary lore input document which represents man-to-lore662 output based on the man page returned from L{getArbitraryManInput}663 """664 return """\665 <?xml version="1.0"?>666 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"667 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">668 <html><head>669 <title>MANHOLE.1</title></head>670 <body>671 672 <h1>MANHOLE.1</h1>673 674 <h2>NAME</h2>675 676 <p>manhole - Connect to a Twisted Manhole service677 </p>678 679 <h2>SYNOPSIS</h2>680 681 <p><strong>manhole</strong> </p>682 683 <h2>DESCRIPTION</h2>684 685 <p>manhole is a GTK interface to Twisted Manhole services. You can execute python686 code as if at an interactive Python console inside a running Twisted process687 with this.</p>688 689 </body>690 </html>691 """692 693 def getArbitraryManHTMLOutput(self, version, prefix=""):694 """695 Get an arbitrary lore output document which represents the lore HTML696 output based on the input document returned from697 L{getArbitraryManLoreOutput}.698 699 @param version: A version string to include in the document.700 @type version: C{str}701 @param prefix: The prefix to include in the link to the index.702 @type prefix: C{str}703 """704 # Try to normalize the XML a little bit.705 return dom.parseString("""\706 <?xml version="1.0" ?><html>707 <head><title>Yo:MANHOLE.1</title></head>708 <body>709 <div class="content">710 711 <span/>712 713 <h2>NAME<a name="auto0"/></h2>714 715 <p>manhole - Connect to a Twisted Manhole service716 </p>717 718 <h2>SYNOPSIS<a name="auto1"/></h2>719 720 <p><strong>manhole</strong> </p>721 722 <h2>DESCRIPTION<a name="auto2"/></h2>723 724 <p>manhole is a GTK interface to Twisted Manhole services. You can execute python725 code as if at an interactive Python console inside a running Twisted process726 with this.</p>727 728 </div>729 <a href="%(prefix)sindex.html">Index</a>730 <span class="version">Version: %(version)s</span>731 </body>732 </html>""" % {733 'prefix': prefix, 'version': version}).toxml("utf-8")734 735 736 737 class DocBuilderTestCase(TestCase, BuilderTestsMixin):738 """739 Tests for L{DocBuilder}.740 741 Note for future maintainers: The exact byte equality assertions throughout742 this suite may need to be updated due to minor differences in lore. They743 should not be taken to mean that Lore must maintain the same byte format744 forever. Feel free to update the tests when Lore changes, but please be745 careful.746 """747 skip = loreSkip748 749 def setUp(self):750 """751 Set up a few instance variables that will be useful.752 753 @ivar builder: A plain L{DocBuilder}.754 @ivar docCounter: An integer to be used as a counter by the755 C{getArbitrary...} methods.756 @ivar howtoDir: A L{FilePath} representing a directory to be used for757 containing Lore documents.758 @ivar templateFile: A L{FilePath} representing a file with759 C{self.template} as its content.760 """761 BuilderTestsMixin.setUp(self)762 self.builder = DocBuilder()763 self.howtoDir = FilePath(self.mktemp())764 self.howtoDir.createDirectory()765 self.templateFile = self.howtoDir.child("template.tpl")766 self.templateFile.setContent(self.template)767 768 769 def test_build(self):770 """771 The L{DocBuilder} runs lore on all .xhtml files within a directory.772 """773 version = "1.2.3"774 input1, output1 = self.getArbitraryLoreInputAndOutput(version)775 input2, output2 = self.getArbitraryLoreInputAndOutput(version)776 777 self.howtoDir.child("one.xhtml").setContent(input1)778 self.howtoDir.child("two.xhtml").setContent(input2)779 780 self.builder.build(version, self.howtoDir, self.howtoDir,781 self.templateFile)782 out1 = self.howtoDir.child('one.html')783 out2 = self.howtoDir.child('two.html')784 self.assertXMLEqual(out1.getContent(), output1)785 self.assertXMLEqual(out2.getContent(), output2)786 787 788 def test_noDocumentsFound(self):789 """790 The C{build} method raises L{NoDocumentsFound} if there are no791 .xhtml files in the given directory.792 """793 self.assertRaises(794 NoDocumentsFound,795 self.builder.build, "1.2.3", self.howtoDir, self.howtoDir,796 self.templateFile)797 798 799 def test_parentDocumentLinking(self):800 """801 The L{DocBuilder} generates correct links from documents to802 template-generated links like stylesheets and index backreferences.803 """804 input = self.getArbitraryLoreInput(0)805 tutoDir = self.howtoDir.child("tutorial")806 tutoDir.createDirectory()807 tutoDir.child("child.xhtml").setContent(input)808 self.builder.build("1.2.3", self.howtoDir, tutoDir, self.templateFile)809 outFile = tutoDir.child('child.html')810 self.assertIn('<a href="../index.html">Index</a>',811 outFile.getContent())812 813 814 def test_siblingDirectoryDocumentLinking(self):815 """816 It is necessary to generate documentation in a directory foo/bar where817 stylesheet and indexes are located in foo/baz. Such resources should be818 appropriately linked to.819 """820 input = self.getArbitraryLoreInput(0)821 resourceDir = self.howtoDir.child("resources")822 docDir = self.howtoDir.child("docs")823 docDir.createDirectory()824 docDir.child("child.xhtml").setContent(input)825 self.builder.build("1.2.3", resourceDir, docDir, self.templateFile)826 outFile = docDir.child('child.html')827 self.assertIn('<a href="../resources/index.html">Index</a>',828 outFile.getContent())829 830 831 def test_apiLinking(self):832 """833 The L{DocBuilder} generates correct links from documents to API834 documentation.835 """836 version = "1.2.3"837 input, output = self.getArbitraryLoreInputAndOutput(version)838 self.howtoDir.child("one.xhtml").setContent(input)839 840 self.builder.build(version, self.howtoDir, self.howtoDir,841 self.templateFile, "scheme:apilinks/%s.ext")842 out = self.howtoDir.child('one.html')843 self.assertIn(844 '<a href="scheme:apilinks/foobar.ext" title="foobar">foobar</a>',845 out.getContent())846 847 848 def test_deleteInput(self):849 """850 L{DocBuilder.build} can be instructed to delete the input files after851 generating the output based on them.852 """853 input1 = self.getArbitraryLoreInput(0)854 self.howtoDir.child("one.xhtml").setContent(input1)855 self.builder.build("whatever", self.howtoDir, self.howtoDir,856 self.templateFile, deleteInput=True)857 self.assertTrue(self.howtoDir.child('one.html').exists())858 self.assertFalse(self.howtoDir.child('one.xhtml').exists())859 860 861 def test_doNotDeleteInput(self):862 """863 Input will not be deleted by default.864 """865 input1 = self.getArbitraryLoreInput(0)866 self.howtoDir.child("one.xhtml").setContent(input1)867 self.builder.build("whatever", self.howtoDir, self.howtoDir,868 self.templateFile)869 self.assertTrue(self.howtoDir.child('one.html').exists())870 self.assertTrue(self.howtoDir.child('one.xhtml').exists())871 872 873 def test_getLinkrelToSameDirectory(self):874 """875 If the doc and resource directories are the same, the linkrel should be876 an empty string.877 """878 linkrel = self.builder.getLinkrel(FilePath("/foo/bar"),879 FilePath("/foo/bar"))880 self.assertEquals(linkrel, "")881 882 883 def test_getLinkrelToParentDirectory(self):884 """885 If the doc directory is a child of the resource directory, the linkrel886 should make use of '..'.887 """888 linkrel = self.builder.getLinkrel(FilePath("/foo"),889 FilePath("/foo/bar"))890 self.assertEquals(linkrel, "../")891 892 893 def test_getLinkrelToSibling(self):894 """895 If the doc directory is a sibling of the resource directory, the896 linkrel should make use of '..' and a named segment.897 """898 linkrel = self.builder.getLinkrel(FilePath("/foo/howto"),899 FilePath("/foo/examples"))900 self.assertEquals(linkrel, "../howto/")901 902 903 def test_getLinkrelToUncle(self):904 """905 If the doc directory is a sibling of the parent of the resource906 directory, the linkrel should make use of multiple '..'s and a named907 segment.908 """909 linkrel = self.builder.getLinkrel(FilePath("/foo/howto"),910 FilePath("/foo/examples/quotes"))911 self.assertEquals(linkrel, "../../howto/")912 913 914 915 461 class APIBuilderTestCase(TestCase): 916 462 """ 917 463 Tests for L{APIBuilder}. … … class APIBuilderTestCase(TestCase): 1051 597 1052 598 1053 599 1054 class ManBuilderTestCase(TestCase, BuilderTestsMixin):1055 """1056 Tests for L{ManBuilder}.1057 """1058 skip = loreSkip1059 1060 def setUp(self):1061 """1062 Set up a few instance variables that will be useful.1063 1064 @ivar builder: A plain L{ManBuilder}.1065 @ivar manDir: A L{FilePath} representing a directory to be used for1066 containing man pages.1067 """1068 BuilderTestsMixin.setUp(self)1069 self.builder = ManBuilder()1070 self.manDir = FilePath(self.mktemp())1071 self.manDir.createDirectory()1072 1073 1074 def test_noDocumentsFound(self):1075 """1076 L{ManBuilder.build} raises L{NoDocumentsFound} if there are no1077 .1 files in the given directory.1078 """1079 self.assertRaises(NoDocumentsFound, self.builder.build, self.manDir)1080 1081 1082 def test_build(self):1083 """1084 Check that L{ManBuilder.build} find the man page in the directory, and1085 successfully produce a Lore content.1086 """1087 manContent = self.getArbitraryManInput()1088 self.manDir.child('test1.1').setContent(manContent)1089 self.builder.build(self.manDir)1090 output = self.manDir.child('test1-man.xhtml').getContent()1091 expected = self.getArbitraryManLoreOutput()1092 # No-op on *nix, fix for windows1093 expected = expected.replace('\n', os.linesep)1094 self.assertEquals(output, expected)1095 1096 1097 def test_toHTML(self):1098 """1099 Check that the content output by C{build} is compatible as input of1100 L{DocBuilder.build}.1101 """1102 manContent = self.getArbitraryManInput()1103 self.manDir.child('test1.1').setContent(manContent)1104 self.builder.build(self.manDir)1105 1106 templateFile = self.manDir.child("template.tpl")1107 templateFile.setContent(DocBuilderTestCase.template)1108 docBuilder = DocBuilder()1109 docBuilder.build("1.2.3", self.manDir, self.manDir,1110 templateFile)1111 output = self.manDir.child('test1-man.html').getContent()1112 1113 self.assertXMLEqual(1114 output,1115 """\1116 <?xml version="1.0" ?><html>1117 <head><title>Yo:MANHOLE.1</title></head>1118 <body>1119 <div class="content">1120 1121 <span/>1122 1123 <h2>NAME<a name="auto0"/></h2>1124 1125 <p>manhole - Connect to a Twisted Manhole service1126 </p>1127 1128 <h2>SYNOPSIS<a name="auto1"/></h2>1129 1130 <p><strong>manhole</strong> </p>1131 1132 <h2>DESCRIPTION<a name="auto2"/></h2>1133 1134 <p>manhole is a GTK interface to Twisted Manhole services. You can execute python1135 code as if at an interactive Python console inside a running Twisted process1136 with this.</p>1137 1138 </div>1139 <a href="index.html">Index</a>1140 <span class="version">Version: 1.2.3</span>1141 </body>1142 </html>""")1143 1144 1145 1146 600 class BookBuilderTests(TestCase, BuilderTestsMixin): 1147 601 """ 1148 602 Tests for L{BookBuilder}. … … class BookBuilderTests(TestCase, BuilderTestsMixin): 1475 929 1476 930 1477 931 1478 class FilePathDeltaTest(TestCase):1479 """1480 Tests for L{filePathDelta}.1481 """1482 1483 def test_filePathDeltaSubdir(self):1484 """1485 L{filePathDelta} can create a simple relative path to a child path.1486 """1487 self.assertEquals(filePathDelta(FilePath("/foo/bar"),1488 FilePath("/foo/bar/baz")),1489 ["baz"])1490 1491 1492 def test_filePathDeltaSiblingDir(self):1493 """1494 L{filePathDelta} can traverse upwards to create relative paths to1495 siblings.1496 """1497 self.assertEquals(filePathDelta(FilePath("/foo/bar"),1498 FilePath("/foo/baz")),1499 ["..", "baz"])1500 1501 1502 def test_filePathNoCommonElements(self):1503 """1504 L{filePathDelta} can create relative paths to totally unrelated paths1505 for maximum portability.1506 """1507 self.assertEquals(filePathDelta(FilePath("/foo/bar"),1508 FilePath("/baz/quux")),1509 ["..", "..", "baz", "quux"])1510 1511 1512 def test_filePathDeltaSimilarEndElements(self):1513 """1514 L{filePathDelta} doesn't take into account final elements when1515 comparing 2 paths, but stops at the first difference.1516 """1517 self.assertEquals(filePathDelta(FilePath("/foo/bar/bar/spam"),1518 FilePath("/foo/bar/baz/spam")),1519 ["..", "..", "baz", "spam"])1520 1521 1522 1523 932 class NewsBuilderTests(TestCase, StructureAssertingMixin): 1524 933 """ 1525 934 Tests for L{NewsBuilder}. … … class NewsBuilderTests(TestCase, StructureAssertingMixin): 1976 1385 1977 1386 1978 1387 1979 class DistributionBuilderTestBase(BuilderTestsMixin, StructureAssertingMixin,1980 TestCase):1981 """1982 Base for tests of L{DistributionBuilder}.1983 """1984 skip = loreSkip1985 1986 def setUp(self):1987 BuilderTestsMixin.setUp(self)1988 1989 self.rootDir = FilePath(self.mktemp())1990 self.rootDir.createDirectory()1991 1992 self.outputDir = FilePath(self.mktemp())1993 self.outputDir.createDirectory()1994 self.builder = DistributionBuilder(self.rootDir, self.outputDir)1995 1996 1997 1998 class DistributionBuilderTest(DistributionBuilderTestBase):1999 2000 def test_twistedDistribution(self):2001 """2002 The Twisted tarball contains everything in the source checkout, with2003 built documentation.2004 """2005 loreInput, loreOutput = self.getArbitraryLoreInputAndOutput("10.0.0")2006 manInput1 = self.getArbitraryManInput()2007 manOutput1 = self.getArbitraryManHTMLOutput("10.0.0", "../howto/")2008 manInput2 = self.getArbitraryManInput()2009 manOutput2 = self.getArbitraryManHTMLOutput("10.0.0", "../howto/")2010 coreIndexInput, coreIndexOutput = self.getArbitraryLoreInputAndOutput(2011 "10.0.0", prefix="howto/")2012 2013 structure = {2014 "README": "Twisted",2015 "unrelated": "x",2016 "LICENSE": "copyright!",2017 "setup.py": "import toplevel",2018 "bin": {"web": {"websetroot": "SET ROOT"},2019 "twistd": "TWISTD"},2020 "twisted":2021 {"web":2022 {"__init__.py": "import WEB",2023 "topfiles": {"setup.py": "import WEBINSTALL",2024 "README": "WEB!"}},2025 "words": {"__init__.py": "import WORDS"},2026 "plugins": {"twisted_web.py": "import WEBPLUG",2027 "twisted_words.py": "import WORDPLUG"}},2028 "doc": {"web": {"howto": {"index.xhtml": loreInput},2029 "man": {"websetroot.1": manInput2}},2030 "core": {"howto": {"template.tpl": self.template},2031 "man": {"twistd.1": manInput1},2032 "index.xhtml": coreIndexInput}}}2033 2034 outStructure = {2035 "README": "Twisted",2036 "unrelated": "x",2037 "LICENSE": "copyright!",2038 "setup.py": "import toplevel",2039 "bin": {"web": {"websetroot": "SET ROOT"},2040 "twistd": "TWISTD"},2041 "twisted":2042 {"web": {"__init__.py": "import WEB",2043 "topfiles": {"setup.py": "import WEBINSTALL",2044 "README": "WEB!"}},2045 "words": {"__init__.py": "import WORDS"},2046 "plugins": {"twisted_web.py": "import WEBPLUG",2047 "twisted_words.py": "import WORDPLUG"}},2048 "doc": {"web": {"howto": {"index.html": loreOutput},2049 "man": {"websetroot.1": manInput2,2050 "websetroot-man.html": manOutput2}},2051 "core": {"howto": {"template.tpl": self.template},2052 "man": {"twistd.1": manInput1,2053 "twistd-man.html": manOutput1},2054 "index.html": coreIndexOutput}}}2055 2056 self.createStructure(self.rootDir, structure)2057 2058 outputFile = self.builder.buildTwisted("10.0.0")2059 2060 self.assertExtractedStructure(outputFile, outStructure)2061 2062 2063 def test_twistedDistributionExcludesWeb2AndVFSAndAdmin(self):2064 """2065 The main Twisted distribution does not include web2 or vfs, or the2066 bin/admin directory.2067 """2068 loreInput, loreOutput = self.getArbitraryLoreInputAndOutput("10.0.0")2069 coreIndexInput, coreIndexOutput = self.getArbitraryLoreInputAndOutput(2070 "10.0.0", prefix="howto/")2071 2072 structure = {2073 "README": "Twisted",2074 "unrelated": "x",2075 "LICENSE": "copyright!",2076 "setup.py": "import toplevel",2077 "bin": {"web2": {"websetroot": "SET ROOT"},2078 "vfs": {"vfsitup": "hee hee"},2079 "twistd": "TWISTD",2080 "admin": {"build-a-thing": "yay"}},2081 "twisted":2082 {"web2":2083 {"__init__.py": "import WEB",2084 "topfiles": {"setup.py": "import WEBINSTALL",2085 "README": "WEB!"}},2086 "vfs":2087 {"__init__.py": "import VFS",2088 "blah blah": "blah blah"},2089 "words": {"__init__.py": "import WORDS"},2090 "plugins": {"twisted_web.py": "import WEBPLUG",2091 "twisted_words.py": "import WORDPLUG",2092 "twisted_web2.py": "import WEB2",2093 "twisted_vfs.py": "import VFS"}},2094 "doc": {"web2": {"excluded!": "yay"},2095 "vfs": {"unrelated": "whatever"},2096 "core": {"howto": {"template.tpl": self.template},2097 "index.xhtml": coreIndexInput}}}2098 2099 outStructure = {2100 "README": "Twisted",2101 "unrelated": "x",2102 "LICENSE": "copyright!",2103 "setup.py": "import toplevel",2104 "bin": {"twistd": "TWISTD"},2105 "twisted":2106 {"words": {"__init__.py": "import WORDS"},2107 "plugins": {"twisted_web.py": "import WEBPLUG",2108 "twisted_words.py": "import WORDPLUG"}},2109 "doc": {"core": {"howto": {"template.tpl": self.template},2110 "index.html": coreIndexOutput}}}2111 self.createStructure(self.rootDir, structure)2112 2113 outputFile = self.builder.buildTwisted("10.0.0")2114 2115 self.assertExtractedStructure(outputFile, outStructure)2116 2117 2118 def test_subProjectLayout(self):2119 """2120 The subproject tarball includes files like so:2121 2122 1. twisted/<subproject>/topfiles defines the files that will be in the2123 top level in the tarball, except LICENSE, which comes from the real2124 top-level directory.2125 2. twisted/<subproject> is included, but without the topfiles entry2126 in that directory. No other twisted subpackages are included.2127 3. twisted/plugins/twisted_<subproject>.py is included, but nothing2128 else in plugins is.2129 """2130 structure = {2131 "README": "HI!@",2132 "unrelated": "x",2133 "LICENSE": "copyright!",2134 "setup.py": "import toplevel",2135 "bin": {"web": {"websetroot": "SET ROOT"},2136 "words": {"im": "#!im"}},2137 "twisted":2138 {"web":2139 {"__init__.py": "import WEB",2140 "topfiles": {"setup.py": "import WEBINSTALL",2141 "README": "WEB!"}},2142 "words": {"__init__.py": "import WORDS"},2143 "plugins": {"twisted_web.py": "import WEBPLUG",2144 "twisted_words.py": "import WORDPLUG"}}}2145 2146 outStructure = {2147 "README": "WEB!",2148 "LICENSE": "copyright!",2149 "setup.py": "import WEBINSTALL",2150 "bin": {"websetroot": "SET ROOT"},2151 "twisted": {"web": {"__init__.py": "import WEB"},2152 "plugins": {"twisted_web.py": "import WEBPLUG"}}}2153 2154 self.createStructure(self.rootDir, structure)2155 2156 outputFile = self.builder.buildSubProject("web", "0.3.0")2157 2158 self.assertExtractedStructure(outputFile, outStructure)2159 2160 2161 def test_minimalSubProjectLayout(self):2162 """2163 buildSubProject should work with minimal subprojects.2164 """2165 structure = {2166 "LICENSE": "copyright!",2167 "bin": {},2168 "twisted":2169 {"web": {"__init__.py": "import WEB",2170 "topfiles": {"setup.py": "import WEBINSTALL"}},2171 "plugins": {}}}2172 2173 outStructure = {2174 "setup.py": "import WEBINSTALL",2175 "LICENSE": "copyright!",2176 "twisted": {"web": {"__init__.py": "import WEB"}}}2177 2178 self.createStructure(self.rootDir, structure)2179 2180 outputFile = self.builder.buildSubProject("web", "0.3.0")2181 2182 self.assertExtractedStructure(outputFile, outStructure)2183 2184 2185 def test_subProjectDocBuilding(self):2186 """2187 When building a subproject release, documentation should be built with2188 lore.2189 """2190 loreInput, loreOutput = self.getArbitraryLoreInputAndOutput("0.3.0")2191 manInput = self.getArbitraryManInput()2192 manOutput = self.getArbitraryManHTMLOutput("0.3.0", "../howto/")2193 structure = {2194 "LICENSE": "copyright!",2195 "twisted": {"web": {"__init__.py": "import WEB",2196 "topfiles": {"setup.py": "import WEBINST"}}},2197 "doc": {"web": {"howto": {"index.xhtml": loreInput},2198 "man": {"twistd.1": manInput}},2199 "core": {"howto": {"template.tpl": self.template}}2200 }2201 }2202 2203 outStructure = {2204 "LICENSE": "copyright!",2205 "setup.py": "import WEBINST",2206 "twisted": {"web": {"__init__.py": "import WEB"}},2207 "doc": {"howto": {"index.html": loreOutput},2208 "man": {"twistd.1": manInput,2209 "twistd-man.html": manOutput}}}2210 2211 self.createStructure(self.rootDir, structure)2212 2213 outputFile = self.builder.buildSubProject("web", "0.3.0")2214 2215 self.assertExtractedStructure(outputFile, outStructure)2216 2217 2218 def test_coreProjectLayout(self):2219 """2220 The core tarball looks a lot like a subproject tarball, except it2221 doesn't include:2222 2223 - Python packages from other subprojects2224 - plugins from other subprojects2225 - scripts from other subprojects2226 """2227 indexInput, indexOutput = self.getArbitraryLoreInputAndOutput(2228 "8.0.0", prefix="howto/")2229 howtoInput, howtoOutput = self.getArbitraryLoreInputAndOutput("8.0.0")2230 specInput, specOutput = self.getArbitraryLoreInputAndOutput(2231 "8.0.0", prefix="../howto/")2232 upgradeInput, upgradeOutput = self.getArbitraryLoreInputAndOutput(2233 "8.0.0", prefix="../howto/")2234 tutorialInput, tutorialOutput = self.getArbitraryLoreInputAndOutput(2235 "8.0.0", prefix="../")2236 2237 structure = {2238 "LICENSE": "copyright!",2239 "twisted": {"__init__.py": "twisted",2240 "python": {"__init__.py": "python",2241 "roots.py": "roots!"},2242 "conch": {"__init__.py": "conch",2243 "unrelated.py": "import conch"},2244 "plugin.py": "plugin",2245 "plugins": {"twisted_web.py": "webplug",2246 "twisted_whatever.py": "include!",2247 "cred.py": "include!"},2248 "topfiles": {"setup.py": "import CORE",2249 "README": "core readme"}},2250 "doc": {"core": {"howto": {"template.tpl": self.template,2251 "index.xhtml": howtoInput,2252 "tutorial":2253 {"index.xhtml": tutorialInput}},2254 "specifications": {"index.xhtml": specInput},2255 "upgrades": {"index.xhtml": upgradeInput},2256 "examples": {"foo.py": "foo.py"},2257 "index.xhtml": indexInput},2258 "web": {"howto": {"index.xhtml": "webindex"}}},2259 "bin": {"twistd": "TWISTD",2260 "web": {"websetroot": "websetroot"}}2261 }2262 2263 outStructure = {2264 "LICENSE": "copyright!",2265 "setup.py": "import CORE",2266 "README": "core readme",2267 "twisted": {"__init__.py": "twisted",2268 "python": {"__init__.py": "python",2269 "roots.py": "roots!"},2270 "plugin.py": "plugin",2271 "plugins": {"twisted_whatever.py": "include!",2272 "cred.py": "include!"}},2273 "doc": {"howto": {"template.tpl": self.template,2274 "index.html": howtoOutput,2275 "tutorial": {"index.html": tutorialOutput}},2276 "specifications": {"index.html": specOutput},2277 "upgrades": {"index.html": upgradeOutput},2278 "examples": {"foo.py": "foo.py"},2279 "index.html": indexOutput},2280 "bin": {"twistd": "TWISTD"},2281 }2282 2283 self.createStructure(self.rootDir, structure)2284 outputFile = self.builder.buildCore("8.0.0")2285 self.assertExtractedStructure(outputFile, outStructure)2286 2287 2288 def test_apiBaseURL(self):2289 """2290 DistributionBuilder builds documentation with the specified2291 API base URL.2292 """2293 apiBaseURL = "http://%s"2294 builder = DistributionBuilder(self.rootDir, self.outputDir,2295 apiBaseURL=apiBaseURL)2296 loreInput, loreOutput = self.getArbitraryLoreInputAndOutput(2297 "0.3.0", apiBaseURL=apiBaseURL)2298 structure = {2299 "LICENSE": "copyright!",2300 "twisted": {"web": {"__init__.py": "import WEB",2301 "topfiles": {"setup.py": "import WEBINST"}}},2302 "doc": {"web": {"howto": {"index.xhtml": loreInput}},2303 "core": {"howto": {"template.tpl": self.template}}2304 }2305 }2306 2307 outStructure = {2308 "LICENSE": "copyright!",2309 "setup.py": "import WEBINST",2310 "twisted": {"web": {"__init__.py": "import WEB"}},2311 "doc": {"howto": {"index.html": loreOutput}}}2312 2313 self.createStructure(self.rootDir, structure)2314 outputFile = builder.buildSubProject("web", "0.3.0")2315 self.assertExtractedStructure(outputFile, outStructure)2316 2317 2318 2319 1388 class BuildAllTarballsTest(DistributionBuilderTestBase): 2320 1389 """ 2321 1390 Tests for L{DistributionBuilder.buildAllTarballs}. … … class ScriptTests(BuilderTestsMixin, StructureAssertingMixin, TestCase): 2631 1700 newsBuilder.buildAll = builds.append 2632 1701 newsBuilder.main(["/foo/bar/baz"]) 2633 1702 self.assertEquals(builds, [FilePath("/foo/bar/baz")]) 1703 1704 1705 -
twisted/topfiles/setup.py
diff --git a/twisted/topfiles/setup.py b/twisted/topfiles/setup.py index 90ba244..2876eff 100644
a b if os.path.exists('twisted'): 17 17 from twisted import copyright 18 18 from twisted.python.dist import setup, ConditionalExtension as Extension 19 19 from twisted.python.dist import getPackages, getDataFiles, getScripts 20 from twisted.python. dist import twisted_subprojects20 from twisted.python._dist import twisted_subprojects 21 21 22 22 23 23