import array
import codecs
import os
import re
import struct
import sys
import textwrap
import io
__author__ = 'David Jean Louis <izimobil@gmail.com>'
__version__ = '1.2.0'
__all__ = ['pofile', 'POFile', 'POEntry', 'mofile', 'MOFile', 'MOEntry',
'default_encoding', 'escape', 'unescape', 'detect_encoding', ]
default_encoding = 'utf-8'
if sys.version_info < (3,):
PY3 = False
text_type = unicode
def b(s):
return s
def u(s):
return unicode(s, "unicode_escape")
else:
PY3 = True
text_type = str
def b(s):
return s.encode("latin-1")
def u(s):
return s
def _pofile_or_mofile(f, type, **kwargs):
enc = kwargs.get('encoding')
if enc is None:
enc = detect_encoding(f, type == 'mofile')
kls = type == 'pofile' and _POFileParser or _MOFileParser
parser = kls(
f,
encoding=enc,
check_for_duplicates=kwargs.get('check_for_duplicates', False),
klass=kwargs.get('klass')
)
instance = parser.parse()
instance.wrapwidth = kwargs.get('wrapwidth', 78)
return instance
def _is_file(filename_or_contents):
try:
return os.path.isfile(filename_or_contents)
except (TypeError, ValueError, UnicodeEncodeError):
return False
def pofile(pofile, **kwargs):
return _pofile_or_mofile(pofile, 'pofile', **kwargs)
def mofile(mofile, **kwargs):
return _pofile_or_mofile(mofile, 'mofile', **kwargs)
def detect_encoding(file, binary_mode=False):
PATTERN = r'"?Content-Type:.+? charset=([\w_\-:\.]+)'
rxt = re.compile(u(PATTERN))
rxb = re.compile(b(PATTERN))
def charset_exists(charset):
try:
codecs.lookup(charset)
except LookupError:
return False
return True
if not _is_file(file):
try:
match = rxt.search(file)
except TypeError:
match = rxb.search(file)
if match:
enc = match.group(1).strip()
if not isinstance(enc, text_type):
enc = enc.decode('utf-8')
if charset_exists(enc):
return enc
else:
if binary_mode or PY3:
mode = 'rb'
rx = rxb
else:
mode = 'r'
rx = rxt
with open(file, mode) as f:
for line in f.readlines():
match = rx.search(line)
if match:
f.close()
enc = match.group(1).strip()
if not isinstance(enc, text_type):
enc = enc.decode('utf-8')
if charset_exists(enc):
return enc
return default_encoding
def escape(st):
return st.replace('\\', r'\\')\
.replace('\t', r'\t')\
.replace('\r', r'\r')\
.replace('\n', r'\n')\
.replace('\v', r'\v')\
.replace('\b', r'\b')\
.replace('\f', r'\f')\
.replace('\"', r'\"')
def unescape(st):
def unescape_repl(m):
m = m.group(1)
if m == 'n':
return '\n'
if m == 't':
return '\t'
if m == 'r':
return '\r'
if m == 'v':
return '\v'
if m == 'b':
return '\b'
if m == 'f':
return '\f'
if m == '\\':
return '\\'
return m return re.sub(r'\\(\\|n|t|r|v|b|f|")', unescape_repl, st)
def natural_sort(lst):
def convert(text):
return int(text) if text.isdigit() else text.lower()
def alphanum_key(key):
return [convert(c) for c in re.split('([0-9]+)', key)]
return sorted(lst, key=alphanum_key)
class _BaseFile(list):
def __init__(self, *args, **kwargs):
list.__init__(self)
pofile = kwargs.get('pofile', None)
if pofile and _is_file(pofile):
self.fpath = pofile
else:
self.fpath = kwargs.get('fpath')
self.wrapwidth = kwargs.get('wrapwidth', 78)
self.encoding = kwargs.get('encoding', default_encoding)
self.check_for_duplicates = kwargs.get('check_for_duplicates', False)
self.header = ''
self.metadata = {}
self.metadata_is_fuzzy = 0
def __unicode__(self):
ret = []
entries = [self.metadata_as_entry()] + \
[e for e in self if not e.obsolete]
for entry in entries:
ret.append(entry.__unicode__(self.wrapwidth))
for entry in self.obsolete_entries():
ret.append(entry.__unicode__(self.wrapwidth))
ret = u('\n').join(ret)
return ret
if PY3:
def __str__(self):
return self.__unicode__()
else:
def __str__(self):
return unicode(self).encode(self.encoding)
def __contains__(self, entry):
return self.find(entry.msgid, by='msgid', msgctxt=entry.msgctxt) \
is not None
def __eq__(self, other):
return str(self) == str(other)
def append(self, entry):
if getattr(self, 'check_for_duplicates', False) and entry in self:
raise ValueError('Entry "%s" already exists' % entry.msgid)
super(_BaseFile, self).append(entry)
def insert(self, index, entry):
if self.check_for_duplicates and entry in self:
raise ValueError('Entry "%s" already exists' % entry.msgid)
super(_BaseFile, self).insert(index, entry)
def metadata_as_entry(self):
e = POEntry(msgid='')
mdata = self.ordered_metadata()
if mdata:
strs = []
for name, value in mdata:
strs.append('%s: %s' % (name, value))
e.msgstr = '\n'.join(strs) + '\n'
if self.metadata_is_fuzzy:
e.flags.append('fuzzy')
return e
def save(self, fpath=None, repr_method='__unicode__', newline=None):
if self.fpath is None and fpath is None:
raise IOError('You must provide a file path to save() method')
contents = getattr(self, repr_method)()
if fpath is None:
fpath = self.fpath
if repr_method == 'to_binary':
with open(fpath, 'wb') as fhandle:
fhandle.write(contents)
else:
with io.open(
fpath,
'w',
encoding=self.encoding,
newline=newline
) as fhandle:
if not isinstance(contents, text_type):
contents = contents.decode(self.encoding)
fhandle.write(contents)
if self.fpath is None and fpath:
self.fpath = fpath
def find(self, st, by='msgid', include_obsolete_entries=False,
msgctxt=False):
if include_obsolete_entries:
entries = self[:]
else:
entries = [e for e in self if not e.obsolete]
matches = []
for e in entries:
if getattr(e, by) == st:
if msgctxt is not False and e.msgctxt != msgctxt:
continue
matches.append(e)
if len(matches) == 1:
return matches[0]
elif len(matches) > 1:
if not msgctxt:
e = None
for m in matches:
if not m.msgctxt:
e = m
if e:
return e
return matches[0]
return None
def ordered_metadata(self):
metadata = self.metadata.copy()
data_order = [
'Project-Id-Version',
'Report-Msgid-Bugs-To',
'POT-Creation-Date',
'PO-Revision-Date',
'Last-Translator',
'Language-Team',
'Language',
'MIME-Version',
'Content-Type',
'Content-Transfer-Encoding',
'Plural-Forms'
]
ordered_data = []
for data in data_order:
try:
value = metadata.pop(data)
ordered_data.append((data, value))
except KeyError:
pass
for data in natural_sort(metadata.keys()):
value = metadata[data]
ordered_data.append((data, value))
return ordered_data
def to_binary(self):
offsets = []
entries = self.translated_entries()
def cmp(_self, other):
self_msgid = _self.msgctxt and _self.msgctxt or _self.msgid
other_msgid = other.msgctxt and other.msgctxt or other.msgid
if self_msgid > other_msgid:
return 1
elif self_msgid < other_msgid:
return -1
else:
return 0
entries.sort(key=lambda o: o.msgid_with_context.encode('utf-8'))
mentry = self.metadata_as_entry()
entries = [mentry] + entries
entries_len = len(entries)
ids, strs = b(''), b('')
for e in entries:
msgid = b('')
if e.msgctxt:
msgid = self._encode(e.msgctxt + '\4')
if e.msgid_plural:
msgstr = []
for index in sorted(e.msgstr_plural.keys()):
msgstr.append(e.msgstr_plural[index])
msgid += self._encode(e.msgid + '\0' + e.msgid_plural)
msgstr = self._encode('\0'.join(msgstr))
else:
msgid += self._encode(e.msgid)
msgstr = self._encode(e.msgstr)
offsets.append((len(ids), len(msgid), len(strs), len(msgstr)))
ids += msgid + b('\0')
strs += msgstr + b('\0')
keystart = 7 * 4 + 16 * entries_len
valuestart = keystart + len(ids)
koffsets = []
voffsets = []
for o1, l1, o2, l2 in offsets:
koffsets += [l1, o1 + keystart]
voffsets += [l2, o2 + valuestart]
offsets = koffsets + voffsets
output = struct.pack(
"Iiiiiii",
MOFile.MAGIC,
0,
entries_len,
7 * 4,
7 * 4 + entries_len * 8,
0, keystart
)
if PY3 and sys.version_info.minor > 1: output += array.array("i", offsets).tobytes()
else:
output += array.array("i", offsets).tostring()
output += ids
output += strs
return output
def _encode(self, mixed):
if isinstance(mixed, text_type):
mixed = mixed.encode(self.encoding)
return mixed
class POFile(_BaseFile):
def __unicode__(self):
ret, headers = '', self.header.split('\n')
for header in headers:
if not len(header):
ret += "#\n"
elif header[:1] in [',', ':']:
ret += '#%s\n' % header
else:
ret += '# %s\n' % header
if not isinstance(ret, text_type):
ret = ret.decode(self.encoding)
return ret + _BaseFile.__unicode__(self)
def save_as_mofile(self, fpath):
_BaseFile.save(self, fpath, 'to_binary')
def percent_translated(self):
total = len([e for e in self if not e.obsolete])
if total == 0:
return 100
translated = len(self.translated_entries())
return int(translated * 100 / float(total))
def translated_entries(self):
return [e for e in self if e.translated()]
def untranslated_entries(self):
return [e for e in self if not e.translated() and not e.obsolete
and not e.fuzzy]
def fuzzy_entries(self):
return [e for e in self if e.fuzzy and not e.obsolete]
def obsolete_entries(self):
return [e for e in self if e.obsolete]
def merge(self, refpot):
self_entries = dict(
(entry.msgid_with_context, entry) for entry in self
)
refpot_msgids = set(entry.msgid_with_context for entry in refpot)
for entry in refpot:
e = self_entries.get(entry.msgid_with_context)
if e is None:
e = POEntry()
self.append(e)
e.merge(entry)
for entry in self:
if entry.msgid_with_context not in refpot_msgids:
entry.obsolete = True
class MOFile(_BaseFile):
MAGIC = 0x950412de
MAGIC_SWAPPED = 0xde120495
def __init__(self, *args, **kwargs):
_BaseFile.__init__(self, *args, **kwargs)
self.magic_number = None
self.version = 0
def save_as_pofile(self, fpath):
_BaseFile.save(self, fpath)
def save(self, fpath=None):
_BaseFile.save(self, fpath, 'to_binary')
def percent_translated(self):
return 100
def translated_entries(self):
return self
def untranslated_entries(self):
return []
def fuzzy_entries(self):
return []
def obsolete_entries(self):
return []
class _BaseEntry(object):
def __init__(self, *args, **kwargs):
self.msgid = kwargs.get('msgid', '')
self.msgstr = kwargs.get('msgstr', '')
self.msgid_plural = kwargs.get('msgid_plural', '')
self.msgstr_plural = kwargs.get('msgstr_plural', {})
self.msgctxt = kwargs.get('msgctxt', None)
self.obsolete = kwargs.get('obsolete', False)
self.encoding = kwargs.get('encoding', default_encoding)
def __unicode__(self, wrapwidth=78):
if self.obsolete:
delflag = '#~ '
else:
delflag = ''
ret = []
if self.msgctxt is not None:
ret += self._str_field("msgctxt", delflag, "", self.msgctxt,
wrapwidth)
ret += self._str_field("msgid", delflag, "", self.msgid, wrapwidth)
if self.msgid_plural:
ret += self._str_field("msgid_plural", delflag, "",
self.msgid_plural, wrapwidth)
if self.msgstr_plural:
msgstrs = self.msgstr_plural
keys = list(msgstrs)
keys.sort()
for index in keys:
msgstr = msgstrs[index]
plural_index = '[%s]' % index
ret += self._str_field("msgstr", delflag, plural_index, msgstr,
wrapwidth)
else:
ret += self._str_field("msgstr", delflag, "", self.msgstr,
wrapwidth)
ret.append('')
ret = u('\n').join(ret)
return ret
if PY3:
def __str__(self):
return self.__unicode__()
else:
def __str__(self):
return unicode(self).encode(self.encoding)
def __eq__(self, other):
return str(self) == str(other)
def _str_field(self, fieldname, delflag, plural_index, field,
wrapwidth=78):
lines = field.splitlines(True)
if len(lines) > 1:
lines = [''] + lines else:
escaped_field = escape(field)
specialchars_count = 0
for c in ['\\', '\n', '\r', '\t', '\v', '\b', '\f', '"']:
specialchars_count += field.count(c)
flength = len(fieldname) + 3
if plural_index:
flength += len(plural_index)
real_wrapwidth = wrapwidth - flength + specialchars_count
if wrapwidth > 0 and len(field) > real_wrapwidth:
lines = [''] + [unescape(item) for item in textwrap.wrap(
escaped_field,
wrapwidth - 2, drop_whitespace=False,
break_long_words=False
)]
else:
lines = [field]
if fieldname.startswith('previous_'):
fieldname = fieldname[9:]
ret = ['%s%s%s "%s"' % (delflag, fieldname, plural_index,
escape(lines.pop(0)))]
for line in lines:
ret.append('%s"%s"' % (delflag, escape(line)))
return ret
@property
def msgid_with_context(self):
if self.msgctxt:
return '%s%s%s' % (self.msgctxt, "\x04", self.msgid)
return self.msgid
class POEntry(_BaseEntry):
def __init__(self, *args, **kwargs):
_BaseEntry.__init__(self, *args, **kwargs)
self.comment = kwargs.get('comment', '')
self.tcomment = kwargs.get('tcomment', '')
self.occurrences = kwargs.get('occurrences', [])
self.flags = kwargs.get('flags', [])
self.previous_msgctxt = kwargs.get('previous_msgctxt', None)
self.previous_msgid = kwargs.get('previous_msgid', None)
self.previous_msgid_plural = kwargs.get('previous_msgid_plural', None)
self.linenum = kwargs.get('linenum', None)
def __unicode__(self, wrapwidth=78):
ret = []
if self.obsolete:
comments = [('tcomment', '# ')]
else:
comments = [('tcomment', '# '), ('comment', '#. ')]
for c in comments:
val = getattr(self, c[0])
if val:
for comment in val.split('\n'):
if wrapwidth > 0 and len(comment) + len(c[1]) > wrapwidth:
ret += textwrap.wrap(
comment,
wrapwidth,
initial_indent=c[1],
subsequent_indent=c[1],
break_long_words=False
)
else:
ret.append('%s%s' % (c[1], comment))
if not self.obsolete and self.occurrences:
filelist = []
for fpath, lineno in self.occurrences:
if lineno:
filelist.append('%s:%s' % (fpath, lineno))
else:
filelist.append(fpath)
filestr = ' '.join(filelist)
if wrapwidth > 0 and len(filestr) + 3 > wrapwidth:
ret += [line.replace('*', '-') for line in textwrap.wrap(
filestr.replace('-', '*'),
wrapwidth,
initial_indent='#: ',
subsequent_indent='#: ',
break_long_words=False
)]
else:
ret.append('#: ' + filestr)
if self.flags:
ret.append('#, %s' % ', '.join(self.flags))
fields = ['previous_msgctxt', 'previous_msgid',
'previous_msgid_plural']
if self.obsolete:
prefix = "#~| "
else:
prefix = "#| "
for f in fields:
val = getattr(self, f)
if val is not None:
ret += self._str_field(f, prefix, "", val, wrapwidth)
ret.append(_BaseEntry.__unicode__(self, wrapwidth))
ret = u('\n').join(ret)
return ret
def __cmp__(self, other):
if self.obsolete != other.obsolete:
if self.obsolete:
return -1
else:
return 1
occ1 = sorted(self.occurrences[:])
occ2 = sorted(other.occurrences[:])
if occ1 > occ2:
return 1
if occ1 < occ2:
return -1
msgctxt = self.msgctxt or '0'
othermsgctxt = other.msgctxt or '0'
if msgctxt > othermsgctxt:
return 1
elif msgctxt < othermsgctxt:
return -1
msgid_plural = self.msgid_plural or '0'
othermsgid_plural = other.msgid_plural or '0'
if msgid_plural > othermsgid_plural:
return 1
elif msgid_plural < othermsgid_plural:
return -1
if self.msgstr_plural and isinstance(self.msgstr_plural, dict):
msgstr_plural = list(self.msgstr_plural.values())
else:
msgstr_plural = []
if other.msgstr_plural and isinstance(other.msgstr_plural, dict):
othermsgstr_plural = list(other.msgstr_plural.values())
else:
othermsgstr_plural = []
if msgstr_plural > othermsgstr_plural:
return 1
elif msgstr_plural < othermsgstr_plural:
return -1
if self.msgid > other.msgid:
return 1
elif self.msgid < other.msgid:
return -1
if self.msgstr > other.msgstr:
return 1
elif self.msgstr < other.msgstr:
return -1
return 0
def __gt__(self, other):
return self.__cmp__(other) > 0
def __lt__(self, other):
return self.__cmp__(other) < 0
def __ge__(self, other):
return self.__cmp__(other) >= 0
def __le__(self, other):
return self.__cmp__(other) <= 0
def __eq__(self, other):
return self.__cmp__(other) == 0
def __ne__(self, other):
return self.__cmp__(other) != 0
def translated(self):
if self.obsolete or self.fuzzy:
return False
if self.msgstr != '':
return True
if self.msgstr_plural:
for pos in self.msgstr_plural:
if self.msgstr_plural[pos] == '':
return False
return True
return False
def merge(self, other):
self.msgid = other.msgid
self.msgctxt = other.msgctxt
self.occurrences = other.occurrences
self.comment = other.comment
fuzzy = self.fuzzy
self.flags = other.flags[:] if fuzzy:
self.flags.append('fuzzy')
self.msgid_plural = other.msgid_plural
self.obsolete = other.obsolete
self.previous_msgctxt = other.previous_msgctxt
self.previous_msgid = other.previous_msgid
self.previous_msgid_plural = other.previous_msgid_plural
if other.msgstr_plural:
for pos in other.msgstr_plural:
try:
self.msgstr_plural[pos]
except KeyError:
self.msgstr_plural[pos] = ''
@property
def fuzzy(self):
return 'fuzzy' in self.flags
@fuzzy.setter
def fuzzy(self, value):
if value and not self.fuzzy:
self.flags.insert(0, 'fuzzy')
elif not value and self.fuzzy:
self.flags.remove('fuzzy')
def __hash__(self):
return hash((self.msgid, self.msgstr))
class MOEntry(_BaseEntry):
def __init__(self, *args, **kwargs):
_BaseEntry.__init__(self, *args, **kwargs)
self.comment = ''
self.tcomment = ''
self.occurrences = []
self.flags = []
self.previous_msgctxt = None
self.previous_msgid = None
self.previous_msgid_plural = None
def __hash__(self):
return hash((self.msgid, self.msgstr))
class _POFileParser(object):
def __init__(self, pofile, *args, **kwargs):
enc = kwargs.get('encoding', default_encoding)
if _is_file(pofile):
try:
self.fhandle = io.open(pofile, 'rt', encoding=enc)
except LookupError:
enc = default_encoding
self.fhandle = io.open(pofile, 'rt', encoding=enc)
else:
self.fhandle = pofile.splitlines()
klass = kwargs.get('klass')
if klass is None:
klass = POFile
self.instance = klass(
pofile=pofile,
encoding=enc,
check_for_duplicates=kwargs.get('check_for_duplicates', False)
)
self.transitions = {}
self.current_line = 0
self.current_entry = POEntry(linenum=self.current_line)
self.current_state = 'st'
self.current_token = None
self.msgstr_index = 0
self.entry_obsolete = 0
all = ['st', 'he', 'gc', 'oc', 'fl', 'ct', 'pc', 'pm', 'pp', 'tc',
'ms', 'mp', 'mx', 'mi']
self.add('tc', ['st', 'he'], 'he')
self.add('tc', ['gc', 'oc', 'fl', 'tc', 'pc', 'pm', 'pp', 'ms',
'mp', 'mx', 'mi'], 'tc')
self.add('gc', all, 'gc')
self.add('oc', all, 'oc')
self.add('fl', all, 'fl')
self.add('pc', all, 'pc')
self.add('pm', all, 'pm')
self.add('pp', all, 'pp')
self.add('ct', ['st', 'he', 'gc', 'oc', 'fl', 'tc', 'pc', 'pm',
'pp', 'ms', 'mx'], 'ct')
self.add('mi', ['st', 'he', 'gc', 'oc', 'fl', 'ct', 'tc', 'pc',
'pm', 'pp', 'ms', 'mx'], 'mi')
self.add('mp', ['tc', 'gc', 'pc', 'pm', 'pp', 'mi'], 'mp')
self.add('ms', ['mi', 'mp', 'tc'], 'ms')
self.add('mx', ['mi', 'mx', 'mp', 'tc'], 'mx')
self.add('mc', ['ct', 'mi', 'mp', 'ms', 'mx', 'pm', 'pp', 'pc'], 'mc')
def parse(self):
try:
keywords = {
'msgctxt': 'ct',
'msgid': 'mi',
'msgstr': 'ms',
'msgid_plural': 'mp',
}
prev_keywords = {
'msgid_plural': 'pp',
'msgid': 'pm',
'msgctxt': 'pc',
}
tokens = []
fpath = '%s ' % self.instance.fpath if self.instance.fpath else ''
for line in self.fhandle:
self.current_line += 1
if self.current_line == 1:
BOM = codecs.BOM_UTF8.decode('utf-8')
if line.startswith(BOM):
line = line[len(BOM):]
line = line.strip()
if line == '':
continue
tokens = line.split(None, 2)
nb_tokens = len(tokens)
if tokens[0] == '#~|':
continue
if tokens[0] == '#~' and nb_tokens > 1:
line = line[3:].strip()
tokens = tokens[1:]
nb_tokens -= 1
self.entry_obsolete = 1
else:
self.entry_obsolete = 0
if tokens[0] in keywords and nb_tokens > 1:
line = line[len(tokens[0]):].lstrip()
if re.search(r'([^\\]|^)"', line[1:-1]):
raise IOError('Syntax error in po file %s(line %s): '
'unescaped double quote found' %
(fpath, self.current_line))
self.current_token = line
self.process(keywords[tokens[0]])
continue
self.current_token = line
if tokens[0] == '#:':
if nb_tokens <= 1:
continue
self.process('oc')
elif line[:1] == '"':
if re.search(r'([^\\]|^)"', line[1:-1]):
raise IOError('Syntax error in po file %s(line %s): '
'unescaped double quote found' %
(fpath, self.current_line))
self.process('mc')
elif line[:7] == 'msgstr[':
self.process('mx')
elif tokens[0] == '#,':
if nb_tokens <= 1:
continue
self.process('fl')
elif tokens[0] == '#' or tokens[0].startswith('##'):
if line == '#':
line += ' '
self.process('tc')
elif tokens[0] == '#.':
if nb_tokens <= 1:
continue
self.process('gc')
elif tokens[0] == '#|':
if nb_tokens <= 1:
raise IOError('Syntax error in po file %s(line %s)' %
(fpath, self.current_line))
line = line[2:].lstrip()
self.current_token = line
if tokens[1].startswith('"'):
self.process('mc')
continue
if nb_tokens == 2:
raise IOError('Syntax error in po file %s(line %s): '
'invalid continuation line' %
(fpath, self.current_line))
if tokens[1] not in prev_keywords:
raise IOError('Syntax error in po file %s(line %s): '
'unknown keyword %s' %
(fpath, self.current_line,
tokens[1]))
line = line[len(tokens[1]):].lstrip()
self.current_token = line
self.process(prev_keywords[tokens[1]])
else:
raise IOError('Syntax error in po file %s(line %s)' %
(fpath, self.current_line))
if self.current_entry and len(tokens) > 0 and \
not tokens[0].startswith('#'):
self.instance.append(self.current_entry)
metadataentry = self.instance.find('')
if metadataentry: self.instance.remove(metadataentry)
self.instance.metadata_is_fuzzy = metadataentry.flags
key = None
for msg in metadataentry.msgstr.splitlines():
try:
key, val = msg.split(':', 1)
self.instance.metadata[key] = val.strip()
except (ValueError, KeyError):
if key is not None:
self.instance.metadata[key] += '\n' + msg.strip()
finally:
if not isinstance(self.fhandle, list): self.fhandle.close()
return self.instance
def add(self, symbol, states, next_state):
for state in states:
action = getattr(self, 'handle_%s' % next_state)
self.transitions[(symbol, state)] = (action, next_state)
def process(self, symbol):
try:
(action, state) = self.transitions[(symbol, self.current_state)]
if action():
self.current_state = state
except Exception:
fpath = '%s ' % self.instance.fpath if self.instance.fpath else ''
if hasattr(self.fhandle, 'close'):
self.fhandle.close()
raise IOError('Syntax error in po file %s(line %s)' %
(fpath, self.current_line))
def handle_he(self):
if self.instance.header != '':
self.instance.header += '\n'
self.instance.header += self.current_token[2:]
return 1
def handle_tc(self):
if self.current_state in ['mc', 'ms', 'mx']:
self.instance.append(self.current_entry)
self.current_entry = POEntry(linenum=self.current_line)
if self.current_entry.tcomment != '':
self.current_entry.tcomment += '\n'
tcomment = self.current_token.lstrip('#')
if tcomment.startswith(' '):
tcomment = tcomment[1:]
self.current_entry.tcomment += tcomment
return True
def handle_gc(self):
if self.current_state in ['mc', 'ms', 'mx']:
self.instance.append(self.current_entry)
self.current_entry = POEntry(linenum=self.current_line)
if self.current_entry.comment != '':
self.current_entry.comment += '\n'
self.current_entry.comment += self.current_token[3:]
return True
def handle_oc(self):
if self.current_state in ['mc', 'ms', 'mx']:
self.instance.append(self.current_entry)
self.current_entry = POEntry(linenum=self.current_line)
occurrences = self.current_token[3:].split()
for occurrence in occurrences:
if occurrence != '':
try:
fil, line = occurrence.rsplit(':', 1)
if not line.isdigit():
fil = occurrence
line = ''
self.current_entry.occurrences.append((fil, line))
except (ValueError, AttributeError):
self.current_entry.occurrences.append((occurrence, ''))
return True
def handle_fl(self):
if self.current_state in ['mc', 'ms', 'mx']:
self.instance.append(self.current_entry)
self.current_entry = POEntry(linenum=self.current_line)
self.current_entry.flags += [c.strip() for c in
self.current_token[3:].split(',')]
return True
def handle_pp(self):
if self.current_state in ['mc', 'ms', 'mx']:
self.instance.append(self.current_entry)
self.current_entry = POEntry(linenum=self.current_line)
self.current_entry.previous_msgid_plural = \
unescape(self.current_token[1:-1])
return True
def handle_pm(self):
if self.current_state in ['mc', 'ms', 'mx']:
self.instance.append(self.current_entry)
self.current_entry = POEntry(linenum=self.current_line)
self.current_entry.previous_msgid = \
unescape(self.current_token[1:-1])
return True
def handle_pc(self):
if self.current_state in ['mc', 'ms', 'mx']:
self.instance.append(self.current_entry)
self.current_entry = POEntry(linenum=self.current_line)
self.current_entry.previous_msgctxt = \
unescape(self.current_token[1:-1])
return True
def handle_ct(self):
if self.current_state in ['mc', 'ms', 'mx']:
self.instance.append(self.current_entry)
self.current_entry = POEntry(linenum=self.current_line)
self.current_entry.msgctxt = unescape(self.current_token[1:-1])
return True
def handle_mi(self):
if self.current_state in ['mc', 'ms', 'mx']:
self.instance.append(self.current_entry)
self.current_entry = POEntry(linenum=self.current_line)
self.current_entry.obsolete = self.entry_obsolete
self.current_entry.msgid = unescape(self.current_token[1:-1])
return True
def handle_mp(self):
self.current_entry.msgid_plural = unescape(self.current_token[1:-1])
return True
def handle_ms(self):
self.current_entry.msgstr = unescape(self.current_token[1:-1])
return True
def handle_mx(self):
index = self.current_token[7]
value = self.current_token[self.current_token.find('"') + 1:-1]
self.current_entry.msgstr_plural[int(index)] = unescape(value)
self.msgstr_index = int(index)
return True
def handle_mc(self):
token = unescape(self.current_token[1:-1])
if self.current_state == 'ct':
self.current_entry.msgctxt += token
elif self.current_state == 'mi':
self.current_entry.msgid += token
elif self.current_state == 'mp':
self.current_entry.msgid_plural += token
elif self.current_state == 'ms':
self.current_entry.msgstr += token
elif self.current_state == 'mx':
self.current_entry.msgstr_plural[self.msgstr_index] += token
elif self.current_state == 'pp':
self.current_entry.previous_msgid_plural += token
elif self.current_state == 'pm':
self.current_entry.previous_msgid += token
elif self.current_state == 'pc':
self.current_entry.previous_msgctxt += token
return False
class _MOFileParser(object):
def __init__(self, mofile, *args, **kwargs):
if _is_file(mofile):
self.fhandle = open(mofile, 'rb')
else:
self.fhandle = io.BytesIO(mofile)
klass = kwargs.get('klass')
if klass is None:
klass = MOFile
self.instance = klass(
fpath=mofile,
encoding=kwargs.get('encoding', default_encoding),
check_for_duplicates=kwargs.get('check_for_duplicates', False)
)
def __del__(self):
if self.fhandle and hasattr(self.fhandle, 'close'):
self.fhandle.close()
def parse(self):
magic_number = self._readbinary('<I', 4)
if magic_number == MOFile.MAGIC:
ii = '<II'
elif magic_number == MOFile.MAGIC_SWAPPED:
ii = '>II'
else:
raise IOError('Invalid mo file, magic number is incorrect !')
self.instance.magic_number = magic_number
version, numofstrings = self._readbinary(ii, 8)
if version >> 16 not in (0, 1):
raise IOError('Invalid mo file, unexpected major revision number')
self.instance.version = version
msgids_hash_offset, msgstrs_hash_offset = self._readbinary(ii, 8)
self.fhandle.seek(msgids_hash_offset)
msgids_index = []
for i in range(numofstrings):
msgids_index.append(self._readbinary(ii, 8))
self.fhandle.seek(msgstrs_hash_offset)
msgstrs_index = []
for i in range(numofstrings):
msgstrs_index.append(self._readbinary(ii, 8))
encoding = self.instance.encoding
for i in range(numofstrings):
self.fhandle.seek(msgids_index[i][1])
msgid = self.fhandle.read(msgids_index[i][0])
self.fhandle.seek(msgstrs_index[i][1])
msgstr = self.fhandle.read(msgstrs_index[i][0])
if i == 0 and not msgid: raw_metadata, metadata = msgstr.split(b('\n')), {}
for line in raw_metadata:
tokens = line.split(b(':'), 1)
if tokens[0] != b(''):
try:
k = tokens[0].decode(encoding)
v = tokens[1].decode(encoding)
metadata[k] = v.strip()
except IndexError:
metadata[k] = u('')
self.instance.metadata = metadata
continue
msgid_tokens = msgid.split(b('\0'))
if len(msgid_tokens) > 1:
entry = self._build_entry(
msgid=msgid_tokens[0],
msgid_plural=msgid_tokens[1],
msgstr_plural=dict((k, v) for k, v in
enumerate(msgstr.split(b('\0'))))
)
else:
entry = self._build_entry(msgid=msgid, msgstr=msgstr)
self.instance.append(entry)
self.fhandle.close()
return self.instance
def _build_entry(self, msgid, msgstr=None, msgid_plural=None,
msgstr_plural=None):
msgctxt_msgid = msgid.split(b('\x04'))
encoding = self.instance.encoding
if len(msgctxt_msgid) > 1:
kwargs = {
'msgctxt': msgctxt_msgid[0].decode(encoding),
'msgid': msgctxt_msgid[1].decode(encoding),
}
else:
kwargs = {'msgid': msgid.decode(encoding)}
if msgstr:
kwargs['msgstr'] = msgstr.decode(encoding)
if msgid_plural:
kwargs['msgid_plural'] = msgid_plural.decode(encoding)
if msgstr_plural:
for k in msgstr_plural:
msgstr_plural[k] = msgstr_plural[k].decode(encoding)
kwargs['msgstr_plural'] = msgstr_plural
return MOEntry(**kwargs)
def _readbinary(self, fmt, numbytes):
bytes = self.fhandle.read(numbytes)
tup = struct.unpack(fmt, bytes)
if len(tup) == 1:
return tup[0]
return tup