[Zope-CVS] SVN: zpkgtools/trunk/ New extensibility features

Fred L. Drake, Jr. fdrake at gmail.com
Wed Aug 25 00:45:59 EDT 2004


Log message for revision 27257:
  New extensibility features
  
  - Allow additional support code to be loaded into the distribution's
    Support/ directory.  This can be used in conjunction with the next
    feature:
  
  - Allow use of an alternate distribution class.  This allows a new
    class to be specified to be passed into distutils.core.setup(),
    allowing more customization of distutils.
  
  Minimal documentation is included, along with functional tests of the
  new behavior.
  


Changed:
  U   zpkgtools/trunk/doc/zpkg.txt
  U   zpkgtools/trunk/zpkgtools/app.py
  U   zpkgtools/trunk/zpkgtools/tests/test_app.py


-=-
Modified: zpkgtools/trunk/doc/zpkg.txt
===================================================================
--- zpkgtools/trunk/doc/zpkg.txt	2004-08-24 22:06:52 UTC (rev 27256)
+++ zpkgtools/trunk/doc/zpkg.txt	2004-08-25 04:45:57 UTC (rev 27257)
@@ -34,6 +34,11 @@
   Load the configuration file `FILE` instead of the default
   configuration file.  `FILE` must exist.
 
+--distribution CLASS
+  Set the distribution class to be used by distutils.  By default, the
+  zpkgsetup.dist.ZPkgDistribution class is used, but this allows an
+  alternate class to be used instead.
+
 -f
   Don't load any configuration file, not even the default.
 
@@ -62,6 +67,13 @@
   This can be used to generate smaller distributions when the
   ``zpkgsetup`` package is known to be available on target systems.
 
+--support RESOURCE
+  Include an additional resource in the Support/ directory of the
+  distribution, if the Support/ directory is included at all.  The
+  additional resource should normally be a top-level Python package.
+  This can be used to provide additional software to be used when
+  distutils is running for the distributed package.
+
 -v VERSION
   Set the version number of the release to `VERSION`.
 

Modified: zpkgtools/trunk/zpkgtools/app.py
===================================================================
--- zpkgtools/trunk/zpkgtools/app.py	2004-08-24 22:06:52 UTC (rev 27256)
+++ zpkgtools/trunk/zpkgtools/app.py	2004-08-25 04:45:57 UTC (rev 27257)
@@ -35,6 +35,11 @@
 from zpkgtools import runlog
 
 
+DEFAULT_SUPPORT_PACKAGES = [
+    ("zpkgsetup", ("svn://svn.zope.org/repos/main/zpkgtools/trunk/zpkgsetup")),
+    ]
+
+
 class Application:
     """Application state and logic for **zpkg**."""
 
@@ -78,6 +83,7 @@
             options.release_name = self.resource
         # Create a new directory for all temporary files to go in:
         self.tmpdir = tempfile.mkdtemp(prefix=options.program + "-")
+        self.old_tmpdir = tempfile.tempdir
         tempfile.tempdir = self.tmpdir
         if options.revision_tag:
             self.loader = loader.Loader(tag=options.revision_tag)
@@ -94,6 +100,9 @@
         self.target_file = self.target_name + ".tgz"
         self.destination = os.path.join(self.tmpdir, self.target_name)
         os.mkdir(self.destination)
+        self.support_packages = DEFAULT_SUPPORT_PACKAGES[:]
+        self.support_packages.extend(
+            [(pkg, None) for pkg in options.support_packages])
 
     def build_distribution(self):
         """Create the distribution tree.
@@ -104,6 +113,7 @@
         dep_sources = {} 
         top = self.get_component(self.resource, self.resource_url)
         top.write_package(self.destination)
+        distclass = self.options.distribution_class
         if self.options.collect:
             depsdir = os.path.join(self.destination, "Dependencies")
             first = True
@@ -136,15 +146,18 @@
                 destination = os.path.join(depsdir, fullname)
                 self.add_manifest(destination)
                 component.write_package(destination)
-                component.write_setup_py(pathparts=["..", ".."])
+                component.write_setup_py(pathparts=["..", ".."],
+                                         distclass=distclass)
                 component.write_setup_cfg()
                 self.add_headers(component)
         if self.options.application:
             top.write_setup_py(filename="install.py",
-                               version=self.options.version)
+                               version=self.options.version,
+                               distclass=distclass)
             self.write_application_support(top)
         else:
-            top.write_setup_py(version=self.options.version)
+            top.write_setup_py(version=self.options.version,
+                               distclass=distclass)
         top.write_setup_cfg()
 
     def get_component(self, resource, location):
@@ -222,19 +235,20 @@
         directory, but they won't be added to the set of packages that
         will be installed by the resulting distribution.
         """
-        old_loader = self.loader
-        if self.options.revision_tag:
+        cleanup = False
+        if self.options.revision_tag and self.options.revision_tag != "HEAD":
             # we really don't want the tagged version of the support code
-            self.loader = loader.Loader()
+            old_loader = self.loader
+            self.loader = loader.Loader("HEAD")
+            cleanup = True
         supportdest = os.path.join(self.destination, "Support")
         os.mkdir(supportdest)
         self.add_manifest(supportdest)
-        self.include_support_package(
-            "zpkgsetup", ("svn://svn.zope.org/repos/main/zpkgtools/trunk/"
-                          "zpkgsetup"))
-        if self.options.revision_tag:
+        for name, fallback_url in self.support_packages:
+            self.include_support_package(name, fallback_url)
+        if cleanup:
             self.loader.cleanup()
-        self.loader = old_loader
+            self.loader = old_loader
         source = os.path.join(zpkgtools.__path__[0], "support")
         dest = os.path.join(self.destination, "Support")
         files = os.listdir(source)
@@ -263,6 +277,10 @@
             url = self.locations[name]
         else:
             url = fallback
+            if not url:
+                self.logger.warning("resource %s not configured;"
+                                    " no fallback URL" % name)
+                return
             self.logger.info("resource %s not configured;"
                              " using fallback URL" % name)
         if source is None:
@@ -300,6 +318,8 @@
     def cleanup(self):
         """Remove all temporary data storage."""
         shutil.rmtree(self.tmpdir)
+        if self.tmpdir == tempfile.tempdir:
+            tempfile.tempdir = self.old_tmpdir
 
     def run(self):
         """Run the application, using the other methods of the
@@ -452,7 +472,8 @@
         f.write("optimize = 1\n")
         f.close()
 
-    def write_setup_py(self, filename="setup.py", version=None, pathparts=[]):
+    def write_setup_py(self, filename="setup.py", version=None, pathparts=[],
+                       distclass=None):
         setup_py = os.path.join(self.destination, filename)
         self.ip.add_output(setup_py)
         f = open(setup_py, "w")
@@ -462,7 +483,11 @@
             extrapath = ""
         print >>f, SETUP_HEADER % extrapath
         print >>f, "context = zpkgsetup.setup.SetupContext("
-        print >>f, "    %r, %r, __file__)" % (self.name, version)
+        if distclass:
+            print >>f, "    %r, %r, __file__," % (self.name, version)
+            print >>f, "    %r)" % distclass
+        else:
+            print >>f, "    %r, %r, __file__)" % (self.name, version)
         print >>f
         print >>f, "context.initialize()"
         print >>f, "context.setup()"
@@ -568,6 +593,13 @@
     parser.add_option(
         "-v", dest="version",
         help="version label for the new distribution")
+    parser.add_option(
+        "--support", dest="support_packages", action="append",
+        default=[],
+        help="name additional support package resource", metavar="RESOURCE")
+    parser.add_option(
+        "--distribution", dest="distribution_class",
+        help="name of the distribution class", metavar="CLASS")
 
     options, args = parser.parse_args(argv[1:])
     if len(args) != 1:

Modified: zpkgtools/trunk/zpkgtools/tests/test_app.py
===================================================================
--- zpkgtools/trunk/zpkgtools/tests/test_app.py	2004-08-24 22:06:52 UTC (rev 27256)
+++ zpkgtools/trunk/zpkgtools/tests/test_app.py	2004-08-25 04:45:57 UTC (rev 27257)
@@ -21,6 +21,8 @@
 
 from StringIO import StringIO
 
+import zpkgsetup
+
 from zpkgsetup import package
 from zpkgsetup import publication
 from zpkgsetup.tests import tempfileapi as tempfile
@@ -234,7 +236,26 @@
         self.failUnless(options.application)
         self.failUnless(options.collect)
 
+    def test_distribution_class(self):
+        options = self.parse_args([])
+        self.failIf(options.distribution_class)
+        # long arg:
+        options = self.parse_args(["--distribution", "pkg.mod.Cls"])
+        self.assertEqual(options.distribution_class, "pkg.mod.Cls")
+        options = self.parse_args(["--distribution=pkg.mod.Cls"])
+        self.assertEqual(options.distribution_class, "pkg.mod.Cls")
 
+    def test_support_packages(self):
+        options = self.parse_args([])
+        self.assertEqual(options.support_packages, [])
+        # one package:
+        options = self.parse_args(["--support", "pkg"])
+        self.assertEqual(options.support_packages, ["pkg"])
+        # two packages
+        options = self.parse_args(["--support", "pkg1", "--support=pkg2"])
+        self.assertEqual(options.support_packages, ["pkg1", "pkg2"])
+
+
 class ComponentTestCase(unittest.TestCase):
 
     def setUp(self):
@@ -329,6 +350,136 @@
             shutil.rmtree(dest)
 
 
+class BuilderApplicationTestCase(unittest.TestCase):
+    """Tests of the BuilderApplication object.
+
+    These are pretty much functional tests since they start with a
+    command line, though they don't run in an external process.
+
+    """
+
+    def setUp(self):
+        self.app = None
+        self.extra_files = []
+
+    def tearDown(self):
+        if self.app is not None:
+            self.app.delayed_cleanup()
+            self.app = None
+        for fn in self.extra_files:
+            os.unlink(fn)
+
+    def createApplication(self, args):
+        options = app.parse_args([CMD] + args)
+        self.app = DelayedCleanupBuilderApplication(options)
+        return self.app
+
+    def createPackageMap(self):
+        input = os.path.join(os.path.dirname(__file__), "input")
+        input = os.path.abspath(input)
+        package_map = os.path.join(input, "tmp-packages.map")
+        shutil.copy(os.path.join(input, "packages.map"), package_map)
+        self.extra_files.append(package_map)
+        zpkgsetup_path = os.path.abspath(zpkgsetup.__path__[0])
+        zpkgsetup_path = "file://" + urllib.pathname2url(zpkgsetup_path)
+        f = open(package_map, "a")
+        print >>f
+        print >>f, "zpkgsetup", zpkgsetup_path
+        f.close()
+        # convert package_map to URL so relative names are resolved properly
+        return "file://" + urllib.pathname2url(package_map)
+
+    def test_adding_extra_support_code(self):
+        package_map = self.createPackageMap()
+        app = self.createApplication(
+            ["-f", "-m", package_map, "--support", "package", "package"])
+        app.run()
+        # make sure the extra support code is actually present:
+        support_dir = os.path.join(app.destination, "Support")
+        package_dir = os.path.join(support_dir, "package")
+        self.assert_(os.path.isdir(package_dir))
+        self.assert_(isfile(package_dir, "__init__.py"))
+
+    def test_alternate_distclass(self):
+        # create the distribution tree:
+        package_map = self.createPackageMap()
+        app = self.createApplication(
+            ["-f", "-m", package_map,
+             "--distribution", "mysupport.MyDistribution", "package"])
+        app.run()
+
+        # Add the example distribution class to the built distro:
+        mysupport = os.path.join(app.destination, "Support", "mysupport.py")
+        f = open(mysupport, "w")
+        print >>f, "import distutils.dist"
+        print >>f, "class MyDistribution(distutils.dist.Distribution):"
+        print >>f, "    this_is_mine = 42"
+        f.close()
+
+        # Run the setup.py from the distro and check that we got the
+        # specialized distribution class.  This is really tricky since
+        # we're running what's intended to be an script run in it's
+        # own process inside our process, so we're trying to make
+        # __main__ really look like it's running a script.  And then
+        # we have to restore it.  There's something general lurking
+        # here....
+        import __builtin__
+        import __main__
+        old_stdout = sys.stdout
+        old_stderr = sys.stderr
+        old_sys_argv = sys.argv[:]
+        old_sys_path = sys.path[:]
+        old_main_dict = __main__.__dict__.copy()
+        old_cwd = os.getcwd()
+        setup_script = os.path.join(app.destination, "setup.py")
+        sio = StringIO()
+        d = {"__file__": setup_script,
+             "__name__": "__main__",
+             "__doc__": None,
+             "__builtins__": __builtin__.__dict__}
+        try:
+            sys.stdout = sys.stderr = sio
+            sys.path.insert(0, os.path.join(app.destination, "Support"))
+            sys.argv[:] = [setup_script, "-q", "-n", "--name"]
+            import mysupport
+            __main__.__dict__.clear()
+            __main__.__dict__.update(d)
+            os.chdir(app.destination)
+
+            execfile(setup_script, __main__.__dict__)
+            context = __main__.context
+
+        finally:
+            os.chdir(old_cwd)
+            sys.stdout = old_stdout
+            sys.stderr = old_stderr
+            sys.argv[:] = old_sys_argv
+            sys.path[:] = old_sys_path
+            __main__.__dict__.clear()
+            __main__.__dict__.update(old_main_dict)
+
+        # This makes sure that context.get_distribution_class()
+        # returns the expected class; the actual instance of the class
+        # that's used isn't available to us here, or we'd look at the
+        # instance instead.
+        #
+        cls = context.get_distribution_class()
+        self.assert_(cls is mysupport.MyDistribution)
+
+
+class DelayedCleanupBuilderApplication(app.BuilderApplication):
+
+    def create_tarball(self):
+        pass
+
+    def cleanup(self):
+        pass
+
+    def delayed_cleanup(self):
+        app.BuilderApplication.cleanup(self)
+
+
+
 def isfile(path, *args):
     if args:
         path = os.path.join(path, *args)
@@ -338,6 +489,7 @@
 def test_suite():
     suite = unittest.makeSuite(CommandLineTestCase)
     suite.addTest(unittest.makeSuite(ComponentTestCase))
+    suite.addTest(unittest.makeSuite(BuilderApplicationTestCase))
     return suite
 
 if __name__ == "__main__":



More information about the Zope-CVS mailing list