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