diff --git a/mkdocs_git_authors_plugin/ci.py b/mkdocs_git_authors_plugin/ci.py index 0c0c46f..686b253 100644 --- a/mkdocs_git_authors_plugin/ci.py +++ b/mkdocs_git_authors_plugin/ci.py @@ -6,16 +6,17 @@ Taken from https://github.com/timvink/mkdocs-git-revision-date-localized-plugin/blob/master/mkdocs_git_revision_date_localized_plugin/ci.py """ +import logging import os from contextlib import contextmanager -import logging from pathlib import Path -from typing import Union +from typing import Any, Generator, Union + from mkdocs_git_authors_plugin.git.command import GitCommand @contextmanager -def working_directory(path: Union[str, Path]): +def working_directory(path: Union[str, Path]) -> Generator[None, Any, None]: """ Temporarily change working directory. A context manager which changes the working directory to the given @@ -27,7 +28,7 @@ def working_directory(path: Union[str, Path]): # Do something in new directory # Back to old directory ``` - """ + """ origin = Path().absolute() try: os.chdir(path) diff --git a/mkdocs_git_authors_plugin/config.py b/mkdocs_git_authors_plugin/config.py new file mode 100644 index 0000000..adfbcfe --- /dev/null +++ b/mkdocs_git_authors_plugin/config.py @@ -0,0 +1,21 @@ +from mkdocs.config import config_options +from mkdocs.config.base import Config + + +class GitAuthorsPluginConfig(Config): + show_contribution = config_options.Type(bool, default=False) + show_line_count = config_options.Type(bool, default=False) + show_email_address = config_options.Type(bool, default=True) + href = config_options.Type(str, default="mailto:{email}") + count_empty_lines = config_options.Type(bool, default=True) + fallback_to_empty = config_options.Type(bool, default=False) + exclude = config_options.Type(list, default=[]) + ignore_commits = config_options.Type(str, default="") + ignore_authors = config_options.Type(list, default=[]) + enabled = config_options.Type(bool, default=True) + enabled_on_serve = config_options.Type(bool, default=True) + sort_authors_by = config_options.Type(str, default="name") + authorship_threshold_percent = config_options.Type(int, default=0) + strict = config_options.Type(bool, default=True) + # sort_authors_by_name = config_options.Type(bool, default=True) + # sort_reverse = config_options.Type(bool, default=False) diff --git a/mkdocs_git_authors_plugin/git/author.py b/mkdocs_git_authors_plugin/git/author.py index 7ca9546..48a241c 100644 --- a/mkdocs_git_authors_plugin/git/author.py +++ b/mkdocs_git_authors_plugin/git/author.py @@ -1,8 +1,8 @@ -from .repo import AbstractRepoObject, Repo -from .page import Page -from .commit import Commit +from typing import Dict, Union -from typing import Dict +from mkdocs_git_authors_plugin.git.commit import Commit +from mkdocs_git_authors_plugin.git.page import Page +from mkdocs_git_authors_plugin.git.repo import AbstractRepoObject, Repo class Author(AbstractRepoObject): @@ -10,7 +10,7 @@ class Author(AbstractRepoObject): Abstraction of an author in the Git repository. """ - def __init__(self, repo: Repo, name: str, email: str): + def __init__(self, repo: Repo, name: str, email: str) -> None: """ Instantiate an Author. @@ -24,7 +24,7 @@ def __init__(self, repo: Repo, name: str, email: str): self._email = email self._pages: Dict[str, dict] = dict() - def add_lines(self, page: Page, commit: Commit, lines: int = 1): + def add_lines(self, page: Page, commit: Commit, lines: int = 1) -> None: """ Add line(s) in a given page/commit to the author's data. @@ -42,7 +42,7 @@ def add_lines(self, page: Page, commit: Commit, lines: int = 1): entry["datetime"] = commit_dt entry["datetime_str"] = commit.datetime(str) - def contribution(self, path=None, _type=float): + def contribution(self, path=None, _type=float) -> Union[float, str]: """ The author's relative contribution to a page or the repository. @@ -60,17 +60,17 @@ def contribution(self, path=None, _type=float): formatted string or floating point number """ lines = self.lines(path) - total_lines = ( + total_lines: int = ( self.page(path)["page"].total_lines() if path else self.repo().total_lines() ) # Some pages are empty, that case contribution is 0 by default if total_lines == 0: - result = 0 + result = 0.0 else: result = lines / total_lines - if _type == float: + if _type is float: return result else: return str(round(result * 100, 2)) + "%" @@ -87,10 +87,10 @@ def datetime(self, path, fmt=str): a formatted string (fmt=str) or a datetime.datetime object with tzinfo """ - key = "datetime_str" if fmt == str else "datetime" + key = "datetime_str" if fmt is str else "datetime" return self.page(path).get(key) - def email(self): + def email(self) -> str: """ The author's email address @@ -101,7 +101,7 @@ def email(self): """ return self._email - def lines(self, path=None): + def lines(self, path=None) -> int: """ The author's total number of lines on a page or in the repository. @@ -117,7 +117,7 @@ def lines(self, path=None): else: return sum([v["lines"] for v in self._pages.values()]) - def name(self): + def name(self) -> str: """ The author's full name @@ -128,7 +128,7 @@ def name(self): """ return self._name - def page(self, path, page=None): + def page(self, path, page=None) -> dict: """ A dictionary with the author's contribution to a page. @@ -154,7 +154,7 @@ def page(self, path, page=None): if not self._pages.get(path): self._pages[path] = { "page": page or self.repo().page(path), - "lines": 0 + "lines": 0, # datetime and datetime_str will be populated later } return self._pages[path] diff --git a/mkdocs_git_authors_plugin/git/command.py b/mkdocs_git_authors_plugin/git/command.py index 4cc8ae4..ccc6607 100644 --- a/mkdocs_git_authors_plugin/git/command.py +++ b/mkdocs_git_authors_plugin/git/command.py @@ -1,4 +1,5 @@ import subprocess +from typing import List, Union class GitCommandError(Exception): @@ -24,7 +25,7 @@ class GitCommand(object): In case of an error a verbose GitCommandError is raised. """ - def __init__(self, command: str, args: list = []): + def __init__(self, command: str, args: List = []) -> None: """ Initialize the GitCommand. @@ -40,7 +41,7 @@ def __init__(self, command: str, args: list = []): self._stderr = None self._completed = False - def run(self): + def run(self) -> int: """ Execute the configured Git command. @@ -82,7 +83,7 @@ def run(self): self._completed = True return int(str(p.returncode)) - def set_args(self, args: list): + def set_args(self, args: List) -> None: """ Change the command arguments. @@ -91,7 +92,7 @@ def set_args(self, args: list): """ self._args = args - def set_command(self, command: str): + def set_command(self, command: str) -> None: """ Change the Git command. @@ -100,7 +101,7 @@ def set_command(self, command: str): """ self._command = command - def stderr(self): + def stderr(self) -> Union[None, List[str]]: """ Return the stderr output of the command as a string list. @@ -113,7 +114,7 @@ def stderr(self): raise GitCommandError("Trying to read from uncompleted GitCommand") return self._stderr - def stdout(self): + def stdout(self) -> Union[None, List[str]]: """ Return the stdout output of the command as a string list. diff --git a/mkdocs_git_authors_plugin/git/commit.py b/mkdocs_git_authors_plugin/git/commit.py index 1d3c3a0..c778a73 100644 --- a/mkdocs_git_authors_plugin/git/commit.py +++ b/mkdocs_git_authors_plugin/git/commit.py @@ -1,6 +1,8 @@ import re -from .repo import AbstractRepoObject, Repo -from .. import util +from typing import Union + +from mkdocs_git_authors_plugin import util +from mkdocs_git_authors_plugin.git.repo import AbstractRepoObject, Repo class Commit(AbstractRepoObject): @@ -58,7 +60,7 @@ def author(self): """ return self._author - def datetime(self, _type=str): + def datetime(self, _type=str) -> Union[str, util.datetime]: """ The commit's commit time. @@ -71,4 +73,4 @@ def datetime(self, _type=str): The commit's commit time, either as a formatted string (_type=str) or as a datetime.datetime expression with tzinfo """ - return self._datetime_string if _type == str else self._datetime + return self._datetime_string if _type is str else self._datetime diff --git a/mkdocs_git_authors_plugin/git/page.py b/mkdocs_git_authors_plugin/git/page.py index d6663b9..2cb3f45 100644 --- a/mkdocs_git_authors_plugin/git/page.py +++ b/mkdocs_git_authors_plugin/git/page.py @@ -1,11 +1,11 @@ -from pathlib import Path -import re import logging -from .repo import Repo, AbstractRepoObject -from .command import GitCommand, GitCommandError - +import re +from pathlib import Path from typing import List +from mkdocs_git_authors_plugin.git.command import GitCommand, GitCommandError +from mkdocs_git_authors_plugin.git.repo import AbstractRepoObject, Repo + logger = logging.getLogger("mkdocs.plugins") @@ -18,7 +18,7 @@ class Page(AbstractRepoObject): modified by that commit. """ - def __init__(self, repo: Repo, path: Path, strict: bool): + def __init__(self, repo: Repo, path: Path, strict: bool) -> None: """ Instantiate a Page object @@ -32,7 +32,7 @@ def __init__(self, repo: Repo, path: Path, strict: bool): self._total_lines = 0 self._authors: List[dict] = list() self._strict = strict - + try: self._process_git_blame() except GitCommandError: @@ -47,7 +47,7 @@ def __init__(self, repo: Repo, path: Path, strict: bool): % path ) - def add_total_lines(self, cnt: int = 1): + def add_total_lines(self, cnt: int = 1) -> None: """ Add line(s) to the count of total lines for the page. @@ -56,7 +56,7 @@ def add_total_lines(self, cnt: int = 1): """ self._total_lines += cnt - def get_authors(self): + def get_authors(self) -> List[dict]: """ Return a sorted list of authors for the page @@ -86,7 +86,7 @@ def get_authors(self): ] return self._authors - def _process_git_blame(self): + def _process_git_blame(self) -> None: """ Execute git blame and parse the results. @@ -190,7 +190,9 @@ def _process_git_blame(self): author_tz=commit_data.get("author-tz"), summary=commit_data.get("summary"), ) - if commit.author().email() not in ignore_authors and (len(line) > 1 or self.repo().config("count_empty_lines")): + if commit.author().email() not in ignore_authors and ( + len(line) > 1 or self.repo().config("count_empty_lines") + ): author = commit.author() if author not in self._authors: self._authors.append(author) @@ -198,7 +200,7 @@ def _process_git_blame(self): self.add_total_lines() self.repo().add_total_lines() - def path(self): + def path(self) -> Path: """ The path to the markdown file. @@ -209,7 +211,7 @@ def path(self): """ return self._path - def total_lines(self): + def total_lines(self) -> int: """ Total number of lines in the markdown source file. diff --git a/mkdocs_git_authors_plugin/git/repo.py b/mkdocs_git_authors_plugin/git/repo.py index f7424f0..15af92a 100644 --- a/mkdocs_git_authors_plugin/git/repo.py +++ b/mkdocs_git_authors_plugin/git/repo.py @@ -1,5 +1,7 @@ from pathlib import Path -from .command import GitCommand +from typing import Any, Union + +from mkdocs_git_authors_plugin.git.command import GitCommand class Repo(object): @@ -7,7 +9,7 @@ class Repo(object): Abstraction of a Git repository (i.e. the MkDocs project). """ - def __init__(self): + def __init__(self) -> None: self._root = self.find_repo_root() self._total_lines = 0 @@ -18,7 +20,7 @@ def __init__(self): # Store Author objects, indexed by email self._authors = {} - def add_total_lines(self, cnt: int = 1): + def add_total_lines(self, cnt: int = 1) -> None: """ Add line(s) to the number of total lines in the repository. @@ -47,7 +49,7 @@ def author(self, name, email: str): self._authors[email] = Author(self, name, email) return self._authors[email] - def get_authors(self): + def get_authors(self) -> list: """ Sorted list of authors in the repository. @@ -67,7 +69,7 @@ def get_authors(self): reverse=reverse, ) - def config(self, key: str = ""): + def config(self, key: str = "") -> Any: """ Return the plugin configuration dictionary or a single config value. @@ -76,7 +78,7 @@ def config(self, key: str = ""): """ return self._config.get(key) if key else self._config - def find_repo_root(self): + def find_repo_root(self) -> str: """ Determine the root directory of the Git repository, in case the current working directory is different from that. @@ -91,9 +93,11 @@ def find_repo_root(self): """ cmd = GitCommand("rev-parse", ["--show-toplevel"]) cmd.run() - return cmd.stdout()[0] + stdout = cmd.stdout() + assert stdout is not None + return stdout[0] - def get_commit(self, sha: str, **kwargs): + def get_commit(self, sha: str, **kwargs) -> Union[Any, None]: """ Return the (cached) Commit object for given sha. @@ -125,7 +129,7 @@ def page(self, path): Returns: Page object """ - if type(path) == str: + if isinstance(path, str): path = Path(path) if not self._pages.get(path): from .page import Page @@ -133,7 +137,7 @@ def page(self, path): self._pages[path] = Page(self, path, self.config("strict")) return self._pages[path] - def set_config(self, plugin_config): + def set_config(self, plugin_config) -> None: """ Store the plugin configuration in the Repo instance. @@ -142,7 +146,7 @@ def set_config(self, plugin_config): """ self._config = plugin_config - def _sort_key(self, author): + def _sort_key(self, author) -> Any: """ Return a sort key for an author. @@ -182,10 +186,10 @@ class AbstractRepoObject(object): Base class for objects that live with a repository context. """ - def __init__(self, repo: Repo): + def __init__(self, repo: Repo) -> None: self._repo = repo - def repo(self): + def repo(self) -> Repo: """ Return a reference to the Repo object. diff --git a/mkdocs_git_authors_plugin/plugin.py b/mkdocs_git_authors_plugin/plugin.py index fa24af1..9c0d0cf 100644 --- a/mkdocs_git_authors_plugin/plugin.py +++ b/mkdocs_git_authors_plugin/plugin.py @@ -1,46 +1,36 @@ -from mkdocs_git_authors_plugin.git.command import GitCommandError -import re import logging -from mkdocs.config import config_options +import re +from typing import Literal, Union + +from mkdocs.config.defaults import MkDocsConfig from mkdocs.plugins import BasePlugin +from mkdocs.structure.files import Files +from mkdocs.structure.nav import Navigation +from mkdocs.structure.pages import Page +from mkdocs.utils.templates import TemplateContext -from . import util -from .git.repo import Repo +from mkdocs_git_authors_plugin import util from mkdocs_git_authors_plugin.ci import raise_ci_warnings +from mkdocs_git_authors_plugin.config import GitAuthorsPluginConfig from mkdocs_git_authors_plugin.exclude import exclude +from mkdocs_git_authors_plugin.git.command import GitCommandError +from mkdocs_git_authors_plugin.git.repo import Repo logger = logging.getLogger("mkdocs.plugins") -class GitAuthorsPlugin(BasePlugin): - config_scheme = ( - ("show_contribution", config_options.Type(bool, default=False)), - ("show_line_count", config_options.Type(bool, default=False)), - ("show_email_address", config_options.Type(bool, default=True)), - ("href", config_options.Type(str, default='mailto:{email}')), - ("count_empty_lines", config_options.Type(bool, default=True)), - ("fallback_to_empty", config_options.Type(bool, default=False)), - ("exclude", config_options.Type(list, default=[])), - ("ignore_commits", config_options.Type(str, default=None)), - ("ignore_authors", config_options.Type(list, default=[])), - ("enabled", config_options.Type(bool, default=True)), - ("enabled_on_serve", config_options.Type(bool, default=True)), - ("sort_authors_by", config_options.Type(str, default="name")), - ("authorship_threshold_percent", config_options.Type(int, default=0)), - ("strict", config_options.Type(bool, default=True)), - # ('sort_authors_by_name', config_options.Type(bool, default=True)), - # ('sort_reverse', config_options.Type(bool, default=False)) - ) - - def __init__(self): +class GitAuthorsPlugin(BasePlugin[GitAuthorsPluginConfig]): + def __init__(self) -> None: self._repo = None self._fallback = False self.is_serve = False - def on_startup(self, command, dirty): + def on_startup( + self, command: Literal["build", "gh-deploy", "serve"], dirty: bool, **kwargs + ) -> None: self.is_serve = command == "serve" - def on_config(self, config, **kwargs): + def on_config(self, config: MkDocsConfig, **kwargs) -> Union[MkDocsConfig, None]: """ Store the plugin configuration in the Repo object. @@ -63,8 +53,8 @@ def on_config(self, config, **kwargs): if not self._is_enabled(): return config - assert self.config["authorship_threshold_percent"] >= 0 - assert self.config["authorship_threshold_percent"] <= 100 + assert self.config.authorship_threshold_percent >= 0 + assert self.config.authorship_threshold_percent <= 100 try: self._repo = Repo() @@ -72,7 +62,7 @@ def on_config(self, config, **kwargs): self.repo().set_config(self.config) raise_ci_warnings(path=self.repo()._root) except GitCommandError: - if self.config["fallback_to_empty"]: + if self.config.fallback_to_empty: self._fallback = True logger.warning( "[git-authors-plugin] Unable to find a git directory and/or git is not installed." @@ -81,7 +71,9 @@ def on_config(self, config, **kwargs): else: raise - def on_files(self, files, config, **kwargs): + def on_files( + self, files: Files, *, config: MkDocsConfig, **kwargs + ) -> Union[Files, None]: """ Preprocess all markdown pages in the project. @@ -114,9 +106,8 @@ def on_files(self, files, config, **kwargs): return for file in files: - # Exclude pages specified in config - excluded_pages = self.config.get("exclude", []) + excluded_pages = self.config.exclude or [] if exclude(file.src_path, excluded_pages): continue @@ -124,7 +115,9 @@ def on_files(self, files, config, **kwargs): if path.endswith(".md"): _ = self.repo().page(path) - def on_page_content(self, html, page, config, files, **kwargs): + def on_page_content( + self, html: str, *, page: Page, config: MkDocsConfig, files: Files, **kwargs + ) -> Union[str, None]: """ Replace jinja tag {{ git_site_authors }} in HTML. @@ -151,7 +144,7 @@ def on_page_content(self, html, page, config, files, **kwargs): return html # Exclude pages specified in config - excluded_pages = self.config.get("exclude", []) + excluded_pages = self.config.exclude or [] if exclude(page.file.src_path, excluded_pages): return html @@ -182,7 +175,15 @@ def on_page_content(self, html, page, config, files, **kwargs): return html - def on_page_context(self, context, page, config, nav, **kwargs): + def on_page_context( + self, + context: TemplateContext, + *, + page: Page, + config: MkDocsConfig, + nav: Navigation, + **kwargs, + ) -> Union[TemplateContext, None]: """ Add 'git_authors' and 'git_authors_summary' variables to template context. @@ -210,7 +211,7 @@ def on_page_context(self, context, page, config, nav, **kwargs): return context # Exclude pages specified in config - excluded_pages = self.config.get("exclude", []) + excluded_pages = self.config.exclude or [] if exclude(page.file.src_path, excluded_pages): logging.debug("on_page_context, Excluding page " + page.file.src_path) return context @@ -239,13 +240,13 @@ def on_page_context(self, context, page, config, nav, **kwargs): return context - def repo(self): + def repo(self) -> Union[Repo, None]: """ Reference to the Repo object of the current project. """ return self._repo - def _is_enabled(self): + def _is_enabled(self) -> bool: """ Consider this plugin to be disabled in the following two conditions: * config.enabled is false @@ -255,9 +256,9 @@ def _is_enabled(self): """ is_enabled = True - if not self.config.get("enabled"): + if not self.config.enabled: is_enabled = False - elif self.is_serve and not self.config.get("enabled_on_serve"): + elif self.is_serve and not self.config.enabled_on_serve: is_enabled = False return is_enabled diff --git a/mkdocs_git_authors_plugin/util.py b/mkdocs_git_authors_plugin/util.py index 92c7f67..7595221 100644 --- a/mkdocs_git_authors_plugin/util.py +++ b/mkdocs_git_authors_plugin/util.py @@ -1,8 +1,11 @@ -from datetime import datetime, timezone, timedelta +from datetime import datetime, timedelta, timezone from pathlib import Path +from typing import Any, Dict, List +from mkdocs_git_authors_plugin.config import GitAuthorsPluginConfig -def commit_datetime(author_time: str, author_tz: str): + +def commit_datetime(author_time: str, author_tz: str) -> datetime: """ Convert a commit's timestamp to an aware datetime object. @@ -23,7 +26,7 @@ def commit_datetime(author_time: str, author_tz: str): ) -def commit_datetime_string(dt: datetime): +def commit_datetime_string(dt: datetime) -> str: """ Return a string representation for a commit's timestamp. @@ -36,7 +39,7 @@ def commit_datetime_string(dt: datetime): return dt.strftime("%c %z") -def page_authors_summary(page, config: dict): +def page_authors_summary(page, config: dict) -> str: """ A summary of the authors' contributions on a page level @@ -59,8 +62,11 @@ def page_authors_summary(page, config: dict): else "" ) if page.repo().config("show_email_address"): - href = page.repo().config("href").format(email=author.email(), - name=author.name()) + href = ( + page.repo() + .config("href") + .format(email=author.email(), name=author.name()) + ) author_name = "%s" % (href, author.name()) else: author_name = author.name() @@ -70,7 +76,7 @@ def page_authors_summary(page, config: dict): return "%s" % authors_summary_str -def site_authors_summary(authors, config: dict): +def site_authors_summary(authors, config: GitAuthorsPluginConfig) -> str: """ A summary list of the authors' contributions on repo level. @@ -92,23 +98,18 @@ def site_authors_summary(authors, config: dict): Returns: Unordered HTML list as a string. """ - show_contribution = config["show_contribution"] - show_line_count = config["show_line_count"] - show_email_address = config["show_email_address"] - result = """