Skip to content
This repository was archived by the owner on Jun 11, 2019. It is now read-only.

Commit d79edd5

Browse files
committed
Setting html_last_updated_fmt value to last commit
Setting last_updated timestamp value to the last authored date of the specific RST file instead of datetime.now(). Fixing broken docsV in TravisCI after using google analytics pypi package. Fixes sphinx-contrib#26
1 parent 407427d commit d79edd5

File tree

9 files changed

+163
-11
lines changed

9 files changed

+163
-11
lines changed

README.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,15 @@ Changelog
4949

5050
This project adheres to `Semantic Versioning <http://semver.org/>`_.
5151

52+
Unreleased
53+
----------
54+
55+
Added
56+
* Time value of ``html_last_updated_fmt`` will be the last git commit (authored) date.
57+
58+
Fixed
59+
* https://github.com/Robpol86/sphinxcontrib-versioning/issues/26
60+
5261
2.2.0 - 2016-09-15
5362
------------------
5463

docs/conf.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
sys.path.append(os.path.realpath(os.path.join(os.path.dirname(__file__), '..')))
1010
author = '@Robpol86'
1111
copyright = '{}, {}'.format(time.strftime('%Y'), author)
12+
html_last_updated_fmt = '%c'
1213
master_doc = 'index'
1314
project = __import__('setup').NAME
1415
pygments_style = 'friendly'

sphinxcontrib/versioning/git.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,13 +112,14 @@ def chunk(iterator, max_size):
112112
yield chunked
113113

114114

115-
def run_command(local_root, command, env_var=True, pipeto=None, retry=0):
115+
def run_command(local_root, command, env_var=True, pipeto=None, retry=0, environ=None):
116116
"""Run a command and return the output.
117117
118118
:raise CalledProcessError: Command exits non-zero.
119119
120120
:param str local_root: Local path to git root directory.
121121
:param iter command: Command to run.
122+
:param dict environ: Environment variables to set/override in the command.
122123
:param bool env_var: Define GIT_DIR environment variable (on non-Windows).
123124
:param function pipeto: Pipe `command`'s stdout to this function (only parameter given).
124125
:param int retry: Retry this many times on CalledProcessError after 0.1 seconds.
@@ -130,6 +131,8 @@ def run_command(local_root, command, env_var=True, pipeto=None, retry=0):
130131

131132
# Setup env.
132133
env = os.environ.copy()
134+
if environ:
135+
env.update(environ)
133136
if env_var and not IS_WINDOWS:
134137
env['GIT_DIR'] = os.path.join(local_root, '.git')
135138
else:
@@ -270,6 +273,8 @@ def fetch_commits(local_root, remotes):
270273
def export(local_root, commit, target):
271274
"""Export git commit to directory. "Extracts" all files at the commit to the target directory.
272275
276+
Set mtime of RST files to last commit date.
277+
273278
:raise CalledProcessError: Unhandled git command failure.
274279
275280
:param str local_root: Local path to git root directory.
@@ -278,6 +283,7 @@ def export(local_root, commit, target):
278283
"""
279284
log = logging.getLogger(__name__)
280285
target = os.path.realpath(target)
286+
mtimes = list()
281287

282288
# Define extract function.
283289
def extract(stdout):
@@ -300,6 +306,8 @@ def extract(stdout):
300306
queued_links.append(info)
301307
else: # Handle files.
302308
tar.extract(member=info, path=target)
309+
if os.path.splitext(info.name)[1].lower() == '.rst':
310+
mtimes.append(info.name)
303311
for info in (i for i in queued_links if os.path.exists(os.path.join(target, i.linkname))):
304312
tar.extract(member=info, path=target)
305313
except tarfile.TarError as exc:
@@ -308,6 +316,11 @@ def extract(stdout):
308316
# Run command.
309317
run_command(local_root, ['git', 'archive', '--format=tar', commit], pipeto=extract)
310318

319+
# Set mtime.
320+
for file_path in mtimes:
321+
last_committed = int(run_command(local_root, ['git', 'log', '-n1', '--format=%at', commit, '--', file_path]))
322+
os.utime(os.path.join(target, file_path), (last_committed, last_committed))
323+
311324

312325
def clone(local_root, new_root, remote, branch, rel_dest, exclude):
313326
"""Clone "local_root" origin into a new directory and check out a specific branch. Optionally run "git rm".

sphinxcontrib/versioning/sphinx_.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
"""Interface with Sphinx."""
22

3+
import datetime
34
import logging
45
import multiprocessing
56
import os
67
import sys
78

8-
from sphinx import application, build_main
9+
from sphinx import application, build_main, locale
910
from sphinx.builders.html import StandaloneHTMLBuilder
1011
from sphinx.config import Config as SphinxConfig
1112
from sphinx.errors import SphinxError
1213
from sphinx.jinja2glue import SphinxFileSystemLoader
14+
from sphinx.util.i18n import format_date
1315

1416
from sphinxcontrib.versioning import __version__
1517
from sphinxcontrib.versioning.lib import Config, HandledError, TempDir
@@ -83,7 +85,7 @@ def html_page_context(cls, app, pagename, templatename, context, doctree):
8385
:param dict context: Jinja2 HTML context.
8486
:param docutils.nodes.document doctree: Tree of docutils nodes.
8587
"""
86-
assert pagename or templatename or doctree # Unused, for linting.
88+
assert templatename or doctree # Unused, for linting.
8789
cls.VERSIONS.context = context
8890
versions = cls.VERSIONS
8991
this_remote = versions[cls.CURRENT_VERSION]
@@ -123,6 +125,14 @@ def html_page_context(cls, app, pagename, templatename, context, doctree):
123125
if STATIC_DIR not in app.config.html_static_path:
124126
app.config.html_static_path.append(STATIC_DIR)
125127

128+
# Reset last_updated with file's mtime (will be last git commit authored date).
129+
if app.config.html_last_updated_fmt is not None:
130+
file_path = app.env.doc2path(pagename)
131+
if os.path.isfile(file_path):
132+
lufmt = app.config.html_last_updated_fmt or getattr(locale, '_')('%b %d, %Y')
133+
mtime = datetime.datetime.fromtimestamp(os.path.getmtime(file_path))
134+
context['last_updated'] = format_date(lufmt, mtime, language=app.config.language, warn=app.warn)
135+
126136

127137
def setup(app):
128138
"""Called by Sphinx during phase 0 (initialization).

tests/conftest.py

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
"""pytest fixtures for this directory."""
22

3+
import datetime
34
import re
5+
import time
46

57
import pytest
68

@@ -9,6 +11,24 @@
911

1012
RE_BANNER = re.compile('>(?:<a href="([^"]+)">)?<b>Warning:</b> This document is for ([^<]+).(?:</a>)?</p>')
1113
RE_URLS = re.compile('<li><a href="[^"]+">[^<]+</a></li>')
14+
ROOT_TS = int(time.mktime((2016, 12, 5, 3, 17, 5, 0, 0, 0)))
15+
16+
17+
def author_committer_dates(offset):
18+
"""Return ISO time for GIT_AUTHOR_DATE and GIT_COMMITTER_DATE environment variables.
19+
20+
Always starts on December 05 2016 03:17:05 AM local time. Committer date always 2 seconds after author date.
21+
22+
:param int offset: Minutes to offset both timestamps.
23+
24+
:return: GIT_AUTHOR_DATE and GIT_COMMITTER_DATE timestamps, can be merged into os.environ.
25+
:rtype: dict
26+
"""
27+
dt = datetime.datetime.fromtimestamp(ROOT_TS) + datetime.timedelta(minutes=offset)
28+
env = dict(GIT_AUTHOR_DATE=str(dt))
29+
dt += datetime.timedelta(seconds=2)
30+
env['GIT_COMMITTER_DATE'] = str(dt)
31+
return env
1232

1333

1434
def run(directory, command, *args, **kwargs):
@@ -32,6 +52,8 @@ def pytest_namespace():
3252
:rtype: dict
3353
"""
3454
return dict(
55+
author_committer_dates=author_committer_dates,
56+
ROOT_TS=ROOT_TS,
3557
run=run,
3658
)
3759

@@ -132,7 +154,7 @@ def fx_local_commit(local_empty):
132154
"""
133155
local_empty.join('README').write('Dummy readme file.')
134156
run(local_empty, ['git', 'add', 'README'])
135-
run(local_empty, ['git', 'commit', '-m', 'Initial commit.'])
157+
run(local_empty, ['git', 'commit', '-m', 'Initial commit.'], environ=author_committer_dates(0))
136158
return local_empty
137159

138160

@@ -191,12 +213,12 @@ def outdate_local(tmpdir, local_light, remote):
191213
run(local_ahead, ['git', 'clone', remote, '.'])
192214
run(local_ahead, ['git', 'checkout', '-b', 'un_pushed_branch'])
193215
local_ahead.join('README').write('changed')
194-
run(local_ahead, ['git', 'commit', '-am', 'Changed new branch'])
216+
run(local_ahead, ['git', 'commit', '-am', 'Changed new branch'], environ=author_committer_dates(1))
195217
run(local_ahead, ['git', 'tag', 'nb_tag'])
196218
run(local_ahead, ['git', 'checkout', '--orphan', 'orphaned_branch'])
197219
local_ahead.join('README').write('new')
198220
run(local_ahead, ['git', 'add', 'README'])
199-
run(local_ahead, ['git', 'commit', '-m', 'Added new README'])
221+
run(local_ahead, ['git', 'commit', '-m', 'Added new README'], environ=author_committer_dates(2))
200222
run(local_ahead, ['git', 'tag', '--annotate', '-m', 'Tag annotation.', 'ob_at'])
201223
run(local_ahead, ['git', 'push', 'origin', 'nb_tag', 'orphaned_branch', 'ob_at'])
202224
return local_ahead
@@ -248,7 +270,7 @@ def fx_local_docs(local):
248270
'Sub page documentation 3.\n'
249271
)
250272
run(local, ['git', 'add', 'conf.py', 'contents.rst', 'one.rst', 'two.rst', 'three.rst'])
251-
run(local, ['git', 'commit', '-m', 'Adding docs.'])
273+
run(local, ['git', 'commit', '-m', 'Adding docs.'], environ=author_committer_dates(3))
252274
run(local, ['git', 'push', 'origin', 'master'])
253275
return local
254276

@@ -263,7 +285,7 @@ def local_docs_ghp(local_docs):
263285
run(local_docs, ['git', 'rm', '-rf', '.'])
264286
local_docs.join('README').write('Orphaned branch for HTML docs.')
265287
run(local_docs, ['git', 'add', 'README'])
266-
run(local_docs, ['git', 'commit', '-m', 'Initial Commit'])
288+
run(local_docs, ['git', 'commit', '-m', 'Initial Commit'], environ=author_committer_dates(4))
267289
run(local_docs, ['git', 'push', 'origin', 'gh-pages'])
268290
run(local_docs, ['git', 'checkout', 'master'])
269291
return local_docs

tests/test_git/test_export.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
"""Test function in module."""
22

3+
import time
4+
from datetime import datetime
35
from os.path import join
46
from subprocess import CalledProcessError
57

@@ -115,3 +117,51 @@ def test_symlink(tmpdir, local):
115117
pytest.run(local, ['git', 'diff-index', '--quiet', 'HEAD', '--']) # Exit 0 if nothing changed.
116118
files = sorted(f.relto(target) for f in target.listdir())
117119
assert files == ['README', 'good_symlink']
120+
121+
122+
def test_timezones(tmpdir, local):
123+
"""Test mtime on RST files with different git commit timezones.
124+
125+
:param tmpdir: pytest fixture.
126+
:param local: conftest fixture.
127+
"""
128+
files_dates = [
129+
('local.rst', ''),
130+
('UTC.rst', ' +0000'),
131+
('PDT.rst', ' -0700'),
132+
('PST.rst', ' -0800'),
133+
]
134+
135+
# Commit files.
136+
for name, offset in files_dates:
137+
local.ensure(name)
138+
pytest.run(local, ['git', 'add', name])
139+
env = pytest.author_committer_dates(0)
140+
env['GIT_AUTHOR_DATE'] += offset
141+
env['GIT_COMMITTER_DATE'] += offset
142+
pytest.run(local, ['git', 'commit', '-m', 'Added ' + name], environ=env)
143+
144+
# Run.
145+
target = tmpdir.ensure_dir('target')
146+
sha = pytest.run(local, ['git', 'rev-parse', 'HEAD']).strip()
147+
export(str(local), sha, str(target))
148+
149+
# Validate.
150+
actual = {i[0]: str(datetime.fromtimestamp(target.join(i[0]).mtime())) for i in files_dates}
151+
if -time.timezone == -28800:
152+
expected = {
153+
'local.rst': '2016-12-05 03:17:05',
154+
'UTC.rst': '2016-12-04 19:17:05',
155+
'PDT.rst': '2016-12-05 02:17:05',
156+
'PST.rst': '2016-12-05 03:17:05',
157+
}
158+
elif -time.timezone == 0:
159+
expected = {
160+
'local.rst': '2016-12-05 03:17:05',
161+
'UTC.rst': '2016-12-05 03:17:05',
162+
'PDT.rst': '2016-12-05 10:17:05',
163+
'PST.rst': '2016-12-05 11:17:05',
164+
}
165+
else:
166+
return pytest.skip('Need to add expected for {} timezone.'.format(-time.timezone))
167+
assert actual == expected

tests/test_git/test_filter_and_date.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@
66

77
from sphinxcontrib.versioning.git import filter_and_date, GitError, list_remote
88

9-
BEFORE = int(time.time())
10-
119

1210
def test_one_commit(local):
1311
"""Test with one commit.
@@ -24,7 +22,7 @@ def test_one_commit(local):
2422
# Test with existing conf_rel_path.
2523
dates = filter_and_date(str(local), ['README'], [sha])
2624
assert list(dates) == [sha]
27-
assert dates[sha][0] >= BEFORE
25+
assert dates[sha][0] >= pytest.ROOT_TS
2826
assert dates[sha][0] < time.time()
2927
assert dates[sha][1] == 'README'
3028

tests/test_routines/test_build_all.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Test function in module."""
22

3+
import re
34
from os.path import join
45

56
import pytest
@@ -9,6 +10,8 @@
910
from sphinxcontrib.versioning.routines import build_all, gather_git_info
1011
from sphinxcontrib.versioning.versions import Versions
1112

13+
RE_LAST_UPDATED = re.compile(r'Last updated[^\n]+\n')
14+
1215

1316
def test_single(tmpdir, local_docs, urls):
1417
"""With single version.
@@ -273,6 +276,51 @@ def test_banner_tag(tmpdir, banner, config, local_docs, recent):
273276
banner(dst.join(old, 'two.html'), '', 'an old version of Python')
274277

275278

279+
def test_last_updated(tmpdir, local_docs):
280+
"""Test last updated timestamp derived from git authored time.
281+
282+
:param tmpdir: pytest fixture.
283+
:param local_docs: conftest fixture.
284+
"""
285+
local_docs.join('conf.py').write(
286+
'html_last_updated_fmt = "%c"\n'
287+
'html_theme="sphinx_rtd_theme"\n'
288+
)
289+
local_docs.join('two.rst').write('Changed\n', mode='a')
290+
pytest.run(local_docs, ['git', 'commit', '-am', 'Changed two.'], environ=pytest.author_committer_dates(10))
291+
pytest.run(local_docs, ['git', 'checkout', '-b', 'other', 'master'])
292+
local_docs.join('three.rst').write('Changed\n', mode='a')
293+
pytest.run(local_docs, ['git', 'commit', '-am', 'Changed three.'], environ=pytest.author_committer_dates(11))
294+
pytest.run(local_docs, ['git', 'push', 'origin', 'master', 'other'])
295+
296+
versions = Versions(gather_git_info(str(local_docs), ['conf.py'], tuple(), tuple()))
297+
298+
# Export.
299+
exported_root = tmpdir.ensure_dir('exported_root')
300+
export(str(local_docs), versions['master']['sha'], str(exported_root.join(versions['master']['sha'])))
301+
export(str(local_docs), versions['other']['sha'], str(exported_root.join(versions['other']['sha'])))
302+
303+
# Run.
304+
destination = tmpdir.ensure_dir('destination')
305+
build_all(str(exported_root), str(destination), versions)
306+
307+
# Verify master.
308+
one = RE_LAST_UPDATED.findall(destination.join('master', 'one.html').read())
309+
two = RE_LAST_UPDATED.findall(destination.join('master', 'two.html').read())
310+
three = RE_LAST_UPDATED.findall(destination.join('master', 'three.html').read())
311+
assert one == ['Last updated on Dec 5, 2016, 3:20:05 AM.\n']
312+
assert two == ['Last updated on Dec 5, 2016, 3:27:05 AM.\n']
313+
assert three == ['Last updated on Dec 5, 2016, 3:20:05 AM.\n']
314+
315+
# Verify other.
316+
one = RE_LAST_UPDATED.findall(destination.join('other', 'one.html').read())
317+
two = RE_LAST_UPDATED.findall(destination.join('other', 'two.html').read())
318+
three = RE_LAST_UPDATED.findall(destination.join('other', 'three.html').read())
319+
assert one == ['Last updated on Dec 5, 2016, 3:20:05 AM.\n']
320+
assert two == ['Last updated on Dec 5, 2016, 3:27:05 AM.\n']
321+
assert three == ['Last updated on Dec 5, 2016, 3:28:05 AM.\n']
322+
323+
276324
@pytest.mark.parametrize('parallel', [False, True])
277325
def test_error(tmpdir, config, local_docs, urls, parallel):
278326
"""Test with a bad root ref. Also test skipping bad non-root refs.

tox.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ passenv =
5757
SSH_AUTH_SOCK
5858
TRAVIS*
5959
USER
60+
usedevelop = False
6061

6162
[flake8]
6263
exclude = .tox/*,build/*,docs/*,env/*,get-pip.py

0 commit comments

Comments
 (0)