Ticket #4138: sdist-support-4138.4.patch

File sdist-support-4138.4.patch, 141.0 KB (added by TimAllen, 12 years ago)

New patch against current Twisted trunk.

  • setup.py

    diff --git a/setup.py b/setup.py
    index c8b5877..529fda5 100755
    a b def getExtensions(): 
    3131                execfile(setup_py, ns, ns)
    3232                if "extensions" in ns:
    3333                    extensions.extend(ns["extensions"])
    34                    
     34
    3535    return extensions
    3636
    3737
    def main(args): 
    4343    if os.path.exists('twisted'):
    4444        sys.path.insert(0, '.')
    4545    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
    4748
    4849    # "" is included because core scripts are directly in bin/
    4950    projects = [''] + [x for x in os.listdir('bin')
    on event-based network programming and multiprotocol integration. 
    7273            packages = getPackages('twisted'),
    7374            conditionalExtensions = getExtensions(),
    7475            scripts = scripts,
    75             data_files=getDataFiles('twisted'),
     76            data_files=getDataFiles('twisted'),
     77            cmdclass = {'sdist': _SDistTwisted},
    7678            )
    7779
    7880    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"""
     5Code to support distutils making a distribution of a release.
     6
     7Much of this code used to live in L{twisted.python._release}, but there is
     8a distinction between a "distribution" and a "release". Only Twisted devs can
     9make a release (using the code in C{t.p._release} to build API documentation,
     10change version numbers, package tarballs and so forth), but anybody anywhere
     11can use distutils to make a distribution of the files of a particular release.
     12
     13Because Twisted's release code is only designed to work in a POSIX environment,
     14it's not appropriate for the generic distutils code to depend on it; therefore
     15this module contains code for bundling the files of a release into
     16a distribution, and both C{setup.py} and C{t.p._release} depend on it.
     17"""
     18
     19import os, fnmatch, tarfile, errno, shutil
     20from twisted.lore.scripts import lore
     21
     22
     23twisted_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.
     31EXCLUDE_PATTERNS = ["{arch}", "_darcs", "*.py[cdo]", "*.s[ol]", ".*", "*~"]
     32
     33
     34
     35def 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
     57class NoDocumentsFound(Exception):
     58    """
     59    Raised when no input documents are found.
     60    """
     61
     62
     63
     64class 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
     81class 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
     162class 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
     191def _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
     257class 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
     537def 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
     553def filePathDelta(origin, destination):
     554    """
     555    Return a list of strings that represent C{destination} as a path relative
     556    to C{origin}.
     557
     558    It is assumed that both paths represent directories, not files. That is to
     559    say, the delta of L{twisted.python.filepath.FilePath} /foo/bar to
     560    L{twisted.python.filepath.FilePath} /foo/baz will be C{../baz},
     561    not C{baz}.
     562
     563    @type origin: L{twisted.python.filepath.FilePath}
     564    @param origin: The origin of the relative path.
     565
     566    @type destination: L{twisted.python.filepath.FilePath}
     567    @param destination: The destination of the relative path.
     568    """
     569    commonItems = 0
     570    path1 = origin.path.split(os.sep)
     571    path2 = destination.path.split(os.sep)
     572    for elem1, elem2 in zip(path1, path2):
     573        if elem1 == elem2:
     574            commonItems += 1
     575        else:
     576            break
     577    path = [".."] * (len(path1) - commonItems)
     578    return path + path2[commonItems:]
     579
     580
     581
  • twisted/python/_release.py

    diff --git a/twisted/python/_release.py b/twisted/python/_release.py
    index 10dc03c..fbaf835 100644
    a b import re 
    1818import sys
    1919import os
    2020from tempfile import mkdtemp
    21 import tarfile
    2221
    2322from subprocess import PIPE, STDOUT, Popen
    2423
    2524from twisted.python.versions import Version
    2625from twisted.python.filepath import FilePath
    27 from twisted.python.dist import twisted_subprojects
    28 
    29 # This import is an example of why you shouldn't use this module unless you're
    30 # radix
    31 try:
    32     from twisted.lore.scripts import lore
    33 except ImportError:
    34     pass
     26from twisted.python._dist import LoreBuilderMixin, DistributionBuilder
     27from twisted.python._dist import makeAPIBaseURL, twisted_subprojects
    3528
    3629# The offset between a year and the corresponding major version number.
    3730VERSION_OFFSET = 2000
    def changeAllProjectVersions(root, versionTemplate, today=None): 
    164157
    165158
    166159
    167 
    168160class Project(object):
    169161    """
    170162    A representation of a project that has a version.
    def updateTwistedVersionInformation(baseDirectory, now): 
    238230        project.updateVersion(getNextVersion(project.getVersion(), now=now))
    239231
    240232
     233
    241234def generateVersionFileData(version):
    242235    """
    243236    Generate the data to be placed into a _version.py file.
    version = versions.Version(%r, %s, %s, %s%s) 
    256249    return data
    257250
    258251
     252
    259253def replaceProjectVersion(filename, newversion):
    260254    """
    261255    Write version specification code into the given filename, which
    def replaceInFile(filename, oldToNew): 
    291285
    292286
    293287
    294 class NoDocumentsFound(Exception):
    295     """
    296     Raised when no input documents are found.
    297     """
    298 
    299 
    300 
    301 class LoreBuilderMixin(object):
    302     """
    303     Base class for builders which invoke lore.
    304     """
    305     def lore(self, arguments):
    306         """
    307         Run lore with the given arguments.
    308 
    309         @param arguments: A C{list} of C{str} giving command line arguments to
    310             lore which should be used.
    311         """
    312         options = lore.Options()
    313         options.parseOptions(["--null"] + arguments)
    314         lore.runGivenOptions(options)
    315 
    316 
    317 
    318 class DocBuilder(LoreBuilderMixin):
    319     """
    320     Generate HTML documentation for projects.
    321     """
    322 
    323     def build(self, version, resourceDir, docDir, template, apiBaseURL=None,
    324               deleteInput=False):
    325         """
    326         Build the documentation in C{docDir} with Lore.
    327 
    328         Input files ending in .xhtml will be considered. Output will written as
    329         .html files.
    330 
    331         @param version: the version of the documentation to pass to lore.
    332         @type version: C{str}
    333 
    334         @param resourceDir: The directory which contains the toplevel index and
    335             stylesheet file for this section of documentation.
    336         @type resourceDir: L{twisted.python.filepath.FilePath}
    337 
    338         @param docDir: The directory of the documentation.
    339         @type docDir: L{twisted.python.filepath.FilePath}
    340 
    341         @param template: The template used to generate the documentation.
    342         @type template: L{twisted.python.filepath.FilePath}
    343 
    344         @type apiBaseURL: C{str} or C{NoneType}
    345         @param apiBaseURL: A format string which will be interpolated with the
    346             fully-qualified Python name for each API link.  For example, to
    347             generate the Twisted 8.0.0 documentation, pass
    348             C{"http://twistedmatrix.com/documents/8.0.0/api/%s.html"}.
    349 
    350         @param deleteInput: If True, the input documents will be deleted after
    351             their output is generated.
    352         @type deleteInput: C{bool}
    353 
    354         @raise NoDocumentsFound: When there are no .xhtml files in the given
    355             C{docDir}.
    356         """
    357         linkrel = self.getLinkrel(resourceDir, docDir)
    358         inputFiles = docDir.globChildren("*.xhtml")
    359         filenames = [x.path for x in inputFiles]
    360         if not filenames:
    361             raise NoDocumentsFound("No input documents found in %s" % (docDir,))
    362         if apiBaseURL is not None:
    363             arguments = ["--config", "baseurl=" + apiBaseURL]
    364         else:
    365             arguments = []
    366         arguments.extend(["--config", "template=%s" % (template.path,),
    367                           "--config", "ext=.html",
    368                           "--config", "version=%s" % (version,),
    369                           "--linkrel", linkrel] + filenames)
    370         self.lore(arguments)
    371         if deleteInput:
    372             for inputFile in inputFiles:
    373                 inputFile.remove()
    374 
    375 
    376     def getLinkrel(self, resourceDir, docDir):
    377         """
    378         Calculate a value appropriate for Lore's --linkrel option.
    379 
    380         Lore's --linkrel option defines how to 'find' documents that are
    381         linked to from TEMPLATE files (NOT document bodies). That is, it's a
    382         prefix for links ('a' and 'link') in the template.
    383 
    384         @param resourceDir: The directory which contains the toplevel index and
    385             stylesheet file for this section of documentation.
    386         @type resourceDir: L{twisted.python.filepath.FilePath}
    387 
    388         @param docDir: The directory containing documents that must link to
    389             C{resourceDir}.
    390         @type docDir: L{twisted.python.filepath.FilePath}
    391         """
    392         if resourceDir != docDir:
    393             return '/'.join(filePathDelta(docDir, resourceDir)) + "/"
    394         else:
    395             return ""
    396 
    397 
    398 
    399 class ManBuilder(LoreBuilderMixin):
    400     """
    401     Generate man pages of the different existing scripts.
    402     """
    403 
    404     def build(self, manDir):
    405         """
    406         Generate Lore input files from the man pages in C{manDir}.
    407 
    408         Input files ending in .1 will be considered. Output will written as
    409         -man.xhtml files.
    410 
    411         @param manDir: The directory of the man pages.
    412         @type manDir: L{twisted.python.filepath.FilePath}
    413 
    414         @raise NoDocumentsFound: When there are no .1 files in the given
    415             C{manDir}.
    416         """
    417         inputFiles = manDir.globChildren("*.1")
    418         filenames = [x.path for x in inputFiles]
    419         if not filenames:
    420             raise NoDocumentsFound("No manual pages found in %s" % (manDir,))
    421         arguments = ["--input", "man",
    422                      "--output", "lore",
    423                      "--config", "ext=-man.xhtml"] + filenames
    424         self.lore(arguments)
    425 
    426 
    427 
    428288class APIBuilder(object):
    429289    """
    430290    Generate API documentation from source files using
    class NewsBuilder(object): 
    924784
    925785
    926786
    927 def filePathDelta(origin, destination):
    928     """
    929     Return a list of strings that represent C{destination} as a path relative
    930     to C{origin}.
    931 
    932     It is assumed that both paths represent directories, not files. That is to
    933     say, the delta of L{twisted.python.filepath.FilePath} /foo/bar to
    934     L{twisted.python.filepath.FilePath} /foo/baz will be C{../baz},
    935     not C{baz}.
    936 
    937     @type origin: L{twisted.python.filepath.FilePath}
    938     @param origin: The origin of the relative path.
    939 
    940     @type destination: L{twisted.python.filepath.FilePath}
    941     @param destination: The destination of the relative path.
    942     """
    943     commonItems = 0
    944     path1 = origin.path.split(os.sep)
    945     path2 = destination.path.split(os.sep)
    946     for elem1, elem2 in zip(path1, path2):
    947         if elem1 == elem2:
    948             commonItems += 1
    949         else:
    950             break
    951     path = [".."] * (len(path1) - commonItems)
    952     return path + path2[commonItems:]
    953 
    954 
    955 
    956 class DistributionBuilder(object):
    957     """
    958     A builder of Twisted distributions.
    959 
    960     This knows how to build tarballs for Twisted and all of its subprojects.
    961 
    962     @type blacklist: C{list} of C{str}
    963     @cvar blacklist: The list of subproject names to exclude from the main
    964         Twisted tarball and for which no individual project tarballs will be
    965         built. The same list as C{PROJECT_BLACKLIST}.
    966     """
    967 
    968     from twisted.python.dist import twisted_subprojects as subprojects
    969     blacklist = PROJECT_BLACKLIST
    970 
    971     def __init__(self, rootDirectory, outputDirectory, apiBaseURL=None):
    972         """
    973         Create a distribution builder.
    974 
    975         @param rootDirectory: root of a Twisted export which will populate
    976             subsequent tarballs.
    977         @type rootDirectory: L{FilePath}.
    978 
    979         @param outputDirectory: The directory in which to create the tarballs.
    980         @type outputDirectory: L{FilePath}
    981 
    982         @type apiBaseURL: C{str} or C{NoneType}
    983         @param apiBaseURL: A format string which will be interpolated with the
    984             fully-qualified Python name for each API link.  For example, to
    985             generate the Twisted 8.0.0 documentation, pass
    986             C{"http://twistedmatrix.com/documents/8.0.0/api/%s.html"}.
    987         """
    988         self.rootDirectory = rootDirectory
    989         self.outputDirectory = outputDirectory
    990         self.apiBaseURL = apiBaseURL
    991         self.manBuilder = ManBuilder()
    992         self.docBuilder = DocBuilder()
    993 
    994 
    995     def _buildDocInDir(self, path, version, howtoPath):
    996         """
    997         Generate documentation in the given path, building man pages first if
    998         necessary and swallowing errors (so that directories without lore
    999         documentation in them are ignored).
    1000 
    1001         @param path: The path containing documentation to build.
    1002         @type path: L{FilePath}
    1003         @param version: The version of the project to include in all generated
    1004             pages.
    1005         @type version: C{str}
    1006         @param howtoPath: The "resource path" as L{DocBuilder} describes it.
    1007         @type howtoPath: L{FilePath}
    1008         """
    1009         templatePath = self.rootDirectory.child("doc").child("core"
    1010             ).child("howto").child("template.tpl")
    1011         if path.basename() == "man":
    1012             self.manBuilder.build(path)
    1013         if path.isdir():
    1014             try:
    1015                 self.docBuilder.build(version, howtoPath, path,
    1016                     templatePath, self.apiBaseURL, True)
    1017             except NoDocumentsFound:
    1018                 pass
    1019 
    1020 
    1021     def buildTwisted(self, version):
    1022         """
    1023         Build the main Twisted distribution in C{Twisted-<version>.tar.bz2}.
    1024 
    1025         Projects listed in in L{blacklist} will not have their plugins, code,
    1026         documentation, or bin directories included.
    1027 
    1028         bin/admin is also excluded.
    1029 
    1030         @type version: C{str}
    1031         @param version: The version of Twisted to build.
    1032 
    1033         @return: The tarball file.
    1034         @rtype: L{FilePath}.
    1035         """
    1036         releaseName = "Twisted-%s" % (version,)
    1037         buildPath = lambda *args: '/'.join((releaseName,) + args)
    1038 
    1039         outputFile = self.outputDirectory.child(releaseName + ".tar.bz2")
    1040         tarball = tarfile.TarFile.open(outputFile.path, 'w:bz2')
    1041 
    1042         docPath = self.rootDirectory.child("doc")
    1043 
    1044         # Generate docs!
    1045         if docPath.isdir():
    1046             for subProjectDir in docPath.children():
    1047                 if (subProjectDir.isdir()
    1048                     and subProjectDir.basename() not in self.blacklist):
    1049                     for child in subProjectDir.walk():
    1050                         self._buildDocInDir(child, version,
    1051                             subProjectDir.child("howto"))
    1052 
    1053         # Now, this part is nasty.  We need to exclude blacklisted subprojects
    1054         # from the main Twisted distribution. This means we need to exclude
    1055         # their bin directories, their documentation directories, their
    1056         # plugins, and their python packages. Given that there's no "add all
    1057         # but exclude these particular paths" functionality in tarfile, we have
    1058         # to walk through all these directories and add things that *aren't*
    1059         # part of the blacklisted projects.
    1060 
    1061         for binthing in self.rootDirectory.child("bin").children():
    1062             # bin/admin should also not be included.
    1063             if binthing.basename() not in self.blacklist + ["admin"]:
    1064                 tarball.add(binthing.path,
    1065                             buildPath("bin", binthing.basename()))
    1066 
    1067         bad_plugins = ["twisted_%s.py" % (blacklisted,)
    1068                        for blacklisted in self.blacklist]
    1069 
    1070         for submodule in self.rootDirectory.child("twisted").children():
    1071             if submodule.basename() == "plugins":
    1072                 for plugin in submodule.children():
    1073                     if plugin.basename() not in bad_plugins:
    1074                         tarball.add(plugin.path, buildPath("twisted", "plugins",
    1075                                                            plugin.basename()))
    1076             elif submodule.basename() not in self.blacklist:
    1077                 tarball.add(submodule.path, buildPath("twisted",
    1078                                                       submodule.basename()))
    1079 
    1080         for docDir in self.rootDirectory.child("doc").children():
    1081             if docDir.basename() not in self.blacklist:
    1082                 tarball.add(docDir.path, buildPath("doc", docDir.basename()))
    1083 
    1084         for toplevel in self.rootDirectory.children():
    1085             if not toplevel.isdir():
    1086                 tarball.add(toplevel.path, buildPath(toplevel.basename()))
    1087 
    1088         tarball.close()
    1089 
    1090         return outputFile
    1091 
    1092 
    1093     def buildCore(self, version):
    1094         """
    1095         Build a core distribution in C{TwistedCore-<version>.tar.bz2}.
    1096 
    1097         This is very similar to L{buildSubProject}, but core tarballs and the
    1098         input are laid out slightly differently.
    1099 
    1100          - scripts are in the top level of the C{bin} directory.
    1101          - code is included directly from the C{twisted} directory, excluding
    1102            subprojects.
    1103          - all plugins except the subproject plugins are included.
    1104 
    1105         @type version: C{str}
    1106         @param version: The version of Twisted to build.
    1107 
    1108         @return: The tarball file.
    1109         @rtype: L{FilePath}.
    1110         """
    1111         releaseName = "TwistedCore-%s" % (version,)
    1112         outputFile = self.outputDirectory.child(releaseName + ".tar.bz2")
    1113         buildPath = lambda *args: '/'.join((releaseName,) + args)
    1114         tarball = self._createBasicSubprojectTarball(
    1115             "core", version, outputFile)
    1116 
    1117         # Include the bin directory for the subproject.
    1118         for path in self.rootDirectory.child("bin").children():
    1119             if not path.isdir():
    1120                 tarball.add(path.path, buildPath("bin", path.basename()))
    1121 
    1122         # Include all files within twisted/ that aren't part of a subproject.
    1123         for path in self.rootDirectory.child("twisted").children():
    1124             if path.basename() == "plugins":
    1125                 for plugin in path.children():
    1126                     for subproject in self.subprojects:
    1127                         if plugin.basename() == "twisted_%s.py" % (subproject,):
    1128                             break
    1129                     else:
    1130                         tarball.add(plugin.path,
    1131                                     buildPath("twisted", "plugins",
    1132                                               plugin.basename()))
    1133             elif not path.basename() in self.subprojects + ["topfiles"]:
    1134                 tarball.add(path.path, buildPath("twisted", path.basename()))
    1135 
    1136         tarball.add(self.rootDirectory.child("twisted").child("topfiles").path,
    1137                     releaseName)
    1138         tarball.close()
    1139 
    1140         return outputFile
    1141 
    1142 
    1143     def buildSubProject(self, projectName, version):
    1144         """
    1145         Build a subproject distribution in
    1146         C{Twisted<Projectname>-<version>.tar.bz2}.
    1147 
    1148         @type projectName: C{str}
    1149         @param projectName: The lowercase name of the subproject to build.
    1150         @type version: C{str}
    1151         @param version: The version of Twisted to build.
    1152 
    1153         @return: The tarball file.
    1154         @rtype: L{FilePath}.
    1155         """
    1156         releaseName = "Twisted%s-%s" % (projectName.capitalize(), version)
    1157         outputFile = self.outputDirectory.child(releaseName + ".tar.bz2")
    1158         buildPath = lambda *args: '/'.join((releaseName,) + args)
    1159         subProjectDir = self.rootDirectory.child("twisted").child(projectName)
    1160 
    1161         tarball = self._createBasicSubprojectTarball(projectName, version,
    1162                                                      outputFile)
    1163 
    1164         tarball.add(subProjectDir.child("topfiles").path, releaseName)
    1165 
    1166         # Include all files in the subproject package except for topfiles.
    1167         for child in subProjectDir.children():
    1168             name = child.basename()
    1169             if name != "topfiles":
    1170                 tarball.add(
    1171                     child.path,
    1172                     buildPath("twisted", projectName, name))
    1173 
    1174         pluginsDir = self.rootDirectory.child("twisted").child("plugins")
    1175         # Include the plugin for the subproject.
    1176         pluginFileName = "twisted_%s.py" % (projectName,)
    1177         pluginFile = pluginsDir.child(pluginFileName)
    1178         if pluginFile.exists():
    1179             tarball.add(pluginFile.path,
    1180                         buildPath("twisted", "plugins", pluginFileName))
    1181 
    1182         # Include the bin directory for the subproject.
    1183         binPath = self.rootDirectory.child("bin").child(projectName)
    1184         if binPath.isdir():
    1185             tarball.add(binPath.path, buildPath("bin"))
    1186         tarball.close()
    1187 
    1188         return outputFile
    1189 
    1190 
    1191     def _createBasicSubprojectTarball(self, projectName, version, outputFile):
    1192         """
    1193         Helper method to create and fill a tarball with things common between
    1194         subprojects and core.
    1195 
    1196         @param projectName: The subproject's name.
    1197         @type projectName: C{str}
    1198         @param version: The version of the release.
    1199         @type version: C{str}
    1200         @param outputFile: The location of the tar file to create.
    1201         @type outputFile: L{FilePath}
    1202         """
    1203         releaseName = "Twisted%s-%s" % (projectName.capitalize(), version)
    1204         buildPath = lambda *args: '/'.join((releaseName,) + args)
    1205 
    1206         tarball = tarfile.TarFile.open(outputFile.path, 'w:bz2')
    1207 
    1208         tarball.add(self.rootDirectory.child("LICENSE").path,
    1209                     buildPath("LICENSE"))
    1210 
    1211         docPath = self.rootDirectory.child("doc").child(projectName)
    1212 
    1213         if docPath.isdir():
    1214             for child in docPath.walk():
    1215                 self._buildDocInDir(child, version, docPath.child("howto"))
    1216             tarball.add(docPath.path, buildPath("doc"))
    1217 
    1218         return tarball
    1219 
    1220 
    1221 
    1222787class UncleanWorkingDirectory(Exception):
    1223788    """
    1224789    Raised when the working directory of an SVN checkout is unclean.
    1225790    """
    1226791
    1227792
     793
    1228794class NotWorkingDirectory(Exception):
    1229795    """
    1230796    Raised when a directory does not appear to be an SVN working directory.
    1231797    """
    1232798
    1233799
     800
    1234801def buildAllTarballs(checkout, destination):
    1235802    """
    1236803    Build complete tarballs (including documentation) for Twisted and all
    def buildAllTarballs(checkout, destination): 
    1265832    version = Project(twistedPath).getVersion()
    1266833    versionString = version.base()
    1267834
    1268     apiBaseURL = "http://twistedmatrix.com/documents/%s/api/%%s.html" % (
    1269         versionString)
    1270835    if not destination.exists():
    1271836        destination.createDirectory()
    1272     db = DistributionBuilder(export, destination, apiBaseURL=apiBaseURL)
     837    db = DistributionBuilder(export, destination,
     838            apiBaseURL=makeAPIBaseURL(versionString))
    1273839
    1274840    db.buildCore(versionString)
    1275841    for subproject in twisted_subprojects:
    def buildAllTarballs(checkout, destination): 
    1281847    workPath.remove()
    1282848
    1283849
     850
    1284851class ChangeVersionsScript(object):
    1285852    """
    1286853    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. 
    66Maintainer: Christopher Armstrong
    77"""
    88
    9 import sys, os
    10 from distutils.command import build_scripts, install_data, build_ext, build_py
     9import os
     10from distutils.command import (build_scripts, install_data, build_ext,
     11                               sdist)
    1112from distutils.errors import CompileError
    1213from distutils import core
    1314from distutils.core import Extension
     15from twisted.python.filepath import FilePath
     16from twisted.python._dist import DistributionBuilder, makeAPIBaseURL
     17from twisted.python._dist import isDistributable
    1418
    15 twisted_subprojects = ["conch", "lore", "mail", "names",
    16                        "news", "pair", "runner", "web", "web2",
    17                        "words", "vfs"]
    1819
    1920
    2021class ConditionalExtension(Extension):
    def setup(**kw): 
    4647    """
    4748    return core.setup(**get_setup_args(**kw))
    4849
     50
     51
    4952def get_setup_args(**kw):
    5053    if 'twisted_subproject' in kw:
    5154        if 'twisted' not in os.listdir('.'):
    def get_setup_args(**kw): 
    7376            kw.setdefault('py_modules', []).extend(py_modules)
    7477            del kw['plugins']
    7578
    76     if 'cmdclass' not in kw:
    77         kw['cmdclass'] = {
     79    defaultCmdClasses = {
    7880            'install_data': install_data_twisted,
    7981            '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
    8287
    8388    if "conditionalExtensions" in kw:
    8489        extensions = kw["conditionalExtensions"]
    def get_setup_args(**kw): 
    100105        kw.setdefault('cmdclass', {})['build_ext'] = my_build_ext
    101106    return kw
    102107
     108
     109
    103110def getVersion(proj, base="twisted"):
    104111    """
    105112    Extract the version number for a given project.
    def getVersion(proj, base="twisted"): 
    120127    return ns['version'].base()
    121128
    122129
    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 fnmatch
    129 
    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 names
    134              if n not in EXCLUDE_NAMES]
    135     # This is needed when building a distro from a working
    136     # copy (likely a checkout) rather than a pristine export:
    137     for pattern in EXCLUDE_PATTERNS:
    138         names = [n for n in names
    139                  if (not fnmatch.fnmatch(n, pattern))
    140                  and (not n.endswith('.py'))]
    141     return names
    142130
    143131def relativeTo(base, relativee):
    144132    """
    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.
    155146    """
    156147    basepath = os.path.abspath(base)
    157148    relativee = os.path.abspath(relativee)
    def relativeTo(base, relativee): 
    163154    raise ValueError("%s is not a subpath of %s" % (relativee, basepath))
    164155
    165156
     157
    166158def getDataFiles(dname, ignore=None, parent=None):
    167159    """
    168160    Get all the data files that should be included in this distutils Project.
    def getDataFiles(dname, ignore=None, parent=None): 
    186178    result = []
    187179    for directory, subdirectories, filenames in os.walk(dname):
    188180        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
    197192        if resultfiles:
     193            # Sort our results so that tests pass regardless of the underlying
     194            # filesystem's file order.
     195            resultfiles.sort()
    198196            result.append((relativeTo(parent, directory),
    199197                           [relativeTo(parent,
    200198                                       os.path.join(directory, filename))
    201199                            for filename in resultfiles]))
    202200    return result
    203201
     202
     203
    204204def getPackages(dname, pkgname=None, results=None, ignore=None, parent=None):
    205205    """
    206206    Get all packages which are under dname. This is necessary for
    def getScripts(projname, basedir=''): 
    251251                  [os.path.join(scriptdir, x) for x in thingies])
    252252
    253253
    254 ## Helpers and distutil tweaks
    255 
    256 class build_py_twisted(build_py.build_py):
    257     """
    258     Changes behavior in Python 2.2 to support simultaneous specification of
    259     `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 
    268254
     255## Helpers and distutil tweaks
    269256
    270257class build_scripts_twisted(build_scripts.build_scripts):
    271258    """Renames scripts so they end with '.py' on Windows."""
    class build_ext_twisted(build_ext.build_ext): 
    359346        self.compiler.announce("checking for %s ..." % header_name, 0)
    360347        return self._compile_helper("#include <%s>\n" % header_name)
    361348
     349
     350class _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
     4import os, stat, errno, tarfile
     5from xml.dom import minidom as dom
     6from twisted.trial.unittest import TestCase
     7from twisted.python.filepath import FilePath
     8
     9from twisted.python._dist import DocBuilder, ManBuilder, isDistributable
     10from twisted.python._dist import makeAPIBaseURL, DistributionBuilder
     11from twisted.python._dist import NoDocumentsFound, filePathDelta
     12from 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
     18SCRIPT_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.
     26try:
     27    from twisted.lore.scripts import lore
     28except ImportError:
     29    loreSkip = "Lore is not present."
     30else:
     31    loreSkip = None
     32
     33
     34
     35class 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
     147class 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
     257manhole \- Connect to a Twisted Manhole service
     258.SH SYNOPSIS
     259.B manhole
     260.SH DESCRIPTION
     261manhole is a GTK interface to Twisted Manhole services. You can execute python
     262code as if at an interactive Python console inside a running Twisted process
     263with 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
     293code as if at an interactive Python console inside a running Twisted process
     294with 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
     332code as if at an interactive Python console inside a running Twisted process
     333with 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
     344class 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
     522class 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
     603code as if at an interactive Python console inside a running Twisted process
     604with 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
     614class 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
     633class 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
     1151class 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
     1189class 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
     1209class FilePathDeltaTest(TestCase):
     1210    """
     1211    Tests for L{filePathDelta}.
     1212    """
     1213
     1214    def test_filePathDeltaSubdir(self):
     1215        """
     1216        L{filePathDelta} can create a simple relative path to a child path.
     1217        """
     1218        self.assertEquals(filePathDelta(FilePath("/foo/bar"),
     1219                                        FilePath("/foo/bar/baz")),
     1220                          ["baz"])
     1221
     1222
     1223    def test_filePathDeltaSiblingDir(self):
     1224        """
     1225        L{filePathDelta} can traverse upwards to create relative paths to
     1226        siblings.
     1227        """
     1228        self.assertEquals(filePathDelta(FilePath("/foo/bar"),
     1229                                        FilePath("/foo/baz")),
     1230                          ["..", "baz"])
     1231
     1232
     1233    def test_filePathNoCommonElements(self):
     1234        """
     1235        L{filePathDelta} can create relative paths to totally unrelated paths
     1236        for maximum portability.
     1237        """
     1238        self.assertEquals(filePathDelta(FilePath("/foo/bar"),
     1239                                        FilePath("/baz/quux")),
     1240                          ["..", "..", "baz", "quux"])
     1241
     1242
     1243    def test_filePathDeltaSimilarEndElements(self):
     1244        """
     1245        L{filePathDelta} doesn't take into account final elements when
     1246        comparing 2 paths, but stops at the first difference.
     1247        """
     1248        self.assertEquals(filePathDelta(FilePath("/foo/bar/bar/spam"),
     1249                                        FilePath("/foo/bar/baz/spam")),
     1250                          ["..", "..", "baz", "spam"])
  • twisted/python/test/test_dist.py

    diff --git a/twisted/python/test/test_dist.py b/twisted/python/test/test_dist.py
    index c69717d..00b819d 100644
    a b from distutils.core import Distribution 
    1313from twisted.trial.unittest import TestCase
    1414
    1515from twisted.python import dist
    16 from twisted.python.dist import get_setup_args, ConditionalExtension
     16from twisted.python.dist import (get_setup_args, ConditionalExtension,
     17                                 install_data_twisted, build_scripts_twisted,
     18                                 getDataFiles)
    1719from twisted.python.filepath import FilePath
    1820
    1921
    class SetupTest(TestCase): 
    2527        """
    2628        Passing C{conditionalExtensions} as a list of L{ConditionalExtension}
    2729        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.
    2931        """
    3032        good_ext = ConditionalExtension("whatever", ["whatever.c"],
    3133                                        condition=lambda b: True)
    class SetupTest(TestCase): 
    5557        self.assertEquals(ext.define_macros, [("whatever", 2), ("WIN32", 1)])
    5658
    5759
     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
    5895
    5996class GetVersionTest(TestCase):
    6097    """
    class GetScriptsTest(TestCase): 
    171208        os.mkdir(basedir)
    172209        scripts = dist.getScripts('noscripts', basedir=basedir)
    173210        self.assertEquals(scripts, [])
     211
     212
     213
     214class GetDataFilesTests(TestCase):
     215    """
     216    Tests for L{getDataFiles}.
     217    """
     218
     219    def _makeBaseDir(self):
     220        """
     221        Make a directory for getDataFiles to search.
     222        """
     223        rawBaseDir = os.path.join(".", self.mktemp())
     224        baseDir = FilePath(rawBaseDir)
     225        baseDir.makedirs()
     226
     227        return rawBaseDir, baseDir
     228
     229
     230    def test_basicOperation(self):
     231        """
     232        L{getDataFiles} finds a single data file in a given directory.
     233        """
     234        # The directory where we'll put our data file.
     235        rawBaseDir, baseDir = self._makeBaseDir()
     236
     237        # A data file to be found.
     238        baseDir.child("foo.txt").touch()
     239
     240        results = getDataFiles(baseDir.path)
     241        self.assertEquals(
     242            results,
     243            [(rawBaseDir, [os.path.join(rawBaseDir, "foo.txt")])])
     244
     245
     246    def test_directoryRecursion(self):
     247        """
     248        L{getDataFiles} searches for data files inside subdirectories.
     249        """
     250        rawBaseDir, baseDir = self._makeBaseDir()
     251
     252        subDir = baseDir.child("foo")
     253        subDir.makedirs()
     254
     255        subDir.child("bar.txt").touch()
     256
     257        subSubDir = subDir.child("baz")
     258        subSubDir.makedirs()
     259
     260        subSubDir.child("qux.txt").touch()
     261
     262        results = getDataFiles(baseDir.path)
     263        self.assertEquals(
     264            results,
     265            [(os.path.join(rawBaseDir, "foo"),
     266              [os.path.join(rawBaseDir, "foo", "bar.txt")]),
     267             (os.path.join(rawBaseDir, "foo", "baz"),
     268              [os.path.join(rawBaseDir, "foo", "baz", "qux.txt")])])
     269
     270
     271    def test_ignoreVCSMetadata(self):
     272        """
     273        L{getDataFiles} ignores Subversion metadata files.
     274        """
     275        rawBaseDir, baseDir = self._makeBaseDir()
     276
     277        # Top-level directory contains a VCS dir, containing ignorable data.
     278        vcsDir = baseDir.child(".svn")
     279        vcsDir.makedirs()
     280        vcsDir.child("data.txt").touch()
     281
     282        # Subdirectory contains a valid data file.
     283        subDir = baseDir.child("foo")
     284        subDir.makedirs()
     285        subDir.child("bar.txt").touch()
     286
     287        # Subdirectory contains another VCS dir, with more ignorable data.
     288        subVcsDir = subDir.child("_darcs")
     289        subVcsDir.makedirs()
     290        subVcsDir.child("data.txt").touch()
     291
     292        # Subdirectory contains an ignorable VCS file.
     293        subDir.child(".cvsignore").touch()
     294
     295        results = getDataFiles(baseDir.path)
     296        self.assertEquals(
     297            results,
     298            [(os.path.join(rawBaseDir, "foo"),
     299              [os.path.join(rawBaseDir, "foo", "bar.txt")])])
     300
     301
     302    def test_ignoreArbitrarySubdirectories(self):
     303        """
     304        L{getDataFiles} ignores any filenames it's asked to ignore.
     305        """
     306        rawBaseDir, baseDir = self._makeBaseDir()
     307
     308        subDir = baseDir.child("foo")
     309        subDir.makedirs()
     310
     311        # Make an ordinary subdirectory with some data files.
     312        subDir.child("bar.txt").touch()
     313        subDir.child("ignorable").touch() # not a dir, won't be ignored
     314
     315        # Make a subdirectory with an ignorable name, and some data files.
     316        ignorableSubDir = baseDir.child("ignorable")
     317        ignorableSubDir.makedirs()
     318        ignorableSubDir.child("bar.txt").touch()
     319
     320        results = getDataFiles(baseDir.path, ignore=["ignorable"])
     321        self.assertEquals(
     322            results,
     323            [(os.path.join(rawBaseDir, "foo"),
     324              [os.path.join(rawBaseDir, "foo", "bar.txt"),
     325               os.path.join(rawBaseDir, "foo", "ignorable")])])
     326
     327
     328    def test_ignoreNonDataFiles(self):
     329        """
     330        L{getDataFiles} ignores Python code, backup files and bytecode.
     331        """
     332        rawBaseDir, baseDir = self._makeBaseDir()
     333
     334        # All these are not data files, and should be ignored.
     335        baseDir.child("module.py").touch()
     336        baseDir.child("module.pyc").touch()
     337        baseDir.child("module.pyo").touch()
     338
     339        subDir = baseDir.child("foo")
     340        subDir.makedirs()
     341
     342        subDir.child("bar.txt").touch()
     343
     344        # An editor-made backup of bar.txt should be ignored.
     345        subDir.child("bar.txt~").touch()
     346
     347        results = getDataFiles(baseDir.path)
     348        self.assertEquals(
     349            results,
     350            [(os.path.join(rawBaseDir, "foo"),
     351              [os.path.join(rawBaseDir, "foo", "bar.txt")])])
     352
     353
     354    def test_pathsRelativeToParent(self):
     355        """
     356        L{getDataFiles} returns paths relative to the parent parameter.
     357        """
     358        rawBaseDir, baseDir = self._makeBaseDir()
     359
     360        # munge rawBaseDir in a way that we can recognise later.
     361        mungedBaseDir = os.path.join(rawBaseDir, "foo/../")
     362
     363        subDir = baseDir.child("foo")
     364        subDir.makedirs()
     365
     366        subDir.child("bar.txt").touch()
     367
     368        results = getDataFiles(subDir.path, parent=mungedBaseDir)
     369        self.assertEquals(
     370            results,
     371            [(os.path.join(mungedBaseDir, "foo"),
     372              [os.path.join(mungedBaseDir, "foo", "bar.txt")])])
  • twisted/python/test/test_release.py

    diff --git a/twisted/python/test/test_release.py b/twisted/python/test/test_release.py
    index a9eb872..84d1302 100644
    a b import warnings 
    1313import operator
    1414import os, sys, signal
    1515from StringIO import StringIO
    16 import tarfile
    17 from xml.dom import minidom as dom
    1816
    1917from datetime import date
    2018
    from twisted.python._release import replaceProjectVersion 
    3129from twisted.python._release import updateTwistedVersionInformation, Project
    3230from twisted.python._release import generateVersionFileData
    3331from twisted.python._release import changeAllProjectVersions
    34 from twisted.python._release import VERSION_OFFSET, DocBuilder, ManBuilder
    35 from twisted.python._release import NoDocumentsFound, filePathDelta
     32from twisted.python._release import VERSION_OFFSET
    3633from twisted.python._release import CommandFailed, BookBuilder
    37 from twisted.python._release import DistributionBuilder, APIBuilder
     34from twisted.python._release import APIBuilder
    3835from twisted.python._release import BuildAPIDocsScript
    3936from twisted.python._release import buildAllTarballs, runCommand
    4037from twisted.python._release import UncleanWorkingDirectory, NotWorkingDirectory
    4138from twisted.python._release import ChangeVersionsScript, BuildTarballsScript
    4239from twisted.python._release import NewsBuilder
    4340
     41from twisted.python.test.test__dist import loreSkip, StructureAssertingMixin
     42from twisted.python.test.test__dist import BuilderTestsMixin
     43from twisted.python.test.test__dist import DistributionBuilderTestBase
     44
    4445if os.name != 'posix':
    4546    skip = "Release toolchain only supported on POSIX."
    4647else:
    def genVersion(*args, **kwargs): 
    9293
    9394
    9495
    95 class StructureAssertingMixin(object):
    96     """
    97     A mixin for L{TestCase} subclasses which provides some methods for asserting
    98     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 their
    103         structure.
    104 
    105         @param root: The directory in which to create the structure.  It must
    106             already exist.
    107         @type root: L{FilePath}
    108 
    109         @param dirDict: The dict defining the structure. Keys should be strings
    110             naming files, values should be strings describing file contents OR
    111             dicts describing subdirectories.  All files are written in binary
    112             mode.  Any string values are assumed to describe text files and
    113             will have their newlines replaced with the platform-native newline
    114             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 the
    135             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 the
    162             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 
    17596class ChangeVersionTest(TestCase, StructureAssertingMixin):
    17697    """
    17798    Twisted has the ability to change versions.
    class VersionWritingTest(TestCase): 
    537458
    538459
    539460
    540 class BuilderTestsMixin(object):
    541     """
    542     A mixin class which provides various methods for creating sample Lore input
    543     and output.
    544 
    545     @cvar template: The lore template that will be used to prepare sample
    546     output.
    547     @type template: C{str}
    548 
    549     @ivar docCounter: A counter which is incremented every time input is
    550         generated and which is included in the documents.
    551     @type docCounter: C{int}
    552     """
    553     template = '''
    554     <html>
    555     <head><title>Yo:</title></head>
    556     <body>
    557     <div class="body" />
    558     <a href="index.html">Index</a>
    559     <span class="version">Version: </span>
    560     </body>
    561     </html>
    562     '''
    563 
    564     def setUp(self):
    565         """
    566         Initialize the doc counter which ensures documents are unique.
    567         """
    568         self.docCounter = 0
    569 
    570 
    571     def assertXMLEqual(self, first, second):
    572         """
    573         Verify that two strings represent the same XML document.
    574         """
    575         self.assertEqual(
    576             dom.parseString(first).toxml(),
    577             dom.parseString(second).toxml())
    578 
    579 
    580     def getArbitraryOutput(self, version, counter, prefix="", apiBaseURL="%s"):
    581         """
    582         Get the correct HTML output for the arbitrary input returned by
    583         L{getArbitraryLoreInput} for the given parameters.
    584 
    585         @param version: The version string to include in the output.
    586         @type version: C{str}
    587         @param counter: A counter to include in the output.
    588         @type counter: C{int}
    589         """
    590         document = """\
    591 <?xml version="1.0"?><html>
    592     <head><title>Yo:Hi! Title: %(count)d</title></head>
    593     <body>
    594     <div class="content">Hi! %(count)d<div class="API"><a href="%(foobarLink)s" title="foobar">foobar</a></div></div>
    595     <a href="%(prefix)sindex.html">Index</a>
    596     <span class="version">Version: %(version)s</span>
    597     </body>
    598     </html>"""
    599         # Try to normalize irrelevant whitespace.
    600         return dom.parseString(
    601             document % {"count": counter, "prefix": prefix,
    602                         "version": version,
    603                         "foobarLink": apiBaseURL % ("foobar",)}).toxml('utf-8')
    604 
    605 
    606     def getArbitraryLoreInput(self, counter):
    607         """
    608         Get an arbitrary, unique (for this test case) string of lore input.
    609 
    610         @param counter: A counter to include in the input.
    611         @type counter: C{int}
    612         """
    613         template = (
    614             '<html>'
    615             '<head><title>Hi! Title: %(count)s</title></head>'
    616             '<body>'
    617             'Hi! %(count)s'
    618             '<div class="API">foobar</div>'
    619             '</body>'
    620             '</html>')
    621         return template % {"count": counter}
    622 
    623 
    624     def getArbitraryLoreInputAndOutput(self, version, prefix="",
    625                                        apiBaseURL="%s"):
    626         """
    627         Get an input document along with expected output for lore run on that
    628         output document, assuming an appropriately-specified C{self.template}.
    629 
    630         @param version: A version string to include in the input and output.
    631         @type version: C{str}
    632         @param prefix: The prefix to include in the link to the index.
    633         @type prefix: C{str}
    634 
    635         @return: A two-tuple of input and expected output.
    636         @rtype: C{(str, str)}.
    637         """
    638         self.docCounter += 1
    639         return (self.getArbitraryLoreInput(self.docCounter),
    640                 self.getArbitraryOutput(version, self.docCounter,
    641                                         prefix=prefix, apiBaseURL=apiBaseURL))
    642 
    643 
    644     def getArbitraryManInput(self):
    645         """
    646         Get an arbitrary man page content.
    647         """
    648         return """.TH MANHOLE "1" "August 2001" "" ""
    649 .SH NAME
    650 manhole \- Connect to a Twisted Manhole service
    651 .SH SYNOPSIS
    652 .B manhole
    653 .SH DESCRIPTION
    654 manhole is a GTK interface to Twisted Manhole services. You can execute python
    655 code as if at an interactive Python console inside a running Twisted process
    656 with this."""
    657 
    658 
    659     def getArbitraryManLoreOutput(self):
    660         """
    661         Get an arbitrary lore input document which represents man-to-lore
    662         output based on the man page returned from L{getArbitraryManInput}
    663         """
    664         return """\
    665 <?xml version="1.0"?>
    666 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
    667     "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
    668 <html><head>
    669 <title>MANHOLE.1</title></head>
    670 <body>
    671 
    672 <h1>MANHOLE.1</h1>
    673 
    674 <h2>NAME</h2>
    675 
    676 <p>manhole - Connect to a Twisted Manhole service
    677 </p>
    678 
    679 <h2>SYNOPSIS</h2>
    680 
    681 <p><strong>manhole</strong> </p>
    682 
    683 <h2>DESCRIPTION</h2>
    684 
    685 <p>manhole is a GTK interface to Twisted Manhole services. You can execute python
    686 code as if at an interactive Python console inside a running Twisted process
    687 with this.</p>
    688 
    689 </body>
    690 </html>
    691 """
    692 
    693     def getArbitraryManHTMLOutput(self, version, prefix=""):
    694         """
    695         Get an arbitrary lore output document which represents the lore HTML
    696         output based on the input document returned from
    697         L{getArbitraryManLoreOutput}.
    698 
    699         @param version: A version string to include in the document.
    700         @type version: C{str}
    701         @param prefix: The prefix to include in the link to the index.
    702         @type prefix: C{str}
    703         """
    704         # Try to normalize the XML a little bit.
    705         return dom.parseString("""\
    706 <?xml version="1.0" ?><html>
    707     <head><title>Yo:MANHOLE.1</title></head>
    708     <body>
    709     <div class="content">
    710 
    711 <span/>
    712 
    713 <h2>NAME<a name="auto0"/></h2>
    714 
    715 <p>manhole - Connect to a Twisted Manhole service
    716 </p>
    717 
    718 <h2>SYNOPSIS<a name="auto1"/></h2>
    719 
    720 <p><strong>manhole</strong> </p>
    721 
    722 <h2>DESCRIPTION<a name="auto2"/></h2>
    723 
    724 <p>manhole is a GTK interface to Twisted Manhole services. You can execute python
    725 code as if at an interactive Python console inside a running Twisted process
    726 with this.</p>
    727 
    728 </div>
    729     <a href="%(prefix)sindex.html">Index</a>
    730     <span class="version">Version: %(version)s</span>
    731     </body>
    732     </html>""" % {
    733             'prefix': prefix, 'version': version}).toxml("utf-8")
    734 
    735 
    736 
    737 class DocBuilderTestCase(TestCase, BuilderTestsMixin):
    738     """
    739     Tests for L{DocBuilder}.
    740 
    741     Note for future maintainers: The exact byte equality assertions throughout
    742     this suite may need to be updated due to minor differences in lore. They
    743     should not be taken to mean that Lore must maintain the same byte format
    744     forever. Feel free to update the tests when Lore changes, but please be
    745     careful.
    746     """
    747     skip = loreSkip
    748 
    749     def setUp(self):
    750         """
    751         Set up a few instance variables that will be useful.
    752 
    753         @ivar builder: A plain L{DocBuilder}.
    754         @ivar docCounter: An integer to be used as a counter by the
    755             C{getArbitrary...} methods.
    756         @ivar howtoDir: A L{FilePath} representing a directory to be used for
    757             containing Lore documents.
    758         @ivar templateFile: A L{FilePath} representing a file with
    759             C{self.template} as its content.
    760         """
    761         BuilderTestsMixin.setUp(self)
    762         self.builder = DocBuilder()
    763         self.howtoDir = FilePath(self.mktemp())
    764         self.howtoDir.createDirectory()
    765         self.templateFile = self.howtoDir.child("template.tpl")
    766         self.templateFile.setContent(self.template)
    767 
    768 
    769     def test_build(self):
    770         """
    771         The L{DocBuilder} runs lore on all .xhtml files within a directory.
    772         """
    773         version = "1.2.3"
    774         input1, output1 = self.getArbitraryLoreInputAndOutput(version)
    775         input2, output2 = self.getArbitraryLoreInputAndOutput(version)
    776 
    777         self.howtoDir.child("one.xhtml").setContent(input1)
    778         self.howtoDir.child("two.xhtml").setContent(input2)
    779 
    780         self.builder.build(version, self.howtoDir, self.howtoDir,
    781                            self.templateFile)
    782         out1 = self.howtoDir.child('one.html')
    783         out2 = self.howtoDir.child('two.html')
    784         self.assertXMLEqual(out1.getContent(), output1)
    785         self.assertXMLEqual(out2.getContent(), output2)
    786 
    787 
    788     def test_noDocumentsFound(self):
    789         """
    790         The C{build} method raises L{NoDocumentsFound} if there are no
    791         .xhtml files in the given directory.
    792         """
    793         self.assertRaises(
    794             NoDocumentsFound,
    795             self.builder.build, "1.2.3", self.howtoDir, self.howtoDir,
    796             self.templateFile)
    797 
    798 
    799     def test_parentDocumentLinking(self):
    800         """
    801         The L{DocBuilder} generates correct links from documents to
    802         template-generated links like stylesheets and index backreferences.
    803         """
    804         input = self.getArbitraryLoreInput(0)
    805         tutoDir = self.howtoDir.child("tutorial")
    806         tutoDir.createDirectory()
    807         tutoDir.child("child.xhtml").setContent(input)
    808         self.builder.build("1.2.3", self.howtoDir, tutoDir, self.templateFile)
    809         outFile = tutoDir.child('child.html')
    810         self.assertIn('<a href="../index.html">Index</a>',
    811                       outFile.getContent())
    812 
    813 
    814     def test_siblingDirectoryDocumentLinking(self):
    815         """
    816         It is necessary to generate documentation in a directory foo/bar where
    817         stylesheet and indexes are located in foo/baz. Such resources should be
    818         appropriately linked to.
    819         """
    820         input = self.getArbitraryLoreInput(0)
    821         resourceDir = self.howtoDir.child("resources")
    822         docDir = self.howtoDir.child("docs")
    823         docDir.createDirectory()
    824         docDir.child("child.xhtml").setContent(input)
    825         self.builder.build("1.2.3", resourceDir, docDir, self.templateFile)
    826         outFile = docDir.child('child.html')
    827         self.assertIn('<a href="../resources/index.html">Index</a>',
    828                       outFile.getContent())
    829 
    830 
    831     def test_apiLinking(self):
    832         """
    833         The L{DocBuilder} generates correct links from documents to API
    834         documentation.
    835         """
    836         version = "1.2.3"
    837         input, output = self.getArbitraryLoreInputAndOutput(version)
    838         self.howtoDir.child("one.xhtml").setContent(input)
    839 
    840         self.builder.build(version, self.howtoDir, self.howtoDir,
    841                            self.templateFile, "scheme:apilinks/%s.ext")
    842         out = self.howtoDir.child('one.html')
    843         self.assertIn(
    844             '<a href="scheme:apilinks/foobar.ext" title="foobar">foobar</a>',
    845             out.getContent())
    846 
    847 
    848     def test_deleteInput(self):
    849         """
    850         L{DocBuilder.build} can be instructed to delete the input files after
    851         generating the output based on them.
    852         """
    853         input1 = self.getArbitraryLoreInput(0)
    854         self.howtoDir.child("one.xhtml").setContent(input1)
    855         self.builder.build("whatever", self.howtoDir, self.howtoDir,
    856                            self.templateFile, deleteInput=True)
    857         self.assertTrue(self.howtoDir.child('one.html').exists())
    858         self.assertFalse(self.howtoDir.child('one.xhtml').exists())
    859 
    860 
    861     def test_doNotDeleteInput(self):
    862         """
    863         Input will not be deleted by default.
    864         """
    865         input1 = self.getArbitraryLoreInput(0)
    866         self.howtoDir.child("one.xhtml").setContent(input1)
    867         self.builder.build("whatever", self.howtoDir, self.howtoDir,
    868                            self.templateFile)
    869         self.assertTrue(self.howtoDir.child('one.html').exists())
    870         self.assertTrue(self.howtoDir.child('one.xhtml').exists())
    871 
    872 
    873     def test_getLinkrelToSameDirectory(self):
    874         """
    875         If the doc and resource directories are the same, the linkrel should be
    876         an empty string.
    877         """
    878         linkrel = self.builder.getLinkrel(FilePath("/foo/bar"),
    879                                           FilePath("/foo/bar"))
    880         self.assertEquals(linkrel, "")
    881 
    882 
    883     def test_getLinkrelToParentDirectory(self):
    884         """
    885         If the doc directory is a child of the resource directory, the linkrel
    886         should make use of '..'.
    887         """
    888         linkrel = self.builder.getLinkrel(FilePath("/foo"),
    889                                           FilePath("/foo/bar"))
    890         self.assertEquals(linkrel, "../")
    891 
    892 
    893     def test_getLinkrelToSibling(self):
    894         """
    895         If the doc directory is a sibling of the resource directory, the
    896         linkrel should make use of '..' and a named segment.
    897         """
    898         linkrel = self.builder.getLinkrel(FilePath("/foo/howto"),
    899                                           FilePath("/foo/examples"))
    900         self.assertEquals(linkrel, "../howto/")
    901 
    902 
    903     def test_getLinkrelToUncle(self):
    904         """
    905         If the doc directory is a sibling of the parent of the resource
    906         directory, the linkrel should make use of multiple '..'s and a named
    907         segment.
    908         """
    909         linkrel = self.builder.getLinkrel(FilePath("/foo/howto"),
    910                                           FilePath("/foo/examples/quotes"))
    911         self.assertEquals(linkrel, "../../howto/")
    912 
    913 
    914 
    915461class APIBuilderTestCase(TestCase):
    916462    """
    917463    Tests for L{APIBuilder}.
    class APIBuilderTestCase(TestCase): 
    1051597
    1052598
    1053599
    1054 class ManBuilderTestCase(TestCase, BuilderTestsMixin):
    1055     """
    1056     Tests for L{ManBuilder}.
    1057     """
    1058     skip = loreSkip
    1059 
    1060     def setUp(self):
    1061         """
    1062         Set up a few instance variables that will be useful.
    1063 
    1064         @ivar builder: A plain L{ManBuilder}.
    1065         @ivar manDir: A L{FilePath} representing a directory to be used for
    1066             containing man pages.
    1067         """
    1068         BuilderTestsMixin.setUp(self)
    1069         self.builder = ManBuilder()
    1070         self.manDir = FilePath(self.mktemp())
    1071         self.manDir.createDirectory()
    1072 
    1073 
    1074     def test_noDocumentsFound(self):
    1075         """
    1076         L{ManBuilder.build} raises L{NoDocumentsFound} if there are no
    1077         .1 files in the given directory.
    1078         """
    1079         self.assertRaises(NoDocumentsFound, self.builder.build, self.manDir)
    1080 
    1081 
    1082     def test_build(self):
    1083         """
    1084         Check that L{ManBuilder.build} find the man page in the directory, and
    1085         successfully produce a Lore content.
    1086         """
    1087         manContent = self.getArbitraryManInput()
    1088         self.manDir.child('test1.1').setContent(manContent)
    1089         self.builder.build(self.manDir)
    1090         output = self.manDir.child('test1-man.xhtml').getContent()
    1091         expected = self.getArbitraryManLoreOutput()
    1092         # No-op on *nix, fix for windows
    1093         expected = expected.replace('\n', os.linesep)
    1094         self.assertEquals(output, expected)
    1095 
    1096 
    1097     def test_toHTML(self):
    1098         """
    1099         Check that the content output by C{build} is compatible as input of
    1100         L{DocBuilder.build}.
    1101         """
    1102         manContent = self.getArbitraryManInput()
    1103         self.manDir.child('test1.1').setContent(manContent)
    1104         self.builder.build(self.manDir)
    1105 
    1106         templateFile = self.manDir.child("template.tpl")
    1107         templateFile.setContent(DocBuilderTestCase.template)
    1108         docBuilder = DocBuilder()
    1109         docBuilder.build("1.2.3", self.manDir, self.manDir,
    1110                          templateFile)
    1111         output = self.manDir.child('test1-man.html').getContent()
    1112 
    1113         self.assertXMLEqual(
    1114             output,
    1115             """\
    1116 <?xml version="1.0" ?><html>
    1117     <head><title>Yo:MANHOLE.1</title></head>
    1118     <body>
    1119     <div class="content">
    1120 
    1121 <span/>
    1122 
    1123 <h2>NAME<a name="auto0"/></h2>
    1124 
    1125 <p>manhole - Connect to a Twisted Manhole service
    1126 </p>
    1127 
    1128 <h2>SYNOPSIS<a name="auto1"/></h2>
    1129 
    1130 <p><strong>manhole</strong> </p>
    1131 
    1132 <h2>DESCRIPTION<a name="auto2"/></h2>
    1133 
    1134 <p>manhole is a GTK interface to Twisted Manhole services. You can execute python
    1135 code as if at an interactive Python console inside a running Twisted process
    1136 with this.</p>
    1137 
    1138 </div>
    1139     <a href="index.html">Index</a>
    1140     <span class="version">Version: 1.2.3</span>
    1141     </body>
    1142     </html>""")
    1143 
    1144 
    1145 
    1146600class BookBuilderTests(TestCase, BuilderTestsMixin):
    1147601    """
    1148602    Tests for L{BookBuilder}.
    class BookBuilderTests(TestCase, BuilderTestsMixin): 
    1475929
    1476930
    1477931
    1478 class FilePathDeltaTest(TestCase):
    1479     """
    1480     Tests for L{filePathDelta}.
    1481     """
    1482 
    1483     def test_filePathDeltaSubdir(self):
    1484         """
    1485         L{filePathDelta} can create a simple relative path to a child path.
    1486         """
    1487         self.assertEquals(filePathDelta(FilePath("/foo/bar"),
    1488                                         FilePath("/foo/bar/baz")),
    1489                           ["baz"])
    1490 
    1491 
    1492     def test_filePathDeltaSiblingDir(self):
    1493         """
    1494         L{filePathDelta} can traverse upwards to create relative paths to
    1495         siblings.
    1496         """
    1497         self.assertEquals(filePathDelta(FilePath("/foo/bar"),
    1498                                         FilePath("/foo/baz")),
    1499                           ["..", "baz"])
    1500 
    1501 
    1502     def test_filePathNoCommonElements(self):
    1503         """
    1504         L{filePathDelta} can create relative paths to totally unrelated paths
    1505         for maximum portability.
    1506         """
    1507         self.assertEquals(filePathDelta(FilePath("/foo/bar"),
    1508                                         FilePath("/baz/quux")),
    1509                           ["..", "..", "baz", "quux"])
    1510 
    1511 
    1512     def test_filePathDeltaSimilarEndElements(self):
    1513         """
    1514         L{filePathDelta} doesn't take into account final elements when
    1515         comparing 2 paths, but stops at the first difference.
    1516         """
    1517         self.assertEquals(filePathDelta(FilePath("/foo/bar/bar/spam"),
    1518                                         FilePath("/foo/bar/baz/spam")),
    1519                           ["..", "..", "baz", "spam"])
    1520 
    1521 
    1522 
    1523932class NewsBuilderTests(TestCase, StructureAssertingMixin):
    1524933    """
    1525934    Tests for L{NewsBuilder}.
    class NewsBuilderTests(TestCase, StructureAssertingMixin): 
    19761385
    19771386
    19781387
    1979 class DistributionBuilderTestBase(BuilderTestsMixin, StructureAssertingMixin,
    1980                                    TestCase):
    1981     """
    1982     Base for tests of L{DistributionBuilder}.
    1983     """
    1984     skip = loreSkip
    1985 
    1986     def setUp(self):
    1987         BuilderTestsMixin.setUp(self)
    1988 
    1989         self.rootDir = FilePath(self.mktemp())
    1990         self.rootDir.createDirectory()
    1991 
    1992         self.outputDir = FilePath(self.mktemp())
    1993         self.outputDir.createDirectory()
    1994         self.builder = DistributionBuilder(self.rootDir, self.outputDir)
    1995 
    1996 
    1997 
    1998 class DistributionBuilderTest(DistributionBuilderTestBase):
    1999 
    2000     def test_twistedDistribution(self):
    2001         """
    2002         The Twisted tarball contains everything in the source checkout, with
    2003         built documentation.
    2004         """
    2005         loreInput, loreOutput = self.getArbitraryLoreInputAndOutput("10.0.0")
    2006         manInput1 = self.getArbitraryManInput()
    2007         manOutput1 = self.getArbitraryManHTMLOutput("10.0.0", "../howto/")
    2008         manInput2 = self.getArbitraryManInput()
    2009         manOutput2 = self.getArbitraryManHTMLOutput("10.0.0", "../howto/")
    2010         coreIndexInput, coreIndexOutput = self.getArbitraryLoreInputAndOutput(
    2011             "10.0.0", prefix="howto/")
    2012 
    2013         structure = {
    2014             "README": "Twisted",
    2015             "unrelated": "x",
    2016             "LICENSE": "copyright!",
    2017             "setup.py": "import toplevel",
    2018             "bin": {"web": {"websetroot": "SET ROOT"},
    2019                     "twistd": "TWISTD"},
    2020             "twisted":
    2021                 {"web":
    2022                      {"__init__.py": "import WEB",
    2023                       "topfiles": {"setup.py": "import WEBINSTALL",
    2024                                    "README": "WEB!"}},
    2025                  "words": {"__init__.py": "import WORDS"},
    2026                  "plugins": {"twisted_web.py": "import WEBPLUG",
    2027                              "twisted_words.py": "import WORDPLUG"}},
    2028             "doc": {"web": {"howto": {"index.xhtml": loreInput},
    2029                             "man": {"websetroot.1": manInput2}},
    2030                     "core": {"howto": {"template.tpl": self.template},
    2031                              "man": {"twistd.1": manInput1},
    2032                              "index.xhtml": coreIndexInput}}}
    2033 
    2034         outStructure = {
    2035             "README": "Twisted",
    2036             "unrelated": "x",
    2037             "LICENSE": "copyright!",
    2038             "setup.py": "import toplevel",
    2039             "bin": {"web": {"websetroot": "SET ROOT"},
    2040                     "twistd": "TWISTD"},
    2041             "twisted":
    2042                 {"web": {"__init__.py": "import WEB",
    2043                          "topfiles": {"setup.py": "import WEBINSTALL",
    2044                                       "README": "WEB!"}},
    2045                  "words": {"__init__.py": "import WORDS"},
    2046                  "plugins": {"twisted_web.py": "import WEBPLUG",
    2047                              "twisted_words.py": "import WORDPLUG"}},
    2048             "doc": {"web": {"howto": {"index.html": loreOutput},
    2049                             "man": {"websetroot.1": manInput2,
    2050                                     "websetroot-man.html": manOutput2}},
    2051                     "core": {"howto": {"template.tpl": self.template},
    2052                              "man": {"twistd.1": manInput1,
    2053                                      "twistd-man.html": manOutput1},
    2054                              "index.html": coreIndexOutput}}}
    2055 
    2056         self.createStructure(self.rootDir, structure)
    2057 
    2058         outputFile = self.builder.buildTwisted("10.0.0")
    2059 
    2060         self.assertExtractedStructure(outputFile, outStructure)
    2061 
    2062 
    2063     def test_twistedDistributionExcludesWeb2AndVFSAndAdmin(self):
    2064         """
    2065         The main Twisted distribution does not include web2 or vfs, or the
    2066         bin/admin directory.
    2067         """
    2068         loreInput, loreOutput = self.getArbitraryLoreInputAndOutput("10.0.0")
    2069         coreIndexInput, coreIndexOutput = self.getArbitraryLoreInputAndOutput(
    2070             "10.0.0", prefix="howto/")
    2071 
    2072         structure = {
    2073             "README": "Twisted",
    2074             "unrelated": "x",
    2075             "LICENSE": "copyright!",
    2076             "setup.py": "import toplevel",
    2077             "bin": {"web2": {"websetroot": "SET ROOT"},
    2078                     "vfs": {"vfsitup": "hee hee"},
    2079                     "twistd": "TWISTD",
    2080                     "admin": {"build-a-thing": "yay"}},
    2081             "twisted":
    2082                 {"web2":
    2083                      {"__init__.py": "import WEB",
    2084                       "topfiles": {"setup.py": "import WEBINSTALL",
    2085                                    "README": "WEB!"}},
    2086                  "vfs":
    2087                      {"__init__.py": "import VFS",
    2088                       "blah blah": "blah blah"},
    2089                  "words": {"__init__.py": "import WORDS"},
    2090                  "plugins": {"twisted_web.py": "import WEBPLUG",
    2091                              "twisted_words.py": "import WORDPLUG",
    2092                              "twisted_web2.py": "import WEB2",
    2093                              "twisted_vfs.py": "import VFS"}},
    2094             "doc": {"web2": {"excluded!": "yay"},
    2095                     "vfs": {"unrelated": "whatever"},
    2096                     "core": {"howto": {"template.tpl": self.template},
    2097                              "index.xhtml": coreIndexInput}}}
    2098 
    2099         outStructure = {
    2100             "README": "Twisted",
    2101             "unrelated": "x",
    2102             "LICENSE": "copyright!",
    2103             "setup.py": "import toplevel",
    2104             "bin": {"twistd": "TWISTD"},
    2105             "twisted":
    2106                 {"words": {"__init__.py": "import WORDS"},
    2107                  "plugins": {"twisted_web.py": "import WEBPLUG",
    2108                              "twisted_words.py": "import WORDPLUG"}},
    2109             "doc": {"core": {"howto": {"template.tpl": self.template},
    2110                              "index.html": coreIndexOutput}}}
    2111         self.createStructure(self.rootDir, structure)
    2112 
    2113         outputFile = self.builder.buildTwisted("10.0.0")
    2114 
    2115         self.assertExtractedStructure(outputFile, outStructure)
    2116 
    2117 
    2118     def test_subProjectLayout(self):
    2119         """
    2120         The subproject tarball includes files like so:
    2121 
    2122         1. twisted/<subproject>/topfiles defines the files that will be in the
    2123            top level in the tarball, except LICENSE, which comes from the real
    2124            top-level directory.
    2125         2. twisted/<subproject> is included, but without the topfiles entry
    2126            in that directory. No other twisted subpackages are included.
    2127         3. twisted/plugins/twisted_<subproject>.py is included, but nothing
    2128            else in plugins is.
    2129         """
    2130         structure = {
    2131             "README": "HI!@",
    2132             "unrelated": "x",
    2133             "LICENSE": "copyright!",
    2134             "setup.py": "import toplevel",
    2135             "bin": {"web": {"websetroot": "SET ROOT"},
    2136                     "words": {"im": "#!im"}},
    2137             "twisted":
    2138                 {"web":
    2139                      {"__init__.py": "import WEB",
    2140                       "topfiles": {"setup.py": "import WEBINSTALL",
    2141                                    "README": "WEB!"}},
    2142                  "words": {"__init__.py": "import WORDS"},
    2143                  "plugins": {"twisted_web.py": "import WEBPLUG",
    2144                              "twisted_words.py": "import WORDPLUG"}}}
    2145 
    2146         outStructure = {
    2147             "README": "WEB!",
    2148             "LICENSE": "copyright!",
    2149             "setup.py": "import WEBINSTALL",
    2150             "bin": {"websetroot": "SET ROOT"},
    2151             "twisted": {"web": {"__init__.py": "import WEB"},
    2152                         "plugins": {"twisted_web.py": "import WEBPLUG"}}}
    2153 
    2154         self.createStructure(self.rootDir, structure)
    2155 
    2156         outputFile = self.builder.buildSubProject("web", "0.3.0")
    2157 
    2158         self.assertExtractedStructure(outputFile, outStructure)
    2159 
    2160 
    2161     def test_minimalSubProjectLayout(self):
    2162         """
    2163         buildSubProject should work with minimal subprojects.
    2164         """
    2165         structure = {
    2166             "LICENSE": "copyright!",
    2167             "bin": {},
    2168             "twisted":
    2169                 {"web": {"__init__.py": "import WEB",
    2170                          "topfiles": {"setup.py": "import WEBINSTALL"}},
    2171                  "plugins": {}}}
    2172 
    2173         outStructure = {
    2174             "setup.py": "import WEBINSTALL",
    2175             "LICENSE": "copyright!",
    2176             "twisted": {"web": {"__init__.py": "import WEB"}}}
    2177 
    2178         self.createStructure(self.rootDir, structure)
    2179 
    2180         outputFile = self.builder.buildSubProject("web", "0.3.0")
    2181 
    2182         self.assertExtractedStructure(outputFile, outStructure)
    2183 
    2184 
    2185     def test_subProjectDocBuilding(self):
    2186         """
    2187         When building a subproject release, documentation should be built with
    2188         lore.
    2189         """
    2190         loreInput, loreOutput = self.getArbitraryLoreInputAndOutput("0.3.0")
    2191         manInput = self.getArbitraryManInput()
    2192         manOutput = self.getArbitraryManHTMLOutput("0.3.0", "../howto/")
    2193         structure = {
    2194             "LICENSE": "copyright!",
    2195             "twisted": {"web": {"__init__.py": "import WEB",
    2196                                 "topfiles": {"setup.py": "import WEBINST"}}},
    2197             "doc": {"web": {"howto": {"index.xhtml": loreInput},
    2198                             "man": {"twistd.1": manInput}},
    2199                     "core": {"howto": {"template.tpl": self.template}}
    2200                     }
    2201             }
    2202 
    2203         outStructure = {
    2204             "LICENSE": "copyright!",
    2205             "setup.py": "import WEBINST",
    2206             "twisted": {"web": {"__init__.py": "import WEB"}},
    2207             "doc": {"howto": {"index.html": loreOutput},
    2208                     "man": {"twistd.1": manInput,
    2209                             "twistd-man.html": manOutput}}}
    2210 
    2211         self.createStructure(self.rootDir, structure)
    2212 
    2213         outputFile = self.builder.buildSubProject("web", "0.3.0")
    2214 
    2215         self.assertExtractedStructure(outputFile, outStructure)
    2216 
    2217 
    2218     def test_coreProjectLayout(self):
    2219         """
    2220         The core tarball looks a lot like a subproject tarball, except it
    2221         doesn't include:
    2222 
    2223         - Python packages from other subprojects
    2224         - plugins from other subprojects
    2225         - scripts from other subprojects
    2226         """
    2227         indexInput, indexOutput = self.getArbitraryLoreInputAndOutput(
    2228             "8.0.0", prefix="howto/")
    2229         howtoInput, howtoOutput = self.getArbitraryLoreInputAndOutput("8.0.0")
    2230         specInput, specOutput = self.getArbitraryLoreInputAndOutput(
    2231             "8.0.0", prefix="../howto/")
    2232         upgradeInput, upgradeOutput = self.getArbitraryLoreInputAndOutput(
    2233             "8.0.0", prefix="../howto/")
    2234         tutorialInput, tutorialOutput = self.getArbitraryLoreInputAndOutput(
    2235             "8.0.0", prefix="../")
    2236 
    2237         structure = {
    2238             "LICENSE": "copyright!",
    2239             "twisted": {"__init__.py": "twisted",
    2240                         "python": {"__init__.py": "python",
    2241                                    "roots.py": "roots!"},
    2242                         "conch": {"__init__.py": "conch",
    2243                                   "unrelated.py": "import conch"},
    2244                         "plugin.py": "plugin",
    2245                         "plugins": {"twisted_web.py": "webplug",
    2246                                     "twisted_whatever.py": "include!",
    2247                                     "cred.py": "include!"},
    2248                         "topfiles": {"setup.py": "import CORE",
    2249                                      "README": "core readme"}},
    2250             "doc": {"core": {"howto": {"template.tpl": self.template,
    2251                                        "index.xhtml": howtoInput,
    2252                                        "tutorial":
    2253                                            {"index.xhtml": tutorialInput}},
    2254                              "specifications": {"index.xhtml": specInput},
    2255                              "upgrades": {"index.xhtml": upgradeInput},
    2256                              "examples": {"foo.py": "foo.py"},
    2257                              "index.xhtml": indexInput},
    2258                     "web": {"howto": {"index.xhtml": "webindex"}}},
    2259             "bin": {"twistd": "TWISTD",
    2260                     "web": {"websetroot": "websetroot"}}
    2261             }
    2262 
    2263         outStructure = {
    2264             "LICENSE": "copyright!",
    2265             "setup.py": "import CORE",
    2266             "README": "core readme",
    2267             "twisted": {"__init__.py": "twisted",
    2268                         "python": {"__init__.py": "python",
    2269                                    "roots.py": "roots!"},
    2270                         "plugin.py": "plugin",
    2271                         "plugins": {"twisted_whatever.py": "include!",
    2272                                     "cred.py": "include!"}},
    2273             "doc": {"howto": {"template.tpl": self.template,
    2274                               "index.html": howtoOutput,
    2275                               "tutorial": {"index.html": tutorialOutput}},
    2276                     "specifications": {"index.html": specOutput},
    2277                     "upgrades": {"index.html": upgradeOutput},
    2278                     "examples": {"foo.py": "foo.py"},
    2279                     "index.html": indexOutput},
    2280             "bin": {"twistd": "TWISTD"},
    2281             }
    2282 
    2283         self.createStructure(self.rootDir, structure)
    2284         outputFile = self.builder.buildCore("8.0.0")
    2285         self.assertExtractedStructure(outputFile, outStructure)
    2286 
    2287 
    2288     def test_apiBaseURL(self):
    2289         """
    2290         DistributionBuilder builds documentation with the specified
    2291         API base URL.
    2292         """
    2293         apiBaseURL = "http://%s"
    2294         builder = DistributionBuilder(self.rootDir, self.outputDir,
    2295                                       apiBaseURL=apiBaseURL)
    2296         loreInput, loreOutput = self.getArbitraryLoreInputAndOutput(
    2297             "0.3.0", apiBaseURL=apiBaseURL)
    2298         structure = {
    2299             "LICENSE": "copyright!",
    2300             "twisted": {"web": {"__init__.py": "import WEB",
    2301                                 "topfiles": {"setup.py": "import WEBINST"}}},
    2302             "doc": {"web": {"howto": {"index.xhtml": loreInput}},
    2303                     "core": {"howto": {"template.tpl": self.template}}
    2304                     }
    2305             }
    2306 
    2307         outStructure = {
    2308             "LICENSE": "copyright!",
    2309             "setup.py": "import WEBINST",
    2310             "twisted": {"web": {"__init__.py": "import WEB"}},
    2311             "doc": {"howto": {"index.html": loreOutput}}}
    2312 
    2313         self.createStructure(self.rootDir, structure)
    2314         outputFile = builder.buildSubProject("web", "0.3.0")
    2315         self.assertExtractedStructure(outputFile, outStructure)
    2316 
    2317 
    2318 
    23191388class BuildAllTarballsTest(DistributionBuilderTestBase):
    23201389    """
    23211390    Tests for L{DistributionBuilder.buildAllTarballs}.
    class ScriptTests(BuilderTestsMixin, StructureAssertingMixin, TestCase): 
    26311700        newsBuilder.buildAll = builds.append
    26321701        newsBuilder.main(["/foo/bar/baz"])
    26331702        self.assertEquals(builds, [FilePath("/foo/bar/baz")])
     1703
     1704
     1705
  • twisted/topfiles/setup.py

    diff --git a/twisted/topfiles/setup.py b/twisted/topfiles/setup.py
    index 90ba244..2876eff 100644
    a b if os.path.exists('twisted'): 
    1717from twisted import copyright
    1818from twisted.python.dist import setup, ConditionalExtension as Extension
    1919from twisted.python.dist import getPackages, getDataFiles, getScripts
    20 from twisted.python.dist import twisted_subprojects
     20from twisted.python._dist import twisted_subprojects
    2121
    2222
    2323