Example #1
0
    def run_validation(self, value):
        try:
            host, port = value.rsplit(':', 1)
        except Exception:
            raise ValidationError("Must be a string of format 'IP:PORT'")

        if host != 'localhost':
            try:
                # Validate and normalize IP Address
                host = str(ipaddress.ip_address(host))
            except ValueError as e:
                raise ValidationError(e)

        try:
            port = int(port)
        except Exception:
            raise ValidationError("'{}' is not a valid port".format(port))

        class Address(namedtuple('Address', 'host port')):
            def __str__(self):
                return '{}:{}'.format(self.host, self.port)

        return Address(host, port)
Example #2
0
    def post_validation(self, config, key_name):

        super().post_validation(config, key_name)

        # Validate that the docs_dir and site_dir don't contain the
        # other as this will lead to copying back and forth on each
        # and eventually make a deep nested mess.
        if (config['docs_dir'] +
                os.sep).startswith(config['site_dir'].rstrip(os.sep) + os.sep):
            raise ValidationError(
                ("The 'docs_dir' should not be within the 'site_dir' as this "
                 "can mean the source files are overwritten by the output or "
                 "it will be deleted if --clean is passed to mkdocs build."
                 "(site_dir: '{}', docs_dir: '{}')").format(
                     config['site_dir'], config['docs_dir']))
        elif (config['site_dir'] +
              os.sep).startswith(config['docs_dir'].rstrip(os.sep) + os.sep):
            raise ValidationError(
                ("The 'site_dir' should not be within the 'docs_dir' as this "
                 "leads to the build directory being copied into itself and "
                 "duplicate nested files in the 'site_dir'."
                 "(site_dir: '{}', docs_dir: '{}')").format(
                     config['site_dir'], config['docs_dir']))
Example #3
0
    def run_validation(self, value):
        if not isinstance(value, (list, tuple)):
            raise ValidationError('Invalid Plugins configuration. Expected a list of plugins')
        plgins = plugins.PluginCollection()
        for item in value:
            if isinstance(item, dict):
                if len(item) > 1:
                    raise ValidationError('Invalid Plugins configuration')
                name, cfg = item.popitem()
                cfg = cfg or {}  # Users may define a null (None) config
                if not isinstance(cfg, dict):
                    raise ValidationError('Invalid config options for '
                                          'the "{0}" plugin.'.format(name))
                item = name
            else:
                cfg = {}

            if not isinstance(item, utils.string_types):
                raise ValidationError('Invalid Plugins configuration')

            plgins[item] = self.load_plugin(item, cfg)

        return plgins
Example #4
0
    def run_validation(self, value):

        if not isinstance(value, list):
            raise ValidationError("Expected a list, got {0}".format(
                type(value)))

        if len(value) == 0:
            return

        # TODO: Remove in 1.0
        config_types = set(type(l) for l in value)

        if config_types.issubset(set([utils.text_type, dict, str])):
            return value

        if config_types.issubset(set([utils.text_type, list, str])):
            return legacy.pages_compat_shim(value)

        raise ValidationError("Invalid pages config. {0} {1}".format(
            config_types, set([
                utils.text_type,
                dict,
            ])))
Example #5
0
    def run_validation(self, value):
        themes = utils.get_theme_names()

        if value in themes:
            if value in ['mkdocs', 'readthedocs']:
                return value

            self.warnings.append(
                ("The theme '{0}' will be removed in an upcoming MkDocs "
                 "release. See http://www.mkdocs.org/about/release-notes/ "
                 "for more details").format(value))
            return value

        raise ValidationError("Unrecognised theme.")
Example #6
0
    def validate(self, value):
        if value is None and self.default is not None:
            value = {'name': self.default}

        if isinstance(value, str):
            value = {'name': value}

        themes = utils.get_theme_names()

        if isinstance(value, dict):
            if 'name' in value:
                if value['name'] is None or value['name'] in themes:
                    return value

                raise ValidationError(
                    "Unrecognised theme name: '{}'. The available installed themes "
                    "are: {}".format(value['name'], ', '.join(themes)))

            raise ValidationError("No theme name set.")

        raise ValidationError(
            'Invalid type "{}". Expected a string or key/value pairs.'.format(
                type(value)))
Example #7
0
 def run_validation(self, value):
     if not isinstance(value, (list, tuple)):
         raise ValidationError('Invalid Markdown Extensions configuration')
     extensions = []
     for item in value:
         if isinstance(item, dict):
             if len(item) > 1:
                 raise ValidationError(
                     'Invalid Markdown Extensions configuration')
             ext, cfg = item.popitem()
             extensions.append(ext)
             if cfg is None:
                 continue
             if not isinstance(cfg, dict):
                 raise ValidationError(
                     'Invalid config options for Markdown '
                     "Extension '{0}'.".format(ext))
             self.configdata[ext] = cfg
         elif isinstance(item, utils.string_types):
             extensions.append(item)
         else:
             raise ValidationError(
                 'Invalid Markdown Extensions configuration')
     return utils.reduce_list(self.builtins + extensions)
Example #8
0
 def run_validation(self, value, *, top=True):
     if isinstance(value, list):
         for subitem in value:
             self._validate_nav_item(subitem)
         if top and not value:
             value = None
     elif isinstance(value, dict) and value and not top:
         # TODO: this should be an error.
         self.warnings.append(f"Expected nav to be a list, got {self._repr_item(value)}")
         for subitem in value.values():
             self.run_validation(subitem, top=False)
     elif isinstance(value, str) and not top:
         pass
     else:
         raise ValidationError(f"Expected nav to be a list, got {self._repr_item(value)}")
     return value
Example #9
0
    def validate(self, value):
        """
        Perform some initial validation.

        If the option is empty (None) and isn't required, leave it as such. If
        it is empty but has a default, use that. Finally, call the
        run_validation method on the subclass unless.
        """

        if value is None:
            if self.default is not None:
                value = self.default
            elif not self.required:
                return
            elif self.required:
                raise ValidationError("Required configuration not provided.")

        return self.run_validation(value)
Example #10
0
    def post_validation(self, config, key_name):
        theme_config = config[key_name]

        # TODO: Remove when theme_dir is fully deprecated.
        if config['theme_dir'] is not None:
            if 'custom_dir' not in theme_config:
                # Only pass in 'theme_dir' if it is set and 'custom_dir' is not set.
                theme_config['custom_dir'] = config['theme_dir']
            if not any(['theme' in c for c in config.user_configs]):
                # If the user did not define a theme, but did define theme_dir, then remove default set in validate.
                theme_config['name'] = None

        if not theme_config['name'] and 'custom_dir' not in theme_config:
            raise ValidationError(
                "At least one of 'theme.name' or 'theme.custom_dir' must be defined."
            )

        # Ensure custom_dir is an absolute path
        if 'custom_dir' in theme_config and not os.path.isabs(
                theme_config['custom_dir']):
            theme_config['custom_dir'] = os.path.abspath(
                theme_config['custom_dir'])

        config[key_name] = theme.Theme(**theme_config)
Example #11
0
    def pre_validation(self, config, key_name):
        self.option.pre_validation(config, key_name)

        if config.get(key_name) is not None:
            if self.removed:
                raise ValidationError(self.message.format(key_name))
            self.warnings.append(self.message.format(key_name))

            if self.moved_to is not None:
                if '.' not in self.moved_to:
                    target = config
                    target_key = self.moved_to
                else:
                    move_to, target_key = self.moved_to.rsplit('.', 1)

                    target = config
                    for key in move_to.split('.'):
                        target = target.setdefault(key, {})

                        if not isinstance(target, dict):
                            # We can't move it for the user
                            return

                target[target_key] = config.pop(key_name)
Example #12
0
 def run_validation(self, value):
     raise ValidationError('For internal use only.')
Example #13
0
 def run_validation(self, value):
     if isinstance(value, list):
         return value
     else:
         raise ValidationError("Expected a list, got {0}".format(
             type(value)))
Example #14
0
def parse_locale(locale):
    try:
        return Locale.parse(locale, sep='_')
    except (ValueError, UnknownLocaleError, TypeError) as e:
        raise ValidationError(f'Invalid value for locale: {str(e)}')
Example #15
0
def on_config_event(plugin, config, **kwargs):
    """Configuration event for 'mkdocs-mdpo-plugin'.

    * Define properly `lc_messages`, `languages` and `locale_dir`
      configuration settings.
    * Loads `mkdocs-mdpo` extension.
    * Configures md4c extensions accordingly to Python-Markdown extensions.
    * Stores the Markdown extensions used in the build in the
      ``extensions.markdown`` property of the plugin instance.
    * Creates the persistent files cache for the project.
    """
    if plugin.config['lc_messages'] is True:
        plugin.config['lc_messages'] = 'LC_MESSAGES'
    elif not plugin.config['lc_messages']:
        plugin.config['lc_messages'] = ''

    try:
        _using_material_theme = config['theme'].name == 'material'
    except KeyError:
        _using_material_theme = None

    # load language selection settings from material or mdpo configuration
    def _languages_required():
        msg = (
            'You must define the languages you will translate the'
            ' content into using'
            f"{' either' if _using_material_theme else ' the'}"
            " 'plugins.mdpo.languages'"
        )
        if _using_material_theme:
            msg += " or 'extra.alternate'"
        msg += (
            ' configuration setting'
            f"{'s' if _using_material_theme else ''}."
        )
        return ValidationError(msg)

    languages = plugin.config.get('languages')
    if not languages:
        if _using_material_theme:
            if 'extra' not in config:
                raise _languages_required()
            alternate = config['extra'].get('alternate')
            if not alternate:
                raise _languages_required()
            plugin.config['languages'] = [alt['lang'] for alt in alternate]
        else:
            raise _languages_required()
    else:
        if not isinstance(languages, list):
            raise ValidationError(
                'Expected "languages" config setting to be a list',
            )
        for i, language in enumerate(languages):
            if not isinstance(language, str):
                raise ValidationError(
                    f'Expected "languages[{i}]" config setting to'
                    f' be a string but is {language}',
                )

    default_language = plugin.config.get('default_language')
    if not default_language:
        # use mkdocs>=v1.2.0 theme localization setting
        theme_locale = (
            None if 'theme' not in config
            else config['theme']._vars.get('locale')
        )
        if theme_locale and theme_locale in plugin.config['languages']:
            plugin.config['default_language'] = theme_locale
        else:
            if (
                not isinstance(plugin.config['languages'], list)
                or not plugin.config['languages']
            ):
                raise _languages_required()

            plugin.config['default_language'] = plugin.config['languages'][0]

    # ----------------------------------------------------------

    # extensions configuration
    markdown_extensions = config.get('markdown_extensions')

    # configure MD4C extensions
    if markdown_extensions:
        if 'tables' not in markdown_extensions:
            if 'tables' in plugin.extensions.md4c:
                plugin.extensions.md4c.remove('tables')
        else:
            if 'tables' not in plugin.extensions.md4c:
                plugin.extensions.md4c.append('tables')
        if 'wikilinks' not in markdown_extensions:
            if 'wikilinks' in plugin.extensions.md4c:
                plugin.extensions.md4c.remove('wikilinks')
        else:
            if 'wikilinks' not in plugin.extensions.md4c:
                plugin.extensions.md4c.append('wikilinks')

        # spaces after '#' are optional in Python-Markdown for headers,
        # but the extension 'pymdownx.saneheaders' makes them mandatory
        if 'pymdownx.saneheaders' in markdown_extensions:
            if 'permissive_atx_headers' in plugin.extensions.md4c:
                plugin.extensions.md4c.remove('permissive_atx_headers')
        else:
            if 'permissive_atx_headers' not in plugin.extensions.md4c:
                plugin.extensions.md4c.append('permissive_atx_headers')

        # 'pymdownx.tasklist' enables 'tasklists' MD4C extentsion
        if 'pymdownx.tasklist' in markdown_extensions:
            if 'tasklists' not in plugin.extensions.md4c:
                plugin.extensions.md4c.append('tasklists')
        else:
            if 'tasklists' in plugin.extensions.md4c:
                plugin.extensions.md4c.remove('tasklists')

        # 'pymdownx.tilde' enables strikethrough syntax, but only works
        # if the MD4C extension is disabled
        if 'pymdownx.tilde' in markdown_extensions:
            if 'strikethrough' in plugin.extensions.md4c:
                plugin.extensions.md4c.remove('strikethrough')

        # configure internal 'mkdocs-mdpo' extension
        if 'mkdocs-mdpo' in markdown_extensions:  # pragma: no cover
            config['markdown_extensions'].remove('mkdocs-mdpo')
        config['markdown_extensions'].append('mkdocs-mdpo')

    # install a i18n aware version of sitemap.xml if not provided by the user
    theme_custom_dir = config['theme']._vars.get('custom_dir', '.')
    if not os.path.isfile(os.path.join(theme_custom_dir, 'sitemap.xml')):
        custom_sitemap_dir = os.path.join(
            os.path.dirname(installation_path),
            'custom_mdpo_sitemap',
        )
        config['theme'].dirs.insert(0, custom_sitemap_dir)

    # check that cross language search configuration is valid
    if plugin.config.get('cross_language_search') is False:
        if 'search' not in config['plugins']:
            raise ValidationError(
                '"cross_language_search" setting is disabled but'
                ' no "search" plugin has been added to "plugins"',
            )
        else:
            plugin_names = [p for p in config['plugins']]
            if plugin_names.index('search') > plugin_names.index('mdpo'):
                raise ValidationError(
                    '"search" plugin must be placed before "mdpo"'
                    ' plugin if you want to disable "cross_language_search".',
                )
    elif not _using_material_theme and 'search' in config['plugins']:
        # if cross_language_search is active, add all languages to 'search'
        # plugin configuration
        lunr_languages = get_lunr_languages()
        search_langs = config['plugins']['search'].config.get('lang', [])
        for language in plugin.config['languages']:
            if language in lunr_languages:
                if language not in search_langs:
                    config['plugins']['search'].config['lang'].append(
                        language,
                    )
                    logger.debug(
                        f"[mdpo] Adding '{language}' to"
                        " 'plugins.search.lang' option",
                    )
            elif language != 'en':  # English does not need steemer
                logger.info(
                    f"[mdpo] Language '{language}' is not supported by"
                    " lunr.js, not adding it to 'plugins.search.lang'"
                    ' option',
                )

    # check that minimum translated messages required for each language
    # is a valid value
    min_translated = plugin.config.get('min_translated_messages')
    if min_translated is not None:
        try:
            if '%' in str(min_translated):
                min_translated = max(-100, -float(min_translated.strip('%')))
            else:
                min_translated = max(int(min_translated), 0)
        except Exception:
            raise ValidationError(
                f"The value '{min_translated}' for"
                " 'min_translated_messages' config setting"
                '  is not a valid percentage or number.',
            )
        else:
            plugin.config['min_translated_messages'] = min_translated

    # check that 'exclude' contains a valid list
    exclude = plugin.config.get('exclude', [])
    if not isinstance(exclude, list):
        raise ValidationError(
            'Expected mdpo\'s "exclude" setting to be a list, but found'
            f' the value {str(exclude)} of type {type(exclude).__name__}',
        )
    else:
        for i, path in enumerate(exclude):
            if not isinstance(path, str):
                raise ValidationError(
                    f'Expected mdpo\'s setting "exclude[{i}]" value to'
                    f' be a string, but found the value {str(path)} of'
                    f' type {type(path).__name__}',
                )
    plugin.config['exclude'] = exclude

    # translation of configuration settings
    valid_translate_settings = ['site_name', 'site_description']
    for setting in plugin.config.get('translate', []):
        if setting not in valid_translate_settings:
            valid_translate_settings_readable = ' and '.join([
                f"'{opt}'" for opt in valid_translate_settings
            ])
            raise ValidationError(
                f"The setting '{setting}' is not supported for"
                " 'plugins.mdpo.translate' config setting. Valid settings"
                f' are {valid_translate_settings_readable}',
            )
        elif (
            setting == 'site_description'
            and not config.get('site_description')
        ):
            logger.warn(
                '[mdpo] "site_description" is configured to be translated'
                ' but was not defined in mkdocs.yml',
            )
            plugin.config['translate'].remove('site_description')

    # store reference in plugin to markdown_extensions for later usage
    plugin.extensions.markdown = markdown_extensions
Example #16
0
 def run_validation(self, value):
     if isinstance(value, list):
         return list(os.path.normcase(path) for path in value)
     else:
         raise ValidationError("Expected a list, got {0}".format(
             type(value)))