Ticket #4138: sdist-support-4138.3.patch

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

New patch against current Twisted trunk, without regressions.

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

    diff --git a/twisted/topfiles/setup.py b/twisted/topfiles/setup.py
    index 90ba244..2876eff 100644
    a b if os.path.exists('twisted'): 
    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