-
Notifications
You must be signed in to change notification settings - Fork 34
/
distribute_regen
executable file
·202 lines (170 loc) · 7.08 KB
/
distribute_regen
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
#!/usr/bin/env python2
# -*- coding: utf-8 -*-
from __future__ import print_function
import itertools as it, operator as op, functools as ft
from subprocess import check_call, Popen, PIPE
from os.path import join, exists, abspath, expanduser, dirname, basename
from io import open
import os, sys
import argparse
parser = argparse.ArgumentParser(
description='Auto-update python package build system.'
' Updates "version" in setup.py,'
' produces README.txt, checks *.py files for syntax errors.'
' Special checks are made to check if the tool is run from git pre-commit hook.')
parser.add_argument('--git-dir',
help='Project root directory, assumed to be "." by default.')
parser.add_argument('--setup',
help='Path to "setup.py" file to regenerate (default: $GIT_DIR/setup.py, if exists).')
optz = parser.parse_args()
if optz.git_dir: os.chdir(optz.git_dir)
if not optz.setup and exists('setup.py'): optz.setup = 'setup.py'
conf_dir = expanduser('~/.config/distribute_regen')
conf_overrides = join(conf_dir, 'overrides')
### Auto-detect which actvities to perform
regen_git = basename(sys.argv[0]) == 'pre-commit'
if regen_git: # break immediately if there's no changes
if not Popen(['git', 'diff', 'HEAD', '--quiet'], env=dict()).wait(): sys.exit()
regen_setup = bool(optz.setup)
regen_docs = dict(
readme=exists('README.md')
and not any(map(exists, ['README', 'README.rst']))
and exists('README.txt'),
readme_rst=exists('README.rst'),
doc=exists('doc/Makefile') )
### Do the stuff
dirty = False # whether git-commit should be aborted
## Check for lame syntax errors
import py_compile
for src in ( line.strip() for line in
Popen(['find', '-name', '*.py', '-print'], stdout=PIPE).stdout ):
if src.endswith('.tpl.py'): continue # templates are, by definition, incomplete
if regen_setup:
## Update version in setup.py
from datetime import datetime, timedelta
ts = datetime.now().replace( day=1, hour=0,
minute=0, second=0, microsecond=0 ) - timedelta(0, 1)
ver_minor = len(list(Popen([ 'git', 'rev-list',
'--since={}'.format(ts.strftime('%Y-%m-%dT%H:%M:%S')), 'master' ], stdout=PIPE).stdout))
version = datetime.now()
version = '{}.{}.{}'.format(version.year % 100, version.month, ver_minor)
from tempfile import NamedTemporaryFile
import re, ast
ast_body = lambda src: ast.parse('\n'.join(
line for line in src if not line.strip().startswith('#') )).body
with open(optz.setup, 'r+b') as src:
# Find setup(...) call in module context
for line in reversed(ast_body(src)):
if isinstance(line, ast.Expr):
try:
if line.value.func.id == 'setup': break
except AttributeError: continue
else: raise KeyError('Failed to find "setup(...)" call in AST')
for kw in line.value.keywords:
if kw.arg == 'version':
try: version_orig = kw.value.s
except AttributeError:
try: # check if it's properly imported from somewhere as __version__
if (hasattr(kw.value, 'attr') and kw.value.attr == '__version__')\
or '__version__' in kw.value.id: version_orig = False
else: raise AttributeError(kw.value.id)
except AttributeError: version_orig = None
break
else:
raise KeyError('Failed to find "version=" keyword in setup(...) call')
# Handle case when version is stored as __version__ inside a module
if version_orig is False:
from glob import iglob
for mod_init in iglob(join(dirname(optz.setup), '*', '__init__.py')):
with open(mod_init) as src:
for line in reversed(ast_body(src)):
if isinstance(line, ast.Assign)\
and '__version__' in (getattr(t, 'id', None) for t in line.targets):
version_orig = line.value.s
break
version_re = re.compile( r'(?P<pre>__version__\s*=\s*u?[\'"]+)'\
+ re.escape(repr(version_orig).strip('\'"')) + r'(?P<post>[\'"]+)' )
src = open(mod_init)
elif version_orig:
version_re = re.compile( r'(?P<pre>\bversion\s*=\s*u?[\'"]+)'\
+ re.escape(repr(version_orig).strip('\'"')) + r'(?P<post>[\'"]+)' )
if version_orig:
src.seek(0)
with NamedTemporaryFile(
dir=dirname(src.name),
prefix=basename(src.name + '.'), delete=False ) as tmp:
try:
verson_updated = False
for line in src:
line_org = line
line, changes = version_re.subn(r'\g<pre>{}\g<post>'.format(version), line)
if changes:
if verson_updated:
raise KeyError('Multiple lines matching "version=" regex')
else: verson_updated = True
tmp.write(line)
tmp.flush()
# Check if there's any diff in contents as a result
from hashlib import sha1
src.seek(0), tmp.seek(0)
if sha1(src.read()).hexdigest() != sha1(tmp.read()).hexdigest():
dirty = True
os.rename(tmp.name, src.name)
finally:
try: os.unlink(tmp.name)
except (OSError, IOError): pass
src.close() # in case it was substituted with something else
if any(regen_docs.values()):
if regen_docs.get('readme'):
## Regenerate README.txt (ReST) from README.md (Markdown)
from tempfile import NamedTemporaryFile
with NamedTemporaryFile(dir='.', prefix='README.txt.', delete=False) as tmp:
try:
if Popen( ['pandoc', '-f', 'markdown', '-t', 'rst', 'README.md'],
stdout=tmp, env=dict(PATH=os.environ['PATH']) ).wait():
raise RuntimeError('pandoc process exited with error')
os.rename(tmp.name, 'README.txt')
except Exception as err:
try: os.unlink(tmp.name)
except (OSError, IOError): pass
raise
proc_desc = Popen([ 'python2', 'setup.py',
'--long-description' ], stdout=PIPE, env=dict(PATH=os.environ['PATH']))
if Popen( ['rst2html.py', '--strict'], stdin=proc_desc.stdout,
stdout=open(os.devnull, 'w') ).wait() or proc_desc.wait():
raise RuntimeError('Failed to validate produced README.txt')
if regen_docs.get('readme') or regen_docs.get('readme_rst'):
proc = Popen(['rst2html.py', '--strict', 'README.rst'], stdout=open(os.devnull, 'w'))
if proc.wait(): raise RuntimeError('Failed to validate README.rst')
if regen_docs.get('doc'):
## Rebuild "doc" path
# Optional patch for Makefile will be applied from conf_dir
# XXX: maybe some more generic hook will be better here
patch = abspath('doc/Makefile')\
.lstrip(os.sep).replace('-', '--').replace(os.sep, '-') + '.patch'
if not exists(patch): patch = None
else: check_call(['cp', '-a', 'doc/Makefile', 'doc/Makefile.tmp-bak'])
try:
if patch:
patch_args = ['--batch', '-p1', 'doc/Makefile', patch]
check_call(['patch', '--dry-run'] + patch_args)
check_call(['patch'] + patch_args)
if Popen(['make'], cwd='doc', stdout=open(os.devnull, 'w')).wait():
raise RuntimeError('Failed to rebuild stuff in "doc" dir')
finally:
if patch: check_call(['mv', 'doc/Makefile.tmp-bak', 'doc/Makefile'])
if regen_git:
## Check if no changes were made and ack the commit
if not dirty: sys.exit(0)
# Add new files to index asap
if not os.fork():
os.setsid()
from time import sleep
while True:
if exists('.git/index.lock'): sleep(0.1)
else: break
Popen(['git', 'add', 'setup.py'], env=dict()).wait()
sys.exit()
# Abort the current commit due to changes made
print('setup.py regenerated, re-initiate commit process manually', file=sys.stderr)
sys.exit(1)