Package translate :: Package storage :: Module statsdb

[hide private]
[frames] | no frames]

# Source Code for Module translate.storage.statsdb

      1  #!/usr/bin/env python
      2  # -*- coding: utf-8 -*-
      3  #
      4  # Copyright 2007-2009 Zuza Software Foundation
      5  #
      6  # This file is part of the Translate Toolkit.
      7  #
      8  # This program is free software; you can redistribute it and/or modify
      9  # it under the terms of the GNU General Public License as published by
     10  # the Free Software Foundation; either version 2 of the License, or
     11  # (at your option) any later version.
     12  #
     13  # This program is distributed in the hope that it will be useful,
     14  # but WITHOUT ANY WARRANTY; without even the implied warranty of
     15  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     16  # GNU General Public License for more details.
     17  #
     18  # You should have received a copy of the GNU General Public License
     19  # along with this program; if not, see <http://www.gnu.org/licenses/>.
     20
     21
     22  """Module to provide a cache of statistics in a database.
     23
     24  @organization: Zuza Software Foundation
     25  @copyright: 2007 Zuza Software Foundation
     26  @license: U{GPL <http://www.fsf.org/licensing/licenses/gpl.html>}
     27  """
     28
     29  from UserDict import UserDict
     30
     31  from translate import __version__ as toolkitversion
     32  from translate.storage import factory
     33  from translate.misc.multistring import multistring
     34  from translate.lang.common import Common
     35
     36  try:
     37      from sqlite3 import dbapi2
     38  except ImportError:
     39      from pysqlite2 import dbapi2
     40  import os.path
     41  import re
     42  import sys
     43  import stat
     44  import thread
     45
     46  kdepluralre = re.compile("^_n: ")
     47  brtagre = re.compile("<br\s*?/?>")
     48  xmltagre = re.compile("<[^>]+>")
     49  numberre = re.compile("\\D\\.\\D")
     50
     51  state_strings = {0: "untranslated", 1: "translated", 2: "fuzzy"}
     52
     53 -def wordcount(string):
     54      # TODO: po class should understand KDE style plurals
     55      string = kdepluralre.sub("", string)
     56      string = brtagre.sub("\n", string)
     57      string = xmltagre.sub("", string)
     58      string = numberre.sub(" ", string)
     59      #TODO: This should still use the correct language to count in the target
     60      #language
     61      return len(Common.words(string))
     62
     63 -def wordsinunit(unit):
     64      """Counts the words in the unit's source and target, taking plurals into
     65      account. The target words are only counted if the unit is translated."""
     66      (sourcewords, targetwords) = (0, 0)
     67      if isinstance(unit.source, multistring):
     68          sourcestrings = unit.source.strings
     69      else:
     70          sourcestrings = [unit.source or ""]
     71      for s in sourcestrings:
     72          sourcewords += wordcount(s)
     73      if not unit.istranslated():
     74          return sourcewords, targetwords
     75      if isinstance(unit.target, multistring):
     76          targetstrings = unit.target.strings
     77      else:
     78          targetstrings = [unit.target or ""]
     79      for s in targetstrings:
     80          targetwords += wordcount(s)
     81      return sourcewords, targetwords
     82
     83 -class Record(UserDict):
     84 -    def __init__(self, record_keys, record_values=None, compute_derived_values = lambda x: x):
     85          if record_values == None:
     86              record_values = (0 for _i in record_keys)
     87          self.record_keys = record_keys
     88          self.data = dict(zip(record_keys, record_values))
     89          self._compute_derived_values = compute_derived_values
     90          self._compute_derived_values(self)
     91
     92 -    def to_tuple(self):
     93          return tuple(self[key] for key in self.record_keys)
     94
     95 -    def __add__(self, other):
     96          result = Record(self.record_keys)
     97          for key in self.keys():
     98              result[key] = self[key] + other[key]
     99          self._compute_derived_values(self)
    100          return result
    101
    102 -    def __sub__(self, other):
    103          result = Record(self.record_keys)
    104          for key in self.keys():
    105              result[key] = self[key] - other[key]
    106          self._compute_derived_values(self)
    107          return result
    108
    109 -    def as_string_for_db(self):
    110          return ",".join([repr(x) for x in self.to_tuple()])
    111
    112 -def transaction(f):
    113      """Modifies f to commit database changes if it executes without exceptions.
    114      Otherwise it rolls back the database.
    115
    116      ALL publicly accessible methods in StatsCache MUST be decorated with this
    117      decorator.
    118      """
    119
    120      def decorated_f(self, *args, **kwargs):
    121          try:
    122              result = f(self, *args, **kwargs)
    123              self.con.commit()
    124              return result
    125          except:
    126              # If ANY exception is raised, we're left in an
    127              # uncertain state and we MUST roll back any changes to avoid getting
    128              # stuck in an inconsistent state.
    129              if self.con:
    130                  self.con.rollback()
    131              raise
    132      return decorated_f
    133
    134  UNTRANSLATED, TRANSLATED, FUZZY = 0, 1, 2
    135 -def statefordb(unit):
    136      """Returns the numeric database state for the unit."""
    137      if unit.istranslated():
    138          return TRANSLATED
    139      if unit.isfuzzy() and unit.target:
    140          return FUZZY
    141      return UNTRANSLATED
    142
    143 -class FileTotals(object):
    144      keys = ['translatedsourcewords',
    145              'fuzzysourcewords',
    146              'untranslatedsourcewords',
    147              'translated',
    148              'fuzzy',
    149              'untranslated',
    150              'translatedtargetwords']
    151
    152 -    def db_keys(self):
    153          return ",".join(self.keys)
    154
    155 -    def __init__(self, cur):
    156          self.cur = cur
    157          self.cur.execute("""
    158              CREATE TABLE IF NOT EXISTS filetotals(
    159                  fileid                  INTEGER PRIMARY KEY AUTOINCREMENT,
    160                  translatedsourcewords   INTEGER NOT NULL,
    161                  fuzzysourcewords        INTEGER NOT NULL,
    162                  untranslatedsourcewords INTEGER NOT NULL,
    163                  translated              INTEGER NOT NULL,
    164                  fuzzy                   INTEGER NOT NULL,
    165                  untranslated            INTEGER NOT NULL,
    166                  translatedtargetwords   INTEGER NOT NULL);""")
    167
    168 -    def new_record(cls, state_for_db=None, sourcewords=None, targetwords=None):
    169          record = Record(cls.keys, compute_derived_values = cls._compute_derived_values)
    170          if state_for_db is not None:
    171              if state_for_db is UNTRANSLATED:
    172                  record['untranslated'] = 1
    173                  record['untranslatedsourcewords'] = sourcewords
    174              if state_for_db is TRANSLATED:
    175                  record['translated'] = 1
    176                  record['translatedsourcewords'] = sourcewords
    177                  record['translatedtargetwords'] = targetwords
    178              elif state_for_db is FUZZY:
    179                  record['fuzzy'] = 1
    180                  record['fuzzysourcewords'] = sourcewords
    181          return record
    182
    183      new_record = classmethod(new_record)
    184
    185 -    def _compute_derived_values(cls, record):
    186          record["total"]            = record["untranslated"] + \
    187                                       record["translated"] + \
    188                                       record["fuzzy"]
    189          record["totalsourcewords"] = record["untranslatedsourcewords"] + \
    190                                       record["translatedsourcewords"] + \
    191                                       record["fuzzysourcewords"]
    192          record["review"]           = 0
    193      _compute_derived_values = classmethod(_compute_derived_values)
    194
    195 -    def __getitem__(self, fileid):
    196          result = self.cur.execute("""
    197              SELECT %(keys)s
    198              FROM   filetotals
    199              WHERE  fileid=?;""" % {'keys': self.db_keys()}, (fileid,))
    200          return Record(FileTotals.keys, result.fetchone(), self._compute_derived_values)
    201
    202 -    def __setitem__(self, fileid, record):
    203          self.cur.execute("""
    204              INSERT OR REPLACE into filetotals
    205              VALUES (%(fileid)d, %(vals)s);
    206          """ % {'fileid': fileid, 'vals': record.as_string_for_db()})
    207
    208 -    def __delitem__(self, fileid):
    209          self.cur.execute("""
    210              DELETE FROM filetotals
    211              WHERE fileid=?;
    212          """,  (fileid,))
    213
    214 -def emptyfiletotals():
    215      """Returns a dictionary with all statistics initalised to 0."""
    216      return FileTotals.new_record()
    217
    218 -def emptyfilechecks():
    219      return {}
    220
    221 -def emptyfilestats():
    222      return {"total": [], "translated": [], "fuzzy": [], "untranslated": []}
    223
    224 -def emptyunitstats():
    225      return {"sourcewordcount": [], "targetwordcount": []}
    226
    227  # We allow the caller to specify which value to return when errors_return_empty
    228  # is True. We do this, since Poolte wants None to be returned when it calls
    229  # get_mod_info directly, whereas we want an integer to be returned for
    230  # uses of get_mod_info within this module.
    231  # TODO: Get rid of empty_return when Pootle code is improved to not require
    232  #       this.
    233 -def get_mod_info(file_path):
    234      file_stat = os.stat(file_path)
    235      assert not stat.S_ISDIR(file_stat.st_mode)
    236      return file_stat.st_mtime, file_stat.st_size
    237
    238 -def suggestion_extension():
    239      return os.path.extsep + 'pending'
    240
    241 -def suggestion_filename(filename):
    242      return filename + suggestion_extension()
    243
    244  # ALL PUBLICLY ACCESSIBLE METHODS MUST BE DECORATED WITH THE transaction DECORATOR.
    245 -class StatsCache(object):
    246      """An object instantiated as a singleton for each statsfile that provides
    247      access to the database cache from a pool of StatsCache objects."""
    248      _caches = {}
    249      defaultfile = None
    250      con = None
    251      """This cache's connection"""
    252      cur = None
    253      """The current cursor"""
    254
    255 -    def __new__(cls, statsfile=None):
    256          current_thread = thread.get_ident()
    257          def make_database(statsfile):
    258              def connect(cache):
    259                  cache.con = dbapi2.connect(statsfile)
    260                  cache.cur = cache.con.cursor()
    261
    262              def clear_old_data(cache):
    263                  try:
    264                      cache.cur.execute("""SELECT toolkitbuild FROM files""")
    265                      val = cache.cur.fetchone()
    266                      # If the database is empty, we have no idea whether its layout
    267                      # is correct, so we might as well delete it.
    268                      if val is None or val[0] < toolkitversion.build:
    269                          cache.con.close()
    270                          del cache
    271                          os.unlink(statsfile)
    272                          return True
    273                      return False
    274                  except dbapi2.OperationalError:
    275                      return False
    276
    277              cache = cls._caches.setdefault(current_thread, {})[statsfile] = object.__new__(cls)
    278              connect(cache)
    279              if clear_old_data(cache):
    280                  connect(cache)
    281              cache.create()
    282              return cache
    283
    284          if not statsfile:
    285              if not cls.defaultfile:
    286                  userdir = os.path.expanduser("~")
    287                  cachedir = None
    288                  if os.name == "nt":
    289                      cachedir = os.path.join(userdir, "Translate Toolkit")
    290                  else:
    291                      cachedir = os.path.join(userdir, ".translate_toolkit")
    292                  if not os.path.exists(cachedir):
    293                      os.mkdir(cachedir)
    294                  cls.defaultfile = os.path.realpath(os.path.join(cachedir, "stats.db"))
    295              statsfile = cls.defaultfile
    296          else:
    297              statsfile = os.path.realpath(statsfile)
    298          # First see if a cache for this file already exists:
    299          if current_thread in cls._caches and statsfile in cls._caches[current_thread]:
    300              return cls._caches[current_thread][statsfile]
    301          # No existing cache. Let's build a new one and keep a copy
    302          return make_database(statsfile)
    303
    304      @transaction
    305 -    def create(self):
    306          """Create all tables and indexes."""
    307          self.file_totals = FileTotals(self.cur)
    308
    309          self.cur.execute("""CREATE TABLE IF NOT EXISTS files(
    310              fileid INTEGER PRIMARY KEY AUTOINCREMENT,
    311              path VARCHAR NOT NULL UNIQUE,
    312              st_mtime INTEGER NOT NULL,
    313              st_size INTEGER NOT NULL,
    314              toolkitbuild INTEGER NOT NULL);""")
    315
    316          self.cur.execute("""CREATE UNIQUE INDEX IF NOT EXISTS filepathindex
    317              ON files (path);""")
    318
    319          self.cur.execute("""CREATE TABLE IF NOT EXISTS units(
    320              id INTEGER PRIMARY KEY AUTOINCREMENT,
    321              unitid VARCHAR NOT NULL,
    322              fileid INTEGER NOT NULL,
    323              unitindex INTEGER NOT NULL,
    324              source VARCHAR NOT NULL,
    325              target VARCHAR,
    326              state INTEGER,
    327              sourcewords INTEGER,
    328              targetwords INTEGER);""")
    329
    330          self.cur.execute("""CREATE INDEX IF NOT EXISTS fileidindex
    331              ON units(fileid);""")
    332
    333          self.cur.execute("""CREATE TABLE IF NOT EXISTS checkerconfigs(
    334              configid INTEGER PRIMARY KEY AUTOINCREMENT,
    335              config VARCHAR);""")
    336
    337          self.cur.execute("""CREATE INDEX IF NOT EXISTS configindex
    338              ON checkerconfigs(config);""")
    339
    340          self.cur.execute("""CREATE TABLE IF NOT EXISTS uniterrors(
    341              errorid INTEGER PRIMARY KEY AUTOINCREMENT,
    342              unitindex INTEGER NOT NULL,
    343              fileid INTEGER NOT NULL,
    344              configid INTEGER NOT NULL,
    345              name VARCHAR NOT NULL,
    346              message VARCHAR);""")
    347
    348          self.cur.execute("""CREATE INDEX IF NOT EXISTS uniterrorindex
    349              ON uniterrors(fileid, configid);""")
    350
    351      @transaction
    352 -    def _getfileid(self, filename, check_mod_info=True, store=None):
    353          """return fileid representing the given file in the statscache.
    354
    355          if file not in cache or has been updated since last record
    356          update, recalculate stats.
    357
    358          optional argument store can be used to avoid unnessecary
    359          reparsing of already loaded translation files.
    360
    361          store can be a TranslationFile object or a callback that returns one.
    362          """
    363          if isinstance(filename, str):
    364              filename = unicode(filename, sys.getfilesystemencoding())
    365          realpath = os.path.realpath(filename)
    366          self.cur.execute("""SELECT fileid, st_mtime, st_size FROM files
    367                  WHERE path=?;""", (realpath,))
    368          filerow = self.cur.fetchone()
    369          mod_info = get_mod_info(realpath)
    370          if filerow:
    371              fileid = filerow[0]
    372              if not check_mod_info:
    373                  # Update the mod_info of the file
    374                  self.cur.execute("""UPDATE files
    375                          SET st_mtime=?, st_size=?
    376                          WHERE fileid=?;""", (mod_info[0], mod_info[1], fileid))
    377                  return fileid
    378              if (filerow[1], filerow[2]) == mod_info:
    379                  return fileid
    380
    381          # file wasn't in db at all, lets recache it
    382          if callable(store):
    383              store = store()
    384          else:
    385              store = store or factory.getobject(realpath)
    386
    387          return self._cachestore(store, realpath, mod_info)
    388
    389 -    def _getstoredcheckerconfig(self, checker):
    390          """See if this checker configuration has been used before."""
    391          config = str(checker.config.__dict__)
    392          self.cur.execute("""SELECT configid, config FROM checkerconfigs WHERE
    393              config=?;""", (config,))
    394          configrow = self.cur.fetchone()
    395          if not configrow or configrow[1] != config:
    396              return None
    397          else:
    398              return configrow[0]
    399
    400      @transaction
    401 -    def _cacheunitstats(self, units, fileid, unitindex=None, file_totals_record=FileTotals.new_record()):
    402          """Cache the statistics for the supplied unit(s)."""
    403          unitvalues = []
    404          for index, unit in enumerate(units):
    405              if unit.istranslatable():
    406                  sourcewords, targetwords = wordsinunit(unit)
    407                  if unitindex:
    408                      index = unitindex
    409                  # what about plurals in .source and .target?
    410                  unitvalues.append((unit.getid(), fileid, index, \
    411                                  unit.source, unit.target, \
    412                                  sourcewords, targetwords, \
    413                                  statefordb(unit)))
    414                  file_totals_record = file_totals_record + FileTotals.new_record(statefordb(unit), sourcewords, targetwords)
    415          # XXX: executemany is non-standard
    416          self.cur.executemany("""INSERT INTO units
    417              (unitid, fileid, unitindex, source, target, sourcewords, targetwords, state)
    418              values (?, ?, ?, ?, ?, ?, ?, ?);""",
    419              unitvalues)
    420          self.file_totals[fileid] = file_totals_record
    421          if unitindex:
    422              return state_strings[statefordb(units[0])]
    423          return ""
    424
    425      @transaction
    426 -    def _cachestore(self, store, realpath, mod_info):
    427          """Calculates and caches the statistics of the given store
    428          unconditionally."""
    429          self.cur.execute("""DELETE FROM files WHERE
    430              path=?;""", (realpath,))
    431          self.cur.execute("""INSERT INTO files
    432              (fileid, path, st_mtime, st_size, toolkitbuild) values (NULL, ?, ?, ?, ?);""",
    433              (realpath, mod_info[0], mod_info[1], toolkitversion.build))
    434          fileid = self.cur.lastrowid
    435          self.cur.execute("""DELETE FROM units WHERE
    436              fileid=?""", (fileid,))
    437          self._cacheunitstats(store.units, fileid)
    438          return fileid
    439
    440 -    def filetotals(self, filename, store=None):
    441          """Retrieves the statistics for the given file if possible, otherwise
    442          delegates to cachestore()."""
    443          return self.file_totals[self._getfileid(filename, store=store)]
    444
    445      @transaction
    446 -    def _cacheunitschecks(self, units, fileid, configid, checker, unitindex=None):
    447          """Helper method for cachestorechecks() and recacheunit()"""
    448          # We always want to store one dummy error to know that we have actually
    449          # run the checks on this file with the current checker configuration
    450          dummy = (-1, fileid, configid, "noerror", "")
    451          unitvalues = [dummy]
    452          # if we are doing a single unit, we want to return the checknames
    453          errornames = []
    454          for index, unit in enumerate(units):
    455              if unit.istranslatable():
    456                  # Correctly assign the unitindex
    457                  if unitindex:
    458                      index = unitindex
    459                  failures = checker.run_filters(unit)
    460                  for checkname, checkmessage in failures.iteritems():
    461                      unitvalues.append((index, fileid, configid, checkname, checkmessage))
    462                      errornames.append("check-" + checkname)
    463          checker.setsuggestionstore(None)
    464
    465          if unitindex:
    466              # We are only updating a single unit, so we don't want to add an
    467              # extra noerror-entry
    468              unitvalues.remove(dummy)
    469              errornames.append("total")
    470
    471          # XXX: executemany is non-standard
    472          self.cur.executemany("""INSERT INTO uniterrors
    473              (unitindex, fileid, configid, name, message)
    474              values (?, ?, ?, ?, ?);""",
    475              unitvalues)
    476          return errornames
    477
    478      @transaction
    479 -    def _cachestorechecks(self, fileid, store, checker, configid):
    480          """Calculates and caches the error statistics of the given store
    481          unconditionally."""
    482          # Let's purge all previous failures because they will probably just
    483          # fill up the database without much use.
    484          self.cur.execute("""DELETE FROM uniterrors WHERE
    485              fileid=?;""", (fileid,))
    486          self._cacheunitschecks(store.units, fileid, configid, checker)
    487          return fileid
    488
    489 -    def get_unit_stats(self, fileid, unitid):
    490          values = self.cur.execute("""
    491              SELECT   state, sourcewords, targetwords
    492              FROM     units
    493              WHERE    fileid=? AND unitid=?
    494          """, (fileid, unitid))
    495          result = values.fetchone()
    496          if result is not None:
    497              return result
    498          else:
    499              print >> sys.stderr, """WARNING: Database in inconsistent state.
    500              fileid %d and unitid %s have no entries in the table units.""" % (fileid, unitid)
    501              # If values.fetchone() is None, then we return an empty list,
    502              # to make FileTotals.new_record(*self.get_unit_stats(fileid, unitid))
    503              # do the right thing.
    504              return []
    505
    506      @transaction
    507 -    def recacheunit(self, filename, checker, unit):
    508          """Recalculate all information for a specific unit. This is necessary
    509          for updating all statistics when a translation of a unit took place,
    510          for example.
    511
    512          This method assumes that everything was up to date before (file totals,
    513          checks, checker config, etc."""
    514          fileid = self._getfileid(filename, check_mod_info=False)
    515          configid = self._get_config_id(fileid, checker)
    516          unitid = unit.getid()
    517          # get the unit index
    518          totals_without_unit = self.file_totals[fileid] - \
    519                                     FileTotals.new_record(*self.get_unit_stats(fileid, unitid))
    520          self.cur.execute("""SELECT unitindex FROM units WHERE
    521              fileid=? AND unitid=?;""", (fileid, unitid))
    522          unitindex = self.cur.fetchone()[0]
    523          self.cur.execute("""DELETE FROM units WHERE
    524              fileid=? AND unitid=?;""", (fileid, unitid))
    525          state = [self._cacheunitstats([unit], fileid, unitindex, totals_without_unit)]
    526          # remove the current errors
    527          self.cur.execute("""DELETE FROM uniterrors WHERE
    528              fileid=? AND unitindex=?;""", (fileid, unitindex))
    529          if os.path.exists(suggestion_filename(filename)):
    530              checker.setsuggestionstore(factory.getobject(suggestion_filename(filename), ignore=suggestion_extension()))
    531          state.extend(self._cacheunitschecks([unit], fileid, configid, checker, unitindex))
    532          return state
    533
    534 -    def _checkerrors(self, filename, fileid, configid, checker, store):
    535          def geterrors():
    536              self.cur.execute("""SELECT
    537                  name,
    538                  unitindex
    539                  FROM uniterrors WHERE fileid=? and configid=?
    540                  ORDER BY unitindex;""", (fileid, configid))
    541              return self.cur.fetchone(), self.cur
    542
    543          first, cur = geterrors()
    544          if first is not None:
    545              return first, cur
    546
    547          # This could happen if we haven't done the checks before, or the
    548          # file changed, or we are using a different configuration
    549          if callable(store):
    550              store = store()
    551          else:
    552              store = store or factory.getobject(filename)
    553
    554          if os.path.exists(suggestion_filename(filename)):
    555              checker.setsuggestionstore(factory.getobject(suggestion_filename(filename), ignore=suggestion_extension()))
    556          self._cachestorechecks(fileid, store, checker, configid)
    557          return geterrors()
    558
    559 -    def _geterrors(self, filename, fileid, configid, checker, store):
    560          result = []
    561          first, cur = self._checkerrors(filename, fileid, configid, checker, store)
    562          result.append(first)
    563          result.extend(cur.fetchall())
    564          return result
    565
    566      @transaction
    567 -    def _get_config_id(self, fileid, checker):
    568          configid = self._getstoredcheckerconfig(checker)
    569          if configid:
    570              return configid
    571          self.cur.execute("""INSERT INTO checkerconfigs
    572              (configid, config) values (NULL, ?);""",
    573              (str(checker.config.__dict__),))
    574          return self.cur.lastrowid
    575
    576 -    def filechecks(self, filename, checker, store=None):
    577          """Retrieves the error statistics for the given file if possible,
    578          otherwise delegates to cachestorechecks()."""
    579          fileid = self._getfileid(filename, store=store)
    580          configid = self._get_config_id(fileid, checker)
    581          values = self._geterrors(filename, fileid, configid, checker, store)
    582
    583          errors = emptyfilechecks()
    584          for value in values:
    585              if value[1] == -1:
    586                  continue
    587              checkkey = 'check-' + value[0]      #value[0] is the error name
    588              if not checkkey in errors:
    589                  errors[checkkey] = []
    590              errors[checkkey].append(value[1])   #value[1] is the unitindex
    591
    592          return errors
    593
    594 -    def file_fails_test(self, filename, checker, name):
    595          fileid = self._getfileid(filename)
    596          configid = self._get_config_id(fileid, checker)
    597          self._checkerrors(filename, fileid, configid, checker, None)
    598          self.cur.execute("""SELECT
    599              name,
    600              unitindex
    601              FROM uniterrors
    602              WHERE fileid=? and configid=? and name=?;""", (fileid, configid, name))
    603          return self.cur.fetchone() is not None
    604
    605 -    def filestatestats(self, filename, store=None):
    606          """Return a dictionary of unit stats mapping sets of unit
    607          indices with those states"""
    608          stats = emptyfilestats()
    609          fileid = self._getfileid(filename, store=store)
    610
    611          self.cur.execute("""SELECT
    612              state,
    613              unitindex
    614              FROM units WHERE fileid=?
    615              ORDER BY unitindex;""", (fileid,))
    616          values = self.cur.fetchall()
    617
    618          for value in values:
    619              stats[state_strings[value[0]]].append(value[1])
    620              stats["total"].append(value[1])
    621
    622          return stats
    623
    624 -    def filestats(self, filename, checker, store=None):
    625          """Return a dictionary of property names mapping sets of unit
    626          indices with those properties."""
    627          stats = emptyfilestats()
    628          stats.update(self.filechecks(filename, checker, store))
    629          stats.update(self.filestatestats(filename, store))
    630          return stats
    631
    632 -    def unitstats(self, filename, _lang=None, store=None):
    633          # For now, lang and store are unused. lang will allow the user to
    634          # base stats information on the given language. See the commented
    635          # line containing stats.update below.
    636          """Return a dictionary of property names mapping to arrays which
    637          map unit indices to property values.
    638
    639          Please note that this is different from filestats, since filestats
    640          supplies sets of unit indices with a given property, whereas this
    641          method supplies arrays which map unit indices to given values."""
    642          stats = emptyunitstats()
    643
    644          #stats.update(self.unitchecks(filename, lang, store))
    645          fileid = self._getfileid(filename, store=store)
    646
    647          self.cur.execute("""SELECT
    648            sourcewords, targetwords
    649            FROM units WHERE fileid=?
    650            ORDER BY unitindex;""", (fileid,))
    651
    652          for sourcecount, targetcount in self.cur.fetchall():
    653              stats["sourcewordcount"].append(sourcecount)
    654              stats["targetwordcount"].append(targetcount)
    655
    656          return stats
    657