Skip to content

Commit 8c75306

Browse files
Improve the contribs plugin
1 parent e22bca9 commit 8c75306

File tree

15 files changed

+303
-34
lines changed

15 files changed

+303
-34
lines changed

.github/workflows/build.yml

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ jobs:
2020
strategy:
2121
fail-fast: false
2222
matrix:
23-
python-version: [3.8, 3.9, "3.10"]
23+
python-version: [3.8, 3.9, "3.10", "3.11"]
2424

2525
steps:
2626
- uses: actions/checkout@v1
@@ -47,7 +47,7 @@ jobs:
4747
if: matrix.python-version == '3.10'
4848

4949
- name: Use Python ${{ matrix.python-version }}
50-
uses: actions/setup-python@v3
50+
uses: actions/setup-python@v4
5151
with:
5252
python-version: ${{ matrix.python-version }}
5353

@@ -97,18 +97,18 @@ jobs:
9797
9898
- name: Install distribution dependencies
9999
run: pip install build
100-
if: matrix.python-version == '3.10'
100+
if: matrix.python-version == '3.11'
101101

102102
- name: Create distribution package
103103
run: python -m build
104-
if: matrix.python-version == '3.10'
104+
if: matrix.python-version == '3.11'
105105

106106
- name: Upload distribution package
107107
uses: actions/upload-artifact@master
108108
with:
109109
name: dist
110110
path: dist
111-
if: matrix.python-version == '3.10'
111+
if: matrix.python-version == '3.11'
112112

113113
publish:
114114
runs-on: ubuntu-latest
@@ -121,19 +121,28 @@ jobs:
121121
name: dist
122122
path: dist
123123

124-
- name: Publish distribution 📦 to Test PyPI
125-
uses: pypa/gh-action-pypi-publish@release/v1
124+
- name: Use Python 3.11
125+
uses: actions/setup-python@v1
126126
with:
127-
skip_existing: true
128-
user: __token__
129-
password: ${{ secrets.test_pypi_password }}
130-
repository_url: https://test.pypi.org/legacy/
127+
python-version: '3.11'
128+
129+
- name: Install dependencies
130+
run: |
131+
pip install twine
132+
133+
- name: Publish distribution 📦 to Test PyPI
134+
run: |
135+
twine upload -r testpypi dist/*
136+
env:
137+
TWINE_USERNAME: __token__
138+
TWINE_PASSWORD: ${{ secrets.test_pypi_password }}
131139

132140
- name: Publish distribution 📦 to PyPI
133-
uses: pypa/gh-action-pypi-publish@release/v1
134-
with:
135-
user: __token__
136-
password: ${{ secrets.pypi_password }}
141+
run: |
142+
twine upload -r pypi dist/*
143+
env:
144+
TWINE_USERNAME: __token__
145+
TWINE_PASSWORD: ${{ secrets.pypi_password }}
137146

138147
- name: Download CSS pack
139148
uses: actions/download-artifact@v2

CHANGELOG.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,21 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [1.0.1] - 2023-04-25
9+
- Improves the `contribs` plugin, adding the possibility to document
10+
contributors for a page also using `.txt` files close to `.md` files. This
11+
can be useful in several cases:
12+
- - To document contributors who worked outside of Git, for example when providing
13+
pictures for the page, or written content provided to someone who is
14+
adding content to the MkDocs site.
15+
- - To document contributors following a Git history re-write
16+
- Improves the `contribs` plugin, adding the possibility to exclude files by
17+
glob patterns (fix #33).
18+
- Improves the `contribs` plugin, adding the possibility to merge contributors
19+
by name, for scenarios when the same person commits using different names
20+
(Git reports different contributors in such cases) and it is preferred
21+
displaying information aggregated as single contributor.
22+
823
## [1.0.0] - 2022-12-20
924
- Adds the possibility to specify a `class` for the root HTML element of `cards`.
1025
- Fixes a bug in the `contribs` plugin (adds a carriage return before the

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ test-cov-unit:
3737

3838

3939
test-cov:
40-
pytest --cov-report html --cov=neoteroi
40+
pytest --cov-report html --cov=neoteroi tests
4141

4242

4343
format:

neoteroi/mkdocs/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "1.0.0"
1+
__version__ = "1.0.1"

neoteroi/mkdocs/contribs/__init__.py

Lines changed: 61 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@
88
"""
99
import logging
1010
from datetime import datetime
11+
from fnmatch import fnmatch
1112
from pathlib import Path
1213
from subprocess import CalledProcessError
13-
from typing import List, Optional
14+
from typing import List
1415

1516
from mkdocs.config import config_options as c
1617
from mkdocs.plugins import BasePlugin
@@ -20,10 +21,35 @@
2021
from neoteroi.mkdocs.contribs.domain import ContributionsReader, Contributor
2122
from neoteroi.mkdocs.contribs.git import GitContributionsReader
2223
from neoteroi.mkdocs.contribs.html import ContribsViewOptions, render_contribution_stats
24+
from neoteroi.mkdocs.contribs.txt import TXTContributionsReader
2325

2426
logger = logging.getLogger("MARKDOWN")
2527

2628

29+
class DefaultContributionsReader(ContributionsReader):
30+
"""
31+
Supports both contributors obtained from Git history and from configuration files.
32+
"""
33+
34+
def __init__(self) -> None:
35+
super().__init__()
36+
self._git_reader = GitContributionsReader()
37+
self._txt_reader = TXTContributionsReader()
38+
39+
def get_contributors(self, file_path: Path) -> List[Contributor]:
40+
git_history_contributors = self._git_reader.get_contributors(file_path)
41+
configured_contributors = self._txt_reader.get_contributors(file_path)
42+
return list(
43+
{
44+
item.email: item
45+
for item in configured_contributors + git_history_contributors
46+
}.values()
47+
)
48+
49+
def get_last_modified_date(self, file_path: Path) -> datetime:
50+
return self._git_reader.get_last_modified_date(file_path)
51+
52+
2753
class ContribsPlugin(BasePlugin):
2854
_contribs_reader: ContributionsReader
2955
config_scheme = (
@@ -33,16 +59,14 @@ class ContribsPlugin(BasePlugin):
3359
("contributors", c.Type(list, default=[])),
3460
("show_last_modified_time", c.Type(bool, default=True)),
3561
("show_contributors_title", c.Type(bool, default=False)),
62+
("exclude", c.Type(list, default=[])),
3663
)
3764

3865
def __init__(self) -> None:
3966
super().__init__()
40-
self._contribs_reader = GitContributionsReader()
41-
42-
def _read_contributor_merge_with(self, contributor_info) -> Optional[str]:
43-
return contributor_info.get("merge_with")
67+
self._contribs_reader = DefaultContributionsReader()
4468

45-
def _handle_merge_contributor_info(
69+
def _merge_contributor_by_email(
4670
self,
4771
contributors: List[Contributor],
4872
contributor: Contributor,
@@ -67,6 +91,7 @@ def _handle_merge_contributor_info(
6791
if parent:
6892
parent.count += contributor.count
6993
return True
94+
7095
return False
7196

7297
def _get_contributors(self, page_file: File) -> List[Contributor]:
@@ -83,6 +108,7 @@ def _get_contributors(self, page_file: File) -> List[Contributor]:
83108
contributor_info = next(
84109
(item for item in info if item.get("email") == contributor.email), None
85110
)
111+
86112
if contributor_info:
87113
contributor.image = contributor_info.get("image")
88114
contributor.key = contributor_info.get("key")
@@ -91,8 +117,24 @@ def _get_contributors(self, page_file: File) -> List[Contributor]:
91117
# ignore the contributor's information (can be useful for bots)
92118
continue
93119

120+
if (
121+
"name" in contributor_info
122+
and contributor_info["name"] != contributor.name
123+
):
124+
parent = next(
125+
(
126+
other
127+
for other in contributors
128+
if other.name == contributor_info["name"]
129+
),
130+
None,
131+
)
132+
if parent:
133+
parent.count += contributor.count
134+
continue
135+
94136
# should contributor information be merged with another object?
95-
if self._handle_merge_contributor_info(
137+
if self._merge_contributor_by_email(
96138
contributors, contributor, contributor_info
97139
):
98140
# skip this item as it was merged with another one
@@ -103,7 +145,7 @@ def _get_contributors(self, page_file: File) -> List[Contributor]:
103145
return results
104146

105147
def _get_last_commit_date(self, page_file: File) -> datetime:
106-
return self._contribs_reader.get_last_commit_date(
148+
return self._contribs_reader.get_last_modified_date(
107149
Path("docs") / page_file.src_path
108150
)
109151

@@ -127,7 +169,18 @@ def _set_contributors(self, markdown: str, page: Page) -> str:
127169
)
128170
)
129171

172+
def _is_ignored_page(self, page: Page) -> bool:
173+
if not self.config.get("exclude"):
174+
return False
175+
176+
return any(
177+
fnmatch(page.file.src_path, ignored_pattern)
178+
for ignored_pattern in self.config["exclude"]
179+
)
180+
130181
def on_page_markdown(self, markdown, *args, **kwargs):
182+
if self._is_ignored_page(kwargs["page"]):
183+
return markdown
131184
try:
132185
markdown = self._set_contributors(markdown, kwargs["page"])
133186
except (CalledProcessError, ValueError) as operation_error:

neoteroi/mkdocs/contribs/domain.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,5 +20,5 @@ def get_contributors(self, file_path: Path) -> List[Contributor]:
2020
"""Obtains the list of contributors for a file with the given path."""
2121

2222
@abstractmethod
23-
def get_last_commit_date(self, file_path: Path) -> datetime:
23+
def get_last_modified_date(self, file_path: Path) -> datetime:
2424
"""Reads the last commit date of a file."""

neoteroi/mkdocs/contribs/git.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
"""
2+
This module defines a ContributionsReader that obtains contributors' list for a page
3+
from the Git history. Note that this ContributionsReader won't work well in case of
4+
history rewrites or files renamed without keeping contributor's history.
5+
For this reason, it should be used together with a
6+
"""
17
import re
28
import subprocess
39
from datetime import datetime
@@ -10,7 +16,6 @@
1016

1117

1218
class GitContributionsReader(ContributionsReader):
13-
1419
_name_email_rx = re.compile(r"(?P<name>[^\<]+)<(?P<email>[^\>]+)>")
1520

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

5863
return list(self.parse_committers(result))
5964

60-
def get_last_commit_date(self, file_path: Path) -> datetime:
65+
def get_last_modified_date(self, file_path: Path) -> datetime:
6166
"""Reads the last commit on a file."""
6267
result = self._decode(
6368
subprocess.check_output(

neoteroi/mkdocs/contribs/txt.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import os
2+
import re
3+
from datetime import datetime
4+
from pathlib import Path
5+
from typing import Iterable, List, Tuple
6+
7+
from dateutil.parser import parse
8+
9+
from neoteroi.mkdocs.contribs.domain import ContributionsReader, Contributor
10+
11+
12+
def _read_lines_strip_comments(file_path: Path):
13+
with open(str(file_path), mode="rt", encoding="utf8") as file:
14+
lines = file.readlines()
15+
lines = (re.sub("#.+$", "", x).strip() for x in lines)
16+
return [line for line in lines if line]
17+
18+
19+
class TXTContributionsReader(ContributionsReader):
20+
"""
21+
A ContributionsReader that can read contributors information described in .txt
22+
files.
23+
"""
24+
25+
_contrib_rx = re.compile(
26+
r"(?P<name>[^\<]+)<(?P<email>[^\>]+)>\s\((?P<count>[^\>]+)\)"
27+
)
28+
29+
_last_mod_time_rx = re.compile(
30+
r"^\s*Last\smodified\stime:\s(?P<value>.+)$", re.IGNORECASE | re.MULTILINE
31+
)
32+
33+
def _parse_value(self, value: str) -> Tuple[str, str, int]:
34+
match = self._contrib_rx.search(value)
35+
if match:
36+
values = match.groupdict()
37+
name = values["name"].strip()
38+
email = values["email"].strip()
39+
count = int(values["count"])
40+
else:
41+
name, email, count = ("", "", -1)
42+
return name, email, count
43+
44+
def _get_txt_file_path(self, file_path: Path) -> Path:
45+
path_without_extension = os.path.splitext(file_path)[0]
46+
return Path(path_without_extension + ".contribs.txt")
47+
48+
def _get_contributors_from_txt_file(self, file_path: Path) -> Iterable[Contributor]:
49+
for line in _read_lines_strip_comments(file_path):
50+
name, email, count = self._parse_value(line)
51+
if name and email:
52+
yield Contributor(name, email, count)
53+
54+
def get_contributors(self, file_path: Path) -> List[Contributor]:
55+
"""
56+
Obtains the list of contributors from a txt file with the given path.
57+
The file contents should look like:
58+
59+
Charlie Brown <charlie.brown@peanuts.com> (3)
60+
61+
Having each line with such pattern:
62+
63+
Name <email> (Contributions Count)
64+
65+
and supporting comments using hashes:
66+
# Example comment
67+
"""
68+
txt_path = self._get_txt_file_path(file_path)
69+
70+
if not txt_path.exists():
71+
return []
72+
73+
return list(self._get_contributors_from_txt_file(txt_path))
74+
75+
def get_last_modified_date(self, file_path: Path) -> datetime:
76+
"""Reads the last commit date of a file."""
77+
txt_path = self._get_txt_file_path(file_path)
78+
79+
if not txt_path.exists():
80+
raise FileNotFoundError()
81+
82+
match = self._last_mod_time_rx.search(txt_path.read_text("utf8"))
83+
84+
if match:
85+
return parse(match.groups()[0])
86+
87+
return datetime.min

neoteroi/mkdocs/markdown/tables/spantable.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ def html_class(self) -> Optional[str]:
2424
def _iter_coords(
2525
x: int, y: int, colspan: int, rowspan: int
2626
) -> Iterable[Tuple[int, int]]:
27-
2827
for x_increment in range(colspan):
2928
for y_increment in range(rowspan):
3029
yield (x + x_increment, y + y_increment)

neoteroi/mkdocs/projects/__init__.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212

1313

1414
class ProjectsExtension(Extension):
15-
1615
config = {
1716
"priority": [12, "The priority to be configured for the extension."],
1817
}

0 commit comments

Comments
 (0)