Skip to content

Commit

Permalink
Improve the contribs plugin
Browse files Browse the repository at this point in the history
  • Loading branch information
RobertoPrevato authored Apr 26, 2023
1 parent e22bca9 commit 8c75306
Show file tree
Hide file tree
Showing 15 changed files with 303 additions and 34 deletions.
39 changes: 24 additions & 15 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: [3.8, 3.9, "3.10"]
python-version: [3.8, 3.9, "3.10", "3.11"]

steps:
- uses: actions/checkout@v1
Expand All @@ -47,7 +47,7 @@ jobs:
if: matrix.python-version == '3.10'

- name: Use Python ${{ matrix.python-version }}
uses: actions/setup-python@v3
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}

Expand Down Expand Up @@ -97,18 +97,18 @@ jobs:
- name: Install distribution dependencies
run: pip install build
if: matrix.python-version == '3.10'
if: matrix.python-version == '3.11'

- name: Create distribution package
run: python -m build
if: matrix.python-version == '3.10'
if: matrix.python-version == '3.11'

- name: Upload distribution package
uses: actions/upload-artifact@master
with:
name: dist
path: dist
if: matrix.python-version == '3.10'
if: matrix.python-version == '3.11'

publish:
runs-on: ubuntu-latest
Expand All @@ -121,19 +121,28 @@ jobs:
name: dist
path: dist

- name: Publish distribution 📦 to Test PyPI
uses: pypa/gh-action-pypi-publish@release/v1
- name: Use Python 3.11
uses: actions/setup-python@v1
with:
skip_existing: true
user: __token__
password: ${{ secrets.test_pypi_password }}
repository_url: https://test.pypi.org/legacy/
python-version: '3.11'

- name: Install dependencies
run: |
pip install twine
- name: Publish distribution 📦 to Test PyPI
run: |
twine upload -r testpypi dist/*
env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.test_pypi_password }}

- name: Publish distribution 📦 to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
user: __token__
password: ${{ secrets.pypi_password }}
run: |
twine upload -r pypi dist/*
env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.pypi_password }}

- name: Download CSS pack
uses: actions/download-artifact@v2
Expand Down
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,21 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.0.1] - 2023-04-25
- Improves the `contribs` plugin, adding the possibility to document
contributors for a page also using `.txt` files close to `.md` files. This
can be useful in several cases:
- - To document contributors who worked outside of Git, for example when providing
pictures for the page, or written content provided to someone who is
adding content to the MkDocs site.
- - To document contributors following a Git history re-write
- Improves the `contribs` plugin, adding the possibility to exclude files by
glob patterns (fix #33).
- Improves the `contribs` plugin, adding the possibility to merge contributors
by name, for scenarios when the same person commits using different names
(Git reports different contributors in such cases) and it is preferred
displaying information aggregated as single contributor.

## [1.0.0] - 2022-12-20
- Adds the possibility to specify a `class` for the root HTML element of `cards`.
- Fixes a bug in the `contribs` plugin (adds a carriage return before the
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ test-cov-unit:


test-cov:
pytest --cov-report html --cov=neoteroi
pytest --cov-report html --cov=neoteroi tests


format:
Expand Down
2 changes: 1 addition & 1 deletion neoteroi/mkdocs/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "1.0.0"
__version__ = "1.0.1"
69 changes: 61 additions & 8 deletions neoteroi/mkdocs/contribs/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@
"""
import logging
from datetime import datetime
from fnmatch import fnmatch
from pathlib import Path
from subprocess import CalledProcessError
from typing import List, Optional
from typing import List

from mkdocs.config import config_options as c
from mkdocs.plugins import BasePlugin
Expand All @@ -20,10 +21,35 @@
from neoteroi.mkdocs.contribs.domain import ContributionsReader, Contributor
from neoteroi.mkdocs.contribs.git import GitContributionsReader
from neoteroi.mkdocs.contribs.html import ContribsViewOptions, render_contribution_stats
from neoteroi.mkdocs.contribs.txt import TXTContributionsReader

logger = logging.getLogger("MARKDOWN")


class DefaultContributionsReader(ContributionsReader):
"""
Supports both contributors obtained from Git history and from configuration files.
"""

def __init__(self) -> None:
super().__init__()
self._git_reader = GitContributionsReader()
self._txt_reader = TXTContributionsReader()

def get_contributors(self, file_path: Path) -> List[Contributor]:
git_history_contributors = self._git_reader.get_contributors(file_path)
configured_contributors = self._txt_reader.get_contributors(file_path)
return list(
{
item.email: item
for item in configured_contributors + git_history_contributors
}.values()
)

def get_last_modified_date(self, file_path: Path) -> datetime:
return self._git_reader.get_last_modified_date(file_path)


class ContribsPlugin(BasePlugin):
_contribs_reader: ContributionsReader
config_scheme = (
Expand All @@ -33,16 +59,14 @@ class ContribsPlugin(BasePlugin):
("contributors", c.Type(list, default=[])),
("show_last_modified_time", c.Type(bool, default=True)),
("show_contributors_title", c.Type(bool, default=False)),
("exclude", c.Type(list, default=[])),
)

def __init__(self) -> None:
super().__init__()
self._contribs_reader = GitContributionsReader()

def _read_contributor_merge_with(self, contributor_info) -> Optional[str]:
return contributor_info.get("merge_with")
self._contribs_reader = DefaultContributionsReader()

def _handle_merge_contributor_info(
def _merge_contributor_by_email(
self,
contributors: List[Contributor],
contributor: Contributor,
Expand All @@ -67,6 +91,7 @@ def _handle_merge_contributor_info(
if parent:
parent.count += contributor.count
return True

return False

def _get_contributors(self, page_file: File) -> List[Contributor]:
Expand All @@ -83,6 +108,7 @@ def _get_contributors(self, page_file: File) -> List[Contributor]:
contributor_info = next(
(item for item in info if item.get("email") == contributor.email), None
)

if contributor_info:
contributor.image = contributor_info.get("image")
contributor.key = contributor_info.get("key")
Expand All @@ -91,8 +117,24 @@ def _get_contributors(self, page_file: File) -> List[Contributor]:
# ignore the contributor's information (can be useful for bots)
continue

if (
"name" in contributor_info
and contributor_info["name"] != contributor.name
):
parent = next(
(
other
for other in contributors
if other.name == contributor_info["name"]
),
None,
)
if parent:
parent.count += contributor.count
continue

# should contributor information be merged with another object?
if self._handle_merge_contributor_info(
if self._merge_contributor_by_email(
contributors, contributor, contributor_info
):
# skip this item as it was merged with another one
Expand All @@ -103,7 +145,7 @@ def _get_contributors(self, page_file: File) -> List[Contributor]:
return results

def _get_last_commit_date(self, page_file: File) -> datetime:
return self._contribs_reader.get_last_commit_date(
return self._contribs_reader.get_last_modified_date(
Path("docs") / page_file.src_path
)

Expand All @@ -127,7 +169,18 @@ def _set_contributors(self, markdown: str, page: Page) -> str:
)
)

def _is_ignored_page(self, page: Page) -> bool:
if not self.config.get("exclude"):
return False

return any(
fnmatch(page.file.src_path, ignored_pattern)
for ignored_pattern in self.config["exclude"]
)

def on_page_markdown(self, markdown, *args, **kwargs):
if self._is_ignored_page(kwargs["page"]):
return markdown
try:
markdown = self._set_contributors(markdown, kwargs["page"])
except (CalledProcessError, ValueError) as operation_error:
Expand Down
2 changes: 1 addition & 1 deletion neoteroi/mkdocs/contribs/domain.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,5 @@ def get_contributors(self, file_path: Path) -> List[Contributor]:
"""Obtains the list of contributors for a file with the given path."""

@abstractmethod
def get_last_commit_date(self, file_path: Path) -> datetime:
def get_last_modified_date(self, file_path: Path) -> datetime:
"""Reads the last commit date of a file."""
9 changes: 7 additions & 2 deletions neoteroi/mkdocs/contribs/git.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
"""
This module defines a ContributionsReader that obtains contributors' list for a page
from the Git history. Note that this ContributionsReader won't work well in case of
history rewrites or files renamed without keeping contributor's history.
For this reason, it should be used together with a
"""
import re
import subprocess
from datetime import datetime
Expand All @@ -10,7 +16,6 @@


class GitContributionsReader(ContributionsReader):

_name_email_rx = re.compile(r"(?P<name>[^\<]+)<(?P<email>[^\>]+)>")

def _decode(self, value: bytes) -> str:
Expand Down Expand Up @@ -57,7 +62,7 @@ def get_contributors(self, file_path: Path) -> List[Contributor]:

return list(self.parse_committers(result))

def get_last_commit_date(self, file_path: Path) -> datetime:
def get_last_modified_date(self, file_path: Path) -> datetime:
"""Reads the last commit on a file."""
result = self._decode(
subprocess.check_output(
Expand Down
87 changes: 87 additions & 0 deletions neoteroi/mkdocs/contribs/txt.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import os
import re
from datetime import datetime
from pathlib import Path
from typing import Iterable, List, Tuple

from dateutil.parser import parse

from neoteroi.mkdocs.contribs.domain import ContributionsReader, Contributor


def _read_lines_strip_comments(file_path: Path):
with open(str(file_path), mode="rt", encoding="utf8") as file:
lines = file.readlines()
lines = (re.sub("#.+$", "", x).strip() for x in lines)
return [line for line in lines if line]


class TXTContributionsReader(ContributionsReader):
"""
A ContributionsReader that can read contributors information described in .txt
files.
"""

_contrib_rx = re.compile(
r"(?P<name>[^\<]+)<(?P<email>[^\>]+)>\s\((?P<count>[^\>]+)\)"
)

_last_mod_time_rx = re.compile(
r"^\s*Last\smodified\stime:\s(?P<value>.+)$", re.IGNORECASE | re.MULTILINE
)

def _parse_value(self, value: str) -> Tuple[str, str, int]:
match = self._contrib_rx.search(value)
if match:
values = match.groupdict()
name = values["name"].strip()
email = values["email"].strip()
count = int(values["count"])
else:
name, email, count = ("", "", -1)
return name, email, count

def _get_txt_file_path(self, file_path: Path) -> Path:
path_without_extension = os.path.splitext(file_path)[0]
return Path(path_without_extension + ".contribs.txt")

def _get_contributors_from_txt_file(self, file_path: Path) -> Iterable[Contributor]:
for line in _read_lines_strip_comments(file_path):
name, email, count = self._parse_value(line)
if name and email:
yield Contributor(name, email, count)

def get_contributors(self, file_path: Path) -> List[Contributor]:
"""
Obtains the list of contributors from a txt file with the given path.
The file contents should look like:
Charlie Brown <[email protected]> (3)
Having each line with such pattern:
Name <email> (Contributions Count)
and supporting comments using hashes:
# Example comment
"""
txt_path = self._get_txt_file_path(file_path)

if not txt_path.exists():
return []

return list(self._get_contributors_from_txt_file(txt_path))

def get_last_modified_date(self, file_path: Path) -> datetime:
"""Reads the last commit date of a file."""
txt_path = self._get_txt_file_path(file_path)

if not txt_path.exists():
raise FileNotFoundError()

match = self._last_mod_time_rx.search(txt_path.read_text("utf8"))

if match:
return parse(match.groups()[0])

return datetime.min
1 change: 0 additions & 1 deletion neoteroi/mkdocs/markdown/tables/spantable.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ def html_class(self) -> Optional[str]:
def _iter_coords(
x: int, y: int, colspan: int, rowspan: int
) -> Iterable[Tuple[int, int]]:

for x_increment in range(colspan):
for y_increment in range(rowspan):
yield (x + x_increment, y + y_increment)
Expand Down
1 change: 0 additions & 1 deletion neoteroi/mkdocs/projects/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@


class ProjectsExtension(Extension):

config = {
"priority": [12, "The priority to be configured for the extension."],
}
Expand Down
1 change: 0 additions & 1 deletion neoteroi/mkdocs/spantable/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@


class SpanTableProcessor(BlockProcessor):

START_RE = re.compile(r"""(?P<indent>\s*)::spantable::[\w\s]*""", re.DOTALL)

END_RE = re.compile(r"\s*:{2}end-spantable:{2}\s*\n?")
Expand Down
Loading

0 comments on commit 8c75306

Please sign in to comment.