diff --git a/.travis.yml b/.travis.yml index 0f711594..701eb6da 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,19 +8,15 @@ matrix: - env: TOXENV=flake8 - env: TOXENV=markdown-lint before_install: npm install -g markdownlint-cli - - env: TOXENV=linkchecker - python: '2.7' + # Until Linkchecker is updated to Python 3, we will have skip this test. + # Watch https://github.com/linkchecker/linkchecker for updates. + # - env: TOXENV=linkchecker + # python: '2.7' - env: TOXENV=jshint before_install: npm install -g jshint - env: TOXENV=csslint before_install: npm install -g csslint # Python version specific - - python: '2.7' - env: TOXENV=py27-integration - - python: '2.7' - env: TOXENV=py27-min-req - - python: '2.7' - env: TOXENV=py27-unittests - python: '3.4' env: TOXENV=py34-integration - python: '3.4' @@ -51,12 +47,6 @@ matrix: env: TOXENV=py37-unittests dist: xenial sudo: true - - python: 'pypy' - env: TOXENV=pypy-integration - - python: 'pypy' - env: TOXENV=pypy-min-req - - python: 'pypy' - env: TOXENV=pypy-unittests - python: 'pypy3' env: TOXENV=pypy3-integration - python: 'pypy3' @@ -83,4 +73,4 @@ deploy: on: tags: true repo: mkdocs/mkdocs - condition: "$TOXENV = py27-integration" + condition: "$TOXENV = py37-integration" diff --git a/appveyor.yml b/appveyor.yml index 3e04b92e..995d64e9 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,9 +1,6 @@ build: false environment: matrix: - - TOXENV: py27-integration - - TOXENV: py27-min-req - - TOXENV: py27-unittests - TOXENV: py34-integration - TOXENV: py34-min-req - TOXENV: py34-unittests @@ -20,9 +17,9 @@ environment: init: - "ECHO %TOXENV%" install: - - "c:\\python27\\Scripts\\pip install tox" + - "c:\\python37\\Scripts\\pip install tox" test_script: - "git clean -f -d -x" - - "c:\\python27\\Scripts\\tox --version" - - "c:\\python27\\Scripts\\pip --version" - - "c:\\python27\\Scripts\\tox" + - "c:\\python37\\Scripts\\tox --version" + - "c:\\python37\\Scripts\\pip --version" + - "c:\\python37\\Scripts\\tox" diff --git a/docs/about/release-notes.md b/docs/about/release-notes.md index 980aa962..b833e6db 100644 --- a/docs/about/release-notes.md +++ b/docs/about/release-notes.md @@ -56,6 +56,7 @@ your global navigation uses more than one level, things will likely be broken. ### Other Changes and Additions to Version 1.1 +* Drop support for Python 2.7. MkDocs is PY3 only now (#1926). * Bugfix: Select appropriate asyncio event loop on Windows for Python 3.8+ (#1885). * Bugfix: Ensure nested index pages do not get identified as the homepage (#1919). * Bugfix: Properly identify deployment version (#1879). diff --git a/docs/user-guide/plugins.md b/docs/user-guide/plugins.md index 761bfc40..1aea139b 100644 --- a/docs/user-guide/plugins.md +++ b/docs/user-guide/plugins.md @@ -76,7 +76,7 @@ All `BasePlugin` subclasses contain the following attributes: class MyPlugin(mkdocs.plugins.BasePlugin): config_scheme = ( - ('foo', mkdocs.config.config_options.Type(mkdocs.utils.string_types, default='a default value')), + ('foo', mkdocs.config.config_options.Type(str, default='a default value')), ('bar', mkdocs.config.config_options.Type(int, default=0)), ('baz', mkdocs.config.config_options.Type(bool, default=True)) ) diff --git a/mkdocs/__init__.py b/mkdocs/__init__.py index a2569085..c359894e 100644 --- a/mkdocs/__init__.py +++ b/mkdocs/__init__.py @@ -1,7 +1,5 @@ #!/usr/bin/env python -# coding: utf-8 -from __future__ import unicode_literals # For acceptable version formats, see https://www.python.org/dev/peps/pep-0440/ __version__ = '1.1.dev0' diff --git a/mkdocs/__main__.py b/mkdocs/__main__.py index a3f7cdae..f8939fc0 100644 --- a/mkdocs/__main__.py +++ b/mkdocs/__main__.py @@ -1,18 +1,20 @@ #!/usr/bin/env python -# coding: utf-8 -from __future__ import unicode_literals import os import sys import logging import click -import socket -from mkdocs import __version__ -from mkdocs import utils -from mkdocs import exceptions -from mkdocs import config -from mkdocs.commands import build, gh_deploy, new, serve +# TODO: Remove this check at some point in the future. +# (also remove flake8's 'ignore E402' comments below) +if sys.version_info[0] < 3: # pragma: no cover + raise ImportError('A recent version of Python 3 is required.') + +from mkdocs import __version__ # noqa: E402 +from mkdocs import utils # noqa: E402 +from mkdocs import exceptions # noqa: E402 +from mkdocs import config # noqa: E402 +from mkdocs.commands import build, gh_deploy, new, serve # noqa: E402 log = logging.getLogger(__name__) @@ -22,7 +24,7 @@ log = logging.getLogger(__name__) click.disable_unicode_literals_warning = True -class State(object): +class State: ''' Maintain logging level.''' def __init__(self, log_name='mkdocs', level=logging.INFO): @@ -96,7 +98,7 @@ pgk_dir = os.path.dirname(os.path.abspath(__file__)) @click.group(context_settings={'help_option_names': ['-h', '--help']}) @click.version_option( - '{0} from {1} (Python {2})'.format(__version__, pgk_dir, sys.version[:3]), + '{} from {} (Python {})'.format(__version__, pgk_dir, sys.version[:3]), '-V', '--version') @common_options def cli(): @@ -133,7 +135,7 @@ def serve_command(dev_addr, config_file, strict, theme, theme_dir, livereload): theme_dir=theme_dir, livereload=livereload ) - except (exceptions.ConfigurationError, socket.error) as e: # pragma: no cover + except (exceptions.ConfigurationError, OSError) as e: # pragma: no cover # Avoid ugly, unhelpful traceback raise SystemExit('\n' + str(e)) diff --git a/mkdocs/commands/build.py b/mkdocs/commands/build.py index c43b489d..831ce216 100644 --- a/mkdocs/commands/build.py +++ b/mkdocs/commands/build.py @@ -1,12 +1,9 @@ -# coding: utf-8 - -from __future__ import unicode_literals from datetime import datetime from calendar import timegm import logging import os import gzip -import io +from urllib.parse import urlparse from jinja2.exceptions import TemplateNotFound import jinja2 @@ -17,7 +14,7 @@ from mkdocs.structure.nav import get_navigation import mkdocs -class DuplicateFilter(object): +class DuplicateFilter: ''' Avoid logging duplicate messages. ''' def __init__(self): self.msgs = set() @@ -82,7 +79,7 @@ def _build_template(name, template, files, config, nav): # See https://github.com/mkdocs/mkdocs/issues/77. # However, if site_url is not set, assume the docs root and server root # are the same. See https://github.com/mkdocs/mkdocs/issues/1598. - base_url = utils.urlparse(config['site_url'] or '/').path + base_url = urlparse(config['site_url'] or '/').path else: base_url = utils.get_relative_url('.', name) @@ -139,7 +136,7 @@ def _build_extra_template(template_name, files, config, nav): return try: - with io.open(file.abs_src_path, 'r', encoding='utf-8', errors='strict') as f: + with open(file.abs_src_path, 'r', encoding='utf-8', errors='strict') as f: template = jinja2.Template(f.read()) except Exception as e: log.warn("Error reading template '{}': {}".format(template_name, e)) diff --git a/mkdocs/commands/gh_deploy.py b/mkdocs/commands/gh_deploy.py index c16ef8d0..0be0f588 100644 --- a/mkdocs/commands/gh_deploy.py +++ b/mkdocs/commands/gh_deploy.py @@ -1,4 +1,3 @@ -from __future__ import unicode_literals import logging import subprocess import os @@ -131,5 +130,5 @@ def gh_deploy(config, message=None, force=False, ignore_version=False): username, repo = path.split('/', 1) if repo.endswith('.git'): repo = repo[:-len('.git')] - url = 'https://%s.github.io/%s/' % (username, repo) + url = 'https://{}.github.io/{}/'.format(username, repo) log.info('Your documentation should shortly be available at: ' + url) diff --git a/mkdocs/commands/new.py b/mkdocs/commands/new.py index 850610f6..0afe9d39 100644 --- a/mkdocs/commands/new.py +++ b/mkdocs/commands/new.py @@ -1,7 +1,3 @@ -# coding: utf-8 -from __future__ import unicode_literals - -import io import logging import os @@ -43,7 +39,7 @@ def new(output_dir): os.mkdir(output_dir) log.info('Writing config file: %s', config_path) - io.open(config_path, 'w', encoding='utf-8').write(config_text) + open(config_path, 'w', encoding='utf-8').write(config_text) if os.path.exists(index_path): return @@ -51,4 +47,4 @@ def new(output_dir): log.info('Writing initial docs: %s', index_path) if not os.path.exists(docs_dir): os.mkdir(docs_dir) - io.open(index_path, 'w', encoding='utf-8').write(index_text) + open(index_path, 'w', encoding='utf-8').write(index_text) diff --git a/mkdocs/commands/serve.py b/mkdocs/commands/serve.py index 8045c82d..f40d8d20 100644 --- a/mkdocs/commands/serve.py +++ b/mkdocs/commands/serve.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - import logging import shutil import tempfile @@ -45,7 +43,7 @@ def _get_handler(site_dir, StaticFileHandler): if isfile(join(site_dir, error_page)): self.write(Loader(site_dir).load(error_page).generate()) else: - super(WebHandler, self).write_error(status_code, **kwargs) + super().write_error(status_code, **kwargs) return WebHandler @@ -61,7 +59,7 @@ def _livereload(host, port, config, builder, site_dir): class LiveReloadServer(Server): def get_web_handlers(self, script): - handlers = super(LiveReloadServer, self).get_web_handlers(script) + handlers = super().get_web_handlers(script) # replace livereload handler return [(handlers[0][0], _get_handler(site_dir, livereload.handlers.StaticFileHandler), handlers[0][2],)] @@ -130,7 +128,7 @@ def serve(config_file=None, dev_addr=None, strict=None, theme=None, site_dir=site_dir ) # Override a few config settings after validation - config['site_url'] = 'http://{0}/'.format(config['dev_addr']) + config['site_url'] = 'http://{}/'.format(config['dev_addr']) live_server = livereload in ['dirty', 'livereload'] dirty = livereload == 'dirty' diff --git a/mkdocs/config/base.py b/mkdocs/config/base.py index a3db6abe..246cb1b4 100644 --- a/mkdocs/config/base.py +++ b/mkdocs/config/base.py @@ -1,8 +1,8 @@ -from __future__ import unicode_literals import logging import os import sys from yaml import YAMLError +from collections import UserDict from mkdocs import exceptions from mkdocs import utils @@ -15,7 +15,7 @@ class ValidationError(Exception): """Raised during the validation process of the config on errors.""" -class Config(utils.UserDict): +class Config(UserDict): """ MkDocs Configuration dict @@ -31,7 +31,7 @@ class Config(utils.UserDict): self._schema = schema self._schema_keys = set(dict(schema).keys()) # Ensure config_file_path is a Unicode string - if config_file_path is not None and not isinstance(config_file_path, utils.text_type): + if config_file_path is not None and not isinstance(config_file_path, str): try: # Assume config_file_path is encoded with the file system encoding. config_file_path = config_file_path.decode(encoding=sys.getfilesystemencoding()) @@ -67,7 +67,7 @@ class Config(utils.UserDict): for key in (set(self.keys()) - self._schema_keys): warnings.append(( - key, "Unrecognised configuration name: {0}".format(key) + key, "Unrecognised configuration name: {}".format(key) )) return failed, warnings @@ -124,7 +124,7 @@ class Config(utils.UserDict): raise exceptions.ConfigurationError( "The configuration is invalid. The expected type was a key " "value mapping (a python dict) but we got an object of type: " - "{0}".format(type(patch))) + "{}".format(type(patch))) self.user_configs.append(patch) self.data.update(patch) @@ -149,15 +149,15 @@ def _open_config_file(config_file): if hasattr(config_file, 'closed') and config_file.closed: config_file = config_file.name - log.debug("Loading configuration file: {0}".format(config_file)) + log.debug("Loading configuration file: {}".format(config_file)) # If it is a string, we can assume it is a path and attempt to open it. - if isinstance(config_file, utils.string_types): + if isinstance(config_file, str): if os.path.exists(config_file): config_file = open(config_file, 'rb') else: raise exceptions.ConfigurationError( - "Config file '{0}' does not exist.".format(config_file)) + "Config file '{}' does not exist.".format(config_file)) # Ensure file descriptor is at begining config_file.seek(0) @@ -207,11 +207,11 @@ def load_config(config_file=None, **kwargs): if len(errors) > 0: raise exceptions.ConfigurationError( - "Aborted with {0} Configuration Errors!".format(len(errors)) + "Aborted with {} Configuration Errors!".format(len(errors)) ) elif cfg['strict'] and len(warnings) > 0: raise exceptions.ConfigurationError( - "Aborted with {0} Configuration Warnings in 'strict' mode!".format(len(warnings)) + "Aborted with {} Configuration Warnings in 'strict' mode!".format(len(warnings)) ) return cfg diff --git a/mkdocs/config/config_options.py b/mkdocs/config/config_options.py index 7012804b..5fd61f1d 100644 --- a/mkdocs/config/config_options.py +++ b/mkdocs/config/config_options.py @@ -1,15 +1,13 @@ -from __future__ import unicode_literals - import os -import sys from collections import Sequence, namedtuple +from urllib.parse import urlparse import markdown from mkdocs import utils, theme, plugins from mkdocs.config.base import Config, ValidationError -class BaseConfigOption(object): +class BaseConfigOption: def __init__(self): self.warnings = [] @@ -101,7 +99,7 @@ class OptionallyRequired(BaseConfigOption): """ def __init__(self, default=None, required=False): - super(OptionallyRequired, self).__init__() + super().__init__() self.default = default self.required = required @@ -140,14 +138,14 @@ class Type(OptionallyRequired): """ def __init__(self, type_, length=None, **kwargs): - super(Type, self).__init__(**kwargs) + super().__init__(**kwargs) self._type = type_ self.length = length def run_validation(self, value): if not isinstance(value, self._type): - msg = ("Expected type: {0} but received: {1}" + msg = ("Expected type: {} but received: {}" .format(self._type, type(value))) elif self.length is not None and len(value) != self.length: msg = ("Expected type: {0} with length {2} but received: {1} with " @@ -167,20 +165,20 @@ class Choice(OptionallyRequired): """ def __init__(self, choices, **kwargs): - super(Choice, self).__init__(**kwargs) + super().__init__(**kwargs) try: length = len(choices) except TypeError: length = 0 - if not length or isinstance(choices, utils.string_types): + if not length or isinstance(choices, str): raise ValueError('Expected iterable of choices, got {}', choices) self.choices = choices def run_validation(self, value): if value not in self.choices: - msg = ("Expected one of: {0} but received: {1}" + msg = ("Expected one of: {} but received: {}" .format(self.choices, value)) else: return value @@ -191,7 +189,7 @@ class Choice(OptionallyRequired): class Deprecated(BaseConfigOption): def __init__(self, moved_to=None): - super(Deprecated, self).__init__() + super().__init__() self.default = None self.moved_to = moved_to @@ -200,7 +198,7 @@ class Deprecated(BaseConfigOption): if config.get(key_name) is None or self.moved_to is None: return - warning = ('The configuration option {0} has been deprecated and ' + warning = ('The configuration option {} has been deprecated and ' 'will be removed in a future release of MkDocs.' ''.format(key_name)) self.warnings.append(warning) @@ -238,11 +236,11 @@ class IpAddress(OptionallyRequired): try: port = int(port) except Exception: - raise ValidationError("'{0}' is not a valid port".format(port)) + raise ValidationError("'{}' is not a valid port".format(port)) class Address(namedtuple('Address', 'host port')): def __str__(self): - return '{0}:{1}'.format(self.host, self.port) + return '{}:{}'.format(self.host, self.port) return Address(host, port) @@ -255,14 +253,14 @@ class URL(OptionallyRequired): """ def __init__(self, default='', required=False): - super(URL, self).__init__(default, required) + super().__init__(default, required) def run_validation(self, value): if value == '': return value try: - parsed_url = utils.urlparse(value) + parsed_url = urlparse(value) except (AttributeError, TypeError): raise ValidationError("Unable to parse the URL.") @@ -282,7 +280,7 @@ class RepoURL(URL): """ def post_validation(self, config, key_name): - repo_host = utils.urlparse(config['repo_url']).netloc.lower() + repo_host = urlparse(config['repo_url']).netloc.lower() edit_uri = config.get('edit_uri') # derive repo_name from repo_url if unset @@ -321,7 +319,7 @@ class FilesystemObject(Type): Base class for options that point to filesystem objects. """ def __init__(self, exists=False, **kwargs): - super(FilesystemObject, self).__init__(type_=utils.string_types, **kwargs) + super().__init__(type_=str, **kwargs) self.exists = exists self.config_dir = None @@ -329,22 +327,14 @@ class FilesystemObject(Type): self.config_dir = os.path.dirname(config.config_file_path) if config.config_file_path else None def run_validation(self, value): - value = super(FilesystemObject, self).run_validation(value) - # PY2 only: Ensure value is a Unicode string. On PY3 byte strings fail - # the type test (super.run_validation) so we never get this far. - if not isinstance(value, utils.text_type): - try: - # Assume value is encoded with the file system encoding. - value = value.decode(encoding=sys.getfilesystemencoding()) - except UnicodeDecodeError: - raise ValidationError("The path is not a Unicode string.") + value = super().run_validation(value) if self.config_dir and not os.path.isabs(value): value = os.path.join(self.config_dir, value) if self.exists and not self.existence_test(value): raise ValidationError("The path {path} isn't an existing {name}.". format(path=value, name=self.name)) value = os.path.abspath(value) - assert isinstance(value, utils.text_type) + assert isinstance(value, str) return value @@ -364,7 +354,7 @@ class Dir(FilesystemObject): # Validate that the dir is not the parent dir of the config file. if os.path.dirname(config.config_file_path) == config[key_name]: raise ValidationError( - ("The '{0}' should not be the parent directory of the config " + ("The '{}' should not be the parent directory of the config " "file. Use a child directory instead so that the config file " "is a sibling of the config file.").format(key_name)) @@ -388,7 +378,7 @@ class SiteDir(Dir): def post_validation(self, config, key_name): - super(SiteDir, self).post_validation(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 @@ -398,14 +388,14 @@ class SiteDir(Dir): ("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: '{0}', docs_dir: '{1}')" + "(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: '{0}', docs_dir: '{1}')" + "(site_dir: '{}', docs_dir: '{}')" ).format(config['site_dir'], config['docs_dir'])) @@ -417,14 +407,14 @@ class Theme(BaseConfigOption): """ def __init__(self, default=None): - super(Theme, self).__init__() + super().__init__() self.default = default def validate(self, value): if value is None and self.default is not None: value = {'name': self.default} - if isinstance(value, utils.string_types): + if isinstance(value, str): value = {'name': value} themes = utils.get_theme_names() @@ -435,13 +425,13 @@ class Theme(BaseConfigOption): return value raise ValidationError( - "Unrecognised theme name: '{0}'. The available installed themes " - "are: {1}".format(value['name'], ', '.join(themes)) + "Unrecognised theme name: '{}'. The available installed themes " + "are: {}".format(value['name'], ', '.join(themes)) ) raise ValidationError("No theme name set.") - raise ValidationError('Invalid type "{0}". Expected a string or key/value pairs.'.format(type(value))) + raise ValidationError('Invalid type "{}". Expected a string or key/value pairs.'.format(type(value))) def post_validation(self, config, key_name): theme_config = config[key_name] @@ -469,24 +459,24 @@ class Nav(OptionallyRequired): """ def __init__(self, **kwargs): - super(Nav, self).__init__(**kwargs) + super().__init__(**kwargs) self.file_match = utils.is_markdown_file def run_validation(self, value): if not isinstance(value, list): raise ValidationError( - "Expected a list, got {0}".format(type(value))) + "Expected a list, got {}".format(type(value))) if len(value) == 0: return - config_types = set(type(l) for l in value) - if config_types.issubset({utils.text_type, dict, str}): + config_types = {type(l) for l in value} + if config_types.issubset({str, dict, str}): return value - raise ValidationError("Invalid pages config. {0} {1}".format( - config_types, {utils.text_type, dict} + raise ValidationError("Invalid pages config. {} {}".format( + config_types, {str, dict} )) def post_validation(self, config, key_name): @@ -522,7 +512,7 @@ class MarkdownExtensions(OptionallyRequired): config options for them if desired. """ def __init__(self, builtins=None, configkey='mdx_configs', **kwargs): - super(MarkdownExtensions, self).__init__(**kwargs) + super().__init__(**kwargs) self.builtins = builtins or [] self.configkey = configkey self.configdata = {} @@ -541,9 +531,9 @@ class MarkdownExtensions(OptionallyRequired): continue if not isinstance(cfg, dict): raise ValidationError('Invalid config options for Markdown ' - "Extension '{0}'.".format(ext)) + "Extension '{}'.".format(ext)) self.configdata[ext] = cfg - elif isinstance(item, utils.string_types): + elif isinstance(item, str): extensions.append(item) else: raise ValidationError('Invalid Markdown Extensions configuration') @@ -571,7 +561,7 @@ class Plugins(OptionallyRequired): """ def __init__(self, **kwargs): - super(Plugins, self).__init__(**kwargs) + super().__init__(**kwargs) self.installed_plugins = plugins.get_plugins() self.config_file_path = None @@ -590,12 +580,12 @@ class Plugins(OptionallyRequired): 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)) + 'the "{}" plugin.'.format(name)) item = name else: cfg = {} - if not isinstance(item, utils.string_types): + if not isinstance(item, str): raise ValidationError('Invalid Plugins configuration') plgins[item] = self.load_plugin(item, cfg) @@ -604,12 +594,12 @@ class Plugins(OptionallyRequired): def load_plugin(self, name, config): if name not in self.installed_plugins: - raise ValidationError('The "{0}" plugin is not installed'.format(name)) + raise ValidationError('The "{}" plugin is not installed'.format(name)) Plugin = self.installed_plugins[name].load() if not issubclass(Plugin, plugins.BasePlugin): - raise ValidationError('{0}.{1} must be a subclass of {2}.{3}'.format( + raise ValidationError('{}.{} must be a subclass of {}.{}'.format( Plugin.__module__, Plugin.__name__, plugins.BasePlugin.__module__, plugins.BasePlugin.__name__)) diff --git a/mkdocs/config/defaults.py b/mkdocs/config/defaults.py index 4512a125..103e5fa1 100644 --- a/mkdocs/config/defaults.py +++ b/mkdocs/config/defaults.py @@ -1,6 +1,3 @@ -from __future__ import unicode_literals - -from mkdocs import utils from mkdocs.config import config_options # NOTE: The order here is important. During validation some config options @@ -14,10 +11,10 @@ from mkdocs.config import config_options DEFAULT_SCHEMA = ( # Reserved for internal use, stores the mkdocs.yml config file. - ('config_file_path', config_options.Type(utils.string_types)), + ('config_file_path', config_options.Type(str)), # The title to use for the documentation - ('site_name', config_options.Type(utils.string_types, required=True)), + ('site_name', config_options.Type(str, required=True)), # Defines the structure of the navigation. ('nav', config_options.Nav()), @@ -29,9 +26,9 @@ DEFAULT_SCHEMA = ( # A description for the documentation project that will be added to the # HTML meta tags. - ('site_description', config_options.Type(utils.string_types)), + ('site_description', config_options.Type(str)), # The name of the author to add to the HTML meta tags - ('site_author', config_options.Type(utils.string_types)), + ('site_author', config_options.Type(str)), # The MkDocs theme for the documentation. ('theme', config_options.Theme(default='mkdocs')), @@ -43,7 +40,7 @@ DEFAULT_SCHEMA = ( ('site_dir', config_options.SiteDir(default='site')), # A copyright notice to add to the footer of documentation. - ('copyright', config_options.Type(utils.string_types)), + ('copyright', config_options.Type(str)), # set of values for Google analytics containing the account IO and domain, # this should look like, ['UA-27795084-5', 'mkdocs.org'] @@ -67,13 +64,13 @@ DEFAULT_SCHEMA = ( # Default, If repo_url is unset then None, otherwise # "GitHub", "Bitbucket" or "GitLab" for known url or Hostname # for unknown urls. - ('repo_name', config_options.Type(utils.string_types)), + ('repo_name', config_options.Type(str)), # Specify a URI to the docs dir in the project source repo, relative to the # repo_url. When set, a link directly to the page in the source repo will # be added to the generated HTML. If repo_url is not set also, this option # is ignored. - ('edit_uri', config_options.Type(utils.string_types)), + ('edit_uri', config_options.Type(str)), # Specify which css or javascript files from the docs directory should be # additionally included in the site. @@ -98,10 +95,10 @@ DEFAULT_SCHEMA = ( # the remote branch to commit to when using gh-deploy ('remote_branch', config_options.Type( - utils.string_types, default='gh-pages')), + str, default='gh-pages')), # the remote name to push to when using gh-deploy - ('remote_name', config_options.Type(utils.string_types, default='origin')), + ('remote_name', config_options.Type(str, default='origin')), # extra is a mapping/dictionary of data that is passed to the template. # This allows template authors to require extra configuration that not diff --git a/mkdocs/contrib/search/__init__.py b/mkdocs/contrib/search/__init__.py index dae70b93..0c5ac4a3 100644 --- a/mkdocs/contrib/search/__init__.py +++ b/mkdocs/contrib/search/__init__.py @@ -1,7 +1,3 @@ -# coding: utf-8 - -from __future__ import absolute_import, unicode_literals - import os import logging from mkdocs import utils @@ -22,7 +18,7 @@ class LangOption(config_options.OptionallyRequired): return os.path.isfile(path) def run_validation(self, value): - if isinstance(value, utils.string_types): + if isinstance(value, str): value = [value] elif not isinstance(value, (list, tuple)): raise config_options.ValidationError('Expected a list of language codes.') @@ -39,7 +35,7 @@ class SearchPlugin(BasePlugin): config_scheme = ( ('lang', LangOption(default=['en'])), - ('separator', config_options.Type(utils.string_types, default=r'[\s\-]+')), + ('separator', config_options.Type(str, default=r'[\s\-]+')), ('prebuild_index', config_options.Choice((False, True, 'node', 'python'), default=False)), ) diff --git a/mkdocs/contrib/search/search_index.py b/mkdocs/contrib/search/search_index.py index de8d9875..c373cb32 100644 --- a/mkdocs/contrib/search/search_index.py +++ b/mkdocs/contrib/search/search_index.py @@ -1,7 +1,3 @@ -# coding: utf-8 - -from __future__ import unicode_literals - import os import re import json @@ -10,17 +6,12 @@ import subprocess from lunr import lunr -from mkdocs import utils - -try: # pragma: no cover - from html.parser import HTMLParser # noqa -except ImportError: # pragma: no cover - from HTMLParser import HTMLParser # noqa +from html.parser import HTMLParser log = logging.getLogger(__name__) -class SearchIndex(object): +class SearchIndex: """ Search index is a collection of pages and sections (heading tags and their following content are sections). @@ -52,7 +43,7 @@ class SearchIndex(object): self._entries.append({ 'title': title, - 'text': utils.text_type(text.encode('utf-8'), encoding='utf-8'), + 'text': str(text.encode('utf-8'), encoding='utf-8'), 'location': loc }) @@ -96,7 +87,7 @@ class SearchIndex(object): if toc_item is not None: self._add_entry( title=toc_item.title, - text=u" ".join(section.text), + text=" ".join(section.text), loc=abs_url + toc_item.url ) @@ -125,7 +116,7 @@ class SearchIndex(object): log.debug('Pre-built search index created successfully.') else: log.warning('Failed to pre-build search index. Error: {}'.format(err)) - except (OSError, IOError, ValueError) as e: + except (OSError, ValueError) as e: log.warning('Failed to pre-build search index. Error: {}'.format(e)) elif self.config['prebuild_index'] == 'python': idx = lunr( @@ -151,9 +142,7 @@ class HTMLStripper(HTMLParser): """ def __init__(self, *args, **kwargs): - # HTMLParser is a old-style class in Python 2, so - # super() wont work here. - HTMLParser.__init__(self, *args, **kwargs) + super().__init__(*args, **kwargs) self.data = [] @@ -167,7 +156,7 @@ class HTMLStripper(HTMLParser): return '\n'.join(self.data) -class ContentSection(object): +class ContentSection: """ Used by the ContentParser class to capture the information we need when it is parsing the HMTL. @@ -195,9 +184,7 @@ class ContentParser(HTMLParser): def __init__(self, *args, **kwargs): - # HTMLParser is a old-style class in Python 2, so - # super() wont work here. - HTMLParser.__init__(self, *args, **kwargs) + super().__init__(*args, **kwargs) self.data = [] self.section = None diff --git a/mkdocs/exceptions.py b/mkdocs/exceptions.py index cf4ff46b..f968df35 100644 --- a/mkdocs/exceptions.py +++ b/mkdocs/exceptions.py @@ -1,4 +1,3 @@ -from __future__ import unicode_literals from click import ClickException diff --git a/mkdocs/plugins.py b/mkdocs/plugins.py index 6eeda114..02e5c4ec 100644 --- a/mkdocs/plugins.py +++ b/mkdocs/plugins.py @@ -1,11 +1,8 @@ -# coding: utf-8 - """ Implements the plugin API for MkDocs. """ -from __future__ import unicode_literals import pkg_resources import logging @@ -29,10 +26,10 @@ def get_plugins(): plugins = pkg_resources.iter_entry_points(group='mkdocs.plugins') - return dict((plugin.name, plugin) for plugin in plugins) + return {plugin.name: plugin for plugin in plugins} -class BasePlugin(object): +class BasePlugin: """ Plugin base class. @@ -61,7 +58,7 @@ class PluginCollection(OrderedDict): """ def __init__(self, *args, **kwargs): - super(PluginCollection, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.events = {x: [] for x in EVENTS} def _register_event(self, event_name, method): @@ -74,7 +71,7 @@ class PluginCollection(OrderedDict): '{0}.{1} only accepts values which are instances of {3}.{4} ' 'sublcasses'.format(self.__module__, self.__name__, BasePlugin.__module__, BasePlugin.__name__)) - super(PluginCollection, self).__setitem__(key, value, **kwargs) + super().__setitem__(key, value, **kwargs) # Register all of the event methods defined for this Plugin. for event_name in (x for x in dir(value) if x.startswith('on_')): method = getattr(value, event_name) diff --git a/mkdocs/structure/files.py b/mkdocs/structure/files.py index 013a51b2..b10dd4b3 100644 --- a/mkdocs/structure/files.py +++ b/mkdocs/structure/files.py @@ -1,10 +1,8 @@ -# coding: utf-8 - -from __future__ import unicode_literals import fnmatch import os import logging from functools import cmp_to_key +from urllib.parse import quote as urlquote from mkdocs import utils @@ -13,7 +11,7 @@ log = logging.getLogger(__name__) log.addFilter(utils.warning_filter) -class Files(object): +class Files: """ A collection of File objects. """ def __init__(self, files): self._files = files @@ -67,7 +65,7 @@ class Files(object): """ Retrieve static files from Jinja environment and add to collection. """ def filter(name): patterns = ['.*', '*.py', '*.pyc', '*.html', '*readme*', 'mkdocs_theme.yml'] - patterns.extend('*{0}'.format(x) for x in utils.markdown_extensions) + patterns.extend('*{}'.format(x) for x in utils.markdown_extensions) patterns.extend(config['theme'].static_templates) for pattern in patterns: if fnmatch.fnmatch(name.lower(), pattern): @@ -84,7 +82,7 @@ class Files(object): break -class File(object): +class File: """ A MkDocs File object. @@ -128,7 +126,7 @@ class File(object): def __eq__(self, other): def sub_dict(d): - return dict((key, value) for key, value in d.items() if key in ['src_path', 'abs_src_path', 'url']) + return {key: value for key, value in d.items() if key in ['src_path', 'abs_src_path', 'url']} return (isinstance(other, self.__class__) and sub_dict(self.__dict__) == sub_dict(other.__dict__)) @@ -167,7 +165,7 @@ class File(object): url = '.' else: url = dirname + '/' - return utils.urlquote(url) + return urlquote(url) def url_relative_to(self, other): """ Return url for file relative to other file. """ diff --git a/mkdocs/structure/nav.py b/mkdocs/structure/nav.py index a3487eb8..21ae3053 100644 --- a/mkdocs/structure/nav.py +++ b/mkdocs/structure/nav.py @@ -1,16 +1,14 @@ -# coding: utf-8 - -from __future__ import unicode_literals import logging +from urllib.parse import urlparse from mkdocs.structure.pages import Page -from mkdocs.utils import string_types, nest_paths, urlparse, warning_filter +from mkdocs.utils import nest_paths, warning_filter log = logging.getLogger(__name__) log.addFilter(warning_filter) -class Navigation(object): +class Navigation: def __init__(self, items, pages): self.items = items # Nested List with full navigation of Sections, Pages, and Links. self.pages = pages # Flat List of subset of Pages in nav, in order. @@ -31,7 +29,7 @@ class Navigation(object): return len(self.items) -class Section(object): +class Section: def __init__(self, title, children): self.title = title self.children = children @@ -44,7 +42,7 @@ class Section(object): self.is_link = False def __repr__(self): - return "Section(title='{0}')".format(self.title) + return "Section(title='{}')".format(self.title) def _get_active(self): """ Return active status of section. """ @@ -71,7 +69,7 @@ class Section(object): return '\n'.join(ret) -class Link(object): +class Link: def __init__(self, title, url): self.title = title self.url = url @@ -150,7 +148,7 @@ def _data_to_navigation(data, files, config): if isinstance(data, dict): return [ _data_to_navigation((key, value), files, config) - if isinstance(value, string_types) else + if isinstance(value, str) else Section(title=key, children=_data_to_navigation(value, files, config)) for key, value in data.items() ] diff --git a/mkdocs/structure/pages.py b/mkdocs/structure/pages.py index a15a28a8..a10e52ec 100644 --- a/mkdocs/structure/pages.py +++ b/mkdocs/structure/pages.py @@ -1,11 +1,8 @@ -# coding: utf-8 - -from __future__ import unicode_literals - import os -import io import datetime import logging +from urllib.parse import urlparse, urlunparse, urljoin +from urllib.parse import unquote as urlunquote import markdown from markdown.extensions import Extension @@ -13,13 +10,13 @@ from markdown.treeprocessors import Treeprocessor from markdown.util import AMP_SUBSTITUTE from mkdocs.structure.toc import get_toc -from mkdocs.utils import meta, urlparse, urlunparse, urljoin, urlunquote, get_markdown_title, warning_filter +from mkdocs.utils import meta, get_markdown_title, warning_filter log = logging.getLogger(__name__) log.addFilter(warning_filter) -class Page(object): +class Page: def __init__(self, title, file, config): file.page = self self.file = file @@ -57,7 +54,7 @@ class Page(object): def __eq__(self, other): def sub_dict(d): - return dict((key, value) for key, value in d.items() if key in ['title', 'file']) + return {key: value for key, value in d.items() if key in ['title', 'file']} return (isinstance(other, self.__class__) and sub_dict(self.__dict__) == sub_dict(other.__dict__)) @@ -128,9 +125,9 @@ class Page(object): ) if source is None: try: - with io.open(self.file.abs_src_path, 'r', encoding='utf-8-sig', errors='strict') as f: + with open(self.file.abs_src_path, 'r', encoding='utf-8-sig', errors='strict') as f: source = f.read() - except IOError: + except OSError: log.error('File not found: {}'.format(self.file.src_path)) raise except ValueError: diff --git a/mkdocs/structure/toc.py b/mkdocs/structure/toc.py index bb6c7898..009872f9 100644 --- a/mkdocs/structure/toc.py +++ b/mkdocs/structure/toc.py @@ -1,16 +1,11 @@ -# coding: utf-8 """ Deals with generating the per-page table of contents. For the sake of simplicity we use an existing markdown extension to generate an HTML table of contents, and then parse that into the underlying data. """ -from __future__ import unicode_literals -try: # pragma: no cover - from html.parser import HTMLParser # noqa -except ImportError: # pragma: no cover - from HTMLParser import HTMLParser # noqa +from html.parser import HTMLParser def get_toc(toc_html): @@ -18,7 +13,7 @@ def get_toc(toc_html): return TableOfContents(items) -class TableOfContents(object): +class TableOfContents: """ Represents the table of contents for a given page. """ @@ -35,7 +30,7 @@ class TableOfContents(object): return ''.join([str(item) for item in self]) -class AnchorLink(object): +class AnchorLink: """ A single entry in the table of contents. """ @@ -48,7 +43,7 @@ class AnchorLink(object): def indent_print(self, depth=0): indent = ' ' * depth - ret = '%s%s - %s\n' % (indent, self.title, self.url) + ret = '{}{} - {}\n'.format(indent, self.title, self.url) for item in self.children: ret += item.indent_print(depth + 1) return ret diff --git a/mkdocs/tests/base.py b/mkdocs/tests/base.py index 7aa62168..00183fd2 100644 --- a/mkdocs/tests/base.py +++ b/mkdocs/tests/base.py @@ -1,18 +1,8 @@ -from __future__ import unicode_literals import textwrap import markdown import os -import logging -import collections -import unittest from functools import wraps - -try: - # py>=3.2 - from tempfile import TemporaryDirectory -except ImportError: - from backports.tempfile import TemporaryDirectory - +from tempfile import TemporaryDirectory from mkdocs import config from mkdocs import utils @@ -72,7 +62,7 @@ def tempdir(files=None, **kw): assert os.path.isfile(os.path.join(tdir, 'foo.txt')) pth = os.path.join(tdir, 'bar.txt') assert os.path.isfile(pth) - with io.open(pth, 'r', encoding='utf-8') as f: + with open(pth, 'r', encoding='utf-8') as f: assert f.read() == 'bar content' """ files = {f: '' for f in files} if isinstance(files, (list, tuple)) else files or {} @@ -92,7 +82,7 @@ def tempdir(files=None, **kw): return decorator -class PathAssertionMixin(object): +class PathAssertionMixin: """ Assertion methods for testing paths. @@ -137,97 +127,3 @@ class PathAssertionMixin(object): if os.path.isfile(path): msg = self._formatMessage(None, "The path '{}' is a directory that exists".format(path)) raise self.failureException(msg) - - -# Backport unittest.TestCase.assertLogs for Python 2.7 -# see https://github.com/python/cpython/blob/3.6/Lib/unittest/case.py - -if not utils.PY3: - _LoggingWatcher = collections.namedtuple("_LoggingWatcher", - ["records", "output"]) - - class _CapturingHandler(logging.Handler): - """ - A logging handler capturing all (raw and formatted) logging output. - """ - - def __init__(self): - logging.Handler.__init__(self) - self.watcher = _LoggingWatcher([], []) - - def flush(self): - pass - - def emit(self, record): - self.watcher.records.append(record) - msg = self.format(record) - self.watcher.output.append(msg) - - class _AssertLogsContext(object): - """A context manager used to implement TestCase.assertLogs().""" - - LOGGING_FORMAT = "%(levelname)s:%(name)s:%(message)s" - - def __init__(self, test_case, logger_name, level): - self.test_case = test_case - self.logger_name = logger_name - if level: - self.level = logging._levelNames.get(level, level) - else: - self.level = logging.INFO - self.msg = None - - def __enter__(self): - if isinstance(self.logger_name, logging.Logger): - logger = self.logger = self.logger_name - else: - logger = self.logger = logging.getLogger(self.logger_name) - formatter = logging.Formatter(self.LOGGING_FORMAT) - handler = _CapturingHandler() - handler.setFormatter(formatter) - self.watcher = handler.watcher - self.old_handlers = logger.handlers[:] - self.old_level = logger.level - self.old_propagate = logger.propagate - logger.handlers = [handler] - logger.setLevel(self.level) - logger.propagate = False - return handler.watcher - - def __exit__(self, exc_type, exc_value, tb): - self.logger.handlers = self.old_handlers - self.logger.propagate = self.old_propagate - self.logger.setLevel(self.old_level) - if exc_type is not None: - # let unexpected exceptions pass through - return False - if len(self.watcher.records) == 0: - self._raiseFailure( - "no logs of level {} or higher triggered on {}" - .format(logging.getLevelName(self.level), self.logger.name)) - - def _raiseFailure(self, standardMsg): - msg = self.test_case._formatMessage(self.msg, standardMsg) - raise self.test_case.failureException(msg) - - class LogTestCase(unittest.TestCase): - def assertLogs(self, logger=None, level=None): - """Fail unless a log message of level *level* or higher is emitted - on *logger_name* or its children. If omitted, *level* defaults to - INFO and *logger* defaults to the root logger. - This method must be used as a context manager, and will yield - a recording object with two attributes: `output` and `records`. - At the end of the context manager, the `output` attribute will - be a list of the matching formatted log messages and the - `records` attribute will be a list of the corresponding LogRecord - objects. - Example:: - with self.assertLogs('foo', level='INFO') as cm: - logging.getLogger('foo').info('first message') - logging.getLogger('foo.bar').error('second message') - self.assertEqual(cm.output, ['INFO:foo:first message', - 'ERROR:foo.bar:second message']) - """ - return _AssertLogsContext(self, logger, level) -else: - LogTestCase = unittest.TestCase diff --git a/mkdocs/tests/build_tests.py b/mkdocs/tests/build_tests.py index 9f4d96fe..a56fc47b 100644 --- a/mkdocs/tests/build_tests.py +++ b/mkdocs/tests/build_tests.py @@ -1,14 +1,13 @@ #!/usr/bin/env python -# coding: utf-8 -from __future__ import unicode_literals -import mock +from unittest import mock +import unittest from mkdocs.structure.pages import Page from mkdocs.structure.files import File, Files from mkdocs.structure.nav import get_navigation from mkdocs.commands import build -from mkdocs.tests.base import load_config, LogTestCase, tempdir, PathAssertionMixin +from mkdocs.tests.base import load_config, tempdir, PathAssertionMixin from mkdocs.utils import meta @@ -22,7 +21,21 @@ def build_page(title, path, config, md_src=''): return page, files -class BuildTests(PathAssertionMixin, LogTestCase): +class BuildTests(PathAssertionMixin, unittest.TestCase): + + def assert_mock_called_once(self, mock): + """assert that the mock was called only once. + + The `mock.assert_called_once()` method was added in PY36. + TODO: Remove this when PY35 support is dropped. + """ + try: + mock.assert_called_once() + except AttributeError: + if not mock.call_count == 1: + msg = ("Expected '%s' to have been called once. Called %s times." % + (mock._mock_name or 'mock', self.call_count)) + raise AssertionError(msg) # Test build.get_context @@ -184,8 +197,8 @@ class BuildTests(PathAssertionMixin, LogTestCase): cfg = load_config() env = cfg['theme'].get_env() build._build_theme_template('main.html', env, mock.Mock(), cfg, mock.Mock()) - mock_write_file.assert_called_once() - mock_build_template.assert_called_once() + self.assert_mock_called_once(mock_write_file) + self.assert_mock_called_once(mock_build_template) @mock.patch('mkdocs.utils.write_file') @mock.patch('mkdocs.commands.build._build_template', return_value='some content') @@ -194,9 +207,9 @@ class BuildTests(PathAssertionMixin, LogTestCase): cfg = load_config() env = cfg['theme'].get_env() build._build_theme_template('sitemap.xml', env, mock.Mock(), cfg, mock.Mock()) - mock_write_file.assert_called_once() - mock_build_template.assert_called_once() - mock_gzip_open.assert_called_once() + self.assert_mock_called_once(mock_write_file) + self.assert_mock_called_once(mock_build_template) + self.assert_mock_called_once(mock_gzip_open) @mock.patch('mkdocs.utils.write_file') @mock.patch('mkdocs.commands.build._build_template', return_value='') @@ -224,11 +237,11 @@ class BuildTests(PathAssertionMixin, LogTestCase): ["INFO:mkdocs.commands.build:Template skipped: 'main.html' generated empty output."] ) mock_write_file.assert_not_called() - mock_build_template.assert_called_once() + self.assert_mock_called_once(mock_build_template) # Test build._build_extra_template - @mock.patch('io.open', mock.mock_open(read_data='template content')) + @mock.patch('mkdocs.commands.build.open', mock.mock_open(read_data='template content')) def test_build_extra_template(self): cfg = load_config() files = Files([ @@ -236,7 +249,7 @@ class BuildTests(PathAssertionMixin, LogTestCase): ]) build._build_extra_template('foo.html', files, cfg, mock.Mock()) - @mock.patch('io.open', mock.mock_open(read_data='template content')) + @mock.patch('mkdocs.commands.build.open', mock.mock_open(read_data='template content')) def test_skip_missing_extra_template(self): cfg = load_config() files = Files([ @@ -249,7 +262,7 @@ class BuildTests(PathAssertionMixin, LogTestCase): ["WARNING:mkdocs.commands.build:Template skipped: 'missing.html' not found in docs_dir."] ) - @mock.patch('io.open', side_effect=IOError('Error message.')) + @mock.patch('mkdocs.commands.build.open', side_effect=OSError('Error message.')) def test_skip_ioerror_extra_template(self, mock_open): cfg = load_config() files = Files([ @@ -262,7 +275,7 @@ class BuildTests(PathAssertionMixin, LogTestCase): ["WARNING:mkdocs.commands.build:Error reading template 'foo.html': Error message."] ) - @mock.patch('io.open', mock.mock_open(read_data='')) + @mock.patch('mkdocs.commands.build.open', mock.mock_open(read_data='')) def test_skip_extra_template_empty_output(self): cfg = load_config() files = Files([ @@ -306,20 +319,20 @@ class BuildTests(PathAssertionMixin, LogTestCase): self.assertEqual(page.content, None) @tempdir(files={'index.md': 'new page content'}) - @mock.patch('io.open', side_effect=IOError('Error message.')) + @mock.patch('mkdocs.structure.pages.open', side_effect=OSError('Error message.')) def test_populate_page_read_error(self, docs_dir, mock_open): cfg = load_config(docs_dir=docs_dir) file = File('missing.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']) page = Page('Foo', file, cfg) with self.assertLogs('mkdocs', level='ERROR') as cm: - self.assertRaises(IOError, build._populate_page, page, cfg, Files([file])) + self.assertRaises(OSError, build._populate_page, page, cfg, Files([file])) self.assertEqual( cm.output, [ 'ERROR:mkdocs.structure.pages:File not found: missing.md', "ERROR:mkdocs.commands.build:Error reading page 'missing.md': Error message." ] ) - mock_open.assert_called_once() + self.assert_mock_called_once(mock_open) # Test build._build_page @@ -355,7 +368,7 @@ class BuildTests(PathAssertionMixin, LogTestCase): # cm.output, # ["INFO:mkdocs.commands.build:Page skipped: 'index.md'. Generated empty output."] # ) - # mock_template.render.assert_called_once() + # self.assert_mock_called_once(mock_template.render) # self.assertPathNotFile(site_dir, 'index.html') @tempdir(files={'index.md': 'page content'}) @@ -385,7 +398,7 @@ class BuildTests(PathAssertionMixin, LogTestCase): page.markdown = 'page content' page.content = '

page content

' build._build_page(page, cfg, files, nav, cfg['theme'].get_env(), dirty=True) - mock_write_file.assert_called_once() + self.assert_mock_called_once(mock_write_file) @tempdir() def test_build_page_custom_template(self, site_dir): @@ -402,7 +415,7 @@ class BuildTests(PathAssertionMixin, LogTestCase): self.assertPathIsFile(site_dir, 'index.html') @tempdir() - @mock.patch('mkdocs.utils.write_file', side_effect=IOError('Error message.')) + @mock.patch('mkdocs.utils.write_file', side_effect=OSError('Error message.')) def test_build_page_error(self, site_dir, mock_write_file): cfg = load_config(site_dir=site_dir, nav=['index.md'], plugins=[]) files = Files([File('index.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls'])]) @@ -413,12 +426,12 @@ class BuildTests(PathAssertionMixin, LogTestCase): page.markdown = 'page content' page.content = '

page content

' with self.assertLogs('mkdocs', level='ERROR') as cm: - self.assertRaises(IOError, build._build_page, page, cfg, files, nav, cfg['theme'].get_env()) + self.assertRaises(OSError, build._build_page, page, cfg, files, nav, cfg['theme'].get_env()) self.assertEqual( cm.output, ["ERROR:mkdocs.commands.build:Error building page 'index.md': Error message."] ) - mock_write_file.assert_called_once() + self.assert_mock_called_once(mock_write_file) # Test build.build diff --git a/mkdocs/tests/cli_tests.py b/mkdocs/tests/cli_tests.py index febbc20c..d1f02e2b 100644 --- a/mkdocs/tests/cli_tests.py +++ b/mkdocs/tests/cli_tests.py @@ -1,19 +1,14 @@ #!/usr/bin/env python -# coding: utf-8 -from __future__ import unicode_literals import unittest -import mock +from unittest import mock import logging -import sys import io from click.testing import CliRunner from mkdocs import __main__ as cli -PY3 = sys.version_info[0] == 3 - class CLITests(unittest.TestCase): @@ -46,10 +41,7 @@ class CLITests(unittest.TestCase): self.assertEqual(mock_serve.call_count, 1) args, kwargs = mock_serve.call_args self.assertTrue('config_file' in kwargs) - if PY3: - self.assertIsInstance(kwargs['config_file'], io.BufferedReader) - else: - self.assertTrue(isinstance(kwargs['config_file'], file)) # noqa: F821 + self.assertIsInstance(kwargs['config_file'], io.BufferedReader) self.assertEqual(kwargs['config_file'].name, 'mkdocs.yml') @mock.patch('mkdocs.commands.serve.serve', autospec=True) @@ -224,10 +216,7 @@ class CLITests(unittest.TestCase): self.assertEqual(mock_load_config.call_count, 1) args, kwargs = mock_load_config.call_args self.assertTrue('config_file' in kwargs) - if PY3: - self.assertIsInstance(kwargs['config_file'], io.BufferedReader) - else: - self.assertTrue(isinstance(kwargs['config_file'], file)) # noqa: F821 + self.assertIsInstance(kwargs['config_file'], io.BufferedReader) self.assertEqual(kwargs['config_file'].name, 'mkdocs.yml') @mock.patch('mkdocs.config.load_config', autospec=True) @@ -402,10 +391,7 @@ class CLITests(unittest.TestCase): self.assertEqual(mock_load_config.call_count, 1) args, kwargs = mock_load_config.call_args self.assertTrue('config_file' in kwargs) - if PY3: - self.assertIsInstance(kwargs['config_file'], io.BufferedReader) - else: - self.assertTrue(isinstance(kwargs['config_file'], file)) # noqa: F821 + self.assertIsInstance(kwargs['config_file'], io.BufferedReader) self.assertEqual(kwargs['config_file'].name, 'mkdocs.yml') @mock.patch('mkdocs.config.load_config', autospec=True) diff --git a/mkdocs/tests/config/base_tests.py b/mkdocs/tests/config/base_tests.py index 3b4223c8..4277f87f 100644 --- a/mkdocs/tests/config/base_tests.py +++ b/mkdocs/tests/config/base_tests.py @@ -1,15 +1,9 @@ -from __future__ import unicode_literals import os import tempfile import unittest +from tempfile import TemporaryDirectory -try: - # py>=3.2 - from tempfile import TemporaryDirectory -except ImportError: - from backports.tempfile import TemporaryDirectory - -from mkdocs import exceptions, utils +from mkdocs import exceptions from mkdocs.config import base, defaults from mkdocs.config.config_options import BaseConfigOption @@ -274,6 +268,6 @@ class ConfigBaseTests(unittest.TestCase): self.assertEqual(cfg['site_name'], 'MkDocs Test') self.assertEqual(cfg['docs_dir'], docs_dir) self.assertEqual(cfg.config_file_path, config_fname) - self.assertIsInstance(cfg.config_file_path, utils.text_type) + self.assertIsInstance(cfg.config_file_path, str) finally: config_dir.cleanup() diff --git a/mkdocs/tests/config/config_options_tests.py b/mkdocs/tests/config/config_options_tests.py index d0c5a109..da55c1fc 100644 --- a/mkdocs/tests/config/config_options_tests.py +++ b/mkdocs/tests/config/config_options_tests.py @@ -1,14 +1,9 @@ -# coding=UTF-8 - -from __future__ import unicode_literals - import os import sys import unittest -from mock import patch +from unittest.mock import patch import mkdocs -from mkdocs import utils from mkdocs.config import config_options from mkdocs.config.base import Config @@ -54,7 +49,7 @@ class TypeTest(unittest.TestCase): def test_single_type(self): - option = config_options.Type(utils.string_types) + option = config_options.Type(str) value = option.validate("Testing") self.assertEqual(value, "Testing") @@ -71,7 +66,7 @@ class TypeTest(unittest.TestCase): option.validate, {'a': 1}) def test_length(self): - option = config_options.Type(utils.string_types, length=7) + option = config_options.Type(str, length=7) value = option.validate("Testing") self.assertEqual(value, "Testing") @@ -105,7 +100,7 @@ class IpAddressTest(unittest.TestCase): option = config_options.IpAddress() value = option.validate(addr) - self.assertEqual(utils.text_type(value), addr) + self.assertEqual(str(value), addr) self.assertEqual(value.host, '127.0.0.1') self.assertEqual(value.port, 8000) @@ -114,7 +109,7 @@ class IpAddressTest(unittest.TestCase): option = config_options.IpAddress() value = option.validate(addr) - self.assertEqual(utils.text_type(value), addr) + self.assertEqual(str(value), addr) self.assertEqual(value.host, '[::1]') self.assertEqual(value.port, 8000) @@ -123,7 +118,7 @@ class IpAddressTest(unittest.TestCase): option = config_options.IpAddress() value = option.validate(addr) - self.assertEqual(utils.text_type(value), addr) + self.assertEqual(str(value), addr) self.assertEqual(value.host, 'localhost') self.assertEqual(value.port, 8000) @@ -132,7 +127,7 @@ class IpAddressTest(unittest.TestCase): option = config_options.IpAddress(default=addr) value = option.validate(None) - self.assertEqual(utils.text_type(value), addr) + self.assertEqual(str(value), addr) self.assertEqual(value.host, '127.0.0.1') self.assertEqual(value.port, 8000) @@ -310,7 +305,7 @@ class DirTest(unittest.TestCase): self.assertEqual(len(fails), 0) self.assertEqual(len(warns), 0) - self.assertIsInstance(cfg['dir'], utils.text_type) + self.assertIsInstance(cfg['dir'], str) def test_dir_filesystemencoding(self): cfg = Config( @@ -326,16 +321,9 @@ class DirTest(unittest.TestCase): fails, warns = cfg.validate() - if utils.PY3: - # In PY3 string_types does not include byte strings so validation fails - self.assertEqual(len(fails), 1) - self.assertEqual(len(warns), 0) - else: - # In PY2 string_types includes byte strings so validation passes - # This test confirms that the byte string is properly decoded - self.assertEqual(len(fails), 0) - self.assertEqual(len(warns), 0) - self.assertIsInstance(cfg['dir'], utils.text_type) + # str does not include byte strings so validation fails + self.assertEqual(len(fails), 1) + self.assertEqual(len(warns), 0) def test_dir_bad_encoding_fails(self): cfg = Config( @@ -351,12 +339,7 @@ class DirTest(unittest.TestCase): fails, warns = cfg.validate() - if sys.platform.startswith('win') and not utils.PY3: - # PY2 on Windows seems to be able to decode anything we give it. - # But that just means less possable errors for those users so we allow it. - self.assertEqual(len(fails), 0) - else: - self.assertEqual(len(fails), 1) + self.assertEqual(len(fails), 1) self.assertEqual(len(warns), 0) def test_config_dir_prepended(self): @@ -376,7 +359,7 @@ class DirTest(unittest.TestCase): self.assertEqual(len(fails), 0) self.assertEqual(len(warns), 0) - self.assertIsInstance(cfg['dir'], utils.text_type) + self.assertIsInstance(cfg['dir'], str) self.assertEqual(cfg['dir'], os.path.join(base_path, 'foo')) def test_dir_is_config_dir_fails(self): diff --git a/mkdocs/tests/config/config_tests.py b/mkdocs/tests/config/config_tests.py index 5a90480e..5a52051f 100644 --- a/mkdocs/tests/config/config_tests.py +++ b/mkdocs/tests/config/config_tests.py @@ -1,30 +1,17 @@ #!/usr/bin/env python -# coding: utf-8 -from __future__ import unicode_literals import os import tempfile import unittest - -try: - # py>=3.2 - from tempfile import TemporaryDirectory -except ImportError: - from backports.tempfile import TemporaryDirectory - +from tempfile import TemporaryDirectory import mkdocs from mkdocs import config -from mkdocs import utils from mkdocs.config import config_options from mkdocs.exceptions import ConfigurationError from mkdocs.tests.base import dedent -def ensure_utf(string): - return string.encode('utf-8') if not utils.PY3 else string - - class ConfigTests(unittest.TestCase): def test_missing_config_file(self): @@ -60,7 +47,7 @@ class ConfigTests(unittest.TestCase): """) config_file = tempfile.NamedTemporaryFile('w', delete=False) try: - config_file.write(ensure_utf(file_contents)) + config_file.write(file_contents) config_file.flush() config_file.close() @@ -92,7 +79,7 @@ class ConfigTests(unittest.TestCase): config_path = os.path.join(temp_path, 'mkdocs.yml') config_file = open(config_path, 'w') - config_file.write(ensure_utf(file_contents)) + config_file.write(file_contents) config_file.flush() config_file.close() @@ -209,7 +196,7 @@ class ConfigTests(unittest.TestCase): self.assertEqual(len(errors), 0) self.assertEqual(c['theme'].dirs, result['dirs']) self.assertEqual(c['theme'].static_templates, set(result['static_templates'])) - self.assertEqual(dict([(k, c['theme'][k]) for k in iter(c['theme'])]), result['vars']) + self.assertEqual({k: c['theme'][k] for k in iter(c['theme'])}, result['vars']) def test_empty_nav(self): conf = config.Config(schema=config.DEFAULT_SCHEMA) @@ -269,7 +256,7 @@ class ConfigTests(unittest.TestCase): c = config.Config(schema=( ('docs_dir', config_options.Dir(default='docs')), ('site_dir', config_options.SiteDir(default='site')), - ('config_file_path', config_options.Type(utils.string_types)) + ('config_file_path', config_options.Type(str)) )) c.load_dict(patch) diff --git a/mkdocs/tests/gh_deploy_tests.py b/mkdocs/tests/gh_deploy_tests.py index 13f14879..65f0c66a 100644 --- a/mkdocs/tests/gh_deploy_tests.py +++ b/mkdocs/tests/gh_deploy_tests.py @@ -1,15 +1,27 @@ -from __future__ import unicode_literals - import unittest -import mock +from unittest import mock -from mkdocs.tests.base import load_config, LogTestCase +from mkdocs.tests.base import load_config from mkdocs.commands import gh_deploy from mkdocs import __version__ class TestGitHubDeploy(unittest.TestCase): + def assert_mock_called_once(self, mock): + """assert that the mock was called only once. + + The `mock.assert_called_once()` method was added in PY36. + TODO: Remove this when PY35 support is dropped. + """ + try: + mock.assert_called_once() + except AttributeError: + if not mock.call_count == 1: + msg = ("Expected '%s' to have been called once. Called %s times." % + (mock._mock_name or 'mock', self.call_count)) + raise AssertionError(msg) + @mock.patch('subprocess.Popen') def test_is_cwd_git_repo(self, mock_popeno): @@ -29,7 +41,7 @@ class TestGitHubDeploy(unittest.TestCase): mock_popeno().communicate.return_value = (b'6d98394\n', b'') - self.assertEqual(gh_deploy._get_current_sha('.'), u'6d98394') + self.assertEqual(gh_deploy._get_current_sha('.'), '6d98394') @mock.patch('subprocess.Popen') def test_get_remote_url_ssh(self, mock_popeno): @@ -39,7 +51,7 @@ class TestGitHubDeploy(unittest.TestCase): b'' ) - expected = (u'git@', u'mkdocs/mkdocs.git') + expected = ('git@', 'mkdocs/mkdocs.git') self.assertEqual(expected, gh_deploy._get_remote_url('origin')) @mock.patch('subprocess.Popen') @@ -50,7 +62,7 @@ class TestGitHubDeploy(unittest.TestCase): b'' ) - expected = (u'https://', u'mkdocs/mkdocs.git') + expected = ('https://', 'mkdocs/mkdocs.git') self.assertEqual(expected, gh_deploy._get_remote_url('origin')) @mock.patch('subprocess.Popen') @@ -93,7 +105,7 @@ class TestGitHubDeploy(unittest.TestCase): @mock.patch('mkdocs.commands.gh_deploy._is_cwd_git_repo', return_value=True) @mock.patch('mkdocs.commands.gh_deploy._get_current_sha', return_value='shashas') @mock.patch('mkdocs.commands.gh_deploy._get_remote_url', return_value=( - u'git@', u'mkdocs/mkdocs.git')) + 'git@', 'mkdocs/mkdocs.git')) @mock.patch('mkdocs.commands.gh_deploy._check_version') @mock.patch('mkdocs.commands.gh_deploy.ghp_import.ghp_import', return_value=(True, '')) def test_deploy_hostname(self, mock_import, check_version, get_remote, get_sha, is_repo): @@ -114,7 +126,7 @@ class TestGitHubDeploy(unittest.TestCase): remote_branch='test', ) gh_deploy.gh_deploy(config) - check_version.assert_called_once() + self.assert_mock_called_once(check_version) @mock.patch('mkdocs.commands.gh_deploy._is_cwd_git_repo', return_value=True) @mock.patch('mkdocs.commands.gh_deploy._get_current_sha', return_value='shashas') @@ -147,7 +159,7 @@ class TestGitHubDeploy(unittest.TestCase): error_string) -class TestGitHubDeployLogs(LogTestCase): +class TestGitHubDeployLogs(unittest.TestCase): @mock.patch('subprocess.Popen') def test_mkdocs_newer(self, mock_popeno): diff --git a/mkdocs/tests/integration.py b/mkdocs/tests/integration.py index 0ea7b653..f4a3f2ab 100644 --- a/mkdocs/tests/integration.py +++ b/mkdocs/tests/integration.py @@ -14,7 +14,6 @@ TODOs - Build documentation other than just MkDocs as it is relatively simple. """ -from __future__ import unicode_literals import click import logging @@ -50,7 +49,7 @@ def main(output=None): log.debug("Building installed themes.") for theme in sorted(MKDOCS_THEMES): - log.debug("Building theme: {0}".format(theme)) + log.debug("Building theme: {}".format(theme)) project_dir = os.path.dirname(MKDOCS_CONFIG) out = os.path.join(output, theme) command = base_cmd + [out, '--theme', theme] @@ -58,13 +57,13 @@ def main(output=None): log.debug("Building test projects.") for project in os.listdir(TEST_PROJECTS): - log.debug("Building test project: {0}".format(project)) + log.debug("Building test project: {}".format(project)) project_dir = os.path.join(TEST_PROJECTS, project) out = os.path.join(output, project) command = base_cmd + [out, ] subprocess.check_call(command, cwd=project_dir) - log.debug("Theme and integration builds are in {0}".format(output)) + log.debug("Theme and integration builds are in {}".format(output)) if __name__ == '__main__': diff --git a/mkdocs/tests/new_tests.py b/mkdocs/tests/new_tests.py index 0b81dd02..735fdf5d 100644 --- a/mkdocs/tests/new_tests.py +++ b/mkdocs/tests/new_tests.py @@ -1,7 +1,5 @@ #!/usr/bin/env python -# coding: utf-8 -from __future__ import unicode_literals import tempfile import unittest import os diff --git a/mkdocs/tests/plugin_tests.py b/mkdocs/tests/plugin_tests.py index 23683bd5..4c847161 100644 --- a/mkdocs/tests/plugin_tests.py +++ b/mkdocs/tests/plugin_tests.py @@ -1,20 +1,17 @@ #!/usr/bin/env python -# coding: utf-8 -from __future__ import unicode_literals import unittest -import mock +from unittest import mock import os from mkdocs import plugins -from mkdocs import utils from mkdocs import config class DummyPlugin(plugins.BasePlugin): config_scheme = ( - ('foo', config.config_options.Type(utils.string_types, default='default foo')), + ('foo', config.config_options.Type(str, default='default foo')), ('bar', config.config_options.Type(int, default=0)), ('dir', config.config_options.Dir(exists=False)), ) diff --git a/mkdocs/tests/search_tests.py b/mkdocs/tests/search_tests.py index c4b2310e..c90d06c2 100644 --- a/mkdocs/tests/search_tests.py +++ b/mkdocs/tests/search_tests.py @@ -1,9 +1,7 @@ #!/usr/bin/env python -# coding: utf-8 -from __future__ import unicode_literals import unittest -import mock +from unittest import mock import json from mkdocs.structure.files import File @@ -110,7 +108,7 @@ class SearchPluginTests(unittest.TestCase): result = plugin.on_config(load_config(theme='mkdocs', extra_javascript=[])) self.assertFalse(result['theme']['search_index_only']) self.assertFalse(result['theme']['include_search_page']) - self.assertEqual(result['theme'].static_templates, set(['404.html', 'sitemap.xml'])) + self.assertEqual(result['theme'].static_templates, {'404.html', 'sitemap.xml'}) self.assertEqual(len(result['theme'].dirs), 3) self.assertEqual(result['extra_javascript'], ['search/main.js']) @@ -121,7 +119,7 @@ class SearchPluginTests(unittest.TestCase): result = plugin.on_config(config) self.assertFalse(result['theme']['search_index_only']) self.assertTrue(result['theme']['include_search_page']) - self.assertEqual(result['theme'].static_templates, set(['404.html', 'sitemap.xml', 'search.html'])) + self.assertEqual(result['theme'].static_templates, {'404.html', 'sitemap.xml', 'search.html'}) self.assertEqual(len(result['theme'].dirs), 3) self.assertEqual(result['extra_javascript'], ['search/main.js']) @@ -132,7 +130,7 @@ class SearchPluginTests(unittest.TestCase): result = plugin.on_config(config) self.assertTrue(result['theme']['search_index_only']) self.assertFalse(result['theme']['include_search_page']) - self.assertEqual(result['theme'].static_templates, set(['404.html', 'sitemap.xml'])) + self.assertEqual(result['theme'].static_templates, {'404.html', 'sitemap.xml'}) self.assertEqual(len(result['theme'].dirs), 2) self.assertEqual(len(result['extra_javascript']), 0) @@ -309,15 +307,15 @@ class SearchIndexTests(unittest.TestCase): self.assertEqual(index._entries[1]['title'], "Heading 1") self.assertEqual(index._entries[1]['text'], "Content 1") - self.assertEqual(index._entries[1]['location'], "{0}#heading-1".format(loc)) + self.assertEqual(index._entries[1]['location'], "{}#heading-1".format(loc)) self.assertEqual(index._entries[2]['title'], "Heading 2") self.assertEqual(strip_whitespace(index._entries[2]['text']), "Content2") - self.assertEqual(index._entries[2]['location'], "{0}#heading-2".format(loc)) + self.assertEqual(index._entries[2]['location'], "{}#heading-2".format(loc)) self.assertEqual(index._entries[3]['title'], "Heading 3") self.assertEqual(strip_whitespace(index._entries[3]['text']), "Content3") - self.assertEqual(index._entries[3]['location'], "{0}#heading-3".format(loc)) + self.assertEqual(index._entries[3]['location'], "{}#heading-3".format(loc)) @mock.patch('subprocess.Popen', autospec=True) def test_prebuild_index(self, mock_popen): @@ -361,7 +359,7 @@ class SearchIndexTests(unittest.TestCase): # See https://stackoverflow.com/a/36501078/866026 mock_popen.return_value = mock.Mock() mock_popen_obj = mock_popen.return_value - mock_popen_obj.communicate.side_effect = IOError + mock_popen_obj.communicate.side_effect = OSError mock_popen_obj.returncode = 1 index = search_index.SearchIndex(prebuild_index=True) diff --git a/mkdocs/tests/structure/file_tests.py b/mkdocs/tests/structure/file_tests.py index c8a2f82c..59bcdb8b 100644 --- a/mkdocs/tests/structure/file_tests.py +++ b/mkdocs/tests/structure/file_tests.py @@ -1,7 +1,6 @@ import unittest import os -import io -import mock +from unittest import mock from mkdocs.structure.files import Files, File, get_files, _sort_files, _filter_paths from mkdocs.tests.base import load_config, tempdir, PathAssertionMixin @@ -619,7 +618,7 @@ class TestFiles(PathAssertionMixin, unittest.TestCase): dest_path = os.path.join(dest_dir, 'test.txt') file.copy_file(dirty=False) self.assertPathIsFile(dest_path) - with io.open(dest_path, 'r', encoding='utf-8') as f: + with open(dest_path, 'r', encoding='utf-8') as f: self.assertEqual(f.read(), 'source content') @tempdir(files={'test.txt': 'destination content'}) @@ -630,7 +629,7 @@ class TestFiles(PathAssertionMixin, unittest.TestCase): dest_path = os.path.join(dest_dir, 'test.txt') file.copy_file(dirty=True) self.assertPathIsFile(dest_path) - with io.open(dest_path, 'r', encoding='utf-8') as f: + with open(dest_path, 'r', encoding='utf-8') as f: self.assertEqual(f.read(), 'source content') @tempdir(files={'test.txt': 'destination content'}) @@ -641,5 +640,5 @@ class TestFiles(PathAssertionMixin, unittest.TestCase): dest_path = os.path.join(dest_dir, 'test.txt') file.copy_file(dirty=True) self.assertPathIsFile(dest_path) - with io.open(dest_path, 'r', encoding='utf-8') as f: + with open(dest_path, 'r', encoding='utf-8') as f: self.assertEqual(f.read(), 'destination content') diff --git a/mkdocs/tests/structure/nav_tests.py b/mkdocs/tests/structure/nav_tests.py index b1bd875f..ae130fcf 100644 --- a/mkdocs/tests/structure/nav_tests.py +++ b/mkdocs/tests/structure/nav_tests.py @@ -1,17 +1,15 @@ #!/usr/bin/env python -# coding: utf-8 -from __future__ import unicode_literals import sys import unittest from mkdocs.structure.nav import get_navigation from mkdocs.structure.files import File, Files from mkdocs.structure.pages import Page -from mkdocs.tests.base import dedent, load_config, LogTestCase +from mkdocs.tests.base import dedent, load_config -class SiteNavigationTests(LogTestCase): +class SiteNavigationTests(unittest.TestCase): maxDiff = None diff --git a/mkdocs/tests/structure/page_tests.py b/mkdocs/tests/structure/page_tests.py index fd015ead..c5bcea84 100644 --- a/mkdocs/tests/structure/page_tests.py +++ b/mkdocs/tests/structure/page_tests.py @@ -1,20 +1,12 @@ -from __future__ import unicode_literals - import unittest import os import sys -import mock -import io - -try: - # py>=3.2 - from tempfile import TemporaryDirectory -except ImportError: - from backports.tempfile import TemporaryDirectory +from unittest import mock +from tempfile import TemporaryDirectory from mkdocs.structure.pages import Page from mkdocs.structure.files import File, Files -from mkdocs.tests.base import load_config, dedent, LogTestCase +from mkdocs.tests.base import load_config, dedent class PageTests(unittest.TestCase): @@ -426,7 +418,7 @@ class PageTests(unittest.TestCase): fl = File('index.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']) pg = Page(None, fl, cfg) # Create an UTF-8 Encoded file with BOM (as Micorsoft editors do). See #1186 - with io.open(fl.abs_src_path, 'w', encoding='utf-8-sig') as f: + with open(fl.abs_src_path, 'w', encoding='utf-8-sig') as f: f.write(md_src) # Now read the file. pg.read_source(cfg) @@ -673,7 +665,7 @@ class PageTests(unittest.TestCase): cfg = load_config() fl = File('missing.md', cfg['docs_dir'], cfg['site_dir'], cfg['use_directory_urls']) pg = Page('Foo', fl, cfg) - self.assertRaises(IOError, pg.read_source, cfg) + self.assertRaises(OSError, pg.read_source, cfg) class SourceDateEpochTests(unittest.TestCase): @@ -695,7 +687,7 @@ class SourceDateEpochTests(unittest.TestCase): del os.environ['SOURCE_DATE_EPOCH'] -class RelativePathExtensionTests(LogTestCase): +class RelativePathExtensionTests(unittest.TestCase): DOCS_DIR = os.path.join(os.path.abspath(os.path.dirname(__file__)), '../integration/subpages/docs') @@ -709,112 +701,112 @@ class RelativePathExtensionTests(LogTestCase): pg.render(cfg, Files(fs)) return pg.content - @mock.patch('io.open', mock.mock_open(read_data='[link](non-index.md)')) + @mock.patch('mkdocs.structure.pages.open', mock.mock_open(read_data='[link](non-index.md)')) def test_relative_html_link(self): self.assertEqual( self.get_rendered_result(['index.md', 'non-index.md']), '

link

' # No trailing / ) - @mock.patch('io.open', mock.mock_open(read_data='[link](index.md)')) + @mock.patch('mkdocs.structure.pages.open', mock.mock_open(read_data='[link](index.md)')) def test_relative_html_link_index(self): self.assertEqual( self.get_rendered_result(['non-index.md', 'index.md']), '

link

' ) - @mock.patch('io.open', mock.mock_open(read_data='[link](sub2/index.md)')) + @mock.patch('mkdocs.structure.pages.open', mock.mock_open(read_data='[link](sub2/index.md)')) def test_relative_html_link_sub_index(self): self.assertEqual( self.get_rendered_result(['index.md', 'sub2/index.md']), '

link

' # No trailing / ) - @mock.patch('io.open', mock.mock_open(read_data='[link](sub2/non-index.md)')) + @mock.patch('mkdocs.structure.pages.open', mock.mock_open(read_data='[link](sub2/non-index.md)')) def test_relative_html_link_sub_page(self): self.assertEqual( self.get_rendered_result(['index.md', 'sub2/non-index.md']), '

link

' # No trailing / ) - @mock.patch('io.open', mock.mock_open(read_data='[link](file%20name.md)')) + @mock.patch('mkdocs.structure.pages.open', mock.mock_open(read_data='[link](file%20name.md)')) def test_relative_html_link_with_encoded_space(self): self.assertEqual( self.get_rendered_result(['index.md', 'file name.md']), '

link

' ) - @mock.patch('io.open', mock.mock_open(read_data='[link](file name.md)')) + @mock.patch('mkdocs.structure.pages.open', mock.mock_open(read_data='[link](file name.md)')) def test_relative_html_link_with_unencoded_space(self): self.assertEqual( self.get_rendered_result(['index.md', 'file name.md']), '

link

' ) - @mock.patch('io.open', mock.mock_open(read_data='[link](../index.md)')) + @mock.patch('mkdocs.structure.pages.open', mock.mock_open(read_data='[link](../index.md)')) def test_relative_html_link_parent_index(self): self.assertEqual( self.get_rendered_result(['sub2/non-index.md', 'index.md']), '

link

' ) - @mock.patch('io.open', mock.mock_open(read_data='[link](non-index.md#hash)')) + @mock.patch('mkdocs.structure.pages.open', mock.mock_open(read_data='[link](non-index.md#hash)')) def test_relative_html_link_hash(self): self.assertEqual( self.get_rendered_result(['index.md', 'non-index.md']), '

link

' ) - @mock.patch('io.open', mock.mock_open(read_data='[link](sub2/index.md#hash)')) + @mock.patch('mkdocs.structure.pages.open', mock.mock_open(read_data='[link](sub2/index.md#hash)')) def test_relative_html_link_sub_index_hash(self): self.assertEqual( self.get_rendered_result(['index.md', 'sub2/index.md']), '

link

' ) - @mock.patch('io.open', mock.mock_open(read_data='[link](sub2/non-index.md#hash)')) + @mock.patch('mkdocs.structure.pages.open', mock.mock_open(read_data='[link](sub2/non-index.md#hash)')) def test_relative_html_link_sub_page_hash(self): self.assertEqual( self.get_rendered_result(['index.md', 'sub2/non-index.md']), '

link

' ) - @mock.patch('io.open', mock.mock_open(read_data='[link](#hash)')) + @mock.patch('mkdocs.structure.pages.open', mock.mock_open(read_data='[link](#hash)')) def test_relative_html_link_hash_only(self): self.assertEqual( self.get_rendered_result(['index.md']), '

link

' ) - @mock.patch('io.open', mock.mock_open(read_data='![image](image.png)')) + @mock.patch('mkdocs.structure.pages.open', mock.mock_open(read_data='![image](image.png)')) def test_relative_image_link_from_homepage(self): self.assertEqual( self.get_rendered_result(['index.md', 'image.png']), '

image

' # no opening ./ ) - @mock.patch('io.open', mock.mock_open(read_data='![image](../image.png)')) + @mock.patch('mkdocs.structure.pages.open', mock.mock_open(read_data='![image](../image.png)')) def test_relative_image_link_from_subpage(self): self.assertEqual( self.get_rendered_result(['sub2/non-index.md', 'image.png']), '

image

' ) - @mock.patch('io.open', mock.mock_open(read_data='![image](image.png)')) + @mock.patch('mkdocs.structure.pages.open', mock.mock_open(read_data='![image](image.png)')) def test_relative_image_link_from_sibling(self): self.assertEqual( self.get_rendered_result(['non-index.md', 'image.png']), '

image

' ) - @mock.patch('io.open', mock.mock_open(read_data='*__not__ a link*.')) + @mock.patch('mkdocs.structure.pages.open', mock.mock_open(read_data='*__not__ a link*.')) def test_no_links(self): self.assertEqual( self.get_rendered_result(['index.md']), '

not a link.

' ) - @mock.patch('io.open', mock.mock_open(read_data='[link](non-existant.md)')) + @mock.patch('mkdocs.structure.pages.open', mock.mock_open(read_data='[link](non-existant.md)')) def test_bad_relative_html_link(self): with self.assertLogs('mkdocs', level='WARNING') as cm: self.assertEqual( @@ -827,21 +819,21 @@ class RelativePathExtensionTests(LogTestCase): "to 'non-existant.md' which is not found in the documentation files."] ) - @mock.patch('io.open', mock.mock_open(read_data='[external link](http://example.com/index.md)')) + @mock.patch('mkdocs.structure.pages.open', mock.mock_open(read_data='[external](http://example.com/index.md)')) def test_external_link(self): self.assertEqual( self.get_rendered_result(['index.md']), - '

external link

' + '

external

' ) - @mock.patch('io.open', mock.mock_open(read_data='[absolute link](/path/to/file.md)')) + @mock.patch('mkdocs.structure.pages.open', mock.mock_open(read_data='[absolute link](/path/to/file.md)')) def test_absolute_link(self): self.assertEqual( self.get_rendered_result(['index.md']), '

absolute link

' ) - @mock.patch('io.open', mock.mock_open(read_data='')) + @mock.patch('mkdocs.structure.pages.open', mock.mock_open(read_data='')) def test_email_link(self): self.assertEqual( self.get_rendered_result(['index.md']), diff --git a/mkdocs/tests/structure/toc_tests.py b/mkdocs/tests/structure/toc_tests.py index 716b0397..e0c254d6 100644 --- a/mkdocs/tests/structure/toc_tests.py +++ b/mkdocs/tests/structure/toc_tests.py @@ -1,7 +1,5 @@ #!/usr/bin/env python -# coding: utf-8 -from __future__ import unicode_literals import unittest from mkdocs.structure.toc import get_toc from mkdocs.tests.base import dedent, get_markdown_toc @@ -192,7 +190,6 @@ class TableOfContentsTests(unittest.TestCase): def get_level_sequence(items): for item in items: yield item.level - for c in get_level_sequence(item.children): - yield c + yield from get_level_sequence(item.children) self.assertEqual(tuple(get_level_sequence(toc)), (0, 1, 2, 2, 1)) diff --git a/mkdocs/tests/theme_tests.py b/mkdocs/tests/theme_tests.py index f87a58ac..21676da6 100644 --- a/mkdocs/tests/theme_tests.py +++ b/mkdocs/tests/theme_tests.py @@ -1,9 +1,7 @@ -from __future__ import unicode_literals - import os import tempfile import unittest -import mock +from unittest import mock import mkdocs from mkdocs.theme import Theme @@ -16,7 +14,7 @@ theme_dir = os.path.abspath(os.path.join(mkdocs_dir, 'themes')) def get_vars(theme): """ Return dict of theme vars. """ - return dict([(k, theme[k]) for k in iter(theme)]) + return {k: theme[k] for k in iter(theme)} class ThemeTests(unittest.TestCase): @@ -27,7 +25,7 @@ class ThemeTests(unittest.TestCase): theme.dirs, [os.path.join(theme_dir, 'mkdocs'), mkdocs_templates_dir] ) - self.assertEqual(theme.static_templates, set(['404.html', 'sitemap.xml'])) + self.assertEqual(theme.static_templates, {'404.html', 'sitemap.xml'}) self.assertEqual(get_vars(theme), { 'include_search_page': False, 'search_index_only': False, @@ -62,7 +60,7 @@ class ThemeTests(unittest.TestCase): theme = Theme(name='mkdocs', static_templates='foo.html') self.assertEqual( theme.static_templates, - set(['404.html', 'sitemap.xml', 'foo.html']) + {'404.html', 'sitemap.xml', 'foo.html'} ) def test_vars(self): @@ -79,7 +77,7 @@ class ThemeTests(unittest.TestCase): def test_no_theme_config(self, m): theme = Theme(name='mkdocs') self.assertEqual(m.call_count, 1) - self.assertEqual(theme.static_templates, set(['sitemap.xml'])) + self.assertEqual(theme.static_templates, {'sitemap.xml'}) def test_inherited_theme(self): m = mock.Mock(side_effect=[ @@ -98,5 +96,5 @@ class ThemeTests(unittest.TestCase): ] ) self.assertEqual( - theme.static_templates, set(['sitemap.xml', 'child.html', 'parent.html']) + theme.static_templates, {'sitemap.xml', 'child.html', 'parent.html'} ) diff --git a/mkdocs/tests/utils/ghp_import_tests.py b/mkdocs/tests/utils/ghp_import_tests.py index c65337c4..84b4451f 100644 --- a/mkdocs/tests/utils/ghp_import_tests.py +++ b/mkdocs/tests/utils/ghp_import_tests.py @@ -1,9 +1,7 @@ #!/usr/bin/env python -# coding: utf-8 -from __future__ import unicode_literals -import mock +from unittest import mock import os import subprocess import tempfile @@ -46,7 +44,7 @@ class UtilsTests(unittest.TestCase): result = ghp_import.get_prev_commit('test-branch') - self.assertEqual(result, u'4c82346e4b1b816be89dd709d35a6b169aa3df61') + self.assertEqual(result, '4c82346e4b1b816be89dd709d35a6b169aa3df61') mock_popen.assert_called_once_with( ['git', 'rev-list', '--max-count=1', 'test-branch', '--'], stdin=subprocess.PIPE, stdout=subprocess.PIPE, @@ -62,7 +60,7 @@ class UtilsTests(unittest.TestCase): result = ghp_import.get_config('user.name') - self.assertEqual(result, u'Dougal Matthews') + self.assertEqual(result, 'Dougal Matthews') mock_popen.assert_called_once_with( ['git', 'config', 'user.name'], stdout=subprocess.PIPE, stdin=subprocess.PIPE) diff --git a/mkdocs/tests/utils/utils_tests.py b/mkdocs/tests/utils/utils_tests.py index 00e9adae..609b9f00 100644 --- a/mkdocs/tests/utils/utils_tests.py +++ b/mkdocs/tests/utils/utils_tests.py @@ -1,9 +1,7 @@ #!/usr/bin/env python -# coding: utf-8 -from __future__ import unicode_literals -import mock +from unittest import mock import os import unittest import tempfile @@ -304,8 +302,8 @@ class UtilsTests(unittest.TestCase): ) config = utils.yaml_load(yaml_src) - self.assertTrue(isinstance(config['key'], utils.text_type)) - self.assertTrue(isinstance(config['key2'][0], utils.text_type)) + self.assertTrue(isinstance(config['key'], str)) + self.assertTrue(isinstance(config['key2'][0], str)) def test_copy_files(self): src_paths = [ diff --git a/mkdocs/theme.py b/mkdocs/theme.py index bcdbf84b..3f1c3803 100644 --- a/mkdocs/theme.py +++ b/mkdocs/theme.py @@ -1,6 +1,3 @@ -# coding: utf-8 - -from __future__ import unicode_literals import os import jinja2 import logging @@ -13,7 +10,7 @@ log = logging.getLogger(__name__) log.addFilter(utils.warning_filter) -class Theme(object): +class Theme: """ A Theme object. @@ -55,9 +52,9 @@ class Theme(object): self._vars.update(user_config) def __repr__(self): - return "{0}(name='{1}', dirs={2}, static_templates={3}, {4})".format( + return "{}(name='{}', dirs={}, static_templates={}, {})".format( self.__class__.__name__, self.name, self.dirs, list(self.static_templates), - ', '.join('{0}={1}'.format(k, repr(v)) for k, v in self._vars.items()) + ', '.join('{}={}'.format(k, repr(v)) for k, v in self._vars.items()) ) def __getitem__(self, key): @@ -84,10 +81,10 @@ class Theme(object): theme_config = utils.yaml_load(f) if theme_config is None: theme_config = {} - except IOError as e: + except OSError as e: log.debug(e) raise ValidationError( - "The theme '{0}' does not appear to have a configuration file. " + "The theme '{}' does not appear to have a configuration file. " "Please upgrade to a current version of the theme.".format(name) ) @@ -98,8 +95,8 @@ class Theme(object): themes = utils.get_theme_names() if parent_theme not in themes: raise ValidationError( - "The theme '{0}' inherits from '{1}', which does not appear to be installed. " - "The available installed themes are: {2}".format(name, parent_theme, ', '.join(themes)) + "The theme '{}' inherits from '{}', which does not appear to be installed. " + "The available installed themes are: {}".format(name, parent_theme, ', '.join(themes)) ) self._load_theme_config(parent_theme) diff --git a/mkdocs/utils/__init__.py b/mkdocs/utils/__init__.py index 8ee3e2e8..7b26a7c2 100644 --- a/mkdocs/utils/__init__.py +++ b/mkdocs/utils/__init__.py @@ -1,5 +1,3 @@ -# coding: utf-8 - """ Standalone file utils. @@ -7,47 +5,19 @@ Nothing in this module should have an knowledge of config or the layout and structure of the site and pages in the site. """ -from __future__ import unicode_literals import logging import os import pkg_resources import shutil import re -import sys import yaml import fnmatch import posixpath +from urllib.parse import urlparse from mkdocs import exceptions -try: # pragma: no cover - from urllib.parse import urlparse, urlunparse, urljoin # noqa - from urllib.parse import quote as urlquote # noqa - from urllib.parse import unquote as urlunquote # noqa - from collections import UserDict # noqa -except ImportError: # pragma: no cover - from urlparse import urlparse, urlunparse, urljoin # noqa - from urllib import quote # noqa - from urllib import unquote # noqa - from UserDict import UserDict # noqa - - -PY3 = sys.version_info[0] == 3 - -if PY3: # pragma: no cover - string_types = str, # noqa - text_type = str # noqa -else: # pragma: no cover - string_types = basestring, # noqa - text_type = unicode # noqa - - def urlunquote(path): # noqa - return unquote(path.encode('utf8', errors='backslashreplace')).decode('utf8', errors='replace') - - def urlquote(path): # noqa - return quote(path.encode('utf8', errors='backslashreplace')).decode('utf8', errors='replace') - log = logging.getLogger(__name__) markdown_extensions = [ @@ -200,7 +170,7 @@ def is_markdown_file(path): https://superuser.com/questions/249436/file-extension-for-markdown-files """ - return any(fnmatch.fnmatch(path.lower(), '*{0}'.format(x)) for x in markdown_extensions) + return any(fnmatch.fnmatch(path.lower(), '*{}'.format(x)) for x in markdown_extensions) def is_html_file(path): @@ -298,7 +268,7 @@ def get_themes(): if theme.name in builtins and theme.dist.key != 'mkdocs': raise exceptions.ConfigurationError( - "The theme {0} is a builtin theme but {1} provides a theme " + "The theme {} is a builtin theme but {} provides a theme " "with the same name".format(theme.name, theme.dist.key)) elif theme.name in themes: diff --git a/mkdocs/utils/ghp_import.py b/mkdocs/utils/ghp_import.py index 9b0ebb9a..6abcc679 100644 --- a/mkdocs/utils/ghp_import.py +++ b/mkdocs/utils/ghp_import.py @@ -15,7 +15,6 @@ # 0. opan saurce LOL -from __future__ import unicode_literals import errno import logging @@ -42,7 +41,7 @@ if sys.version_info[0] == 3: def write(pipe, data): try: pipe.stdin.write(data) - except IOError as e: + except OSError as e: if e.errno != errno.EPIPE: raise else: @@ -69,7 +68,7 @@ def normalize_path(path): def try_rebase(remote, branch): - cmd = ['git', 'rev-list', '--max-count=1', '%s/%s' % (remote, branch)] + cmd = ['git', 'rev-list', '--max-count=1', '{}/{}'.format(remote, branch)] p = sp.Popen(cmd, stdin=sp.PIPE, stdout=sp.PIPE, stderr=sp.PIPE) (rev, _) = p.communicate() if p.wait() != 0: @@ -99,14 +98,14 @@ def mk_when(timestamp=None): if timestamp is None: timestamp = int(time.time()) currtz = "%+05d" % (-1 * time.timezone / 36) # / 3600 * 100 - return "%s %s" % (timestamp, currtz) + return "{} {}".format(timestamp, currtz) def start_commit(pipe, branch, message): uname = dec(get_config("user.name")) email = dec(get_config("user.email")) write(pipe, enc('commit refs/heads/%s\n' % branch)) - write(pipe, enc('committer %s <%s> %s\n' % (uname, email, mk_when()))) + write(pipe, enc('committer {} <{}> {}\n'.format(uname, email, mk_when()))) write(pipe, enc('data %d\n%s\n' % (len(message), message))) head = get_prev_commit(branch) if head: diff --git a/requirements/project.txt b/requirements/project.txt index 57670b45..c96acee9 100644 --- a/requirements/project.txt +++ b/requirements/project.txt @@ -1,7 +1,7 @@ -click>=3.3 -Jinja2>=2.7.1 -livereload>=2.5.1 -Markdown>=2.5 -PyYAML>=3.13 -tornado>=5.0 +click>=7.0 +Jinja2>=2.10.3 +livereload>=2.6.1 +Markdown>=3.0.1 +PyYAML>=5.2 +tornado>=5.1.1 mdx_gh_links>=0.2 diff --git a/requirements/test.txt b/requirements/test.txt index d9425c03..9e8ef9b6 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -1,4 +1,3 @@ coverage flake8 nose -mock diff --git a/setup.py b/setup.py index e42ed4ee..10c90f4b 100755 --- a/setup.py +++ b/setup.py @@ -1,7 +1,5 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- -from __future__ import print_function from setuptools import setup import re import os @@ -64,7 +62,7 @@ setup( 'PyYAML>=3.10', 'tornado>=5.0' ], - python_requires='>=2.7.9,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*', + python_requires='>=3.4', entry_points={ 'console_scripts': [ 'mkdocs = mkdocs.__main__:cli', @@ -85,13 +83,12 @@ setup( 'License :: OSI Approved :: BSD License', 'Operating System :: OS Independent', 'Programming Language :: Python', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3 :: Only', "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", 'Topic :: Documentation', diff --git a/tox.ini b/tox.ini index 7b816f6a..2a26dc88 100644 --- a/tox.ini +++ b/tox.ini @@ -1,19 +1,18 @@ [tox] envlist = - py{27,34,35,36,37,py,py3}-{unittests,integration,min-req}, + py{34,35,36,37,py3}-{unittests,integration,min-req}, flake8, markdown-lint, linkchecker, jshint, csslint [testenv] passenv = LANG deps= - py{27,34,35,36,37,py,py3}-{unittests,integration}: -rrequirements/project.txt - py{27,34,35,36,37,py,py3}-min-req: -rrequirements/project-min.txt - py{27,34,35,36,37,py,py3}-{unittests,min-req}: -rrequirements/test.txt - py{27,py}-{unittests,min-req}: backports.tempfile + py{34,35,36,37,py3}-{unittests,integration}: -rrequirements/project.txt + py{34,35,36,37,py3}-min-req: -rrequirements/project-min.txt + py{34,35,36,37,py3}-{unittests,min-req}: -rrequirements/test.txt commands= {envpython} --version - py{27,34,35,36,37,py,py3}-{unittests,min-req}: {envbindir}/nosetests --with-coverage --cover-package mkdocs mkdocs - py{27,34,35,36,37,py,py3}-integration: {envpython} -m mkdocs.tests.integration --output={envtmpdir}/builds + py{34,35,36,37,py3}-{unittests,min-req}: {envbindir}/nosetests --with-coverage --cover-package mkdocs mkdocs + py{34,35,36,37,py3}-integration: {envpython} -m mkdocs.tests.integration --output={envtmpdir}/builds [testenv:flake8] deps=-rrequirements/test.txt