]> scripts.mit.edu Git - wizard.git/commitdiff
Document wizard.app, and refactor APIs.
authorEdward Z. Yang <ezyang@mit.edu>
Fri, 16 Oct 2009 23:33:35 +0000 (19:33 -0400)
committerEdward Z. Yang <ezyang@mit.edu>
Fri, 16 Oct 2009 23:33:35 +0000 (19:33 -0400)
Signed-off-by: Edward Z. Yang <ezyang@mit.edu>
doc/index.rst
doc/module/wizard.app.rst [new file with mode: 0644]
wizard/app/__init__.py
wizard/app/mediawiki.py
wizard/app/php.py
wizard/command/upgrade.py
wizard/deploy.py

index 48f70383a94666f3ad5e386774ed99f80f128a9a..ccfd0baedc92491b0d912c7a29f5375c2fc488fd 100644 (file)
@@ -62,6 +62,7 @@ Modules
     :maxdepth: 1
 
     module/wizard
+    module/wizard.app
     module/wizard.cache
     module/wizard.deploy
     module/wizard.install
diff --git a/doc/module/wizard.app.rst b/doc/module/wizard.app.rst
new file mode 100644 (file)
index 0000000..6f27427
--- /dev/null
@@ -0,0 +1,38 @@
+:mod:`wizard.app`
+=================
+
+.. automodule:: wizard.app
+
+Classes
+-------
+.. autoclass:: Application
+    :members:
+.. autoclass:: ApplicationVersion
+    :members:
+
+Functions
+---------
+.. autofunction:: applications
+.. autofunction:: expand_re
+.. autofunction:: make_extractors
+.. autofunction:: make_substitutions
+.. autofunction:: filename_regex_extractor
+.. autofunction:: filename_regex_substitution
+
+Exceptions
+----------
+.. autoexception:: Error
+.. autoexception:: NoRepositoryError
+    :members:
+.. autoexception:: DeploymentParseError
+    :members:
+.. autoexception:: NoSuchApplication
+    :members:
+.. autoexception:: UpgradeFailure
+    :members:
+.. autoexception:: UpgradeVerificationFailure
+    :members:
+.. autoexception:: BackupFailure
+    :members:
+.. autoexception:: RestoreFailure
+    :members:
index 88e9fec7f45cf8327b0a240880e2c3571a0f4496..08212ee1b3eb273b1902c95b14933fca5e47b5f0 100644 (file)
@@ -1,8 +1,27 @@
+"""
+Plumbing object model for representing applications we want to
+install.  This module does the heavy lifting, but you probably
+want to use :class:`wizard.deploy.Deployment` which is more user-friendly.
+You'll need to know how to overload the :class:`Application` class
+and use some of the functions in this module in order to specify
+new applications.
+
+.. testsetup:: *
+
+    import re
+    import shutil
+    import os
+    from wizard import deploy, util
+    from wizard.app import *
+"""
+
 import os.path
 import re
 import distutils.version
+import decorator
 
 import wizard
+from wizard import util
 
 _application_list = [
     "mediawiki", "wordpress", "joomla", "e107", "gallery2",
@@ -27,7 +46,7 @@ class Application(object):
     .. note::
         Many of these methods assume a specific working
         directory; prefer using the corresponding methods
-        in :class:`deploy.Deployment` and its subclasses.
+        in :class:`wizard.deploy.Deployment` and its subclasses.
     """
     #: String name of the application
     name = None
@@ -66,16 +85,22 @@ class Application(object):
             self.versions[version] = ApplicationVersion(distutils.version.LooseVersion(version), self)
         return self.versions[version]
     def extract(self, deployment):
-        """Extracts wizard variables from a deployment."""
+        """
+        Extracts wizard variables from a deployment.  Default implementation
+        uses :attr:`extractors`.
+        """
         result = {}
         for k,extractor in self.extractors.items():
             result[k] = extractor(deployment)
         return result
     def parametrize(self, deployment):
         """
-        Takes a generic source checkout and parametrizes
-        it according to the values of deployment.  This function
-        operates on the current working directory.
+        Takes a generic source checkout and parametrizes it according to the
+        values of ``deployment``.  This function operates on the current
+        working directory.  ``deployment`` should **not** be the same as the
+        current working directory.  Default implementation uses
+        :attr:`parametrized_files` and a simple search and replace on those
+        files.
         """
         variables = deployment.extract()
         for file in self.parametrized_files:
@@ -90,11 +115,11 @@ class Application(object):
             f.write(contents)
     def resolveConflicts(self, deployment):
         """
-        Resolves conflicted files in the current working
-        directory.  Returns whether or not all conflicted
-        files were resolved or not.  Fully resolved files are
-        added to the index, but no commit is made.  By default
-        this is a no-op and returns ``False``.
+        Resolves conflicted files in the current working directory.  Returns
+        whether or not all conflicted files were resolved or not.  Fully
+        resolved files are added to the index, but no commit is made.  By
+        default this is a no-op and returns ``False``; subclasses should
+        replace this with useful behavior.
         """
         return False
     def prepareMerge(self, deployment):
@@ -103,16 +128,16 @@ class Application(object):
         order to make a merge go more smoothly.  This is usually
         used to fix botched line-endings.  If you add new files,
         you have to 'git add' them; this is not necessary for edits.
-        By default this is a no-op.
+        By default this is a no-op; subclasses should replace this
+        with useful behavior.
         """
         pass
     def prepareConfig(self, deployment):
         """
         Takes a deployment and replaces any explicit instances
         of a configuration variable with generic ``WIZARD_*`` constants.
-        The default implementation uses :attr:`substitutions`;
-        you can override this method to provide arbitrary extra
-        behavior.
+        The default implementation uses :attr:`substitutions`, and
+        emits warnings when it encounters keys in :attr:`deprecated_keys`.
         """
         for key, subst in self.substitutions.items():
             subs = subst(deployment)
@@ -120,70 +145,87 @@ class Application(object):
                 logging.warning("No substitutions for %s" % key)
     def install(self, version, options):
         """
-        Run for 'wizard configure' (and, by proxy, 'wizard install')
-        to configure an application.  This assumes that the current
-        working directory is a deployment.  (This function does not take
-        a :class:`deploy.Deployment` as a parameter, as those operations are
-        not meaningful yet.)
+        Run for 'wizard configure' (and, by proxy, 'wizard install') to
+        configure an application.  This assumes that the current working
+        directory is a deployment.  (Unlike its kin, this function does not
+        take a :class:`wizard.deploy.Deployment` as a parameter.)  Subclasses should
+        provide an implementation.
         """
         raise NotImplemented
     def upgrade(self, deployment, version, options):
         """
         Run for 'wizard upgrade' to upgrade database schemas and other
-        non-versioned data in an application.  This assumes that
-        the current working directory is the deployment.
+        non-versioned data in an application after the filesystem has been
+        upgraded.  This assumes that the current working directory is the
+        deployment.  Subclasses should provide an implementation.
         """
         raise NotImplemented
     def backup(self, deployment, options):
         """
         Run for 'wizard backup' and upgrades to backup database schemas
         and other non-versioned data in an application.  This assumes
-        that the current working directory is the deployment.
+        that the current working directory is the deployment.  Subclasses
+        should provide an implementation.
+
+        .. note::
+            Static user files may not need to be backed up, since in
+            many applications upgrades do not modify static files.
         """
         raise NotImplemented
     def restore(self, deployment, backup, options):
         """
         Run for 'wizard restore' and failed upgrades to restore database
         and other non-versioned data to a backed up version.  This assumes
-        that the current working directory is the deployment.
+        that the current working directory is the deployment.  Subclasses
+        should provide an implementation.
         """
         raise NotImplemented
     def detectVersion(self, deployment):
         """
         Checks source files to determine the version manually.  This assumes
-        that the current working directory is the deployment.
+        that the current working directory is the deployment.  Subclasses
+        should provide an implementation.
         """
-        return None
+        raise NotImplemented
     def checkWeb(self, deployment, output=None):
         """
         Checks if the autoinstall is viewable from the web.  To get
         the HTML source that was retrieved, pass a variable containing
         an empty list to ``output``; it will be mutated to have its
-        first element be the output.
+        first element be the output.  Subclasses should provide an
+        implementation.
+
+        .. note::
+            Finding a reasonable heuristic that works across skinning
+            choices can be difficult.  We've had reasonable success
+            searching for metadata.  Be sure that the standard error
+            page does not contain the features you search for.  Try
+            not to depend on pages that are not the main page.
         """
         raise NotImplemented
     def checkConfig(self, deployment):
         """
         Checks whether or not an autoinstall has been configured/installed
         for use.  Assumes that the current working directory is the deployment.
+        Subclasses should provide an implementation.
         """
         raise NotImplemented
     @property
     def extractors(self):
         """
         Dictionary of variable names to extractor functions.  These functions
-        take a :class:`deploy.Deployment` as an argument and return the value of
+        take a :class:`wizard.deploy.Deployment` as an argument and return the value of
         the variable, or ``None`` if it could not be found.
-        See also :func:`wizard.app.filename_regex_extractor`.
+        See also :func:`filename_regex_extractor`.
         """
         return {}
     @property
     def substitutions(self):
         """
         Dictionary of variable names to substitution functions.  These functions
-        take a :class:`deploy.Deployment` as an argument and modify the deployment such
+        take a :class:`wizard.deploy.Deployment` as an argument and modify the deployment such
         that an explicit instance of the variable is released with the generic
-        WIZARD_* constant.  See also :func:`wizard.app.filename_regex_substitution`.
+        WIZARD_* constant.  See also :func:`filename_regex_substitution`.
         """
         return {}
     @staticmethod
@@ -237,7 +279,7 @@ class ApplicationVersion(object):
 
         Use this only for cases when speed is of primary importance;
         the data in version is unreliable and when possible, you should
-        prefer directly instantiating a :class:`deploy.Deployment` and having it query
+        prefer directly instantiating a :class:`wizard.deploy.Deployment` and having it query
         the autoinstall itself for information.
 
         The `value` to parse will vary.  For old style installs, it
@@ -275,83 +317,132 @@ class ApplicationVersion(object):
             raise NoSuchApplication(app)
 
 def expand_re(val):
+    """
+    Takes a tree of values (implement using nested lists) and
+    transforms them into regular expressions.
+
+        >>> expand_re('*')
+        '\\\\*'
+        >>> expand_re(['a', 'b'])
+        '(?:a|b)'
+        >>> expand_re(['*', ['b', 'c']])
+        '(?:\\\\*|(?:b|c))'
+    """
     if isinstance(val, str):
         return re.escape(val)
     else:
         return '(?:' + '|'.join(map(expand_re, val)) + ')'
 
-def filename_regex_extractor(f):
+def make_extractors(seed):
+    """
+    Take a dictionary of ``key``s to ``(file, regex)`` tuples and convert them into
+    extractor functions (which take a :class:`wizard.deploy.Deployment`
+    and return the value of the second subpattern of ``regex`` when matched
+    with the contents of ``file``).
+    """
+    return util.dictmap(lambda a: filename_regex_extractor(*a), seed)
+
+def make_substitutions(seed):
+    """
+    Take a dictionary of ``key``s to ``(file, regex)`` tuples and convert them into substitution
+    functions (which take a :class:`wizard.deploy.Deployment`, replace the second subpattern
+    of ``regex`` with ``key`` in ``file``, and returns the number of substitutions made.)
+    """
+    return util.dictkmap(lambda k, v: filename_regex_substitution(k, *v), seed)
+
+# The following two functions are *highly* functional, and I recommend
+# not touching them unless you know what you're doing.
+
+def filename_regex_extractor(file, regex):
     """
-    This is a decorator to apply to functions that take a name and return
-    (filename, RegexObject) tuples.  It converts it into a function
-    that takes a name and returns another function (the actual extractor)
-    which takes a deployment and returns the value of the extracted variable.
+    .. highlight:: haskell
 
-    The regular expression requires a very specific form, essentially ()()()
-    (with the second subgroup being the value we care about), so that we can
-    reuse the regex for other things.
+    Given a relative file name ``file``, a regular expression ``regex``, and a
+    :class:`wizard.deploy.Deployment` extracts a value out of the file in that
+    deployment.  This function is curried, so you pass just ``file`` and
+    ``regex``, and then pass ``deployment`` to the resulting function.
 
     Its Haskell-style type signature would be::
 
-        (String -> (Filename, Regex)) -> (String -> (Deployment -> String))
+        Filename -> Regex -> (Deployment -> String)
+
+    The regular expression requires a very specific form, essentially ``()()()``
+    (with the second subgroup being the value to extract).  These enables
+    the regular expression to be used equivalently with filename
 
-    For convenience purposes, we also accept [Filename], in which case
+    .. highlight:: python
+
+    For convenience purposes, we also accept ``[Filename]``, in which case
     we use the first entry (index 0).  Passing an empty list is invalid.
+
+        >>> open("test-settings.extractor.ini", "w").write("config_var = 3\\n")
+        >>> f = filename_regex_extractor('test-settings.extractor.ini', re.compile('^(config_var\s*=\s*)(.*)()$'))
+        >>> f(deploy.Deployment("."))
+        '3'
+        >>> os.unlink("test-settings.extractor.ini")
+
+    .. note::
+        The first application of ``regex`` and ``file`` is normally performed
+        at compile-time inside a submodule; the second application is
+        performed at runtime.
     """
-    def g(var):
-        file, regex = f(var)
-        if not isinstance(file, str):
-            file = file[0]
-        def h(deployment):
-            try:
-                contents = deployment.read(file) # cached
-            except IOError:
-                return None
-            match = regex.search(contents)
-            if not match: return None
-            # assumes that the second match is the one we want.
-            return match.group(2)
-        return h
-    return g
-
-def filename_regex_substitution(f):
+    if not isinstance(file, str):
+        file = file[0]
+    def h(deployment):
+        try:
+            contents = deployment.read(file) # cached
+        except IOError:
+            return None
+        match = regex.search(contents)
+        if not match: return None
+        # assumes that the second match is the one we want.
+        return match.group(2)
+    return h
+
+def filename_regex_substitution(key, files, regex):
     """
-    This is a decorator to apply to functions that take a name and return
-    (filename, RegexObject) tuples.  It converts it into a function
-    that takes a name and returns another function (that does substitution)
-    which takes a deployment and modifies its files to replace explicit
-    values with their generic WIZARD_* equivalents.  The final function returns
-    the number of replacements made.
+    .. highlight:: haskell
 
-    The regular expression requires a very specific form, essentially ()()()
-    (with the second subgroup being the value to be replaced).
+    Given a Wizard ``key`` (``WIZARD_*``), a list of ``files``, a
+    regular expression ``regex``, and a :class:`wizard.deploy.Deployment`
+    performs a substitution of the second subpattern of ``regex``
+    with ``key``.  Returns the number of replacements made.  This function
+    is curried, so you pass just ``key``, ``files`` and ``regex``, and
+    then pass ``deployment`` to the resulting function.
 
     Its Haskell-style type signature would be::
 
-        (String -> ([Filename], Regex)) -> (String -> (Deployment -> IO Int))
+        Key -> ([File], Regex) -> (Deployment -> IO Int)
 
-    For convenience purposes, we also accept Filename, in which case it is treated
+    .. highlight:: python
+
+    For convenience purposes, we also accept ``Filename``, in which case it is treated
     as a single item list.
+
+        >>> open("test-settings.substitution.ini", "w").write("config_var = 3")
+        >>> f = filename_regex_substitution('WIZARD_KEY', 'test-settings.substitution.ini', re.compile('^(config_var\s*=\s*)(.*)()$'))
+        >>> f(deploy.Deployment("."))
+        1
+        >>> print open("test-settings.substitution.ini", "r").read()
+        config_var = WIZARD_KEY
+        >>> os.unlink("test-settings.substitution.ini")
     """
-    def g(key, var):
-        files, regex = f(var)
-        if isinstance(files, str):
-            files = (files,)
-        def h(deployment):
-            base = deployment.location
-            subs = 0
-            for file in files:
-                file = os.path.join(base, file)
-                try:
-                    contents = open(file, "r").read()
-                    contents, n = regex.subn("\\1" + key + "\\3", contents)
-                    subs += n
-                    open(file, "w").write(contents)
-                except IOError:
-                    pass
-            return subs
-        return h
-    return g
+    if isinstance(files, str):
+        files = (files,)
+    def h(deployment):
+        base = deployment.location
+        subs = 0
+        for file in files:
+            file = os.path.join(base, file)
+            try:
+                contents = open(file, "r").read()
+                contents, n = regex.subn("\\1" + key + "\\3", contents)
+                subs += n
+                open(file, "w").write(contents)
+            except IOError:
+                pass
+        return subs
+    return h
 
 class Error(wizard.Error):
     """Generic error class for this module."""
index d7dc035036e7d5e7b8f102cb2067140881fbd207..a959fa49410ce594cdc58d833d3664369dad1599 100644 (file)
@@ -12,9 +12,7 @@ from wizard.app import php
 def make_filename_regex(var):
     return 'LocalSettings.php', re.compile('^(\$' + app.expand_re(var) + r'''\s*=\s*)(.*)(;)''', re.M)
 
-make_extractor = app.filename_regex_extractor(make_filename_regex)
-make_substitution = app.filename_regex_substitution(make_filename_regex)
-seed = {
+seed = util.dictmap(make_filename_regex, {
         'WIZARD_IP': 'IP', # obsolete, remove after we're done
         'WIZARD_SITENAME': 'wgSitename',
         'WIZARD_SCRIPTPATH': 'wgScriptPath',
@@ -24,7 +22,7 @@ seed = {
         'WIZARD_DBUSER': 'wgDBuser',
         'WIZARD_DBPASSWORD': 'wgDBpassword',
         'WIZARD_SECRETKEY': ('wgSecretKey', 'wgProxyKey'),
-        }
+        })
 
 resolutions = {
 'LocalSettings.php': [
@@ -127,13 +125,13 @@ class Application(app.Application):
     @property
     def extractors(self):
         if not self._extractors:
-            self._extractors = util.dictmap(make_extractor, seed)
+            self._extractors = app.make_extractors(seed)
             self._extractors.update(php.extractors)
         return self._extractors
     @property
     def substitutions(self):
         if not self._substitutions:
-            self._substitutions = util.dictkmap(make_substitution, seed)
+            self._substitutions = app.make_substitutions(seed)
             self._substitutions.update(php.substitutions)
         return self._substitutions
     @property
index fdf201248c2d4d3ee6742dfbb19c3af18fe6b658..d2ac57413b05ac077bb4cecd7da7ace9840dbe60 100644 (file)
@@ -5,14 +5,12 @@ from wizard import app, util
 def make_filename_regex(var):
     return 'php.ini', re.compile('^(' + app.expand_re(var) + r'\s*=\s*)(.*)()$', re.M)
 
-make_extractor = app.filename_regex_extractor(make_filename_regex)
-make_substitution = app.filename_regex_substitution(make_filename_regex)
-seed = {
+seed = util.dictmap(make_filename_regex, {
         'WIZARD_SESSIONNAME': 'session.name',
         'WIZARD_TMPDIR': ('upload_tmp_dir', 'session.save_path'),
-        }
+        })
 
-extractors = util.dictmap(make_extractor, seed)
-substitutions = util.dictkmap(make_substitution, seed)
+extractors = app.make_extractors(seed)
+substitutions = app.make_substitutions(seed)
 deprecated_keys = set([])
 
index 4c2632df64c43c94ba3c447d2942c29b927aa277..06be3db1eb23548d64f0d57fce5d5042f364f502 100644 (file)
@@ -89,7 +89,7 @@ def main(argv, baton):
                 open(".git/WIZARD_SIZE", "w").write(str(scripts.get_disk_usage()))
                 if options.log_file:
                     open(".git/WIZARD_LOG_FILE", "w").write(options.log_file)
-                perform_merge(sh, repo, wc, version, use_shm, kib_limit and kib_limit - kib_usage or None)
+                perform_merge(sh, repo, d, wc, version, use_shm, kib_limit and kib_limit - kib_usage or None)
         # variables: version, user_commit, next_commit, temp_wc_dir
         with util.ChangeDirectory(temp_wc_dir):
             try:
@@ -202,7 +202,7 @@ def perform_tmp_clone(sh, use_shm):
     sh.call("git", "clone", "-q", "--shared", ".", temp_wc_dir)
     return temp_dir, temp_wc_dir
 
-def perform_merge(sh, repo, wc, version, use_shm, kib_avail):
+def perform_merge(sh, repo, d, wc, version, use_shm, kib_avail):
     # Note: avail_quota == None means unlimited
     # naive merge algorithm:
     # sh.call("git", "merge", "-m", message, "scripts/master")
@@ -210,7 +210,7 @@ def perform_merge(sh, repo, wc, version, use_shm, kib_avail):
     def make_virtual_commit(tag, parents = []):
         """WARNING: Changes state of Git repository"""
         sh.call("git", "checkout", "-q", tag, "--")
-        wc.parametrize()
+        wc.parametrize(d)
         for file in wc.application.parametrized_files:
             try:
                 sh.call("git", "add", "--", file)
index 005a60218e2593ca5437bb3c9edfd116be5a08a3..ffccf47f11726312a634eb35223abfb990f42ce2 100644 (file)
@@ -332,13 +332,14 @@ class WorkingCopy(Deployment):
     deployment.  More operations are permitted on these copies.
     """
     @chdir_to_location
-    def parametrize(self):
+    def parametrize(self, deployment):
         """
         Edits files in ``dir`` to replace WIZARD_* variables with literal
-        instances.  This is used for constructing virtual merge bases, and
-        as such dir will generally not equal :attr:`location`.
+        instances based on ``deployment``.  This is used for constructing
+        virtual merge bases, and as such ``deployment`` will generally not
+        equal ``self``.
         """
-        return self.application.parametrize(self)
+        return self.application.parametrize(self, deployment)
     @chdir_to_location
     def prepareConfig(self):
         """