Skip to content

Commit

Permalink
Expand type checking coverage (#3019)
Browse files Browse the repository at this point in the history
  • Loading branch information
oprypin committed Oct 25, 2022
1 parent d76cae9 commit 1fa2af7
Show file tree
Hide file tree
Showing 14 changed files with 63 additions and 49 deletions.
2 changes: 1 addition & 1 deletion mkdocs/__main__.py
Expand Up @@ -106,7 +106,7 @@ def __del__(self):
dev_addr_help = "IP address and port to serve documentation locally (default: localhost:8000)"
strict_help = "Enable strict mode. This will cause MkDocs to abort the build on any warnings."
theme_help = "The theme to use when building your documentation."
theme_choices = utils.get_theme_names()
theme_choices = sorted(utils.get_theme_names())
site_dir_help = "The directory to output the result of the documentation build."
use_directory_urls_help = "Use directory URLs when building pages (the default)."
reload_help = "Enable the live reloading in the development server (this is the default)"
Expand Down
20 changes: 10 additions & 10 deletions mkdocs/commands/gh_deploy.py
Expand Up @@ -4,6 +4,7 @@
import os
import re
import subprocess
from typing import Optional, Tuple, Union

import ghp_import
from packaging import version
Expand All @@ -17,7 +18,7 @@
default_message = """Deployed {sha} with MkDocs version: {version}"""


def _is_cwd_git_repo():
def _is_cwd_git_repo() -> bool:
try:
proc = subprocess.Popen(
['git', 'rev-parse', '--is-inside-work-tree'],
Expand All @@ -31,7 +32,7 @@ def _is_cwd_git_repo():
return proc.wait() == 0


def _get_current_sha(repo_path):
def _get_current_sha(repo_path) -> str:
proc = subprocess.Popen(
['git', 'rev-parse', '--short', 'HEAD'],
cwd=repo_path or None,
Expand All @@ -44,7 +45,7 @@ def _get_current_sha(repo_path):
return sha


def _get_remote_url(remote_name):
def _get_remote_url(remote_name: str) -> Union[Tuple[str, str], Tuple[None, None]]:
# No CNAME found. We will use the origin URL to determine the GitHub
# pages location.
remote = f"remote.{remote_name}.url"
Expand All @@ -57,17 +58,16 @@ def _get_remote_url(remote_name):
stdout, _ = proc.communicate()
url = stdout.decode('utf-8').strip()

host = None
path = None
if 'github.com/' in url:
host, path = url.split('github.com/', 1)
elif 'github.com:' in url:
host, path = url.split('github.com:', 1)

else:
return None, None
return host, path


def _check_version(branch):
def _check_version(branch: str) -> None:
proc = subprocess.Popen(
['git', 'show', '-s', '--format=%s', f'refs/heads/{branch}'],
stdout=subprocess.PIPE,
Expand Down Expand Up @@ -97,12 +97,12 @@ def _check_version(branch):

def gh_deploy(
config: MkDocsConfig,
message=None,
message: Optional[str] = None,
force=False,
no_history=False,
ignore_version=False,
shell=False,
):
) -> None:
if not _is_cwd_git_repo():
log.error('Cannot deploy - this directory does not appear to be a git repository')

Expand Down Expand Up @@ -156,7 +156,7 @@ def gh_deploy(

host, path = _get_remote_url(remote_name)

if host is None:
if host is None or path is None:
# This could be a GitHub Enterprise deployment.
log.info('Your documentation should be available shortly.')
else:
Expand Down
6 changes: 4 additions & 2 deletions mkdocs/commands/serve.py
Expand Up @@ -5,6 +5,7 @@
import shutil
import tempfile
from os.path import isdir, isfile, join
from typing import Optional
from urllib.parse import urlsplit

import jinja2.exceptions
Expand Down Expand Up @@ -58,7 +59,7 @@ def mount_path(config: MkDocsConfig):
live_server = livereload in ('dirty', 'livereload')
dirty = livereload == 'dirty'

def builder(config=None):
def builder(config: Optional[MkDocsConfig] = None):
log.info("Building documentation...")
if config is None:
config = get_config()
Expand Down Expand Up @@ -86,12 +87,13 @@ def builder(config=None):
builder=builder, host=host, port=port, root=site_dir, mount_path=mount_path(config)
)

def error_handler(code):
def error_handler(code) -> Optional[bytes]:
if code in (404, 500):
error_page = join(site_dir, f'{code}.html')
if isfile(error_page):
with open(error_page, 'rb') as f:
return f.read()
return None

server.error_handler = error_handler

Expand Down
2 changes: 1 addition & 1 deletion mkdocs/config/__init__.py
@@ -1,3 +1,3 @@
from mkdocs.config.base import Config, load_config

__all__ = [load_config.__name__, Config.__name__]
__all__ = ['load_config', 'Config']
2 changes: 2 additions & 0 deletions mkdocs/config/config_options.py
Expand Up @@ -1042,6 +1042,8 @@ def _load_hook(self, name, path):
if spec is None:
raise ValidationError(f"Cannot import path '{path}' as a Python module")
module = importlib.util.module_from_spec(spec)
if spec.loader is None:
raise ValidationError(f"Cannot import path '{path}' as a Python module")
spec.loader.exec_module(module)
return module

Expand Down
4 changes: 2 additions & 2 deletions mkdocs/contrib/search/__init__.py
Expand Up @@ -26,10 +26,10 @@ def get_lunr_supported_lang(self, lang):
if os.path.isfile(os.path.join(base_path, 'lunr-language', f'lunr.{lang_part}.js')):
return lang_part

def run_validation(self, value):
def run_validation(self, value: object):
if isinstance(value, str):
value = [value]
elif not isinstance(value, (list, tuple)):
if not isinstance(value, list):
raise c.ValidationError('Expected a list of language codes.')
for lang in list(value):
if lang != 'en':
Expand Down
29 changes: 18 additions & 11 deletions mkdocs/livereload/__init__.py
Expand Up @@ -18,7 +18,8 @@
import urllib.parse
import warnings
import wsgiref.simple_server
from typing import Any, Callable, Dict, Optional, Tuple
import wsgiref.util
from typing import Any, BinaryIO, Callable, Dict, Iterable, Optional, Tuple

import watchdog.events
import watchdog.observers.polling
Expand Down Expand Up @@ -62,7 +63,7 @@ class LiveReloadServer(socketserver.ThreadingMixIn, wsgiref.simple_server.WSGISe

def __init__(
self,
builder: Callable,
builder: Callable[[], None],
host: str,
port: int,
root: str,
Expand All @@ -85,7 +86,7 @@ def __init__(
self.build_delay = 0.1
self.shutdown_delay = shutdown_delay
# To allow custom error pages.
self.error_handler = lambda code: None
self.error_handler: Callable[[int], Optional[bytes]] = lambda code: None

super().__init__((host, port), _Handler, **kwargs)
self.set_app(self.serve_request)
Expand All @@ -94,7 +95,9 @@ def __init__(
self._visible_epoch = self._wanted_epoch # Latest fully built version of the site.
self._epoch_cond = threading.Condition() # Must be held when accessing _visible_epoch.

self._to_rebuild: Dict[Callable, bool] = {} # Used as an ordered set of functions to call.
self._to_rebuild: Dict[
Callable[[], None], bool
] = {} # Used as an ordered set of functions to call.
self._rebuild_cond = threading.Condition() # Must be held when accessing _to_rebuild.

self._shutdown = False
Expand All @@ -104,12 +107,15 @@ def __init__(
self._watched_paths: Dict[str, int] = {}
self._watch_refs: Dict[str, Any] = {}

def watch(self, path: str, func: Optional[Callable] = None, recursive: bool = True) -> None:
def watch(
self, path: str, func: Optional[Callable[[], None]] = None, recursive: bool = True
) -> None:
"""Add the 'path' to watched paths, call the function and reload when any file changes under it."""
path = os.path.abspath(path)
if func in (None, self.builder):
func = self.builder
if func is None or func is self.builder:
funct = self.builder
else:
funct = func
warnings.warn(
"Plugins should not pass the 'func' parameter of watch(). "
"The ability to execute custom callbacks will be removed soon.",
Expand All @@ -127,7 +133,7 @@ def callback(event):
return
log.debug(str(event))
with self._rebuild_cond:
self._to_rebuild[func] = True
self._to_rebuild[funct] = True
self._rebuild_cond.notify_all()

handler = watchdog.events.FileSystemEventHandler()
Expand Down Expand Up @@ -194,7 +200,7 @@ def shutdown(self, wait=False) -> None:
self.serve_thread.join()
self.observer.join()

def serve_request(self, environ, start_response):
def serve_request(self, environ, start_response) -> Iterable[bytes]:
try:
result = self._serve_request(environ, start_response)
except Exception:
Expand All @@ -218,7 +224,7 @@ def serve_request(self, environ, start_response):
start_response(msg, [("Content-Type", "text/html")])
return [error_content]

def _serve_request(self, environ, start_response):
def _serve_request(self, environ, start_response) -> Optional[Iterable[bytes]]:
# https://bugs.python.org/issue16679
# https://github.com/bottlepy/bottle/blob/f9b1849db4/bottle.py#L984
path = environ["PATH_INFO"].encode("latin-1").decode("utf-8", "ignore")
Expand Down Expand Up @@ -260,7 +266,7 @@ def condition():
epoch = self._visible_epoch

try:
file = open(file_path, "rb")
file: BinaryIO = open(file_path, "rb")
except OSError:
if not path.endswith("/") and os.path.isfile(os.path.join(file_path, "index.html")):
start_response("302 Found", [("Location", urllib.parse.quote(path) + "/")])
Expand Down Expand Up @@ -301,6 +307,7 @@ def _inject_js_into_html(self, content, epoch):
def _log_poll_request(cls, url, request_id):
log.info(f"Browser connected: {url}")

@classmethod
def _guess_type(cls, path):
# MkDocs only ensures a few common types (as seen in livereload_tests.py::test_mime_types).
# Other uncommon types will not be accepted.
Expand Down
2 changes: 1 addition & 1 deletion mkdocs/localization.py
Expand Up @@ -63,7 +63,7 @@ def install_translations(

def _get_merged_translations(
theme_dirs: Sequence[str], locales_dir: str, locale: Locale
) -> Translations:
) -> Optional[Translations]:
merged_translations: Optional[Translations] = None

log.debug(f"Looking for translations for locale '{locale}'")
Expand Down
2 changes: 1 addition & 1 deletion mkdocs/plugins.py
Expand Up @@ -499,7 +499,7 @@ def run_event(self, name: str, item: None = None, **kwargs) -> Any:
def run_event(self, name: str, item: T, **kwargs) -> T:
...

def run_event(self, name: str, item=None, **kwargs) -> Optional[T]:
def run_event(self, name: str, item=None, **kwargs):
"""
Run all registered methods of an event.
Expand Down
6 changes: 3 additions & 3 deletions mkdocs/structure/nav.py
Expand Up @@ -217,13 +217,13 @@ def _data_to_navigation(data, files: Files, config: Union[MkDocsConfig, Mapping[
T = TypeVar('T')


def _get_by_type(nav, T: Type[T]) -> List[T]:
def _get_by_type(nav, t: Type[T]) -> List[T]:
ret = []
for item in nav:
if isinstance(item, T):
if isinstance(item, t):
ret.append(item)
if item.children:
ret.extend(_get_by_type(item.children, T))
ret.extend(_get_by_type(item.children, t))
return ret


Expand Down
2 changes: 1 addition & 1 deletion mkdocs/tests/config/base_tests.py
Expand Up @@ -206,7 +206,7 @@ def run_validation(self, value):
('invalid_option', ValidationError('pre_validation error')),
('invalid_option', ValidationError('run_validation error')),
],
),
)
self.assertEqual(warnings, [])

def test_run_and_post_validation_errors(self):
Expand Down
16 changes: 8 additions & 8 deletions mkdocs/tests/config/config_options_legacy_tests.py
Expand Up @@ -1201,31 +1201,31 @@ def test_subconfig_ignored(self):
"""Default behaviour of subconfig: validation is ignored"""

# Nominal
class Schema:
class Schema1:
option = c.SubConfig(('cc', c.Choice(('foo', 'bar'))))

conf = self.get_config(Schema, {'option': {'cc': 'foo'}})
conf = self.get_config(Schema1, {'option': {'cc': 'foo'}})
self.assertEqual(conf, {'option': {'cc': 'foo'}})

# Invalid option: No error
class Schema:
class Schema2:
option = c.SubConfig(('cc', c.Choice(('foo', 'bar'))))

conf = self.get_config(Schema, {'option': {'cc': True}})
conf = self.get_config(Schema2, {'option': {'cc': True}})
self.assertEqual(conf, {'option': {'cc': True}})

# Missing option: Will be considered optional with default None
class Schema:
class Schema3:
option = c.SubConfig(('cc', c.Choice(('foo', 'bar'))))

conf = self.get_config(Schema, {'option': {}})
conf = self.get_config(Schema3, {'option': {}})
self.assertEqual(conf, {'option': {'cc': None}})

# Unknown option: No warning
class Schema:
class Schema4:
option = c.SubConfig(('cc', c.Choice(('foo', 'bar'))))

conf = self.get_config(Schema, {'option': {'unknown_key_is_ok': 0}})
conf = self.get_config(Schema4, {'option': {'unknown_key_is_ok': 0}})
self.assertEqual(conf, {'option': {'cc': None, 'unknown_key_is_ok': 0}})

def test_subconfig_unknown_option(self):
Expand Down
13 changes: 7 additions & 6 deletions mkdocs/tests/utils/utils_tests.py
Expand Up @@ -12,6 +12,7 @@
from mkdocs.structure.files import File
from mkdocs.structure.pages import Page
from mkdocs.tests.base import dedent, load_config, tempdir
from mkdocs.utils import meta

BASEYML = """
INHERIT: parent.yml
Expand Down Expand Up @@ -551,7 +552,7 @@ def test_mm_meta_data(self):
"""
)
self.assertEqual(
utils.meta.get_data(doc),
meta.get_data(doc),
(
"Doc body",
{
Expand All @@ -565,7 +566,7 @@ def test_mm_meta_data(self):

def test_mm_meta_data_blank_first_line(self):
doc = '\nfoo: bar\nDoc body'
self.assertEqual(utils.meta.get_data(doc), (doc.lstrip(), {}))
self.assertEqual(meta.get_data(doc), (doc.lstrip(), {}))

def test_yaml_meta_data(self):
doc = dedent(
Expand All @@ -583,7 +584,7 @@ def test_yaml_meta_data(self):
"""
)
self.assertEqual(
utils.meta.get_data(doc),
meta.get_data(doc),
(
"Doc body",
{
Expand All @@ -604,7 +605,7 @@ def test_yaml_meta_data_not_dict(self):
Doc body
"""
)
self.assertEqual(utils.meta.get_data(doc), (doc, {}))
self.assertEqual(meta.get_data(doc), (doc, {}))

def test_yaml_meta_data_invalid(self):
doc = dedent(
Expand All @@ -615,15 +616,15 @@ def test_yaml_meta_data_invalid(self):
Doc body
"""
)
self.assertEqual(utils.meta.get_data(doc), (doc, {}))
self.assertEqual(meta.get_data(doc), (doc, {}))

def test_no_meta_data(self):
doc = dedent(
"""
Doc body
"""
)
self.assertEqual(utils.meta.get_data(doc), (doc, {}))
self.assertEqual(meta.get_data(doc), (doc, {}))


class LogCounterTests(unittest.TestCase):
Expand Down

0 comments on commit 1fa2af7

Please sign in to comment.