moinsetup

moinsetup.py

24:fcb96dada0ea
2010-11-19 Paul Boddie Introduced configuration file usage instead of having to specify everything as command line arguments. Fixed MoinMoin 1.9 CGI usage enforcement in moin.cgi.
     1 #!/usr/bin/env python     2      3 from os.path import abspath, exists, extsep, isdir, join, normpath, split     4 from getpass import getpass     5 from glob import glob     6 import os     7 import sys     8 import shutil     9 import re    10     11 # Regular expressions for editing MoinMoin scripts and configuration files.    12     13 def compile_definition(name):    14     return re.compile(r"^(\s*)#*\s*(%s =).*$" % name, re.MULTILINE)    15     16 moin_cgi_prefix          = re.compile("^#sys\.path\.insert\(0, 'PREFIX.*$", re.MULTILINE)    17 moin_cgi_wikiconfig      = re.compile("^#sys\.path\.insert\(0, '/path/to/wikiconfigdir.*$", re.MULTILINE)    18 moin_cgi_properties      = compile_definition("properties")    19 moin_cgi_fix_script_name = compile_definition("fix_script_name")    20 moin_cgi_force_cgi       = re.compile("^#(os.environ\['FCGI_FORCE_CGI'\].*)$", re.MULTILINE)    21     22 css_import_stylesheet    = re.compile("(\s*@import\s+[\"'])(.*?)([\"']\s*;)")    23     24 # Templates for Apache site definitions.    25     26 apache_site = """    27 ScriptAlias %(url_path)s "%(web_app_dir)s/moin.cgi"    28 """    29     30 apache_site_extra_moin18 = """    31 Alias %(static_url_path)s "%(htdocs_dir)s/"    32 """    33     34 # Limited hosting .htaccess definitions require the following settings to be    35 # configured in the main Apache configuration files:    36 #    37 # Options ExecCGI FollowSymLinks Indexes SymLinksIfOwnerMatch    38 # AllowOverride FileInfo Limit    39 # AddHandler cgi-script .cgi    40     41 apache_htaccess_combined_mod_rewrite = """    42 DirectoryIndex moin.cgi    43 RewriteEngine On    44 RewriteBase %(url_path)s    45 RewriteCond %%{REQUEST_FILENAME} !-f    46 RewriteCond %%{REQUEST_FILENAME} !-d    47 RewriteRule ^(.*) moin.cgi/$1 [PT,L,QSA]    48 """    49     50 # Utility functions.    51     52 def readfile(filename):    53     f = open(filename)    54     try:    55         return f.read()    56     finally:    57         f.close()    58     59 def writefile(filename, s):    60     f = open(filename, "w")    61     try:    62         f.write(s)    63     finally:    64         f.close()    65     66 def status(message):    67     print message    68     69 note = status    70 error = status    71     72 def format(s, indent):    73     return re.sub("\n\s+", "\n" + " " * indent, s)    74     75 # Classes.    76     77 class Configuration:    78     79     "A class representing the configuration."    80     81     special_names = ["site_name"]    82     83     def __init__(self, filename):    84         self.content = readfile(filename)    85         self.filename = filename    86     87     def get_pattern(self, name):    88     89         # Make underscores optional for certain names.    90     91         if name in self.special_names:    92             name = name.replace("_", "_?")    93     94         return compile_definition(name)    95     96     def set(self, name, value, count=None, raw=0):    97     98         """    99         Set the configuration parameter having the given 'name' with the given   100         'value', limiting the number of appropriately named parameters changed   101         to 'count', if specified.   102    103         If the configuration parameter of the given 'name' does not exist,   104         insert such a parameter at the end of the file.   105    106         If the optional 'raw' parameter is specified and set to a true value,   107         the provided 'value' is inserted directly into the configuration file.   108         """   109    110         if not self.replace(name, value, count, raw):   111             self.insert(name, value, raw)   112    113     def replace(self, name, value, count=None, raw=0):   114    115         """   116         Replace configuration parameters having the given 'name' with the given   117         'value', limiting the number of appropriately named parameters changed   118         to 'count', if specified.   119    120         If the optional 'raw' parameter is specified and set to a true value,   121         the provided 'value' is inserted directly into the configuration file.   122    123         Return the number of substitutions made.   124         """   125    126         if raw:   127             substitution = r"\1\2 %s" % value   128         else:   129             substitution = r"\1\2 %r" % value   130    131         pattern = self.get_pattern(name)   132    133         if count is None:   134             self.content, n = pattern.subn(substitution, self.content)   135         else:   136             self.content, n = pattern.subn(substitution, self.content, count=count)   137    138         return n   139    140     def insert(self, name, value, raw=0):   141    142         """   143         Insert the configuration parameter having the given 'name' and 'value'.   144    145         If the optional 'raw' parameter is specified and set to a true value,   146         the provided 'value' is inserted directly into the configuration file.   147         """   148    149         if raw:   150             insertion = "\n    %s = %s\n"   151         else:   152             insertion = "\n    %s = %r\n"   153    154         self.content += insertion % (name, value)   155    156     def close(self):   157    158         "Close the file, writing the content."   159    160         writefile(self.filename, self.content)   161    162 class Installation:   163    164     "A class for installing and initialising MoinMoin."   165    166     method_names = (   167         "setup",   168         "setup_wiki",   169         "install_moin",   170         "install_data",   171         "configure_moin",   172         "edit_moin_script",   173         "edit_moin_web_script",   174         "add_superuser",   175         "make_site_files",   176         "make_post_install_script",   177         "reconfigure_moin",   178         "install_theme",   179         "install_extension_package",   180         "install_plugins",   181         "install_actions",   182         "install_macros",   183         "install_theme_resources",   184         "edit_theme_stylesheet"   185         )   186    187     # NOTE: Need to detect Web server user.   188    189     web_user = "www-data"   190     web_group = "www-data"   191    192     # MoinMoin resources.   193    194     theme_master = "modernized"   195     extra_theme_css_files = ["SlideShow.css"]   196    197     def __init__(self, moin_distribution, prefix, web_app_dir, web_site_dir,   198         common_dir, url_path, superuser, site_name, front_page_name,   199         theme_default=None):   200    201         """   202         Initialise a Wiki installation using the following:   203    204           * moin_distribution - the directory containing a MoinMoin source   205                                 distribution   206           * prefix            - the installation prefix (equivalent to /usr)   207           * web_app_dir       - the directory where Web applications and scripts   208                                 reside (such as /home/www-user/cgi-bin)   209           * web_site_dir      - the directory where Web site definitions reside   210                                 (such as /etc/apache2/sites-available)   211           * common_dir        - the directory where the Wiki configuration,   212                                 resources and instance will reside (such as   213                                 /home/www-user/mywiki)   214           * url_path          - the URL path at which the Wiki will be made   215                                 available (such as / or /mywiki)   216           * superuser         - the name of the site's superuser (such as   217                                 "AdminUser")   218           * site_name         - the name of the site (such as "My Wiki")   219           * front_page_name   - the front page name for the site (such as   220                                 "FrontPage" or a specific name for the site)   221           * theme_default     - optional: the default theme (such as modern)   222         """   223    224         self.moin_distribution = moin_distribution   225         self.superuser = superuser   226         self.site_name = site_name   227         self.front_page_name = front_page_name   228         self.theme_default = theme_default   229    230         # NOTE: Support the detection of the Apache sites directory.   231    232         self.prefix, self.web_app_dir, self.web_site_dir, self.common_dir = \   233             map(abspath, (prefix, web_app_dir, web_site_dir, common_dir))   234    235         # Strip any trailing "/" from the URL path.   236    237         if url_path != "/" and url_path.endswith("/"):   238             self.url_path = url_path[:-1]   239         else:   240             self.url_path = url_path   241    242         # Define and create specific directories.   243    244         self.conf_dir = join(self.common_dir, "conf")   245         self.instance_dir = join(self.common_dir, "wikidata")   246    247         # Define useful directories.   248    249         self.prefix_site_packages = join(self.prefix, "lib", "python%s.%s" % sys.version_info[:2], "site-packages")   250    251         # Find the version.   252    253         self.moin_version = self.get_moin_version()   254    255         # The static resources reside in different locations depending on the   256         # version of MoinMoin. Moreover, these resources may end up in a   257         # published directory for 1.8 installations where the Web server cannot   258         # be instructed to fetch the content from outside certain designated   259         # locations.   260    261         # 1.9: moin/lib/python2.x/site-packages/MoinMoin/web/static/htdocs   262    263         if self.moin_version.startswith("1.9"):   264             self.htdocs_dir = self.htdocs_dir_source = join(self.prefix_site_packages, "MoinMoin", "web", "static", "htdocs")   265    266         # 1.8: moin/share/moin/htdocs (optionally copied to a Web directory)   267    268         else:   269             self.htdocs_dir_source = join(self.instance_dir, "share", "moin", "htdocs")   270    271             if self.limited_hosting():   272                 self.htdocs_dir = join(self.web_app_dir, self.get_static_identifier())   273             else:   274                 self.htdocs_dir = self.htdocs_dir_source   275    276     def get_moin_version(self):   277    278         "Inspect the MoinMoin package information, returning the version."   279    280         this_dir = os.getcwd()   281         os.chdir(self.moin_distribution)   282    283         try:   284             try:   285                 f = open("PKG-INFO")   286                 try:   287                     for line in f.xreadlines():   288                         columns = line.split()   289                         if columns[0] == "Version:":   290                             return columns[1]   291    292                     return None   293    294                 finally:   295                     f.close()   296    297             except IOError:   298                 f = os.popen("%s -c 'from MoinMoin.version import release; print release'" % sys.executable)   299                 try:   300                     return f.read().strip()   301                 finally:   302                     f.close()   303         finally:   304             os.chdir(this_dir)   305    306     def get_static_identifier(self):   307    308         "Return the static URL/directory identifier for the Wiki."   309    310         return "moin_static%s" % self.moin_version.replace(".", "")   311    312     def get_plugin_directory(self, plugin_type):   313    314         "Return the directory for plugins of the given 'plugin_type'."   315    316         data_dir = join(self.conf_dir, "data")   317         return join(data_dir, "plugin", plugin_type)   318    319     def limited_hosting(self):   320    321         "Return whether limited Web hosting is being used."   322    323         return self.web_site_dir == self.web_app_dir   324    325     def ensure_directories(self):   326    327         "Make sure that all the directories are available."   328    329         for d in (self.conf_dir, self.instance_dir, self.web_app_dir, self.web_site_dir):   330             if not exists(d):   331                 os.makedirs(d)   332    333     def get_theme_directories(self, theme_name=None):   334    335         """   336         Return tuples of the form (theme name, theme directory) for all themes,   337         or for a single theme if the optional 'theme_name' is specified.   338         """   339    340         filenames = theme_name and [theme_name] or os.listdir(self.htdocs_dir)   341         directories = []   342    343         for filename in filenames:   344             theme_dir = join(self.htdocs_dir, filename)   345    346             if not exists(theme_dir) or not isdir(theme_dir):   347                 continue   348    349             directories.append((filename, theme_dir))   350    351         return directories   352    353     # Main methods.   354    355     def setup(self):   356    357         "Set up the installation."   358    359         self.ensure_directories()   360         self.install_moin()   361         self._setup_wiki()   362    363     def setup_wiki(self):   364    365         "Set up a Wiki without installing MoinMoin."   366    367         self.ensure_directories()   368         self.install_moin(data_only=1)   369         self._setup_wiki()   370    371     def _setup_wiki(self):   372    373         "Set up a Wiki without installing MoinMoin."   374    375         self.install_data()   376         self.configure_moin()   377         self.edit_moin_script()   378         self.edit_moin_web_script()   379         self.add_superuser()   380         self.make_site_files()   381         self.make_post_install_script()   382    383     def install_moin(self, data_only=0):   384    385         "Enter the distribution directory and run the setup script."   386    387         # NOTE: Possibly check for an existing installation and skip repeated   388         # NOTE: installation attempts.   389    390         this_dir = os.getcwd()   391         os.chdir(self.moin_distribution)   392    393         log_filename = "install-%s.log" % split(self.common_dir)[-1]   394    395         status("Installing MoinMoin in %s..." % self.prefix)   396    397         if data_only:   398             install_cmd = "install_data"   399             options = "--install-dir='%s'" % self.instance_dir   400         else:   401             install_cmd = "install"   402             options = "--prefix='%s' --install-data='%s' --record='%s'" % (self.prefix, self.instance_dir, log_filename)   403    404         os.system("python setup.py --quiet %s %s --force" % (install_cmd, options))   405    406         os.chdir(this_dir)   407    408     def install_data(self):   409    410         "Install Wiki data."   411    412         # The default wikiconfig assumes data and underlay in the same directory.   413    414         status("Installing data and underlay in %s..." % self.conf_dir)   415    416         for d in ("data", "underlay"):   417             source = join(self.moin_distribution, "wiki", d)   418             source_tar = source + os.path.extsep + "tar"   419             d_tar = source + os.path.extsep + "tar"   420    421             if os.path.exists(source):   422                 shutil.copytree(source, join(self.conf_dir, d))   423             elif os.path.exists(source_tar):   424                 shutil.copy(source_tar, self.conf_dir)   425                 os.system("tar xf %s -C %s" % (d_tar, self.conf_dir))   426             else:   427                 status("Could not copy %s into installed Wiki." % d)   428    429         # Copy static Web data if appropriate.   430    431         if not self.moin_version.startswith("1.9") and self.limited_hosting():   432    433             if not exists(self.htdocs_dir):   434                 os.mkdir(self.htdocs_dir)   435    436             for item in os.listdir(self.htdocs_dir_source):   437                 path = join(self.htdocs_dir_source, item)   438                 if isdir(path):   439                     shutil.copytree(path, join(self.htdocs_dir, item))   440                 else:   441                     shutil.copy(path, join(self.htdocs_dir, item))   442    443     def configure_moin(self):   444    445         "Edit the Wiki configuration file."   446    447         # NOTE: Single Wiki only so far.   448    449         # Static URLs seem to be different in MoinMoin 1.9.x.   450         # For earlier versions, reserve URL space alongside the Wiki.   451         # NOTE: MoinMoin usually uses an apparently common URL space associated   452         # NOTE: with the version, but more specific locations are probably   453         # NOTE: acceptable if less efficient.   454    455         if self.moin_version.startswith("1.9"):   456             self.static_url_path = self.url_path   457             url_prefix_static = "%r + url_prefix_static" % self.static_url_path   458         else:   459             # Add the static identifier to the URL path. For example:   460             # /         -> /moin_static187   461             # /hgwiki   -> /hgwiki/moin_static187   462    463             self.static_url_path = self.url_path + (self.url_path != "/" and "-" or "") + self.get_static_identifier()   464             url_prefix_static = "%r" % self.static_url_path   465    466         # Copy the Wiki configuration file from the distribution.   467    468         wikiconfig_py = join(self.conf_dir, "wikiconfig.py")   469         shutil.copyfile(join(self.moin_distribution, "wiki", "config", "wikiconfig.py"), wikiconfig_py)   470    471         status("Editing configuration from %s..." % wikiconfig_py)   472    473         # Edit the Wiki configuration file.   474    475         wikiconfig = Configuration(wikiconfig_py)   476    477         try:   478             wikiconfig.set("url_prefix_static", url_prefix_static, raw=1)   479             wikiconfig.set("superuser", [self.superuser])   480             wikiconfig.set("acl_rights_before", u"%s:read,write,delete,revert,admin" % self.superuser)   481    482             if not self.moin_version.startswith("1.9"):   483                 data_dir = join(self.conf_dir, "data")   484                 data_underlay_dir = join(self.conf_dir, "underlay")   485    486                 wikiconfig.set("data_dir", data_dir)   487                 wikiconfig.set("data_underlay_dir", data_underlay_dir)   488    489             self._configure_moin(wikiconfig)   490    491         finally:   492             wikiconfig.close()   493    494     def _configure_moin(self, wikiconfig):   495    496         """   497         Configure Moin, accessing the configuration file using 'wikiconfig'.   498         """   499    500         wikiconfig.set("site_name", self.site_name)   501         wikiconfig.set("page_front_page", self.front_page_name, count=1)   502    503         if self.theme_default is not None:   504             wikiconfig.set("theme_default", self.theme_default)   505    506     def edit_moin_script(self):   507    508         "Edit the moin script."   509    510         moin_script = join(self.prefix, "bin", "moin")   511    512         status("Editing moin script at %s..." % moin_script)   513    514         s = readfile(moin_script)   515         s = s.replace("#import sys", "import sys\nsys.path.insert(0, %r)" % self.prefix_site_packages)   516    517         writefile(moin_script, s)   518    519     def edit_moin_web_script(self):   520    521         "Edit and install CGI script."   522    523         # NOTE: CGI only so far.   524         # NOTE: Permissions should be checked.   525    526         if self.moin_version.startswith("1.9"):   527             moin_cgi = join(self.instance_dir, "share", "moin", "server", "moin.fcgi")   528         else:   529             moin_cgi = join(self.instance_dir, "share", "moin", "server", "moin.cgi")   530    531         moin_cgi_installed = join(self.web_app_dir, "moin.cgi")   532    533         status("Editing moin.cgi script from %s..." % moin_cgi)   534    535         s = readfile(moin_cgi)   536         s = moin_cgi_prefix.sub("sys.path.insert(0, %r)" % self.prefix_site_packages, s)   537         s = moin_cgi_wikiconfig.sub("sys.path.insert(0, %r)" % self.conf_dir, s)   538    539         # Handle differences in script names when using limited hosting with   540         # URL rewriting.   541    542         if self.limited_hosting():   543             if self.moin_version.startswith("1.9"):   544                 s = moin_cgi_fix_script_name.sub(r"\1\2 %r" % self.url_path, s)   545             else:   546                 s = moin_cgi_properties.sub(r"\1\2 %r" % {"script_name" : self.url_path}, s)   547    548         # NOTE: Use CGI for now.   549    550         if self.moin_version.startswith("1.9"):   551             s = moin_cgi_force_cgi.sub(r"\1", s)   552    553         writefile(moin_cgi_installed, s)   554         os.system("chmod a+rx '%s'" % moin_cgi_installed)   555    556     def add_superuser(self):   557    558         "Add the superuser account."   559    560         moin_script = join(self.prefix, "bin", "moin")   561    562         print "Creating superuser", self.superuser, "using..."   563         email = raw_input("E-mail address: ")   564         password = getpass("Password: ")   565    566         path = os.environ.get("PYTHONPATH", "")   567    568         if path:   569             os.environ["PYTHONPATH"] = path + ":" + self.conf_dir   570         else:   571             os.environ["PYTHONPATH"] = self.conf_dir   572    573         os.system(moin_script + " account create --name='%s' --email='%s' --password='%s'" % (self.superuser, email, password))   574    575         if path:   576             os.environ["PYTHONPATH"] = path   577         else:   578             del os.environ["PYTHONPATH"]   579    580     def make_site_files(self):   581    582         "Make the Apache site files."   583    584         # NOTE: Using local namespace for substitution.   585    586         # Where the site definitions and applications directories are different,   587         # use a normal site definition.   588    589         if not self.limited_hosting():   590    591             site_def = join(self.web_site_dir, self.site_name)   592    593             s = apache_site % self.__dict__   594    595             if not self.moin_version.startswith("1.9"):   596                 s += apache_site_extra_moin18 % self.__dict__   597    598         # Otherwise, use an .htaccess file.   599    600         else:   601             site_def = join(self.web_site_dir, ".htaccess")   602    603             s = apache_htaccess_combined_mod_rewrite % self.__dict__   604    605         status("Writing Apache site definitions to %s..." % site_def)   606    607         writefile(site_def, s)   608    609     def make_post_install_script(self):   610    611         "Write a post-install script with additional actions."   612    613         this_user = os.environ["USER"]   614         postinst_script = "moinsetup-post.sh"   615    616         s = "#!/bin/sh\n"   617    618         for d in ("data", "underlay"):   619             s += "chown -R %s.%s '%s'\n" % (this_user, self.web_group, join(self.conf_dir, d))   620             s += "chmod -R g+w '%s'\n" % join(self.conf_dir, d)   621    622         if not self.moin_version.startswith("1.9"):   623             s += "chown -R %s.%s '%s'\n" % (this_user, self.web_group, self.htdocs_dir)   624    625         writefile(postinst_script, s)   626         os.chmod(postinst_script, 0755)   627         note("Run %s as root to set file ownership and permissions." % postinst_script)   628    629     # Accessory methods.   630    631     def reconfigure_moin(self, name=None, value=None, raw=0):   632    633         "Edit the installed Wiki configuration file."   634    635         wikiconfig_py = join(self.conf_dir, "wikiconfig.py")   636    637         status("Editing configuration from %s..." % wikiconfig_py)   638    639         wikiconfig = Configuration(wikiconfig_py)   640    641         try:   642             # Perform default configuration.   643    644             if name is None and value is None:   645                 self._configure_moin(wikiconfig)   646             else:   647                 wikiconfig.set(name, value, raw=raw)   648    649         finally:   650             wikiconfig.close()   651    652     def install_theme(self, theme_dir):   653    654         "Install Wiki theme provided in the given 'theme_dir'."   655    656         theme_dir = normpath(theme_dir)   657         theme_name = split(theme_dir)[-1]   658         theme_module = join(theme_dir, theme_name + extsep + "py")   659    660         plugin_theme_dir = self.get_plugin_directory("theme")   661    662         # Copy the theme module.   663    664         status("Copying theme module to %s..." % plugin_theme_dir)   665    666         shutil.copy(theme_module, plugin_theme_dir)   667    668         # Copy the resources.   669    670         resources_dir = join(self.htdocs_dir, theme_name)   671    672         if not exists(resources_dir):   673             os.mkdir(resources_dir)   674    675         status("Copying theme resources to %s..." % resources_dir)   676    677         for d in ("css", "img"):   678             target_dir = join(resources_dir, d)   679             if exists(target_dir):   680                 status("Replacing %s..." % target_dir)   681                 shutil.rmtree(target_dir)   682             shutil.copytree(join(theme_dir, d), target_dir)   683    684         # Copy additional resources from other themes.   685    686         resources_source_dir = join(self.htdocs_dir, self.theme_master)   687         target_dir = join(resources_dir, "css")   688    689         status("Copying resources from %s..." % resources_source_dir)   690    691         for css_file in self.extra_theme_css_files:   692             css_file_path = join(resources_source_dir, "css", css_file)   693             if exists(css_file_path):   694                 shutil.copy(css_file_path, target_dir)   695    696         note("Don't forget to add theme resources for extensions for this theme.")   697         note("Don't forget to edit theme stylesheets for any extensions.")   698    699     def install_extension_package(self, extension_dir):   700    701         "Install any libraries from 'extension_dir' using a setup script."   702    703         this_dir = os.getcwd()   704         os.chdir(extension_dir)   705         os.system("python setup.py install --prefix=%s" % self.prefix)   706         os.chdir(this_dir)   707    708     def install_plugins(self, plugins_dir, plugin_type):   709    710         """   711         Install Wiki actions provided in the given 'plugins_dir' of the   712         specified 'plugin_type'.   713         """   714    715         plugin_target_dir = self.get_plugin_directory(plugin_type)   716    717         # Copy the modules.   718    719         status("Copying %s modules to %s..." % (plugin_type, plugin_target_dir))   720    721         for module in glob(join(plugins_dir, "*%spy" % extsep)):   722             shutil.copy(module, plugin_target_dir)   723    724     def install_actions(self, actions_dir):   725    726         "Install Wiki actions provided in the given 'actions_dir'."   727    728         self.install_plugins(actions_dir, "action")   729    730     def install_macros(self, macros_dir):   731    732         "Install Wiki macros provided in the given 'macros_dir'."   733    734         self.install_plugins(macros_dir, "macro")   735    736     def install_theme_resources(self, theme_resources_dir, theme_name=None):   737    738         """   739         Install theme resources provided in the given 'theme_resources_dir'. If   740         a specific 'theme_name' is given, only that theme will be given the   741         specified resources.   742         """   743    744         for theme_name, theme_dir in self.get_theme_directories(theme_name):   745    746             # Copy the resources.   747    748             copied = 0   749    750             for d in ("css", "img"):   751                 source_dir = join(theme_resources_dir, d)   752                 target_dir = join(theme_dir, d)   753    754                 if not exists(target_dir):   755                     continue   756    757                 for resource in glob(join(source_dir, "*%s*" % extsep)):   758                     shutil.copy(resource, target_dir)   759                     copied = 1   760    761             if copied:   762                 status("Copied theme resources into %s..." % theme_dir)   763    764         note("Don't forget to edit theme stylesheets for any extensions.")   765    766     def edit_theme_stylesheet(self, theme_stylesheet, imported_stylesheet, action="ensure", theme_name=None):   767    768         """   769         Edit the given 'theme_stylesheet', ensuring (or removing) a reference to   770         the 'imported_stylesheet' according to the given 'action' (optional,   771         defaulting to "ensure"). If a specific 'theme_name' is given, only that   772         theme will be affected.   773         """   774    775         if action == "ensure":   776             ensure = 1   777         elif action == "remove":   778             ensure = 0   779         else:   780             error("Action %s not valid: it must be given as either 'ensure' or 'remove'." % action)   781             return   782    783         for theme_name, theme_dir in self.get_theme_directories(theme_name):   784    785             # Locate the resources.   786    787             css_dir = join(theme_dir, "css")   788    789             if not exists(css_dir):   790                 continue   791    792             theme_stylesheet_filename = join(css_dir, theme_stylesheet)   793             imported_stylesheet_filename = join(css_dir, imported_stylesheet)   794    795             if not exists(theme_stylesheet_filename):   796                 error("Stylesheet %s not defined in theme %s." % (theme_stylesheet, theme_name))   797                 continue   798    799             if not exists(imported_stylesheet_filename):   800                 error("Stylesheet %s not defined in theme %s." % (imported_stylesheet, theme_name))   801                 continue   802    803             # Edit the resources.   804    805             s = readfile(theme_stylesheet_filename)   806             after_point = 0   807    808             for stylesheet_import in css_import_stylesheet.finditer(s):   809                 before, filename, after = stylesheet_import.groups()   810                 before_point, after_point = stylesheet_import.span()   811    812                 # Test the import for a reference to the requested imported   813                 # stylesheet.   814    815                 if filename == imported_stylesheet:   816                     if ensure:   817                         break   818                     else:   819                         if s[after_point:after_point+1] == "\n":   820                             after_point += 1   821                         s = "%s%s" % (s[:before_point], s[after_point:])   822    823                         status("Removing %s from %s in theme %s..." % (imported_stylesheet, theme_stylesheet, theme_name))   824                         writefile(theme_stylesheet_filename, s)   825                         break   826    827             # Where no import references the imported stylesheet, insert a   828             # reference into the theme stylesheet.   829    830             else:   831                 if ensure:   832    833                     # Assume that the stylesheet can follow other imports.   834    835                     if s[after_point:after_point+1] == "\n":   836                         after_point += 1   837                     s = "%s%s\n%s" % (s[:after_point], '@import "%s";' % imported_stylesheet, s[after_point:])   838    839                     status("Adding %s to %s in theme %s..." % (imported_stylesheet, theme_stylesheet, theme_name))   840                     writefile(theme_stylesheet_filename, s)   841    842 def show_methods():   843     print "Methods:"   844     print   845     for method_name in Installation.method_names:   846         doc = getattr(Installation, method_name).__doc__.strip()   847         print "%-30s%-s" % (method_name, format(doc, 30))   848     print   849    850 # Command line option syntax.   851    852 syntax_description = "[ -f <config-filename> ] --method=METHOD [ <method-argument> ... ]"   853    854 # Main program.   855    856 if __name__ == "__main__":   857     from ConfigParser import ConfigParser   858     import sys, cmdsyntax   859    860     # Check the command syntax.   861    862     syntax = cmdsyntax.Syntax(syntax_description)   863     try:   864         matches = syntax.get_args(sys.argv[1:])   865         args = matches[0]   866     except IndexError:   867         print "Syntax:"   868         print sys.argv[0], syntax_description   869         print   870         show_methods()   871         sys.exit(1)   872    873     # Obtain configuration details.   874    875     try:   876         config_filename = args.get("config-filename", "moinsetup.cfg")   877         config = ConfigParser()   878         config.read(config_filename)   879    880         # Obtain as many arguments as needed from the configuration.   881    882         config_arguments = dict(config.items("installation") + config.items("site"))   883         method_arguments = args.get("method-argument", [])   884    885         # Attempt to initialise the configuration.   886    887         installation = Installation(**config_arguments)   888    889     except TypeError:   890         print "Configuration settings:"   891         print   892         print Installation.__init__.__doc__   893         print   894         sys.exit(1)   895    896     # Obtain the method.   897    898     try:   899         method = getattr(installation, args["method"])   900     except AttributeError:   901         show_methods()   902         sys.exit(1)   903    904     try:   905         method(*method_arguments)   906     except TypeError:   907         print "Method documentation:"   908         print   909         print method.__doc__   910         print   911         raise   912    913 # vim: tabstop=4 expandtab shiftwidth=4