sphinxnotes-recentupdate 1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,276 @@
1
+ """
2
+ sphinxnotes.recentupdate
3
+ ~~~~~~~~~~~~~~~~~~~~~~~~
4
+
5
+ Get the document update information from git and display it in Sphinx documentation.
6
+
7
+ :copyright: Copyright 2021 Shengyu Zhang
8
+ :license: BSD, see LICENSE for details.
9
+ """
10
+
11
+ from __future__ import annotations
12
+ from typing import Iterable, TYPE_CHECKING
13
+ from textwrap import dedent
14
+ from datetime import datetime
15
+ from dataclasses import dataclass
16
+ from os import path
17
+ from pathlib import Path
18
+
19
+ from docutils import nodes
20
+ from docutils.statemachine import StringList
21
+ from docutils.parsers.rst import directives
22
+
23
+ from sphinx.util import logging
24
+ from sphinx.util.docutils import SphinxDirective
25
+ from sphinx.util.nodes import nested_parse_with_titles
26
+ from sphinx.util.matching import Matcher
27
+
28
+ if TYPE_CHECKING:
29
+ from sphinx.application import Sphinx
30
+
31
+ from git import Repo
32
+ import jinja2
33
+
34
+ from . import meta
35
+
36
+
37
+ logger = logging.getLogger(__name__)
38
+
39
+
40
+ class Environment(jinja2.Environment):
41
+ datefmt: str
42
+
43
+ def __init__(self, datefmt: str, *args, **kwargs):
44
+ super().__init__(*args, **kwargs)
45
+ self.datefmt = datefmt
46
+ self.filters['strftime'] = self._strftime_filter
47
+ self.filters['roles'] = self._roles_filter
48
+
49
+ def _strftime_filter(self, value, format=None) -> str:
50
+ """
51
+ Filter for stringify datetime given format.
52
+ if no format given, use confval "recentupdate_date_format".
53
+ """
54
+ if format is None:
55
+ format = self.datefmt
56
+ return value.strftime(format)
57
+
58
+ def _roles_filter(self, value: Iterable[str], role: str) -> Iterable[str]:
59
+ """
60
+ A heplfer filter for converting list of string to list of role.
61
+
62
+ For example::
63
+
64
+ {{ ["foo", "bar"] | roles("doc") }}
65
+
66
+ Produces ``[":doc:`foo`", ":doc:`bar`"]``.
67
+ """
68
+ return map(lambda x: ':%s:`%s`' % (role, x), value)
69
+
70
+
71
+ @dataclass
72
+ class Revision(object):
73
+ """
74
+ Revision represents a git commit which contains document changes.
75
+ """
76
+
77
+ #: Git commit message
78
+ message: str
79
+ #: Git commit author
80
+ author: str
81
+ #: Git commit author date
82
+ date: datetime
83
+
84
+ # FYI, possible status letters are:
85
+ # :A: addition of a file
86
+ # :C: copy of a file into a new one
87
+ # :D: deletion of a file
88
+ # :M: modification of the contents or mode of a file
89
+ # :R: renaming of a file
90
+ # :T: change in the type of the file
91
+ # :U: file is unmerged (you must complete the merge before it can be committed)
92
+ # :X: "unknown" change type (most probably a bug, please report it)
93
+
94
+ #: List of docname, corresponding to files which are modified
95
+ addition: list[str]
96
+ #: List of docname, corresponding to files which are newly added
97
+ modification: list[str]
98
+ #: List of docname, corresponding to files which are deleted
99
+ deletion: list[str]
100
+
101
+
102
+ class RecentUpdateDirective(SphinxDirective):
103
+ """Directive for displaying recent update."""
104
+
105
+ # Member of parent
106
+ has_content: bool = True
107
+ required_arguments: int = 0
108
+ optional_arguments: int = 1
109
+ final_argument_whitespace: bool = False
110
+ option_spec = {}
111
+
112
+ #: Repo info
113
+ repo: Repo = None
114
+
115
+ def _get_docname(self, relfn_to_repo: str) -> str | None:
116
+ relsrcdir_to_repo = path.relpath(self.env.srcdir, self.repo.working_dir)
117
+ relfn_to_srcdir = path.relpath(relfn_to_repo, relsrcdir_to_repo)
118
+ absfn = path.abspath(relfn_to_srcdir)
119
+ if Path(path.commonpath([self.env.srcdir, absfn])) != self.env.srcdir:
120
+ logger.debug(f'Skip {relfn_to_repo}: out of srcdir')
121
+ return None
122
+
123
+ excluded = Matcher(self.config.exclude_patterns)
124
+ if excluded(relfn_to_srcdir):
125
+ logger.debug(f'Skip {relfn_to_repo}: excluded by exclude_patterns confval')
126
+ return None
127
+
128
+ docname, ext = path.splitext(relfn_to_srcdir)
129
+ source_suffix = list(self.config.source_suffix.keys())
130
+ if not ext or ext not in source_suffix:
131
+ logger.debug(f'Skip {relfn_to_repo}: not {source_suffix} files')
132
+ return None
133
+
134
+ for p in self.config.recentupdate_exclude_path:
135
+ absp = path.abspath(p)
136
+ if path.commonpath([absp, absfn]) == absp:
137
+ logger.debug(
138
+ f'Skip {relfn_to_repo}: excluded by recentupdate_exclude_path confval'
139
+ )
140
+ return None
141
+
142
+ logger.debug(f'Get docname: {docname}')
143
+ return docname
144
+
145
+ def _context(self, count: int) -> dict[str, any]:
146
+ revisions = []
147
+ res = {'revisions': revisions}
148
+
149
+ cur = self.repo.head.commit
150
+ if cur is None:
151
+ return res
152
+
153
+ # Get recent N commits which contain document changes (N = count)
154
+ n = 0
155
+ while n < count:
156
+ prev = cur.parents[0] if len(cur.parents) != 0 else None
157
+ if prev is None:
158
+ break
159
+
160
+ matches = [
161
+ x in cur.message for x in self.config.recentupdate_exclude_commit
162
+ ]
163
+ if any(matches):
164
+ logger.debug(
165
+ f'Skip commit {cur.hexsha}: excluded by recentupdate_exclude_commit confval'
166
+ )
167
+ cur = prev
168
+ continue
169
+
170
+ m = []
171
+ a = []
172
+ d = []
173
+ diff_idx = prev.tree.diff(cur)
174
+ for diff in diff_idx:
175
+ docname = self._get_docname(diff.a_path)
176
+ if docname is None:
177
+ # Skip files out of srcdir
178
+ continue
179
+
180
+ if diff.change_type == 'M':
181
+ m.append(docname)
182
+ elif diff.change_type == 'A':
183
+ a.append(docname)
184
+ elif diff.change_type == 'D':
185
+ d.append(docname)
186
+ else:
187
+ logger.warning(
188
+ f'Skip {diff.a_path}: unsupport change type {diff.change_type}'
189
+ )
190
+
191
+ if len(m) + len(a) + len(d) == 0:
192
+ # Dont create revisions when no document changes
193
+ logger.debug(f'Skip commit {cur.hexsha}: no document changes')
194
+ cur = prev
195
+ continue
196
+
197
+ revisions.append(
198
+ Revision(
199
+ message=cur.message,
200
+ author=cur.author,
201
+ date=datetime.utcfromtimestamp(cur.authored_date),
202
+ modification=m,
203
+ addition=a,
204
+ deletion=d,
205
+ )
206
+ )
207
+ cur = prev
208
+ n += 1
209
+
210
+ logger.warning(
211
+ f'[recentupdate] Intend to get recent {count} commits, eventually get {n}'
212
+ )
213
+
214
+ return res
215
+
216
+ def run(self) -> list[nodes.Node]:
217
+ if len(self.arguments) >= 1:
218
+ count = directives.nonnegative_int(self.arguments[0])
219
+ else:
220
+ count = self.config.recentupdate_count
221
+
222
+ # Render reST from Jinja template, then parse it in to document
223
+ env = Environment(self.config.recentupdate_date_format)
224
+
225
+ try:
226
+ template = env.from_string(
227
+ '\n'.join(list(self.content)) or self.config.recentupdate_template
228
+ )
229
+ lines = template.render(self._context(count)).split('\n')
230
+ except Exception as e:
231
+ msg = f'failed to render recentupdate template: {e}'
232
+ logger.warning(msg, location=self.state.parent)
233
+ sm = nodes.system_message(
234
+ msg, type='WARNING', level=2, backrefs=[], source=''
235
+ )
236
+ return [sm]
237
+ else:
238
+ nested_parse_with_titles(self.state, StringList(lines), self.state.parent)
239
+ return []
240
+
241
+
242
+ DEFAULT_TEMPLATE = dedent("""
243
+ {% for r in revisions %}
244
+ {{ r.date | strftime }}
245
+ :Author: {{ r.author }}
246
+ :Message: {{ r.message }}
247
+
248
+ {% if r.modification %}
249
+ - Modified {{ r.modification | roles("doc") | join(", ") }}
250
+ {% endif %}
251
+ {% if r.addition %}
252
+ - Added {{ r.addition | roles("doc") | join(", ") }}
253
+ {% endif %}
254
+ {% if r.deletion %}
255
+ - Deleted {{ r.deletion | join(", ") }}
256
+ {% endif %}
257
+ {% endfor %}
258
+ """)
259
+
260
+
261
+ def setup(app: Sphinx):
262
+ """Sphinx extension entrypoint."""
263
+ meta.pre_setup(app)
264
+
265
+ # Set current git repo
266
+ RecentUpdateDirective.repo = Repo(app.srcdir, search_parent_directories=True)
267
+
268
+ app.add_directive('recentupdate', RecentUpdateDirective)
269
+
270
+ app.add_config_value('recentupdate_count', 10, 'env')
271
+ app.add_config_value('recentupdate_template', DEFAULT_TEMPLATE, 'env')
272
+ app.add_config_value('recentupdate_date_format', '%Y-%m-%d', 'env')
273
+ app.add_config_value('recentupdate_exclude_path', [], 'env')
274
+ app.add_config_value('recentupdate_exclude_commit', ['skip-recentupdate'], 'env')
275
+
276
+ return meta.post_setup(app)
@@ -0,0 +1,35 @@
1
+ # This file is generated from sphinx-notes/cookiecutter.
2
+ # DO NOT EDIT!!!
3
+
4
+ ################################################################################
5
+ # Project meta infos.
6
+ ################################################################################
7
+
8
+ from __future__ import annotations
9
+ from importlib import metadata
10
+
11
+ __project__ = 'sphinxnotes-recentupdate'
12
+ __author__ = 'Shengyu Zhang'
13
+ __desc__ = 'Get the document update information from git and display it in Sphinx documentation'
14
+
15
+ try:
16
+ __version__ = metadata.version('sphinxnotes-recentupdate')
17
+ except metadata.PackageNotFoundError:
18
+ __version__ = 'unknown'
19
+
20
+
21
+ ################################################################################
22
+ # Sphinx extension utils.
23
+ ################################################################################
24
+
25
+
26
+ def pre_setup(app):
27
+ app.require_sphinx('7.0')
28
+
29
+
30
+ def post_setup(app):
31
+ return {
32
+ 'version': __version__,
33
+ 'parallel_read_safe': True,
34
+ 'parallel_write_safe': True,
35
+ }
File without changes
@@ -0,0 +1,80 @@
1
+ Metadata-Version: 2.4
2
+ Name: sphinxnotes-recentupdate
3
+ Version: 1.0
4
+ Summary: Get the document update information from git and display it in Sphinx documentation
5
+ Author: Shengyu Zhang
6
+ Maintainer: Shengyu Zhang
7
+ License-Expression: BSD-3-Clause
8
+ Project-URL: homepage, https://sphinx.silverrainz.me/recentupdate
9
+ Project-URL: documentation, https://sphinx.silverrainz.me/recentupdate
10
+ Project-URL: repository, https://github.com/sphinx-notes/recentupdate
11
+ Project-URL: changelog, https://sphinx.silverrainz.me/recentupdate/changelog.html
12
+ Project-URL: tracker, https://github.com/sphinx-notes/recentupdate/issues
13
+ Keywords: sphinx,extension,documentation,sphinxnotes
14
+ Classifier: Development Status :: 3 - Alpha
15
+ Classifier: Environment :: Plugins
16
+ Classifier: Framework :: Sphinx
17
+ Classifier: Framework :: Sphinx :: Extension
18
+ Classifier: Operating System :: OS Independent
19
+ Classifier: Programming Language :: Python
20
+ Classifier: Programming Language :: Python :: 3
21
+ Classifier: Topic :: Documentation
22
+ Classifier: Topic :: Documentation :: Sphinx
23
+ Requires-Python: >=3.12
24
+ Description-Content-Type: text/x-rst
25
+ License-File: LICENSE
26
+ Requires-Dist: Sphinx>=7.0
27
+ Requires-Dist: GitPython
28
+ Requires-Dist: Jinja2
29
+ Provides-Extra: dev
30
+ Requires-Dist: build; extra == "dev"
31
+ Requires-Dist: twine; extra == "dev"
32
+ Requires-Dist: cruft; extra == "dev"
33
+ Requires-Dist: ruff>=0.11.10; extra == "dev"
34
+ Requires-Dist: pre-commit; extra == "dev"
35
+ Provides-Extra: test
36
+ Requires-Dist: pytest; extra == "test"
37
+ Provides-Extra: docs
38
+ Requires-Dist: furo; extra == "docs"
39
+ Requires-Dist: sphinx_design; extra == "docs"
40
+ Requires-Dist: sphinx_copybutton; extra == "docs"
41
+ Requires-Dist: sphinxcontrib-gtagjs; extra == "docs"
42
+ Requires-Dist: sphinx-sitemap; extra == "docs"
43
+ Requires-Dist: sphinxext-opengraph; extra == "docs"
44
+ Requires-Dist: sphinx-last-updated-by-git; extra == "docs"
45
+ Requires-Dist: sphinxnotes-project; extra == "docs"
46
+ Requires-Dist: sphinxnotes-comboroles; extra == "docs"
47
+ Dynamic: license-file
48
+
49
+ .. This file is generated from sphinx-notes/cookiecutter.
50
+ You need to consider modifying the TEMPLATE or modifying THIS FILE.
51
+
52
+ ========================
53
+ sphinxnotes-recentupdate
54
+ ========================
55
+
56
+ .. |docs| image:: https://img.shields.io/github/deployments/sphinx-notes/recentupdate/github-pages
57
+ :target: https://sphinx.silverrainz.me/recentupdate
58
+ :alt: Documentation Status
59
+ .. |license| image:: https://img.shields.io/github/license/sphinx-notes/recentupdate
60
+ :target: https://github.com/sphinx-notes/recentupdate/blob/master/LICENSE
61
+ :alt: Open Source License
62
+ .. |pypi| image:: https://img.shields.io/pypi/v/sphinxnotes-recentupdate.svg
63
+ :target: https://pypi.python.org/pypi/sphinxnotes-recentupdate
64
+ :alt: PyPI Package
65
+ .. |download| image:: https://img.shields.io/pypi/dm/sphinxnotes-recentupdate
66
+ :target: https://pypi.python.org/pypi/sphinxnotes-recentupdate
67
+ :alt: PyPI Package Downloads
68
+
69
+ |docs| |license| |pypi| |download|
70
+
71
+ Get the document update information from git and display it in Sphinx documentation.
72
+
73
+ .. INTRODUCTION START
74
+ (MUST written in standard reStructuredText, without Sphinx stuff)
75
+
76
+ .. INTRODUCTION END
77
+
78
+ Please refer to Documentation_ for more details.
79
+
80
+ .. _Documentation: https://sphinx.silverrainz.me/recentupdate
@@ -0,0 +1,8 @@
1
+ sphinxnotes/recentupdate/__init__.py,sha256=32kX_x5XyvY4HERoJpMscl-UtAq-VsLDuT42Rh_neDE,9148
2
+ sphinxnotes/recentupdate/meta.py,sha256=Spl5MmwH1Nthcm-wbW2UzZnqrfKdLiOmfa2V2sEKHTw,1018
3
+ sphinxnotes/recentupdate/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ sphinxnotes_recentupdate-1.0.dist-info/licenses/LICENSE,sha256=Sa0uIqyOtooTO3N9W29mvi1uHW1uL0lmU7oRYumv8sA,1520
5
+ sphinxnotes_recentupdate-1.0.dist-info/METADATA,sha256=85RSHYtjoOWqufu258MPNf3ipcKinOztoy78VYK2hYE,3231
6
+ sphinxnotes_recentupdate-1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
7
+ sphinxnotes_recentupdate-1.0.dist-info/top_level.txt,sha256=V-VWtuPpvntuonwz7_KceUnT4-CbPR1gJ26BTFpvcJI,12
8
+ sphinxnotes_recentupdate-1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,29 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2025, Shengyu Zhang
4
+ All rights reserved.
5
+
6
+ Redistribution and use in source and binary forms, with or without
7
+ modification, are permitted provided that the following conditions are met:
8
+
9
+ 1. Redistributions of source code must retain the above copyright notice, this
10
+ list of conditions and the following disclaimer.
11
+
12
+ 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ this list of conditions and the following disclaimer in the documentation
14
+ and/or other materials provided with the distribution.
15
+
16
+ 3. Neither the name of the copyright holder nor the names of its
17
+ contributors may be used to endorse or promote products derived from
18
+ this software without specific prior written permission.
19
+
20
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1 @@
1
+ sphinxnotes