##// END OF EJS Templates
*: kill all unnecessary shebangs.
Dan Villiom Podlaski Christiansen -
r12865:4c50552f stable
parent child Browse files
Show More
@@ -1,374 +1,373 b''
1 #!/usr/bin/env python
2 1 #
3 2 # This is an experimental py3k-enabled mercurial setup script.
4 3 #
5 4 # 'python setup.py install', or
6 5 # 'python setup.py --help' for more options
7 6
8 7 from distutils.command.build_py import build_py_2to3
9 8 from lib2to3.refactor import get_fixers_from_package as getfixers
10 9
11 10 import sys
12 11 if not hasattr(sys, 'version_info') or sys.version_info < (2, 4, 0, 'final'):
13 12 raise SystemExit("Mercurial requires Python 2.4 or later.")
14 13
15 14 if sys.version_info[0] >= 3:
16 15 def b(s):
17 16 '''A helper function to emulate 2.6+ bytes literals using string
18 17 literals.'''
19 18 return s.encode('latin1')
20 19 else:
21 20 def b(s):
22 21 '''A helper function to emulate 2.6+ bytes literals using string
23 22 literals.'''
24 23 return s
25 24
26 25 # Solaris Python packaging brain damage
27 26 try:
28 27 import hashlib
29 28 sha = hashlib.sha1()
30 29 except:
31 30 try:
32 31 import sha
33 32 except:
34 33 raise SystemExit(
35 34 "Couldn't import standard hashlib (incomplete Python install).")
36 35
37 36 try:
38 37 import zlib
39 38 except:
40 39 raise SystemExit(
41 40 "Couldn't import standard zlib (incomplete Python install).")
42 41
43 42 try:
44 43 import bz2
45 44 except:
46 45 raise SystemExit(
47 46 "Couldn't import standard bz2 (incomplete Python install).")
48 47
49 48 import os, subprocess, time
50 49 import shutil
51 50 import tempfile
52 51 from distutils import log
53 52 from distutils.core import setup, Extension
54 53 from distutils.dist import Distribution
55 54 from distutils.command.build import build
56 55 from distutils.command.build_ext import build_ext
57 56 from distutils.command.build_py import build_py
58 57 from distutils.spawn import spawn, find_executable
59 58 from distutils.ccompiler import new_compiler
60 59 from distutils.errors import CCompilerError
61 60
62 61 scripts = ['hg']
63 62 if os.name == 'nt':
64 63 scripts.append('contrib/win32/hg.bat')
65 64
66 65 # simplified version of distutils.ccompiler.CCompiler.has_function
67 66 # that actually removes its temporary files.
68 67 def hasfunction(cc, funcname):
69 68 tmpdir = tempfile.mkdtemp(prefix='hg-install-')
70 69 devnull = oldstderr = None
71 70 try:
72 71 try:
73 72 fname = os.path.join(tmpdir, 'funcname.c')
74 73 f = open(fname, 'w')
75 74 f.write('int main(void) {\n')
76 75 f.write(' %s();\n' % funcname)
77 76 f.write('}\n')
78 77 f.close()
79 78 # Redirect stderr to /dev/null to hide any error messages
80 79 # from the compiler.
81 80 # This will have to be changed if we ever have to check
82 81 # for a function on Windows.
83 82 devnull = open('/dev/null', 'w')
84 83 oldstderr = os.dup(sys.stderr.fileno())
85 84 os.dup2(devnull.fileno(), sys.stderr.fileno())
86 85 objects = cc.compile([fname], output_dir=tmpdir)
87 86 cc.link_executable(objects, os.path.join(tmpdir, "a.out"))
88 87 except:
89 88 return False
90 89 return True
91 90 finally:
92 91 if oldstderr is not None:
93 92 os.dup2(oldstderr, sys.stderr.fileno())
94 93 if devnull is not None:
95 94 devnull.close()
96 95 shutil.rmtree(tmpdir)
97 96
98 97 # py2exe needs to be installed to work
99 98 try:
100 99 import py2exe
101 100 py2exeloaded = True
102 101
103 102 # Help py2exe to find win32com.shell
104 103 try:
105 104 import modulefinder
106 105 import win32com
107 106 for p in win32com.__path__[1:]: # Take the path to win32comext
108 107 modulefinder.AddPackagePath("win32com", p)
109 108 pn = "win32com.shell"
110 109 __import__(pn)
111 110 m = sys.modules[pn]
112 111 for p in m.__path__[1:]:
113 112 modulefinder.AddPackagePath(pn, p)
114 113 except ImportError:
115 114 pass
116 115
117 116 except ImportError:
118 117 py2exeloaded = False
119 118 pass
120 119
121 120 def runcmd(cmd, env):
122 121 p = subprocess.Popen(cmd, stdout=subprocess.PIPE,
123 122 stderr=subprocess.PIPE, env=env)
124 123 out, err = p.communicate()
125 124 # If root is executing setup.py, but the repository is owned by
126 125 # another user (as in "sudo python setup.py install") we will get
127 126 # trust warnings since the .hg/hgrc file is untrusted. That is
128 127 # fine, we don't want to load it anyway. Python may warn about
129 128 # a missing __init__.py in mercurial/locale, we also ignore that.
130 129 err = [e for e in err.splitlines()
131 130 if not e.startswith(b('Not trusting file')) \
132 131 and not e.startswith(b('warning: Not importing'))]
133 132 if err:
134 133 return ''
135 134 return out
136 135
137 136 version = ''
138 137
139 138 if os.path.isdir('.hg'):
140 139 # Execute hg out of this directory with a custom environment which
141 140 # includes the pure Python modules in mercurial/pure. We also take
142 141 # care to not use any hgrc files and do no localization.
143 142 pypath = ['mercurial', os.path.join('mercurial', 'pure')]
144 143 env = {'PYTHONPATH': os.pathsep.join(pypath),
145 144 'HGRCPATH': '',
146 145 'LANGUAGE': 'C'}
147 146 if 'LD_LIBRARY_PATH' in os.environ:
148 147 env['LD_LIBRARY_PATH'] = os.environ['LD_LIBRARY_PATH']
149 148 if 'SystemRoot' in os.environ:
150 149 # Copy SystemRoot into the custom environment for Python 2.6
151 150 # under Windows. Otherwise, the subprocess will fail with
152 151 # error 0xc0150004. See: http://bugs.python.org/issue3440
153 152 env['SystemRoot'] = os.environ['SystemRoot']
154 153 cmd = [sys.executable, 'hg', 'id', '-i', '-t']
155 154 l = runcmd(cmd, env).split()
156 155 while len(l) > 1 and l[-1][0].isalpha(): # remove non-numbered tags
157 156 l.pop()
158 157 if len(l) > 1: # tag found
159 158 version = l[-1]
160 159 if l[0].endswith('+'): # propagate the dirty status to the tag
161 160 version += '+'
162 161 elif len(l) == 1: # no tag found
163 162 cmd = [sys.executable, 'hg', 'parents', '--template',
164 163 '{latesttag}+{latesttagdistance}-']
165 164 version = runcmd(cmd, env) + l[0]
166 165 if version.endswith('+'):
167 166 version += time.strftime('%Y%m%d')
168 167 elif os.path.exists('.hg_archival.txt'):
169 168 kw = dict([[t.strip() for t in l.split(':', 1)]
170 169 for l in open('.hg_archival.txt')])
171 170 if 'tag' in kw:
172 171 version = kw['tag']
173 172 elif 'latesttag' in kw:
174 173 version = '%(latesttag)s+%(latesttagdistance)s-%(node).12s' % kw
175 174 else:
176 175 version = kw.get('node', '')[:12]
177 176
178 177 if version:
179 178 f = open("mercurial/__version__.py", "w")
180 179 f.write('# this file is autogenerated by setup.py\n')
181 180 f.write('version = "%s"\n' % version)
182 181 f.close()
183 182
184 183
185 184 try:
186 185 from mercurial import __version__
187 186 version = __version__.version
188 187 except ImportError:
189 188 version = 'unknown'
190 189
191 190 class hgbuildmo(build):
192 191
193 192 description = "build translations (.mo files)"
194 193
195 194 def run(self):
196 195 if not find_executable('msgfmt'):
197 196 self.warn("could not find msgfmt executable, no translations "
198 197 "will be built")
199 198 return
200 199
201 200 podir = 'i18n'
202 201 if not os.path.isdir(podir):
203 202 self.warn("could not find %s/ directory" % podir)
204 203 return
205 204
206 205 join = os.path.join
207 206 for po in os.listdir(podir):
208 207 if not po.endswith('.po'):
209 208 continue
210 209 pofile = join(podir, po)
211 210 modir = join('locale', po[:-3], 'LC_MESSAGES')
212 211 mofile = join(modir, 'hg.mo')
213 212 mobuildfile = join('mercurial', mofile)
214 213 cmd = ['msgfmt', '-v', '-o', mobuildfile, pofile]
215 214 if sys.platform != 'sunos5':
216 215 # msgfmt on Solaris does not know about -c
217 216 cmd.append('-c')
218 217 self.mkpath(join('mercurial', modir))
219 218 self.make_file([pofile], mobuildfile, spawn, (cmd,))
220 219
221 220 # Insert hgbuildmo first so that files in mercurial/locale/ are found
222 221 # when build_py is run next.
223 222 build.sub_commands.insert(0, ('build_mo', None))
224 223 # We also need build_ext before build_py. Otherwise, when 2to3 is called (in
225 224 # build_py), it will not find osutil & friends, thinking that those modules are
226 225 # global and, consequently, making a mess, now that all module imports are
227 226 # global.
228 227 build.sub_commands.insert(1, ('build_ext', None))
229 228
230 229 Distribution.pure = 0
231 230 Distribution.global_options.append(('pure', None, "use pure (slow) Python "
232 231 "code instead of C extensions"))
233 232
234 233 class hgbuildext(build_ext):
235 234
236 235 def build_extension(self, ext):
237 236 try:
238 237 build_ext.build_extension(self, ext)
239 238 except CCompilerError:
240 239 if not hasattr(ext, 'optional') or not ext.optional:
241 240 raise
242 241 log.warn("Failed to build optional extension '%s' (skipping)",
243 242 ext.name)
244 243
245 244 class hgbuildpy(build_py_2to3):
246 245 fixer_names = sorted(set(getfixers("lib2to3.fixes") +
247 246 getfixers("hgfixes")))
248 247
249 248 def finalize_options(self):
250 249 build_py.finalize_options(self)
251 250
252 251 if self.distribution.pure:
253 252 if self.py_modules is None:
254 253 self.py_modules = []
255 254 for ext in self.distribution.ext_modules:
256 255 if ext.name.startswith("mercurial."):
257 256 self.py_modules.append("mercurial.pure.%s" % ext.name[10:])
258 257 self.distribution.ext_modules = []
259 258
260 259 def find_modules(self):
261 260 modules = build_py.find_modules(self)
262 261 for module in modules:
263 262 if module[0] == "mercurial.pure":
264 263 if module[1] != "__init__":
265 264 yield ("mercurial", module[1], module[2])
266 265 else:
267 266 yield module
268 267
269 268 def run(self):
270 269 # In the build_py_2to3 class, self.updated_files = [], but I couldn't
271 270 # see when that variable was updated to point to the updated files, as
272 271 # its names suggests. Thus, I decided to just find_all_modules and feed
273 272 # them to 2to3. Unfortunately, subsequent calls to setup3k.py will
274 273 # incur in 2to3 analysis overhead.
275 274 self.updated_files = [i[2] for i in self.find_all_modules()]
276 275
277 276 # Base class code
278 277 if self.py_modules:
279 278 self.build_modules()
280 279 if self.packages:
281 280 self.build_packages()
282 281 self.build_package_data()
283 282
284 283 # 2to3
285 284 self.run_2to3(self.updated_files)
286 285
287 286 # Remaining base class code
288 287 self.byte_compile(self.get_outputs(include_bytecode=0))
289 288
290 289 cmdclass = {'build_mo': hgbuildmo,
291 290 'build_ext': hgbuildext,
292 291 'build_py': hgbuildpy}
293 292
294 293 packages = ['mercurial', 'mercurial.hgweb', 'hgext', 'hgext.convert',
295 294 'hgext.highlight', 'hgext.zeroconf']
296 295
297 296 pymodules = []
298 297
299 298 extmodules = [
300 299 Extension('mercurial.base85', ['mercurial/base85.c']),
301 300 Extension('mercurial.bdiff', ['mercurial/bdiff.c']),
302 301 Extension('mercurial.diffhelpers', ['mercurial/diffhelpers.c']),
303 302 Extension('mercurial.mpatch', ['mercurial/mpatch.c']),
304 303 Extension('mercurial.parsers', ['mercurial/parsers.c']),
305 304 ]
306 305
307 306 # disable osutil.c under windows + python 2.4 (issue1364)
308 307 if sys.platform == 'win32' and sys.version_info < (2, 5, 0, 'final'):
309 308 pymodules.append('mercurial.pure.osutil')
310 309 else:
311 310 extmodules.append(Extension('mercurial.osutil', ['mercurial/osutil.c']))
312 311
313 312 if sys.platform == 'linux2' and os.uname()[2] > '2.6':
314 313 # The inotify extension is only usable with Linux 2.6 kernels.
315 314 # You also need a reasonably recent C library.
316 315 # In any case, if it fails to build the error will be skipped ('optional').
317 316 cc = new_compiler()
318 317 if hasfunction(cc, 'inotify_add_watch'):
319 318 inotify = Extension('hgext.inotify.linux._inotify',
320 319 ['hgext/inotify/linux/_inotify.c'],
321 320 ['mercurial'])
322 321 inotify.optional = True
323 322 extmodules.append(inotify)
324 323 packages.extend(['hgext.inotify', 'hgext.inotify.linux'])
325 324
326 325 packagedata = {'mercurial': ['locale/*/LC_MESSAGES/hg.mo',
327 326 'help/*.txt']}
328 327
329 328 def ordinarypath(p):
330 329 return p and p[0] != '.' and p[-1] != '~'
331 330
332 331 for root in ('templates',):
333 332 for curdir, dirs, files in os.walk(os.path.join('mercurial', root)):
334 333 curdir = curdir.split(os.sep, 1)[1]
335 334 dirs[:] = filter(ordinarypath, dirs)
336 335 for f in filter(ordinarypath, files):
337 336 f = os.path.join(curdir, f)
338 337 packagedata['mercurial'].append(f)
339 338
340 339 datafiles = []
341 340 setupversion = version
342 341 extra = {}
343 342
344 343 if py2exeloaded:
345 344 extra['console'] = [
346 345 {'script':'hg',
347 346 'copyright':'Copyright (C) 2005-2010 Matt Mackall and others',
348 347 'product_version':version}]
349 348
350 349 if os.name == 'nt':
351 350 # Windows binary file versions for exe/dll files must have the
352 351 # form W.X.Y.Z, where W,X,Y,Z are numbers in the range 0..65535
353 352 setupversion = version.split('+', 1)[0]
354 353
355 354 setup(name='mercurial',
356 355 version=setupversion,
357 356 author='Matt Mackall',
358 357 author_email='mpm@selenic.com',
359 358 url='http://mercurial.selenic.com/',
360 359 description='Scalable distributed SCM',
361 360 license='GNU GPLv2+',
362 361 scripts=scripts,
363 362 packages=packages,
364 363 py_modules=pymodules,
365 364 ext_modules=extmodules,
366 365 data_files=datafiles,
367 366 package_data=packagedata,
368 367 cmdclass=cmdclass,
369 368 options=dict(py2exe=dict(packages=['hgext', 'email']),
370 369 bdist_mpkg=dict(zipdist=True,
371 370 license='COPYING',
372 371 readme='contrib/macosx/Readme.html',
373 372 welcome='contrib/macosx/Welcome.html')),
374 373 **extra)
@@ -1,289 +1,287 b''
1 #!/usr/bin/env python
2
3 1 """\
4 2 reorder a revlog (the manifest by default) to save space
5 3
6 4 Specifically, this topologically sorts the revisions in the revlog so that
7 5 revisions on the same branch are adjacent as much as possible. This is a
8 6 workaround for the fact that Mercurial computes deltas relative to the
9 7 previous revision rather than relative to a parent revision.
10 8
11 9 This is *not* safe to run on a changelog.
12 10 """
13 11
14 12 # Originally written by Benoit Boissinot <benoit.boissinot at ens-lyon.org>
15 13 # as a patch to rewrite-log. Cleaned up, refactored, documented, and
16 14 # renamed by Greg Ward <greg at gerg.ca>.
17 15
18 16 # XXX would be nice to have a way to verify the repository after shrinking,
19 17 # e.g. by comparing "before" and "after" states of random changesets
20 18 # (maybe: export before, shrink, export after, diff).
21 19
22 20 import os, tempfile, errno
23 21 from mercurial import revlog, transaction, node, util
24 22 from mercurial import changegroup
25 23 from mercurial.i18n import _
26 24
27 25
28 26 def postorder(start, edges):
29 27 result = []
30 28 visit = list(start)
31 29 finished = set()
32 30
33 31 while visit:
34 32 cur = visit[-1]
35 33 for p in edges[cur]:
36 34 if p not in finished:
37 35 visit.append(p)
38 36 break
39 37 else:
40 38 result.append(cur)
41 39 finished.add(cur)
42 40 visit.pop()
43 41
44 42 return result
45 43
46 44 def toposort_reversepostorder(ui, rl):
47 45 # postorder of the reverse directed graph
48 46
49 47 # map rev to list of parent revs (p2 first)
50 48 parents = {}
51 49 heads = set()
52 50 ui.status(_('reading revs\n'))
53 51 try:
54 52 for rev in rl:
55 53 ui.progress(_('reading'), rev, total=len(rl))
56 54 (p1, p2) = rl.parentrevs(rev)
57 55 if p1 == p2 == node.nullrev:
58 56 parents[rev] = () # root node
59 57 elif p1 == p2 or p2 == node.nullrev:
60 58 parents[rev] = (p1,) # normal node
61 59 else:
62 60 parents[rev] = (p2, p1) # merge node
63 61 heads.add(rev)
64 62 for p in parents[rev]:
65 63 heads.discard(p)
66 64 finally:
67 65 ui.progress(_('reading'), None)
68 66
69 67 heads = list(heads)
70 68 heads.sort(reverse=True)
71 69
72 70 ui.status(_('sorting revs\n'))
73 71 return postorder(heads, parents)
74 72
75 73 def toposort_postorderreverse(ui, rl):
76 74 # reverse-postorder of the reverse directed graph
77 75
78 76 children = {}
79 77 roots = set()
80 78 ui.status(_('reading revs\n'))
81 79 try:
82 80 for rev in rl:
83 81 ui.progress(_('reading'), rev, total=len(rl))
84 82 (p1, p2) = rl.parentrevs(rev)
85 83 if p1 == p2 == node.nullrev:
86 84 roots.add(rev)
87 85 children[rev] = []
88 86 if p1 != node.nullrev:
89 87 children[p1].append(rev)
90 88 if p2 != node.nullrev:
91 89 children[p2].append(rev)
92 90 finally:
93 91 ui.progress(_('reading'), None)
94 92
95 93 roots = list(roots)
96 94 roots.sort()
97 95
98 96 ui.status(_('sorting revs\n'))
99 97 result = postorder(roots, children)
100 98 result.reverse()
101 99 return result
102 100
103 101 def writerevs(ui, r1, r2, order, tr):
104 102
105 103 ui.status(_('writing revs\n'))
106 104
107 105 count = [0]
108 106 def progress(*args):
109 107 ui.progress(_('writing'), count[0], total=len(order))
110 108 count[0] += 1
111 109
112 110 order = [r1.node(r) for r in order]
113 111
114 112 # this is a bit ugly, but it works
115 113 lookup = lambda x: "%020d" % r1.linkrev(r1.rev(x))
116 114 unlookup = lambda x: int(x, 10)
117 115
118 116 try:
119 117 group = util.chunkbuffer(r1.group(order, lookup, progress))
120 118 group = changegroup.unbundle10(group, "UN")
121 119 r2.addgroup(group, unlookup, tr)
122 120 finally:
123 121 ui.progress(_('writing'), None)
124 122
125 123 def report(ui, r1, r2):
126 124 def getsize(r):
127 125 s = 0
128 126 for fn in (r.indexfile, r.datafile):
129 127 try:
130 128 s += os.stat(fn).st_size
131 129 except OSError, inst:
132 130 if inst.errno != errno.ENOENT:
133 131 raise
134 132 return s
135 133
136 134 oldsize = float(getsize(r1))
137 135 newsize = float(getsize(r2))
138 136
139 137 # argh: have to pass an int to %d, because a float >= 2^32
140 138 # blows up under Python 2.5 or earlier
141 139 ui.write(_('old file size: %12d bytes (%6.1f MiB)\n')
142 140 % (int(oldsize), oldsize / 1024 / 1024))
143 141 ui.write(_('new file size: %12d bytes (%6.1f MiB)\n')
144 142 % (int(newsize), newsize / 1024 / 1024))
145 143
146 144 shrink_percent = (oldsize - newsize) / oldsize * 100
147 145 shrink_factor = oldsize / newsize
148 146 ui.write(_('shrinkage: %.1f%% (%.1fx)\n')
149 147 % (shrink_percent, shrink_factor))
150 148
151 149 def shrink(ui, repo, **opts):
152 150 """shrink a revlog by reordering revisions
153 151
154 152 Rewrites all the entries in some revlog of the current repository
155 153 (by default, the manifest log) to save space.
156 154
157 155 Different sort algorithms have different performance
158 156 characteristics. Use ``--sort`` to select a sort algorithm so you
159 157 can determine which works best for your data.
160 158 """
161 159
162 160 if not repo.local():
163 161 raise util.Abort(_('not a local repository: %s') % repo.root)
164 162
165 163 fn = opts.get('revlog')
166 164 if not fn:
167 165 indexfn = repo.sjoin('00manifest.i')
168 166 else:
169 167 if not fn.endswith('.i'):
170 168 raise util.Abort(_('--revlog option must specify the revlog index '
171 169 'file (*.i), not %s') % opts.get('revlog'))
172 170
173 171 indexfn = os.path.realpath(fn)
174 172 store = repo.sjoin('')
175 173 if not indexfn.startswith(store):
176 174 raise util.Abort(_('--revlog option must specify a revlog in %s, '
177 175 'not %s') % (store, indexfn))
178 176
179 177 sortname = opts['sort']
180 178 try:
181 179 toposort = globals()['toposort_' + sortname]
182 180 except KeyError:
183 181 raise util.Abort(_('no such toposort algorithm: %s') % sortname)
184 182
185 183 if not os.path.exists(indexfn):
186 184 raise util.Abort(_('no such file: %s') % indexfn)
187 185 if '00changelog' in indexfn:
188 186 raise util.Abort(_('shrinking the changelog '
189 187 'will corrupt your repository'))
190 188
191 189 ui.write(_('shrinking %s\n') % indexfn)
192 190 prefix = os.path.basename(indexfn)[:-1]
193 191 tmpindexfn = util.mktempcopy(indexfn, emptyok=True)
194 192
195 193 r1 = revlog.revlog(util.opener(os.getcwd(), audit=False), indexfn)
196 194 r2 = revlog.revlog(util.opener(os.getcwd(), audit=False), tmpindexfn)
197 195
198 196 datafn, tmpdatafn = r1.datafile, r2.datafile
199 197
200 198 oldindexfn = indexfn + '.old'
201 199 olddatafn = datafn + '.old'
202 200 if os.path.exists(oldindexfn) or os.path.exists(olddatafn):
203 201 raise util.Abort(_('one or both of\n'
204 202 ' %s\n'
205 203 ' %s\n'
206 204 'exists from a previous run; please clean up '
207 205 'before running again') % (oldindexfn, olddatafn))
208 206
209 207 # Don't use repo.transaction(), because then things get hairy with
210 208 # paths: some need to be relative to .hg, and some need to be
211 209 # absolute. Doing it this way keeps things simple: everything is an
212 210 # absolute path.
213 211 lock = repo.lock(wait=False)
214 212 tr = transaction.transaction(ui.warn,
215 213 open,
216 214 repo.sjoin('journal'))
217 215
218 216 def ignoremissing(func):
219 217 def f(*args, **kw):
220 218 try:
221 219 return func(*args, **kw)
222 220 except OSError, inst:
223 221 if inst.errno != errno.ENOENT:
224 222 raise
225 223 return f
226 224
227 225 try:
228 226 try:
229 227 order = toposort(ui, r1)
230 228
231 229 suboptimal = 0
232 230 for i in xrange(1, len(order)):
233 231 parents = [p for p in r1.parentrevs(order[i])
234 232 if p != node.nullrev]
235 233 if parents and order[i - 1] not in parents:
236 234 suboptimal += 1
237 235 ui.note(_('%d suboptimal nodes\n') % suboptimal)
238 236
239 237 writerevs(ui, r1, r2, order, tr)
240 238 report(ui, r1, r2)
241 239 tr.close()
242 240 except:
243 241 # Abort transaction first, so we truncate the files before
244 242 # deleting them.
245 243 tr.abort()
246 244 for fn in (tmpindexfn, tmpdatafn):
247 245 ignoremissing(os.unlink)(fn)
248 246 raise
249 247 if not opts.get('dry_run'):
250 248 # racy, both files cannot be renamed atomically
251 249 # copy files
252 250 util.os_link(indexfn, oldindexfn)
253 251 ignoremissing(util.os_link)(datafn, olddatafn)
254 252
255 253 # rename
256 254 util.rename(tmpindexfn, indexfn)
257 255 try:
258 256 os.chmod(tmpdatafn, os.stat(datafn).st_mode)
259 257 util.rename(tmpdatafn, datafn)
260 258 except OSError, inst:
261 259 if inst.errno != errno.ENOENT:
262 260 raise
263 261 ignoremissing(os.unlink)(datafn)
264 262 else:
265 263 for fn in (tmpindexfn, tmpdatafn):
266 264 ignoremissing(os.unlink)(fn)
267 265 finally:
268 266 lock.release()
269 267
270 268 if not opts.get('dry_run'):
271 269 ui.write(_('note: old revlog saved in:\n'
272 270 ' %s\n'
273 271 ' %s\n'
274 272 '(You can delete those files when you are satisfied that your\n'
275 273 'repository is still sane. '
276 274 'Running \'hg verify\' is strongly recommended.)\n')
277 275 % (oldindexfn, olddatafn))
278 276
279 277 cmdtable = {
280 278 'shrink': (shrink,
281 279 [('', 'revlog', '', _('index (.i) file of the revlog to shrink')),
282 280 ('n', 'dry-run', None, _('do not shrink, simulate only')),
283 281 ('', 'sort', 'reversepostorder', 'name of sort algorithm to use'),
284 282 ],
285 283 _('hg shrink [--revlog PATH]'))
286 284 }
287 285
288 286 if __name__ == "__main__":
289 287 print "shrink-revlog.py is now an extension (see hg help extensions)"
@@ -1,1102 +1,1101 b''
1 #!/usr/bin/env python
2 1 # -*- coding: utf-8 -*-
3 2 # $Id: manpage.py 6110 2009-08-31 14:40:33Z grubert $
4 3 # Author: Engelbert Gruber <grubert@users.sourceforge.net>
5 4 # Copyright: This module is put into the public domain.
6 5
7 6 """
8 7 Simple man page writer for reStructuredText.
9 8
10 9 Man pages (short for "manual pages") contain system documentation on unix-like
11 10 systems. The pages are grouped in numbered sections:
12 11
13 12 1 executable programs and shell commands
14 13 2 system calls
15 14 3 library functions
16 15 4 special files
17 16 5 file formats
18 17 6 games
19 18 7 miscellaneous
20 19 8 system administration
21 20
22 21 Man pages are written *troff*, a text file formatting system.
23 22
24 23 See http://www.tldp.org/HOWTO/Man-Page for a start.
25 24
26 25 Man pages have no subsection only parts.
27 26 Standard parts
28 27
29 28 NAME ,
30 29 SYNOPSIS ,
31 30 DESCRIPTION ,
32 31 OPTIONS ,
33 32 FILES ,
34 33 SEE ALSO ,
35 34 BUGS ,
36 35
37 36 and
38 37
39 38 AUTHOR .
40 39
41 40 A unix-like system keeps an index of the DESCRIPTIONs, which is accesable
42 41 by the command whatis or apropos.
43 42
44 43 """
45 44
46 45 __docformat__ = 'reStructuredText'
47 46
48 47 import re
49 48
50 49 from docutils import nodes, writers, languages
51 50 import roman
52 51
53 52 FIELD_LIST_INDENT = 7
54 53 DEFINITION_LIST_INDENT = 7
55 54 OPTION_LIST_INDENT = 7
56 55 BLOCKQOUTE_INDENT = 3.5
57 56
58 57 # Define two macros so man/roff can calculate the
59 58 # indent/unindent margins by itself
60 59 MACRO_DEF = (r""".
61 60 .nr rst2man-indent-level 0
62 61 .
63 62 .de1 rstReportMargin
64 63 \\$1 \\n[an-margin]
65 64 level \\n[rst2man-indent-level]
66 65 level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
67 66 -
68 67 \\n[rst2man-indent0]
69 68 \\n[rst2man-indent1]
70 69 \\n[rst2man-indent2]
71 70 ..
72 71 .de1 INDENT
73 72 .\" .rstReportMargin pre:
74 73 . RS \\$1
75 74 . nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin]
76 75 . nr rst2man-indent-level +1
77 76 .\" .rstReportMargin post:
78 77 ..
79 78 .de UNINDENT
80 79 . RE
81 80 .\" indent \\n[an-margin]
82 81 .\" old: \\n[rst2man-indent\\n[rst2man-indent-level]]
83 82 .nr rst2man-indent-level -1
84 83 .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
85 84 .in \\n[rst2man-indent\\n[rst2man-indent-level]]u
86 85 ..
87 86 """)
88 87
89 88 class Writer(writers.Writer):
90 89
91 90 supported = ('manpage')
92 91 """Formats this writer supports."""
93 92
94 93 output = None
95 94 """Final translated form of `document`."""
96 95
97 96 def __init__(self):
98 97 writers.Writer.__init__(self)
99 98 self.translator_class = Translator
100 99
101 100 def translate(self):
102 101 visitor = self.translator_class(self.document)
103 102 self.document.walkabout(visitor)
104 103 self.output = visitor.astext()
105 104
106 105
107 106 class Table:
108 107 def __init__(self):
109 108 self._rows = []
110 109 self._options = ['center']
111 110 self._tab_char = '\t'
112 111 self._coldefs = []
113 112 def new_row(self):
114 113 self._rows.append([])
115 114 def append_separator(self, separator):
116 115 """Append the separator for table head."""
117 116 self._rows.append([separator])
118 117 def append_cell(self, cell_lines):
119 118 """cell_lines is an array of lines"""
120 119 start = 0
121 120 if len(cell_lines) > 0 and cell_lines[0] == '.sp\n':
122 121 start = 1
123 122 self._rows[-1].append(cell_lines[start:])
124 123 if len(self._coldefs) < len(self._rows[-1]):
125 124 self._coldefs.append('l')
126 125 def _minimize_cell(self, cell_lines):
127 126 """Remove leading and trailing blank and ``.sp`` lines"""
128 127 while (cell_lines and cell_lines[0] in ('\n', '.sp\n')):
129 128 del cell_lines[0]
130 129 while (cell_lines and cell_lines[-1] in ('\n', '.sp\n')):
131 130 del cell_lines[-1]
132 131 def as_list(self):
133 132 text = ['.TS\n']
134 133 text.append(' '.join(self._options) + ';\n')
135 134 text.append('|%s|.\n' % ('|'.join(self._coldefs)))
136 135 for row in self._rows:
137 136 # row = array of cells. cell = array of lines.
138 137 text.append('_\n') # line above
139 138 text.append('T{\n')
140 139 for i in range(len(row)):
141 140 cell = row[i]
142 141 self._minimize_cell(cell)
143 142 text.extend(cell)
144 143 if not text[-1].endswith('\n'):
145 144 text[-1] += '\n'
146 145 if i < len(row)-1:
147 146 text.append('T}'+self._tab_char+'T{\n')
148 147 else:
149 148 text.append('T}\n')
150 149 text.append('_\n')
151 150 text.append('.TE\n')
152 151 return text
153 152
154 153 class Translator(nodes.NodeVisitor):
155 154 """"""
156 155
157 156 words_and_spaces = re.compile(r'\S+| +|\n')
158 157 document_start = """Man page generated from reStructeredText."""
159 158
160 159 def __init__(self, document):
161 160 nodes.NodeVisitor.__init__(self, document)
162 161 self.settings = settings = document.settings
163 162 lcode = settings.language_code
164 163 self.language = languages.get_language(lcode)
165 164 self.head = []
166 165 self.body = []
167 166 self.foot = []
168 167 self.section_level = 0
169 168 self.context = []
170 169 self.topic_class = ''
171 170 self.colspecs = []
172 171 self.compact_p = 1
173 172 self.compact_simple = None
174 173 # the list style "*" bullet or "#" numbered
175 174 self._list_char = []
176 175 # writing the header .TH and .SH NAME is postboned after
177 176 # docinfo.
178 177 self._docinfo = {
179 178 "title" : "", "title_upper": "",
180 179 "subtitle" : "",
181 180 "manual_section" : "", "manual_group" : "",
182 181 "author" : [],
183 182 "date" : "",
184 183 "copyright" : "",
185 184 "version" : "",
186 185 }
187 186 self._docinfo_keys = [] # a list to keep the sequence as in source.
188 187 self._docinfo_names = {} # to get name from text not normalized.
189 188 self._in_docinfo = None
190 189 self._active_table = None
191 190 self._in_literal = False
192 191 self.header_written = 0
193 192 self._line_block = 0
194 193 self.authors = []
195 194 self.section_level = 0
196 195 self._indent = [0]
197 196 # central definition of simple processing rules
198 197 # what to output on : visit, depart
199 198 # Do not use paragraph requests ``.PP`` because these set indentation.
200 199 # use ``.sp``. Remove superfluous ``.sp`` in ``astext``.
201 200 #
202 201 # Fonts are put on a stack, the top one is used.
203 202 # ``.ft P`` or ``\\fP`` pop from stack.
204 203 # ``B`` bold, ``I`` italic, ``R`` roman should be available.
205 204 # Hopefully ``C`` courier too.
206 205 self.defs = {
207 206 'indent' : ('.INDENT %.1f\n', '.UNINDENT\n'),
208 207 'definition_list_item' : ('.TP', ''),
209 208 'field_name' : ('.TP\n.B ', '\n'),
210 209 'literal' : ('\\fB', '\\fP'),
211 210 'literal_block' : ('.sp\n.nf\n.ft C\n', '\n.ft P\n.fi\n'),
212 211
213 212 'option_list_item' : ('.TP\n', ''),
214 213
215 214 'reference' : (r'\%', r'\:'),
216 215 'emphasis': ('\\fI', '\\fP'),
217 216 'strong' : ('\\fB', '\\fP'),
218 217 'term' : ('\n.B ', '\n'),
219 218 'title_reference' : ('\\fI', '\\fP'),
220 219
221 220 'topic-title' : ('.SS ',),
222 221 'sidebar-title' : ('.SS ',),
223 222
224 223 'problematic' : ('\n.nf\n', '\n.fi\n'),
225 224 }
226 225 # NOTE don't specify the newline before a dot-command, but ensure
227 226 # it is there.
228 227
229 228 def comment_begin(self, text):
230 229 """Return commented version of the passed text WITHOUT end of
231 230 line/comment."""
232 231 prefix = '.\\" '
233 232 out_text = ''.join(
234 233 [(prefix + in_line + '\n')
235 234 for in_line in text.split('\n')])
236 235 return out_text
237 236
238 237 def comment(self, text):
239 238 """Return commented version of the passed text."""
240 239 return self.comment_begin(text)+'.\n'
241 240
242 241 def ensure_eol(self):
243 242 """Ensure the last line in body is terminated by new line."""
244 243 if self.body[-1][-1] != '\n':
245 244 self.body.append('\n')
246 245
247 246 def astext(self):
248 247 """Return the final formatted document as a string."""
249 248 if not self.header_written:
250 249 # ensure we get a ".TH" as viewers require it.
251 250 self.head.append(self.header())
252 251 # filter body
253 252 for i in xrange(len(self.body)-1, 0, -1):
254 253 # remove superfluous vertical gaps.
255 254 if self.body[i] == '.sp\n':
256 255 if self.body[i - 1][:4] in ('.BI ','.IP '):
257 256 self.body[i] = '.\n'
258 257 elif (self.body[i - 1][:3] == '.B ' and
259 258 self.body[i - 2][:4] == '.TP\n'):
260 259 self.body[i] = '.\n'
261 260 elif (self.body[i - 1] == '\n' and
262 261 self.body[i - 2][0] != '.' and
263 262 (self.body[i - 3][:7] == '.TP\n.B '
264 263 or self.body[i - 3][:4] == '\n.B ')
265 264 ):
266 265 self.body[i] = '.\n'
267 266 return ''.join(self.head + self.body + self.foot)
268 267
269 268 def deunicode(self, text):
270 269 text = text.replace(u'\xa0', '\\ ')
271 270 text = text.replace(u'\u2020', '\\(dg')
272 271 return text
273 272
274 273 def visit_Text(self, node):
275 274 text = node.astext()
276 275 text = text.replace('\\','\\e')
277 276 replace_pairs = [
278 277 (u'-', ur'\-'),
279 278 (u'\'', ur'\(aq'),
280 279 (u'´', ur'\''),
281 280 (u'`', ur'\(ga'),
282 281 ]
283 282 for (in_char, out_markup) in replace_pairs:
284 283 text = text.replace(in_char, out_markup)
285 284 # unicode
286 285 text = self.deunicode(text)
287 286 if self._in_literal:
288 287 # prevent interpretation of "." at line start
289 288 if text[0] == '.':
290 289 text = '\\&' + text
291 290 text = text.replace('\n.', '\n\\&.')
292 291 self.body.append(text)
293 292
294 293 def depart_Text(self, node):
295 294 pass
296 295
297 296 def list_start(self, node):
298 297 class enum_char:
299 298 enum_style = {
300 299 'bullet' : '\\(bu',
301 300 'emdash' : '\\(em',
302 301 }
303 302
304 303 def __init__(self, style):
305 304 self._style = style
306 305 if 'start' in node:
307 306 self._cnt = node['start'] - 1
308 307 else:
309 308 self._cnt = 0
310 309 self._indent = 2
311 310 if style == 'arabic':
312 311 # indentation depends on number of childrens
313 312 # and start value.
314 313 self._indent = len(str(len(node.children)))
315 314 self._indent += len(str(self._cnt)) + 1
316 315 elif style == 'loweralpha':
317 316 self._cnt += ord('a') - 1
318 317 self._indent = 3
319 318 elif style == 'upperalpha':
320 319 self._cnt += ord('A') - 1
321 320 self._indent = 3
322 321 elif style.endswith('roman'):
323 322 self._indent = 5
324 323
325 324 def next(self):
326 325 if self._style == 'bullet':
327 326 return self.enum_style[self._style]
328 327 elif self._style == 'emdash':
329 328 return self.enum_style[self._style]
330 329 self._cnt += 1
331 330 # TODO add prefix postfix
332 331 if self._style == 'arabic':
333 332 return "%d." % self._cnt
334 333 elif self._style in ('loweralpha', 'upperalpha'):
335 334 return "%c." % self._cnt
336 335 elif self._style.endswith('roman'):
337 336 res = roman.toRoman(self._cnt) + '.'
338 337 if self._style.startswith('upper'):
339 338 return res.upper()
340 339 return res.lower()
341 340 else:
342 341 return "%d." % self._cnt
343 342 def get_width(self):
344 343 return self._indent
345 344 def __repr__(self):
346 345 return 'enum_style-%s' % list(self._style)
347 346
348 347 if 'enumtype' in node:
349 348 self._list_char.append(enum_char(node['enumtype']))
350 349 else:
351 350 self._list_char.append(enum_char('bullet'))
352 351 if len(self._list_char) > 1:
353 352 # indent nested lists
354 353 self.indent(self._list_char[-2].get_width())
355 354 else:
356 355 self.indent(self._list_char[-1].get_width())
357 356
358 357 def list_end(self):
359 358 self.dedent()
360 359 self._list_char.pop()
361 360
362 361 def header(self):
363 362 tmpl = (".TH %(title_upper)s %(manual_section)s"
364 363 " \"%(date)s\" \"%(version)s\" \"%(manual_group)s\"\n"
365 364 ".SH NAME\n"
366 365 "%(title)s \- %(subtitle)s\n")
367 366 return tmpl % self._docinfo
368 367
369 368 def append_header(self):
370 369 """append header with .TH and .SH NAME"""
371 370 # NOTE before everything
372 371 # .TH title_upper section date source manual
373 372 if self.header_written:
374 373 return
375 374 self.body.append(self.header())
376 375 self.body.append(MACRO_DEF)
377 376 self.header_written = 1
378 377
379 378 def visit_address(self, node):
380 379 self.visit_docinfo_item(node, 'address')
381 380
382 381 def depart_address(self, node):
383 382 pass
384 383
385 384 def visit_admonition(self, node, name=None):
386 385 if name:
387 386 self.body.append('.IP %s\n' %
388 387 self.language.labels.get(name, name))
389 388
390 389 def depart_admonition(self, node):
391 390 self.body.append('.RE\n')
392 391
393 392 def visit_attention(self, node):
394 393 self.visit_admonition(node, 'attention')
395 394
396 395 depart_attention = depart_admonition
397 396
398 397 def visit_docinfo_item(self, node, name):
399 398 if name == 'author':
400 399 self._docinfo[name].append(node.astext())
401 400 else:
402 401 self._docinfo[name] = node.astext()
403 402 self._docinfo_keys.append(name)
404 403 raise nodes.SkipNode
405 404
406 405 def depart_docinfo_item(self, node):
407 406 pass
408 407
409 408 def visit_author(self, node):
410 409 self.visit_docinfo_item(node, 'author')
411 410
412 411 depart_author = depart_docinfo_item
413 412
414 413 def visit_authors(self, node):
415 414 # _author is called anyway.
416 415 pass
417 416
418 417 def depart_authors(self, node):
419 418 pass
420 419
421 420 def visit_block_quote(self, node):
422 421 # BUG/HACK: indent alway uses the _last_ indention,
423 422 # thus we need two of them.
424 423 self.indent(BLOCKQOUTE_INDENT)
425 424 self.indent(0)
426 425
427 426 def depart_block_quote(self, node):
428 427 self.dedent()
429 428 self.dedent()
430 429
431 430 def visit_bullet_list(self, node):
432 431 self.list_start(node)
433 432
434 433 def depart_bullet_list(self, node):
435 434 self.list_end()
436 435
437 436 def visit_caption(self, node):
438 437 pass
439 438
440 439 def depart_caption(self, node):
441 440 pass
442 441
443 442 def visit_caution(self, node):
444 443 self.visit_admonition(node, 'caution')
445 444
446 445 depart_caution = depart_admonition
447 446
448 447 def visit_citation(self, node):
449 448 num, text = node.astext().split(None, 1)
450 449 num = num.strip()
451 450 self.body.append('.IP [%s] 5\n' % num)
452 451
453 452 def depart_citation(self, node):
454 453 pass
455 454
456 455 def visit_citation_reference(self, node):
457 456 self.body.append('['+node.astext()+']')
458 457 raise nodes.SkipNode
459 458
460 459 def visit_classifier(self, node):
461 460 pass
462 461
463 462 def depart_classifier(self, node):
464 463 pass
465 464
466 465 def visit_colspec(self, node):
467 466 self.colspecs.append(node)
468 467
469 468 def depart_colspec(self, node):
470 469 pass
471 470
472 471 def write_colspecs(self):
473 472 self.body.append("%s.\n" % ('L '*len(self.colspecs)))
474 473
475 474 def visit_comment(self, node,
476 475 sub=re.compile('-(?=-)').sub):
477 476 self.body.append(self.comment(node.astext()))
478 477 raise nodes.SkipNode
479 478
480 479 def visit_contact(self, node):
481 480 self.visit_docinfo_item(node, 'contact')
482 481
483 482 depart_contact = depart_docinfo_item
484 483
485 484 def visit_container(self, node):
486 485 pass
487 486
488 487 def depart_container(self, node):
489 488 pass
490 489
491 490 def visit_compound(self, node):
492 491 pass
493 492
494 493 def depart_compound(self, node):
495 494 pass
496 495
497 496 def visit_copyright(self, node):
498 497 self.visit_docinfo_item(node, 'copyright')
499 498
500 499 def visit_danger(self, node):
501 500 self.visit_admonition(node, 'danger')
502 501
503 502 depart_danger = depart_admonition
504 503
505 504 def visit_date(self, node):
506 505 self.visit_docinfo_item(node, 'date')
507 506
508 507 def visit_decoration(self, node):
509 508 pass
510 509
511 510 def depart_decoration(self, node):
512 511 pass
513 512
514 513 def visit_definition(self, node):
515 514 pass
516 515
517 516 def depart_definition(self, node):
518 517 pass
519 518
520 519 def visit_definition_list(self, node):
521 520 self.indent(DEFINITION_LIST_INDENT)
522 521
523 522 def depart_definition_list(self, node):
524 523 self.dedent()
525 524
526 525 def visit_definition_list_item(self, node):
527 526 self.body.append(self.defs['definition_list_item'][0])
528 527
529 528 def depart_definition_list_item(self, node):
530 529 self.body.append(self.defs['definition_list_item'][1])
531 530
532 531 def visit_description(self, node):
533 532 pass
534 533
535 534 def depart_description(self, node):
536 535 pass
537 536
538 537 def visit_docinfo(self, node):
539 538 self._in_docinfo = 1
540 539
541 540 def depart_docinfo(self, node):
542 541 self._in_docinfo = None
543 542 # NOTE nothing should be written before this
544 543 self.append_header()
545 544
546 545 def visit_doctest_block(self, node):
547 546 self.body.append(self.defs['literal_block'][0])
548 547 self._in_literal = True
549 548
550 549 def depart_doctest_block(self, node):
551 550 self._in_literal = False
552 551 self.body.append(self.defs['literal_block'][1])
553 552
554 553 def visit_document(self, node):
555 554 # no blank line between comment and header.
556 555 self.body.append(self.comment(self.document_start).rstrip()+'\n')
557 556 # writing header is postboned
558 557 self.header_written = 0
559 558
560 559 def depart_document(self, node):
561 560 if self._docinfo['author']:
562 561 self.body.append('.SH AUTHOR\n%s\n'
563 562 % ', '.join(self._docinfo['author']))
564 563 skip = ('author', 'copyright', 'date',
565 564 'manual_group', 'manual_section',
566 565 'subtitle',
567 566 'title', 'title_upper', 'version')
568 567 for name in self._docinfo_keys:
569 568 if name == 'address':
570 569 self.body.append("\n%s:\n%s%s.nf\n%s\n.fi\n%s%s" % (
571 570 self.language.labels.get(name, name),
572 571 self.defs['indent'][0] % 0,
573 572 self.defs['indent'][0] % BLOCKQOUTE_INDENT,
574 573 self._docinfo[name],
575 574 self.defs['indent'][1],
576 575 self.defs['indent'][1]))
577 576 elif not name in skip:
578 577 if name in self._docinfo_names:
579 578 label = self._docinfo_names[name]
580 579 else:
581 580 label = self.language.labels.get(name, name)
582 581 self.body.append("\n%s: %s\n" % (label, self._docinfo[name]))
583 582 if self._docinfo['copyright']:
584 583 self.body.append('.SH COPYRIGHT\n%s\n'
585 584 % self._docinfo['copyright'])
586 585 self.body.append(self.comment(
587 586 'Generated by docutils manpage writer.\n'))
588 587
589 588 def visit_emphasis(self, node):
590 589 self.body.append(self.defs['emphasis'][0])
591 590
592 591 def depart_emphasis(self, node):
593 592 self.body.append(self.defs['emphasis'][1])
594 593
595 594 def visit_entry(self, node):
596 595 # a cell in a table row
597 596 if 'morerows' in node:
598 597 self.document.reporter.warning('"table row spanning" not supported',
599 598 base_node=node)
600 599 if 'morecols' in node:
601 600 self.document.reporter.warning(
602 601 '"table cell spanning" not supported', base_node=node)
603 602 self.context.append(len(self.body))
604 603
605 604 def depart_entry(self, node):
606 605 start = self.context.pop()
607 606 self._active_table.append_cell(self.body[start:])
608 607 del self.body[start:]
609 608
610 609 def visit_enumerated_list(self, node):
611 610 self.list_start(node)
612 611
613 612 def depart_enumerated_list(self, node):
614 613 self.list_end()
615 614
616 615 def visit_error(self, node):
617 616 self.visit_admonition(node, 'error')
618 617
619 618 depart_error = depart_admonition
620 619
621 620 def visit_field(self, node):
622 621 pass
623 622
624 623 def depart_field(self, node):
625 624 pass
626 625
627 626 def visit_field_body(self, node):
628 627 if self._in_docinfo:
629 628 name_normalized = self._field_name.lower().replace(" ","_")
630 629 self._docinfo_names[name_normalized] = self._field_name
631 630 self.visit_docinfo_item(node, name_normalized)
632 631 raise nodes.SkipNode
633 632
634 633 def depart_field_body(self, node):
635 634 pass
636 635
637 636 def visit_field_list(self, node):
638 637 self.indent(FIELD_LIST_INDENT)
639 638
640 639 def depart_field_list(self, node):
641 640 self.dedent()
642 641
643 642 def visit_field_name(self, node):
644 643 if self._in_docinfo:
645 644 self._field_name = node.astext()
646 645 raise nodes.SkipNode
647 646 else:
648 647 self.body.append(self.defs['field_name'][0])
649 648
650 649 def depart_field_name(self, node):
651 650 self.body.append(self.defs['field_name'][1])
652 651
653 652 def visit_figure(self, node):
654 653 self.indent(2.5)
655 654 self.indent(0)
656 655
657 656 def depart_figure(self, node):
658 657 self.dedent()
659 658 self.dedent()
660 659
661 660 def visit_footer(self, node):
662 661 self.document.reporter.warning('"footer" not supported',
663 662 base_node=node)
664 663
665 664 def depart_footer(self, node):
666 665 pass
667 666
668 667 def visit_footnote(self, node):
669 668 num, text = node.astext().split(None, 1)
670 669 num = num.strip()
671 670 self.body.append('.IP [%s] 5\n' % self.deunicode(num))
672 671
673 672 def depart_footnote(self, node):
674 673 pass
675 674
676 675 def footnote_backrefs(self, node):
677 676 self.document.reporter.warning('"footnote_backrefs" not supported',
678 677 base_node=node)
679 678
680 679 def visit_footnote_reference(self, node):
681 680 self.body.append('['+self.deunicode(node.astext())+']')
682 681 raise nodes.SkipNode
683 682
684 683 def depart_footnote_reference(self, node):
685 684 pass
686 685
687 686 def visit_generated(self, node):
688 687 pass
689 688
690 689 def depart_generated(self, node):
691 690 pass
692 691
693 692 def visit_header(self, node):
694 693 raise NotImplementedError, node.astext()
695 694
696 695 def depart_header(self, node):
697 696 pass
698 697
699 698 def visit_hint(self, node):
700 699 self.visit_admonition(node, 'hint')
701 700
702 701 depart_hint = depart_admonition
703 702
704 703 def visit_subscript(self, node):
705 704 self.body.append('\\s-2\\d')
706 705
707 706 def depart_subscript(self, node):
708 707 self.body.append('\\u\\s0')
709 708
710 709 def visit_superscript(self, node):
711 710 self.body.append('\\s-2\\u')
712 711
713 712 def depart_superscript(self, node):
714 713 self.body.append('\\d\\s0')
715 714
716 715 def visit_attribution(self, node):
717 716 self.body.append('\\(em ')
718 717
719 718 def depart_attribution(self, node):
720 719 self.body.append('\n')
721 720
722 721 def visit_image(self, node):
723 722 self.document.reporter.warning('"image" not supported',
724 723 base_node=node)
725 724 text = []
726 725 if 'alt' in node.attributes:
727 726 text.append(node.attributes['alt'])
728 727 if 'uri' in node.attributes:
729 728 text.append(node.attributes['uri'])
730 729 self.body.append('[image: %s]\n' % ('/'.join(text)))
731 730 raise nodes.SkipNode
732 731
733 732 def visit_important(self, node):
734 733 self.visit_admonition(node, 'important')
735 734
736 735 depart_important = depart_admonition
737 736
738 737 def visit_label(self, node):
739 738 # footnote and citation
740 739 if (isinstance(node.parent, nodes.footnote)
741 740 or isinstance(node.parent, nodes.citation)):
742 741 raise nodes.SkipNode
743 742 self.document.reporter.warning('"unsupported "label"',
744 743 base_node=node)
745 744 self.body.append('[')
746 745
747 746 def depart_label(self, node):
748 747 self.body.append(']\n')
749 748
750 749 def visit_legend(self, node):
751 750 pass
752 751
753 752 def depart_legend(self, node):
754 753 pass
755 754
756 755 # WHAT should we use .INDENT, .UNINDENT ?
757 756 def visit_line_block(self, node):
758 757 self._line_block += 1
759 758 if self._line_block == 1:
760 759 self.body.append('.sp\n')
761 760 self.body.append('.nf\n')
762 761 else:
763 762 self.body.append('.in +2\n')
764 763
765 764 def depart_line_block(self, node):
766 765 self._line_block -= 1
767 766 if self._line_block == 0:
768 767 self.body.append('.fi\n')
769 768 self.body.append('.sp\n')
770 769 else:
771 770 self.body.append('.in -2\n')
772 771
773 772 def visit_line(self, node):
774 773 pass
775 774
776 775 def depart_line(self, node):
777 776 self.body.append('\n')
778 777
779 778 def visit_list_item(self, node):
780 779 # man 7 man argues to use ".IP" instead of ".TP"
781 780 self.body.append('.IP %s %d\n' % (
782 781 self._list_char[-1].next(),
783 782 self._list_char[-1].get_width(),))
784 783
785 784 def depart_list_item(self, node):
786 785 pass
787 786
788 787 def visit_literal(self, node):
789 788 self.body.append(self.defs['literal'][0])
790 789
791 790 def depart_literal(self, node):
792 791 self.body.append(self.defs['literal'][1])
793 792
794 793 def visit_literal_block(self, node):
795 794 self.body.append(self.defs['literal_block'][0])
796 795 self._in_literal = True
797 796
798 797 def depart_literal_block(self, node):
799 798 self._in_literal = False
800 799 self.body.append(self.defs['literal_block'][1])
801 800
802 801 def visit_meta(self, node):
803 802 raise NotImplementedError, node.astext()
804 803
805 804 def depart_meta(self, node):
806 805 pass
807 806
808 807 def visit_note(self, node):
809 808 self.visit_admonition(node, 'note')
810 809
811 810 depart_note = depart_admonition
812 811
813 812 def indent(self, by=0.5):
814 813 # if we are in a section ".SH" there already is a .RS
815 814 step = self._indent[-1]
816 815 self._indent.append(by)
817 816 self.body.append(self.defs['indent'][0] % step)
818 817
819 818 def dedent(self):
820 819 self._indent.pop()
821 820 self.body.append(self.defs['indent'][1])
822 821
823 822 def visit_option_list(self, node):
824 823 self.indent(OPTION_LIST_INDENT)
825 824
826 825 def depart_option_list(self, node):
827 826 self.dedent()
828 827
829 828 def visit_option_list_item(self, node):
830 829 # one item of the list
831 830 self.body.append(self.defs['option_list_item'][0])
832 831
833 832 def depart_option_list_item(self, node):
834 833 self.body.append(self.defs['option_list_item'][1])
835 834
836 835 def visit_option_group(self, node):
837 836 # as one option could have several forms it is a group
838 837 # options without parameter bold only, .B, -v
839 838 # options with parameter bold italic, .BI, -f file
840 839 #
841 840 # we do not know if .B or .BI
842 841 self.context.append('.B') # blind guess
843 842 self.context.append(len(self.body)) # to be able to insert later
844 843 self.context.append(0) # option counter
845 844
846 845 def depart_option_group(self, node):
847 846 self.context.pop() # the counter
848 847 start_position = self.context.pop()
849 848 text = self.body[start_position:]
850 849 del self.body[start_position:]
851 850 self.body.append('%s%s\n' % (self.context.pop(), ''.join(text)))
852 851
853 852 def visit_option(self, node):
854 853 # each form of the option will be presented separately
855 854 if self.context[-1] > 0:
856 855 self.body.append(', ')
857 856 if self.context[-3] == '.BI':
858 857 self.body.append('\\')
859 858 self.body.append(' ')
860 859
861 860 def depart_option(self, node):
862 861 self.context[-1] += 1
863 862
864 863 def visit_option_string(self, node):
865 864 # do not know if .B or .BI
866 865 pass
867 866
868 867 def depart_option_string(self, node):
869 868 pass
870 869
871 870 def visit_option_argument(self, node):
872 871 self.context[-3] = '.BI' # bold/italic alternate
873 872 if node['delimiter'] != ' ':
874 873 self.body.append('\\fB%s ' % node['delimiter'])
875 874 elif self.body[len(self.body)-1].endswith('='):
876 875 # a blank only means no blank in output, just changing font
877 876 self.body.append(' ')
878 877 else:
879 878 # blank backslash blank, switch font then a blank
880 879 self.body.append(' \\ ')
881 880
882 881 def depart_option_argument(self, node):
883 882 pass
884 883
885 884 def visit_organization(self, node):
886 885 self.visit_docinfo_item(node, 'organization')
887 886
888 887 def depart_organization(self, node):
889 888 pass
890 889
891 890 def visit_paragraph(self, node):
892 891 # ``.PP`` : Start standard indented paragraph.
893 892 # ``.LP`` : Start block paragraph, all except the first.
894 893 # ``.P [type]`` : Start paragraph type.
895 894 # NOTE dont use paragraph starts because they reset indentation.
896 895 # ``.sp`` is only vertical space
897 896 self.ensure_eol()
898 897 self.body.append('.sp\n')
899 898
900 899 def depart_paragraph(self, node):
901 900 self.body.append('\n')
902 901
903 902 def visit_problematic(self, node):
904 903 self.body.append(self.defs['problematic'][0])
905 904
906 905 def depart_problematic(self, node):
907 906 self.body.append(self.defs['problematic'][1])
908 907
909 908 def visit_raw(self, node):
910 909 if node.get('format') == 'manpage':
911 910 self.body.append(node.astext() + "\n")
912 911 # Keep non-manpage raw text out of output:
913 912 raise nodes.SkipNode
914 913
915 914 def visit_reference(self, node):
916 915 """E.g. link or email address."""
917 916 self.body.append(self.defs['reference'][0])
918 917
919 918 def depart_reference(self, node):
920 919 self.body.append(self.defs['reference'][1])
921 920
922 921 def visit_revision(self, node):
923 922 self.visit_docinfo_item(node, 'revision')
924 923
925 924 depart_revision = depart_docinfo_item
926 925
927 926 def visit_row(self, node):
928 927 self._active_table.new_row()
929 928
930 929 def depart_row(self, node):
931 930 pass
932 931
933 932 def visit_section(self, node):
934 933 self.section_level += 1
935 934
936 935 def depart_section(self, node):
937 936 self.section_level -= 1
938 937
939 938 def visit_status(self, node):
940 939 self.visit_docinfo_item(node, 'status')
941 940
942 941 depart_status = depart_docinfo_item
943 942
944 943 def visit_strong(self, node):
945 944 self.body.append(self.defs['strong'][0])
946 945
947 946 def depart_strong(self, node):
948 947 self.body.append(self.defs['strong'][1])
949 948
950 949 def visit_substitution_definition(self, node):
951 950 """Internal only."""
952 951 raise nodes.SkipNode
953 952
954 953 def visit_substitution_reference(self, node):
955 954 self.document.reporter.warning('"substitution_reference" not supported',
956 955 base_node=node)
957 956
958 957 def visit_subtitle(self, node):
959 958 if isinstance(node.parent, nodes.sidebar):
960 959 self.body.append(self.defs['strong'][0])
961 960 elif isinstance(node.parent, nodes.document):
962 961 self.visit_docinfo_item(node, 'subtitle')
963 962 elif isinstance(node.parent, nodes.section):
964 963 self.body.append(self.defs['strong'][0])
965 964
966 965 def depart_subtitle(self, node):
967 966 # document subtitle calls SkipNode
968 967 self.body.append(self.defs['strong'][1]+'\n.PP\n')
969 968
970 969 def visit_system_message(self, node):
971 970 # TODO add report_level
972 971 #if node['level'] < self.document.reporter['writer'].report_level:
973 972 # Level is too low to display:
974 973 # raise nodes.SkipNode
975 974 attr = {}
976 975 backref_text = ''
977 976 if node.hasattr('id'):
978 977 attr['name'] = node['id']
979 978 if node.hasattr('line'):
980 979 line = ', line %s' % node['line']
981 980 else:
982 981 line = ''
983 982 self.body.append('.IP "System Message: %s/%s (%s:%s)"\n'
984 983 % (node['type'], node['level'], node['source'], line))
985 984
986 985 def depart_system_message(self, node):
987 986 pass
988 987
989 988 def visit_table(self, node):
990 989 self._active_table = Table()
991 990
992 991 def depart_table(self, node):
993 992 self.ensure_eol()
994 993 self.body.extend(self._active_table.as_list())
995 994 self._active_table = None
996 995
997 996 def visit_target(self, node):
998 997 # targets are in-document hyper targets, without any use for man-pages.
999 998 raise nodes.SkipNode
1000 999
1001 1000 def visit_tbody(self, node):
1002 1001 pass
1003 1002
1004 1003 def depart_tbody(self, node):
1005 1004 pass
1006 1005
1007 1006 def visit_term(self, node):
1008 1007 self.body.append(self.defs['term'][0])
1009 1008
1010 1009 def depart_term(self, node):
1011 1010 self.body.append(self.defs['term'][1])
1012 1011
1013 1012 def visit_tgroup(self, node):
1014 1013 pass
1015 1014
1016 1015 def depart_tgroup(self, node):
1017 1016 pass
1018 1017
1019 1018 def visit_thead(self, node):
1020 1019 # MAYBE double line '='
1021 1020 pass
1022 1021
1023 1022 def depart_thead(self, node):
1024 1023 # MAYBE double line '='
1025 1024 pass
1026 1025
1027 1026 def visit_tip(self, node):
1028 1027 self.visit_admonition(node, 'tip')
1029 1028
1030 1029 depart_tip = depart_admonition
1031 1030
1032 1031 def visit_title(self, node):
1033 1032 if isinstance(node.parent, nodes.topic):
1034 1033 self.body.append(self.defs['topic-title'][0])
1035 1034 elif isinstance(node.parent, nodes.sidebar):
1036 1035 self.body.append(self.defs['sidebar-title'][0])
1037 1036 elif isinstance(node.parent, nodes.admonition):
1038 1037 self.body.append('.IP "')
1039 1038 elif self.section_level == 0:
1040 1039 self._docinfo['title'] = node.astext()
1041 1040 # document title for .TH
1042 1041 self._docinfo['title_upper'] = node.astext().upper()
1043 1042 raise nodes.SkipNode
1044 1043 elif self.section_level == 1:
1045 1044 self.body.append('.SH ')
1046 1045 for n in node.traverse(nodes.Text):
1047 1046 n.parent.replace(n, nodes.Text(n.astext().upper()))
1048 1047 else:
1049 1048 self.body.append('.SS ')
1050 1049
1051 1050 def depart_title(self, node):
1052 1051 if isinstance(node.parent, nodes.admonition):
1053 1052 self.body.append('"')
1054 1053 self.body.append('\n')
1055 1054
1056 1055 def visit_title_reference(self, node):
1057 1056 """inline citation reference"""
1058 1057 self.body.append(self.defs['title_reference'][0])
1059 1058
1060 1059 def depart_title_reference(self, node):
1061 1060 self.body.append(self.defs['title_reference'][1])
1062 1061
1063 1062 def visit_topic(self, node):
1064 1063 pass
1065 1064
1066 1065 def depart_topic(self, node):
1067 1066 pass
1068 1067
1069 1068 def visit_sidebar(self, node):
1070 1069 pass
1071 1070
1072 1071 def depart_sidebar(self, node):
1073 1072 pass
1074 1073
1075 1074 def visit_rubric(self, node):
1076 1075 pass
1077 1076
1078 1077 def depart_rubric(self, node):
1079 1078 pass
1080 1079
1081 1080 def visit_transition(self, node):
1082 1081 # .PP Begin a new paragraph and reset prevailing indent.
1083 1082 # .sp N leaves N lines of blank space.
1084 1083 # .ce centers the next line
1085 1084 self.body.append('\n.sp\n.ce\n----\n')
1086 1085
1087 1086 def depart_transition(self, node):
1088 1087 self.body.append('\n.ce 0\n.sp\n')
1089 1088
1090 1089 def visit_version(self, node):
1091 1090 self.visit_docinfo_item(node, 'version')
1092 1091
1093 1092 def visit_warning(self, node):
1094 1093 self.visit_admonition(node, 'warning')
1095 1094
1096 1095 depart_warning = depart_admonition
1097 1096
1098 1097 def unimplemented_visit(self, node):
1099 1098 raise NotImplementedError('visiting unimplemented node type: %s'
1100 1099 % node.__class__.__name__)
1101 1100
1102 1101 # vim: set fileencoding=utf-8 et ts=4 ai :
@@ -1,1681 +1,1680 b''
1 #!/usr/bin/env python
2 1 # -*- coding: utf-8 -*-
3 2 # no-check-code
4 3 #
5 4 # License: MIT (see LICENSE file provided)
6 5 # vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4:
7 6
8 7 """
9 8 **polib** allows you to manipulate, create, modify gettext files (pot, po
10 9 and mo files). You can load existing files, iterate through it's entries,
11 10 add, modify entries, comments or metadata, etc... or create new po files
12 11 from scratch.
13 12
14 13 **polib** provides a simple and pythonic API, exporting only three
15 14 convenience functions (*pofile*, *mofile* and *detect_encoding*), and the
16 15 four core classes, *POFile*, *MOFile*, *POEntry* and *MOEntry* for creating
17 16 new files/entries.
18 17
19 18 **Basic example**:
20 19
21 20 >>> import polib
22 21 >>> # load an existing po file
23 22 >>> po = polib.pofile('tests/test_utf8.po')
24 23 >>> for entry in po:
25 24 ... # do something with entry...
26 25 ... pass
27 26 >>> # add an entry
28 27 >>> entry = polib.POEntry(msgid='Welcome', msgstr='Bienvenue')
29 28 >>> entry.occurrences = [('welcome.py', '12'), ('anotherfile.py', '34')]
30 29 >>> po.append(entry)
31 30 >>> # to save our modified po file:
32 31 >>> # po.save()
33 32 >>> # or you may want to compile the po file
34 33 >>> # po.save_as_mofile('tests/test_utf8.mo')
35 34 """
36 35
37 36 __author__ = 'David JEAN LOUIS <izimobil@gmail.com>'
38 37 __version__ = '0.5.2'
39 38 __all__ = ['pofile', 'POFile', 'POEntry', 'mofile', 'MOFile', 'MOEntry',
40 39 'detect_encoding', 'escape', 'unescape', 'detect_encoding',]
41 40
42 41 import codecs
43 42 import struct
44 43 import textwrap
45 44 import types
46 45 import re
47 46
48 47 default_encoding = 'utf-8'
49 48
50 49 # function pofile() {{{
51 50
52 51 def pofile(fpath, **kwargs):
53 52 """
54 53 Convenience function that parse the po/pot file *fpath* and return
55 54 a POFile instance.
56 55
57 56 **Keyword arguments**:
58 57 - *fpath*: string, full or relative path to the po/pot file to parse
59 58 - *wrapwidth*: integer, the wrap width, only useful when -w option was
60 59 passed to xgettext (optional, default to 78)
61 60 - *autodetect_encoding*: boolean, if set to False the function will
62 61 not try to detect the po file encoding (optional, default to True)
63 62 - *encoding*: string, an encoding, only relevant if autodetect_encoding
64 63 is set to False
65 64 - *check_for_duplicates*: whether to check for duplicate entries when
66 65 adding entries to the file, default: False (optional)
67 66
68 67 **Example**:
69 68
70 69 >>> import polib
71 70 >>> po = polib.pofile('tests/test_weird_occurrences.po',
72 71 ... check_for_duplicates=True)
73 72 >>> po #doctest: +ELLIPSIS
74 73 <POFile instance at ...>
75 74 >>> import os, tempfile
76 75 >>> all_attrs = ('msgctxt', 'msgid', 'msgstr', 'msgid_plural',
77 76 ... 'msgstr_plural', 'obsolete', 'comment', 'tcomment',
78 77 ... 'occurrences', 'flags', 'previous_msgctxt',
79 78 ... 'previous_msgid', 'previous_msgid_plural')
80 79 >>> for fname in ['test_iso-8859-15.po', 'test_utf8.po']:
81 80 ... orig_po = polib.pofile('tests/'+fname)
82 81 ... tmpf = tempfile.NamedTemporaryFile().name
83 82 ... orig_po.save(tmpf)
84 83 ... try:
85 84 ... new_po = polib.pofile(tmpf)
86 85 ... for old, new in zip(orig_po, new_po):
87 86 ... for attr in all_attrs:
88 87 ... if getattr(old, attr) != getattr(new, attr):
89 88 ... getattr(old, attr)
90 89 ... getattr(new, attr)
91 90 ... finally:
92 91 ... os.unlink(tmpf)
93 92 >>> po_file = polib.pofile('tests/test_save_as_mofile.po')
94 93 >>> tmpf = tempfile.NamedTemporaryFile().name
95 94 >>> po_file.save_as_mofile(tmpf)
96 95 >>> try:
97 96 ... mo_file = polib.mofile(tmpf)
98 97 ... for old, new in zip(po_file, mo_file):
99 98 ... if po_file._encode(old.msgid) != mo_file._encode(new.msgid):
100 99 ... 'OLD: ', po_file._encode(old.msgid)
101 100 ... 'NEW: ', mo_file._encode(new.msgid)
102 101 ... if po_file._encode(old.msgstr) != mo_file._encode(new.msgstr):
103 102 ... 'OLD: ', po_file._encode(old.msgstr)
104 103 ... 'NEW: ', mo_file._encode(new.msgstr)
105 104 ... print new.msgstr
106 105 ... finally:
107 106 ... os.unlink(tmpf)
108 107 """
109 108 if kwargs.get('autodetect_encoding', True) == True:
110 109 enc = detect_encoding(fpath)
111 110 else:
112 111 enc = kwargs.get('encoding', default_encoding)
113 112 check_for_duplicates = kwargs.get('check_for_duplicates', False)
114 113 parser = _POFileParser(
115 114 fpath,
116 115 encoding=enc,
117 116 check_for_duplicates=kwargs.get('check_for_duplicates', False)
118 117 )
119 118 instance = parser.parse()
120 119 instance.wrapwidth = kwargs.get('wrapwidth', 78)
121 120 return instance
122 121
123 122 # }}}
124 123 # function mofile() {{{
125 124
126 125 def mofile(fpath, **kwargs):
127 126 """
128 127 Convenience function that parse the mo file *fpath* and return
129 128 a MOFile instance.
130 129
131 130 **Keyword arguments**:
132 131 - *fpath*: string, full or relative path to the mo file to parse
133 132 - *wrapwidth*: integer, the wrap width, only useful when -w option was
134 133 passed to xgettext to generate the po file that was used to format
135 134 the mo file (optional, default to 78)
136 135 - *autodetect_encoding*: boolean, if set to False the function will
137 136 not try to detect the po file encoding (optional, default to True)
138 137 - *encoding*: string, an encoding, only relevant if autodetect_encoding
139 138 is set to False
140 139 - *check_for_duplicates*: whether to check for duplicate entries when
141 140 adding entries to the file, default: False (optional)
142 141
143 142 **Example**:
144 143
145 144 >>> import polib
146 145 >>> mo = polib.mofile('tests/test_utf8.mo', check_for_duplicates=True)
147 146 >>> mo #doctest: +ELLIPSIS
148 147 <MOFile instance at ...>
149 148 >>> import os, tempfile
150 149 >>> for fname in ['test_iso-8859-15.mo', 'test_utf8.mo']:
151 150 ... orig_mo = polib.mofile('tests/'+fname)
152 151 ... tmpf = tempfile.NamedTemporaryFile().name
153 152 ... orig_mo.save(tmpf)
154 153 ... try:
155 154 ... new_mo = polib.mofile(tmpf)
156 155 ... for old, new in zip(orig_mo, new_mo):
157 156 ... if old.msgid != new.msgid:
158 157 ... old.msgstr
159 158 ... new.msgstr
160 159 ... finally:
161 160 ... os.unlink(tmpf)
162 161 """
163 162 if kwargs.get('autodetect_encoding', True) == True:
164 163 enc = detect_encoding(fpath, True)
165 164 else:
166 165 enc = kwargs.get('encoding', default_encoding)
167 166 parser = _MOFileParser(
168 167 fpath,
169 168 encoding=enc,
170 169 check_for_duplicates=kwargs.get('check_for_duplicates', False)
171 170 )
172 171 instance = parser.parse()
173 172 instance.wrapwidth = kwargs.get('wrapwidth', 78)
174 173 return instance
175 174
176 175 # }}}
177 176 # function detect_encoding() {{{
178 177
179 178 def detect_encoding(fpath, binary_mode=False):
180 179 """
181 180 Try to detect the encoding used by the file *fpath*. The function will
182 181 return polib default *encoding* if it's unable to detect it.
183 182
184 183 **Keyword argument**:
185 184 - *fpath*: string, full or relative path to the mo file to parse.
186 185
187 186 **Examples**:
188 187
189 188 >>> print(detect_encoding('tests/test_noencoding.po'))
190 189 utf-8
191 190 >>> print(detect_encoding('tests/test_utf8.po'))
192 191 UTF-8
193 192 >>> print(detect_encoding('tests/test_utf8.mo', True))
194 193 UTF-8
195 194 >>> print(detect_encoding('tests/test_iso-8859-15.po'))
196 195 ISO_8859-15
197 196 >>> print(detect_encoding('tests/test_iso-8859-15.mo', True))
198 197 ISO_8859-15
199 198 """
200 199 import re
201 200 rx = re.compile(r'"?Content-Type:.+? charset=([\w_\-:\.]+)')
202 201 if binary_mode:
203 202 mode = 'rb'
204 203 else:
205 204 mode = 'r'
206 205 f = open(fpath, mode)
207 206 for l in f.readlines():
208 207 match = rx.search(l)
209 208 if match:
210 209 f.close()
211 210 return match.group(1).strip()
212 211 f.close()
213 212 return default_encoding
214 213
215 214 # }}}
216 215 # function escape() {{{
217 216
218 217 def escape(st):
219 218 """
220 219 Escape special chars and return the given string *st*.
221 220
222 221 **Examples**:
223 222
224 223 >>> escape('\\t and \\n and \\r and " and \\\\')
225 224 '\\\\t and \\\\n and \\\\r and \\\\" and \\\\\\\\'
226 225 """
227 226 return st.replace('\\', r'\\')\
228 227 .replace('\t', r'\t')\
229 228 .replace('\r', r'\r')\
230 229 .replace('\n', r'\n')\
231 230 .replace('\"', r'\"')
232 231
233 232 # }}}
234 233 # function unescape() {{{
235 234
236 235 def unescape(st):
237 236 """
238 237 Unescape special chars and return the given string *st*.
239 238
240 239 **Examples**:
241 240
242 241 >>> unescape('\\\\t and \\\\n and \\\\r and \\\\" and \\\\\\\\')
243 242 '\\t and \\n and \\r and " and \\\\'
244 243 >>> unescape(r'\\n')
245 244 '\\n'
246 245 >>> unescape(r'\\\\n')
247 246 '\\\\n'
248 247 >>> unescape(r'\\\\n\\n')
249 248 '\\\\n\\n'
250 249 """
251 250 def unescape_repl(m):
252 251 m = m.group(1)
253 252 if m == 'n':
254 253 return '\n'
255 254 if m == 't':
256 255 return '\t'
257 256 if m == 'r':
258 257 return '\r'
259 258 if m == '\\':
260 259 return '\\'
261 260 return m # handles escaped double quote
262 261 return re.sub(r'\\(\\|n|t|r|")', unescape_repl, st)
263 262
264 263 # }}}
265 264 # class _BaseFile {{{
266 265
267 266 class _BaseFile(list):
268 267 """
269 268 Common parent class for POFile and MOFile classes.
270 269 This class must **not** be instanciated directly.
271 270 """
272 271
273 272 def __init__(self, *args, **kwargs):
274 273 """
275 274 Constructor.
276 275
277 276 **Keyword arguments**:
278 277 - *fpath*: string, path to po or mo file
279 278 - *wrapwidth*: integer, the wrap width, only useful when -w option
280 279 was passed to xgettext to generate the po file that was used to
281 280 format the mo file, default to 78 (optional),
282 281 - *encoding*: string, the encoding to use, defaults to
283 282 "default_encoding" global variable (optional),
284 283 - *check_for_duplicates*: whether to check for duplicate entries
285 284 when adding entries to the file, default: False (optional).
286 285 """
287 286 list.__init__(self)
288 287 # the opened file handle
289 288 self.fpath = kwargs.get('fpath')
290 289 # the width at which lines should be wrapped
291 290 self.wrapwidth = kwargs.get('wrapwidth', 78)
292 291 # the file encoding
293 292 self.encoding = kwargs.get('encoding', default_encoding)
294 293 # whether to check for duplicate entries or not
295 294 self.check_for_duplicates = kwargs.get('check_for_duplicates', False)
296 295 # header
297 296 self.header = ''
298 297 # both po and mo files have metadata
299 298 self.metadata = {}
300 299 self.metadata_is_fuzzy = 0
301 300
302 301 def __str__(self):
303 302 """
304 303 String representation of the file.
305 304 """
306 305 ret = []
307 306 entries = [self.metadata_as_entry()] + \
308 307 [e for e in self if not e.obsolete]
309 308 for entry in entries:
310 309 ret.append(entry.__str__(self.wrapwidth))
311 310 for entry in self.obsolete_entries():
312 311 ret.append(entry.__str__(self.wrapwidth))
313 312 return '\n'.join(ret)
314 313
315 314 def __contains__(self, entry):
316 315 """
317 316 Overriden method to implement the membership test (in and not in).
318 317 The method considers that an entry is in the file if it finds an
319 318 entry that has the same msgid (case sensitive).
320 319
321 320 **Keyword argument**:
322 321 - *entry*: an instance of polib._BaseEntry
323 322
324 323 **Tests**:
325 324 >>> po = POFile()
326 325 >>> e1 = POEntry(msgid='foobar', msgstr='spam')
327 326 >>> e2 = POEntry(msgid='barfoo', msgstr='spam')
328 327 >>> e3 = POEntry(msgid='foobar', msgstr='eggs')
329 328 >>> e4 = POEntry(msgid='spameggs', msgstr='eggs')
330 329 >>> po.append(e1)
331 330 >>> po.append(e2)
332 331 >>> e1 in po
333 332 True
334 333 >>> e2 not in po
335 334 False
336 335 >>> e3 in po
337 336 True
338 337 >>> e4 in po
339 338 False
340 339 """
341 340 return self.find(entry.msgid, by='msgid') is not None
342 341
343 342 def append(self, entry):
344 343 """
345 344 Overriden method to check for duplicates entries, if a user tries to
346 345 add an entry that already exists, the method will raise a ValueError
347 346 exception.
348 347
349 348 **Keyword argument**:
350 349 - *entry*: an instance of polib._BaseEntry
351 350
352 351 **Tests**:
353 352 >>> e1 = POEntry(msgid='foobar', msgstr='spam')
354 353 >>> e2 = POEntry(msgid='foobar', msgstr='eggs')
355 354 >>> po = POFile(check_for_duplicates=True)
356 355 >>> po.append(e1)
357 356 >>> try:
358 357 ... po.append(e2)
359 358 ... except ValueError, e:
360 359 ... unicode(e)
361 360 u'Entry "foobar" already exists'
362 361 """
363 362 if self.check_for_duplicates and entry in self:
364 363 raise ValueError('Entry "%s" already exists' % entry.msgid)
365 364 super(_BaseFile, self).append(entry)
366 365
367 366 def insert(self, index, entry):
368 367 """
369 368 Overriden method to check for duplicates entries, if a user tries to
370 369 insert an entry that already exists, the method will raise a ValueError
371 370 exception.
372 371
373 372 **Keyword arguments**:
374 373 - *index*: index at which the entry should be inserted
375 374 - *entry*: an instance of polib._BaseEntry
376 375
377 376 **Tests**:
378 377 >>> import polib
379 378 >>> polib.check_for_duplicates = True
380 379 >>> e1 = POEntry(msgid='foobar', msgstr='spam')
381 380 >>> e2 = POEntry(msgid='barfoo', msgstr='eggs')
382 381 >>> e3 = POEntry(msgid='foobar', msgstr='eggs')
383 382 >>> po = POFile(check_for_duplicates=True)
384 383 >>> po.insert(0, e1)
385 384 >>> po.insert(1, e2)
386 385 >>> try:
387 386 ... po.insert(0, e3)
388 387 ... except ValueError, e:
389 388 ... unicode(e)
390 389 u'Entry "foobar" already exists'
391 390 """
392 391 if self.check_for_duplicates and entry in self:
393 392 raise ValueError('Entry "%s" already exists' % entry.msgid)
394 393 super(_BaseFile, self).insert(index, entry)
395 394
396 395 def __repr__(self):
397 396 """Return the official string representation of the object."""
398 397 return '<%s instance at %x>' % (self.__class__.__name__, id(self))
399 398
400 399 def metadata_as_entry(self):
401 400 """
402 401 Return the metadata as an entry:
403 402
404 403 >>> import polib
405 404 >>> po = polib.pofile('tests/test_fuzzy_header.po')
406 405 >>> unicode(po) == unicode(open('tests/test_fuzzy_header.po').read())
407 406 True
408 407 """
409 408 e = POEntry(msgid='')
410 409 mdata = self.ordered_metadata()
411 410 if mdata:
412 411 strs = []
413 412 e._multiline_str['msgstr'] = ''
414 413 for name, value in mdata:
415 414 # Strip whitespace off each line in a multi-line entry
416 415 strs.append('%s: %s' % (name, value))
417 416 e.msgstr = '\n'.join(strs) + '\n'
418 417 e._multiline_str['msgstr'] = '__POLIB__NL__'.join(
419 418 [s + '\n' for s in strs])
420 419 if self.metadata_is_fuzzy:
421 420 e.flags.append('fuzzy')
422 421 return e
423 422
424 423 def save(self, fpath=None, repr_method='__str__'):
425 424 """
426 425 Save the po file to file *fpath* if no file handle exists for
427 426 the object. If there's already an open file and no fpath is
428 427 provided, then the existing file is rewritten with the modified
429 428 data.
430 429
431 430 **Keyword arguments**:
432 431 - *fpath*: string, full or relative path to the file.
433 432 - *repr_method*: string, the method to use for output.
434 433 """
435 434 if self.fpath is None and fpath is None:
436 435 raise IOError('You must provide a file path to save() method')
437 436 contents = getattr(self, repr_method)()
438 437 if fpath is None:
439 438 fpath = self.fpath
440 439 if repr_method == 'to_binary':
441 440 fhandle = open(fpath, 'wb')
442 441 else:
443 442 fhandle = codecs.open(fpath, 'w', self.encoding)
444 443 if type(contents) != types.UnicodeType:
445 444 contents = contents.decode(self.encoding)
446 445 fhandle.write(contents)
447 446 fhandle.close()
448 447
449 448 def find(self, st, by='msgid'):
450 449 """
451 450 Find entry which msgid (or property identified by the *by*
452 451 attribute) matches the string *st*.
453 452
454 453 **Keyword arguments**:
455 454 - *st*: string, the string to search for
456 455 - *by*: string, the comparison attribute
457 456
458 457 **Examples**:
459 458
460 459 >>> po = pofile('tests/test_utf8.po')
461 460 >>> entry = po.find('Thursday')
462 461 >>> entry.msgstr
463 462 u'Jueves'
464 463 >>> entry = po.find('Some unexistant msgid')
465 464 >>> entry is None
466 465 True
467 466 >>> entry = po.find('Jueves', 'msgstr')
468 467 >>> entry.msgid
469 468 u'Thursday'
470 469 """
471 470 for e in self:
472 471 if getattr(e, by) == st:
473 472 return e
474 473 return None
475 474
476 475 def ordered_metadata(self):
477 476 """
478 477 Convenience method that return the metadata ordered. The return
479 478 value is list of tuples (metadata name, metadata_value).
480 479 """
481 480 # copy the dict first
482 481 metadata = self.metadata.copy()
483 482 data_order = [
484 483 'Project-Id-Version',
485 484 'Report-Msgid-Bugs-To',
486 485 'POT-Creation-Date',
487 486 'PO-Revision-Date',
488 487 'Last-Translator',
489 488 'Language-Team',
490 489 'MIME-Version',
491 490 'Content-Type',
492 491 'Content-Transfer-Encoding'
493 492 ]
494 493 ordered_data = []
495 494 for data in data_order:
496 495 try:
497 496 value = metadata.pop(data)
498 497 ordered_data.append((data, value))
499 498 except KeyError:
500 499 pass
501 500 # the rest of the metadata won't be ordered there are no specs for this
502 501 keys = metadata.keys()
503 502 list(keys).sort()
504 503 for data in keys:
505 504 value = metadata[data]
506 505 ordered_data.append((data, value))
507 506 return ordered_data
508 507
509 508 def to_binary(self):
510 509 """
511 510 Return the mofile binary representation.
512 511 """
513 512 import array
514 513 import struct
515 514 import types
516 515 offsets = []
517 516 entries = self.translated_entries()
518 517 # the keys are sorted in the .mo file
519 518 def cmp(_self, other):
520 519 if _self.msgid > other.msgid:
521 520 return 1
522 521 elif _self.msgid < other.msgid:
523 522 return -1
524 523 else:
525 524 return 0
526 525 # add metadata entry
527 526 entries.sort(cmp)
528 527 mentry = self.metadata_as_entry()
529 528 mentry.msgstr = mentry.msgstr.replace('\\n', '').lstrip()
530 529 entries = [mentry] + entries
531 530 entries_len = len(entries)
532 531 ids, strs = '', ''
533 532 for e in entries:
534 533 # For each string, we need size and file offset. Each string is
535 534 # NUL terminated; the NUL does not count into the size.
536 535 if e.msgid_plural:
537 536 indexes = e.msgstr_plural.keys()
538 537 indexes.sort()
539 538 msgstr = []
540 539 for index in indexes:
541 540 msgstr.append(e.msgstr_plural[index])
542 541 msgid = self._encode(e.msgid + '\0' + e.msgid_plural)
543 542 msgstr = self._encode('\0'.join(msgstr))
544 543 else:
545 544 msgid = self._encode(e.msgid)
546 545 msgstr = self._encode(e.msgstr)
547 546 offsets.append((len(ids), len(msgid), len(strs), len(msgstr)))
548 547 ids += msgid + '\0'
549 548 strs += msgstr + '\0'
550 549 # The header is 7 32-bit unsigned integers.
551 550 keystart = 7*4+16*entries_len
552 551 # and the values start after the keys
553 552 valuestart = keystart + len(ids)
554 553 koffsets = []
555 554 voffsets = []
556 555 # The string table first has the list of keys, then the list of values.
557 556 # Each entry has first the size of the string, then the file offset.
558 557 for o1, l1, o2, l2 in offsets:
559 558 koffsets += [l1, o1+keystart]
560 559 voffsets += [l2, o2+valuestart]
561 560 offsets = koffsets + voffsets
562 561 output = struct.pack("IIIIIII",
563 562 0x950412de, # Magic number
564 563 0, # Version
565 564 entries_len, # # of entries
566 565 7*4, # start of key index
567 566 7*4+entries_len*8, # start of value index
568 567 0, 0) # size and offset of hash table
569 568 output += array.array("I", offsets).tostring()
570 569 output += ids
571 570 output += strs
572 571 return output
573 572
574 573 def _encode(self, mixed):
575 574 """
576 575 Encode the given argument with the file encoding if the type is unicode
577 576 and return the encoded string.
578 577 """
579 578 if type(mixed) == types.UnicodeType:
580 579 return mixed.encode(self.encoding)
581 580 return mixed
582 581
583 582 # }}}
584 583 # class POFile {{{
585 584
586 585 class POFile(_BaseFile):
587 586 '''
588 587 Po (or Pot) file reader/writer.
589 588 POFile objects inherit the list objects methods.
590 589
591 590 **Example**:
592 591
593 592 >>> po = POFile()
594 593 >>> entry1 = POEntry(
595 594 ... msgid="Some english text",
596 595 ... msgstr="Un texte en anglais"
597 596 ... )
598 597 >>> entry1.occurrences = [('testfile', 12),('another_file', 1)]
599 598 >>> entry1.comment = "Some useful comment"
600 599 >>> entry2 = POEntry(
601 600 ... msgid="Peace in some languages",
602 601 ... msgstr="Pace سلام שלום Hasîtî 和平"
603 602 ... )
604 603 >>> entry2.occurrences = [('testfile', 15),('another_file', 5)]
605 604 >>> entry2.comment = "Another useful comment"
606 605 >>> entry3 = POEntry(
607 606 ... msgid='Some entry with quotes " \\"',
608 607 ... msgstr='Un message unicode avec des quotes " \\"'
609 608 ... )
610 609 >>> entry3.comment = "Test string quoting"
611 610 >>> po.append(entry1)
612 611 >>> po.append(entry2)
613 612 >>> po.append(entry3)
614 613 >>> po.header = "Some Header"
615 614 >>> print(po)
616 615 # Some Header
617 616 msgid ""
618 617 msgstr ""
619 618 <BLANKLINE>
620 619 #. Some useful comment
621 620 #: testfile:12 another_file:1
622 621 msgid "Some english text"
623 622 msgstr "Un texte en anglais"
624 623 <BLANKLINE>
625 624 #. Another useful comment
626 625 #: testfile:15 another_file:5
627 626 msgid "Peace in some languages"
628 627 msgstr "Pace سلام שלום Hasîtî 和平"
629 628 <BLANKLINE>
630 629 #. Test string quoting
631 630 msgid "Some entry with quotes \\" \\""
632 631 msgstr "Un message unicode avec des quotes \\" \\""
633 632 <BLANKLINE>
634 633 '''
635 634
636 635 def __str__(self):
637 636 """Return the string representation of the po file"""
638 637 ret, headers = '', self.header.split('\n')
639 638 for header in headers:
640 639 if header[:1] in [',', ':']:
641 640 ret += '#%s\n' % header
642 641 else:
643 642 ret += '# %s\n' % header
644 643 return ret + _BaseFile.__str__(self)
645 644
646 645 def save_as_mofile(self, fpath):
647 646 """
648 647 Save the binary representation of the file to *fpath*.
649 648
650 649 **Keyword arguments**:
651 650 - *fpath*: string, full or relative path to the file.
652 651 """
653 652 _BaseFile.save(self, fpath, 'to_binary')
654 653
655 654 def percent_translated(self):
656 655 """
657 656 Convenience method that return the percentage of translated
658 657 messages.
659 658
660 659 **Example**:
661 660
662 661 >>> import polib
663 662 >>> po = polib.pofile('tests/test_pofile_helpers.po')
664 663 >>> po.percent_translated()
665 664 50
666 665 >>> po = POFile()
667 666 >>> po.percent_translated()
668 667 100
669 668 """
670 669 total = len([e for e in self if not e.obsolete])
671 670 if total == 0:
672 671 return 100
673 672 translated = len(self.translated_entries())
674 673 return int((100.00 / float(total)) * translated)
675 674
676 675 def translated_entries(self):
677 676 """
678 677 Convenience method that return a list of translated entries.
679 678
680 679 **Example**:
681 680
682 681 >>> import polib
683 682 >>> po = polib.pofile('tests/test_pofile_helpers.po')
684 683 >>> len(po.translated_entries())
685 684 6
686 685 """
687 686 return [e for e in self if e.translated()]
688 687
689 688 def untranslated_entries(self):
690 689 """
691 690 Convenience method that return a list of untranslated entries.
692 691
693 692 **Example**:
694 693
695 694 >>> import polib
696 695 >>> po = polib.pofile('tests/test_pofile_helpers.po')
697 696 >>> len(po.untranslated_entries())
698 697 4
699 698 """
700 699 return [e for e in self if not e.translated() and not e.obsolete \
701 700 and not 'fuzzy' in e.flags]
702 701
703 702 def fuzzy_entries(self):
704 703 """
705 704 Convenience method that return the list of 'fuzzy' entries.
706 705
707 706 **Example**:
708 707
709 708 >>> import polib
710 709 >>> po = polib.pofile('tests/test_pofile_helpers.po')
711 710 >>> len(po.fuzzy_entries())
712 711 2
713 712 """
714 713 return [e for e in self if 'fuzzy' in e.flags]
715 714
716 715 def obsolete_entries(self):
717 716 """
718 717 Convenience method that return the list of obsolete entries.
719 718
720 719 **Example**:
721 720
722 721 >>> import polib
723 722 >>> po = polib.pofile('tests/test_pofile_helpers.po')
724 723 >>> len(po.obsolete_entries())
725 724 4
726 725 """
727 726 return [e for e in self if e.obsolete]
728 727
729 728 def merge(self, refpot):
730 729 """
731 730 XXX this could not work if encodings are different, needs thinking
732 731 and general refactoring of how polib handles encoding...
733 732
734 733 Convenience method that merge the current pofile with the pot file
735 734 provided. It behaves exactly as the gettext msgmerge utility:
736 735
737 736 - comments of this file will be preserved, but extracted comments
738 737 and occurrences will be discarded
739 738 - any translations or comments in the file will be discarded,
740 739 however dot comments and file positions will be preserved
741 740
742 741 **Keyword argument**:
743 742 - *refpot*: object POFile, the reference catalog.
744 743
745 744 **Example**:
746 745
747 746 >>> import polib
748 747 >>> refpot = polib.pofile('tests/test_merge.pot')
749 748 >>> po = polib.pofile('tests/test_merge_before.po')
750 749 >>> po.merge(refpot)
751 750 >>> expected_po = polib.pofile('tests/test_merge_after.po')
752 751 >>> unicode(po) == unicode(expected_po)
753 752 True
754 753 """
755 754 for entry in refpot:
756 755 e = self.find(entry.msgid)
757 756 if e is None:
758 757 e = POEntry()
759 758 self.append(e)
760 759 e.merge(entry)
761 760 # ok, now we must "obsolete" entries that are not in the refpot
762 761 # anymore
763 762 for entry in self:
764 763 if refpot.find(entry.msgid) is None:
765 764 entry.obsolete = True
766 765
767 766 # }}}
768 767 # class MOFile {{{
769 768
770 769 class MOFile(_BaseFile):
771 770 '''
772 771 Mo file reader/writer.
773 772 MOFile objects inherit the list objects methods.
774 773
775 774 **Example**:
776 775
777 776 >>> mo = MOFile()
778 777 >>> entry1 = POEntry(
779 778 ... msgid="Some english text",
780 779 ... msgstr="Un texte en anglais"
781 780 ... )
782 781 >>> entry2 = POEntry(
783 782 ... msgid="I need my dirty cheese",
784 783 ... msgstr="Je veux mon sale fromage"
785 784 ... )
786 785 >>> entry3 = MOEntry(
787 786 ... msgid='Some entry with quotes " \\"',
788 787 ... msgstr='Un message unicode avec des quotes " \\"'
789 788 ... )
790 789 >>> mo.append(entry1)
791 790 >>> mo.append(entry2)
792 791 >>> mo.append(entry3)
793 792 >>> print(mo)
794 793 msgid ""
795 794 msgstr ""
796 795 <BLANKLINE>
797 796 msgid "Some english text"
798 797 msgstr "Un texte en anglais"
799 798 <BLANKLINE>
800 799 msgid "I need my dirty cheese"
801 800 msgstr "Je veux mon sale fromage"
802 801 <BLANKLINE>
803 802 msgid "Some entry with quotes \\" \\""
804 803 msgstr "Un message unicode avec des quotes \\" \\""
805 804 <BLANKLINE>
806 805 '''
807 806
808 807 def __init__(self, *args, **kwargs):
809 808 """
810 809 MOFile constructor. Mo files have two other properties:
811 810 - magic_number: the magic_number of the binary file,
812 811 - version: the version of the mo spec.
813 812 """
814 813 _BaseFile.__init__(self, *args, **kwargs)
815 814 self.magic_number = None
816 815 self.version = 0
817 816
818 817 def save_as_pofile(self, fpath):
819 818 """
820 819 Save the string representation of the file to *fpath*.
821 820
822 821 **Keyword argument**:
823 822 - *fpath*: string, full or relative path to the file.
824 823 """
825 824 _BaseFile.save(self, fpath)
826 825
827 826 def save(self, fpath):
828 827 """
829 828 Save the binary representation of the file to *fpath*.
830 829
831 830 **Keyword argument**:
832 831 - *fpath*: string, full or relative path to the file.
833 832 """
834 833 _BaseFile.save(self, fpath, 'to_binary')
835 834
836 835 def percent_translated(self):
837 836 """
838 837 Convenience method to keep the same interface with POFile instances.
839 838 """
840 839 return 100
841 840
842 841 def translated_entries(self):
843 842 """
844 843 Convenience method to keep the same interface with POFile instances.
845 844 """
846 845 return self
847 846
848 847 def untranslated_entries(self):
849 848 """
850 849 Convenience method to keep the same interface with POFile instances.
851 850 """
852 851 return []
853 852
854 853 def fuzzy_entries(self):
855 854 """
856 855 Convenience method to keep the same interface with POFile instances.
857 856 """
858 857 return []
859 858
860 859 def obsolete_entries(self):
861 860 """
862 861 Convenience method to keep the same interface with POFile instances.
863 862 """
864 863 return []
865 864
866 865 # }}}
867 866 # class _BaseEntry {{{
868 867
869 868 class _BaseEntry(object):
870 869 """
871 870 Base class for POEntry or MOEntry objects.
872 871 This class must *not* be instanciated directly.
873 872 """
874 873
875 874 def __init__(self, *args, **kwargs):
876 875 """Base Entry constructor."""
877 876 self.msgid = kwargs.get('msgid', '')
878 877 self.msgstr = kwargs.get('msgstr', '')
879 878 self.msgid_plural = kwargs.get('msgid_plural', '')
880 879 self.msgstr_plural = kwargs.get('msgstr_plural', {})
881 880 self.obsolete = kwargs.get('obsolete', False)
882 881 self.encoding = kwargs.get('encoding', default_encoding)
883 882 self.msgctxt = kwargs.get('msgctxt', None)
884 883 self._multiline_str = {}
885 884
886 885 def __repr__(self):
887 886 """Return the official string representation of the object."""
888 887 return '<%s instance at %x>' % (self.__class__.__name__, id(self))
889 888
890 889 def __str__(self, wrapwidth=78):
891 890 """
892 891 Common string representation of the POEntry and MOEntry
893 892 objects.
894 893 """
895 894 if self.obsolete:
896 895 delflag = '#~ '
897 896 else:
898 897 delflag = ''
899 898 ret = []
900 899 # write the msgctxt if any
901 900 if self.msgctxt is not None:
902 901 ret += self._str_field("msgctxt", delflag, "", self.msgctxt)
903 902 # write the msgid
904 903 ret += self._str_field("msgid", delflag, "", self.msgid)
905 904 # write the msgid_plural if any
906 905 if self.msgid_plural:
907 906 ret += self._str_field("msgid_plural", delflag, "", self.msgid_plural)
908 907 if self.msgstr_plural:
909 908 # write the msgstr_plural if any
910 909 msgstrs = self.msgstr_plural
911 910 keys = list(msgstrs)
912 911 keys.sort()
913 912 for index in keys:
914 913 msgstr = msgstrs[index]
915 914 plural_index = '[%s]' % index
916 915 ret += self._str_field("msgstr", delflag, plural_index, msgstr)
917 916 else:
918 917 # otherwise write the msgstr
919 918 ret += self._str_field("msgstr", delflag, "", self.msgstr)
920 919 ret.append('')
921 920 return '\n'.join(ret)
922 921
923 922 def _str_field(self, fieldname, delflag, plural_index, field):
924 923 if (fieldname + plural_index) in self._multiline_str:
925 924 field = self._multiline_str[fieldname + plural_index]
926 925 lines = [''] + field.split('__POLIB__NL__')
927 926 else:
928 927 lines = field.splitlines(True)
929 928 if len(lines) > 1:
930 929 lines = ['']+lines # start with initial empty line
931 930 else:
932 931 lines = [field] # needed for the empty string case
933 932 if fieldname.startswith('previous_'):
934 933 # quick and dirty trick to get the real field name
935 934 fieldname = fieldname[9:]
936 935
937 936 ret = ['%s%s%s "%s"' % (delflag, fieldname, plural_index,
938 937 escape(lines.pop(0)))]
939 938 for mstr in lines:
940 939 ret.append('%s"%s"' % (delflag, escape(mstr)))
941 940 return ret
942 941
943 942 # }}}
944 943 # class POEntry {{{
945 944
946 945 class POEntry(_BaseEntry):
947 946 """
948 947 Represents a po file entry.
949 948
950 949 **Examples**:
951 950
952 951 >>> entry = POEntry(msgid='Welcome', msgstr='Bienvenue')
953 952 >>> entry.occurrences = [('welcome.py', 12), ('anotherfile.py', 34)]
954 953 >>> print(entry)
955 954 #: welcome.py:12 anotherfile.py:34
956 955 msgid "Welcome"
957 956 msgstr "Bienvenue"
958 957 <BLANKLINE>
959 958 >>> entry = POEntry()
960 959 >>> entry.occurrences = [('src/some-very-long-filename-that-should-not-be-wrapped-even-if-it-is-larger-than-the-wrap-limit.c', 32), ('src/eggs.c', 45)]
961 960 >>> entry.comment = 'A plural translation. This is a very very very long line please do not wrap, this is just for testing comment wrapping...'
962 961 >>> entry.tcomment = 'A plural translation. This is a very very very long line please do not wrap, this is just for testing comment wrapping...'
963 962 >>> entry.flags.append('c-format')
964 963 >>> entry.previous_msgctxt = '@somecontext'
965 964 >>> entry.previous_msgid = 'I had eggs but no spam !'
966 965 >>> entry.previous_msgid_plural = 'I had eggs and %d spam !'
967 966 >>> entry.msgctxt = '@somenewcontext'
968 967 >>> entry.msgid = 'I have spam but no egg !'
969 968 >>> entry.msgid_plural = 'I have spam and %d eggs !'
970 969 >>> entry.msgstr_plural[0] = "J'ai du jambon mais aucun oeuf !"
971 970 >>> entry.msgstr_plural[1] = "J'ai du jambon et %d oeufs !"
972 971 >>> print(entry)
973 972 #. A plural translation. This is a very very very long line please do not
974 973 #. wrap, this is just for testing comment wrapping...
975 974 # A plural translation. This is a very very very long line please do not wrap,
976 975 # this is just for testing comment wrapping...
977 976 #: src/some-very-long-filename-that-should-not-be-wrapped-even-if-it-is-larger-than-the-wrap-limit.c:32
978 977 #: src/eggs.c:45
979 978 #, c-format
980 979 #| msgctxt "@somecontext"
981 980 #| msgid "I had eggs but no spam !"
982 981 #| msgid_plural "I had eggs and %d spam !"
983 982 msgctxt "@somenewcontext"
984 983 msgid "I have spam but no egg !"
985 984 msgid_plural "I have spam and %d eggs !"
986 985 msgstr[0] "J'ai du jambon mais aucun oeuf !"
987 986 msgstr[1] "J'ai du jambon et %d oeufs !"
988 987 <BLANKLINE>
989 988 """
990 989
991 990 def __init__(self, *args, **kwargs):
992 991 """POEntry constructor."""
993 992 _BaseEntry.__init__(self, *args, **kwargs)
994 993 self.comment = kwargs.get('comment', '')
995 994 self.tcomment = kwargs.get('tcomment', '')
996 995 self.occurrences = kwargs.get('occurrences', [])
997 996 self.flags = kwargs.get('flags', [])
998 997 self.previous_msgctxt = kwargs.get('previous_msgctxt', None)
999 998 self.previous_msgid = kwargs.get('previous_msgid', None)
1000 999 self.previous_msgid_plural = kwargs.get('previous_msgid_plural', None)
1001 1000
1002 1001 def __str__(self, wrapwidth=78):
1003 1002 """
1004 1003 Return the string representation of the entry.
1005 1004 """
1006 1005 if self.obsolete:
1007 1006 return _BaseEntry.__str__(self)
1008 1007 ret = []
1009 1008 # comment first, if any (with text wrapping as xgettext does)
1010 1009 if self.comment != '':
1011 1010 for comment in self.comment.split('\n'):
1012 1011 if wrapwidth > 0 and len(comment) > wrapwidth-3:
1013 1012 ret += textwrap.wrap(comment, wrapwidth,
1014 1013 initial_indent='#. ',
1015 1014 subsequent_indent='#. ',
1016 1015 break_long_words=False)
1017 1016 else:
1018 1017 ret.append('#. %s' % comment)
1019 1018 # translator comment, if any (with text wrapping as xgettext does)
1020 1019 if self.tcomment != '':
1021 1020 for tcomment in self.tcomment.split('\n'):
1022 1021 if wrapwidth > 0 and len(tcomment) > wrapwidth-2:
1023 1022 ret += textwrap.wrap(tcomment, wrapwidth,
1024 1023 initial_indent='# ',
1025 1024 subsequent_indent='# ',
1026 1025 break_long_words=False)
1027 1026 else:
1028 1027 ret.append('# %s' % tcomment)
1029 1028 # occurrences (with text wrapping as xgettext does)
1030 1029 if self.occurrences:
1031 1030 filelist = []
1032 1031 for fpath, lineno in self.occurrences:
1033 1032 if lineno:
1034 1033 filelist.append('%s:%s' % (fpath, lineno))
1035 1034 else:
1036 1035 filelist.append(fpath)
1037 1036 filestr = ' '.join(filelist)
1038 1037 if wrapwidth > 0 and len(filestr)+3 > wrapwidth:
1039 1038 # XXX textwrap split words that contain hyphen, this is not
1040 1039 # what we want for filenames, so the dirty hack is to
1041 1040 # temporally replace hyphens with a char that a file cannot
1042 1041 # contain, like "*"
1043 1042 lines = textwrap.wrap(filestr.replace('-', '*'),
1044 1043 wrapwidth,
1045 1044 initial_indent='#: ',
1046 1045 subsequent_indent='#: ',
1047 1046 break_long_words=False)
1048 1047 # end of the replace hack
1049 1048 for line in lines:
1050 1049 ret.append(line.replace('*', '-'))
1051 1050 else:
1052 1051 ret.append('#: '+filestr)
1053 1052 # flags
1054 1053 if self.flags:
1055 1054 flags = []
1056 1055 for flag in self.flags:
1057 1056 flags.append(flag)
1058 1057 ret.append('#, %s' % ', '.join(flags))
1059 1058
1060 1059 # previous context and previous msgid/msgid_plural
1061 1060 if self.previous_msgctxt:
1062 1061 ret += self._str_field("previous_msgctxt", "#| ", "",
1063 1062 self.previous_msgctxt)
1064 1063 if self.previous_msgid:
1065 1064 ret += self._str_field("previous_msgid", "#| ", "",
1066 1065 self.previous_msgid)
1067 1066 if self.previous_msgid_plural:
1068 1067 ret += self._str_field("previous_msgid_plural", "#| ", "",
1069 1068 self.previous_msgid_plural)
1070 1069
1071 1070 ret.append(_BaseEntry.__str__(self))
1072 1071 return '\n'.join(ret)
1073 1072
1074 1073 def __cmp__(self, other):
1075 1074 '''
1076 1075 Called by comparison operations if rich comparison is not defined.
1077 1076
1078 1077 **Tests**:
1079 1078 >>> a = POEntry(msgid='a', occurrences=[('b.py', 1), ('b.py', 3)])
1080 1079 >>> b = POEntry(msgid='b', occurrences=[('b.py', 1), ('b.py', 3)])
1081 1080 >>> c1 = POEntry(msgid='c1', occurrences=[('a.py', 1), ('b.py', 1)])
1082 1081 >>> c2 = POEntry(msgid='c2', occurrences=[('a.py', 1), ('a.py', 3)])
1083 1082 >>> po = POFile()
1084 1083 >>> po.append(a)
1085 1084 >>> po.append(b)
1086 1085 >>> po.append(c1)
1087 1086 >>> po.append(c2)
1088 1087 >>> po.sort()
1089 1088 >>> print(po)
1090 1089 #
1091 1090 msgid ""
1092 1091 msgstr ""
1093 1092 <BLANKLINE>
1094 1093 #: a.py:1 a.py:3
1095 1094 msgid "c2"
1096 1095 msgstr ""
1097 1096 <BLANKLINE>
1098 1097 #: a.py:1 b.py:1
1099 1098 msgid "c1"
1100 1099 msgstr ""
1101 1100 <BLANKLINE>
1102 1101 #: b.py:1 b.py:3
1103 1102 msgid "a"
1104 1103 msgstr ""
1105 1104 <BLANKLINE>
1106 1105 #: b.py:1 b.py:3
1107 1106 msgid "b"
1108 1107 msgstr ""
1109 1108 <BLANKLINE>
1110 1109 '''
1111 1110 def compare_occurrences(a, b):
1112 1111 """
1113 1112 Compare an entry occurrence with another one.
1114 1113 """
1115 1114 if a[0] != b[0]:
1116 1115 return a[0] < b[0]
1117 1116 if a[1] != b[1]:
1118 1117 return a[1] < b[1]
1119 1118 return 0
1120 1119
1121 1120 # First: Obsolete test
1122 1121 if self.obsolete != other.obsolete:
1123 1122 if self.obsolete:
1124 1123 return -1
1125 1124 else:
1126 1125 return 1
1127 1126 # Work on a copy to protect original
1128 1127 occ1 = self.occurrences[:]
1129 1128 occ2 = other.occurrences[:]
1130 1129 # Sorting using compare method
1131 1130 occ1.sort(compare_occurrences)
1132 1131 occ2.sort(compare_occurrences)
1133 1132 # Comparing sorted occurrences
1134 1133 pos = 0
1135 1134 for entry1 in occ1:
1136 1135 try:
1137 1136 entry2 = occ2[pos]
1138 1137 except IndexError:
1139 1138 return 1
1140 1139 pos = pos + 1
1141 1140 if entry1[0] != entry2[0]:
1142 1141 if entry1[0] > entry2[0]:
1143 1142 return 1
1144 1143 else:
1145 1144 return -1
1146 1145 if entry1[1] != entry2[1]:
1147 1146 if entry1[1] > entry2[1]:
1148 1147 return 1
1149 1148 else:
1150 1149 return -1
1151 1150 # Finally: Compare message ID
1152 1151 if self.msgid > other.msgid: return 1
1153 1152 else: return -1
1154 1153
1155 1154 def translated(self):
1156 1155 """
1157 1156 Return True if the entry has been translated or False.
1158 1157 """
1159 1158 if self.obsolete or 'fuzzy' in self.flags:
1160 1159 return False
1161 1160 if self.msgstr != '':
1162 1161 return True
1163 1162 if self.msgstr_plural:
1164 1163 for pos in self.msgstr_plural:
1165 1164 if self.msgstr_plural[pos] == '':
1166 1165 return False
1167 1166 return True
1168 1167 return False
1169 1168
1170 1169 def merge(self, other):
1171 1170 """
1172 1171 Merge the current entry with the given pot entry.
1173 1172 """
1174 1173 self.msgid = other.msgid
1175 1174 self.occurrences = other.occurrences
1176 1175 self.comment = other.comment
1177 1176 self.flags = other.flags
1178 1177 self.msgid_plural = other.msgid_plural
1179 1178 if other.msgstr_plural:
1180 1179 for pos in other.msgstr_plural:
1181 1180 try:
1182 1181 # keep existing translation at pos if any
1183 1182 self.msgstr_plural[pos]
1184 1183 except KeyError:
1185 1184 self.msgstr_plural[pos] = ''
1186 1185
1187 1186 # }}}
1188 1187 # class MOEntry {{{
1189 1188
1190 1189 class MOEntry(_BaseEntry):
1191 1190 """
1192 1191 Represents a mo file entry.
1193 1192
1194 1193 **Examples**:
1195 1194
1196 1195 >>> entry = MOEntry()
1197 1196 >>> entry.msgid = 'translate me !'
1198 1197 >>> entry.msgstr = 'traduisez moi !'
1199 1198 >>> print(entry)
1200 1199 msgid "translate me !"
1201 1200 msgstr "traduisez moi !"
1202 1201 <BLANKLINE>
1203 1202 """
1204 1203
1205 1204 def __str__(self, wrapwidth=78):
1206 1205 """
1207 1206 Return the string representation of the entry.
1208 1207 """
1209 1208 return _BaseEntry.__str__(self, wrapwidth)
1210 1209
1211 1210 # }}}
1212 1211 # class _POFileParser {{{
1213 1212
1214 1213 class _POFileParser(object):
1215 1214 """
1216 1215 A finite state machine to parse efficiently and correctly po
1217 1216 file format.
1218 1217 """
1219 1218
1220 1219 def __init__(self, fpath, *args, **kwargs):
1221 1220 """
1222 1221 Constructor.
1223 1222
1224 1223 **Arguments**:
1225 1224 - *fpath*: string, path to the po file
1226 1225 - *encoding*: string, the encoding to use, defaults to
1227 1226 "default_encoding" global variable (optional),
1228 1227 - *check_for_duplicates*: whether to check for duplicate entries
1229 1228 when adding entries to the file, default: False (optional).
1230 1229 """
1231 1230 enc = kwargs.get('encoding', default_encoding)
1232 1231 check_dup = kwargs.get('check_for_duplicates', False)
1233 1232 try:
1234 1233 self.fhandle = codecs.open(fpath, 'rU', enc)
1235 1234 except LookupError:
1236 1235 enc = default_encoding
1237 1236 self.fhandle = codecs.open(fpath, 'rU', enc)
1238 1237 self.instance = POFile(
1239 1238 fpath=fpath,
1240 1239 encoding=enc,
1241 1240 check_for_duplicates=check_dup
1242 1241 )
1243 1242 self.transitions = {}
1244 1243 self.current_entry = POEntry()
1245 1244 self.current_state = 'ST'
1246 1245 self.current_token = None
1247 1246 # two memo flags used in handlers
1248 1247 self.msgstr_index = 0
1249 1248 self.entry_obsolete = 0
1250 1249 # Configure the state machine, by adding transitions.
1251 1250 # Signification of symbols:
1252 1251 # * ST: Beginning of the file (start)
1253 1252 # * HE: Header
1254 1253 # * TC: a translation comment
1255 1254 # * GC: a generated comment
1256 1255 # * OC: a file/line occurence
1257 1256 # * FL: a flags line
1258 1257 # * CT: a message context
1259 1258 # * PC: a previous msgctxt
1260 1259 # * PM: a previous msgid
1261 1260 # * PP: a previous msgid_plural
1262 1261 # * MI: a msgid
1263 1262 # * MP: a msgid plural
1264 1263 # * MS: a msgstr
1265 1264 # * MX: a msgstr plural
1266 1265 # * MC: a msgid or msgstr continuation line
1267 1266 all = ['ST', 'HE', 'GC', 'OC', 'FL', 'CT', 'PC', 'PM', 'PP', 'TC',
1268 1267 'MS', 'MP', 'MX', 'MI']
1269 1268
1270 1269 self.add('TC', ['ST', 'HE'], 'HE')
1271 1270 self.add('TC', ['GC', 'OC', 'FL', 'TC', 'PC', 'PM', 'PP', 'MS',
1272 1271 'MP', 'MX', 'MI'], 'TC')
1273 1272 self.add('GC', all, 'GC')
1274 1273 self.add('OC', all, 'OC')
1275 1274 self.add('FL', all, 'FL')
1276 1275 self.add('PC', all, 'PC')
1277 1276 self.add('PM', all, 'PM')
1278 1277 self.add('PP', all, 'PP')
1279 1278 self.add('CT', ['ST', 'HE', 'GC', 'OC', 'FL', 'TC', 'PC', 'PM',
1280 1279 'PP', 'MS', 'MX'], 'CT')
1281 1280 self.add('MI', ['ST', 'HE', 'GC', 'OC', 'FL', 'CT', 'TC', 'PC',
1282 1281 'PM', 'PP', 'MS', 'MX'], 'MI')
1283 1282 self.add('MP', ['TC', 'GC', 'PC', 'PM', 'PP', 'MI'], 'MP')
1284 1283 self.add('MS', ['MI', 'MP', 'TC'], 'MS')
1285 1284 self.add('MX', ['MI', 'MX', 'MP', 'TC'], 'MX')
1286 1285 self.add('MC', ['CT', 'MI', 'MP', 'MS', 'MX', 'PM', 'PP', 'PC'], 'MC')
1287 1286
1288 1287 def parse(self):
1289 1288 """
1290 1289 Run the state machine, parse the file line by line and call process()
1291 1290 with the current matched symbol.
1292 1291 """
1293 1292 i, lastlen = 1, 0
1294 1293 for line in self.fhandle:
1295 1294 line = line.strip()
1296 1295 if line == '':
1297 1296 i = i+1
1298 1297 continue
1299 1298 if line[:3] == '#~ ':
1300 1299 line = line[3:]
1301 1300 self.entry_obsolete = 1
1302 1301 else:
1303 1302 self.entry_obsolete = 0
1304 1303 self.current_token = line
1305 1304 if line[:2] == '#:':
1306 1305 # we are on a occurrences line
1307 1306 self.process('OC', i)
1308 1307 elif line[:9] == 'msgctxt "':
1309 1308 # we are on a msgctxt
1310 1309 self.process('CT', i)
1311 1310 elif line[:7] == 'msgid "':
1312 1311 # we are on a msgid
1313 1312 self.process('MI', i)
1314 1313 elif line[:8] == 'msgstr "':
1315 1314 # we are on a msgstr
1316 1315 self.process('MS', i)
1317 1316 elif line[:1] == '"' or line[:4] == '#| "':
1318 1317 # we are on a continuation line or some metadata
1319 1318 self.process('MC', i)
1320 1319 elif line[:14] == 'msgid_plural "':
1321 1320 # we are on a msgid plural
1322 1321 self.process('MP', i)
1323 1322 elif line[:7] == 'msgstr[':
1324 1323 # we are on a msgstr plural
1325 1324 self.process('MX', i)
1326 1325 elif line[:3] == '#, ':
1327 1326 # we are on a flags line
1328 1327 self.process('FL', i)
1329 1328 elif line[:2] == '# ' or line == '#':
1330 1329 if line == '#': line = line + ' '
1331 1330 # we are on a translator comment line
1332 1331 self.process('TC', i)
1333 1332 elif line[:2] == '#.':
1334 1333 # we are on a generated comment line
1335 1334 self.process('GC', i)
1336 1335 elif line[:15] == '#| msgid_plural':
1337 1336 # we are on a previous msgid_plural
1338 1337 self.process('PP', i)
1339 1338 elif line[:8] == '#| msgid':
1340 1339 self.process('PM', i)
1341 1340 # we are on a previous msgid
1342 1341 elif line[:10] == '#| msgctxt':
1343 1342 # we are on a previous msgctxt
1344 1343 self.process('PC', i)
1345 1344 i = i+1
1346 1345
1347 1346 if self.current_entry:
1348 1347 # since entries are added when another entry is found, we must add
1349 1348 # the last entry here (only if there are lines)
1350 1349 self.instance.append(self.current_entry)
1351 1350 # before returning the instance, check if there's metadata and if
1352 1351 # so extract it in a dict
1353 1352 firstentry = self.instance[0]
1354 1353 if firstentry.msgid == '': # metadata found
1355 1354 # remove the entry
1356 1355 firstentry = self.instance.pop(0)
1357 1356 self.instance.metadata_is_fuzzy = firstentry.flags
1358 1357 key = None
1359 1358 for msg in firstentry.msgstr.splitlines():
1360 1359 try:
1361 1360 key, val = msg.split(':', 1)
1362 1361 self.instance.metadata[key] = val.strip()
1363 1362 except:
1364 1363 if key is not None:
1365 1364 self.instance.metadata[key] += '\n'+ msg.strip()
1366 1365 # close opened file
1367 1366 self.fhandle.close()
1368 1367 return self.instance
1369 1368
1370 1369 def add(self, symbol, states, next_state):
1371 1370 """
1372 1371 Add a transition to the state machine.
1373 1372 Keywords arguments:
1374 1373
1375 1374 symbol -- string, the matched token (two chars symbol)
1376 1375 states -- list, a list of states (two chars symbols)
1377 1376 next_state -- the next state the fsm will have after the action
1378 1377 """
1379 1378 for state in states:
1380 1379 action = getattr(self, 'handle_%s' % next_state.lower())
1381 1380 self.transitions[(symbol, state)] = (action, next_state)
1382 1381
1383 1382 def process(self, symbol, linenum):
1384 1383 """
1385 1384 Process the transition corresponding to the current state and the
1386 1385 symbol provided.
1387 1386
1388 1387 Keywords arguments:
1389 1388 symbol -- string, the matched token (two chars symbol)
1390 1389 linenum -- integer, the current line number of the parsed file
1391 1390 """
1392 1391 try:
1393 1392 (action, state) = self.transitions[(symbol, self.current_state)]
1394 1393 if action():
1395 1394 self.current_state = state
1396 1395 except Exception, exc:
1397 1396 raise IOError('Syntax error in po file (line %s)' % linenum)
1398 1397
1399 1398 # state handlers
1400 1399
1401 1400 def handle_he(self):
1402 1401 """Handle a header comment."""
1403 1402 if self.instance.header != '':
1404 1403 self.instance.header += '\n'
1405 1404 self.instance.header += self.current_token[2:]
1406 1405 return 1
1407 1406
1408 1407 def handle_tc(self):
1409 1408 """Handle a translator comment."""
1410 1409 if self.current_state in ['MC', 'MS', 'MX']:
1411 1410 self.instance.append(self.current_entry)
1412 1411 self.current_entry = POEntry()
1413 1412 if self.current_entry.tcomment != '':
1414 1413 self.current_entry.tcomment += '\n'
1415 1414 self.current_entry.tcomment += self.current_token[2:]
1416 1415 return True
1417 1416
1418 1417 def handle_gc(self):
1419 1418 """Handle a generated comment."""
1420 1419 if self.current_state in ['MC', 'MS', 'MX']:
1421 1420 self.instance.append(self.current_entry)
1422 1421 self.current_entry = POEntry()
1423 1422 if self.current_entry.comment != '':
1424 1423 self.current_entry.comment += '\n'
1425 1424 self.current_entry.comment += self.current_token[3:]
1426 1425 return True
1427 1426
1428 1427 def handle_oc(self):
1429 1428 """Handle a file:num occurence."""
1430 1429 if self.current_state in ['MC', 'MS', 'MX']:
1431 1430 self.instance.append(self.current_entry)
1432 1431 self.current_entry = POEntry()
1433 1432 occurrences = self.current_token[3:].split()
1434 1433 for occurrence in occurrences:
1435 1434 if occurrence != '':
1436 1435 try:
1437 1436 fil, line = occurrence.split(':')
1438 1437 if not line.isdigit():
1439 1438 fil = fil + line
1440 1439 line = ''
1441 1440 self.current_entry.occurrences.append((fil, line))
1442 1441 except:
1443 1442 self.current_entry.occurrences.append((occurrence, ''))
1444 1443 return True
1445 1444
1446 1445 def handle_fl(self):
1447 1446 """Handle a flags line."""
1448 1447 if self.current_state in ['MC', 'MS', 'MX']:
1449 1448 self.instance.append(self.current_entry)
1450 1449 self.current_entry = POEntry()
1451 1450 self.current_entry.flags += self.current_token[3:].split(', ')
1452 1451 return True
1453 1452
1454 1453 def handle_pp(self):
1455 1454 """Handle a previous msgid_plural line."""
1456 1455 if self.current_state in ['MC', 'MS', 'MX']:
1457 1456 self.instance.append(self.current_entry)
1458 1457 self.current_entry = POEntry()
1459 1458 self.current_entry.previous_msgid_plural = \
1460 1459 unescape(self.current_token[17:-1])
1461 1460 return True
1462 1461
1463 1462 def handle_pm(self):
1464 1463 """Handle a previous msgid line."""
1465 1464 if self.current_state in ['MC', 'MS', 'MX']:
1466 1465 self.instance.append(self.current_entry)
1467 1466 self.current_entry = POEntry()
1468 1467 self.current_entry.previous_msgid = \
1469 1468 unescape(self.current_token[10:-1])
1470 1469 return True
1471 1470
1472 1471 def handle_pc(self):
1473 1472 """Handle a previous msgctxt line."""
1474 1473 if self.current_state in ['MC', 'MS', 'MX']:
1475 1474 self.instance.append(self.current_entry)
1476 1475 self.current_entry = POEntry()
1477 1476 self.current_entry.previous_msgctxt = \
1478 1477 unescape(self.current_token[12:-1])
1479 1478 return True
1480 1479
1481 1480 def handle_ct(self):
1482 1481 """Handle a msgctxt."""
1483 1482 if self.current_state in ['MC', 'MS', 'MX']:
1484 1483 self.instance.append(self.current_entry)
1485 1484 self.current_entry = POEntry()
1486 1485 self.current_entry.msgctxt = unescape(self.current_token[9:-1])
1487 1486 return True
1488 1487
1489 1488 def handle_mi(self):
1490 1489 """Handle a msgid."""
1491 1490 if self.current_state in ['MC', 'MS', 'MX']:
1492 1491 self.instance.append(self.current_entry)
1493 1492 self.current_entry = POEntry()
1494 1493 self.current_entry.obsolete = self.entry_obsolete
1495 1494 self.current_entry.msgid = unescape(self.current_token[7:-1])
1496 1495 return True
1497 1496
1498 1497 def handle_mp(self):
1499 1498 """Handle a msgid plural."""
1500 1499 self.current_entry.msgid_plural = unescape(self.current_token[14:-1])
1501 1500 return True
1502 1501
1503 1502 def handle_ms(self):
1504 1503 """Handle a msgstr."""
1505 1504 self.current_entry.msgstr = unescape(self.current_token[8:-1])
1506 1505 return True
1507 1506
1508 1507 def handle_mx(self):
1509 1508 """Handle a msgstr plural."""
1510 1509 index, value = self.current_token[7], self.current_token[11:-1]
1511 1510 self.current_entry.msgstr_plural[index] = unescape(value)
1512 1511 self.msgstr_index = index
1513 1512 return True
1514 1513
1515 1514 def handle_mc(self):
1516 1515 """Handle a msgid or msgstr continuation line."""
1517 1516 token = unescape(self.current_token[1:-1])
1518 1517 if self.current_state == 'CT':
1519 1518 typ = 'msgctxt'
1520 1519 self.current_entry.msgctxt += token
1521 1520 elif self.current_state == 'MI':
1522 1521 typ = 'msgid'
1523 1522 self.current_entry.msgid += token
1524 1523 elif self.current_state == 'MP':
1525 1524 typ = 'msgid_plural'
1526 1525 self.current_entry.msgid_plural += token
1527 1526 elif self.current_state == 'MS':
1528 1527 typ = 'msgstr'
1529 1528 self.current_entry.msgstr += token
1530 1529 elif self.current_state == 'MX':
1531 1530 typ = 'msgstr[%s]' % self.msgstr_index
1532 1531 self.current_entry.msgstr_plural[self.msgstr_index] += token
1533 1532 elif self.current_state == 'PP':
1534 1533 typ = 'previous_msgid_plural'
1535 1534 token = token[3:]
1536 1535 self.current_entry.previous_msgid_plural += token
1537 1536 elif self.current_state == 'PM':
1538 1537 typ = 'previous_msgid'
1539 1538 token = token[3:]
1540 1539 self.current_entry.previous_msgid += token
1541 1540 elif self.current_state == 'PC':
1542 1541 typ = 'previous_msgctxt'
1543 1542 token = token[3:]
1544 1543 self.current_entry.previous_msgctxt += token
1545 1544 if typ not in self.current_entry._multiline_str:
1546 1545 self.current_entry._multiline_str[typ] = token
1547 1546 else:
1548 1547 self.current_entry._multiline_str[typ] += "__POLIB__NL__" + token
1549 1548 # don't change the current state
1550 1549 return False
1551 1550
1552 1551 # }}}
1553 1552 # class _MOFileParser {{{
1554 1553
1555 1554 class _MOFileParser(object):
1556 1555 """
1557 1556 A class to parse binary mo files.
1558 1557 """
1559 1558 BIG_ENDIAN = 0xde120495
1560 1559 LITTLE_ENDIAN = 0x950412de
1561 1560
1562 1561 def __init__(self, fpath, *args, **kwargs):
1563 1562 """
1564 1563 Constructor.
1565 1564
1566 1565 **Arguments**:
1567 1566 - *fpath*: string, path to the po file
1568 1567 - *encoding*: string, the encoding to use, defaults to
1569 1568 "default_encoding" global variable (optional),
1570 1569 - *check_for_duplicates*: whether to check for duplicate entries
1571 1570 when adding entries to the file, default: False (optional).
1572 1571 """
1573 1572 enc = kwargs.get('encoding', default_encoding)
1574 1573 check_dup = kwargs.get('check_for_duplicates', False)
1575 1574 self.fhandle = open(fpath, 'rb')
1576 1575 self.instance = MOFile(
1577 1576 fpath=fpath,
1578 1577 encoding=enc,
1579 1578 check_for_duplicates=check_dup
1580 1579 )
1581 1580
1582 1581 def parse_magicnumber(self):
1583 1582 """
1584 1583 Parse the magic number and raise an exception if not valid.
1585 1584 """
1586 1585
1587 1586 def parse(self):
1588 1587 """
1589 1588 Build the instance with the file handle provided in the
1590 1589 constructor.
1591 1590 """
1592 1591 magic_number = self._readbinary('<I', 4)
1593 1592 if magic_number == self.LITTLE_ENDIAN:
1594 1593 ii = '<II'
1595 1594 elif magic_number == self.BIG_ENDIAN:
1596 1595 ii = '>II'
1597 1596 else:
1598 1597 raise IOError('Invalid mo file, magic number is incorrect !')
1599 1598 self.instance.magic_number = magic_number
1600 1599 # parse the version number and the number of strings
1601 1600 self.instance.version, numofstrings = self._readbinary(ii, 8)
1602 1601 # original strings and translation strings hash table offset
1603 1602 msgids_hash_offset, msgstrs_hash_offset = self._readbinary(ii, 8)
1604 1603 # move to msgid hash table and read length and offset of msgids
1605 1604 self.fhandle.seek(msgids_hash_offset)
1606 1605 msgids_index = []
1607 1606 for i in range(numofstrings):
1608 1607 msgids_index.append(self._readbinary(ii, 8))
1609 1608 # move to msgstr hash table and read length and offset of msgstrs
1610 1609 self.fhandle.seek(msgstrs_hash_offset)
1611 1610 msgstrs_index = []
1612 1611 for i in range(numofstrings):
1613 1612 msgstrs_index.append(self._readbinary(ii, 8))
1614 1613 # build entries
1615 1614 for i in range(numofstrings):
1616 1615 self.fhandle.seek(msgids_index[i][1])
1617 1616 msgid = self.fhandle.read(msgids_index[i][0])
1618 1617 self.fhandle.seek(msgstrs_index[i][1])
1619 1618 msgstr = self.fhandle.read(msgstrs_index[i][0])
1620 1619 if i == 0: # metadata
1621 1620 raw_metadata, metadata = msgstr.split('\n'), {}
1622 1621 for line in raw_metadata:
1623 1622 tokens = line.split(':', 1)
1624 1623 if tokens[0] != '':
1625 1624 try:
1626 1625 metadata[tokens[0]] = tokens[1].strip()
1627 1626 except IndexError:
1628 1627 metadata[tokens[0]] = ''
1629 1628 self.instance.metadata = metadata
1630 1629 continue
1631 1630 # test if we have a plural entry
1632 1631 msgid_tokens = msgid.split('\0')
1633 1632 if len(msgid_tokens) > 1:
1634 1633 entry = MOEntry(
1635 1634 msgid=msgid_tokens[0],
1636 1635 msgid_plural=msgid_tokens[1],
1637 1636 msgstr_plural=dict((k,v) for k,v in \
1638 1637 enumerate(msgstr.split('\0')))
1639 1638 )
1640 1639 else:
1641 1640 entry = MOEntry(msgid=msgid, msgstr=msgstr)
1642 1641 self.instance.append(entry)
1643 1642 # close opened file
1644 1643 self.fhandle.close()
1645 1644 return self.instance
1646 1645
1647 1646 def _readbinary(self, fmt, numbytes):
1648 1647 """
1649 1648 Private method that unpack n bytes of data using format <fmt>.
1650 1649 It returns a tuple or a mixed value if the tuple length is 1.
1651 1650 """
1652 1651 bytes = self.fhandle.read(numbytes)
1653 1652 tup = struct.unpack(fmt, bytes)
1654 1653 if len(tup) == 1:
1655 1654 return tup[0]
1656 1655 return tup
1657 1656
1658 1657 # }}}
1659 1658 # __main__ {{{
1660 1659
1661 1660 if __name__ == '__main__':
1662 1661 """
1663 1662 **Main function**::
1664 1663 - to **test** the module just run: *python polib.py [-v]*
1665 1664 - to **profile** the module: *python polib.py -p <some_pofile.po>*
1666 1665 """
1667 1666 import sys
1668 1667 if len(sys.argv) > 2 and sys.argv[1] == '-p':
1669 1668 def test(f):
1670 1669 if f.endswith('po'):
1671 1670 p = pofile(f)
1672 1671 else:
1673 1672 p = mofile(f)
1674 1673 s = unicode(p)
1675 1674 import profile
1676 1675 profile.run('test("'+sys.argv[2]+'")')
1677 1676 else:
1678 1677 import doctest
1679 1678 doctest.testmod()
1680 1679
1681 1680 # }}}
@@ -1,392 +1,391 b''
1 #!/usr/bin/env python
2 1 #
3 2 # This is the mercurial setup script.
4 3 #
5 4 # 'python setup.py install', or
6 5 # 'python setup.py --help' for more options
7 6
8 7 import sys
9 8 if not hasattr(sys, 'version_info') or sys.version_info < (2, 4, 0, 'final'):
10 9 raise SystemExit("Mercurial requires Python 2.4 or later.")
11 10
12 11 if sys.version_info[0] >= 3:
13 12 def b(s):
14 13 '''A helper function to emulate 2.6+ bytes literals using string
15 14 literals.'''
16 15 return s.encode('latin1')
17 16 else:
18 17 def b(s):
19 18 '''A helper function to emulate 2.6+ bytes literals using string
20 19 literals.'''
21 20 return s
22 21
23 22 # Solaris Python packaging brain damage
24 23 try:
25 24 import hashlib
26 25 sha = hashlib.sha1()
27 26 except:
28 27 try:
29 28 import sha
30 29 except:
31 30 raise SystemExit(
32 31 "Couldn't import standard hashlib (incomplete Python install).")
33 32
34 33 try:
35 34 import zlib
36 35 except:
37 36 raise SystemExit(
38 37 "Couldn't import standard zlib (incomplete Python install).")
39 38
40 39 try:
41 40 import bz2
42 41 except:
43 42 raise SystemExit(
44 43 "Couldn't import standard bz2 (incomplete Python install).")
45 44
46 45 import os, subprocess, time
47 46 import shutil
48 47 import tempfile
49 48 from distutils import log
50 49 from distutils.core import setup, Extension
51 50 from distutils.dist import Distribution
52 51 from distutils.command.build import build
53 52 from distutils.command.build_ext import build_ext
54 53 from distutils.command.build_py import build_py
55 54 from distutils.command.install_scripts import install_scripts
56 55 from distutils.spawn import spawn, find_executable
57 56 from distutils.ccompiler import new_compiler
58 57 from distutils.errors import CCompilerError
59 58 from distutils.sysconfig import get_python_inc
60 59
61 60 scripts = ['hg']
62 61 if os.name == 'nt':
63 62 scripts.append('contrib/win32/hg.bat')
64 63
65 64 # simplified version of distutils.ccompiler.CCompiler.has_function
66 65 # that actually removes its temporary files.
67 66 def hasfunction(cc, funcname):
68 67 tmpdir = tempfile.mkdtemp(prefix='hg-install-')
69 68 devnull = oldstderr = None
70 69 try:
71 70 try:
72 71 fname = os.path.join(tmpdir, 'funcname.c')
73 72 f = open(fname, 'w')
74 73 f.write('int main(void) {\n')
75 74 f.write(' %s();\n' % funcname)
76 75 f.write('}\n')
77 76 f.close()
78 77 # Redirect stderr to /dev/null to hide any error messages
79 78 # from the compiler.
80 79 # This will have to be changed if we ever have to check
81 80 # for a function on Windows.
82 81 devnull = open('/dev/null', 'w')
83 82 oldstderr = os.dup(sys.stderr.fileno())
84 83 os.dup2(devnull.fileno(), sys.stderr.fileno())
85 84 objects = cc.compile([fname], output_dir=tmpdir)
86 85 cc.link_executable(objects, os.path.join(tmpdir, "a.out"))
87 86 except:
88 87 return False
89 88 return True
90 89 finally:
91 90 if oldstderr is not None:
92 91 os.dup2(oldstderr, sys.stderr.fileno())
93 92 if devnull is not None:
94 93 devnull.close()
95 94 shutil.rmtree(tmpdir)
96 95
97 96 # py2exe needs to be installed to work
98 97 try:
99 98 import py2exe
100 99 py2exeloaded = True
101 100
102 101 # Help py2exe to find win32com.shell
103 102 try:
104 103 import modulefinder
105 104 import win32com
106 105 for p in win32com.__path__[1:]: # Take the path to win32comext
107 106 modulefinder.AddPackagePath("win32com", p)
108 107 pn = "win32com.shell"
109 108 __import__(pn)
110 109 m = sys.modules[pn]
111 110 for p in m.__path__[1:]:
112 111 modulefinder.AddPackagePath(pn, p)
113 112 except ImportError:
114 113 pass
115 114
116 115 except ImportError:
117 116 py2exeloaded = False
118 117 pass
119 118
120 119 def runcmd(cmd, env):
121 120 p = subprocess.Popen(cmd, stdout=subprocess.PIPE,
122 121 stderr=subprocess.PIPE, env=env)
123 122 out, err = p.communicate()
124 123 # If root is executing setup.py, but the repository is owned by
125 124 # another user (as in "sudo python setup.py install") we will get
126 125 # trust warnings since the .hg/hgrc file is untrusted. That is
127 126 # fine, we don't want to load it anyway. Python may warn about
128 127 # a missing __init__.py in mercurial/locale, we also ignore that.
129 128 err = [e for e in err.splitlines()
130 129 if not e.startswith(b('Not trusting file')) \
131 130 and not e.startswith(b('warning: Not importing'))]
132 131 if err:
133 132 return ''
134 133 return out
135 134
136 135 version = ''
137 136
138 137 if os.path.isdir('.hg'):
139 138 # Execute hg out of this directory with a custom environment which
140 139 # includes the pure Python modules in mercurial/pure. We also take
141 140 # care to not use any hgrc files and do no localization.
142 141 pypath = ['mercurial', os.path.join('mercurial', 'pure')]
143 142 env = {'PYTHONPATH': os.pathsep.join(pypath),
144 143 'HGRCPATH': '',
145 144 'LANGUAGE': 'C'}
146 145 if 'LD_LIBRARY_PATH' in os.environ:
147 146 env['LD_LIBRARY_PATH'] = os.environ['LD_LIBRARY_PATH']
148 147 if 'SystemRoot' in os.environ:
149 148 # Copy SystemRoot into the custom environment for Python 2.6
150 149 # under Windows. Otherwise, the subprocess will fail with
151 150 # error 0xc0150004. See: http://bugs.python.org/issue3440
152 151 env['SystemRoot'] = os.environ['SystemRoot']
153 152 cmd = [sys.executable, 'hg', 'id', '-i', '-t']
154 153 l = runcmd(cmd, env).split()
155 154 while len(l) > 1 and l[-1][0].isalpha(): # remove non-numbered tags
156 155 l.pop()
157 156 if len(l) > 1: # tag found
158 157 version = l[-1]
159 158 if l[0].endswith('+'): # propagate the dirty status to the tag
160 159 version += '+'
161 160 elif len(l) == 1: # no tag found
162 161 cmd = [sys.executable, 'hg', 'parents', '--template',
163 162 '{latesttag}+{latesttagdistance}-']
164 163 version = runcmd(cmd, env) + l[0]
165 164 if version.endswith('+'):
166 165 version += time.strftime('%Y%m%d')
167 166 elif os.path.exists('.hg_archival.txt'):
168 167 kw = dict([[t.strip() for t in l.split(':', 1)]
169 168 for l in open('.hg_archival.txt')])
170 169 if 'tag' in kw:
171 170 version = kw['tag']
172 171 elif 'latesttag' in kw:
173 172 version = '%(latesttag)s+%(latesttagdistance)s-%(node).12s' % kw
174 173 else:
175 174 version = kw.get('node', '')[:12]
176 175
177 176 if version:
178 177 f = open("mercurial/__version__.py", "w")
179 178 f.write('# this file is autogenerated by setup.py\n')
180 179 f.write('version = "%s"\n' % version)
181 180 f.close()
182 181
183 182
184 183 try:
185 184 from mercurial import __version__
186 185 version = __version__.version
187 186 except ImportError:
188 187 version = 'unknown'
189 188
190 189 class hgbuildmo(build):
191 190
192 191 description = "build translations (.mo files)"
193 192
194 193 def run(self):
195 194 if not find_executable('msgfmt'):
196 195 self.warn("could not find msgfmt executable, no translations "
197 196 "will be built")
198 197 return
199 198
200 199 podir = 'i18n'
201 200 if not os.path.isdir(podir):
202 201 self.warn("could not find %s/ directory" % podir)
203 202 return
204 203
205 204 join = os.path.join
206 205 for po in os.listdir(podir):
207 206 if not po.endswith('.po'):
208 207 continue
209 208 pofile = join(podir, po)
210 209 modir = join('locale', po[:-3], 'LC_MESSAGES')
211 210 mofile = join(modir, 'hg.mo')
212 211 mobuildfile = join('mercurial', mofile)
213 212 cmd = ['msgfmt', '-v', '-o', mobuildfile, pofile]
214 213 if sys.platform != 'sunos5':
215 214 # msgfmt on Solaris does not know about -c
216 215 cmd.append('-c')
217 216 self.mkpath(join('mercurial', modir))
218 217 self.make_file([pofile], mobuildfile, spawn, (cmd,))
219 218
220 219
221 220 # Insert hgbuildmo first so that files in mercurial/locale/ are found
222 221 # when build_py is run next.
223 222 build.sub_commands.insert(0, ('build_mo', None))
224 223
225 224 Distribution.pure = 0
226 225 Distribution.global_options.append(('pure', None, "use pure (slow) Python "
227 226 "code instead of C extensions"))
228 227
229 228 class hgbuildext(build_ext):
230 229
231 230 def build_extension(self, ext):
232 231 try:
233 232 build_ext.build_extension(self, ext)
234 233 except CCompilerError:
235 234 if not getattr(ext, 'optional', False):
236 235 raise
237 236 log.warn("Failed to build optional extension '%s' (skipping)",
238 237 ext.name)
239 238
240 239 class hgbuildpy(build_py):
241 240
242 241 def finalize_options(self):
243 242 build_py.finalize_options(self)
244 243
245 244 if self.distribution.pure:
246 245 if self.py_modules is None:
247 246 self.py_modules = []
248 247 for ext in self.distribution.ext_modules:
249 248 if ext.name.startswith("mercurial."):
250 249 self.py_modules.append("mercurial.pure.%s" % ext.name[10:])
251 250 self.distribution.ext_modules = []
252 251 else:
253 252 if not os.path.exists(os.path.join(get_python_inc(), 'Python.h')):
254 253 raise SystemExit("Python headers are required to build Mercurial")
255 254
256 255 def find_modules(self):
257 256 modules = build_py.find_modules(self)
258 257 for module in modules:
259 258 if module[0] == "mercurial.pure":
260 259 if module[1] != "__init__":
261 260 yield ("mercurial", module[1], module[2])
262 261 else:
263 262 yield module
264 263
265 264 class hginstallscripts(install_scripts):
266 265 '''
267 266 This is a specialization of install_scripts that replaces the @LIBDIR@ with
268 267 the configured directory for modules. If possible, the path is made relative
269 268 to the directory for scripts.
270 269 '''
271 270
272 271 def initialize_options(self):
273 272 install_scripts.initialize_options(self)
274 273
275 274 self.install_lib = None
276 275
277 276 def finalize_options(self):
278 277 install_scripts.finalize_options(self)
279 278 self.set_undefined_options('install',
280 279 ('install_lib', 'install_lib'))
281 280
282 281 def run(self):
283 282 install_scripts.run(self)
284 283
285 284 if (os.path.splitdrive(self.install_dir)[0] !=
286 285 os.path.splitdrive(self.install_lib)[0]):
287 286 # can't make relative paths from one drive to another, so use an
288 287 # absolute path instead
289 288 libdir = self.install_lib
290 289 else:
291 290 common = os.path.commonprefix((self.install_dir, self.install_lib))
292 291 rest = self.install_dir[len(common):]
293 292 uplevel = len([n for n in os.path.split(rest) if n])
294 293
295 294 libdir = uplevel * ('..' + os.sep) + self.install_lib[len(common):]
296 295
297 296 for outfile in self.outfiles:
298 297 data = open(outfile, 'rb').read()
299 298
300 299 # skip binary files
301 300 if '\0' in data:
302 301 continue
303 302
304 303 data = data.replace('@LIBDIR@', libdir.encode('string_escape'))
305 304 open(outfile, 'wb').write(data)
306 305
307 306 cmdclass = {'build_mo': hgbuildmo,
308 307 'build_ext': hgbuildext,
309 308 'build_py': hgbuildpy,
310 309 'install_scripts': hginstallscripts}
311 310
312 311 packages = ['mercurial', 'mercurial.hgweb', 'hgext', 'hgext.convert',
313 312 'hgext.highlight', 'hgext.zeroconf']
314 313
315 314 pymodules = []
316 315
317 316 extmodules = [
318 317 Extension('mercurial.base85', ['mercurial/base85.c']),
319 318 Extension('mercurial.bdiff', ['mercurial/bdiff.c']),
320 319 Extension('mercurial.diffhelpers', ['mercurial/diffhelpers.c']),
321 320 Extension('mercurial.mpatch', ['mercurial/mpatch.c']),
322 321 Extension('mercurial.parsers', ['mercurial/parsers.c']),
323 322 ]
324 323
325 324 # disable osutil.c under windows + python 2.4 (issue1364)
326 325 if sys.platform == 'win32' and sys.version_info < (2, 5, 0, 'final'):
327 326 pymodules.append('mercurial.pure.osutil')
328 327 else:
329 328 extmodules.append(Extension('mercurial.osutil', ['mercurial/osutil.c']))
330 329
331 330 if sys.platform == 'linux2' and os.uname()[2] > '2.6':
332 331 # The inotify extension is only usable with Linux 2.6 kernels.
333 332 # You also need a reasonably recent C library.
334 333 # In any case, if it fails to build the error will be skipped ('optional').
335 334 cc = new_compiler()
336 335 if hasfunction(cc, 'inotify_add_watch'):
337 336 inotify = Extension('hgext.inotify.linux._inotify',
338 337 ['hgext/inotify/linux/_inotify.c'],
339 338 ['mercurial'])
340 339 inotify.optional = True
341 340 extmodules.append(inotify)
342 341 packages.extend(['hgext.inotify', 'hgext.inotify.linux'])
343 342
344 343 packagedata = {'mercurial': ['locale/*/LC_MESSAGES/hg.mo',
345 344 'help/*.txt']}
346 345
347 346 def ordinarypath(p):
348 347 return p and p[0] != '.' and p[-1] != '~'
349 348
350 349 for root in ('templates',):
351 350 for curdir, dirs, files in os.walk(os.path.join('mercurial', root)):
352 351 curdir = curdir.split(os.sep, 1)[1]
353 352 dirs[:] = filter(ordinarypath, dirs)
354 353 for f in filter(ordinarypath, files):
355 354 f = os.path.join(curdir, f)
356 355 packagedata['mercurial'].append(f)
357 356
358 357 datafiles = []
359 358 setupversion = version
360 359 extra = {}
361 360
362 361 if py2exeloaded:
363 362 extra['console'] = [
364 363 {'script':'hg',
365 364 'copyright':'Copyright (C) 2005-2010 Matt Mackall and others',
366 365 'product_version':version}]
367 366
368 367 if os.name == 'nt':
369 368 # Windows binary file versions for exe/dll files must have the
370 369 # form W.X.Y.Z, where W,X,Y,Z are numbers in the range 0..65535
371 370 setupversion = version.split('+', 1)[0]
372 371
373 372 setup(name='mercurial',
374 373 version=setupversion,
375 374 author='Matt Mackall',
376 375 author_email='mpm@selenic.com',
377 376 url='http://mercurial.selenic.com/',
378 377 description='Scalable distributed SCM',
379 378 license='GNU GPLv2+',
380 379 scripts=scripts,
381 380 packages=packages,
382 381 py_modules=pymodules,
383 382 ext_modules=extmodules,
384 383 data_files=datafiles,
385 384 package_data=packagedata,
386 385 cmdclass=cmdclass,
387 386 options=dict(py2exe=dict(packages=['hgext', 'email']),
388 387 bdist_mpkg=dict(zipdist=True,
389 388 license='COPYING',
390 389 readme='contrib/macosx/Readme.html',
391 390 welcome='contrib/macosx/Welcome.html')),
392 391 **extra)
@@ -1,54 +1,52 b''
1 #!/usr/bin/env python
2
3 1 import struct
4 2 from mercurial import bdiff, mpatch
5 3
6 4 def test1(a, b):
7 5 d = bdiff.bdiff(a, b)
8 6 c = a
9 7 if d:
10 8 c = mpatch.patches(a, [d])
11 9 if c != b:
12 10 print "***", repr(a), repr(b)
13 11 print "bad:"
14 12 print repr(c)[:200]
15 13 print repr(d)
16 14
17 15 def test(a, b):
18 16 print "***", repr(a), repr(b)
19 17 test1(a, b)
20 18 test1(b, a)
21 19
22 20 test("a\nc\n\n\n\n", "a\nb\n\n\n")
23 21 test("a\nb\nc\n", "a\nc\n")
24 22 test("", "")
25 23 test("a\nb\nc", "a\nb\nc")
26 24 test("a\nb\nc\nd\n", "a\nd\n")
27 25 test("a\nb\nc\nd\n", "a\nc\ne\n")
28 26 test("a\nb\nc\n", "a\nc\n")
29 27 test("a\n", "c\na\nb\n")
30 28 test("a\n", "")
31 29 test("a\n", "b\nc\n")
32 30 test("a\n", "c\na\n")
33 31 test("", "adjfkjdjksdhfksj")
34 32 test("", "ab")
35 33 test("", "abc")
36 34 test("a", "a")
37 35 test("ab", "ab")
38 36 test("abc", "abc")
39 37 test("a\n", "a\n")
40 38 test("a\nb", "a\nb")
41 39
42 40 #issue1295
43 41 def showdiff(a, b):
44 42 bin = bdiff.bdiff(a, b)
45 43 pos = 0
46 44 while pos < len(bin):
47 45 p1, p2, l = struct.unpack(">lll", bin[pos:pos + 12])
48 46 pos += 12
49 47 print p1, p2, repr(bin[pos:pos + l])
50 48 pos += l
51 49 showdiff("x\n\nx\n\nx\n\nx\n\nz\n", "x\n\nx\n\ny\n\nx\n\nx\n\nz\n")
52 50 showdiff("x\n\nx\n\nx\n\nx\n\nz\n", "x\n\nx\n\ny\n\nx\n\ny\n\nx\n\nz\n")
53 51
54 52 print "done"
@@ -1,40 +1,38 b''
1 #!/usr/bin/env python
2
3 1 from mercurial import demandimport
4 2 demandimport.enable()
5 3
6 4 import re
7 5
8 6 rsub = re.sub
9 7 def f(obj):
10 8 l = repr(obj)
11 9 l = rsub("0x[0-9a-fA-F]+", "0x?", l)
12 10 l = rsub("from '.*'", "from '?'", l)
13 11 return l
14 12
15 13 import os
16 14
17 15 print "os =", f(os)
18 16 print "os.system =", f(os.system)
19 17 print "os =", f(os)
20 18
21 19 from mercurial import util
22 20
23 21 print "util =", f(util)
24 22 print "util.system =", f(util.system)
25 23 print "util =", f(util)
26 24 print "util.system =", f(util.system)
27 25
28 26 import re as fred
29 27 print "fred =", f(fred)
30 28
31 29 import sys as re
32 30 print "re =", f(re)
33 31
34 32 print "fred =", f(fred)
35 33 print "fred.sub =", f(fred.sub)
36 34 print "fred =", f(fred)
37 35
38 36 print "re =", f(re)
39 37 print "re.stderr =", f(re.stderr)
40 38 print "re =", f(re)
@@ -1,12 +1,10 b''
1 #!/usr/bin/env python
2
3 1 from mercurial.hg import parseurl
4 2
5 3 def testparse(url, branch=[]):
6 4 print '%s, branches: %r' % parseurl(url, branch)
7 5
8 6 testparse('http://example.com/no/anchor')
9 7 testparse('http://example.com/an/anchor#foo')
10 8 testparse('http://example.com/no/anchor/branches', branch=['foo'])
11 9 testparse('http://example.com/an/anchor/branches#bar', branch=['foo'])
12 10 testparse('http://example.com/an/anchor/branches-None#foo', branch=None)
@@ -1,29 +1,27 b''
1 #!/usr/bin/env python
2
3 1 from mercurial import store
4 2
5 3 auxencode = lambda f: store._auxencode(f, True)
6 4 hybridencode = lambda f: store._hybridencode(f, auxencode)
7 5
8 6 enc = hybridencode # used for 'dotencode' repo format
9 7
10 8 def show(s):
11 9 print "A = '%s'" % s
12 10 print "B = '%s'" % enc(s)
13 11 print
14 12
15 13 show('data/aux.bla/bla.aux/prn/PRN/lpt/com3/nul/coma/foo.NUL/normal.c.i')
16 14
17 15 show('data/AUX/SECOND/X.PRN/FOURTH/FI:FTH/SIXTH/SEVENTH/EIGHTH/NINETH/'
18 16 'TENTH/ELEVENTH/LOREMIPSUM.TXT.i')
19 17 show('data/enterprise/openesbaddons/contrib-imola/corba-bc/netbeansplugin/'
20 18 'wsdlExtension/src/main/java/META-INF/services/org.netbeans.modules'
21 19 '.xml.wsdl.bindingsupport.spi.ExtensibilityElementTemplateProvider.i')
22 20 show('data/AUX.THE-QUICK-BROWN-FOX-JU:MPS-OVER-THE-LAZY-DOG-THE-QUICK-'
23 21 'BROWN-FOX-JUMPS-OVER-THE-LAZY-DOG.TXT.i')
24 22 show('data/Project Planning/Resources/AnotherLongDirectoryName/'
25 23 'Followedbyanother/AndAnother/AndThenAnExtremelyLongFileName.txt')
26 24 show('data/Project.Planning/Resources/AnotherLongDirectoryName/'
27 25 'Followedbyanother/AndAnother/AndThenAnExtremelyLongFileName.txt')
28 26 show('data/foo.../foo / /a./_. /__/.x../ bla/.FOO/something.i')
29 27
@@ -1,228 +1,226 b''
1 #!/usr/bin/env python
2
3 1 from pprint import pprint
4 2 from mercurial import minirst
5 3
6 4 def debugformat(title, text, width, **kwargs):
7 5 print "%s formatted to fit within %d characters:" % (title, width)
8 6 print "-" * 70
9 7 formatted = minirst.format(text, width, **kwargs)
10 8 if type(formatted) == tuple:
11 9 print formatted[0]
12 10 print "-" * 70
13 11 pprint(formatted[1])
14 12 else:
15 13 print formatted
16 14 print "-" * 70
17 15 print
18 16
19 17 paragraphs = """
20 18 This is some text in the first paragraph.
21 19
22 20 A small indented paragraph.
23 21 It is followed by some lines
24 22 containing random whitespace.
25 23 \n \n \nThe third and final paragraph.
26 24 """
27 25
28 26 debugformat('paragraphs', paragraphs, 60)
29 27 debugformat('paragraphs', paragraphs, 30)
30 28
31 29
32 30 definitions = """
33 31 A Term
34 32 Definition. The indented
35 33 lines make up the definition.
36 34 Another Term
37 35 Another definition. The final line in the
38 36 definition determines the indentation, so
39 37 this will be indented with four spaces.
40 38
41 39 A Nested/Indented Term
42 40 Definition.
43 41 """
44 42
45 43 debugformat('definitions', definitions, 60)
46 44 debugformat('definitions', definitions, 30)
47 45
48 46
49 47 literals = r"""
50 48 The fully minimized form is the most
51 49 convenient form::
52 50
53 51 Hello
54 52 literal
55 53 world
56 54
57 55 In the partially minimized form a paragraph
58 56 simply ends with space-double-colon. ::
59 57
60 58 ////////////////////////////////////////
61 59 long un-wrapped line in a literal block
62 60 \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\
63 61
64 62 ::
65 63
66 64 This literal block is started with '::',
67 65 the so-called expanded form. The paragraph
68 66 with '::' disappears in the final output.
69 67 """
70 68
71 69 debugformat('literals', literals, 60)
72 70 debugformat('literals', literals, 30)
73 71
74 72
75 73 lists = """
76 74 - This is the first list item.
77 75
78 76 Second paragraph in the first list item.
79 77
80 78 - List items need not be separated
81 79 by a blank line.
82 80 - And will be rendered without
83 81 one in any case.
84 82
85 83 We can have indented lists:
86 84
87 85 - This is an indented list item
88 86
89 87 - Another indented list item::
90 88
91 89 - A literal block in the middle
92 90 of an indented list.
93 91
94 92 (The above is not a list item since we are in the literal block.)
95 93
96 94 ::
97 95
98 96 Literal block with no indentation (apart from
99 97 the two spaces added to all literal blocks).
100 98
101 99 1. This is an enumerated list (first item).
102 100 2. Continuing with the second item.
103 101
104 102 (1) foo
105 103 (2) bar
106 104
107 105 1) Another
108 106 2) List
109 107
110 108 Line blocks are also a form of list:
111 109
112 110 | This is the first line.
113 111 The line continues here.
114 112 | This is the second line.
115 113 """
116 114
117 115 debugformat('lists', lists, 60)
118 116 debugformat('lists', lists, 30)
119 117
120 118
121 119 options = """
122 120 There is support for simple option lists,
123 121 but only with long options:
124 122
125 123 --all Output all.
126 124 --both Output both (this description is
127 125 quite long).
128 126 --long Output all day long.
129 127
130 128 --par This option has two paragraphs in its description.
131 129 This is the first.
132 130
133 131 This is the second. Blank lines may be omitted between
134 132 options (as above) or left in (as here).
135 133
136 134 The next paragraph looks like an option list, but lacks the two-space
137 135 marker after the option. It is treated as a normal paragraph:
138 136
139 137 --foo bar baz
140 138 """
141 139
142 140 debugformat('options', options, 60)
143 141 debugformat('options', options, 30)
144 142
145 143
146 144 fields = """
147 145 :a: First item.
148 146 :ab: Second item. Indentation and wrapping
149 147 is handled automatically.
150 148
151 149 Next list:
152 150
153 151 :small: The larger key below triggers full indentation here.
154 152 :much too large: This key is big enough to get its own line.
155 153 """
156 154
157 155 debugformat('fields', fields, 60)
158 156 debugformat('fields', fields, 30)
159 157
160 158 containers = """
161 159 Normal output.
162 160
163 161 .. container:: debug
164 162
165 163 Initial debug output.
166 164
167 165 .. container:: verbose
168 166
169 167 Verbose output.
170 168
171 169 .. container:: debug
172 170
173 171 Debug output.
174 172 """
175 173
176 174 debugformat('containers (normal)', containers, 60)
177 175 debugformat('containers (verbose)', containers, 60, keep=['verbose'])
178 176 debugformat('containers (debug)', containers, 60, keep=['debug'])
179 177 debugformat('containers (verbose debug)', containers, 60,
180 178 keep=['verbose', 'debug'])
181 179
182 180 roles = """Please see :hg:`add`."""
183 181 debugformat('roles', roles, 60)
184 182
185 183
186 184 sections = """
187 185 Title
188 186 =====
189 187
190 188 Section
191 189 -------
192 190
193 191 Subsection
194 192 ''''''''''
195 193
196 194 Markup: ``foo`` and :hg:`help`
197 195 ------------------------------
198 196 """
199 197 debugformat('sections', sections, 20)
200 198
201 199
202 200 admonitions = """
203 201 .. note::
204 202 This is a note
205 203
206 204 - Bullet 1
207 205 - Bullet 2
208 206
209 207 .. warning:: This is a warning Second
210 208 input line of warning
211 209
212 210 .. danger::
213 211 This is danger
214 212 """
215 213
216 214 debugformat('admonitions', admonitions, 30)
217 215
218 216 comments = """
219 217 Some text.
220 218
221 219 .. A comment
222 220
223 221 .. An indented comment
224 222
225 223 Some indented text.
226 224 """
227 225
228 226 debugformat('comments', comments, 30)
@@ -1,11 +1,9 b''
1 #!/usr/bin/env python
2
3 1 from hgext import color
4 2
5 3 # ensure errors aren't buffered
6 4 testui = color.colorui()
7 5 testui.pushbuffer()
8 6 testui.write('buffered\n')
9 7 testui.warn('warning\n')
10 8 testui.write_err('error\n')
11 9 print repr(testui.popbuffer())
@@ -1,83 +1,81 b''
1 #!/usr/bin/env python
2
3 1 from mercurial import ui, dispatch, error
4 2
5 3 testui = ui.ui()
6 4 parsed = dispatch._parseconfig(testui, [
7 5 'values.string=string value',
8 6 'values.bool1=true',
9 7 'values.bool2=false',
10 8 'lists.list1=foo',
11 9 'lists.list2=foo bar baz',
12 10 'lists.list3=alice, bob',
13 11 'lists.list4=foo bar baz alice, bob',
14 12 'lists.list5=abc d"ef"g "hij def"',
15 13 'lists.list6="hello world", "how are you?"',
16 14 'lists.list7=Do"Not"Separate',
17 15 'lists.list8="Do"Separate',
18 16 'lists.list9="Do\\"NotSeparate"',
19 17 'lists.list10=string "with extraneous" quotation mark"',
20 18 'lists.list11=x, y',
21 19 'lists.list12="x", "y"',
22 20 'lists.list13=""" key = "x", "y" """',
23 21 'lists.list14=,,,, ',
24 22 'lists.list15=" just with starting quotation',
25 23 'lists.list16="longer quotation" with "no ending quotation',
26 24 'lists.list17=this is \\" "not a quotation mark"',
27 25 'lists.list18=\n \n\nding\ndong',
28 26 ])
29 27
30 28 print repr(testui.configitems('values'))
31 29 print repr(testui.configitems('lists'))
32 30 print "---"
33 31 print repr(testui.config('values', 'string'))
34 32 print repr(testui.config('values', 'bool1'))
35 33 print repr(testui.config('values', 'bool2'))
36 34 print repr(testui.config('values', 'unknown'))
37 35 print "---"
38 36 try:
39 37 print repr(testui.configbool('values', 'string'))
40 38 except error.ConfigError, inst:
41 39 print inst
42 40 print repr(testui.configbool('values', 'bool1'))
43 41 print repr(testui.configbool('values', 'bool2'))
44 42 print repr(testui.configbool('values', 'bool2', True))
45 43 print repr(testui.configbool('values', 'unknown'))
46 44 print repr(testui.configbool('values', 'unknown', True))
47 45 print "---"
48 46 print repr(testui.configlist('lists', 'list1'))
49 47 print repr(testui.configlist('lists', 'list2'))
50 48 print repr(testui.configlist('lists', 'list3'))
51 49 print repr(testui.configlist('lists', 'list4'))
52 50 print repr(testui.configlist('lists', 'list4', ['foo']))
53 51 print repr(testui.configlist('lists', 'list5'))
54 52 print repr(testui.configlist('lists', 'list6'))
55 53 print repr(testui.configlist('lists', 'list7'))
56 54 print repr(testui.configlist('lists', 'list8'))
57 55 print repr(testui.configlist('lists', 'list9'))
58 56 print repr(testui.configlist('lists', 'list10'))
59 57 print repr(testui.configlist('lists', 'list11'))
60 58 print repr(testui.configlist('lists', 'list12'))
61 59 print repr(testui.configlist('lists', 'list13'))
62 60 print repr(testui.configlist('lists', 'list14'))
63 61 print repr(testui.configlist('lists', 'list15'))
64 62 print repr(testui.configlist('lists', 'list16'))
65 63 print repr(testui.configlist('lists', 'list17'))
66 64 print repr(testui.configlist('lists', 'list18'))
67 65 print repr(testui.configlist('lists', 'unknown'))
68 66 print repr(testui.configlist('lists', 'unknown', ''))
69 67 print repr(testui.configlist('lists', 'unknown', 'foo'))
70 68 print repr(testui.configlist('lists', 'unknown', ['foo']))
71 69 print repr(testui.configlist('lists', 'unknown', 'foo bar'))
72 70 print repr(testui.configlist('lists', 'unknown', 'foo, bar'))
73 71 print repr(testui.configlist('lists', 'unknown', ['foo bar']))
74 72 print repr(testui.configlist('lists', 'unknown', ['foo', 'bar']))
75 73
76 74 print repr(testui.config('values', 'String'))
77 75
78 76 def function():
79 77 pass
80 78
81 79 # values that aren't strings should work
82 80 testui.setconfig('hook', 'commit', function)
83 81 print function == testui.config('hook', 'commit')
@@ -1,49 +1,47 b''
1 #!/usr/bin/env python
2
3 1 import os
4 2 from mercurial import ui
5 3
6 4 hgrc = os.environ['HGRCPATH']
7 5 f = open(hgrc)
8 6 basehgrc = f.read()
9 7 f.close()
10 8
11 9 print ' hgrc settings command line options final result '
12 10 print ' quiet verbo debug quiet verbo debug quiet verbo debug'
13 11
14 12 for i in xrange(64):
15 13 hgrc_quiet = bool(i & 1<<0)
16 14 hgrc_verbose = bool(i & 1<<1)
17 15 hgrc_debug = bool(i & 1<<2)
18 16 cmd_quiet = bool(i & 1<<3)
19 17 cmd_verbose = bool(i & 1<<4)
20 18 cmd_debug = bool(i & 1<<5)
21 19
22 20 f = open(hgrc, 'w')
23 21 f.write(basehgrc)
24 22 f.write('\n[ui]\n')
25 23 if hgrc_quiet:
26 24 f.write('quiet = True\n')
27 25 if hgrc_verbose:
28 26 f.write('verbose = True\n')
29 27 if hgrc_debug:
30 28 f.write('debug = True\n')
31 29 f.close()
32 30
33 31 u = ui.ui()
34 32 if cmd_quiet or cmd_debug or cmd_verbose:
35 33 u.setconfig('ui', 'quiet', str(bool(cmd_quiet)))
36 34 u.setconfig('ui', 'verbose', str(bool(cmd_verbose)))
37 35 u.setconfig('ui', 'debug', str(bool(cmd_debug)))
38 36
39 37 check = ''
40 38 if u.debugflag:
41 39 if not u.verbose or u.quiet:
42 40 check = ' *'
43 41 elif u.verbose and u.quiet:
44 42 check = ' +'
45 43
46 44 print ('%2d %5s %5s %5s %5s %5s %5s -> %5s %5s %5s%s'
47 45 % (i, hgrc_quiet, hgrc_verbose, hgrc_debug,
48 46 cmd_quiet, cmd_verbose, cmd_debug,
49 47 u.quiet, u.verbose, u.debugflag, check))
@@ -1,39 +1,38 b''
1 #!/usr/bin/env python
2 1 import sys
3 2
4 3 def check(a, b):
5 4 if a != b:
6 5 print (a, b)
7 6
8 7 def cert(cn):
9 8 return dict(subject=((('commonName', cn),),))
10 9
11 10 from mercurial.url import _verifycert
12 11
13 12 # Test non-wildcard certificates
14 13 check(_verifycert(cert('example.com'), 'example.com'),
15 14 None)
16 15 check(_verifycert(cert('example.com'), 'www.example.com'),
17 16 'certificate is for example.com')
18 17 check(_verifycert(cert('www.example.com'), 'example.com'),
19 18 'certificate is for www.example.com')
20 19
21 20 # Test wildcard certificates
22 21 check(_verifycert(cert('*.example.com'), 'www.example.com'),
23 22 None)
24 23 check(_verifycert(cert('*.example.com'), 'example.com'),
25 24 'certificate is for *.example.com')
26 25 check(_verifycert(cert('*.example.com'), 'w.w.example.com'),
27 26 'certificate is for *.example.com')
28 27
29 28 # Avoid some pitfalls
30 29 check(_verifycert(cert('*.foo'), 'foo'),
31 30 'certificate is for *.foo')
32 31 check(_verifycert(cert('*o'), 'foo'),
33 32 'certificate is for *o')
34 33
35 34 check(_verifycert({'subject': ()},
36 35 'example.com'),
37 36 'no commonName found in certificate')
38 37 check(_verifycert(None, 'example.com'),
39 38 'no certificate received')
General Comments 0
You need to be logged in to leave comments. Login now