Ticket #4138: sdist-support-4138.3.patch
File sdist-support-4138.3.patch, 141.7 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 c64ed3e..b96762d 100644
a b import textwrap 16 16 from datetime import date 17 17 import sys 18 18 import os 19 from tempfile import mkdtemp20 19 import tarfile 20 import errno 21 import shutil 22 from tempfile import mkdtemp 21 23 22 24 from subprocess import PIPE, STDOUT, Popen 23 25 24 26 from twisted.python.versions import Version 25 27 from twisted.python.filepath import FilePath 26 from twisted.python.dist import twisted_subprojects 27 28 # This import is an example of why you shouldn't use this module unless you're 29 # radix 30 try: 31 from twisted.lore.scripts import lore 32 except ImportError: 33 pass 28 from twisted.python._dist import LoreBuilderMixin, DistributionBuilder 29 from twisted.python._dist import makeAPIBaseURL, twisted_subprojects 30 from twisted.python._dist import ManBuilder, DocBuilder, NoDocumentsFound 31 from twisted.python._dist import isDistributable 34 32 35 33 # The offset between a year and the corresponding major version number. 36 34 VERSION_OFFSET = 2000 … … def getNextVersion(version, now=None): 109 107 return Version(version.package, major, minor, 0) 110 108 111 109 110 112 111 def changeAllProjectVersions(root, versionTemplate): 113 112 """ 114 113 Change the version of all projects (including core and all subprojects). … … def changeAllProjectVersions(root, versionTemplate): 139 138 140 139 141 140 142 143 141 class Project(object): 144 142 """ 145 143 A representation of a project that has a version. … … def updateTwistedVersionInformation(baseDirectory, now): 213 211 project.updateVersion(getNextVersion(project.getVersion(), now=now)) 214 212 215 213 214 216 215 def generateVersionFileData(version): 217 216 """ 218 217 Generate the data to be placed into a _version.py file. … … version = versions.Version(%r, %s, %s, %s%s) 231 230 return data 232 231 233 232 233 234 234 def replaceProjectVersion(filename, newversion): 235 235 """ 236 236 Write version specification code into the given filename, which … … def replaceInFile(filename, oldToNew): 266 266 267 267 268 268 269 class NoDocumentsFound(Exception):270 """271 Raised when no input documents are found.272 """273 274 275 276 class LoreBuilderMixin(object):277 """278 Base class for builders which invoke lore.279 """280 def lore(self, arguments):281 """282 Run lore with the given arguments.283 284 @param arguments: A C{list} of C{str} giving command line arguments to285 lore which should be used.286 """287 options = lore.Options()288 options.parseOptions(["--null"] + arguments)289 lore.runGivenOptions(options)290 291 292 293 class DocBuilder(LoreBuilderMixin):294 """295 Generate HTML documentation for projects.296 """297 298 def build(self, version, resourceDir, docDir, template, apiBaseURL=None,299 deleteInput=False):300 """301 Build the documentation in C{docDir} with Lore.302 303 Input files ending in .xhtml will be considered. Output will written as304 .html files.305 306 @param version: the version of the documentation to pass to lore.307 @type version: C{str}308 309 @param resourceDir: The directory which contains the toplevel index and310 stylesheet file for this section of documentation.311 @type resourceDir: L{twisted.python.filepath.FilePath}312 313 @param docDir: The directory of the documentation.314 @type docDir: L{twisted.python.filepath.FilePath}315 316 @param template: The template used to generate the documentation.317 @type template: L{twisted.python.filepath.FilePath}318 319 @type apiBaseURL: C{str} or C{NoneType}320 @param apiBaseURL: A format string which will be interpolated with the321 fully-qualified Python name for each API link. For example, to322 generate the Twisted 8.0.0 documentation, pass323 C{"http://twistedmatrix.com/documents/8.0.0/api/%s.html"}.324 325 @param deleteInput: If True, the input documents will be deleted after326 their output is generated.327 @type deleteInput: C{bool}328 329 @raise NoDocumentsFound: When there are no .xhtml files in the given330 C{docDir}.331 """332 linkrel = self.getLinkrel(resourceDir, docDir)333 inputFiles = docDir.globChildren("*.xhtml")334 filenames = [x.path for x in inputFiles]335 if not filenames:336 raise NoDocumentsFound("No input documents found in %s" % (docDir,))337 if apiBaseURL is not None:338 arguments = ["--config", "baseurl=" + apiBaseURL]339 else:340 arguments = []341 arguments.extend(["--config", "template=%s" % (template.path,),342 "--config", "ext=.html",343 "--config", "version=%s" % (version,),344 "--linkrel", linkrel] + filenames)345 self.lore(arguments)346 if deleteInput:347 for inputFile in inputFiles:348 inputFile.remove()349 350 351 def getLinkrel(self, resourceDir, docDir):352 """353 Calculate a value appropriate for Lore's --linkrel option.354 355 Lore's --linkrel option defines how to 'find' documents that are356 linked to from TEMPLATE files (NOT document bodies). That is, it's a357 prefix for links ('a' and 'link') in the template.358 359 @param resourceDir: The directory which contains the toplevel index and360 stylesheet file for this section of documentation.361 @type resourceDir: L{twisted.python.filepath.FilePath}362 363 @param docDir: The directory containing documents that must link to364 C{resourceDir}.365 @type docDir: L{twisted.python.filepath.FilePath}366 """367 if resourceDir != docDir:368 return '/'.join(filePathDelta(docDir, resourceDir)) + "/"369 else:370 return ""371 372 373 374 class ManBuilder(LoreBuilderMixin):375 """376 Generate man pages of the different existing scripts.377 """378 379 def build(self, manDir):380 """381 Generate Lore input files from the man pages in C{manDir}.382 383 Input files ending in .1 will be considered. Output will written as384 -man.xhtml files.385 386 @param manDir: The directory of the man pages.387 @type manDir: L{twisted.python.filepath.FilePath}388 389 @raise NoDocumentsFound: When there are no .1 files in the given390 C{manDir}.391 """392 inputFiles = manDir.globChildren("*.1")393 filenames = [x.path for x in inputFiles]394 if not filenames:395 raise NoDocumentsFound("No manual pages found in %s" % (manDir,))396 arguments = ["--input", "man",397 "--output", "lore",398 "--config", "ext=-man.xhtml"] + filenames399 self.lore(arguments)400 401 402 403 269 class APIBuilder(object): 404 270 """ 405 271 Generate API documentation from source files using … … class NewsBuilder(object): 810 676 811 677 812 678 813 def filePathDelta(origin, destination):814 """815 Return a list of strings that represent C{destination} as a path relative816 to C{origin}.817 818 It is assumed that both paths represent directories, not files. That is to819 say, the delta of L{twisted.python.filepath.FilePath} /foo/bar to820 L{twisted.python.filepath.FilePath} /foo/baz will be C{../baz},821 not C{baz}.822 823 @type origin: L{twisted.python.filepath.FilePath}824 @param origin: The origin of the relative path.825 826 @type destination: L{twisted.python.filepath.FilePath}827 @param destination: The destination of the relative path.828 """829 commonItems = 0830 path1 = origin.path.split(os.sep)831 path2 = destination.path.split(os.sep)832 for elem1, elem2 in zip(path1, path2):833 if elem1 == elem2:834 commonItems += 1835 else:836 break837 path = [".."] * (len(path1) - commonItems)838 return path + path2[commonItems:]839 840 841 842 class DistributionBuilder(object):843 """844 A builder of Twisted distributions.845 846 This knows how to build tarballs for Twisted and all of its subprojects.847 848 @type blacklist: C{list} of C{str}849 @cvar blacklist: The list subproject names to exclude from the main Twisted850 tarball and for which no individual project tarballs will be built.851 """852 853 from twisted.python.dist import twisted_subprojects as subprojects854 blacklist = ["vfs", "web2"]855 856 def __init__(self, rootDirectory, outputDirectory, apiBaseURL=None):857 """858 Create a distribution builder.859 860 @param rootDirectory: root of a Twisted export which will populate861 subsequent tarballs.862 @type rootDirectory: L{FilePath}.863 864 @param outputDirectory: The directory in which to create the tarballs.865 @type outputDirectory: L{FilePath}866 867 @type apiBaseURL: C{str} or C{NoneType}868 @param apiBaseURL: A format string which will be interpolated with the869 fully-qualified Python name for each API link. For example, to870 generate the Twisted 8.0.0 documentation, pass871 C{"http://twistedmatrix.com/documents/8.0.0/api/%s.html"}.872 """873 self.rootDirectory = rootDirectory874 self.outputDirectory = outputDirectory875 self.apiBaseURL = apiBaseURL876 self.manBuilder = ManBuilder()877 self.docBuilder = DocBuilder()878 879 880 def _buildDocInDir(self, path, version, howtoPath):881 """882 Generate documentation in the given path, building man pages first if883 necessary and swallowing errors (so that directories without lore884 documentation in them are ignored).885 886 @param path: The path containing documentation to build.887 @type path: L{FilePath}888 @param version: The version of the project to include in all generated889 pages.890 @type version: C{str}891 @param howtoPath: The "resource path" as L{DocBuilder} describes it.892 @type howtoPath: L{FilePath}893 """894 templatePath = self.rootDirectory.child("doc").child("core"895 ).child("howto").child("template.tpl")896 if path.basename() == "man":897 self.manBuilder.build(path)898 if path.isdir():899 try:900 self.docBuilder.build(version, howtoPath, path,901 templatePath, self.apiBaseURL, True)902 except NoDocumentsFound:903 pass904 905 906 def buildTwisted(self, version):907 """908 Build the main Twisted distribution in C{Twisted-<version>.tar.bz2}.909 910 Projects listed in in L{blacklist} will not have their plugins, code,911 documentation, or bin directories included.912 913 bin/admin is also excluded.914 915 @type version: C{str}916 @param version: The version of Twisted to build.917 918 @return: The tarball file.919 @rtype: L{FilePath}.920 """921 releaseName = "Twisted-%s" % (version,)922 buildPath = lambda *args: '/'.join((releaseName,) + args)923 924 outputFile = self.outputDirectory.child(releaseName + ".tar.bz2")925 tarball = tarfile.TarFile.open(outputFile.path, 'w:bz2')926 927 docPath = self.rootDirectory.child("doc")928 929 # Generate docs!930 if docPath.isdir():931 for subProjectDir in docPath.children():932 if (subProjectDir.isdir()933 and subProjectDir.basename() not in self.blacklist):934 for child in subProjectDir.walk():935 self._buildDocInDir(child, version,936 subProjectDir.child("howto"))937 938 # Now, this part is nasty. We need to exclude blacklisted subprojects939 # from the main Twisted distribution. This means we need to exclude940 # their bin directories, their documentation directories, their941 # plugins, and their python packages. Given that there's no "add all942 # but exclude these particular paths" functionality in tarfile, we have943 # to walk through all these directories and add things that *aren't*944 # part of the blacklisted projects.945 946 for binthing in self.rootDirectory.child("bin").children():947 # bin/admin should also not be included.948 if binthing.basename() not in self.blacklist + ["admin"]:949 tarball.add(binthing.path,950 buildPath("bin", binthing.basename()))951 952 bad_plugins = ["twisted_%s.py" % (blacklisted,)953 for blacklisted in self.blacklist]954 955 for submodule in self.rootDirectory.child("twisted").children():956 if submodule.basename() == "plugins":957 for plugin in submodule.children():958 if plugin.basename() not in bad_plugins:959 tarball.add(plugin.path, buildPath("twisted", "plugins",960 plugin.basename()))961 elif submodule.basename() not in self.blacklist:962 tarball.add(submodule.path, buildPath("twisted",963 submodule.basename()))964 965 for docDir in self.rootDirectory.child("doc").children():966 if docDir.basename() not in self.blacklist:967 tarball.add(docDir.path, buildPath("doc", docDir.basename()))968 969 for toplevel in self.rootDirectory.children():970 if not toplevel.isdir():971 tarball.add(toplevel.path, buildPath(toplevel.basename()))972 973 tarball.close()974 975 return outputFile976 977 978 def buildCore(self, version):979 """980 Build a core distribution in C{TwistedCore-<version>.tar.bz2}.981 982 This is very similar to L{buildSubProject}, but core tarballs and the983 input are laid out slightly differently.984 985 - scripts are in the top level of the C{bin} directory.986 - code is included directly from the C{twisted} directory, excluding987 subprojects.988 - all plugins except the subproject plugins are included.989 990 @type version: C{str}991 @param version: The version of Twisted to build.992 993 @return: The tarball file.994 @rtype: L{FilePath}.995 """996 releaseName = "TwistedCore-%s" % (version,)997 outputFile = self.outputDirectory.child(releaseName + ".tar.bz2")998 buildPath = lambda *args: '/'.join((releaseName,) + args)999 tarball = self._createBasicSubprojectTarball(1000 "core", version, outputFile)1001 1002 # Include the bin directory for the subproject.1003 for path in self.rootDirectory.child("bin").children():1004 if not path.isdir():1005 tarball.add(path.path, buildPath("bin", path.basename()))1006 1007 # Include all files within twisted/ that aren't part of a subproject.1008 for path in self.rootDirectory.child("twisted").children():1009 if path.basename() == "plugins":1010 for plugin in path.children():1011 for subproject in self.subprojects:1012 if plugin.basename() == "twisted_%s.py" % (subproject,):1013 break1014 else:1015 tarball.add(plugin.path,1016 buildPath("twisted", "plugins",1017 plugin.basename()))1018 elif not path.basename() in self.subprojects + ["topfiles"]:1019 tarball.add(path.path, buildPath("twisted", path.basename()))1020 1021 tarball.add(self.rootDirectory.child("twisted").child("topfiles").path,1022 releaseName)1023 tarball.close()1024 1025 return outputFile1026 1027 1028 def buildSubProject(self, projectName, version):1029 """1030 Build a subproject distribution in1031 C{Twisted<Projectname>-<version>.tar.bz2}.1032 1033 @type projectName: C{str}1034 @param projectName: The lowercase name of the subproject to build.1035 @type version: C{str}1036 @param version: The version of Twisted to build.1037 1038 @return: The tarball file.1039 @rtype: L{FilePath}.1040 """1041 releaseName = "Twisted%s-%s" % (projectName.capitalize(), version)1042 outputFile = self.outputDirectory.child(releaseName + ".tar.bz2")1043 buildPath = lambda *args: '/'.join((releaseName,) + args)1044 subProjectDir = self.rootDirectory.child("twisted").child(projectName)1045 1046 tarball = self._createBasicSubprojectTarball(projectName, version,1047 outputFile)1048 1049 tarball.add(subProjectDir.child("topfiles").path, releaseName)1050 1051 # Include all files in the subproject package except for topfiles.1052 for child in subProjectDir.children():1053 name = child.basename()1054 if name != "topfiles":1055 tarball.add(1056 child.path,1057 buildPath("twisted", projectName, name))1058 1059 pluginsDir = self.rootDirectory.child("twisted").child("plugins")1060 # Include the plugin for the subproject.1061 pluginFileName = "twisted_%s.py" % (projectName,)1062 pluginFile = pluginsDir.child(pluginFileName)1063 if pluginFile.exists():1064 tarball.add(pluginFile.path,1065 buildPath("twisted", "plugins", pluginFileName))1066 1067 # Include the bin directory for the subproject.1068 binPath = self.rootDirectory.child("bin").child(projectName)1069 if binPath.isdir():1070 tarball.add(binPath.path, buildPath("bin"))1071 tarball.close()1072 1073 return outputFile1074 1075 1076 def _createBasicSubprojectTarball(self, projectName, version, outputFile):1077 """1078 Helper method to create and fill a tarball with things common between1079 subprojects and core.1080 1081 @param projectName: The subproject's name.1082 @type projectName: C{str}1083 @param version: The version of the release.1084 @type version: C{str}1085 @param outputFile: The location of the tar file to create.1086 @type outputFile: L{FilePath}1087 """1088 releaseName = "Twisted%s-%s" % (projectName.capitalize(), version)1089 buildPath = lambda *args: '/'.join((releaseName,) + args)1090 1091 tarball = tarfile.TarFile.open(outputFile.path, 'w:bz2')1092 1093 tarball.add(self.rootDirectory.child("LICENSE").path,1094 buildPath("LICENSE"))1095 1096 docPath = self.rootDirectory.child("doc").child(projectName)1097 1098 if docPath.isdir():1099 for child in docPath.walk():1100 self._buildDocInDir(child, version, docPath.child("howto"))1101 tarball.add(docPath.path, buildPath("doc"))1102 1103 return tarball1104 1105 1106 1107 679 class UncleanWorkingDirectory(Exception): 1108 680 """ 1109 681 Raised when the working directory of an SVN checkout is unclean. 1110 682 """ 1111 683 1112 684 685 1113 686 class NotWorkingDirectory(Exception): 1114 687 """ 1115 688 Raised when a directory does not appear to be an SVN working directory. 1116 689 """ 1117 690 1118 691 692 1119 693 def buildAllTarballs(checkout, destination): 1120 694 """ 1121 695 Build complete tarballs (including documentation) for Twisted and all … … def buildAllTarballs(checkout, destination): 1150 724 version = Project(twistedPath).getVersion() 1151 725 versionString = version.base() 1152 726 1153 apiBaseURL = "http://twistedmatrix.com/documents/%s/api/%%s.html" % (1154 versionString)1155 727 if not destination.exists(): 1156 728 destination.createDirectory() 1157 db = DistributionBuilder(export, destination, apiBaseURL=apiBaseURL) 729 db = DistributionBuilder(export, destination, 730 apiBaseURL=makeAPIBaseURL(versionString)) 1158 731 1159 732 db.buildCore(versionString) 1160 733 for subproject in twisted_subprojects: … … def buildAllTarballs(checkout, destination): 1166 739 workPath.remove() 1167 740 1168 741 742 1169 743 class ChangeVersionsScript(object): 1170 744 """ 1171 745 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..6d887e4 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 fcbe655..c6b1e96 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: … … else: 57 58 58 59 59 60 try: 61 from popen2 import Popen4 62 except ImportError: 63 popen4Skip = "popen2.Popen4 is not available." 64 else: 65 popen4Skip = skip 66 67 try: 60 68 import pydoctor.driver 61 69 # it might not be installed, or it might use syntax not available in 62 70 # this version of Python. … … def genVersion(*args, **kwargs): 92 100 93 101 94 102 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 103 class ChangeVersionTest(TestCase, StructureAssertingMixin): 176 104 """ 177 105 Twisted has the ability to change versions. … … class VersionWritingTest(TestCase): 480 408 481 409 482 410 483 class BuilderTestsMixin(object):484 """485 A mixin class which provides various methods for creating sample Lore input486 and output.487 488 @cvar template: The lore template that will be used to prepare sample489 output.490 @type template: C{str}491 492 @ivar docCounter: A counter which is incremented every time input is493 generated and which is included in the documents.494 @type docCounter: C{int}495 """496 template = '''497 <html>498 <head><title>Yo:</title></head>499 <body>500 <div class="body" />501 <a href="index.html">Index</a>502 <span class="version">Version: </span>503 </body>504 </html>505 '''506 507 def setUp(self):508 """509 Initialize the doc counter which ensures documents are unique.510 """511 self.docCounter = 0512 513 514 def assertXMLEqual(self, first, second):515 """516 Verify that two strings represent the same XML document.517 """518 self.assertEqual(519 dom.parseString(first).toxml(),520 dom.parseString(second).toxml())521 522 523 def getArbitraryOutput(self, version, counter, prefix="", apiBaseURL="%s"):524 """525 Get the correct HTML output for the arbitrary input returned by526 L{getArbitraryLoreInput} for the given parameters.527 528 @param version: The version string to include in the output.529 @type version: C{str}530 @param counter: A counter to include in the output.531 @type counter: C{int}532 """533 document = """\534 <?xml version="1.0"?><html>535 <head><title>Yo:Hi! Title: %(count)d</title></head>536 <body>537 <div class="content">Hi! %(count)d<div class="API"><a href="%(foobarLink)s" title="foobar">foobar</a></div></div>538 <a href="%(prefix)sindex.html">Index</a>539 <span class="version">Version: %(version)s</span>540 </body>541 </html>"""542 # Try to normalize irrelevant whitespace.543 return dom.parseString(544 document % {"count": counter, "prefix": prefix,545 "version": version,546 "foobarLink": apiBaseURL % ("foobar",)}).toxml('utf-8')547 548 549 def getArbitraryLoreInput(self, counter):550 """551 Get an arbitrary, unique (for this test case) string of lore input.552 553 @param counter: A counter to include in the input.554 @type counter: C{int}555 """556 template = (557 '<html>'558 '<head><title>Hi! Title: %(count)s</title></head>'559 '<body>'560 'Hi! %(count)s'561 '<div class="API">foobar</div>'562 '</body>'563 '</html>')564 return template % {"count": counter}565 566 567 def getArbitraryLoreInputAndOutput(self, version, prefix="",568 apiBaseURL="%s"):569 """570 Get an input document along with expected output for lore run on that571 output document, assuming an appropriately-specified C{self.template}.572 573 @param version: A version string to include in the input and output.574 @type version: C{str}575 @param prefix: The prefix to include in the link to the index.576 @type prefix: C{str}577 578 @return: A two-tuple of input and expected output.579 @rtype: C{(str, str)}.580 """581 self.docCounter += 1582 return (self.getArbitraryLoreInput(self.docCounter),583 self.getArbitraryOutput(version, self.docCounter,584 prefix=prefix, apiBaseURL=apiBaseURL))585 586 587 def getArbitraryManInput(self):588 """589 Get an arbitrary man page content.590 """591 return """.TH MANHOLE "1" "August 2001" "" ""592 .SH NAME593 manhole \- Connect to a Twisted Manhole service594 .SH SYNOPSIS595 .B manhole596 .SH DESCRIPTION597 manhole is a GTK interface to Twisted Manhole services. You can execute python598 code as if at an interactive Python console inside a running Twisted process599 with this."""600 601 602 def getArbitraryManLoreOutput(self):603 """604 Get an arbitrary lore input document which represents man-to-lore605 output based on the man page returned from L{getArbitraryManInput}606 """607 return """\608 <?xml version="1.0"?>609 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"610 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">611 <html><head>612 <title>MANHOLE.1</title></head>613 <body>614 615 <h1>MANHOLE.1</h1>616 617 <h2>NAME</h2>618 619 <p>manhole - Connect to a Twisted Manhole service620 </p>621 622 <h2>SYNOPSIS</h2>623 624 <p><strong>manhole</strong> </p>625 626 <h2>DESCRIPTION</h2>627 628 <p>manhole is a GTK interface to Twisted Manhole services. You can execute python629 code as if at an interactive Python console inside a running Twisted process630 with this.</p>631 632 </body>633 </html>634 """635 636 def getArbitraryManHTMLOutput(self, version, prefix=""):637 """638 Get an arbitrary lore output document which represents the lore HTML639 output based on the input document returned from640 L{getArbitraryManLoreOutput}.641 642 @param version: A version string to include in the document.643 @type version: C{str}644 @param prefix: The prefix to include in the link to the index.645 @type prefix: C{str}646 """647 # Try to normalize the XML a little bit.648 return dom.parseString("""\649 <?xml version="1.0" ?><html>650 <head><title>Yo:MANHOLE.1</title></head>651 <body>652 <div class="content">653 654 <span/>655 656 <h2>NAME<a name="auto0"/></h2>657 658 <p>manhole - Connect to a Twisted Manhole service659 </p>660 661 <h2>SYNOPSIS<a name="auto1"/></h2>662 663 <p><strong>manhole</strong> </p>664 665 <h2>DESCRIPTION<a name="auto2"/></h2>666 667 <p>manhole is a GTK interface to Twisted Manhole services. You can execute python668 code as if at an interactive Python console inside a running Twisted process669 with this.</p>670 671 </div>672 <a href="%(prefix)sindex.html">Index</a>673 <span class="version">Version: %(version)s</span>674 </body>675 </html>""" % {676 'prefix': prefix, 'version': version}).toxml("utf-8")677 678 679 680 class DocBuilderTestCase(TestCase, BuilderTestsMixin):681 """682 Tests for L{DocBuilder}.683 684 Note for future maintainers: The exact byte equality assertions throughout685 this suite may need to be updated due to minor differences in lore. They686 should not be taken to mean that Lore must maintain the same byte format687 forever. Feel free to update the tests when Lore changes, but please be688 careful.689 """690 skip = loreSkip691 692 def setUp(self):693 """694 Set up a few instance variables that will be useful.695 696 @ivar builder: A plain L{DocBuilder}.697 @ivar docCounter: An integer to be used as a counter by the698 C{getArbitrary...} methods.699 @ivar howtoDir: A L{FilePath} representing a directory to be used for700 containing Lore documents.701 @ivar templateFile: A L{FilePath} representing a file with702 C{self.template} as its content.703 """704 BuilderTestsMixin.setUp(self)705 self.builder = DocBuilder()706 self.howtoDir = FilePath(self.mktemp())707 self.howtoDir.createDirectory()708 self.templateFile = self.howtoDir.child("template.tpl")709 self.templateFile.setContent(self.template)710 711 712 def test_build(self):713 """714 The L{DocBuilder} runs lore on all .xhtml files within a directory.715 """716 version = "1.2.3"717 input1, output1 = self.getArbitraryLoreInputAndOutput(version)718 input2, output2 = self.getArbitraryLoreInputAndOutput(version)719 720 self.howtoDir.child("one.xhtml").setContent(input1)721 self.howtoDir.child("two.xhtml").setContent(input2)722 723 self.builder.build(version, self.howtoDir, self.howtoDir,724 self.templateFile)725 out1 = self.howtoDir.child('one.html')726 out2 = self.howtoDir.child('two.html')727 self.assertXMLEqual(out1.getContent(), output1)728 self.assertXMLEqual(out2.getContent(), output2)729 730 731 def test_noDocumentsFound(self):732 """733 The C{build} method raises L{NoDocumentsFound} if there are no734 .xhtml files in the given directory.735 """736 self.assertRaises(737 NoDocumentsFound,738 self.builder.build, "1.2.3", self.howtoDir, self.howtoDir,739 self.templateFile)740 741 742 def test_parentDocumentLinking(self):743 """744 The L{DocBuilder} generates correct links from documents to745 template-generated links like stylesheets and index backreferences.746 """747 input = self.getArbitraryLoreInput(0)748 tutoDir = self.howtoDir.child("tutorial")749 tutoDir.createDirectory()750 tutoDir.child("child.xhtml").setContent(input)751 self.builder.build("1.2.3", self.howtoDir, tutoDir, self.templateFile)752 outFile = tutoDir.child('child.html')753 self.assertIn('<a href="../index.html">Index</a>',754 outFile.getContent())755 756 757 def test_siblingDirectoryDocumentLinking(self):758 """759 It is necessary to generate documentation in a directory foo/bar where760 stylesheet and indexes are located in foo/baz. Such resources should be761 appropriately linked to.762 """763 input = self.getArbitraryLoreInput(0)764 resourceDir = self.howtoDir.child("resources")765 docDir = self.howtoDir.child("docs")766 docDir.createDirectory()767 docDir.child("child.xhtml").setContent(input)768 self.builder.build("1.2.3", resourceDir, docDir, self.templateFile)769 outFile = docDir.child('child.html')770 self.assertIn('<a href="../resources/index.html">Index</a>',771 outFile.getContent())772 773 774 def test_apiLinking(self):775 """776 The L{DocBuilder} generates correct links from documents to API777 documentation.778 """779 version = "1.2.3"780 input, output = self.getArbitraryLoreInputAndOutput(version)781 self.howtoDir.child("one.xhtml").setContent(input)782 783 self.builder.build(version, self.howtoDir, self.howtoDir,784 self.templateFile, "scheme:apilinks/%s.ext")785 out = self.howtoDir.child('one.html')786 self.assertIn(787 '<a href="scheme:apilinks/foobar.ext" title="foobar">foobar</a>',788 out.getContent())789 790 791 def test_deleteInput(self):792 """793 L{DocBuilder.build} can be instructed to delete the input files after794 generating the output based on them.795 """796 input1 = self.getArbitraryLoreInput(0)797 self.howtoDir.child("one.xhtml").setContent(input1)798 self.builder.build("whatever", self.howtoDir, self.howtoDir,799 self.templateFile, deleteInput=True)800 self.assertTrue(self.howtoDir.child('one.html').exists())801 self.assertFalse(self.howtoDir.child('one.xhtml').exists())802 803 804 def test_doNotDeleteInput(self):805 """806 Input will not be deleted by default.807 """808 input1 = self.getArbitraryLoreInput(0)809 self.howtoDir.child("one.xhtml").setContent(input1)810 self.builder.build("whatever", self.howtoDir, self.howtoDir,811 self.templateFile)812 self.assertTrue(self.howtoDir.child('one.html').exists())813 self.assertTrue(self.howtoDir.child('one.xhtml').exists())814 815 816 def test_getLinkrelToSameDirectory(self):817 """818 If the doc and resource directories are the same, the linkrel should be819 an empty string.820 """821 linkrel = self.builder.getLinkrel(FilePath("/foo/bar"),822 FilePath("/foo/bar"))823 self.assertEquals(linkrel, "")824 825 826 def test_getLinkrelToParentDirectory(self):827 """828 If the doc directory is a child of the resource directory, the linkrel829 should make use of '..'.830 """831 linkrel = self.builder.getLinkrel(FilePath("/foo"),832 FilePath("/foo/bar"))833 self.assertEquals(linkrel, "../")834 835 836 def test_getLinkrelToSibling(self):837 """838 If the doc directory is a sibling of the resource directory, the839 linkrel should make use of '..' and a named segment.840 """841 linkrel = self.builder.getLinkrel(FilePath("/foo/howto"),842 FilePath("/foo/examples"))843 self.assertEquals(linkrel, "../howto/")844 845 846 def test_getLinkrelToUncle(self):847 """848 If the doc directory is a sibling of the parent of the resource849 directory, the linkrel should make use of multiple '..'s and a named850 segment.851 """852 linkrel = self.builder.getLinkrel(FilePath("/foo/howto"),853 FilePath("/foo/examples/quotes"))854 self.assertEquals(linkrel, "../../howto/")855 856 857 858 411 class APIBuilderTestCase(TestCase): 859 412 """ 860 413 Tests for L{APIBuilder}. … … class APIBuilderTestCase(TestCase): 994 547 995 548 996 549 997 class ManBuilderTestCase(TestCase, BuilderTestsMixin):998 """999 Tests for L{ManBuilder}.1000 """1001 skip = loreSkip1002 1003 def setUp(self):1004 """1005 Set up a few instance variables that will be useful.1006 1007 @ivar builder: A plain L{ManBuilder}.1008 @ivar manDir: A L{FilePath} representing a directory to be used for1009 containing man pages.1010 """1011 BuilderTestsMixin.setUp(self)1012 self.builder = ManBuilder()1013 self.manDir = FilePath(self.mktemp())1014 self.manDir.createDirectory()1015 1016 1017 def test_noDocumentsFound(self):1018 """1019 L{ManBuilder.build} raises L{NoDocumentsFound} if there are no1020 .1 files in the given directory.1021 """1022 self.assertRaises(NoDocumentsFound, self.builder.build, self.manDir)1023 1024 1025 def test_build(self):1026 """1027 Check that L{ManBuilder.build} find the man page in the directory, and1028 successfully produce a Lore content.1029 """1030 manContent = self.getArbitraryManInput()1031 self.manDir.child('test1.1').setContent(manContent)1032 self.builder.build(self.manDir)1033 output = self.manDir.child('test1-man.xhtml').getContent()1034 expected = self.getArbitraryManLoreOutput()1035 # No-op on *nix, fix for windows1036 expected = expected.replace('\n', os.linesep)1037 self.assertEquals(output, expected)1038 1039 1040 def test_toHTML(self):1041 """1042 Check that the content output by C{build} is compatible as input of1043 L{DocBuilder.build}.1044 """1045 manContent = self.getArbitraryManInput()1046 self.manDir.child('test1.1').setContent(manContent)1047 self.builder.build(self.manDir)1048 1049 templateFile = self.manDir.child("template.tpl")1050 templateFile.setContent(DocBuilderTestCase.template)1051 docBuilder = DocBuilder()1052 docBuilder.build("1.2.3", self.manDir, self.manDir,1053 templateFile)1054 output = self.manDir.child('test1-man.html').getContent()1055 1056 self.assertXMLEqual(1057 output,1058 """\1059 <?xml version="1.0" ?><html>1060 <head><title>Yo:MANHOLE.1</title></head>1061 <body>1062 <div class="content">1063 1064 <span/>1065 1066 <h2>NAME<a name="auto0"/></h2>1067 1068 <p>manhole - Connect to a Twisted Manhole service1069 </p>1070 1071 <h2>SYNOPSIS<a name="auto1"/></h2>1072 1073 <p><strong>manhole</strong> </p>1074 1075 <h2>DESCRIPTION<a name="auto2"/></h2>1076 1077 <p>manhole is a GTK interface to Twisted Manhole services. You can execute python1078 code as if at an interactive Python console inside a running Twisted process1079 with this.</p>1080 1081 </div>1082 <a href="index.html">Index</a>1083 <span class="version">Version: 1.2.3</span>1084 </body>1085 </html>""")1086 1087 1088 1089 550 class BookBuilderTests(TestCase, BuilderTestsMixin): 1090 551 """ 1091 552 Tests for L{BookBuilder}. … … class BookBuilderTests(TestCase, BuilderTestsMixin): 1418 879 1419 880 1420 881 1421 class FilePathDeltaTest(TestCase):1422 """1423 Tests for L{filePathDelta}.1424 """1425 1426 def test_filePathDeltaSubdir(self):1427 """1428 L{filePathDelta} can create a simple relative path to a child path.1429 """1430 self.assertEquals(filePathDelta(FilePath("/foo/bar"),1431 FilePath("/foo/bar/baz")),1432 ["baz"])1433 1434 1435 def test_filePathDeltaSiblingDir(self):1436 """1437 L{filePathDelta} can traverse upwards to create relative paths to1438 siblings.1439 """1440 self.assertEquals(filePathDelta(FilePath("/foo/bar"),1441 FilePath("/foo/baz")),1442 ["..", "baz"])1443 1444 1445 def test_filePathNoCommonElements(self):1446 """1447 L{filePathDelta} can create relative paths to totally unrelated paths1448 for maximum portability.1449 """1450 self.assertEquals(filePathDelta(FilePath("/foo/bar"),1451 FilePath("/baz/quux")),1452 ["..", "..", "baz", "quux"])1453 1454 1455 def test_filePathDeltaSimilarEndElements(self):1456 """1457 L{filePathDelta} doesn't take into account final elements when1458 comparing 2 paths, but stops at the first difference.1459 """1460 self.assertEquals(filePathDelta(FilePath("/foo/bar/bar/spam"),1461 FilePath("/foo/bar/baz/spam")),1462 ["..", "..", "baz", "spam"])1463 1464 1465 1466 882 class NewsBuilderTests(TestCase, StructureAssertingMixin): 1467 883 """ 1468 884 Tests for L{NewsBuilder}. … … class NewsBuilderTests(TestCase, StructureAssertingMixin): 1869 1285 1870 1286 1871 1287 1872 class DistributionBuilderTestBase(BuilderTestsMixin, StructureAssertingMixin,1873 TestCase):1874 """1875 Base for tests of L{DistributionBuilder}.1876 """1877 skip = loreSkip1878 1879 def setUp(self):1880 BuilderTestsMixin.setUp(self)1881 1882 self.rootDir = FilePath(self.mktemp())1883 self.rootDir.createDirectory()1884 1885 self.outputDir = FilePath(self.mktemp())1886 self.outputDir.createDirectory()1887 self.builder = DistributionBuilder(self.rootDir, self.outputDir)1888 1889 1890 1891 class DistributionBuilderTest(DistributionBuilderTestBase):1892 1893 def test_twistedDistribution(self):1894 """1895 The Twisted tarball contains everything in the source checkout, with1896 built documentation.1897 """1898 loreInput, loreOutput = self.getArbitraryLoreInputAndOutput("10.0.0")1899 manInput1 = self.getArbitraryManInput()1900 manOutput1 = self.getArbitraryManHTMLOutput("10.0.0", "../howto/")1901 manInput2 = self.getArbitraryManInput()1902 manOutput2 = self.getArbitraryManHTMLOutput("10.0.0", "../howto/")1903 coreIndexInput, coreIndexOutput = self.getArbitraryLoreInputAndOutput(1904 "10.0.0", prefix="howto/")1905 1906 structure = {1907 "README": "Twisted",1908 "unrelated": "x",1909 "LICENSE": "copyright!",1910 "setup.py": "import toplevel",1911 "bin": {"web": {"websetroot": "SET ROOT"},1912 "twistd": "TWISTD"},1913 "twisted":1914 {"web":1915 {"__init__.py": "import WEB",1916 "topfiles": {"setup.py": "import WEBINSTALL",1917 "README": "WEB!"}},1918 "words": {"__init__.py": "import WORDS"},1919 "plugins": {"twisted_web.py": "import WEBPLUG",1920 "twisted_words.py": "import WORDPLUG"}},1921 "doc": {"web": {"howto": {"index.xhtml": loreInput},1922 "man": {"websetroot.1": manInput2}},1923 "core": {"howto": {"template.tpl": self.template},1924 "man": {"twistd.1": manInput1},1925 "index.xhtml": coreIndexInput}}}1926 1927 outStructure = {1928 "README": "Twisted",1929 "unrelated": "x",1930 "LICENSE": "copyright!",1931 "setup.py": "import toplevel",1932 "bin": {"web": {"websetroot": "SET ROOT"},1933 "twistd": "TWISTD"},1934 "twisted":1935 {"web": {"__init__.py": "import WEB",1936 "topfiles": {"setup.py": "import WEBINSTALL",1937 "README": "WEB!"}},1938 "words": {"__init__.py": "import WORDS"},1939 "plugins": {"twisted_web.py": "import WEBPLUG",1940 "twisted_words.py": "import WORDPLUG"}},1941 "doc": {"web": {"howto": {"index.html": loreOutput},1942 "man": {"websetroot.1": manInput2,1943 "websetroot-man.html": manOutput2}},1944 "core": {"howto": {"template.tpl": self.template},1945 "man": {"twistd.1": manInput1,1946 "twistd-man.html": manOutput1},1947 "index.html": coreIndexOutput}}}1948 1949 self.createStructure(self.rootDir, structure)1950 1951 outputFile = self.builder.buildTwisted("10.0.0")1952 1953 self.assertExtractedStructure(outputFile, outStructure)1954 1955 1956 def test_twistedDistributionExcludesWeb2AndVFSAndAdmin(self):1957 """1958 The main Twisted distribution does not include web2 or vfs, or the1959 bin/admin directory.1960 """1961 loreInput, loreOutput = self.getArbitraryLoreInputAndOutput("10.0.0")1962 coreIndexInput, coreIndexOutput = self.getArbitraryLoreInputAndOutput(1963 "10.0.0", prefix="howto/")1964 1965 structure = {1966 "README": "Twisted",1967 "unrelated": "x",1968 "LICENSE": "copyright!",1969 "setup.py": "import toplevel",1970 "bin": {"web2": {"websetroot": "SET ROOT"},1971 "vfs": {"vfsitup": "hee hee"},1972 "twistd": "TWISTD",1973 "admin": {"build-a-thing": "yay"}},1974 "twisted":1975 {"web2":1976 {"__init__.py": "import WEB",1977 "topfiles": {"setup.py": "import WEBINSTALL",1978 "README": "WEB!"}},1979 "vfs":1980 {"__init__.py": "import VFS",1981 "blah blah": "blah blah"},1982 "words": {"__init__.py": "import WORDS"},1983 "plugins": {"twisted_web.py": "import WEBPLUG",1984 "twisted_words.py": "import WORDPLUG",1985 "twisted_web2.py": "import WEB2",1986 "twisted_vfs.py": "import VFS"}},1987 "doc": {"web2": {"excluded!": "yay"},1988 "vfs": {"unrelated": "whatever"},1989 "core": {"howto": {"template.tpl": self.template},1990 "index.xhtml": coreIndexInput}}}1991 1992 outStructure = {1993 "README": "Twisted",1994 "unrelated": "x",1995 "LICENSE": "copyright!",1996 "setup.py": "import toplevel",1997 "bin": {"twistd": "TWISTD"},1998 "twisted":1999 {"words": {"__init__.py": "import WORDS"},2000 "plugins": {"twisted_web.py": "import WEBPLUG",2001 "twisted_words.py": "import WORDPLUG"}},2002 "doc": {"core": {"howto": {"template.tpl": self.template},2003 "index.html": coreIndexOutput}}}2004 self.createStructure(self.rootDir, structure)2005 2006 outputFile = self.builder.buildTwisted("10.0.0")2007 2008 self.assertExtractedStructure(outputFile, outStructure)2009 2010 2011 def test_subProjectLayout(self):2012 """2013 The subproject tarball includes files like so:2014 2015 1. twisted/<subproject>/topfiles defines the files that will be in the2016 top level in the tarball, except LICENSE, which comes from the real2017 top-level directory.2018 2. twisted/<subproject> is included, but without the topfiles entry2019 in that directory. No other twisted subpackages are included.2020 3. twisted/plugins/twisted_<subproject>.py is included, but nothing2021 else in plugins is.2022 """2023 structure = {2024 "README": "HI!@",2025 "unrelated": "x",2026 "LICENSE": "copyright!",2027 "setup.py": "import toplevel",2028 "bin": {"web": {"websetroot": "SET ROOT"},2029 "words": {"im": "#!im"}},2030 "twisted":2031 {"web":2032 {"__init__.py": "import WEB",2033 "topfiles": {"setup.py": "import WEBINSTALL",2034 "README": "WEB!"}},2035 "words": {"__init__.py": "import WORDS"},2036 "plugins": {"twisted_web.py": "import WEBPLUG",2037 "twisted_words.py": "import WORDPLUG"}}}2038 2039 outStructure = {2040 "README": "WEB!",2041 "LICENSE": "copyright!",2042 "setup.py": "import WEBINSTALL",2043 "bin": {"websetroot": "SET ROOT"},2044 "twisted": {"web": {"__init__.py": "import WEB"},2045 "plugins": {"twisted_web.py": "import WEBPLUG"}}}2046 2047 self.createStructure(self.rootDir, structure)2048 2049 outputFile = self.builder.buildSubProject("web", "0.3.0")2050 2051 self.assertExtractedStructure(outputFile, outStructure)2052 2053 2054 def test_minimalSubProjectLayout(self):2055 """2056 buildSubProject should work with minimal subprojects.2057 """2058 structure = {2059 "LICENSE": "copyright!",2060 "bin": {},2061 "twisted":2062 {"web": {"__init__.py": "import WEB",2063 "topfiles": {"setup.py": "import WEBINSTALL"}},2064 "plugins": {}}}2065 2066 outStructure = {2067 "setup.py": "import WEBINSTALL",2068 "LICENSE": "copyright!",2069 "twisted": {"web": {"__init__.py": "import WEB"}}}2070 2071 self.createStructure(self.rootDir, structure)2072 2073 outputFile = self.builder.buildSubProject("web", "0.3.0")2074 2075 self.assertExtractedStructure(outputFile, outStructure)2076 2077 2078 def test_subProjectDocBuilding(self):2079 """2080 When building a subproject release, documentation should be built with2081 lore.2082 """2083 loreInput, loreOutput = self.getArbitraryLoreInputAndOutput("0.3.0")2084 manInput = self.getArbitraryManInput()2085 manOutput = self.getArbitraryManHTMLOutput("0.3.0", "../howto/")2086 structure = {2087 "LICENSE": "copyright!",2088 "twisted": {"web": {"__init__.py": "import WEB",2089 "topfiles": {"setup.py": "import WEBINST"}}},2090 "doc": {"web": {"howto": {"index.xhtml": loreInput},2091 "man": {"twistd.1": manInput}},2092 "core": {"howto": {"template.tpl": self.template}}2093 }2094 }2095 2096 outStructure = {2097 "LICENSE": "copyright!",2098 "setup.py": "import WEBINST",2099 "twisted": {"web": {"__init__.py": "import WEB"}},2100 "doc": {"howto": {"index.html": loreOutput},2101 "man": {"twistd.1": manInput,2102 "twistd-man.html": manOutput}}}2103 2104 self.createStructure(self.rootDir, structure)2105 2106 outputFile = self.builder.buildSubProject("web", "0.3.0")2107 2108 self.assertExtractedStructure(outputFile, outStructure)2109 2110 2111 def test_coreProjectLayout(self):2112 """2113 The core tarball looks a lot like a subproject tarball, except it2114 doesn't include:2115 2116 - Python packages from other subprojects2117 - plugins from other subprojects2118 - scripts from other subprojects2119 """2120 indexInput, indexOutput = self.getArbitraryLoreInputAndOutput(2121 "8.0.0", prefix="howto/")2122 howtoInput, howtoOutput = self.getArbitraryLoreInputAndOutput("8.0.0")2123 specInput, specOutput = self.getArbitraryLoreInputAndOutput(2124 "8.0.0", prefix="../howto/")2125 upgradeInput, upgradeOutput = self.getArbitraryLoreInputAndOutput(2126 "8.0.0", prefix="../howto/")2127 tutorialInput, tutorialOutput = self.getArbitraryLoreInputAndOutput(2128 "8.0.0", prefix="../")2129 2130 structure = {2131 "LICENSE": "copyright!",2132 "twisted": {"__init__.py": "twisted",2133 "python": {"__init__.py": "python",2134 "roots.py": "roots!"},2135 "conch": {"__init__.py": "conch",2136 "unrelated.py": "import conch"},2137 "plugin.py": "plugin",2138 "plugins": {"twisted_web.py": "webplug",2139 "twisted_whatever.py": "include!",2140 "cred.py": "include!"},2141 "topfiles": {"setup.py": "import CORE",2142 "README": "core readme"}},2143 "doc": {"core": {"howto": {"template.tpl": self.template,2144 "index.xhtml": howtoInput,2145 "tutorial":2146 {"index.xhtml": tutorialInput}},2147 "specifications": {"index.xhtml": specInput},2148 "upgrades": {"index.xhtml": upgradeInput},2149 "examples": {"foo.py": "foo.py"},2150 "index.xhtml": indexInput},2151 "web": {"howto": {"index.xhtml": "webindex"}}},2152 "bin": {"twistd": "TWISTD",2153 "web": {"websetroot": "websetroot"}}2154 }2155 2156 outStructure = {2157 "LICENSE": "copyright!",2158 "setup.py": "import CORE",2159 "README": "core readme",2160 "twisted": {"__init__.py": "twisted",2161 "python": {"__init__.py": "python",2162 "roots.py": "roots!"},2163 "plugin.py": "plugin",2164 "plugins": {"twisted_whatever.py": "include!",2165 "cred.py": "include!"}},2166 "doc": {"howto": {"template.tpl": self.template,2167 "index.html": howtoOutput,2168 "tutorial": {"index.html": tutorialOutput}},2169 "specifications": {"index.html": specOutput},2170 "upgrades": {"index.html": upgradeOutput},2171 "examples": {"foo.py": "foo.py"},2172 "index.html": indexOutput},2173 "bin": {"twistd": "TWISTD"},2174 }2175 2176 self.createStructure(self.rootDir, structure)2177 outputFile = self.builder.buildCore("8.0.0")2178 self.assertExtractedStructure(outputFile, outStructure)2179 2180 2181 def test_apiBaseURL(self):2182 """2183 DistributionBuilder builds documentation with the specified2184 API base URL.2185 """2186 apiBaseURL = "http://%s"2187 builder = DistributionBuilder(self.rootDir, self.outputDir,2188 apiBaseURL=apiBaseURL)2189 loreInput, loreOutput = self.getArbitraryLoreInputAndOutput(2190 "0.3.0", apiBaseURL=apiBaseURL)2191 structure = {2192 "LICENSE": "copyright!",2193 "twisted": {"web": {"__init__.py": "import WEB",2194 "topfiles": {"setup.py": "import WEBINST"}}},2195 "doc": {"web": {"howto": {"index.xhtml": loreInput}},2196 "core": {"howto": {"template.tpl": self.template}}2197 }2198 }2199 2200 outStructure = {2201 "LICENSE": "copyright!",2202 "setup.py": "import WEBINST",2203 "twisted": {"web": {"__init__.py": "import WEB"}},2204 "doc": {"howto": {"index.html": loreOutput}}}2205 2206 self.createStructure(self.rootDir, structure)2207 outputFile = builder.buildSubProject("web", "0.3.0")2208 self.assertExtractedStructure(outputFile, outStructure)2209 2210 2211 2212 1288 class BuildAllTarballsTest(DistributionBuilderTestBase): 2213 1289 """ 2214 1290 Tests for L{DistributionBuilder.buildAllTarballs}. … … class ScriptTests(BuilderTestsMixin, StructureAssertingMixin, TestCase): 2524 1600 newsBuilder.buildAll = builds.append 2525 1601 newsBuilder.main(["/foo/bar/baz"]) 2526 1602 self.assertEquals(builds, [FilePath("/foo/bar/baz")]) 1603 1604 1605 -
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