Przeglądaj źródła

Merge pull request #13 from devsnd/master

implemented configuration file validation and added tests for them, closes #11
Thomas Kluyver 11 lat temu
rodzic
commit
0d4c149f51

+ 9 - 3
nsist/__init__.py

@@ -52,6 +52,7 @@ def fetch_python(version=DEFAULT_PY_VERSION, bitness=DEFAULT_BITNESS,
         return
     logger.info('Downloading Python MSI...')
     urlretrieve(url, target)
+
     urlretrieve(url+'.asc', target+'.asc')
     try:
         keys_file = os.path.join(_PKGDIR, 'python-pubkeys.txt')
@@ -61,6 +62,7 @@ def fetch_python(version=DEFAULT_PY_VERSION, bitness=DEFAULT_BITNESS,
         logger.warn("GPG not available - could not check signature of {0}".format(target))
 
 
+
 def fetch_pylauncher(bitness=DEFAULT_BITNESS, destination=DEFAULT_BUILD_DIR):
     """Fetch the MSI for PyLauncher (required for Python2.x).
 
@@ -267,9 +269,13 @@ def main(argv=None):
     if dirname:
         os.chdir(dirname)
     
-    import configparser
-    cfg = configparser.ConfigParser()
-    cfg.read(config_file)
+    try:
+        from . import configreader
+        cfg = configreader.read_and_validate(config_file)
+    except configreader.InvalidConfig as e:
+        logger.error('Error parsing configuration file:')
+        logger.error(str(e))
+        sys.exit(1)
     appcfg = cfg['Application']
     all_steps(
         appname = appcfg['name'],

+ 100 - 0
nsist/configreader.py

@@ -0,0 +1,100 @@
+#!/usr/bin/python3
+
+import configparser
+
+class SectionValidator(object):
+    def __init__(self, keys):
+        """
+        keys
+            list of tuples containing the names and whether the
+            key is mandatory
+        """
+        self.keys = keys
+    
+    def check(self, config, section_name):
+        """
+        validates the section, if this is the correct validator for it
+        returns True if this is the correct validator for this section
+        
+        raises InvalidConfig if something inside the section is wrong
+        """
+        self._check_mandatory_fields(section_name, config[section_name])
+        self._check_invalid_keys(section_name, config[section_name])
+
+    def _check_mandatory_fields(self, section_name, key):
+        for key_name, mandatory in self.keys:
+            if mandatory:
+                try:
+                    key[key_name]
+                except KeyError:
+                    err_msg = ("The section '{0}' must contain a "
+                               "key '{1}'!").format(
+                                section_name,
+                                key_name)
+                    raise InvalidConfig(err_msg)
+    
+    def _check_invalid_keys(self, section_name, section):
+        for key in section:
+            key_name = str(key)
+            valid_key_names = [s[0] for s in self.keys]
+            is_valid_key = key_name in valid_key_names
+            if not is_valid_key:
+                err_msg = ("'{0}' is not a valid key name for '{1}'. Must "
+                           "be one of these: {2}").format(
+                            key_name,
+                            section_name,
+                            ', '.join(valid_key_names))
+                raise InvalidConfig(err_msg)
+
+# contains all configuration sections and keys
+# the keys are a tuple with their name and a boolean, which
+# tells us whether the option is mandatory
+CONFIG_VALIDATORS = {
+    'Application': SectionValidator([
+        ('name', True),
+        ('version', True),
+        ('entry_point', False),
+        ('script', False),
+        ('icon', False),
+        ('console', False),
+    ]),
+    'Build': SectionValidator([
+        ('directory', False),
+        ('installer_name', False),
+        ('nsi_template', False),
+    ]),
+    'Include': SectionValidator([
+        ('packages', False),
+        ('files', False),
+    ]),
+    'Python': SectionValidator([
+        ('version', True),
+        ('bitness', False),
+    ]),
+    'Shortcut': SectionValidator([
+        ('entry_point', False),
+        ('script', False),
+        ('icon', False),
+        ('console', False),
+    ]),
+}
+
+class InvalidConfig(ValueError):
+    pass
+
+def read_and_validate(config_file):
+    config = configparser.ConfigParser()
+    config.read(config_file)
+    for section in config.sections():
+        if section in CONFIG_VALIDATORS:
+            CONFIG_VALIDATORS[section].check(config, section)
+        elif section.startswith('Shortcut '):
+            CONFIG_VALIDATORS['Shortcut'].check(config, section)
+        else:
+            valid_section_names = CONFIG_VALIDATORS.keys()
+            err_msg = ("{0} is not a valid section header. Must "
+                       "be one of these: {1}").format(
+                       section,
+                       ', '.join(['"%s"' % n for n in valid_section_names]))
+            raise InvalidConfig(err_msg)    
+    return config

+ 19 - 0
nsist/tests/data_files/invalid_config_section.cfg

@@ -0,0 +1,19 @@
+[JamesBrown]
+name=My App
+version=1.0
+# How to launch the app - this calls the 'main' function from the 'myapp' package:
+entry_point=myapp:main
+icon=myapp.ico
+
+[Python]
+version=3.4.0
+
+[Include]
+# Importable packages that your application requires, one per line
+packages = requests
+     bs4
+     html5lib
+
+# Other files and folders that should be installed
+files = LICENSE
+    data_files/

+ 20 - 0
nsist/tests/data_files/invalid_config_subsection.cfg

@@ -0,0 +1,20 @@
+[Application]
+name=My App
+version=1.0
+# How to launch the app - this calls the 'main' function from the 'myapp' package:
+entry_point=myapp:main
+icon=myapp.ico
+this_subsection=is_invalid
+
+[Python]
+version=3.4.0
+
+[Include]
+# Importable packages that your application requires, one per line
+packages = requests
+     bs4
+     html5lib
+
+# Other files and folders that should be installed
+files = LICENSE
+    data_files/

+ 18 - 0
nsist/tests/data_files/missing_config_subsection.cfg

@@ -0,0 +1,18 @@
+[Application]
+version=1.0
+# How to launch the app - this calls the 'main' function from the 'myapp' package:
+entry_point=myapp:main
+icon=myapp.ico
+
+[Python]
+version=3.4.0
+
+[Include]
+# Importable packages that your application requires, one per line
+packages = requests
+     bs4
+     html5lib
+
+# Other files and folders that should be installed
+files = LICENSE
+    data_files/

+ 1 - 0
nsist/tests/data_files/not_a_config.cfg

@@ -0,0 +1 @@
+THIS IS NOT A CONFIG FILE!

+ 19 - 0
nsist/tests/data_files/valid_config.cfg

@@ -0,0 +1,19 @@
+[Application]
+name=My App
+version=1.0
+# How to launch the app - this calls the 'main' function from the 'myapp' package:
+entry_point=myapp:main
+icon=myapp.ico
+
+[Python]
+version=3.4.0
+
+[Include]
+# Importable packages that your application requires, one per line
+packages = requests
+     bs4
+     html5lib
+
+# Other files and folders that should be installed
+files = LICENSE
+    data_files/

+ 24 - 0
nsist/tests/data_files/valid_config_with_shortcut.cfg

@@ -0,0 +1,24 @@
+[Application]
+name=My App
+version=1.0
+# How to launch the app - this calls the 'main' function from the 'myapp' package:
+entry_point=myapp:main
+icon=myapp.ico
+
+[Python]
+version=3.4.0
+
+[Include]
+# Importable packages that your application requires, one per line
+packages = requests
+     bs4
+     html5lib
+
+# Other files and folders that should be installed
+files = LICENSE
+    data_files/
+    
+[Shortcut IPython Notebook]
+entry_point=IPython.html.notebookapp:launch_new_instance
+icon=scripts/ipython_nb.ico
+console=true

+ 34 - 0
nsist/tests/test_configuration_validator.py

@@ -0,0 +1,34 @@
+from nose.tools import *
+import os
+from .. import configreader
+import configparser
+
+DATA_FILES = os.path.join(os.path.dirname(__file__), 'data_files')
+
+def test_valid_config():
+    configfile = os.path.join(DATA_FILES, 'valid_config.cfg')
+    configreader.read_and_validate(configfile)
+
+def test_valid_config_with_shortcut():
+    configfile = os.path.join(DATA_FILES, 'valid_config_with_shortcut.cfg')
+    configreader.read_and_validate(configfile)
+
+@raises(configreader.InvalidConfig)
+def test_invalid_config_keys():
+    configfile = os.path.join(DATA_FILES, 'invalid_config_section.cfg')
+    configreader.read_and_validate(configfile)
+
+@raises(configreader.InvalidConfig)
+def test_invalid_config_subsection():
+    configfile = os.path.join(DATA_FILES, 'invalid_config_subsection.cfg')
+    configreader.read_and_validate(configfile)
+
+@raises(configreader.InvalidConfig)
+def test_missing_config_subsection():
+    configfile = os.path.join(DATA_FILES, 'missing_config_subsection.cfg')
+    configreader.read_and_validate(configfile)
+
+@raises(configparser.Error)
+def test_invalid_config_file():
+    configfile = os.path.join(DATA_FILES, 'not_a_config.cfg')
+    configreader.read_and_validate(configfile)