diff --git a/MANIFEST b/MANIFEST old mode 100644 new mode 100755 index 8c8ad8c76..ea5b24785 --- a/MANIFEST +++ b/MANIFEST @@ -1,16 +1,10 @@ -.mailmap BENCHMARK.txt Dockerfile INSTALL.txt LICENSE.txt MANIFEST README.rst -RELEASE-0.75.0 -RELEASE-0.75.1 -RELEASE-0.75.2 -RELEASE-0.76.0.md -RELEASE-0.77.0.md -UPGRADE-0.75.0 +RELEASE-0.78.0.md archive/CHANGES.txt archive/RELEASE-0.12.0 archive/RELEASE-0.13.0 @@ -84,6 +78,11 @@ archive/RELEASE-0.62.6 archive/RELEASE-0.62.7 archive/RELEASE-0.62.8 archive/RELEASE-0.62.9 +archive/RELEASE-0.75.0 +archive/RELEASE-0.75.1 +archive/RELEASE-0.75.2 +archive/RELEASE-0.76.0.md +archive/RELEASE-0.77.0.md archive/UPGRADE-0.14.0 archive/UPGRADE-0.15.0 archive/UPGRADE-0.16.0 @@ -94,6 +93,7 @@ archive/UPGRADE-0.50.0 archive/UPGRADE-0.60.0 archive/UPGRADE-0.61.0 archive/UPGRADE-0.62.0 +archive/UPGRADE-0.75.0 circle.yml docs/.gitignore docs/autodoc/index.rst @@ -137,7 +137,6 @@ docs/ical.rst docs/igettext.rst docs/index.rst docs/ipkg.rst -docs/log.rst docs/odf.rst docs/rss.rst docs/stl.rst @@ -164,7 +163,11 @@ itools/csv/csv_.py itools/csv/parser.py itools/csv/table.py itools/database/__init__.py -itools/database/catalog.py +itools/database/backends/__init__.py +itools/database/backends/catalog.py +itools/database/backends/git.py +itools/database/backends/lfs.py +itools/database/backends/registry.py itools/database/exceptions.py itools/database/fields.py itools/database/git.py @@ -181,10 +184,10 @@ itools/datatypes/base.py itools/datatypes/datetime_.py itools/datatypes/languages.py itools/datatypes/primitive.py +itools/environment.json itools/fs/__init__.py itools/fs/common.py itools/fs/lfs.py -itools/fs/vfs.py itools/gettext/__init__.py itools/gettext/domains.py itools/gettext/mo.py @@ -193,7 +196,6 @@ itools/handlers/__init__.py itools/handlers/archive.py itools/handlers/base.py itools/handlers/config.py -itools/handlers/database.py itools/handlers/file.py itools/handlers/folder.py itools/handlers/image.py @@ -226,18 +228,25 @@ itools/i18n/oracle.py itools/ical/__init__.py itools/ical/datatypes.py itools/ical/icalendar.py +itools/locale/ca.mo itools/locale/ca.po +itools/locale/de.mo itools/locale/de.po +itools/locale/en.mo itools/locale/en.po +itools/locale/es.mo itools/locale/es.po +itools/locale/fr.mo itools/locale/fr.po +itools/locale/it.mo itools/locale/it.po itools/locale/locale.pot +itools/locale/nl.mo itools/locale/nl.po +itools/locale/nl_BE.mo itools/locale/nl_BE.po +itools/locale/zh.mo itools/locale/zh.po -itools/log/__init__.py -itools/log/log.py itools/loop/__init__.py itools/loop/loop.py itools/odf/OpenDocument-v1.2-cd05-schema.rng @@ -265,6 +274,7 @@ itools/pkg/build.py itools/pkg/build_gulp.py itools/pkg/git.py itools/pkg/handlers.py +itools/pkg/update_locale.py itools/pkg/utils.py itools/python/__init__.py itools/python/python.py @@ -304,7 +314,6 @@ itools/web/exceptions.py itools/web/headers.py itools/web/messages.py itools/web/router.py -itools/web/server.py itools/web/static.py itools/web/utils.py itools/web/views.py @@ -336,7 +345,6 @@ scripts/idb-inspect.py scripts/igettext-build.py scripts/igettext-extract.py scripts/igettext-merge.py -scripts/iodf-greek.py scripts/ipkg-docs.py scripts/ipkg-quality.py scripts/ipkg-update-locale.py @@ -412,7 +420,14 @@ test/test_xml.py test/test_xmlfile.py test/tests/gettext_en_es.xlf test/tests/hello.txt +test/tests/index.html.ca +test/tests/index.html.de test/tests/index.html.en +test/tests/index.html.es +test/tests/index.html.fr +test/tests/index.html.nl +test/tests/index.html.nl_BE +test/tests/index.html.zh test/tests/localizermsgs.tmx test/tests/sample-rss-2.xml test/tests/test.csv diff --git a/README.rst b/README.rst index 063fd1bd1..638aef742 100644 --- a/README.rst +++ b/README.rst @@ -6,16 +6,16 @@ meta-package for easier development and deployment. The packages included are:: - itools itools.ical itools.srx - itools.core itools.log itools.stl - itools.csv itools.loop itools.tmx - itools.database itools.odf itools.uri - itools.datatypes itools.office itools.web - itools.fs itools.pdf itools.workflow - itools.gettext itools.pkg itools.xliff - itools.handlers itools.python itools.xml - itools.html itools.relaxng itools.xmlfile - itools.i18n itools.rss + itools itools.ical itools.stl + itools.core itools.loop itools.tmx + itools.csv itools.odf itools.uri + itools.database itools.office itools.validators + itools.datatypes itools.pdf itools.web + itools.fs itools.pkg itools.workflow + itools.gettext itools.python itools.xliff + itools.handlers itools.relaxng itools.xml + itools.html itools.rss itools.xmlfile + itools.i18n itools.srx The scripts included are:: diff --git a/RELEASE-0.78.0.md b/RELEASE-0.78.0.md old mode 100644 new mode 100755 diff --git a/docs/conf.py b/docs/conf.py index 2304a350e..65b139dda 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -269,7 +269,7 @@ def setup(app): synopsis = "Itools %s module" % module # And the save the file - print '[autodoc] make the modules/%s.rst file' % module + print('[autodoc] make the modules/%s.rst file' % module) with open('autodoc/modules/%s.rst' % module, 'w') as rst_file: rst_file.write(template_module.format(module=module, synopsis=synopsis)) diff --git a/docs/examples/csv/clients.py b/docs/examples/csv/clients.py index f3151c59b..1bad2bb9f 100644 --- a/docs/examples/csv/clients.py +++ b/docs/examples/csv/clients.py @@ -19,7 +19,7 @@ # Import from itools from itools.csv import CSVFile -from itools.handlers import RWDatabase +from itools.database import RWDatabase from itools.datatypes import Integer, Unicode, String, Date @@ -35,7 +35,7 @@ class Clients(CSVFile): if __name__ == '__main__': - rw_database = RWDatabase() + rw_database = RWDatabase(path="docs/examples/csv/", size_min=2048, size_max=4096, backend='lfs') clients = rw_database.get_handler("clients.csv", Clients) # Access a column by its name @@ -48,4 +48,4 @@ class Clients(CSVFile): clients.add_row( [250, u'J. David Ibanez', 'jdavid@itaapy.com', date(2007, 1, 1)]) - print clients.to_str() + print(clients.to_str()) diff --git a/docs/examples/gettext/hello.py b/docs/examples/gettext/hello.py index 4d7c748ad..76235bd00 100644 --- a/docs/examples/gettext/hello.py +++ b/docs/examples/gettext/hello.py @@ -12,7 +12,7 @@ def say_hello(): message = MSG(u'Hello World') - print message.gettext() + print(message.gettext()) def get_template(name): @@ -34,7 +34,7 @@ def get_template(name): def tell_fable(): template = get_template('fable.xhtml') - print open(template).read() + print(open(template).read()) say_hello() diff --git a/docs/examples/html/test_parser.py b/docs/examples/html/test_parser.py index f99b9605b..fb80ce139 100644 --- a/docs/examples/html/test_parser.py +++ b/docs/examples/html/test_parser.py @@ -6,13 +6,13 @@ data = open('hello.html').read() for type, value, line in HTMLParser(data): if type == DOCUMENT_TYPE: - print 'DOC TYPE :', repr(value) + print("DOC TYPE : {}".format(repr(value))) elif type == START_ELEMENT: tag_uri, tag_name, attributes = value - print 'START TAG :', tag_name + print("START TAG : {}".format(tag_name)) elif type == END_ELEMENT: tag_uri, tag_name = value - print 'END TAG :', tag_name + print("END TAG : {}".format(tag_name)) elif type == TEXT: - print 'TEXT :', value + print("TEXT {} :".format(value)) diff --git a/docs/examples/web/cal.py b/docs/examples/web/cal.py index 0a0046f03..a02721763 100644 --- a/docs/examples/web/cal.py +++ b/docs/examples/web/cal.py @@ -23,7 +23,7 @@ import datetime # Import from itools -from itools.handlers import RWDatabase +from itools.database import RWDatabase from itools.loop import Loop from itools.uri import get_reference from itools.web import WebServer, RootResource, Resource, BaseView diff --git a/docs/examples/xml/test_parser.py b/docs/examples/xml/test_parser.py index 984b79052..382ca6e31 100644 --- a/docs/examples/xml/test_parser.py +++ b/docs/examples/xml/test_parser.py @@ -13,10 +13,10 @@ for type, value, line in XMLParser(data): if type == START_ELEMENT: tag_uri, tag_name, attributes = value - print 'START TAG :', tag_name, attributes + print("START TAG : {} {}".format(tag_name, attributes)) elif type == END_ELEMENT: tag_uri, tag_name = value - print 'END TAG :', tag_name + print("END TAG : {}".format(tag_name)) elif type == TEXT: - print 'TEXT :', value + print("TEXT {} :".format(value)) diff --git a/docs/log.rst b/docs/log.rst deleted file mode 100644 index 5f41bd007..000000000 --- a/docs/log.rst +++ /dev/null @@ -1,173 +0,0 @@ -:mod:`itools.log` -- Logging -**************************** - -.. module:: itools.log - :synopsis: Logging - -.. index:: - single: Logging - -.. contents:: - -The :mod:`itools.log` package provides a simple programming interface for -logging events (errors, warning messages, etc.). It is inspired by the -logging facilities included in the `GLib -`_ library. - -Levels -====== - -Every log event belongs to one of these five levels: - -.. data:: FATAL - - Log level for fatal errors, see :func:`log_fatal` - -.. data:: ERROR - - Log level for common errors, see :func:`log_error` - -.. data:: WARNING - - Log level for warning messages, see :func:`log_warning` - -.. data:: INFO - - Log level for informational messages, see :func:`log_info` - -.. data:: DEBUG - - Log level for debug messages, see :func:`log_debug` - - -Logging functions -================= - -For every level there is a function. Below we define the default behavior -of these functions, we will see later how to override this behavior. - -.. function:: log_fatal(message, domain=None) - - Prints the given message into the standard error, and then aborts the - application. - -.. function:: log_error(message, domain=None) - - Prints the given message into the standard error. - -.. function:: log_warning(message, domain=None) - - Prints the given message into the standard error. - -.. function:: log_info(message, domain=None) - - Prints the given message into the standard output. - -.. function:: log_debug(message, domain=None) - - By default this function does nothing, debug messages are ignored. - -The ``domain`` argument allows to classify the log events by application -domains. This argument is optional, if not given then the event belongs to -the default domain. - -.. note:: - - Through :mod:`itools` we define one domain per package (``itools.http``, - ``itools.web``, etc.) - -Here there are some examples: - -.. code-block:: python - - >>> from itools.log import log_fatal, log_error, log_warning, log_debug - >>> log_error('Internal Server Error', domain='itools.http') - 2009-08-21 15:06:22 tucu itools.http[7268]: Internal Server Error - >>> log_debug('I am here') - >>> log_warning('Failed to connect to SMTP host', domain='itools.mail') - 2009-08-21 15:07:23 tucu itools.mail[7268]: Failed to connect to SMTP host - >>> log_fatal('Panic') - 2009-08-21 15:07:39 tucu [7268]: Panic - -It can be appreciated that the format of the log line looks a lot like the -syslog messages of Unix systems; except for the date, which is in a different -format. - -More important is the fact that the itools logging system allows log events to -span multiple lines. For instance, by default, if we are handling an -exception while logging, the traceback will be printed: - -.. code-block:: python - - >>> try: - ... 5/0 - ... except Exception: - ... log_error('Division failed') - ... - 2009-08-21 15:16:53 tucu [7362]: Division failed - Traceback (most recent call last): - File "", line 2, in - ZeroDivisionError: integer division or modulo by zero - -This allows to recover from errors while recording them. - - -Override the default behavior -============================= - -To override the default behavior at least one new logger must be registered, -this is done with the :func:`register_logger` function: - - -.. function:: register_logger(logger, \*domains) - - Register the given logger object for the given domains. - -For instance: - -.. code-block:: python - - from itools.log import Logger, WARNING, register_logger - - logger = Logger('/tmp/log', WARNING) - register_logger(logger, None) - -With the code above errors and warning messages will be written to the -``/tmp/log`` file, while debug and informational messages will be ignored. -This will become the default behavior for all domains. - -Here there is the description of the default logger class: - -.. class:: Logger(log_file=None, min_level=INFO) - - By default messages are printed to the standard error or the standard - output, depending on the level of the message. If the ``log_file`` - argument is given, it must be a file path, then messages will be written - to the indicated file instead of printed. - - By default debug messages are ignored. The argument ``min_level`` allows - to change this, for instance, to log all messages, pass the :data:`DEBUG` - value. - - .. method:: format_header(domain, level, message) - - TODO - - .. method:: get_body() - - TODO - - .. method:: format_body() - - TODO - - .. method:: format(domain, level, message) - - TODO - - .. method:: log(domain, level, message) - - TODO - -It is possible to subclass the :class:`Logger` class to personalize the -behavior of the logger as needed. diff --git a/itools/__init__.py b/itools/__init__.py index e347a13e2..5c824df17 100644 --- a/itools/__init__.py +++ b/itools/__init__.py @@ -14,9 +14,16 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +# Import from standard lib +from logging import getLogger, NullHandler + # Import from itools -from core import get_version +from .core import get_version +getLogger("itools.core").addHandler(NullHandler()) +getLogger("itools.web").addHandler(NullHandler()) +getLogger("itools.database").addHandler(NullHandler()) +getLogger("itools.stl").addHandler(NullHandler()) +getLogger("itools.catalog").addHandler(NullHandler()) __version__ = get_version() - diff --git a/itools/core/__init__.py b/itools/core/__init__.py index 936bcb012..97b2358a8 100644 --- a/itools/core/__init__.py +++ b/itools/core/__init__.py @@ -20,23 +20,22 @@ from sys import platform # Import from itools -from cache import LRUCache -from freeze import freeze, frozendict, frozenlist -from lazy import lazy -from mimetypes_ import add_type, guess_all_extensions, guess_extension -from mimetypes_ import guess_type, has_encoding, has_extension -from odict import OrderedDict -from prototypes import prototype_type, prototype, is_prototype -from prototypes import proto_property, proto_lazy_property -from timezones import fixed_offset, local_tz -from utils import get_abspath, merge_dicts, get_sizeof, get_pipe, get_version +from .cache import LRUCache +from .freeze import freeze, frozendict, frozenlist +from .lazy import lazy +from .mimetypes_ import add_type, guess_all_extensions, guess_extension +from .mimetypes_ import guess_type, has_encoding, has_extension +from .odict import OrderedDict +from .prototypes import prototype_type, prototype, is_prototype +from .prototypes import proto_property, proto_lazy_property +from .timezones import fixed_offset, local_tz +from .utils import get_abspath, merge_dicts, get_sizeof, get_pipe, get_version if platform[:3] == 'win': - from _win import become_daemon, fork, get_time_spent, vmsize + from ._win import become_daemon, fork, get_time_spent, vmsize else: - from _unix import become_daemon, fork, get_time_spent, vmsize - + from ._unix import become_daemon, fork, get_time_spent, vmsize __all__ = [ diff --git a/itools/core/_unix.py b/itools/core/_unix.py index 5a006fe62..803e18b36 100644 --- a/itools/core/_unix.py +++ b/itools/core/_unix.py @@ -21,7 +21,6 @@ from sys import exit, stdin, stdout, stderr - def vmsize(): """Returns the resident size in bytes. """ @@ -51,7 +50,7 @@ def become_daemon(): try: pid = fork() except OSError: - print 'unable to fork' + print('unable to fork') exit(1) if pid == 0: @@ -67,4 +66,3 @@ def become_daemon(): dup2(file_desc, 2) else: exit() - diff --git a/itools/core/_win.py b/itools/core/_win.py index 0cf6ff20d..7fca8089e 100644 --- a/itools/core/_win.py +++ b/itools/core/_win.py @@ -23,8 +23,7 @@ from win32con import PROCESS_QUERY_INFORMATION, PROCESS_VM_READ from win32process import GetProcessTimes, GetProcessMemoryInfo except ImportError: - print 'Warning: win32 is not installed' - + print('Warning: win32 is not installed') def get_win32_handle(pid): @@ -52,19 +51,16 @@ def get_time_spent(mode='both', since=0.0): return (data['KernelTime'] + data['UserTime']) / 10000000.0 - since - def vmsize(): handle = get_win32_handle(getpid()) m = GetProcessMemoryInfo(handle) return m["WorkingSetSize"] - def become_daemon(): - raise NotImplementedError, 'become_daemon not yet implemented for Windows' - + raise NotImplementedError('become_daemon not yet implemented for Windows') def fork(): - raise NotImplementedError, 'fork not yet implemented for Windows' + raise NotImplementedError('fork not yet implemented for Windows') diff --git a/itools/core/cache.py b/itools/core/cache.py index 49993c365..8f448c886 100644 --- a/itools/core/cache.py +++ b/itools/core/cache.py @@ -21,7 +21,7 @@ """ # Import from itools -from odict import OrderedDict +from .odict import OrderedDict class LRUCache(OrderedDict): @@ -63,18 +63,18 @@ def __init__(self, size_min, size_max=None, automatic=True): # Check arguments type if type(size_min) is not int: error = "the 'size_min' argument must be an int, not '%s'" - raise TypeError, error % type(size_min) + raise TypeError(error % type(size_min)) if type(automatic) is not bool: error = "the 'automatic' argument must be an int, not '%s'" - raise TypeError, error % type(automatic) + raise TypeError(error % type(automatic)) if size_max is None: size_max = size_min elif type(size_max) is not int: error = "the 'size_max' argument must be an int, not '%s'" - raise TypeError, error % type(size_max) + raise TypeError(error % type(size_max)) elif size_max < size_min: - raise ValueError, "the 'size_max' is smaller than 'size_min'" + raise ValueError("the 'size_max' is smaller than 'size_min'") # Initialize the dict super(LRUCache, self).__init__() @@ -84,7 +84,6 @@ def __init__(self, size_min, size_max=None, automatic=True): # Whether to free memory automatically or not (boolean) self.automatic = automatic - def _append(self, key): super(LRUCache, self)._append(key) @@ -93,7 +92,6 @@ def _append(self, key): while len(self) > self.size_min: self.popitem() - def touch(self, key): # (1) Get the node from the key-to-node map node = self.key2node[key] @@ -115,4 +113,3 @@ def touch(self, key): node.next = None self.last.next = node self.last = node - diff --git a/itools/core/freeze.py b/itools/core/freeze.py index 316be11c8..3df572302 100644 --- a/itools/core/freeze.py +++ b/itools/core/freeze.py @@ -15,65 +15,50 @@ # along with this program. If not, see . - class frozenlist(list): __slots__ = [] - ####################################################################### # Mutable operations must raise 'TypeError' def __delitem__(self, index): - raise TypeError, 'frozenlists are not mutable' - + raise TypeError('frozenlists are not mutable') def __delslice__(self, left, right): - raise TypeError, 'frozenlists are not mutable' - + raise TypeError('frozenlists are not mutable') def __iadd__(self, alist): - raise TypeError, 'frozenlists are not mutable' - + raise TypeError('frozenlists are not mutable') def __imul__(self, alist): - raise TypeError, 'frozenlists are not mutable' - + raise TypeError('frozenlists are not mutable') def __setitem__(self, index, value): - raise TypeError, 'frozenlists are not mutable' - + raise TypeError('frozenlists are not mutable') def __setslice__(self, left, right, value): - raise TypeError, 'frozenlists are not mutable' - + raise TypeError('frozenlists are not mutable') def append(self, item): - raise TypeError, 'frozenlists are not mutable' - + raise TypeError('frozenlists are not mutable') def extend(self, alist): - raise TypeError, 'frozenlists are not mutable' - + raise TypeError('frozenlists are not mutable') def insert(self, index, value): - raise TypeError, 'frozenlists are not mutable' - + raise TypeError('frozenlists are not mutable') def pop(self, index=-1): - raise TypeError, 'frozenlists are not mutable' - + raise TypeError('frozenlists are not mutable') def remove(self, value): - raise TypeError, 'frozenlists are not mutable' - + raise TypeError('frozenlists are not mutable') def reverse(self): - raise TypeError, 'frozenlists are not mutable' - + raise TypeError('frozenlists are not mutable') def sort(self, cmp=None, key=None, reverse=False): - raise TypeError, 'frozenlists are not mutable' - + raise TypeError('frozenlists are not mutable') ####################################################################### # Non-mutable operations @@ -81,75 +66,60 @@ def __add__(self, alist): alist = list(self) + alist return frozenlist(alist) - def __hash__(self): # TODO Implement frozenlists hash-ability - raise NotImplementedError, 'frozenlists not yet hashable' - + raise NotImplementedError('frozenlists not yet hashable') def __mul__(self, factor): alist = list(self) * factor return frozenlist(alist) - def __rmul__(self, factor): alist = list(self) * factor return frozenlist(alist) - def __repr__(self): - return 'frozenlist([%s])' % ', '.join([ repr(x) for x in self ]) - + return 'frozenlist([%s])' % ', '.join([repr(x) for x in self]) class frozendict(dict): __slots__ = [] - ####################################################################### # Mutable operations must raise 'TypeError' def __delitem__(self, index): - raise TypeError, 'frozendicts are not mutable' - + raise TypeError('frozendicts are not mutable') def __setitem__(self, key, value): - raise TypeError, 'frozendicts are not mutable' - + raise TypeError('frozendicts are not mutable') def clear(self): - raise TypeError, 'frozendicts are not mutable' - + raise TypeError('frozendicts are not mutable') def pop(self, key, default=None): - raise TypeError, 'frozendicts are not mutable' - + raise TypeError('frozendicts are not mutable') def popitem(self): - raise TypeError, 'frozendicts are not mutable' - + raise TypeError('frozendicts are not mutable') def setdefault(self, key, default=None): - raise TypeError, 'frozendicts are not mutable' - + raise TypeError('frozendicts are not mutable') def update(self, a_dict=None, **kw): - raise TypeError, 'frozendicts are not mutable' - + raise TypeError('frozendicts are not mutable') ####################################################################### # Non-mutable operations def __hash__(self): # TODO Implement frozendicts hash-ability - raise NotImplementedError, 'frozendicts not yet hashable' - + raise NotImplementedError('frozendicts not yet hashable') def __repr__(self): - aux = [ "%s: %s" % (repr(k), repr(v)) for k, v in self.items() ] + aux = ["%s: %s" % (repr(k), repr(v)) for k, v in self.items()] return 'frozendict({%s})' % ', '.join(aux) - def freeze(value): # Freeze value_type = type(value) @@ -163,6 +133,4 @@ def freeze(value): if isinstance(value, (frozenlist, frozendict, frozenset)): return value # Error - raise TypeError, 'unable to freeze "%s"' % value_type - - + raise TypeError('unable to freeze "%s"' % value_type) diff --git a/itools/core/lazy.py b/itools/core/lazy.py index 448c53481..8bc67ed6d 100644 --- a/itools/core/lazy.py +++ b/itools/core/lazy.py @@ -15,7 +15,6 @@ # along with this program. If not, see . - # From http://blog.pythonisito.com/2008/08/lazy-descriptors.html class lazy(object): @@ -25,16 +24,13 @@ def __init__(self, meth): self.__name__ = meth.__name__ self.__doc__ = meth.__doc__ - def __get__(self, instance, owner): if instance is None: return self - name = self.meth.func_name + name = self.meth.__name__ value = self.meth(instance) instance.__dict__[name] = value return value - def __repr__(self): return '%s wrapps %s' % (object.__repr__(self), repr(self.meth)) - diff --git a/itools/core/odict.py b/itools/core/odict.py old mode 100644 new mode 100755 index 42dffe648..77830949f --- a/itools/core/odict.py +++ b/itools/core/odict.py @@ -24,15 +24,14 @@ class DNode(object): __slots__ = ['prev', 'next', 'key'] - def __init__(self, key): self.key = key - class OrderedDict(dict): def __init__(self, items=None): + super().__init__() # The doubly-linked list self.first = None self.last = None @@ -43,19 +42,18 @@ def __init__(self, items=None): for key, value in items: self[key] = value - def _check_integrity(self): """This method is for testing purposes, it checks the internal data structures are consistent. """ keys = self.keys() - keys.sort() + keys = sorted(list(keys)) # Check the key-to-node mapping keys2 = self.key2node.keys() - keys2.sort() + keys2 = sorted(list(keys2)) assert keys == keys2 # Check the key-to-node against the doubly-linked list - for key, node in self.key2node.iteritems(): + for key, node in self.key2node.items(): assert type(key) is type(node.key) assert key == node.key # Check the doubly-linked list against the cache @@ -67,7 +65,6 @@ def _check_integrity(self): node = node.next assert len(keys) == 0 - def _append(self, key): node = DNode(key) @@ -83,7 +80,6 @@ def _append(self, key): self.last.next = node self.last = node - def _remove(self, key): # (1) Pop the node from the key-to-node map node = self.key2node.pop(key) @@ -99,7 +95,6 @@ def _remove(self, key): else: node.next.prev = node.prev - ###################################################################### # Override dict API def __iter__(self): @@ -108,82 +103,67 @@ def __iter__(self): yield node.key node = node.next - def __setitem__(self, key, value): dict.__setitem__(self, key, value) self._append(key) - def __delitem__(self, key): self._remove(key) dict.__delitem__(self, key) - def clear(self): dict.clear(self) self.key2node.clear() self.first = self.last = None - def copy(self): message = "use 'copy.deepcopy' to copy an ordered dict" - raise NotImplementedError, message - + raise NotImplementedError(message) def fromkeys(self, seq, value=None): - raise NotImplementedError, "the 'fromkeys' method is not supported" - + raise NotImplementedError("the 'fromkeys' method is not supported") def items(self): return list(self.iteritems()) - def iteritems(self): node = self.first while node is not None: yield node.key, self[node.key] node = node.next - def iterkeys(self): node = self.first while node is not None: yield node.key node = node.next - def itervalues(self): node = self.first while node is not None: yield self[node.key] node = node.next - def keys(self): return list(self.iterkeys()) - def pop(self, key): self._remove(key) return dict.pop(self, key) - def popitem(self): if self.first is None: - raise KeyError, 'popitem(): ordered dict is empty' + raise KeyError('popitem(): ordered dict is empty') key = self.first.key value = self[key] del self[key] return (key, value) - def setdefault(self, key, default=None): - raise NotImplementedError, "the 'setdefault' method is not supported" - + raise NotImplementedError("the 'setdefault' method is not supported") def update(self, value=None, **kw): - raise NotImplementedError, "the 'update' method is not supported" - + raise NotImplementedError("the 'update' method is not supported") def values(self): return list(self.itervalues()) diff --git a/itools/core/prototypes.py b/itools/core/prototypes.py old mode 100644 new mode 100755 index 35f739d8c..13148f210 --- a/itools/core/prototypes.py +++ b/itools/core/prototypes.py @@ -15,15 +15,16 @@ # along with this program. If not, see . # Import from the Standard Library +import traceback +from logging import getLogger from sys import _getframe from types import FunctionType -# Import from itools -from itools.log import log_error - # Import from here -from lazy import lazy +from .lazy import lazy + +log = getLogger("itools.core") """ This module provides prototype-based programming: @@ -53,7 +54,6 @@ class proto2(proto1): """ - class prototype_type(type): def __new__(mcs, class_name, bases, dict): @@ -107,22 +107,17 @@ class A(prototype): del dict[source_name] # Fix the name if type(value) is classmethod: - value.__get__(None, dict).im_func.__name__ = name + value.__get__(None, dict).__func__.__name__ = name elif type(value) is proto_property: value.__name__ = name elif type(value) is proto_lazy_property: value.__name__ = name - # Make and return the class return type.__new__(mcs, class_name, bases, dict) - -class prototype(object): - - __metaclass__ = prototype_type - +class prototype(object, metaclass=prototype_type): def __new__(cls, *args, **kw): """ @@ -140,48 +135,44 @@ def __new__(cls, *args, **kw): # Ok return new_class - def __init__(self, *args, **kw): pass - class proto_property(lazy): def __get__(self, instance, owner): try: value = self.meth(owner) except Exception as e: - msg = 'Error on proto property:\n' - log_error(msg + str(e), domain='itools.core') + tb = traceback.format_exc() + log.error("Error on proto property: {}".format(tb), exc_info=True) raise return value - class proto_lazy_property(lazy): def __get__(self, instance, owner): name = self.__name__ for cls in owner.__mro__: if name in cls.__dict__: - name = self.meth.func_name + name = self.meth.__name__ try: value = self.meth(owner) except Exception as e: - msg = 'Error on proto lazy property:\n' - log_error(msg + str(e), domain='itools.core') + tb = traceback.format_exc() + log.error("Error on proto lazy property: {}".format(tb), exc_info=True) raise setattr(owner, name, value) return value - def is_prototype(value, cls): from itools.gettext import MSG from itools.web import INFO, ERROR for c in [MSG, INFO, ERROR]: if cls is c and isinstance(value, c): - print("Warning: is_prototype(xxx, MSG) is obsolete. MSG is not a prototype anymore") + log.debug("Warning: is_prototype(xxx, MSG) is obsolete. MSG is not a prototype anymore") return True return issubclass(type(value), prototype_type) and issubclass(value, cls) diff --git a/itools/core/timezones.py b/itools/core/timezones.py old mode 100644 new mode 100755 index 5ad907a07..f8287b7d0 --- a/itools/core/timezones.py +++ b/itools/core/timezones.py @@ -73,37 +73,34 @@ def fixed_offset(offset, _tzinfos={}): ########################################################################### # Local Time (copied from from Python datetime doc) ########################################################################### -ZERO = timedelta(0) -STDOFFSET = timedelta(seconds = -timezone) -DSTOFFSET = timedelta(seconds = -altzone) if daylight else STDOFFSET -DSTDIFF = DSTOFFSET - STDOFFSET class LocalTimezone(tzinfo): - def utcoffset(self, dt): - return DSTOFFSET if self._isdst(dt) else STDOFFSET + ZERO = timedelta(0) + STDOFFSET = timedelta(seconds=-timezone) + DSTOFFSET = timedelta(seconds=-altzone) if daylight else STDOFFSET + DSTDIFF = DSTOFFSET - STDOFFSET + def utcoffset(self, dt): + return self.DSTOFFSET if self._isdst(dt) else self.STDOFFSET def dst(self, dt): - return DSTDIFF if self._isdst(dt) else ZERO - + return self.DSTDIFF if self._isdst(dt) else self.ZERO def tzname(self, dt): return tzname[self._isdst(dt)] - def _isdst(self, dt): stamp = mktime((dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second, dt.weekday(), 0, -1)) tt = localtime(stamp) return tt.tm_isdst > 0 - def localize(self, dt, is_dst=False): """Implemented for compatibility with pytz """ if dt.tzinfo is not None: - raise ValueError, 'Not naive datetime (tzinfo is already set)' + raise ValueError('Not naive datetime (tzinfo is already set)') return dt.replace(tzinfo=self) diff --git a/itools/core/utils.py b/itools/core/utils.py index 92b92d72c..cfa9c2709 100644 --- a/itools/core/utils.py +++ b/itools/core/utils.py @@ -50,7 +50,6 @@ def get_abspath(local_path, mname=None): return mpath - def get_version(mname=None): if mname is None: mname = _getframe(1).f_globals.get('__name__') @@ -62,7 +61,6 @@ def get_version(mname=None): return None - def merge_dicts(d, *args, **kw): """Merge two or more dictionaries into a new dictionary object. """ @@ -73,7 +71,6 @@ def merge_dicts(d, *args, **kw): return new_d - def get_sizeof(obj): """Return the size of an object and all objects refered by it. """ @@ -93,13 +90,12 @@ def get_sizeof(obj): return size - def get_pipe(command, cwd=None): """Wrapper around 'subprocess.Popen' """ popen = Popen(command, stdout=PIPE, stderr=PIPE, cwd=cwd) stdoutdata, stderrdata = popen.communicate() if popen.returncode != 0: - raise EnvironmentError, (popen.returncode, stderrdata) + raise EnvironmentError((popen.returncode, stderrdata)) return stdoutdata diff --git a/itools/csv/__init__.py b/itools/csv/__init__.py index 55ec5c177..8592b3818 100644 --- a/itools/csv/__init__.py +++ b/itools/csv/__init__.py @@ -17,11 +17,11 @@ # Import from itools from itools.core import add_type -from csv_ import CSVFile, Row -from parser import parse -from table import Table, Record, UniqueError -from table import parse_table, fold_line, escape_data, is_multilingual -from table import Property, property_to_str, deserialize_parameters +from .csv_ import CSVFile, Row +from .parser import parse +from .table import Table, Record, UniqueError +from .table import parse_table, fold_line, escape_data, is_multilingual +from .table import Property, property_to_str, deserialize_parameters __all__ = [ @@ -44,5 +44,4 @@ ] - add_type('text/comma-separated-values', '.csv') diff --git a/itools/csv/csv_.py b/itools/csv/csv_.py old mode 100644 new mode 100755 index 1a7cc44d9..532653d7f --- a/itools/csv/csv_.py +++ b/itools/csv/csv_.py @@ -21,8 +21,7 @@ # Import from itools from itools.datatypes import String, Unicode from itools.handlers import TextFile, guess_encoding, register_handler_class -from parser import parse - +from .parser import parse # TODO Drop the 'Row' class, use a list or a tuple instead. @@ -37,7 +36,6 @@ def get_value(self, name): return self[column] - def copy(self): clone = self.__class__(self) clone.number = self.number @@ -45,7 +43,6 @@ def copy(self): return clone - class CSVFile(TextFile): class_mimetypes = ['text/comma-separated-values', 'text/x-comma-separated-values', @@ -66,7 +63,6 @@ class CSVFile(TextFile): has_header = False skip_header = False - ######################################################################### # Load & Save ######################################################################### @@ -75,11 +71,9 @@ def reset(self): self.lines = [] self.n_lines = 0 - def new(self): pass - def _load_state_from_file(self, file): # Guess encoding data = file.read() @@ -91,21 +85,22 @@ def _load_state_from_file(self, file): # Header if self.has_header: - self.header = parser.next() + self.header = next(parser) # Content for line in parser: self._add_row(line) - def to_str(self, encoding='UTF-8', separator=',', newline='\n'): + def escape(data): + return '"%s"' % data.replace('"', '""') lines = [] # Header if self.has_header: - line = [ escape(Unicode.encode(x, encoding)) for x in self.header ] + line = [escape(Unicode.encode(x, encoding)) for x in self.header] line = separator.join(line) lines.append(line) @@ -114,23 +109,25 @@ def escape(data): schema = self.schema columns = self.columns if schema and columns: - datatypes = [ (i, schema[x]) for i, x in enumerate(columns) ] + datatypes = [(i, schema[x]) for i, x in enumerate(columns)] for row in self.get_rows(): line = [] for i, datatype in datatypes: - try: - data = datatype.encode(row[i], encoding=encoding) - except TypeError: - data = datatype.encode(row[i]) - line.append(escape(data)) + if isinstance(row[i], str): + line.append(escape(row[i])) + else: + try: + data = datatype.encode(row[i], encoding=encoding) + except TypeError: + data = datatype.encode(row[i]) + line.append(escape(data)) lines.append(separator.join(line)) else: for row in self.get_rows(): - line = [ escape(x) for x in row ] + line = [escape(x) for x in row] lines.append(separator.join(line)) return newline.join(lines) - ######################################################################### # API / Private ######################################################################### @@ -147,20 +144,17 @@ def _add_row(self, row): return row - def get_datatype(self, name): if self.schema is None: # Default return String return self.schema[name] - ######################################################################### # API / Public ######################################################################### def get_nrows(self): - return len([ x for x in self.lines if x is not None]) - + return len([x for x in self.lines if x is not None]) def get_row(self, number): """Return row at the given line number. Count begins at 0. @@ -170,10 +164,9 @@ def get_row(self, number): """ row = self.lines[number] if row is None: - raise IndexError, 'list index out of range' + raise IndexError('list index out of range') return row - def get_rows(self, numbers=None): """Return rows at the given list of line numbers. If no numbers are given, then all rows are returned. @@ -188,14 +181,12 @@ def get_rows(self, numbers=None): for i in numbers: yield self.get_row(i) - def add_row(self, row): """Append new row as an instance of row class. """ self.set_changed() return self._add_row(row) - def update_row(self, index, **kw): row = self.get_row(index) self.set_changed() @@ -210,7 +201,6 @@ def update_row(self, index, **kw): column = columns.index(name) row[column] = kw[name] - def del_row(self, number): """Delete row at the given line number. Count begins at 0. """ @@ -219,10 +209,9 @@ def del_row(self, number): # Remove row = self.lines[number] if row is None: - raise IndexError, 'list assignment index out of range' + raise IndexError('list assignment index out of range') self.lines[number] = None - def del_rows(self, numbers): """Delete rows at the given line numbers. Count begins at 0. """ @@ -231,9 +220,8 @@ def del_rows(self, numbers): for i in numbers: self.del_row(i) - def get_unique_values(self, name): - return set([ x.get_value(name) for x in self.get_rows() ]) + return set([x.get_value(name) for x in self.get_rows()]) register_handler_class(CSVFile) diff --git a/itools/csv/parser.py b/itools/csv/parser.py old mode 100644 new mode 100755 index 4a757f942..8f2164df7 --- a/itools/csv/parser.py +++ b/itools/csv/parser.py @@ -30,7 +30,7 @@ def parse_line(reader, line, datatypes, encoding, n_columns): if len(line) != n_columns: msg = 'CSV syntax error: wrong number of columns at line %d: %s' line_num = getattr(reader, 'line_num', None) - raise ValueError, msg % (line_num, line) + raise ValueError(msg % (line_num, line)) # Decode values decoded = [] @@ -44,7 +44,7 @@ def parse_line(reader, line, datatypes, encoding, n_columns): # Next line try: - next_line = reader.next() + next_line = next(reader) except StopIteration: next_line = None except Exception: @@ -55,7 +55,6 @@ def parse_line(reader, line, datatypes, encoding, n_columns): return decoded, next_line - def parse(data, columns=None, schema=None, guess=False, has_header=False, encoding='UTF-8', **kw): """This method is a generator that returns one CSV row at a time. To @@ -78,12 +77,12 @@ def parse(data, columns=None, schema=None, guess=False, has_header=False, reader = read_csv(lines, **kw) # 2. Find out the number of columns, if not specified - line = reader.next() + line = next(reader) n_columns = len(columns) if columns is not None else len(line) # 3. The header if has_header is True: - datatypes = [ Unicode for x in range(n_columns) ] + datatypes = [Unicode for x in range(n_columns)] datatypes = enumerate(datatypes) datatypes = list(datatypes) header, line = parse_line(reader, line, datatypes, encoding, n_columns) @@ -91,9 +90,9 @@ def parse(data, columns=None, schema=None, guess=False, has_header=False, # 4. The content if schema is not None: - datatypes = [ schema.get(c, String) for c in columns ] + datatypes = [schema.get(c, String) for c in columns] else: - datatypes = [ String for x in range(n_columns) ] + datatypes = [String for x in range(n_columns)] datatypes = enumerate(datatypes) datatypes = list(datatypes) diff --git a/itools/csv/table.py b/itools/csv/table.py old mode 100644 new mode 100755 index 06eae6a2c..205d8f07c --- a/itools/csv/table.py +++ b/itools/csv/table.py @@ -26,9 +26,9 @@ # Import from itools from itools.datatypes import DateTime, String, Unicode -from itools.handlers import File -from csv_ import CSVFile -from parser import parse +from itools.handlers import TextFile +from .csv_ import CSVFile +from .parser import parse ########################################################################### @@ -51,7 +51,6 @@ def unescape_data(data, escape_table=escape_table): return '\\'.join(out) - def escape_data(data, escape_table=escape_table): """Escape the data """ @@ -61,7 +60,6 @@ def escape_data(data, escape_table=escape_table): return data - def unfold_lines(data): """Unfold the folded lines. """ @@ -82,7 +80,6 @@ def unfold_lines(data): yield line - def fold_line(data): """Fold the unfolded line over 75 characters. """ @@ -107,7 +104,6 @@ def fold_line(data): return res - # XXX The RFC only allows '-', we allow more because this is used by # itools.database (metadata). This set matches checkid (itools.handlers) allowed = frozenset(['-', '_', '.', '@']) @@ -121,7 +117,7 @@ def read_name(line, allowed=allowed): # Test first character of name c = line[0] if not c.isalnum() and c != '-': - raise SyntaxError, 'unexpected character (%s)' % c + raise SyntaxError('unexpected character (%s)' % c) # Test the rest idx = 1 @@ -133,9 +129,9 @@ def read_name(line, allowed=allowed): if c.isalnum() or c in allowed: idx += 1 continue - raise SyntaxError, "unexpected character '%s' (%s)" % (c, ord(c)) + raise SyntaxError("unexpected character '%s' (%s)" % (c, ord(c))) - raise SyntaxError, 'unexpected end of line (%s)' % line + raise SyntaxError('unexpected end of line (%s)' % line) # Manage an icalendar content line value property [with parameters] : @@ -178,7 +174,7 @@ def get_tokens(property): if c.isalnum() or c in ('-'): param_name, status = c, 2 else: - raise SyntaxError, error1 % (c, status) + raise SyntaxError(error1 % (c, status)) # param-name begun elif status == 2: @@ -188,7 +184,7 @@ def get_tokens(property): parameters[param_name] = [] status = 3 else: - raise SyntaxError, error1 % (c, status) + raise SyntaxError(error1 % (c, status)) # param-name ended, param-value beginning elif status == 3: @@ -233,7 +229,7 @@ def get_tokens(property): parameters[param_name].append(param_value) status = 3 elif c == '"': - raise SyntaxError, error1 % (c, status) + raise SyntaxError(error1 % (c, status)) else: param_value += c @@ -251,17 +247,16 @@ def get_tokens(property): elif c == ',': status = 3 else: - raise SyntaxError, error1 % (c, status) + raise SyntaxError(error1 % (c, status)) if status not in (7, 8): - raise SyntaxError, 'unexpected property (%s)' % property + raise SyntaxError('unexpected property (%s)' % property) # Unescape special characters (TODO Check the spec) value = unescape_data(value) return value, parameters - def parse_table(data): """This is the public interface of the module "itools.ical.parser", a low-level parser of iCalendar files. @@ -281,7 +276,6 @@ def parse_table(data): yield name, value, parameters - ########################################################################### # Helper functions ########################################################################### @@ -297,21 +291,20 @@ def deserialize_parameters(parameters, schema, default=String(multiple=True)): for name in parameters: datatype = schema.get(name, default) if datatype is None: - raise ValueError, 'parameter "{0}" not defined'.format(name) + raise ValueError('parameter "{0}" not defined'.format(name)) # Decode value = parameters[name] - value = [ decode_param_value(x, datatype) for x in value ] + value = [decode_param_value(x, datatype) for x in value] # Multiple or single if not datatype.multiple: if len(value) > 1: msg = 'parameter "%s" must be a singleton' - raise ValueError, msg % name + raise ValueError(msg % name) value = value[0] # Update parameters[name] = value - ########################################################################### # UniqueError ########################################################################### @@ -323,11 +316,9 @@ def __init__(self, name, value): self.name = name self.value = value - def __str__(self): - return ( - u'the "{field}" field must be unique, the "{value}" value is ' - u' already used.').format(field=self.name, value=self.value) + return ('the "{field}" field must be unique, the "{value}" value is ' + 'already used.').format(field=self.name, value=self.value) ########################################################################### @@ -347,30 +338,26 @@ def __init__(self, value, **kw): self.value = value self.parameters = kw or None - def clone(self): # Copy the value and parameters value = deepcopy(self.value) parameters = {} - for p_key, p_value in self.parameters.iteritems(): + for p_key, p_value in self.parameters.items(): c_value = deepcopy(p_value) parameters[p_key] = c_value return Property(value, **parameters) - def get_parameter(self, name, default=None): if self.parameters is None: return default return self.parameters.get(name, default) - def set_parameter(self, name, value): if self.parameters is None: self.parameters = {} self.parameters[name] = value - def __eq__(self, other): if type(other) is not Property: return False @@ -378,12 +365,10 @@ def __eq__(self, other): return False return self.parameters == other.parameters - def __ne__(self, other): return not self.__eq__(other) - params_escape_table = ( ('"', r'\"'), ('\r', r'\r'), @@ -401,7 +386,7 @@ def encode_param_value(p_name, p_value, p_datatype): # Standard case (ical behavior) if '"' in p_value or '\n' in p_value: error = 'the "%s" parameter contains a double quote' - raise ValueError, error % p_name + raise ValueError(error % p_name) if ';' in p_value or ':' in p_value or ',' in p_value: return '"%s"' % p_value return p_value @@ -426,7 +411,7 @@ def _property_to_str(name, property, datatype, p_schema, encoding='utf-8'): # Parameters if property.parameters: p_names = property.parameters.keys() - p_names.sort() + p_names = sorted(list(p_names)) else: p_names = [] @@ -443,7 +428,7 @@ def _property_to_str(name, property, datatype, p_schema, encoding='utf-8'): # FIXME Use the encoding if is_multiple(p_datatype): p_value = [ - encode_param_value(p_name, x, p_datatype) for x in p_value ] + encode_param_value(p_name, x, p_datatype) for x in p_value] p_value = ','.join(p_value) else: p_value = encode_param_value(p_name, p_value, p_datatype) @@ -456,8 +441,8 @@ def _property_to_str(name, property, datatype, p_schema, encoding='utf-8'): else: value = datatype.encode(property.value) if type(value) is not str: - raise ValueError, 'property "{0}" is not str but {1}'.format( - name, type(value)) + raise ValueError('property "{0}" is not str but {1}'.format( + name, type(value))) value = escape_data(value) # Ok @@ -468,39 +453,35 @@ def _property_to_str(name, property, datatype, p_schema, encoding='utf-8'): def property_to_str(name, property, datatype, p_schema, encoding='utf-8'): try: return _property_to_str(name, property, datatype, p_schema, encoding) - except StandardError: + except Exception: err = 'failed to serialize "%s" property, probably a bad value' - raise ValueError, err % name - + raise ValueError(err % name) class Record(dict): __slots__ = ['id', 'record_properties'] - - def __init__(self, id, record_properties): - self.id = id - self.record_properties = record_properties - + def __init__(self, _id, record_properties): + super().__init__() + self.id = _id + self.record_properties = record_properties def __getattr__(self, name): if name == '__number__': return self.id if name not in self: - raise AttributeError, "'%s' object has no attribute '%s'" % ( - self.__class__.__name__, name) + raise AttributeError("'%s' object has no attribute '%s'" % ( + self.__class__.__name__, name)) property = self[name] if type(property) is list: - return [ x.value for x in property ] + return [x.value for x in property] return property.value - def get_property(self, name): return self.get(name) - # For indexing purposes def get_value(self, name): property = self.get(name) @@ -508,12 +489,11 @@ def get_value(self, name): return None if type(property) is list: - return [ x.value for x in property ] + return [x.value for x in property] return property.value - -class Table(File): +class Table(TextFile): record_class = Record @@ -526,7 +506,6 @@ class Table(File): record_parameters = { 'language': String(multiple=False)} - def get_datatype(self, name): # Table schema if name == 'ts': @@ -535,7 +514,6 @@ def get_datatype(self, name): return self.schema[name] return String(multiple=True) - def get_record_datatype(self, name): # Record schema if name == 'ts': @@ -545,7 +523,6 @@ def get_record_datatype(self, name): # FIXME Probably we should raise an exception here return String(multiple=True) - def properties_to_dict(self, properties, record, first=False): """Add the given "properties" as Property objects or Property objects list to the given dictionnary "record". @@ -566,23 +543,22 @@ def properties_to_dict(self, properties, record, first=False): # Transform values to properties if is_multilingual(datatype): if type(value) is not list: - value = [ value ] + value = [value] record.setdefault(name, []) for p in value: language = p.parameters['language'] record[name] = [ x for x in record[name] - if x.parameters['language'] != language ] + if x.parameters['language'] != language] record[name].append(p) elif datatype.multiple: if type(value) is list: - record[name] = [ to_property(x) for x in value ] + record[name] = [to_property(x) for x in value] else: record[name] = [to_property(value)] else: record[name] = to_property(value) - ####################################################################### # Handlers ####################################################################### @@ -591,14 +567,12 @@ def reset(self): self.records = [] self.changed_properties = False - def new(self): # Add the properties record properties = self.record_class(-1, self.record_properties) properties['ts'] = Property(datetime.now()) self.properties = properties - def _load_state_from_file(self, file): # Load the records records = self.records @@ -644,15 +618,14 @@ def _load_state_from_file(self, file): record.setdefault(name, []).append(property) elif name in record: msg = "record %s: property '%s' can occur only once" - raise ValueError, msg % (uid, name) + raise ValueError(msg % (uid, name)) else: record[name] = property - def _record_to_str(self, id, record): lines = ['id:%d/0\n' % id] names = record.keys() - names.sort() + names = sorted(list(names)) # Table or record schema if id == -1: get_datatype = self.get_datatype @@ -677,7 +650,6 @@ def _record_to_str(self, id, record): lines.append('\n') return ''.join(lines) - def to_str(self): lines = [] # Properties record @@ -695,17 +667,15 @@ def to_str(self): return ''.join(lines) - ####################################################################### # API / Public ####################################################################### - def get_record(self, id): + def get_record(self, _id): try: - return self.records[id] + return self.records[_id] except IndexError: return None - def add_record(self, kw): # Check for duplicate for name in kw: @@ -714,8 +684,8 @@ def add_record(self, kw): if self.search(name, kw[name]): raise UniqueError(name, kw[name]) # Make new record - id = len(self.records) - record = self.record_class(id, self.record_properties) + _id = len(self.records) + record = self.record_class(_id, self.record_properties) self.properties_to_dict(kw, record) record['ts'] = Property(datetime.now()) # Change @@ -724,25 +694,23 @@ def add_record(self, kw): # Back return record - - def update_record(self, id, **kw): - record = self.records[id] + def update_record(self, _id, **kw): + record = self.records[_id] if record is None: msg = 'cannot modify record "%s" because it has been deleted' - raise LookupError, msg % id + raise LookupError(msg % _id) # Check for duplicate for name in kw: datatype = self.get_record_datatype(name) if getattr(datatype, 'unique', False) is True: search = self.search(name, kw[name]) - if search and (search[0] != self.records[id]): + if search and (search[0] != self.records[_id]): raise UniqueError(name, kw[name]) # Update record self.set_changed() self.properties_to_dict(kw, record) record['ts'] = Property(datetime.now()) - def update_properties(self, **kw): record = self.properties if record is None: @@ -757,16 +725,14 @@ def update_properties(self, **kw): self.set_changed() self.changed_properties = True - - def del_record(self, id): - record = self.records[id] + def del_record(self, _id): + record = self.records[_id] if record is None: msg = 'cannot delete record "%s" because it was deleted before' - raise LookupError, msg % id + raise LookupError(msg % _id) # Change self.set_changed() - self.records[id] = None - + self.records[_id] = None def get_record_ids(self): i = 0 @@ -775,16 +741,13 @@ def get_record_ids(self): yield i i += 1 - def get_n_records(self): ids = self.get_record_ids() ids = list(ids) return len(ids) - def get_records(self): - return ( x for x in self.records if x ) - + return (x for x in self.records if x) def get_record_value(self, record, name, language=None): """This is the preferred method for accessing record values. It @@ -808,8 +771,8 @@ def get_record_value(self, record, name, language=None): return datatype.get_default() # Language negotiation ('select_language' is a built-in) if language is None: - languages = [ x.parameters['language'] for x in property - if not datatype.is_empty(x.value) ] + languages = [x.parameters['language'] for x in property + if not datatype.is_empty(x.value)] language = select_language(languages) if language is None and languages: # Pick up one at random (FIXME) @@ -834,19 +797,17 @@ def get_record_value(self, record, name, language=None): return [] return default # Hit - return [ x.value for x in property ] + return [x.value for x in property] # Simple properties if property is None: return datatype.get_default() return property.value - def get_property(self, name): record = self.properties return record.get_value(name) - def get_property_value(self, name): """Return the value if name is in record else if name is define in the schema @@ -865,11 +826,9 @@ def get_property_value(self, name): else: return getattr(datatype, 'default') - def search(self, key, value): get = self.get_record_value - return [ x for x in self.records if x and get(x, key) == value ] - + return [x for x in self.records if x and get(x, key) == value] def update_from_csv(self, data, columns, skip_header=False): """Update the table by adding record from data @@ -888,7 +847,6 @@ def update_from_csv(self, data, columns, skip_header=False): record[key] = line[index] self.add_record(record) - def to_csv(self, columns, separator=None, language=None): """Export the table to CSV handler. As table columns are unordered, the order comes from the "columns" @@ -912,7 +870,7 @@ def to_csv(self, columns, separator=None, language=None): # TODO represent multiple values message = ("multiple values are not supported, " "use a separator") - raise NotImplementedError, message + raise NotImplementedError(message) else: data = datatype.encode(value) line.append(data) diff --git a/itools/database/__init__.py b/itools/database/__init__.py index 929522828..a30117b34 100644 --- a/itools/database/__init__.py +++ b/itools/database/__init__.py @@ -17,16 +17,16 @@ # along with this program. If not, see . # Import from itools -from fields import Field, get_field_and_datatype -from queries import AllQuery, NotQuery, StartQuery, TextQuery -from queries import RangeQuery, PhraseQuery, AndQuery, OrQuery, pprint_query -from magic_ import magic_from_buffer, magic_from_file -from metadata import Metadata -from metadata_parser import MetadataProperty -from registry import get_register_fields, register_field -from resources import Resource -from ro import RODatabase, ReadonlyError -from rw import RWDatabase, make_database +from .fields import Field, get_field_and_datatype +from .queries import AllQuery, NotQuery, StartQuery, TextQuery +from .queries import RangeQuery, PhraseQuery, AndQuery, OrQuery, pprint_query +from .magic_ import magic_from_buffer, magic_from_file +from .metadata import Metadata +from .metadata_parser import MetadataProperty +from .registry import get_register_fields, register_field +from .resources import Resource +from .ro import RODatabase, ReadonlyError +from .rw import RWDatabase, make_database __all__ = [ diff --git a/itools/database/backends/__init__.py b/itools/database/backends/__init__.py index 356eff554..4c0da5bbe 100644 --- a/itools/database/backends/__init__.py +++ b/itools/database/backends/__init__.py @@ -17,6 +17,6 @@ # along with this program. If not, see . # Import from itools -from git import GitBackend -from lfs import LFSBackend -from registry import register_backend, backends_registry +from .git import GitBackend +from .lfs import LFSBackend +from .registry import register_backend, backends_registry diff --git a/itools/database/backends/catalog.py b/itools/database/backends/catalog.py index 6c420faf0..c6c377d23 100644 --- a/itools/database/backends/catalog.py +++ b/itools/database/backends/catalog.py @@ -19,14 +19,13 @@ # Import from the standard library import os -from copy import deepcopy from decimal import Decimal as decimal from datetime import datetime from marshal import dumps, loads from hashlib import sha1 # Import from xapian -from xapian import Database, WritableDatabase, DB_CREATE, DB_OPEN, DB_BACKEND_CHERT +from xapian import Database, WritableDatabase, DB_CREATE, DB_OPEN, DB_BACKEND_GLASS from xapian import Document, Query, QueryParser, Enquire from xapian import sortable_serialise, sortable_unserialise, TermGenerator @@ -35,18 +34,19 @@ from itools.datatypes import Decimal, Integer, Unicode, String from itools.fs import lfs from itools.i18n import is_punctuation -from itools.log import Logger, log_warning, log_info, register_logger +from logging import getLogger from itools.database.queries import AllQuery, _AndQuery, NotQuery, _OrQuery, PhraseQuery from itools.database.queries import RangeQuery, StartQuery, TextQuery, _MultipleQuery +log = getLogger("itools.database") try: from xapian import MultiValueSorter + XAPIAN_VERSION = '1.2' -except: +except Exception: from xapian import MultiValueKeyMaker - XAPIAN_VERSION = '1.4' - + XAPIAN_VERSION = '1.4' # Constants OP_AND = Query.OP_AND @@ -59,46 +59,58 @@ TQ_FLAGS = (QueryParser.FLAG_LOVEHATE + QueryParser.FLAG_PHRASE + QueryParser.FLAG_WILDCARD) -TRANSLATE_MAP = { ord(u'À'): ord(u'A'), - ord(u'Â'): ord(u'A'), - ord(u'â'): ord(u'a'), - ord(u'à'): ord(u'a'), - ord(u'Ç'): ord(u'C'), - ord(u'ç'): ord(u'c'), - ord(u'É'): ord(u'E'), - ord(u'Ê'): ord(u'E'), - ord(u'é'): ord(u'e'), - ord(u'ê'): ord(u'e'), - ord(u'è'): ord(u'e'), - ord(u'ë'): ord(u'e'), - ord(u'Î'): ord(u'I'), - ord(u'î'): ord(u'i'), - ord(u'ï'): ord(u'i'), - ord(u'ô'): ord(u'o'), - ord(u'û'): ord(u'u'), - ord(u'ù'): ord(u'u'), - ord(u'ü'): ord(u'u'), - ord(u"'"): ord(u' ') } +TRANSLATE_MAP = {ord('À'): ord('A'), + ord('Â'): ord('A'), + ord('â'): ord('a'), + ord('à'): ord('a'), + ord('Ç'): ord('C'), + ord('ç'): ord('c'), + ord('É'): ord('E'), + ord('Ê'): ord('E'), + ord('é'): ord('e'), + ord('ê'): ord('e'), + ord('è'): ord('e'), + ord('ë'): ord('e'), + ord('Î'): ord('I'), + ord('î'): ord('i'), + ord('ï'): ord('i'), + ord('ô'): ord('o'), + ord('û'): ord('u'), + ord('ù'): ord('u'), + ord('ü'): ord('u'), + ord("'"): ord(' ')} +MSG_NOT_INDEXED = 'the "{name}" field is not indexed' + + +def bytes_to_str(data): + for encoding in ["utf-8", "windows-1252", "latin-1"]: + try: + if isinstance(data, bytes): + return data.decode(encoding) + else: + return data + except: + pass + raise Exception(f"Type DATA {type(data)} value {data}") -MSG_NOT_INDEXED = 'the "{name}" field is not indexed' def warn_not_indexed(name): - log_warning(MSG_NOT_INDEXED.format(name=name), 'itools.database') + log.warning(MSG_NOT_INDEXED.format(name=name)) + MSG_NOT_STORED = 'the "{name}" field is not stored' + + def warn_not_stored(name): - log_warning(MSG_NOT_STORED.format(name=name), 'itools.database') + log.warning(MSG_NOT_STORED.format(name=name)) -MSG_NOT_INDEXED_NOR_STORED = 'the "{name}" field is not indexed nor stored' -def warn_not_indexed_nor_stored(name): - log_warning(MSG_NOT_INDEXED_NOR_STORED.format(name=name), 'itools.database') +MSG_NOT_INDEXED_NOR_STORED = 'the "{name}" field is not indexed nor stored' -class CatalogLogger(Logger): - def format(self, domain, level, message): - return message + '\n' +def warn_not_indexed_nor_stored(name): + log.warning(MSG_NOT_INDEXED_NOR_STORED.format(name=name)) class Doc(object): @@ -108,16 +120,15 @@ def __init__(self, xdoc, fields, metadata): self._fields = fields self._metadata = metadata - def __getattr__(self, name): # 1. Get the raw value info = self._metadata.get(name) if info is None: - raise AttributeError, MSG_NOT_INDEXED_NOR_STORED.format(name=name) + raise AttributeError(MSG_NOT_INDEXED_NOR_STORED.format(name=name)) stored = info.get('value') if stored is None: - raise AttributeError, MSG_NOT_STORED.format(name=name) + raise AttributeError(MSG_NOT_STORED.format(name=name)) raw_value = self._xdoc.get_value(stored) # 2. Decode @@ -155,7 +166,6 @@ def __getattr__(self, name): setattr(self, name, value) return value - def get_value(self, name, language=None): # Check if stored info = self._metadata.get(name) @@ -198,21 +208,18 @@ def get_value(self, name, language=None): return field_cls.get_default() - class SearchResults(object): def __init__(self, catalog, xquery): self._catalog = catalog self._xquery = xquery - @lazy def _enquire(self): enquire = Enquire(self._catalog._db) enquire.set_query(self._xquery) return enquire - @lazy def _max(self): enquire = self._enquire @@ -220,18 +227,15 @@ def _max(self): doccount = db.get_doccount() return enquire.get_mset(0, doccount).size() - def __len__(self): """Returns the number of documents found.""" return self._max - def search(self, query=None, **kw): xquery = _get_xquery(self._catalog, query, **kw) query = Query(Query.OP_AND, [self._xquery, xquery]) return self.__class__(self._catalog, query) - def get_documents(self, sort_by=None, reverse=False, start=0, size=0): """Returns the documents for the search, sorted by weight. @@ -297,8 +301,8 @@ def get_documents(self, sort_by=None, reverse=False, start=0, size=0): # Construction of the results fields = self._catalog._fields - results = [ Doc(x.document, fields, metadata) - for x in enquire.get_mset(start, size) ] + results = [Doc(x.document, fields, metadata) + for x in enquire.get_mset(start, size)] # sort_by=None/reverse=True if sort_by is None and reverse: @@ -307,11 +311,8 @@ def get_documents(self, sort_by=None, reverse=False, start=0, size=0): return results - class Catalog(object): - nb_changes = 0 - logger = None _db = None read_only = False @@ -332,7 +333,7 @@ def __init__(self, ref, fields, read_only=False, asynchronous_mode=True): self._asynchronous = asynchronous_mode self._fields = fields # FIXME: There's a bug in xapian: - # Wa cannot get stored values if DB not flushed + # We cannot get stored values if DB not flushed self.commit_each_transaction = True # Asynchronous mode if not read_only and asynchronous_mode: @@ -346,37 +347,27 @@ def __init__(self, ref, fields, read_only=False, asynchronous_mode=True): self._load_all_internal() if not read_only: self._init_all_metadata() - # Catalog log - if path: - catalog_log = '{}/catalog.log'.format(path) - self.logger = CatalogLogger(catalog_log) - register_logger(self.logger, 'itools.catalog') - - - def _init_all_metadata(self): + def _init_all_metadata(self, has_changes=False): """Init new metadata (to avoid 'field is not indexed' warning) """ - has_changes = False metadata = self._metadata for name, field_cls in self._fields.items(): if name not in metadata: - print('[Catalog] New field registered: {0}'.format(name)) + log.debug("[Catalog] New field registered: {0}".format(name)) has_changes = True metadata[name] = self._get_info(field_cls, name) else: # If the field was in the catalog but is newly stored - if (not metadata[name].has_key('value') and - getattr(field_cls, 'stored', False)): - print('[Catalog] Indexed field is now stored: {0}'.format(name)) + if 'value' not in metadata[name] and getattr(field_cls, 'stored', False): + log.debug("[Catalog] Indexed field is now stored: {0}".format(name)) has_changes = True metadata[name] = merge_dicts( metadata[name], self._get_info_stored()) # If the field was stored in the catalog but is newly indexed - if (not metadata[name].has_key('prefix') and - getattr(field_cls, 'indexed', False)): - print('[Catalog] Stored field is now indexed: {0}'.format(name)) + if 'prefix' not in metadata[name] and getattr(field_cls, 'indexed', False): + log.debug("[Catalog] Stored field is now indexed: {0}".format(name)) has_changes = True metadata[name] = merge_dicts( metadata[name], @@ -386,7 +377,6 @@ def _init_all_metadata(self): self._db.commit_transaction() self._db.begin_transaction(self.commit_each_transaction) - ####################################################################### # API / Public / Transactions ####################################################################### @@ -394,41 +384,35 @@ def save_changes(self): """Save the last changes to disk. """ if not self._asynchronous: - raise ValueError, "The transactions are synchronous" + raise ValueError("The transactions are synchronous") db = self._db db.commit_transaction() - db.commit() - # FIXME: There's a bug in xapian: - # Wa cannot get stored values if DB not flushed - #if self.nb_changes > 200: - # # XXX Not working since cancel_transaction() - # # cancel all transactions not commited to disk - # # We have to use new strategy to abort transaction - # db.commit() - # if self.logger: - # self.logger.clear() - # self.nb_changes = 0 + if self.commit_each_transaction: + db.commit() + else: + if self.nb_changes > 200: + db.commit() + self.nb_changes = 0 db.begin_transaction(self.commit_each_transaction) - def abort_changes(self): + """Abort the last changes made in memory. """ if not self._asynchronous: - raise ValueError, "The transactions are synchronous" + raise ValueError("The transactions are synchronous") db = self._db if self.commit_each_transaction: db.cancel_transaction() db.begin_transaction(self.commit_each_transaction) else: - raise NotImplementedError + db.cancel_transaction() + db.begin_transaction(self.commit_each_transaction) self._load_all_internal() - def close(self): if self._db is None: - msg = 'Catalog is already closed' - print(msg) + log.info("Catalog is already closed") return if self.read_only: self._db.close() @@ -437,22 +421,18 @@ def close(self): if self.commit_each_transaction: try: self._db.cancel_transaction() - except: - print('Warning: cannot cancel xapian transaction') + except Exception: + log.info("Warning: cannot cancel xapian transaction", exc_info=True) self._db.close() self._db = None else: self._db.close() self._db = None else: - self.abort_changes() self._db.commit_transaction() - self._db.flush() + self._db.commit() self._db.close() self._db = None - if self.logger: - self.logger.clear() - ####################################################################### # API / Public / (Un)Index @@ -461,9 +441,7 @@ def index_document(self, document): self.nb_changes += 1 abspath, term, xdoc = self.get_xdoc_from_document(document) self._db.replace_document(term, xdoc) - if self.logger: - log_info(abspath, domain='itools.catalog') - + log.debug("Indexed : {}".format(abspath)) def unindex_document(self, abspath): """Remove the document that has value stored in its abspath. @@ -471,10 +449,10 @@ def unindex_document(self, abspath): """ self.nb_changes += 1 data = _reduce_size(_encode(self._fields['abspath'], abspath)) + if type(data) is bytes: + data = data.decode("utf-8") self._db.delete_document('Q' + data) - if self.logger: - log_info(abspath, domain='itools.catalog') - + log.debug("Unindexed : {}".format(abspath)) def get_xdoc_from_document(self, doc_values): """Return (abspath, term, xdoc) from the document (resource or values as dict) @@ -489,7 +467,7 @@ def get_xdoc_from_document(self, doc_values): # Make the xapian document metadata_modified = False xdoc = Document() - for name, value in doc_values.iteritems(): + for name, value in doc_values.items(): if name not in fields: warn_not_indexed_nor_stored(name) field_cls = fields[name] @@ -510,12 +488,13 @@ def get_xdoc_from_document(self, doc_values): # the problem is that "_encode != _index" if name == 'abspath': key_value = _reduce_size(_encode(field_cls, value)) + key_value = bytes_to_str(key_value) term = 'Q' + key_value xdoc.add_term(term) # A multilingual value? if isinstance(value, dict): - for language, lang_value in value.iteritems(): + for language, lang_value in value.items(): lang_name = name + '_' + language # New field ? @@ -571,8 +550,7 @@ def get_unique_values(self, name): # Ok prefix = metadata[name]['prefix'] prefix_len = len(prefix) - return set([ t.term[prefix_len:] for t in self._db.allterms(prefix) ]) - + return set([t.term[prefix_len:] for t in self._db.allterms(prefix)]) ####################################################################### # API / Private @@ -583,8 +561,8 @@ def _get_info(self, field_cls, name): if not (issubclass(field_cls, String) and field_cls.stored and field_cls.indexed): - raise ValueError, ('the abspath field must be declared as ' - 'String(stored=True, indexed=True)') + raise ValueError(('the abspath field must be declared as ' + 'String(stored=True, indexed=True)')) # Stored ? info = {} if getattr(field_cls, 'stored', False): @@ -595,20 +573,16 @@ def _get_info(self, field_cls, name): # Ok return info - def _get_info_stored(self): value = self._value_nb self._value_nb += 1 return {'value': value} - def _get_info_indexed(self): prefix = _get_prefix(self._prefix_nb) self._prefix_nb += 1 return {'prefix': prefix} - - def _load_all_internal(self): """Load the metadata from the database """ @@ -616,17 +590,24 @@ def _load_all_internal(self): self._prefix_nb = 0 metadata = self._db.get_metadata('metadata') - if metadata == '': + + if metadata == b'': self._metadata = {} else: - self._metadata = loads(metadata) - for name, info in self._metadata.iteritems(): + try: + self._metadata = loads(metadata) + except ValueError: + # Reload metadata if incompatibility between Python 2 and Python 3 + self._init_all_metadata(has_changes=True) + metadata = self._db.get_metadata('metadata') + self._metadata = loads(metadata) + + for name, info in self._metadata.items(): if 'value' in info: self._value_nb += 1 if 'prefix' in info: self._prefix_nb += 1 - def _query2xquery(self, query): """take a "itools" query and return a "xapian" query """ @@ -642,7 +623,7 @@ def _query2xquery(self, query): if query_class is PhraseQuery: name = query.name if type(name) is not str: - raise TypeError, "unexpected '%s'" % type(name) + raise TypeError("unexpected '%s'" % type(name)) # If there is a problem => an empty result if name not in metadata: warn_not_indexed(name) @@ -651,7 +632,7 @@ def _query2xquery(self, query): try: prefix = info['prefix'] except KeyError: - raise ValueError, 'the field "%s" must be indexed' % name + raise ValueError('the field "%s" must be indexed' % name) field_cls = _get_field_cls(name, fields, info) return _make_PhraseQuery(field_cls, query.value, prefix) @@ -659,7 +640,7 @@ def _query2xquery(self, query): if query_class is RangeQuery: name = query.name if type(name) is not str: - raise TypeError, "unexpected '%s'" % type(name) + raise TypeError("unexpected '%s'" % type(name)) # If there is a problem => an empty result if name not in metadata: warn_not_indexed(name) @@ -668,11 +649,11 @@ def _query2xquery(self, query): info = metadata[name] value = info.get('value') if value is None: - raise AttributeError, MSG_NOT_STORED.format(name=name) + raise AttributeError(MSG_NOT_STORED.format(name=name)) field_cls = _get_field_cls(name, fields, info) if field_cls.multiple: error = 'range-query not supported on multiple fields' - raise ValueError, error + raise ValueError(error) left = query.left if left is not None: @@ -701,7 +682,7 @@ def _query2xquery(self, query): if query_class is StartQuery: name = query.name if type(name) is not str: - raise TypeError, "unexpected '%s'" % type(name) + raise TypeError("unexpected '%s'" % type(name)) # If there is a problem => an empty result if name not in metadata: warn_not_indexed(name) @@ -710,7 +691,7 @@ def _query2xquery(self, query): info = metadata[name] value_nb = info.get('value') if value_nb is None: - raise AttributeError, MSG_NOT_STORED.format(name=name) + raise AttributeError(MSG_NOT_STORED.format(name=name)) field_cls = _get_field_cls(name, fields, info) value = query.value @@ -750,7 +731,7 @@ def _query2xquery(self, query): if query_class is TextQuery: name = query.name if type(name) is not str: - raise TypeError, "unexpected %s for 'name'" % type(name) + raise TypeError("unexpected %s for 'name'" % type(name)) # If there is a problem => an empty result if name not in metadata: warn_not_indexed(name) @@ -761,12 +742,12 @@ def _query2xquery(self, query): try: prefix = info['prefix'] except KeyError: - raise ValueError, 'the field "%s" must be indexed' % name + raise ValueError('the field "%s" must be indexed' % name) # Remove accents from the value value = query.value - if type(value) is not unicode: - raise TypeError, "unexpected %s for 'value'" % type(value) + if type(value) is not str: + raise TypeError("unexpected %s for 'value'" % type(value)) value = value.translate(TRANSLATE_MAP) qp = QueryParser() @@ -780,18 +761,17 @@ def _query2xquery(self, query): # And if query_class is _AndQuery: - return Query(OP_AND, [ i2x(q) for q in query.atoms ]) + return Query(OP_AND, [i2x(q) for q in query.atoms]) # Or if query_class is _OrQuery: - return Query(OP_OR, [ i2x(q) for q in query.atoms ]) + return Query(OP_OR, [i2x(q) for q in query.atoms]) # Not if query_class is NotQuery: return Query(OP_AND_NOT, Query(''), i2x(query.query)) - def make_catalog(uri, fields): """Creates a new and empty catalog in the given uri. @@ -804,17 +784,12 @@ def make_catalog(uri, fields): 'name': Unicode(indexed=True), ...} """ path = lfs.get_absolute_path(uri) - db = WritableDatabase(path, DB_BACKEND_CHERT) - # FIXME GLASS backend seems to be buggy - # db = WritableDatabase(path, DB_CREATE) + db = WritableDatabase(path, DB_BACKEND_GLASS) return Catalog(db, fields) - ############# # Private API - - def _get_prefix(number): """By convention: Q is used for the unique Id of a document @@ -823,9 +798,8 @@ def _get_prefix(number): """ magic_letters = 'ABCDEFGHIJKLMNOPRSTUVWY' size = len(magic_letters) - result = 'X'*(number/size) - return result+magic_letters[number%size] - + result = 'X' * (number // size) + return result + magic_letters[number % size] def _decode_simple_value(field_cls, data): @@ -840,7 +814,6 @@ def _decode_simple_value(field_cls, data): return field_cls.decode(data) - def _decode(field_cls, data): # Singleton if not field_cls.multiple: @@ -851,8 +824,7 @@ def _decode(field_cls, data): value = loads(data) except (ValueError, MemoryError): return _decode_simple_value(field_cls, data) - return [ _decode_simple_value(field_cls, a_value) for a_value in value ] - + return [_decode_simple_value(field_cls, a_value) for a_value in value] # We must overload the normal behaviour (range + optimization) @@ -872,7 +844,6 @@ def _encode_simple_value(field_cls, value): return field_cls.encode(value) - def _encode(field_cls, value): """Used to encode values in stored fields. """ @@ -881,30 +852,29 @@ def _encode(field_cls, value): return _encode_simple_value(field_cls, value) # Case 2: Multiple - value = [ _encode_simple_value(field_cls, a_value) for a_value in value ] + value = [_encode_simple_value(field_cls, a_value) for a_value in value] return dumps(value) - def _get_field_cls(name, fields, info): return fields[name] if (name in fields) else fields[info['from']] - def _reduce_size(data): # 'data' must be a byte string # If the data is too long, we replace it by its sha1 # FIXME Visibly a bug in xapian counts twice the \x00 character # http://bugs.hforge.org/show_bug.cgi?id=940 - if len(data) + data.count("\x00") > 240: + if isinstance(data, str): + data = data.encode("utf-8") + if len(data) + data.count(b"\x00") > 240: return sha1(data).hexdigest() # All OK, we simply return the data return data - def _index_cjk(xdoc, value, prefix, termpos): """ Returns the next word and its position in the data. The analysis @@ -918,21 +888,21 @@ def _index_cjk(xdoc, value, prefix, termpos): 2 -> 0 [stop word] """ state = 0 - previous_cjk = u'' + previous_cjk = '' for c in value: if is_punctuation(c): # Stop word - if previous_cjk and state == 1: # CJK not yielded yet + if previous_cjk and state == 1: # CJK not yielded yet xdoc.add_posting(prefix + previous_cjk, termpos) termpos += 1 # reset state - previous_cjk = u'' + previous_cjk = '' state = 0 else: c = c.lower() if previous_cjk: - xdoc.add_posting(prefix + (u'%s%s' % (previous_cjk, c)), + xdoc.add_posting(prefix + ('%s%s' % (previous_cjk, c)), termpos) termpos += 1 state = 2 @@ -947,13 +917,14 @@ def _index_cjk(xdoc, value, prefix, termpos): return termpos + 1 - def _index_unicode(xdoc, value, prefix, language, termpos, TRANSLATE_MAP=TRANSLATE_MAP): + + value = bytes_to_str(value) # Check type - if type(value) is not unicode: + if type(value) is not str: msg = 'The value "%s", field "%s", is not a unicode' - raise TypeError, msg % (value, prefix) + raise TypeError(msg % (value, prefix)) # Case 1: Japanese or Chinese if language in ['ja', 'zh']: @@ -967,13 +938,12 @@ def _index_unicode(xdoc, value, prefix, language, termpos, value = value.translate(TRANSLATE_MAP) # XXX With the stemmer, the words are saved twice: # with prefix and with Zprefix -# tg.set_stemmer(stemmer) + # tg.set_stemmer(stemmer) tg.index_text(value, 1, prefix) return tg.get_termpos() + 1 - def _index(xdoc, field_cls, value, prefix, language): """To index a field it must be split in a sequence of words and positions: @@ -982,11 +952,12 @@ def _index(xdoc, field_cls, value, prefix, language): Where will be a value. """ + value = bytes_to_str(value) is_multiple = (field_cls.multiple and isinstance(value, (tuple, list, set, frozenset))) # Case 1: Unicode (a complex split) - if issubclass(field_cls, Unicode): + if issubclass(field_cls, Unicode) and value is not None: if is_multiple: termpos = 1 for x in value: @@ -998,15 +969,16 @@ def _index(xdoc, field_cls, value, prefix, language): for position, data in enumerate(value): data = _encode_simple_value(field_cls, data) data = _reduce_size(data) + data = bytes_to_str(data) xdoc.add_posting(prefix + data, position + 1) # Case 3: singleton else: data = _encode_simple_value(field_cls, value) data = _reduce_size(data) + data = bytes_to_str(data) xdoc.add_posting(prefix + data, 1) - def _make_PhraseQuery(field_cls, value, prefix): # Get the words # XXX It's too complex (slow), we must use xapian @@ -1020,13 +992,12 @@ def _make_PhraseQuery(field_cls, value, prefix): for termpos in term_list_item.positer: words.append((termpos, term)) words.sort() - words = [ word[1] for word in words ] + words = [word[1] for word in words] # Make the query return Query(OP_PHRASE, words) - def _get_xquery(catalog, query=None, **kw): # Case 1: a query is given if query is not None: @@ -1040,7 +1011,7 @@ def _get_xquery(catalog, query=None, **kw): metadata = catalog._metadata fields = catalog._fields xqueries = [] - for name, value in kw.iteritems(): + for name, value in kw.items(): # If name is a field not yet indexed, return nothing if name not in metadata: warn_not_indexed(name) diff --git a/itools/database/backends/git.py b/itools/database/backends/git.py index cd5b2dc8a..77eeabaef 100644 --- a/itools/database/backends/git.py +++ b/itools/database/backends/git.py @@ -32,14 +32,17 @@ from itools.database.magic_ import magic_from_buffer from itools.database.git import open_worktree from itools.fs import lfs +from itools.fs.common import WRITE, READ_WRITE, APPEND, READ # Import from here -from catalog import Catalog, _get_xquery, SearchResults, make_catalog -from registry import register_backend +from .catalog import Catalog, _get_xquery, SearchResults, make_catalog +from .registry import register_backend TEST_DB_WITHOUT_COMMITS = bool(int(os.environ.get('TEST_DB_WITHOUT_COMMITS') or 0)) TEST_DB_DESACTIVATE_GIT = bool(int(os.environ.get('TEST_DB_DESACTIVATE_GIT') or 0)) +TEST_DB_DESACTIVATE_STATIC_HISTORY = bool(int(os.environ.get('TEST_DB_DESACTIVATE_STATIC_HISTORY') or 1)) +TEST_DB_DESACTIVATE_PATCH = bool(int(os.environ.get('TEST_DESACTIVATE_PATCH') or 1)) class Heap(object): @@ -67,15 +70,12 @@ def __init__(self): self._dict = {} self._heap = [] - def __len__(self): return len(self._dict) - def get(self, path): return self._dict.get(path) - def __setitem__(self, path, value): if path not in self._dict: n = -path.count('/') if path else 1 @@ -83,14 +83,12 @@ def __setitem__(self, path, value): self._dict[path] = value - def popitem(self): key = heappop(self._heap) path = key[1] return path, self._dict.pop(path) - class GitBackend(object): def __init__(self, path, fields, read_only=False): @@ -107,7 +105,10 @@ def __init__(self, path, fields, read_only=False): error = '"{0}" should be a folder, but it is not'.format(self.path_data) raise ValueError(error) # New interface to Git - self.worktree = open_worktree(self.path_data) + if TEST_DB_DESACTIVATE_GIT is True: + self.worktree = None + else: + self.worktree = open_worktree(self.path_data) # Initialize the database, but chrooted self.fs = lfs.open(self.path_data) # Static FS @@ -118,7 +119,6 @@ def __init__(self, path, fields, read_only=False): # Catalog self.catalog = self.get_catalog() - @classmethod def init_backend(cls, path, fields, init=False, soft=False): # Metadata database @@ -128,14 +128,12 @@ def init_backend(cls, path, fields, init=False, soft=False): # Make catalog make_catalog('{0}/catalog'.format(path), fields) - @classmethod def init_backend_static(cls, path): # Static database lfs.make_folder('{0}/database_static'.format(path)) lfs.make_folder('{0}/database_static/.history'.format(path)) - ####################################################################### # Database API ####################################################################### @@ -148,56 +146,49 @@ def normalize_key(self, path, __root=None): raise ValueError(err.format(path)) return '/'.join(key) - def handler_exists(self, key): fs = self.get_handler_fs_by_key(key) return fs.exists(key) - def get_handler_names(self, key): return self.fs.get_names(key) - - def get_handler_data(self, key): + def get_handler_data(self, key, text=False): if not key: return None fs = self.get_handler_fs_by_key(key) - with fs.open(key) as f: + with fs.open(key, text=text) as f: return f.read() - def get_handler_mimetype(self, key): data = self.get_handler_data(key) return magic_from_buffer(data) - def handler_is_file(self, key): fs = self.get_handler_fs_by_key(key) return fs.is_file(key) - def handler_is_folder(self, key): fs = self.get_handler_fs_by_key(key) return fs.is_folder(key) - def get_handler_mtime(self, key): fs = self.get_handler_fs_by_key(key) return fs.get_mtime(key) - def save_handler(self, key, handler): data = handler.to_str() + text = isinstance(data, str) # Save the file fs = self.get_handler_fs(handler) # Write and truncate (calls to "_save_state" must be done with the # pointer pointing to the beginning) if not fs.exists(key): - with fs.make_file(key) as f: + with fs.make_file(key, text=text) as f: f.write(data) f.truncate(f.tell()) else: - with fs.open(key, 'w') as f: + with fs.open(key, text=text, mode=READ_WRITE) as f: f.write(data) f.truncate(f.tell()) # Set dirty = None @@ -205,23 +196,19 @@ def save_handler(self, key, handler): handler.dirty = None - def traverse_resources(self): raise NotImplementedError - def get_handler_fs(self, handler): if isinstance(handler, Metadata): return self.fs return self.static_fs - def get_handler_fs_by_key(self, key): if key.endswith('metadata'): return self.fs return self.static_fs - def add_handler_into_static_history(self, key): the_time = datetime.now().strftime('%Y%m%d%H%M%S') new_key = '.history/{0}.{1}.{2}'.format(key, the_time, uuid4()) @@ -230,12 +217,13 @@ def add_handler_into_static_history(self, key): self.static_fs.make_folder(parent_path) self.static_fs.copy(key, new_key) - def create_patch(self, added, changed, removed, handlers, git_author): """ We create a patch into database/.git/patchs at each transaction. The idea is to commit into GIT each N transactions on big databases to avoid performances problems. We want to keep a diff on each transaction, to help debug. """ + if TEST_DB_DESACTIVATE_PATCH is True: + return author_id, author_email = git_author diffs = {} # Added @@ -276,17 +264,17 @@ def create_patch(self, added, changed, removed, handlers, git_author): f.write(data) f.truncate(f.tell()) - def do_transaction(self, commit_message, data, added, changed, removed, handlers, docs_to_index, docs_to_unindex): git_author, git_date, git_msg, docs_to_index, docs_to_unindex = data # Statistics self.nb_transactions += 1 # Add static changed & removed files to ~/database_static/.history/ - changed_and_removed = list(changed) + list(removed) - for key in changed_and_removed: - if not key.endswith('metadata'): - self.add_handler_into_static_history(key) + if TEST_DB_DESACTIVATE_STATIC_HISTORY is False: + changed_and_removed = list(changed) + list(removed) + for key in changed_and_removed: + if not key.endswith('metadata'): + self.add_handler_into_static_history(key) # Create patch if there's changed if added or changed or removed: self.create_patch(added, changed, removed, handlers, git_author) @@ -322,7 +310,6 @@ def do_transaction(self, commit_message, data, added, changed, removed, handlers self.catalog.index_document(values) self.catalog.save_changes() - def do_git_big_commit(self): """ Some databases are really bigs (1 millions files). GIT is too slow in this cases. So we don't commit at each transaction, but at each N transactions. @@ -333,28 +320,25 @@ def do_git_big_commit(self): p1.start() self.last_transaction_dtime = datetime.now() - def _do_git_big_commit(self): - worktree = self.worktree - worktree._call(['git', 'add', '-A']) - worktree._call(['git', 'commit', '-m', 'Autocommit']) - + self.worktree._call(['git', 'add', '-A']) + self.worktree._call(['git', 'commit', '-m', 'Autocommit']) def do_git_transaction(self, commit_message, data, added, changed, removed, handlers): worktree = self.worktree # 3. Git add git_add = list(added) + list(changed) git_add = [x for x in git_add if x.endswith('metadata')] - worktree.git_add(*git_add) + self.worktree.git_add(*git_add) # 3. Git rm git_rm = list(removed) git_rm = [x for x in git_rm if x.endswith('metadata')] - worktree.git_rm(*git_rm) + self.worktree.git_rm(*git_rm) # 2. Build the 'git commit' command git_author, git_date, git_msg, docs_to_index, docs_to_unindex = data git_msg = git_msg or 'no comment' # 4. Create the tree - repo = worktree.repo + repo = self.worktree.repo index = repo.index try: head = repo.revparse_single('HEAD') @@ -414,8 +398,7 @@ def do_git_transaction(self, commit_message, data, added, changed, removed, hand else: tb.insert(name, value[0], value[1]) # 5. Git commit - worktree.git_commit(git_msg, git_author, git_date, tree=git_tree) - + self.worktree.git_commit(git_msg, git_author, git_date, tree=git_tree) def abort_transaction(self): self.catalog.abort_changes() @@ -426,13 +409,12 @@ def abort_transaction(self): #else: # self.worktree.repo.checkout_head(strategy) - def flush_catalog(self, docs_to_unindex, docs_to_index): for path in docs_to_unindex: self.catalog.unindex_document(path) for resource, values in docs_to_index: self.catalog.index_document(values) - + self.catalog.save_changes() def get_catalog(self): path = '{0}/catalog'.format(self.path) @@ -440,7 +422,6 @@ def get_catalog(self): return None return Catalog(path, self.fields, read_only=self.read_only) - def search(self, query=None, **kw): """Launch a search in the catalog. """ @@ -448,7 +429,6 @@ def search(self, query=None, **kw): xquery = _get_xquery(catalog, query, **kw) return SearchResults(catalog, xquery) - def close(self): self.catalog.close() diff --git a/itools/database/backends/lfs.py b/itools/database/backends/lfs.py index 82174b2e6..957c3705a 100644 --- a/itools/database/backends/lfs.py +++ b/itools/database/backends/lfs.py @@ -16,12 +16,13 @@ # Import from the Standard Library from os.path import abspath, dirname +import mimetypes # Import from itools -from itools.fs import lfs +from itools.fs import lfs, WRITE # Import from here -from registry import register_backend +from .registry import register_backend class LFSBackend(object): @@ -32,64 +33,56 @@ def __init__(self, path, fields, read_only=False): else: self.fs = lfs - @classmethod def init_backend(cls, path, fields, init=False, soft=False): - self.fs.make_folder('{0}/database'.format(path)) - + lfs.make_folder('{0}/database'.format(path)) def normalize_key(self, path, __root=None): return self.fs.normalize_key(path) - def handler_exists(self, key): return self.fs.exists(key) - def get_handler_names(self, key): return self.fs.get_names(key) - - def get_handler_data(self, key): + def get_handler_data(self, key, text=False): if not key: return None - with self.fs.open(key) as f: - return f.read() - + with self.fs.open(key, text=text) as f: + if isinstance(f, str): + return f + else: + return f.read() def get_handler_mimetype(self, key): return self.fs.get_mimetype(key) - def handler_is_file(self, key): return self.fs.is_file(key) - def handler_is_folder(self, key): return self.fs.is_folder(key) - def get_handler_mtime(self, key): return self.fs.get_mtime(key) - def save_handler(self, key, handler): data = handler.to_str() + text = isinstance(data, str) # Save the file if not self.fs.exists(key): - with self.fs.make_file(key) as f: + with self.fs.make_file(key, text=text) as f: f.write(data) f.truncate(f.tell()) else: - with self.fs.open(key, 'w') as f: + with self.fs.open(key, WRITE, text=text) as f: f.write(data) f.truncate(f.tell()) - def traverse_resources(self): raise NotImplementedError - def do_transaction(self, commit_message, data, added, changed, removed, handlers): # List of Changed added_and_changed = list(added) + list(changed) @@ -103,11 +96,12 @@ def do_transaction(self, commit_message, data, added, changed, removed, handlers for key in removed: self.fs.remove(key) - - def abort_transaction(self): # Cannot abort transaction with this backend pass + def close(self): + self.fs.close() + register_backend('lfs', LFSBackend) diff --git a/itools/database/backends/registry.py b/itools/database/backends/registry.py index b5f4055a7..83456c3d0 100644 --- a/itools/database/backends/registry.py +++ b/itools/database/backends/registry.py @@ -16,7 +16,6 @@ # Import from the Standard Library - backends_registry = {} def register_backend(name, backends_cls): diff --git a/itools/database/exceptions.py b/itools/database/exceptions.py index 070e620e9..197a82e37 100644 --- a/itools/database/exceptions.py +++ b/itools/database/exceptions.py @@ -15,6 +15,6 @@ # along with this program. If not, see . -class ReadonlyError(StandardError): +class ReadonlyError(Exception): pass diff --git a/itools/database/fields.py b/itools/database/fields.py index 642112c97..841ab056a 100644 --- a/itools/database/fields.py +++ b/itools/database/fields.py @@ -30,22 +30,19 @@ class Field(prototype): multiple = False empty_values = (None, '', [], (), {}) base_error_messages = { - 'invalid': MSG(u'Invalid value.'), - 'required': MSG(u'This field is required.'), + 'invalid': MSG('Invalid value.'), + 'required': MSG('This field is required.'), } error_messages = {} validators = [] - def get_datatype(self): return self.datatype - def access(self, mode, resource): # mode may be "read" or "write" return True - def get_validators(self): validators = [] for v in self.validators: @@ -54,7 +51,6 @@ def get_validators(self): validators.append(v) return validators - def get_error_message(self, code): messages = merge_dicts( self.base_error_messages, @@ -62,7 +58,6 @@ def get_error_message(self, code): return messages.get(code) - def get_field_and_datatype(elt): """ Now schema can be Datatype or Field. To be compatible: diff --git a/itools/database/git.py b/itools/database/git.py index b0c38f9d8..1ed5df1cc 100644 --- a/itools/database/git.py +++ b/itools/database/git.py @@ -54,7 +54,6 @@ def make_parent_dirs(path): makedirs(folder) - class Worktree(object): def __init__(self, path, repo): @@ -69,11 +68,11 @@ def __init__(self, path, repo): try: _, _ = self.username, self.useremail except: - print '=========================================' - print 'ERROR: Please configure GIT commiter via' - print ' $ git config --global user.name' - print ' $ git config --global user.email' - print '=========================================' + print('========================================') + print('ERROR: Please configure GIT commiter via') + print(' $ git config --global user.name') + print(' $ git config --global user.email') + print('=========================================') raise @@ -88,12 +87,11 @@ def _get_abspath(self, path): if isabs(path): if path.startswith(self.path): return path - raise ValueError, 'unexpected absolute path "%s"' % path + raise ValueError("unexpected absolute path '{}'".format(path)) if path == '.': return self.path return '%s%s' % (self.path, path) - def _call(self, command): """Interface to cal git.git for functions not yet implemented using libgit2. @@ -101,10 +99,9 @@ def _call(self, command): popen = Popen(command, stdout=PIPE, stderr=PIPE, cwd=self.path) stdoutdata, stderrdata = popen.communicate() if popen.returncode != 0: - raise EnvironmentError, (popen.returncode, stderrdata) + raise EnvironmentError((popen.returncode, stderrdata)) return stdoutdata - def _resolve_reference(self, reference): """This method returns the SHA the given reference points to. For now only HEAD is supported. @@ -125,7 +122,6 @@ def _resolve_reference(self, reference): return reference.target - ####################################################################### # External API ####################################################################### @@ -144,13 +140,13 @@ def walk(self, path='.'): """ # 1. Check and normalize path if isabs(path): - raise ValueError, 'unexpected absolute path "%s"' % path + raise ValueError('unexpected absolute path "%s"' % path) path = normpath(path) if path == '.': path = '' elif path == '.git': - raise ValueError, 'cannot walk .git' + raise ValueError('cannot walk .git') elif not isdir('%s%s' % (self.path, path)): yield path return @@ -173,7 +169,6 @@ def walk(self, path='.'): yield path_rel - def lookup(self, sha): """Return the object by the given SHA. We use a cache to warrant that two calls with the same SHA will resolve to the same object, so the @@ -185,7 +180,6 @@ def lookup(self, sha): return cache[sha] - def lookup_from_commit_by_path(self, commit, path): """Return the object (tree or blob) the given path points to from the given commit, or None if the given path does not exist. @@ -204,7 +198,6 @@ def lookup_from_commit_by_path(self, commit, path): obj = self.lookup(entry.oid) return obj - @property def index(self): """Gives access to the index file. Reloads the index file if it has @@ -216,7 +209,7 @@ def index(self): index = self.repo.index # Bare repository if index is None: - raise RuntimeError, 'expected standard repository, not bare' + raise RuntimeError('expected standard repository, not bare') path = self.index_path if exists(path): @@ -227,7 +220,6 @@ def index(self): return index - def update_tree_cache(self): """libgit2 is able to read the tree cache, but not to write it. To speed up 'git_commit' this method should be called from time to @@ -236,7 +228,6 @@ def update_tree_cache(self): command = ['git', 'write-tree'] self._call(command) - def git_add(self, *args): """Equivalent 'git add', adds the given paths to the index file. If a path is a folder, adds all its content recursively. @@ -247,7 +238,6 @@ def git_add(self, *args): if path[-1] != '/': index.add(path) - def git_rm(self, *args): """Equivalent to 'git rm', removes the given paths from the index file and from the filesystem. If a path is a folder removes all @@ -269,7 +259,6 @@ def git_rm(self, *args): remove('%s/%s' % (root, name)) rmdir(root) - def git_mv(self, source, target, add=True): """Equivalent to 'git mv': moves the file or folder in the filesystem from 'source' to 'target', removes the source from the index file, @@ -296,7 +285,6 @@ def git_mv(self, source, target, add=True): if add is True: self.git_add(target) - @lazy def username(self): cmd = ['git', 'config', '--get', 'user.name'] @@ -306,7 +294,6 @@ def username(self): raise ValueError("Please configure 'git config --global user.name'") return username - @lazy def useremail(self): cmd = ['git', 'config', '--get', 'user.email'] @@ -316,7 +303,6 @@ def useremail(self): raise ValueError("Please configure 'git config --global user.email'") return useremail - def git_tag(self, tag_name, message): """Equivalent to 'git tag', we must give the name of the tag and the message TODO Implement using libgit2 @@ -326,14 +312,12 @@ def git_tag(self, tag_name, message): cmd = ['git', 'tag', '-a', tag_name, '-m', message] return self._call(cmd) - def git_remove_tag(self, tag_name): if not tag_name: raise ValueError('excepted tag name') cmd = ['git', 'tag', '-d', tag_name] return self._call(cmd) - def git_reset(self, reference): """Equivalent to 'git reset --hard', we must provide the reference to reset to """ @@ -342,7 +326,6 @@ def git_reset(self, reference): cmd = ['git', 'reset', '--hard', '-q', reference] return self._call(cmd) - def git_commit(self, message, author=None, date=None, tree=None): """Equivalent to 'git commit', we must give the message and we can also give the author and date. @@ -368,7 +351,12 @@ def git_commit(self, message, author=None, date=None, tree=None): name = self.username email = self.useremail - committer = Signature(name, email, when_time, when_offset) + if not isinstance(name, str): + name = name.decode("utf-8") + if not isinstance(email, str): + email = email.decode("utf-8") + + committer = Signature(name, email, int(when_time), int(when_offset)) # Author if author is None: @@ -383,15 +371,14 @@ def git_commit(self, message, author=None, date=None, tree=None): when_offset = date.utcoffset().seconds / 60 else: err = "Worktree.git_commit doesn't support naive datatime yet" - raise NotImplementedError, err + raise NotImplementedError(err) - author = Signature(author[0], author[1], when_time, when_offset) + author = Signature(author[0], author[1], int(when_time), int(when_offset)) # Create the commit return self.repo.create_commit('HEAD', author, committer, message, tree, parents) - def git_log(self, paths=None, n=None, author=None, grep=None, reverse=False, reference='HEAD'): """Equivalent to 'git log', optional keyword parameters are: @@ -458,7 +445,6 @@ def git_log(self, paths=None, n=None, author=None, grep=None, # Ok return commits - def git_diff(self, since, until=None, paths=None): """Return the diff between two commits, eventually reduced to the given paths. @@ -476,7 +462,6 @@ def git_diff(self, since, until=None, paths=None): cmd.extend(paths) return self._call(cmd) - def git_stats(self, since, until=None, paths=None): """Return statistics of the changes done between two commits, eventually reduced to the given paths. @@ -494,7 +479,6 @@ def git_stats(self, since, until=None, paths=None): cmd.extend(paths) return self._call(cmd) - def get_files_changed(self, since, until): """Return the files that have been changed between two commits. @@ -504,8 +488,7 @@ def get_files_changed(self, since, until): cmd = ['git', 'show', '--numstat', '--pretty=format:', expr] data = self._call(cmd) lines = data.splitlines() - return frozenset([ line.split('\t')[-1] for line in lines if line ]) - + return frozenset([line.split('\t')[-1] for line in lines if line]) def get_metadata(self, reference='HEAD'): """Resolves the given reference and returns metadata information @@ -532,7 +515,6 @@ def get_metadata(self, reference='HEAD'): } - def open_worktree(path, init=False, soft=False): try: if init: diff --git a/itools/database/metadata.py b/itools/database/metadata.py index 96989eb4e..fd81a1e35 100644 --- a/itools/database/metadata.py +++ b/itools/database/metadata.py @@ -15,17 +15,19 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +from logging import getLogger + # Import from itools from itools.core import add_type, freeze from itools.datatypes import String from itools.handlers import File, register_handler_class -from itools.log import log_warning # Import from here -from fields import Field -from metadata_parser import parse_table, MetadataProperty, property_to_str -from metadata_parser import deserialize_parameters +from .fields import Field +from .metadata_parser import parse_table, MetadataProperty, property_to_str +from .metadata_parser import deserialize_parameters +log = getLogger("itools.database") class DefaultField(Field): @@ -35,7 +37,7 @@ class DefaultField(Field): parameters_schema = freeze({}) parameters_schema_default = None multilingual = False - + encrypted = False class Metadata(File): @@ -50,7 +52,6 @@ def reset(self): self.version = None self.properties = {} - def __init__(self, key=None, string=None, database=None, cls=None, **kw): self.cls = cls self.database = database @@ -58,42 +59,38 @@ def __init__(self, key=None, string=None, database=None, cls=None, **kw): proxy = super(Metadata, self) proxy.__init__(key=key, string=string, database=database, **kw) - def new(self, cls=None, format=None, version=None): self.cls = cls self.format = format or cls.class_id self.version = version or cls.class_version - def get_resource_class(self, class_id): if self.cls: return self.cls return self.database.get_resource_class(class_id) - def change_class_id(self, new_class_id): self.cls = None self.set_changed() self.format = new_class_id self.get_resource_class(new_class_id) - def _load_state_from_file(self, file): properties = self.properties data = file.read() parser = parse_table(data) # Read the format & version - name, value, parameters = parser.next() + name, value, parameters = next(parser) if name != 'format': - raise ValueError, 'unexpected "%s" property' % name + raise ValueError('unexpected "%s" property' % name) if 'version' in parameters: version = parameters.pop('version') if len(version) > 1: - raise ValueError, 'version parameter cannot be repeated' + raise ValueError('version parameter cannot be repeated') self.version = version[0] if parameters: - raise ValueError, 'unexpected parameters for the format property' + raise ValueError('unexpected parameters for the format property') self.format = value # Get the schema resource_class = self.get_resource_class(self.format) @@ -101,7 +98,7 @@ def _load_state_from_file(self, file): # Parse for name, value, parameters in parser: if name == 'format': - raise ValueError, 'unexpected "format" property' + raise ValueError('unexpected "format" property') # 1. Get the field field = resource_class.get_field(name) @@ -109,10 +106,10 @@ def _load_state_from_file(self, file): msg = 'unexpected field "{0}" in resource {1}, cls {2}' msg = msg.format(name, self.key, resource_class) if resource_class.fields_soft: - log_warning(msg, domain='itools.database') + log.warning(msg) field = DefaultField else: - raise ValueError, msg + raise ValueError(msg) # 2. Deserialize the parameters params_schema = field.parameters_schema @@ -120,17 +117,20 @@ def _load_state_from_file(self, file): try: deserialize_parameters(parameters, params_schema, params_default) - except ValueError, e: + except ValueError as e: msg = 'in class "{0}", resource {1} property "{2}": {3}' - raise ValueError, msg.format(resource_class, self.key, name, e) + raise ValueError(msg.format(resource_class, self.key, name, e)) # 3. Get the datatype properties if field.multiple and field.multilingual: error = 'property "%s" is both multilingual and multiple' - raise ValueError, error % name + raise ValueError(error % name) # 4. Build the property datatype = field.datatype + datatype.encrypted = field.encrypted + if datatype.encrypted: + value = datatype.decrypt(value) property = MetadataProperty(value, datatype, **parameters) # Case 1: Multilingual @@ -148,7 +148,6 @@ def _load_state_from_file(self, file): else: properties[name] = property - def to_str(self): resource_class = self.get_resource_class(self.format) @@ -158,8 +157,7 @@ def to_str(self): lines = ['format;version=%s:%s\n' % (self.version, self.format)] # Properties are to be sorted by alphabetical order properties = self.properties - names = properties.keys() - names.sort() + names = sorted(list(properties.keys())) # Properties for name in names: @@ -171,17 +169,17 @@ def to_str(self): msg = 'unexpected field "{0}" in resource "{1}" (format "{2}")' msg = msg.format(name, self.key, self.format) if resource_class.fields_soft: - log_warning(msg, domain='itools.database') + log.warning(msg) continue - raise ValueError, msg - + raise ValueError(msg) datatype = field.datatype + datatype.encrypted = field.encrypted params_schema = field.parameters_schema is_empty = datatype.is_empty p_type = type(property) if p_type is dict: - languages = property.keys() - languages.sort() + languages = list(property.keys()) + languages = sorted(languages) lines += [ property_to_str(name, property[x], datatype, params_schema) for x in languages if not is_empty(property[x].value) ] @@ -197,7 +195,6 @@ def to_str(self): return ''.join(lines) - ######################################################################## # API ######################################################################## @@ -241,7 +238,6 @@ def get_property(self, name, language=None): return property[language] - def has_property(self, name, language=None): if name not in self.properties: return False @@ -251,7 +247,6 @@ def has_property(self, name, language=None): return True - def _set_property(self, name, value): properties = self.properties @@ -290,12 +285,10 @@ def _set_property(self, name, value): if not field.datatype.is_empty(value.value): properties.setdefault(name, []).append(value) - def set_property(self, name, value): self.set_changed() self._set_property(name, value) - def del_property(self, name): if name in self.properties: self.set_changed() diff --git a/itools/database/metadata_parser.py b/itools/database/metadata_parser.py old mode 100644 new mode 100755 index 3689a8d62..8b41b0ff4 --- a/itools/database/metadata_parser.py +++ b/itools/database/metadata_parser.py @@ -35,6 +35,20 @@ ('\r', r'\r'), ('\n', r'\n')) +def decode_lines(lines): + new_lines = [] + for line in lines: + if type(line) is bytes: + for encoding in ["utf-8", "latin-1"]: + try: + line = line.decode(encoding) + break + except: + pass + if type(line) is bytes: + raise Exception("Error decoding lines") + new_lines.append(line) + return new_lines def unescape_data(data, escape_table=escape_table): """Unescape the data @@ -48,7 +62,6 @@ def unescape_data(data, escape_table=escape_table): return '\\'.join(out) - def escape_data(data, escape_table=escape_table): """Escape the data """ @@ -58,13 +71,12 @@ def escape_data(data, escape_table=escape_table): return data - def unfold_lines(data): """Unfold the folded lines. """ i = 0 lines = data.splitlines() - + lines = decode_lines(lines) line = '' while i < len(lines): next = lines[i] @@ -79,7 +91,6 @@ def unfold_lines(data): yield line - def fold_line(data): """Fold the unfolded line over 75 characters. """ @@ -104,7 +115,6 @@ def fold_line(data): return res - # XXX The RFC only allows '-', we allow more because this is used by # itools.database (metadata). This set matches checkid (itools.handlers) allowed = frozenset(['-', '_', '.', '@']) @@ -118,7 +128,7 @@ def read_name(line, allowed=allowed): # Test first character of name c = line[0] if not c.isalnum() and c != '-': - raise SyntaxError, 'unexpected character (%s)' % c + raise SyntaxError('unexpected character (%s)' % c) # Test the rest idx = 1 @@ -130,9 +140,9 @@ def read_name(line, allowed=allowed): if c.isalnum() or c in allowed: idx += 1 continue - raise SyntaxError, "unexpected character '%s' (%s)" % (c, ord(c)) + raise SyntaxError("unexpected character '%s' (%s)" % (c, ord(c))) - raise SyntaxError, 'unexpected end of line (%s)' % line + raise SyntaxError('unexpected end of line (%s)' % line) # Manage an icalendar content line value property [with parameters] : @@ -175,7 +185,7 @@ def get_tokens(property): if c.isalnum() or c in ('-'): param_name, status = c, 2 else: - raise SyntaxError, error1 % (c, status) + raise SyntaxError(error1 % (c, status)) # param-name begun elif status == 2: @@ -185,7 +195,7 @@ def get_tokens(property): parameters[param_name] = [] status = 3 else: - raise SyntaxError, error1 % (c, status) + raise SyntaxError(error1 % (c, status)) # param-name ended, param-value beginning elif status == 3: @@ -230,7 +240,7 @@ def get_tokens(property): parameters[param_name].append(param_value) status = 3 elif c == '"': - raise SyntaxError, error1 % (c, status) + raise SyntaxError(error1 % (c, status)) else: param_value += c @@ -248,17 +258,16 @@ def get_tokens(property): elif c == ',': status = 3 else: - raise SyntaxError, error1 % (c, status) + raise SyntaxError(error1 % (c, status)) if status not in (7, 8): - raise SyntaxError, 'unexpected property (%s)' % property + raise SyntaxError('unexpected property (%s)' % property) # Unescape special characters (TODO Check the spec) value = unescape_data(value) return value, parameters - def parse_table(data): """This is the public interface of the module "itools.ical.parser", a low-level parser of iCalendar files. @@ -278,7 +287,6 @@ def parse_table(data): yield name, value, parameters - ########################################################################### # Helper functions ########################################################################### @@ -294,7 +302,7 @@ def deserialize_parameters(parameters, schema, default=String(multiple=True)): for name in parameters: datatype = schema.get(name, default) if datatype is None: - raise ValueError, 'parameter "{0}" not defined'.format(name) + raise ValueError('parameter "{0}" not defined'.format(name)) # Decode value = parameters[name] value = [ decode_param_value(x, datatype) for x in value ] @@ -302,13 +310,12 @@ def deserialize_parameters(parameters, schema, default=String(multiple=True)): if not datatype.multiple: if len(value) > 1: msg = 'parameter "%s" must be a singleton' - raise ValueError, msg % name + raise ValueError(msg % name) value = value[0] # Update parameters[name] = value - class MetadataProperty(object): """A property has a value, and may have one or more parameters. @@ -329,29 +336,25 @@ def value(self): return self.datatype.decode(self.raw_value) return self.raw_value - def clone(self): # Copy the value and parameters value = deepcopy(self.value) parameters = {} - for p_key, p_value in self.parameters.iteritems(): + for p_key, p_value in self.parameters.items(): c_value = deepcopy(p_value) parameters[p_key] = c_value return MetadataProperty(value, self.datatype, **parameters) - def get_parameter(self, name, default=None): if self.parameters is None: return default return self.parameters.get(name, default) - def set_parameter(self, name, value): if self.parameters is None: self.parameters = {} self.parameters[name] = value - def __eq__(self, other): if type(other) is not MetadataProperty: return False @@ -359,14 +362,10 @@ def __eq__(self, other): return False return self.parameters == other.parameters - def __ne__(self, other): return not self.__eq__(other) - - - params_escape_table = ( ('"', r'\"'), ('\r', r'\r'), @@ -384,7 +383,7 @@ def encode_param_value(p_name, p_value, p_datatype): # Standard case (ical behavior) if '"' in p_value or '\n' in p_value: error = 'the "%s" parameter contains a double quote' - raise ValueError, error % p_name + raise ValueError(error % p_name) if ';' in p_value or ':' in p_value or ',' in p_value: return '"%s"' % p_value return p_value @@ -408,8 +407,8 @@ def _property_to_str(name, property, datatype, p_schema, encoding='utf-8'): """ # Parameters if property.parameters: - p_names = property.parameters.keys() - p_names.sort() + p_names = list(property.parameters.keys()) + p_names = sorted(p_names) else: p_names = [] @@ -439,9 +438,11 @@ def _property_to_str(name, property, datatype, p_schema, encoding='utf-8'): else: value = datatype.encode(property.value) if type(value) is not str: - raise ValueError, 'property "{0}" is not str but {1}'.format( - name, type(value)) + raise ValueError('property "{0}" is not str but {1}'.format( + name, type(value))) value = escape_data(value) + if datatype.encrypted: + value = datatype.encrypt(value) # Ok property = '%s%s:%s\n' % (name, parameters, value) @@ -451,6 +452,6 @@ def _property_to_str(name, property, datatype, p_schema, encoding='utf-8'): def property_to_str(name, property, datatype, p_schema, encoding='utf-8'): try: return _property_to_str(name, property, datatype, p_schema, encoding) - except StandardError: + except Exception: err = 'failed to serialize "%s" property, probably a bad value' - raise ValueError, err % name + raise ValueError(err % name) diff --git a/itools/database/queries.py b/itools/database/queries.py index ca774998e..53952943a 100644 --- a/itools/database/queries.py +++ b/itools/database/queries.py @@ -42,7 +42,6 @@ def __repr__(self): self.__repr_parameters__()) - class AllQuery(BaseQuery): def __repr_parameters__(self): @@ -93,7 +92,6 @@ def append(self, atom): raise NotImplementedError - class _AndQuery(_MultipleQuery): def append(self, atom): @@ -107,7 +105,6 @@ def append(self, atom): atoms.append(atom) - class _OrQuery(_MultipleQuery): def append(self, atom): @@ -123,7 +120,6 @@ def append(self, atom): atoms.append(atom) - def _flat_query(cls, *args): query = cls() for subquery in args: @@ -135,52 +131,43 @@ def _flat_query(cls, *args): return query - def AndQuery(*args): return _flat_query(_AndQuery, *args) - def OrQuery(*args): return _flat_query(_OrQuery, *args) - class NotQuery(BaseQuery): def __init__(self, query): self.query = query - def __repr_parameters__(self): return repr(self.query) - class StartQuery(BaseQuery): def __init__(self, name, value): self.name = name self.value = value - def __repr_parameters__(self): return "%r, %r" % (self.name, self.value) - class TextQuery(BaseQuery): def __init__(self, name, value): self.name = name self.value = value - def __repr_parameters__(self): return "%r, %r" % (self.name, self.value) - class QueryPrinter(PrettyPrinter): def _format(self, query, stream, indent, allowance, context, level): diff --git a/itools/database/registry.py b/itools/database/registry.py index b6f67b8c1..f380e24bf 100644 --- a/itools/database/registry.py +++ b/itools/database/registry.py @@ -17,10 +17,9 @@ # Import from the Standard Library from types import MethodType - - fields_registry = {} + def register_field(name, field_cls): if name not in fields_registry: fields_registry[name] = field_cls @@ -38,9 +37,10 @@ def register_field(name, field_cls): old_value = getattr(old, key, None) new_value = getattr(new, key, None) if type(old_value) is MethodType: - old_value = old_value.im_func + old_value = old_value.__func__ if type(new_value) is MethodType: - new_value = new_value.im_func + new_value = new_value.__func__ + if old_value != new_value: msg = 'register conflict over the "{0}" field ({1} is different)' raise ValueError(msg.format(name, key)) diff --git a/itools/database/resources.py b/itools/database/resources.py old mode 100644 new mode 100755 index c5a11713a..5a739aadc --- a/itools/database/resources.py +++ b/itools/database/resources.py @@ -14,14 +14,16 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +from logging import getLogger # Import from itools from itools.core import freeze, is_prototype # Import from itools.database -from fields import Field -from registry import register_field -from ro import RODatabase +from .fields import Field +from .registry import register_field +from .ro import RODatabase +log = getLogger("itools.database") class DBResourceMetaclass(type): @@ -33,8 +35,7 @@ def __new__(mcs, name, bases, dict): # Lookup fields if 'fields' not in dict: - cls.fields = [ x for x in dir(cls) - if is_prototype(getattr(cls, x), Field) ] + cls.fields = [x for x in dir(cls) if is_prototype(getattr(cls, x), Field)] # Register new fields in the catalog for name in cls.fields: @@ -48,13 +49,10 @@ def __new__(mcs, name, bases, dict): return cls +class Resource(object, metaclass=DBResourceMetaclass): -class Resource(object): - - __metaclass__ = DBResourceMetaclass __hash__ = None - fields = freeze([]) # Says what to do when a field not defined by the schema is found. @@ -62,25 +60,21 @@ class Resource(object): # soft = True : log a warning fields_soft = False - @classmethod def get_field(cls, name, soft=True): if name in cls.fields: return getattr(cls, name, None) - msg = 'Undefined field %s on %s' % (name, cls) if soft is True: return None - print('Warning: '+ msg) + log.warning("Warning: Undefined field {} on {}".format(name, cls)) return None - @classmethod def get_fields(self): for name in self.fields: field = self.get_field(name) yield name, field - def get_catalog_values(self): """Returns a dictionary with the values of the fields to be indexed. """ diff --git a/itools/database/ro.py b/itools/database/ro.py old mode 100644 new mode 100755 index 03e35b932..a7c09f1c4 --- a/itools/database/ro.py +++ b/itools/database/ro.py @@ -28,10 +28,10 @@ from itools.uri import Path # Import from itools.database -from backends import GitBackend, backends_registry -from exceptions import ReadonlyError -from metadata import Metadata -from registry import get_register_fields +from .backends import GitBackend, backends_registry +from .exceptions import ReadonlyError +from .metadata import Metadata +from .registry import get_register_fields class SearchResults(object): @@ -40,27 +40,22 @@ def __init__(self, database, results): self.database = database self.results = results - def __len__(self): return len(self.results) - def search(self, query=None, **kw): results = self.results.search(query, **kw) return SearchResults(self.database, results) - def get_documents(self, sort_by=None, reverse=False, start=0, size=0): return self.results.get_documents(sort_by, reverse, start, size) - def get_resources(self, sort_by=None, reverse=False, start=0, size=0): brains = list(self.get_documents(sort_by, reverse, start, size)) for brain in brains: yield self.database.get_resource_from_brain(brain) - class RODatabase(object): read_only = True @@ -79,21 +74,20 @@ def __init__(self, path=None, size_min=4800, size_max=5200, backend='lfs'): # Fields self.fields = get_register_fields() # init backend - self.backend = self.backend_cls(self.path, self.fields, self.read_only) + self.init_backend() # A mapping from key to handler self.cache = LRUCache(size_min, size_max, automatic=False) + def init_backend(self): + self.backend = self.backend_cls(self.path, self.fields, self.read_only) @property def catalog(self): - print('WARNING: Uses of context.database.catalog is obsolete') return self.backend.catalog - def close(self): self.backend.close() - def check_database(self): """This function checks whether the database is in a consisitent state, this is to say whether a transaction was not brutally aborted and left @@ -101,11 +95,8 @@ def check_database(self): This is meant to be used by scripts, like 'icms-start.py' """ - # TODO Check if bare repository is OK - print('Checking database...') return True - ####################################################################### # With statement ####################################################################### @@ -113,7 +104,6 @@ def check_database(self): def __enter__(self): return self - def __exit__(self, exc_type, exc_value, traceback): self.close() @@ -141,7 +131,6 @@ def _sync_filesystem(self, key): # Everything looks fine return handler - def _discard_handler(self, key): """Unconditionally remove the handler identified by the given key from the cache, and invalidate it (and free memory at the same time). @@ -150,13 +139,11 @@ def _discard_handler(self, key): # Invalidate the handler handler.__dict__.clear() - def _abort_changes(self): """To be called to abandon the transaction. """ raise ReadonlyError - def _cleanup(self): """For maintenance operations, this method is automatically called after a transaction is committed or aborted. @@ -169,14 +156,12 @@ def _cleanup(self): #print 'RODatabase._cleanup (1): % 4d %s' % (len(self.cache), vmsize()) #print gc.get_count() - ####################################################################### # Public API ####################################################################### def normalize_key(self, path, __root=Path('/')): return self.backend.normalize_key(path, __root) - def push_handler(self, key, handler): """Adds the given resource to the cache. """ @@ -192,7 +177,6 @@ def push_handler(self, key, handler): if cache_is_full: self.make_room() - def make_room(self): """Remove handlers from the cache until it fits the defined size. @@ -222,8 +206,6 @@ def make_room(self): if cache_is_full and n > 0: self._discard_handler(key) - - def has_handler(self, key): key = self.normalize_key(key) @@ -235,33 +217,26 @@ def has_handler(self, key): # Ask backend return self.backend.handler_exists(key) - def save_handler(self, key, handler): self.backend.save_handler(key, handler) - def get_handler_names(self, key): key = self.normalize_key(key) return self.backend.get_handler_names(key) - - def get_handler_data(self, key): - return self.backend.get_handler_data(key) - + def get_handler_data(self, key, text=False): + return self.backend.get_handler_data(key, text=text) def get_handler_mtime(self, key): return self.backend.get_handler_mtime(key) - def get_mimetype(self, key): return self.backend.get_handler_mimetype(key) - def get_handler_class(self, key): mimetype = self.get_mimetype(key) return get_handler_class_by_mimetype(mimetype) - def _get_handler(self, key, cls=None, soft=False): # Get resource if key in self.removed: @@ -274,8 +249,7 @@ def _get_handler(self, key, cls=None, soft=False): if handler is not None: # Check the class matches if cls is not None and not isinstance(handler, cls): - error = "expected '%s' class, '%s' found" - raise LookupError, error % (cls, handler.__class__) + raise LookupError("expected '{}' class, '{}' found".format(cls, handler.__class__)) # Cache hit self.cache.touch(key) return handler @@ -309,22 +283,18 @@ def _get_handler(self, key, cls=None, soft=False): # Ok return handler - def traverse_resources(self): return self.backend.traverse_resources() - def get_handler(self, key, cls=None, soft=False): key = self.normalize_key(key) return self._get_handler(key, cls, soft) - def get_handlers(self, key): base = self.normalize_key(key) for name in self.get_handler_names(base): yield self._get_handler(base + '/' + name) - def touch_handler(self, key, handler=None): """Report a modification of the key/handler to the database. """ @@ -344,22 +314,17 @@ def touch_handler(self, key, handler=None): # Set in changed list self.changed.add(key) - def set_handler(self, key, handler): - raise ReadonlyError, 'cannot set handler' - + raise ReadonlyError('cannot set handler') def del_handler(self, key): - raise ReadonlyError, 'cannot del handler' - + raise ReadonlyError('cannot del handler') def copy_handler(self, source, target, exclude_patterns=None): - raise ReadonlyError, 'cannot copy handler' - + raise ReadonlyError('cannot copy handler') def move_handler(self, source, target): - raise ReadonlyError, 'cannot move handler' - + raise ReadonlyError('cannot move handler') ####################################################################### # Layer 1: resources @@ -372,18 +337,16 @@ def register_resource_class(self, resource_class, format=None): format = resource_class.class_id self._resources_registry[format] = resource_class - @classmethod def unregister_resource_class(self, resource_class): registry = self._resources_registry - for class_id, cls in registry.items(): + for class_id, cls in list(registry.items()): if resource_class is cls: del registry[class_id] - def get_resource_class(self, class_id): if type(class_id) is not str: - raise TypeError, 'expected byte string, got %s' % class_id + raise TypeError('expected string, got {}'.format(class_id)) # Check dynamic models are not broken registry = self._resources_registry @@ -391,8 +354,7 @@ def get_resource_class(self, class_id): model = self.get_resource(class_id, soft=True) if model is None: registry.pop(class_id, None) - err = 'the resource "%s" does not exist' % class_id - raise LookupError, err + raise LookupError("the resource '{}' does not exist".format(class_id)) # Cache hit cls = registry.get(class_id) @@ -415,10 +377,9 @@ def get_resource_class(self, class_id): # Default return self._resources_registry['application/octet-stream'] - def get_resource_classes(self): registry = self._resources_registry - for class_id, cls in self._resources_registry.items(): + for class_id, cls in list(self._resources_registry.items()): if class_id[0] == '/': model = self.get_resource(class_id, soft=True) if model is None: @@ -427,7 +388,6 @@ def get_resource_classes(self): yield cls - def get_metadata(self, abspath, soft=False): if type(abspath) is str: path = abspath[1:] @@ -437,12 +397,10 @@ def get_metadata(self, abspath, soft=False): path_to_metadata = '%s.metadata' % path return self.get_handler(path_to_metadata, Metadata, soft=soft) - def get_cls(self, class_id): cls = self.get_resource_class(class_id) return cls or self.get_resource_class('application/octet-stream') - def get_resource(self, abspath, soft=False): abspath = Path(abspath) # Get metadata @@ -455,44 +413,34 @@ def get_resource(self, abspath, soft=False): # Ok return cls(abspath=abspath, database=self, metadata=metadata) - def get_resource_from_brain(self, brain): cls = self.get_cls(brain.format) return cls(abspath=Path(brain.abspath), database=self, brain=brain) - def remove_resource(self, resource): - raise ReadonlyError - + raise ReadonlyError def add_resource(self, resource): - raise ReadonlyError - + raise ReadonlyError def change_resource(self, resource): - raise ReadonlyError - + raise ReadonlyError def move_resource(self, source, new_path): - raise ReadonlyError - + raise ReadonlyError def save_changes(self): return - def create_tag(self, tag_name, message=None): raise ReadonlyError - def reset_to_tag(self, tag_name): raise ReadonlyError - def abort_changes(self): return - ####################################################################### # API for path ####################################################################### @@ -502,14 +450,12 @@ def get_basename(path): path = Path(path) return path.get_name() - @staticmethod def get_path(path): if type(path) is not Path: path = Path(path) return str(path) - @staticmethod def resolve(base, path): if type(base) is not Path: @@ -517,7 +463,6 @@ def resolve(base, path): path = base.resolve(path) return str(path) - @staticmethod def resolve2(base, path): if type(base) is not Path: @@ -525,7 +470,6 @@ def resolve2(base, path): path = base.resolve2(path) return str(path) - ####################################################################### # Search ####################################################################### @@ -533,10 +477,8 @@ def search(self, query=None, **kw): results = self.backend.search(query, **kw) return SearchResults(database=self, results=results) - def reindex_catalog(self, base_abspath, recursif=True): raise ReadonlyError - ro_database = RODatabase() diff --git a/itools/database/rw.py b/itools/database/rw.py old mode 100644 new mode 100755 index 023cd3483..edb9f2fba --- a/itools/database/rw.py +++ b/itools/database/rw.py @@ -21,17 +21,18 @@ # Import from the Standard Library from datetime import datetime import fnmatch +from logging import getLogger # Import from itools from itools.fs import lfs from itools.handlers import Folder -from itools.log import log_error # Import from here -from backends import backends_registry -from registry import get_register_fields -from ro import RODatabase +from .backends import backends_registry +from .registry import get_register_fields +from .ro import RODatabase +log = getLogger("itools.database") MSG_URI_IS_BUSY = 'The "%s" URI is busy.' @@ -41,8 +42,7 @@ class RWDatabase(RODatabase): read_only = False def __init__(self, path, size_min, size_max, backend='git'): - proxy = super(RWDatabase, self) - proxy.__init__(path, size_min, size_max, backend) + super().__init__(path, size_min, size_max, backend) # Changes on DB self.added = set() self.changed = set() @@ -90,16 +90,13 @@ def __init__(self, path, size_min, size_max, backend='git'): self.resources_old2new_catalog = {} self.resources_new2old_catalog = {} - def check_catalog(self): pass - def close(self): self.abort_changes() self.backend.close() - def _sync_filesystem(self, key): # Don't check if handler has been modified since last loading, # we only have one writer @@ -121,7 +118,6 @@ def has_handler(self, key): # Normal case return super(RWDatabase, self).has_handler(key) - def _get_handler(self, key, cls=None, soft=False): # A hook to handle the new directories base = key + '/' @@ -133,7 +129,6 @@ def _get_handler(self, key, cls=None, soft=False): # The other files return super(RWDatabase, self)._get_handler(key, cls, soft) - def set_handler(self, key, handler): # TODO: We have to refactor the set_changed() # mechanism in handlers/database @@ -143,14 +138,14 @@ def set_handler(self, key, handler): handler.loaded = True handler.set_changed() if type(handler) is Folder: - raise ValueError, 'unexpected folder (only files can be "set")' + raise ValueError('unexpected folder (only files can be "set")') if handler.key is not None: - raise ValueError, 'only new files can be added, try to clone first' + raise ValueError('only new files can be added, try to clone first') key = self.normalize_key(key) if self._get_handler(key, soft=True) is not None: - raise RuntimeError, MSG_URI_IS_BUSY % key + raise RuntimeError(MSG_URI_IS_BUSY % key) # Added or modified ? if key not in self.added and self.has_handler(key): @@ -163,7 +158,6 @@ def set_handler(self, key, handler): self.removed.discard(key) self.has_changed = True - def del_handler(self, key): key = self.normalize_key(key) @@ -195,7 +189,6 @@ def del_handler(self, key): self.removed.add(key) self.has_changed = True - def touch_handler(self, key, handler=None): key = self.normalize_key(key) # Mark the handler as dirty @@ -212,11 +205,9 @@ def touch_handler(self, key, handler=None): self.changed.add(key) - def save_handler(self, key, handler): self.backend.save_handler(key, handler) - def get_handler_names(self, key): key = self.normalize_key(key) # On the filesystem @@ -234,7 +225,6 @@ def get_handler_names(self, key): names.add(name) return list(names) - def copy_handler(self, source, target, exclude_patterns=None): source = self.normalize_key(source) target = self.normalize_key(target) @@ -252,7 +242,7 @@ def copy_handler(self, source, target, exclude_patterns=None): # Check the target is free if self._get_handler(target, soft=True) is not None: - raise RuntimeError, MSG_URI_IS_BUSY % target + raise RuntimeError(MSG_URI_IS_BUSY % target) handler = self._get_handler(source) if type(handler) is Folder: @@ -265,7 +255,6 @@ def copy_handler(self, source, target, exclude_patterns=None): self.removed.discard(target) self.has_changed = True - def move_handler(self, source, target): source = self.normalize_key(source) target = self.normalize_key(target) @@ -276,7 +265,7 @@ def move_handler(self, source, target): # Check the target is free if self._get_handler(target, soft=True) is not None: - raise RuntimeError, MSG_URI_IS_BUSY % target + raise RuntimeError(MSG_URI_IS_BUSY % target) # Go cache = self.cache @@ -327,7 +316,6 @@ def move_handler(self, source, target): self.removed.discard(target) self.has_changed = True - ####################################################################### # Layer 1: resources ####################################################################### @@ -342,7 +330,6 @@ def remove_resource(self, resource): self.resources_old2new_catalog[path] = None self.resources_new2old_catalog.pop(path, None) - def add_resource(self, resource): self.has_changed = True new2old = self.resources_new2old @@ -351,7 +338,6 @@ def add_resource(self, resource): path = str(x.abspath) new2old[path] = None - def change_resource(self, resource): self.has_changed = True old2new = self.resources_old2new @@ -362,14 +348,13 @@ def change_resource(self, resource): return # Case 2: removed or moved away if path in old2new and not old2new[path]: - raise ValueError, 'cannot change a resource that has been removed' + raise ValueError('cannot change a resource that has been removed') # Case 3: not yet touched old2new[path] = path new2old[path] = path self.resources_old2new_catalog[path] = path self.resources_new2old_catalog[path] = path - def is_changed(self, resource): """We use for this function only the 2 dicts old2new and new2old. """ @@ -378,7 +363,6 @@ def is_changed(self, resource): path = str(resource.abspath) return path in old2new or path in new2old - def move_resource(self, source, new_path): self.has_changed = True old2new = self.resources_old2new @@ -393,7 +377,7 @@ def move_resource(self, source, new_path): target_path = str(target_path) if source_path in old2new and not old2new[source_path]: err = 'cannot move a resource that has been removed' - raise ValueError, err + raise ValueError(err) source_path = new2old.pop(source_path, source_path) if source_path: @@ -402,7 +386,6 @@ def move_resource(self, source, new_path): new2old[target_path] = source_path self.resources_new2old_catalog[target_path] = source_path - ####################################################################### # Transactions ####################################################################### @@ -410,14 +393,13 @@ def _cleanup(self): super(RWDatabase, self)._cleanup() self.has_changed = False - def _abort_changes(self): # 1. Handlers cache = self.cache for key in self.added: self._discard_handler(key) for key in self.changed: - if cache.has_key(key): + if key in cache: # FIXME # We check cache since an handler # can be added & changed at one @@ -438,7 +420,6 @@ def _abort_changes(self): self.resources_old2new_catalog.clear() self.resources_new2old_catalog.clear() - def abort_changes(self): if not self.has_changed: return @@ -446,7 +427,6 @@ def abort_changes(self): self._abort_changes() self._cleanup() - def _before_commit(self): """This method is called before 'save_changes', and gives a chance to the database to check for preconditions, if an error occurs here @@ -457,7 +437,6 @@ def _before_commit(self): """ return None, None, None, [], [] - def _save_changes(self, data, commit_msg=None): # Get data informations the_author, the_date, the_msg, docs_to_index, docs_to_unindex = data @@ -475,7 +454,6 @@ def _save_changes(self, data, commit_msg=None): self.added.clear() self.removed.clear() - def save_changes(self, commit_message=None): if not self.has_changed: return @@ -483,12 +461,12 @@ def save_changes(self, commit_message=None): # the transaction will be aborted try: data = self._before_commit() - except Exception: - log_error('Transaction failed', domain='itools.database') + except Exception as e: + log.error("Transaction failed", exc_info=True) try: self._abort_changes() - except Exception: - log_error('Aborting failed', domain='itools.database') + except Exception as e: + log.error("Aborting failed", exc_info=True) self._cleanup() raise @@ -496,35 +474,31 @@ def save_changes(self, commit_message=None): try: self._save_changes(data, commit_message) except Exception as e: - log_error('Transaction failed', domain='itools.database') + log.error("Transaction failed", exc_info=True) try: self._abort_changes() - except Exception: - log_error('Aborting failed', domain='itools.database') - raise(e) + except Exception as e: + log.error("Aborting failed", exc_info=True) + raise e finally: self._cleanup() - def flush_catalog(self): """ Flush changes in catalog without commiting (allow to search in catalog on changed elements) """ - root = self.get_resource('/') - docs_to_index = set(self.resources_new2old_catalog.keys()) - docs_to_unindex = self.resources_old2new_catalog.keys() - docs_to_unindex = list(set(docs_to_unindex) - docs_to_index) - docs_to_index = list(docs_to_index) - aux = [] - for path in docs_to_index: - resource = root.get_resource(path, soft=True) - if resource: - values = resource.get_catalog_values() - aux.append((resource, values)) - self.backend.flush_catalog(docs_to_unindex, aux) - self.resources_old2new_catalog.clear() - self.resources_new2old_catalog.clear() - + try: + data = self._before_commit() + except Exception as e: + log.error("Transaction failed", exc_info=True) + try: + self._abort_changes() + except Exception as e: + log.error("Aborting failed", exc_info=True) + self._cleanup() + raise e + _, _, _, docs_to_index, docs_to_unindex = data + self.backend.flush_catalog(docs_to_unindex, docs_to_index) def reindex_catalog(self, base_abspath, recursif=True): """Reindex the catalog & return nb resources re-indexed @@ -563,4 +537,4 @@ def make_database(path, size_min, size_max, fields=None, backend=None): backend_cls = backends_registry[backend] backend_cls.init_backend(path, fields) # Ok - return RWDatabase(path, size_min, size_max) + return RWDatabase(path, size_min, size_max, backend=backend) diff --git a/itools/datatypes/__init__.py b/itools/datatypes/__init__.py index 490de92ff..669901051 100644 --- a/itools/datatypes/__init__.py +++ b/itools/datatypes/__init__.py @@ -17,16 +17,16 @@ # along with this program. If not, see . # Import from itools -from base import DataType -from primitive import Boolean, Decimal, Email, Integer, String, Unicode -from primitive import Tokens, MultiLinesTokens, Enumerate -from primitive import PathDataType, URI -from primitive import QName, XMLAttribute, XMLContent -from datetime_ import ISOCalendarDate, ISOTime, ISODateTime, HTTPDate -from languages import LanguageTag +from .base import DataType +from .primitive import Boolean, Decimal, Email, Integer, String, Unicode +from .primitive import Tokens, MultiLinesTokens, Enumerate +from .primitive import PathDataType, URI +from .primitive import QName, XMLAttribute, XMLContent +from .datetime_ import ISOCalendarDate, ISOTime, ISODateTime, HTTPDate +from .languages import LanguageTag # Define alias Date, Time and DateTime (use ISO standard) -from datetime_ import ISOCalendarDate as Date, ISOTime as Time -from datetime_ import ISODateTime as DateTime +from .datetime_ import ISOCalendarDate as Date, ISOTime as Time +from .datetime_ import ISODateTime as DateTime __all__ = [ diff --git a/itools/datatypes/base.py b/itools/datatypes/base.py index af049fd34..54a47581f 100644 --- a/itools/datatypes/base.py +++ b/itools/datatypes/base.py @@ -16,9 +16,37 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +# Import from std +import os + +# Import from external +from cryptography.fernet import Fernet +from cryptography.fernet import InvalidToken + # Import from itools from itools.core import prototype +# Fernet is an abstraction implementation of symmetric encryption +# using AES-256 CBC-MODE with a 32 bytes key +# https://cryptography.io/en/latest/fernet/#fernet-symmetric-encryption + +# To generate a 32 bytes keys use the following methods +# Oneliner CLI : python -c "import base64;import os;print(base64.urlsafe_b64encode(os.urandom(32)))" +# In code : Fernet.generate_key() +FERNET_KEY = os.getenv("FERNET_KEY") + +if FERNET_KEY: + print( + "ENV VAR FERNET_KEY FOR FERNET ENCRYPTION KEY IS SET," + " SENSITIVE VALUES WILL BE ENCRYPTED" + ) + fernet = Fernet(FERNET_KEY) +else: + print( + "ENV VAR FERNET_KEY FOR FERNET ENCRYPTION KEY IS NOT SET," + " SENSITIVE VALUES WILL NOT BE ENCRYPTED" + ) + fernet = None class DataType(prototype): @@ -26,7 +54,7 @@ class DataType(prototype): # Default value default = None multiple = False - + encrypted = False def get_default(cls): default = cls.default @@ -37,21 +65,18 @@ def get_default(cls): return [] return default - @staticmethod def decode(data): - """Deserializes the given byte string to a value with a type. + """Deserializes the given str data to a value with a type. """ raise NotImplementedError - @staticmethod def encode(value): - """Serializes the given value to a byte string. + """Serializes the given value to str. """ raise NotImplementedError - @staticmethod def is_valid(value): """Checks whether the given value is valid. @@ -61,7 +86,6 @@ def is_valid(value): """ return True - @staticmethod def is_empty(value): """Checks whether the given value is empty or not. @@ -70,3 +94,30 @@ def is_empty(value): as empty. (NOTE This is used by the multilingual code.) """ return value is None + + # Encryption/Decryption functions + + @classmethod + def encrypt(cls, value): + if not cls.encrypted: + return value + if not fernet: + # Fernet is not correctly set do not try to encrypt + return value + if type(value) is str: + value = value.encode("utf-8") + return fernet.encrypt(value).decode("utf-8") + + @classmethod + def decrypt(cls, value): + if not cls.encrypted: + return value + if not fernet: + # Fernet is not correctly set do not try to decrypt + return value + try: + if type(value) is str: + value = value.encode("utf-8") + return fernet.decrypt(value).decode("utf-8") + except InvalidToken: + return value diff --git a/itools/datatypes/datetime_.py b/itools/datatypes/datetime_.py index 7c60b54c4..c514738ea 100644 --- a/itools/datatypes/datetime_.py +++ b/itools/datatypes/datetime_.py @@ -27,7 +27,7 @@ # Import from itools from itools.core import fixed_offset -from base import DataType +from .base import DataType ########################################################################### @@ -60,7 +60,6 @@ def decode(data): tz = fixed_offset(tz/60) return tz.localize(naive_dt) - @staticmethod def encode(value): """Encode a datetime object to RFC 1123 format: :: @@ -85,7 +84,6 @@ def encode(value): # XXX Python dates (the datetime.date module) require the month and day, # they are not able to represent lower precision dates as ISO 8601 does. # In the long run we will need to replace Python dates by something else. - class ISOCalendarDate(DataType): """Extended formats (from max. to min. precision): %Y-%m-%d, %Y-%m, %Y @@ -96,6 +94,8 @@ class ISOCalendarDate(DataType): @classmethod def decode(cls, data): + if type(data) is bytes: + data = data.decode("utf-8") if not data: return None format_date = cls.format_date @@ -117,7 +117,6 @@ def decode(cls, data): return date(year, month, day) - @classmethod def encode(cls, value): # We choose the extended format as the canonical representation @@ -125,7 +124,6 @@ def encode(cls, value): return '' return value.strftime(cls.format_date) - @classmethod def is_valid(cls, value): # Only consider the value is valid if we are able to encode it. @@ -147,7 +145,6 @@ class ISOTime(DataType): Basic formats: %H%M%S%f, %H%M%S, %H%M, %H """ - @staticmethod def decode(data): if not data: @@ -220,7 +217,6 @@ def decode(data): microsecond = int(data) return time(hour, minute, second, microsecond, tzinfo=tzinfo) - @staticmethod def encode(value): # We choose the extended format as the canonical representation @@ -232,7 +228,6 @@ def encode(value): return value.strftime(fmt) - class ISODateTime(DataType): cls_date = ISOCalendarDate @@ -241,7 +236,8 @@ class ISODateTime(DataType): def decode(self, value): if not value: return None - + if type(value) is bytes: + value = value.decode("utf-8") value = value.split('T') date, time = value[0], value[1:] @@ -255,7 +251,6 @@ def decode(self, value): return date - def encode(self, value): if value is None: return '' diff --git a/itools/datatypes/languages.py b/itools/datatypes/languages.py index 93014d6f7..372705e77 100644 --- a/itools/datatypes/languages.py +++ b/itools/datatypes/languages.py @@ -16,7 +16,7 @@ # along with this program. If not, see . # Import from itools -from base import DataType +from .base import DataType class LanguageTag(DataType): @@ -29,7 +29,6 @@ def decode(value): else: return (res[0].lower(), res[1].upper()) - @staticmethod def encode(value): language, locality = value diff --git a/itools/datatypes/primitive.py b/itools/datatypes/primitive.py old mode 100644 new mode 100755 index 067332908..1660ee997 --- a/itools/datatypes/primitive.py +++ b/itools/datatypes/primitive.py @@ -30,7 +30,7 @@ from itools.uri import Path, get_reference # Import from here -from base import DataType +from .base import DataType class Integer(DataType): @@ -39,15 +39,13 @@ class Integer(DataType): def decode(value): if value == '': return None - return int(value) - + return int(float(value)) @staticmethod def encode(value): if value is None: return '' - return str(value) - + return str(int(value)) class Decimal(DataType): @@ -65,41 +63,41 @@ def encode(value): return str(value) - -class Unicode(DataType): - - default = u'' - +class String(DataType): @staticmethod def decode(value, encoding='UTF-8'): - return unicode(value, encoding).strip() + if isinstance(value, bytes): + value = value.decode(encoding) + return value @staticmethod def encode(value, encoding='UTF-8'): - return value.strip().encode(encoding) - + if value is None: + return "" + if isinstance(value, bytes): + value = value.decode(encoding) + return value @staticmethod def is_empty(value): - return value == u'' - + return value == '' -class String(DataType): - - @staticmethod - def decode(value): - return value +class Unicode(String): + """ + This exists only for backwards compatibility, to make migration to Pyhon 3 + easier. + The only difference with String is the default value (empty string), and + that itools.catalog will split Unicode in words when indexing. - @staticmethod - def encode(value): - if value is None: - return '' - return value + Text would be a better name than Unicode, but we keep Unicode so we don't + have to change too much code. + """ + default = '' class Boolean(DataType): @@ -111,7 +109,6 @@ class Boolean(DataType): def decode(value): return bool(int(value)) - @staticmethod def encode(value): if value is True: @@ -122,7 +119,6 @@ def encode(value): raise ValueError('{0} value is not a boolean'.format(value)) - class URI(String): # XXX Should we at least normalize the sring when decoding/encoding? @@ -134,13 +130,11 @@ def is_valid(value): return False return True - @staticmethod def is_empty(value): return not value - class PathDataType(DataType): # TODO Do like URI, do not decode (just an string), and use 'is_valid' # instead @@ -157,7 +151,6 @@ def encode(value): return str(value) - # We consider the local part in emails is case-insensitive. This is against # the standard, but corresponds to common usage. email_expr = "^[0-9a-z]+[_\.0-9a-z-'+]*@([0-9a-z-]+\.)+[a-z]{2,6}$" @@ -170,6 +163,8 @@ def encode(value): @staticmethod def decode(value): + if isinstance(value, bytes): + value = value.decode("utf-8") return value.lower() @staticmethod @@ -177,7 +172,6 @@ def is_valid(value): return email_expr.match(value) is not None - class QName(DataType): @staticmethod @@ -187,7 +181,6 @@ def decode(data): return None, data - @staticmethod def encode(value): if value[0] is None: @@ -195,7 +188,6 @@ def encode(value): return '%s:%s' % value - class Tokens(DataType): default = () @@ -204,36 +196,29 @@ class Tokens(DataType): def decode(data): return tuple(data.split()) - @staticmethod def encode(value): return ' '.join(value) - class MultiLinesTokens(DataType): @staticmethod def decode(data): return tuple(data.splitlines()) - @staticmethod def encode(value): return '\n'.join(value) - - ########################################################################### # Enumerates - class Enumerate(String): is_enumerate = True options = freeze([]) - def get_options(cls): """Returns a list of dictionaries in the format [{'name': , 'value': }, ...] @@ -243,7 +228,6 @@ def get_options(cls): """ return deepcopy(cls.options) - def is_valid(self, name): """Returns True if the given name is part of this Enumerate's options. """ @@ -256,7 +240,6 @@ def is_valid(self, name): return True return False - def get_namespace(cls, name): """Extends the options with information about which one is matching the given name. @@ -264,7 +247,6 @@ def get_namespace(cls, name): options = cls.get_options() return enumerate_get_namespace(options, name) - def get_value(cls, name, default=None): """Returns the value matching the given name, or the default value. """ @@ -298,19 +280,21 @@ class XMLContent(object): @staticmethod def encode(value): + if isinstance(value, bytes): + value = value.decode("utf-8") return value.replace('&', '&').replace('<', '<') - @staticmethod def decode(value): return value.replace('&', '&').replace('<', '<') - class XMLAttribute(object): @staticmethod def encode(value): + if isinstance(value, bytes): + value = value.decode("utf-8") value = value.replace('&', '&').replace('<', '<') return value.replace('"', '"') @@ -349,7 +333,6 @@ def encode(value): return dumps(value, cls=NewJSONEncoder) - class JSONArray(JSONObject): """A JSON array, which is a Python list serialized as a JSON string @@ -358,7 +341,6 @@ class JSONArray(JSONObject): default = [] - @staticmethod def is_valid(value): return isinstance(value, list) diff --git a/itools/fs/__init__.py b/itools/fs/__init__.py index 1cc0dad40..14d720ab3 100644 --- a/itools/fs/__init__.py +++ b/itools/fs/__init__.py @@ -18,8 +18,8 @@ # along with this program. If not, see . # Import from itools -from common import READ, WRITE, READ_WRITE, APPEND, FileName -from lfs import lfs +from .common import READ, WRITE, READ_WRITE, APPEND, FileName +from .lfs import lfs __all__ = [ diff --git a/itools/fs/common.py b/itools/fs/common.py index 1618e607a..bacb31f12 100644 --- a/itools/fs/common.py +++ b/itools/fs/common.py @@ -24,9 +24,8 @@ READ = 'r' WRITE = 'w' -READ_WRITE = 'rw' -APPEND = 'a' - +READ_WRITE = 'w+' +APPEND = 'a+' class FileName(DataType): @@ -73,8 +72,6 @@ def encode(value): return name - - def get_mimetype(name): """Try to guess the mimetype given the name. To guess from the name we need to extract the type extension, we use an heuristic for this task, diff --git a/itools/fs/lfs.py b/itools/fs/lfs.py old mode 100644 new mode 100755 index 3a2d939b8..db9aa57b2 --- a/itools/fs/lfs.py +++ b/itools/fs/lfs.py @@ -22,27 +22,33 @@ from datetime import datetime from os import listdir, makedirs, remove as os_remove, renames, walk from os import access, R_OK, W_OK -from os.path import exists, getatime, getctime, getmtime ,getsize +from os.path import exists, getatime, getctime, getmtime, getsize from os.path import isfile, isdir, join, basename, dirname from os.path import abspath, relpath from shutil import rmtree, copytree, copy as shutil_copy +import mimetypes + # Import from itools from itools.uri import Path -from common import WRITE, READ_WRITE, APPEND, READ, get_mimetype - +from .common import WRITE, READ_WRITE, APPEND, READ, get_mimetype -MODES = {WRITE: 'wb', READ_WRITE: 'r+b', APPEND: 'ab', READ: 'rb'} +MODES = { + WRITE: ('w', 'wb'), + READ_WRITE: ('w+', 'wb+'), + APPEND: ('a+', 'ab+'), + READ: ('r', 'rb'), +} class LocalFolder(object): def __init__(self, path='.'): if not exists(path): - raise IOError, "No such directory: '%s'" % path + raise IOError("No such directory: '%s'" % path) if isfile(path): - raise IOError, "Is a directory: '%s'" % path + raise IOError("Is a directory: '%s'" % path) self.path = Path(abspath(path)) @@ -78,23 +84,23 @@ def can_write(self, path): path = self._resolve_path(path) return access(path, W_OK) - - def make_file(self, path): + def make_file(self, path, text=False): path = self._resolve_path(path) parent_path = dirname(path) if exists(parent_path): if exists(path): - raise OSError, "File exists: '%s'" % path + raise OSError("File exists: '%s'" % path) else: makedirs(parent_path) - return file(path, 'wb') - + if text: + return open(path, 'w') + else: + return open(path, 'wb') def make_folder(self, path): path = self._resolve_path(path) return makedirs(path) - def get_ctime(self, path): path = self._resolve_path(path) ctime = getctime(path) @@ -126,14 +132,16 @@ def get_size(self, path): path = self._resolve_path(path) return getsize(path) - - def open(self, path, mode=None): + def open(self, path, mode=None, text=False): path = self._resolve_path(path) if isdir(path): return self.__class__(path) - mode = MODES.get(mode, 'rb') - return file(path, mode) - + mode = MODES.get(mode, ('r', 'rb')) + if text: + mode = mode[0] + else: + mode = mode[1] + return open(path, mode) def remove(self, path): path = self._resolve_path(path) @@ -141,7 +149,11 @@ def remove(self, path): # Remove folder contents rmtree(path) else: - os_remove(path) + try: + os_remove(path) + except OSError as error: + print(error) + print("File path can not be removed") def copy(self, source, target): @@ -167,7 +179,7 @@ def get_names(self, path='.'): path = self._resolve_path(path) try: return listdir(path) - except OSError, e: + except OSError as e: # Path does not exist or is not a directory if e.errno == 2 or e.errno == 20: return [] @@ -177,7 +189,7 @@ def get_names(self, path='.'): def traverse(self, path='.'): path = self._resolve_path(path) if not exists(path): - raise IOError, "No such directory: '%s'" % path + raise IOError("No such directory: '%s'" % path) yield path if isdir(path): for root, folders, files in walk(path, topdown=True): diff --git a/itools/gettext/__init__.py b/itools/gettext/__init__.py index 5523805bd..4e872782d 100644 --- a/itools/gettext/__init__.py +++ b/itools/gettext/__init__.py @@ -18,9 +18,9 @@ # Import from itools from itools.core import add_type, get_abspath -from domains import register_domain, get_domain, MSG, get_language_msg -from mo import MOFile -from po import POFile, POUnit, encode_source +from .domains import register_domain, get_domain, MSG, get_language_msg +from .mo import MOFile +from .po import POFile, POUnit, encode_source __all__ = [ diff --git a/itools/gettext/domains.py b/itools/gettext/domains.py index 0c09a2dcd..a7a81e48c 100644 --- a/itools/gettext/domains.py +++ b/itools/gettext/domains.py @@ -22,13 +22,12 @@ from sys import _getframe # Import from itools -from itools.handlers import Folder from itools.i18n import get_language_name from itools.fs import lfs from itools.xml import XMLParser # Import from here -from mo import MOFile +from .mo import MOFile xhtml_namespaces = { @@ -41,37 +40,40 @@ domains = {} + def register_domain(name, locale_path): if name not in domains: - domains[name] = Domain(locale_path) + domains[name] = locale_path def get_domain(name): - return domains[name] - + domain = domains.get(name) + # Lazy load domain + if isinstance(domain, str): + domain = Domain(domain) + domains[name] = domain + return domain class Domain(dict): def __init__(self, uri): + super().__init__() for key in lfs.get_names(uri): if key[-3:] == '.mo': language = key[:-3] path = '{0}/{1}'.format(uri, key) self[language] = MOFile(path) - def gettext(self, message, language): if language not in self: return message handler = self[language] return handler.gettext(message) - def get_languages(self): - return self.keys() - + return list(self.keys()) class MSGFormatter(Formatter): @@ -94,6 +96,7 @@ def get_value(self, key, args, kw): msg_formatter = MSGFormatter() + class MSG(object): domain = None @@ -109,26 +112,22 @@ def __init__(self, message=None, format=None): if message: self.message = message - def _format(self, message, **kw): if self.format == 'replace': return msg_formatter.vformat(message, [], (self, kw)) elif self.format == 'none': return message elif self.format == 'html': - data = message.encode('utf_8') - return XMLParser(data, namespaces=xhtml_namespaces) + return XMLParser(message, namespaces=xhtml_namespaces) elif self.format == 'replace_html': message = msg_formatter.vformat(message, [], (self, kw)) - data = message.encode('utf_8') - return XMLParser(data, namespaces=xhtml_namespaces) - - raise ValueError, 'unexpected format "{0}"'.format(self.format) + return XMLParser(message, namespaces=xhtml_namespaces) + raise ValueError('unexpected format "{0}"'.format(self.format)) def gettext(self, language=None, **kw): message = self.message - domain = domains.get(self.domain) + domain = get_domain(self.domain) if domain is not None: # Find out the language @@ -144,7 +143,6 @@ def gettext(self, language=None, **kw): return self._format(message, **kw) - def get_language_msg(code): language = get_language_name(code) return MSG(language) diff --git a/itools/gettext/mo.py b/itools/gettext/mo.py index f5c9ce18c..00ebf6a3a 100644 --- a/itools/gettext/mo.py +++ b/itools/gettext/mo.py @@ -30,11 +30,10 @@ class MOFile(File): def _load_state_from_file(self, file): self.translations = GNUTranslations(file) - def gettext(self, message): """Returns the translation for the given message. """ - return self.translations.ugettext(message) + return self.translations.gettext(message) register_handler_class(MOFile) diff --git a/itools/gettext/po.py b/itools/gettext/po.py index 7789c434a..ddc51acea 100644 --- a/itools/gettext/po.py +++ b/itools/gettext/po.py @@ -46,7 +46,6 @@ def __init__(self, line_number, line_type=None): self.line_number = line_number self.line_type = line_type - def __str__(self): if self.line_type is None: return 'syntax error at line %d' % self.line_number @@ -77,6 +76,9 @@ def __str__(self): def get_lines(data): + if isinstance(data, bytes): + data = data.decode("utf-8") + lines = data.split('\n') + [''] line_number = 0 while line_number <= len(lines): @@ -97,7 +99,7 @@ def get_lines(data): yield FUZZY, None, line_number # Reference elif line.startswith('#:'): - yield REFERENCE, line[1:], line_number + yield REFERENCE, line[2:].strip(), line_number # Comment elif line.startswith('#'): yield COMMENT, line[1:], line_number @@ -139,14 +141,14 @@ def encode_source(source): elif type == START_FORMAT: # A lonely tag ? if source[i+1][0] == END_FORMAT: - result.append(u"" % value) + result.append("" % value) else: - result.append(u"" % value) + result.append("" % value) elif type == END_FORMAT: # A lonely tag ? if source[i-1][0] != START_FORMAT: - result.append(u'') - return u''.join(result) + result.append('') + return ''.join(result) def decode_target(target): @@ -202,7 +204,6 @@ def decode_target(target): return result - ########################################################################### # Handler ########################################################################### @@ -213,11 +214,12 @@ def escape(s): expr = compile(r'(\\.)') + + def unescape(s): return expr.sub(lambda x: eval("'%s'" % x.group(0)), s) - class POUnit(object): """An entry in a PO file has the syntax: @@ -259,12 +261,11 @@ def __init__(self, comments, context, source, target, self.target = target self.fuzzy = fuzzy - def to_str(self, encoding='UTF-8'): s = [] # The comments for comment in self.comments: - s.append('#%s\n' % comment.encode(encoding)) + s.append('#%s\n' % comment) # The reference comments i = 1 references = self.references.items() @@ -274,40 +275,35 @@ def to_str(self, encoding='UTF-8'): comma = '' if i == nb_references else ',' line = '#: %s:%s%s\n' % (filename, line, comma) s.append(line) - i+=1 + i += 1 # The Fuzzy flag if self.fuzzy: s.append('#, fuzzy\n') # The msgctxt if self.context is not None: - s.append('msgctxt "%s"\n' % escape(self.context[0].encode( - encoding))) + s.append('msgctxt "%s"\n' % escape(self.context[0])) for string in self.context[1:]: - s.append('"%s"\n' % escape(string.encode(encoding))) + s.append('"%s"\n' % escape(string)) # The msgid - s.append('msgid "%s"\n' % escape(self.source[0].encode(encoding))) + s.append('msgid "%s"\n' % escape(self.source[0])) for string in self.source[1:]: - s.append('"%s"\n' % escape(string.encode(encoding))) + s.append('"%s"\n' % escape(string)) # The msgstr - s.append('msgstr "%s"\n' % escape(self.target[0].encode(encoding))) + s.append('msgstr "%s"\n' % escape(self.target[0])) for string in self.target[1:]: - s.append('"%s"\n' % escape(string.encode(encoding))) - + s.append('"%s"\n' % escape(string)) return ''.join(s) - def __repr__(self): msg = "" return msg % (self.context, self.source, self.target, self.references) - def __eq__(self, other): return ((other.context == self.context) and (other.source == self.source) and (other.target == self.target)) - skeleton = """# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. @@ -327,7 +323,6 @@ def __eq__(self, other): """ - class POFile(TextFile): class_mimetypes = [ @@ -336,7 +331,6 @@ class POFile(TextFile): 'text/x-po'] class_extension = 'po' - def new(self): # XXX Old style (like in the "get_skeleton" times) now = strftime('%Y-%m-%d %H:%m+%Z', gmtime(time())) @@ -366,7 +360,7 @@ def next_entry(self, data): comments.append(value) state = 1 elif line_type == REFERENCE: - for reference in value[3:].split(' '): + for reference in value.split(' '): value, line_no = reference.split(':') references.append((value, line_no)) state = 1 @@ -387,7 +381,7 @@ def next_entry(self, data): if line_type == COMMENT: comments.append(value) elif line_type == REFERENCE: - for reference in value[3:].split(' '): + for reference in value.split(' '): value, line_no = reference.split(':') references.append((value, line_no)) elif line_type == FUZZY and not fuzzy: @@ -479,7 +473,6 @@ def next_entry(self, data): else: raise POSyntaxError(line_number, line_type) - def _load_state_from_file(self, file): """A PO file is made of entries, where entries are separated by one or more blank lines. Each entry consists of a msgid and a msgstr, @@ -504,7 +497,6 @@ def _load_state_from_file(self, file): # Split the data by lines and intialize the line index data = file.read() - # Add entries for entry in self.next_entry(data): comments, references, context, source, target, fuzzy, line_number = entry @@ -517,39 +509,37 @@ def _load_state_from_file(self, file): second_part = ''.join(source) key = (first_part, second_part) if key in self.messages: - raise POError, 'msgid at line %d is duplicated' % line_number + raise POError('msgid at line %d is duplicated' % line_number) # Get the comments and the msgstr in unicode - comments = [ unicode(x, self.encoding) for x in comments ] - - if context is not None: - context = [ unicode(x, self.encoding) for x in context ] - source = [ unicode(x, self.encoding) for x in source ] - target = [ unicode(x, self.encoding) for x in target ] - + # No need to encoding Python 3 + # comments = [str(x, self.encoding) for x in comments] + # + # if context is not None: + # context = [str(x, self.encoding) for x in context] + # source = [str(x, self.encoding) for x in source] + # target = [str(x, self.encoding) for x in target] # Add the message self._set_message(context, source, target, comments, references, fuzzy) - def to_str(self, encoding='UTF-8'): messages = self.messages - message_ids = messages.keys() - message_ids.sort() - messages = [ messages[x].to_str(encoding) for x in message_ids ] + message_ids = sorted(list(messages.keys()), key=lambda x: x[1]) + messages = [messages[x].to_str(encoding) for x in message_ids] return '\n'.join(messages) ####################################################################### # API / Private ####################################################################### - def _set_message(self, context, source, target=freeze([u'']), + def _set_message(self, context, source, target=freeze(['']), comments=freeze([]), references=None, fuzzy=False): - if context is not None and isinstance(context, (str, unicode)): + if context is not None and isinstance(context, (str, str)): context = [context] - if isinstance(source, (str, unicode)): + if isinstance(source, (str, str)): source = [source] - if isinstance(target, (str, unicode)): + if isinstance(target, (str, str)): target = [target] # Make the key @@ -573,25 +563,21 @@ def _set_message(self, context, source, target=freeze([u'']), if not references: return unit for reference in references: - unit.references.setdefault(str(reference[0]), []).append(reference[1]) + unit.references.setdefault(reference[0], []).append(reference[1]) return unit - - ####################################################################### # API / Public ####################################################################### def get_msgids(self): """Returns all the (context, msgid). """ - return self.messages.keys() - + return list(self.messages.keys()) def get_units(self): """Returns all the message (objects of the class ). """ - return self.messages.values() - + return list(self.messages.values()) def get_msgstr(self, source, context=None): """Returns the 'msgstr' for the given (context, msgid). @@ -601,11 +587,9 @@ def get_msgstr(self, source, context=None): return ''.join(message.target) return None - def set_msgstr(self, source, target, context=None): self._set_message(context, [source], [target]) - def gettext(self, source, context=None): """Returns the translation of the given message id. @@ -620,7 +604,6 @@ def gettext(self, source, context=None): return decode_target(target) return source - def add_unit(self, filename, source, context, line): if not source: return None @@ -630,9 +613,8 @@ def add_unit(self, filename, source, context, line): source = encode_source(source) - return self._set_message(context, [source], [u''], [], + return self._set_message(context, [source], [''], [], [(filename, line)]) - register_handler_class(POFile) diff --git a/itools/handlers/__init__.py b/itools/handlers/__init__.py index 60236ae9c..b76f4da39 100644 --- a/itools/handlers/__init__.py +++ b/itools/handlers/__init__.py @@ -31,16 +31,16 @@ """ # Import from itools -from archive import ZIPFile, TARFile, GzipFile, Bzip2File, TGZFile, TBZ2File -from base import Handler -from config import ConfigFile -from file import File -from folder import Folder -from image import Image, SVGFile -from js import JSFile -from registry import register_handler_class, get_handler_class_by_mimetype -from text import TextFile, guess_encoding -from utils import checkid +from .archive import ZIPFile, TARFile, GzipFile, Bzip2File, TGZFile, TBZ2File +from .base import Handler +from .config import ConfigFile +from .file import File +from .folder import Folder +from .image import Image, SVGFile +from .js import JSFile +from .registry import register_handler_class, get_handler_class_by_mimetype +from .text import TextFile, guess_encoding +from .utils import checkid __all__ = [ diff --git a/itools/handlers/archive.py b/itools/handlers/archive.py old mode 100644 new mode 100755 index ef15465a6..258bef493 --- a/itools/handlers/archive.py +++ b/itools/handlers/archive.py @@ -21,12 +21,11 @@ from os.path import join from zipfile import ZipFile from tarfile import open as open_tarfile -from cStringIO import StringIO +from io import StringIO, BytesIO # Import from itools -from file import File -from registry import register_handler_class - +from .file import File +from .registry import register_handler_class class Info(object): @@ -40,18 +39,21 @@ def __init__(self, name, mtime): self.mtime = mtime - class ZIPFile(File): class_mimetypes = ['application/zip'] class_extension = 'zip' - def _open_zipfile(self): - archive = StringIO(self.to_str()) + data = self.to_str() + if isinstance(data, bytes): + archive = BytesIO(data) + elif isinstance(data, str): + archive = StringIO(data) + else: + raise Exception("Error Zipfile") return ZipFile(archive) - def get_members(self): zip = self._open_zipfile() try: @@ -64,7 +66,6 @@ def get_members(self): finally: zip.close() - def get_contents(self): zip = self._open_zipfile() try: @@ -72,7 +73,6 @@ def get_contents(self): finally: zip.close() - def get_file(self, filename): zip = self._open_zipfile() try: @@ -80,7 +80,6 @@ def get_file(self, filename): finally: zip.close() - def extract_to_folder(self, dst): zip = self._open_zipfile() try: @@ -93,19 +92,16 @@ def extract_to_folder(self, dst): zip.close() - class TARFile(File): class_mimetypes = ['application/x-tar'] class_extension = 'tar' class_mode = 'r' - def _open_tarfile(self): archive = StringIO(self.to_str()) return open_tarfile(mode=self.class_mode, fileobj=archive) - def get_members(self): tar = self._open_tarfile() try: @@ -120,7 +116,6 @@ def get_members(self): finally: tar.close() - def get_contents(self): tar = self._open_tarfile() try: @@ -131,7 +126,6 @@ def get_contents(self): finally: tar.close() - def get_file(self, filename): tar = self._open_tarfile() try: @@ -139,7 +133,6 @@ def get_file(self, filename): finally: tar.close() - def extract_to_folder(self, dst): tar = self._open_tarfile() try: @@ -148,7 +141,6 @@ def extract_to_folder(self, dst): tar.close() - class TGZFile(TARFile): class_mimetypes = ['application/x-tgz'] @@ -156,7 +148,6 @@ class TGZFile(TARFile): class_mode = 'r:gz' - class TBZ2File(TARFile): class_mimetypes = ['application/x-tbz2'] @@ -164,21 +155,18 @@ class TBZ2File(TARFile): class_mode = 'r:bz2' - class GzipFile(File): class_mimetypes = ['application/x-gzip'] class_extension = 'gz' - class Bzip2File(File): class_mimetypes = ['application/x-bzip2'] class_extension = 'bz2' - # Register for cls in [ZIPFile, TARFile, TGZFile, TBZ2File, GzipFile, Bzip2File]: register_handler_class(cls) diff --git a/itools/handlers/base.py b/itools/handlers/base.py index 9b9c32bec..17152198f 100644 --- a/itools/handlers/base.py +++ b/itools/handlers/base.py @@ -37,80 +37,71 @@ class Handler(object): database = None key = None - def has_handler(self, reference): database = self.database if database is None: - raise NotImplementedError, MSG_NOT_ATTACHED + raise NotImplementedError(MSG_NOT_ATTACHED) key = database.resolve2(self.key, reference) return database.has_handler(key) - def get_handler_names(self, reference='.'): database = self.database if database is None: - raise NotImplementedError, MSG_NOT_ATTACHED + raise NotImplementedError(MSG_NOT_ATTACHED) key = database.resolve2(self.key, reference) return database.get_handler_names(key) - def get_handler(self, reference, cls=None, soft=False): database = self.database if database is None: - raise NotImplementedError, MSG_NOT_ATTACHED + raise NotImplementedError(MSG_NOT_ATTACHED) key = database.resolve2(self.key, reference) return database._get_handler(key, cls, soft) - def get_handlers(self, reference='.'): database = self.database if database is None: - raise NotImplementedError, MSG_NOT_ATTACHED + raise NotImplementedError(MSG_NOT_ATTACHED) key = database.resolve2(self.key, reference) return database.get_handlers(key) - def set_handler(self, reference, handler): database = self.database if database is None: - raise NotImplementedError, MSG_NOT_ATTACHED + raise NotImplementedError(MSG_NOT_ATTACHED) key = database.resolve2(self.key, reference) database.set_handler(key, handler) - def del_handler(self, reference): database = self.database if database is None: - raise NotImplementedError, MSG_NOT_ATTACHED + raise NotImplementedError(MSG_NOT_ATTACHED) key = database.resolve2(self.key, reference) database.del_handler(key) - def copy_handler(self, source, target, exclude_patterns=None): database = self.database if database is None: - raise NotImplementedError, MSG_NOT_ATTACHED + raise NotImplementedError(MSG_NOT_ATTACHED) source = database.resolve2(self.key, source) target = database.resolve2(self.key, target) database.copy_handler(source, target, exclude_patterns) - def move_handler(self, source, target): database = self.database if database is None: - raise NotImplementedError, MSG_NOT_ATTACHED + raise NotImplementedError(MSG_NOT_ATTACHED) source = database.resolve2(self.key, source) target = database.resolve2(self.key, target) database.move_handler(source, target) - def get_mimetype(self): return self.database.get_mimetype(self.key) diff --git a/itools/handlers/config.py b/itools/handlers/config.py index fc7c85618..690e667f3 100644 --- a/itools/handlers/config.py +++ b/itools/handlers/config.py @@ -27,13 +27,14 @@ # Import from itools from itools.datatypes import String -from text import TextFile +from .text import TextFile ########################################################################### # Lines Analyser (an automaton) ########################################################################### -BLANK, COMMENT, VAR, VAR_START, VAR_CONT, VAR_END, EOF = range(7) +BLANK, COMMENT, VAR, VAR_START, VAR_CONT, VAR_END, EOF = list(range(7)) + def get_lines(file): """Analyses the physical lines, identifies the type and parses them. @@ -89,15 +90,15 @@ def get_lines(file): value = ''.join(groups) value = value.replace('\\"','"') if nb_groups > 3: - raise SyntaxError, 'unescaped char, line %d' % line_num + raise SyntaxError('unescaped char, line %d' % line_num) - if nb_groups in (3,1): + if nb_groups in (3, 1): yield VAR, (name, value), line_num if nb_groups == 2: yield VAR_START, (name, value), line_num state = 1 else: - raise SyntaxError, 'unknown line "%d"' % line_num + raise SyntaxError('unknown line "%d"' % line_num) elif state == 1: # Multiline value if line == '"': @@ -106,9 +107,9 @@ def get_lines(file): groups = split('(?<=[^\\\\])"', line) nb_groups = len(groups) value = groups[0] - value = value.replace('\\"','"') + value = value.replace('\\"', '"') if nb_groups > 2: - raise SyntaxError, 'unescaped char, line %d' % line_num + raise SyntaxError('unescaped char, line %d' % line_num) elif nb_groups == 2: yield VAR_END, value, line_num state = 0 @@ -126,11 +127,11 @@ class Lines(object): def __init__(self, file): self.lines = get_lines(file) - self.next() + next(self) - def next(self): - self.current = self.lines.next() + def __next__(self): + self.current = next(self.lines) @@ -168,26 +169,26 @@ def read_block(lines): """ type, value, line_num = lines.current if type == BLANK: - lines.next() + next(lines) return None elif type == COMMENT: - lines.next() + next(lines) comment = [value] + read_comment(lines) variable = read_variable(lines) return comment, variable elif type == VAR: - lines.next() + next(lines) return [], value elif type == VAR_START: name, value = value - lines.next() + next(lines) value = value + '\n' + read_multiline(lines) return [], (name, value) elif type == VAR_CONT: - lines.next() + next(lines) return None else: - raise SyntaxError, 'unexpected line "%d"' % line_num + raise SyntaxError('unexpected line "%d"' % line_num) def read_comment(lines): @@ -198,7 +199,7 @@ def read_comment(lines): """ type, value, line_num = lines.current if type == COMMENT: - lines.next() + next(lines) return [value] + read_comment(lines) return [] @@ -211,11 +212,11 @@ def read_variable(lines): """ type, value, line_num = lines.current if type == VAR: - lines.next() + next(lines) return value elif type == VAR_START: name, value = value - lines.next() + next(lines) return name, value + '\n' + read_multiline(lines) @@ -227,13 +228,13 @@ def read_multiline(lines): """ type, value, line_num = lines.current if type == VAR_CONT: - lines.next() + next(lines) return value + '\n' + read_multiline(lines) elif type == VAR_END: - lines.next() + next(lines) return value else: - raise SyntaxError, 'unexpected line "%s"' % line_num + raise SyntaxError('unexpected line "%s"' % line_num) ########################################################################### @@ -272,7 +273,6 @@ class ConfigFile(TextFile): class_extension = None schema = None - def new(self, **kw): # Comments are not supported here self.values = {} @@ -281,7 +281,7 @@ def new(self, **kw): n = 0 for name, value in kw.items(): if isinstance(value, str) is False: - raise TypeError, 'the value must be a string.' + raise TypeError('the value must be a string.') # Add the variable, with an empty comment self.lines.append(([], (name, value))) # Separate with a blank line @@ -291,14 +291,13 @@ def new(self, **kw): # Next n += 2 - def _load_state_from_file(self, file): self.lines, self.values = parse(file) - def to_str(self): lines = [] for line in self.lines: + if line is None: # Blank line lines.append('\n') @@ -313,7 +312,6 @@ def to_str(self): return ''.join(lines) - ######################################################################### # API ######################################################################### @@ -328,7 +326,7 @@ def set_value(self, name, value, comment=None): if self.schema is not None and name in self.schema: value = self.schema[name].encode(value) if not isinstance(value, str): - raise TypeError, 'the value must be a string.' + raise TypeError('the value must be a string.') self.set_changed() if name in self.values: @@ -350,7 +348,6 @@ def set_value(self, name, value, comment=None): # Append a blank line self.lines.append(None) - def append_comment(self, comment): """ Appends a solitary comment. @@ -364,7 +361,6 @@ def append_comment(self, comment): # Append a blank line self.lines.append(None) - def get_value(self, name, type=None, default=None): if name not in self.values: if default is not None: @@ -392,7 +388,6 @@ def get_value(self, name, type=None, default=None): return type.decode(value) - def get_comment(self, name): if name not in self.values: return None @@ -402,6 +397,5 @@ def get_comment(self, name): # Return the comment return ' '.join(line[0]) - def has_value(self, name): return name in self.values diff --git a/itools/handlers/file.py b/itools/handlers/file.py old mode 100644 new mode 100755 index 8a2592b87..af5547987 --- a/itools/handlers/file.py +++ b/itools/handlers/file.py @@ -18,15 +18,17 @@ # along with this program. If not, see . # Import from the Standard Library +from logging import getLogger from copy import deepcopy -from cStringIO import StringIO +from io import StringIO, BytesIO from datetime import datetime -from sys import exc_info + # Import from itools.handlers -from base import Handler -from registry import register_handler_class +from .base import Handler +from .registry import register_handler_class +log = getLogger("itools.database") class File(Handler): @@ -49,26 +51,31 @@ class File(Handler): """ class_mimetypes = ['application/octet-stream'] + is_text = False # By default handlers are not loaded timestamp = None dirty = None loaded = False - def __init__(self, key=None, string=None, database=None, **kw): if database is not None: self.database = database else: + try: from itools.database.ro import ro_database self.database = ro_database - except: + except Exception as e: + print(e) if key: - print('Cannot attach handler {0} to a database'.format(key)) - with open(key, 'r') as f: - string = f.read() + log.warning('Cannot attach handler {0} to a database'.format(key)) + try: + string = open(key, 'r').read() + except UnicodeDecodeError: + string = open(key, 'rb').read() key = None + if key is None: self.reset() self.dirty = datetime.now() @@ -82,11 +89,9 @@ def __init__(self, key=None, string=None, database=None, **kw): self.key = self.database.normalize_key(key) self.load_state() - def reset(self): pass - def new(self, data=''): self.data = data @@ -97,21 +102,19 @@ def _load_state_from_file(self, file): """Method to be overriden by sub-classes.""" self.data = file.read() - def load_state(self): - data = self.database.get_handler_data(self.key) + data = self.database.get_handler_data(self.key, text=self.is_text) self.reset() try: self.load_state_from_string(data) except Exception as e: # Update message to add the problematic file - message = '{0} on "{1}"'.format(e.message, self.key) + message = '{0} on "{1}"'.format(e, self.key) self._clean_state() raise self.timestamp = self.database.get_handler_mtime(self.key) self.dirty = None - def load_state_from_file(self, file): self.reset() try: @@ -121,12 +124,15 @@ def load_state_from_file(self, file): raise self.loaded = True - def load_state_from_string(self, string): - file = StringIO(string) + if isinstance(string, bytes): + file = BytesIO(string) + elif isinstance(string, str): + file = StringIO(string) + else: + raise Exception(f"String type error {type(string)}") self.load_state_from_file(file) - def save_state(self): if not self.dirty: return @@ -136,19 +142,17 @@ def save_state(self): self.timestamp = self.database.get_handler_mtime(self.key) self.dirty = None - def save_state_to(self, key): self.database.save_handler(key, self) - clone_exclude = frozenset(['database', 'key', 'timestamp', 'dirty']) + def clone(self, cls=None): # Define the class to build if cls is None: cls = self.__class__ elif not issubclass(cls, self.__class__): - msg = 'the given class must be a subclass of the object' - raise ValueError, msg + raise ValueError("the given class must be a subclass of the object") # Load first, if needed if self.dirty is None: @@ -166,13 +170,12 @@ def clone(self, cls=None): copy.dirty = datetime.now() return copy - def set_changed(self): # Set as changed key = self.key # Invalid handler if key is None and self.dirty is None: - raise RuntimeError, 'cannot change an orphaned file handler' + raise RuntimeError('cannot change an orphaned file handler') # Set as dirty self.dirty = datetime.now() # Free handler (not attached to a database) @@ -182,13 +185,11 @@ def set_changed(self): # Attached database.touch_handler(key, self) - def _clean_state(self): - names = [ x for x in self.__dict__ if x not in ('database', 'key') ] + names = [x for x in self.__dict__ if x not in ('database', 'key')] for name in names: delattr(self, name) - def abort_changes(self): # Not attached to a key or not changed if self.key is None or self.dirty is None: @@ -198,7 +199,6 @@ def abort_changes(self): # Reload state self.load_state() - ######################################################################### # API ######################################################################### @@ -216,23 +216,18 @@ def get_mtime(self): # Not yet loaded, check the FS return self.database.get_handler_mtime(self.key) - def to_str(self): return self.data - def set_data(self, data): self.set_changed() self.data = data - def to_text(self): raise NotImplementedError - def is_empty(self): raise NotImplementedError - register_handler_class(File) diff --git a/itools/handlers/folder.py b/itools/handlers/folder.py index bd1647d77..4ade15c23 100644 --- a/itools/handlers/folder.py +++ b/itools/handlers/folder.py @@ -17,9 +17,8 @@ # along with this program. If not, see . # Import from itools -from base import Handler -from registry import register_handler_class - +from .base import Handler +from .registry import register_handler_class class Context(object): @@ -30,7 +29,6 @@ def __init__(self): self.skip = False - class Folder(Handler): """This is the base handler class for any folder handler. It is also used as the default handler class for any folder resource that has not a more @@ -41,7 +39,6 @@ class Folder(Handler): dirty = False - def __init__(self, key=None, database=None, **kw): if database is not None: self.database = database @@ -51,7 +48,6 @@ def __init__(self, key=None, database=None, **kw): if key is not None: self.key = self.database.normalize_key(key) - def get_mtime(self): """Returns the last modification time. """ @@ -60,7 +56,6 @@ def get_mtime(self): return None - def traverse(self): yield self for name in self.get_handler_names(): @@ -71,7 +66,6 @@ def traverse(self): else: yield handler - def traverse2(self, context=None): if context is None: context = Context() diff --git a/itools/handlers/image.py b/itools/handlers/image.py index 9dbdf38c1..b00657b30 100644 --- a/itools/handlers/image.py +++ b/itools/handlers/image.py @@ -19,7 +19,7 @@ # along with this program. If not, see . # Import from the Standard Library -from cStringIO import StringIO +from io import BytesIO # Import from the Python Image Library try: @@ -38,15 +38,14 @@ rsvg_handle = None # Import from itools -from file import File -from registry import register_handler_class +from .file import File +from .registry import register_handler_class # This number controls the max surface ratio that we can lose when we crop. MAX_CROP_RATIO = 2.0 - class Image(File): class_mimetypes = ['image'] @@ -61,13 +60,12 @@ def _load_state_from_file(self, file): else: self.size = (0, 0) - def _get_handle(self): if PIL is False: return None # Open image - f = StringIO(self.data) + f = BytesIO(self.data) try: im = open_image(f) except (IOError, OverflowError): @@ -76,22 +74,18 @@ def _get_handle(self): # Ok return im - def _get_size(self, handle): return handle.size - def _get_format(self, handle): return handle.format - ######################################################################### # API ######################################################################### def get_size(self): return self.size - def get_thumbnail(self, xnewsize, ynewsize, format=None, fit=False): # Get the handle handle = self._get_handle() @@ -132,9 +126,9 @@ def get_thumbnail(self, xnewsize, ynewsize, format=None, fit=False): im, xsize, ysize = self._scale_down(handle, ratio) # To string - output = StringIO() + output = BytesIO() # JPEG : Convert to RGB - if format.lower() == 'jpeg': + if format.lower() in ("jpeg", "mpo"): im = im.convert("RGB") im.save(output, format, quality=80) value = output.getvalue() @@ -143,7 +137,6 @@ def get_thumbnail(self, xnewsize, ynewsize, format=None, fit=False): # Ok return value, format.lower() - def _scale_down(self, im, ratio): # Convert to RGBA im = im.convert("RGBA") @@ -157,12 +150,10 @@ def _scale_down(self, im, ratio): return im, xsize, ysize - class SVGFile(Image): class_mimetypes = ['image/svg+xml'] - def _get_handle(self): if rsvg_handle is None: return None @@ -173,15 +164,12 @@ def _get_handle(self): svg.close() return svg - def _get_size(self, handle): return handle.get_property('width'), handle.get_property('height') - def _get_format(self, handle): return 'PNG' - def _scale_down(self, handle, ratio): xsize, ysize = self.size if ratio >= 1.0: @@ -206,6 +194,5 @@ def _scale_down(self, handle, ratio): return im, xsize, ysize - register_handler_class(Image) register_handler_class(SVGFile) diff --git a/itools/handlers/js.py b/itools/handlers/js.py index aae207e2b..ea53671eb 100644 --- a/itools/handlers/js.py +++ b/itools/handlers/js.py @@ -15,10 +15,8 @@ # along with this program. If not, see . # Import from itools -from registry import register_handler_class -from text import TextFile - - +from .registry import register_handler_class +from .text import TextFile class JSFile(TextFile): @@ -26,7 +24,6 @@ class JSFile(TextFile): class_mimetypes = ['application/x-javascript', 'application/javascript'] class_extension = 'js' - def get_units(self, srx_handler=None): return [] diff --git a/itools/handlers/registry.py b/itools/handlers/registry.py index 531523042..db5285e5d 100644 --- a/itools/handlers/registry.py +++ b/itools/handlers/registry.py @@ -36,4 +36,4 @@ def get_handler_class_by_mimetype(mimetype, soft=False): if soft: return None - raise ValueError, mimetype + raise ValueError(mimetype) diff --git a/itools/handlers/text.py b/itools/handlers/text.py old mode 100644 new mode 100755 index ddd687933..5624fb14e --- a/itools/handlers/text.py +++ b/itools/handlers/text.py @@ -16,8 +16,8 @@ # along with this program. If not, see . # Import from itools -from file import File -from registry import register_handler_class +from .file import File +from .registry import register_handler_class def guess_encoding(data): @@ -25,9 +25,12 @@ def guess_encoding(data): the wrong encoding, for example many utf8 files will be identified as latin1. """ + if isinstance(data, bytes): + return 'utf8' + for encoding in ('ascii', 'utf8', 'iso8859'): try: - unicode(data, encoding) + data.encode(encoding) except UnicodeError: pass else: @@ -41,17 +44,16 @@ class TextFile(File): class_mimetypes = ['text'] class_extension = 'txt' + is_text = True - - def new(self, data=u''): + def new(self, data=''): self.data = data self.encoding = 'utf-8' - def _load_state_from_file(self, file): data = file.read() self.encoding = guess_encoding(data) - self.data = unicode(data, self.encoding) + self.data = data ######################################################################### @@ -60,18 +62,18 @@ def _load_state_from_file(self, file): def get_encoding(self): return self.encoding - def to_str(self, encoding='utf-8'): - return self.data.encode(encoding) + # XXX self.data should always be unicode + if type(self.data) is bytes: + return self.data + return self.data.encode(encoding) def to_text(self): - return unicode(self.to_str(), 'utf-8') - + return str(self.to_str(), 'utf-8') def is_empty(self): - return self.to_text().strip() == u"" - + return self.to_text().strip() == "" register_handler_class(TextFile) diff --git a/itools/handlers/utils.py b/itools/handlers/utils.py old mode 100644 new mode 100755 index f9fbe3a0c..fa230be12 --- a/itools/handlers/utils.py +++ b/itools/handlers/utils.py @@ -19,60 +19,61 @@ # Import from the Standard Library import unicodedata +import sys +src = (r"""ÄÅÁÀÂÃĀäåáàâãāăÇçÉÈÊËĒéèêëēğÍÌÎÏĪíìîïīıļÑñÖÓÒÔÕØŌöóòôõøōőÜÚÙÛŪüúùûū + ŞşšţÝŸȲýÿȳŽž°«»’""") +dst = (r"""AAAAAAAaaaaaaaaCcEEEEEeeeeegIIIIIiiiiiilNnOOOOOOOooooooooUUUUUuuuuu + SsstYYYyyyZz----""") -src = (ur"""ÄÅÁÀÂÃĀäåáàâãāăÇçÉÈÊËĒéèêëēğÍÌÎÏĪíìîïīıļÑñÖÓÒÔÕØŌöóòôõøōőÜÚÙÛŪüúùûū""" - ur"""ŞşšţÝŸȲýÿȳŽž°«»’""") -dst = (ur"""AAAAAAAaaaaaaaaCcEEEEEeeeeegIIIIIiiiiiilNnOOOOOOOooooooooUUUUUuuuuu""" - ur"""SsstYYYyyyZz----""") transmap = {} for i in range(len(src)): a, b = src[i], dst[i] transmap[ord(a)] = b -transmap[ord(u'æ')] = u'ae' -transmap[ord(u'Æ')] = u'AE' -transmap[ord(u'œ')] = u'oe' -transmap[ord(u'Œ')] = u'OE' -transmap[ord(u'ß')] = u'ss' +transmap[ord('æ')] = 'ae' +transmap[ord('Æ')] = 'AE' +transmap[ord('œ')] = 'oe' +transmap[ord('Œ')] = 'OE' +transmap[ord('ß')] = 'ss' -def checkid(id, soft=True): +def checkid(_id, soft=True): """Turn a bytestring or unicode into an identifier only composed of alphanumerical characters and a limited list of signs. It only supports Latin-based alphabets. """ - if type(id) is str: - id = unicode(id, 'utf8') + if type(_id) is str: + _id = _id # Normalize unicode - id = unicodedata.normalize('NFKC', id) + _id = unicodedata.normalize('NFKC', _id) # Strip diacritics - id = id.strip().translate(transmap) + _id = _id.strip().translate(transmap) # Check for unallowed characters if soft: - allowed_characters = set([u'.', u'-', u'_', u'@']) - id = [ x if (x.isalnum() or x in allowed_characters) else u'-' - for x in id ] - id = u''.join(id) + allowed_characters = {'.', '-', '_', '@'} + _id = [ x if (x.isalnum() or x in allowed_characters) else '-' + for x in _id] + _id = ''.join(_id) # Merge hyphens - id = id.split(u'-') - id = u'-'.join([x for x in id if x]) - id = id.strip('-') + _id = _id.split('-') + _id = '-'.join([x for x in _id if x]) + _id = _id.strip('-') - # Check wether the id is empty - if len(id) == 0: + # Check wether the _id is empty + if len(_id) == 0: return None # No mixed case - id = id.lower() + _id = _id.lower() # Most FS are limited in 255 chars per name # (keep space for ".metadata" extension) - id = id[:246] + _id = _id[:246] # Return a safe ASCII bytestring - return str(id) + return str(_id) diff --git a/itools/html/__init__.py b/itools/html/__init__.py index 4292663a9..43bd71598 100644 --- a/itools/html/__init__.py +++ b/itools/html/__init__.py @@ -19,12 +19,12 @@ # Import from itools from itools.core import add_type, get_abspath from itools.xml import register_dtd, DocType -from filters import sanitize_stream, sanitize_str -from html import HTMLFile -from parser import HTMLParser -from xhtml import XHTMLFile, xhtml_uri, stream_is_empty -from xhtml import stream_to_str_as_html, stream_to_str_as_xhtml -import schema +from . import schema +from .filters import sanitize_stream, sanitize_str +from .html import HTMLFile +from .parser import HTMLParser +from .xhtml import XHTMLFile, xhtml_uri, stream_is_empty +from .xhtml import stream_to_str_as_html, stream_to_str_as_xhtml # Public API diff --git a/itools/html/filters.py b/itools/html/filters.py index 456e7eaba..9e147ec4c 100644 --- a/itools/html/filters.py +++ b/itools/html/filters.py @@ -78,7 +78,7 @@ def sanitize_stream(stream): continue # Filter attributes attributes = attributes.copy() - for attr_key in attributes.keys(): + for attr_key in list(attributes.keys()): attr_value = attributes[attr_key] attr_uri, attr_name = attr_key # Check it is a safe attribute @@ -114,7 +114,6 @@ def sanitize_stream(stream): yield event - def sanitize_str(str): stream = XMLParser(str) return sanitize_stream(stream) diff --git a/itools/html/html.py b/itools/html/html.py index 7da62a43b..f3fb1f86f 100644 --- a/itools/html/html.py +++ b/itools/html/html.py @@ -18,9 +18,8 @@ # Import from itools from itools.handlers import register_handler_class from itools.xmlfile import translate -from xhtml import XHTMLFile, stream_to_str_as_html -from parser import HTMLParser - +from .xhtml import XHTMLFile, stream_to_str_as_html +from .parser import HTMLParser class HTMLFile(XHTMLFile): @@ -51,22 +50,18 @@ def get_skeleton(cls, title=''): '') return skeleton % {'title': title} - def _load_state_from_file(self, file): data = file.read() stream = HTMLParser(data) self.events = list(stream) - def load_state_from_string(self, string): self.reset() stream = HTMLParser(string) self.events = list(stream) - to_str = XHTMLFile.to_html - def translate(self, catalog, srx_handler=None): stream = translate(self.events, catalog, srx_handler) return stream_to_str_as_html(stream) diff --git a/itools/html/parser.py b/itools/html/parser.py old mode 100644 new mode 100755 index b3f5d4716..62321ae12 --- a/itools/html/parser.py +++ b/itools/html/parser.py @@ -16,14 +16,15 @@ # along with this program. If not, see . # Import from the Standard Library -import htmlentitydefs -from HTMLParser import HTMLParser as BaseParser, HTMLParseError +import sys +import html.entities +from html.parser import HTMLParser as BaseParser from warnings import warn # Import from itools from itools.xml import DOCUMENT_TYPE, START_ELEMENT, END_ELEMENT, COMMENT from itools.xml import TEXT, XMLError, DocType -from xhtml import xhtml_uri +from .xhtml import xhtml_uri ########################################################################### @@ -40,7 +41,6 @@ def __init__(self, PubidLiteral=None, SystemLiteral=None, intSubset=None): self.SystemLiteral = SystemLiteral self.intSubset = intSubset - def to_str(self): """Return a 'ready to insert' representation of the doctype """ @@ -223,13 +223,11 @@ class Parser(BaseParser, object): def parse(self, data): self.encoding = 'UTF-8' - self.events = [] self.feed(data) self.close() return self.events - def handle_decl(self, value): # The document type declaration of HTML5 documents is always (case # insensitive): @@ -289,7 +287,6 @@ def read_id(value): SystemLiteral=system_id) self.events.append((DOCUMENT_TYPE, value, line)) - def handle_starttag(self, name, attrs): events = self.events line = self.getpos()[0] @@ -314,32 +311,28 @@ def handle_starttag(self, name, attrs): attribute_value = attribute_name else: message = 'missing attribute value for "%s"' - raise XMLError, message % attribute_name - elif type(attribute_value) is unicode: - attribute_value = attribute_value.encode(self.encoding) + raise XMLError(message % attribute_name) + elif type(attribute_value) is str: + attribute_value = attribute_value attributes[(None, attribute_name)] = attribute_value # Start element events.append((START_ELEMENT, (xhtml_uri, name, attributes), line)) - def handle_endtag(self, name): self.events.append((END_ELEMENT, (xhtml_uri, name), self.getpos()[0])) - def handle_comment(self, data): self.events.append((COMMENT, data, self.getpos()[0])) - def handle_data(self, data): self.events.append((TEXT, data, self.getpos()[0])) - def handle_entityref(self, name): # FIXME Duplicated code, also written in C in "xml/parser.c". - if name in htmlentitydefs.name2codepoint: - codepoint = htmlentitydefs.name2codepoint[name] - char = unichr(codepoint) + if name in html.entities.name2codepoint: + codepoint = html.entities.name2codepoint[name] + char = chr(codepoint) try: char = char.encode(self.encoding) except UnicodeEncodeError: @@ -359,7 +352,6 @@ def handle_entityref(self, name): ## def handle_pi(self, data): - def make_xml_compatible(stream): stack = [] for event in stream: @@ -399,7 +391,7 @@ def make_xml_compatible(stream): last = stack.pop() else: msg = 'missing end tag at line %s' - raise XMLError, msg % (last, line) + raise XMLError(msg % (last, line)) yield event else: yield event @@ -411,17 +403,19 @@ def make_xml_compatible(stream): yield END_ELEMENT, (xhtml_uri, last), line else: msg = 'missing end tag at line %s' - raise XMLError, msg % (last, line) - + raise XMLError(msg % (last, line)) def HTMLParser(data): if type(data) is not str: - raise TypeError, 'expected a byte string, "%s" found' % type(data) + data = data.decode("utf-8") + #raise TypeError('expected a byte string, "%s" found' % type(data)) + try: stream = Parser().parse(data) - except HTMLParseError, message: - raise XMLError, message + except Exception as message: + raise XMLError(message) + stream = make_xml_compatible(stream) # TODO Don't transform to a list, keep the stream return list(stream) diff --git a/itools/html/schema.py b/itools/html/schema.py old mode 100644 new mode 100755 index 110c1f69a..fcd0be3eb --- a/itools/html/schema.py +++ b/itools/html/schema.py @@ -36,7 +36,6 @@ class Boolean(Boolean): def decode(value): return value - @staticmethod def encode(value): return value @@ -259,32 +258,27 @@ def get_attr_datatype(self, name, attr_attributes): return String - class BlockElement(Element): is_inline = False - class EmptyElement(Element): is_empty = True - class EmptyBlockElement(Element): is_inline = False is_empty = True - class InputElement(Element): is_inline = True is_empty = True - def get_attr_datatype(self, attr_name, attributes): if attr_name == 'value': key1 = (html_uri, 'type') @@ -296,7 +290,6 @@ def get_attr_datatype(self, attr_name, attributes): return proxy.get_attr_datatype(attr_name, attributes) - class WebComponentElement(BlockElement): def get_attr_datatype(self, attr_name, attributes): @@ -625,7 +618,6 @@ def get_attr_datatype(self, attr_name, attributes): ] - class HTML5Namespace(XMLNamespace): uri = html_uri @@ -645,7 +637,6 @@ def obsolete_elements_kw(self): kw[name] = element return kw - def get_element_schema(self, name): element = self.elements_kw.get(name) if element is not None: diff --git a/itools/html/xhtml.py b/itools/html/xhtml.py index 3af299027..045166f56 100644 --- a/itools/html/xhtml.py +++ b/itools/html/xhtml.py @@ -27,7 +27,7 @@ from itools.xmlfile import XMLFile # Import from here -from schema import html_namespace, html_uri as xhtml_uri +from .schema import html_namespace, html_uri as xhtml_uri @@ -40,13 +40,17 @@ def get_start_tag(value): qname = get_attribute_qname(attr_uri, attr_name) value = XMLAttribute.encode(value) s += ' %s="%s"' % (qname, value) + # Close the start tag + schema = html_namespace.get_element_schema(tag_name) + if getattr(schema, 'is_empty', False): + return s + '/>' return s + '>' def get_end_tag(value): tag_uri, tag_name = value if tag_uri and tag_uri != xhtml_uri: - raise ValueError, tag_uri + raise ValueError(tag_uri) # Case 1: empty schema = html_namespace.get_element_schema(tag_name) diff --git a/itools/i18n/__init__.py b/itools/i18n/__init__.py index 1a4ff3ee5..37b09be90 100644 --- a/itools/i18n/__init__.py +++ b/itools/i18n/__init__.py @@ -17,13 +17,13 @@ # along with this program. If not, see . # Import from itools -from accept import AcceptLanguageType, get_accept, select_language -from accept import init_language_selector -from fuzzy import get_distance, get_similarity, is_similar, get_most_similar -from languages import has_language, get_languages, get_language_name -from locale_ import format_date, format_time, format_datetime -from locale_ import format_number -from oracle import guess_language, is_asian_character, is_punctuation +from .accept import AcceptLanguageType, get_accept, select_language +from .accept import init_language_selector +from .fuzzy import get_distance, get_similarity, is_similar, get_most_similar +from .languages import has_language, get_languages, get_language_name +from .locale_ import format_date, format_time, format_datetime +from .locale_ import format_number +from .oracle import guess_language, is_asian_character, is_punctuation diff --git a/itools/i18n/accept.py b/itools/i18n/accept.py index 02bae5f8d..5634aca0a 100644 --- a/itools/i18n/accept.py +++ b/itools/i18n/accept.py @@ -20,17 +20,16 @@ """ # Import from the Standard Library -import __builtin__ + +import builtins from decimal import Decimal from locale import getdefaultlocale - zero = Decimal('0.0') one = Decimal('1.0') - # FIXME This class belongs to the public API, but it is not exposed since # we never create it directly (we use AcceptLanguageType). class AcceptLanguage(dict): @@ -47,7 +46,6 @@ def set(self, key, quality): key = '' self[key] = quality - def get_quality(self, language): """This method returns the quality of the given language. As defined by the RFC 2616, if a langage is not defined it inherits the quality @@ -66,7 +64,6 @@ def get_quality(self, language): return self.get(language, zero), steps - def select_language(self, languages): """This is the selection language algorithm, it returns the user prefered language for the given list of available languages, if the @@ -87,7 +84,6 @@ def select_language(self, languages): return language - class AcceptLanguageType(object): @staticmethod @@ -117,13 +113,11 @@ def decode(data): return AcceptLanguage(accept) - @staticmethod def encode(accept): # Sort - accept = [ (y, x) for x, y in accept.items() ] - accept.sort() - accept.reverse() + accept = [(y, x) for x, y in accept.items()] + accept = sorted(accept, key=lambda x: x[0], reverse=True) # Encode data = [] for quality, language in accept: @@ -138,7 +132,6 @@ def encode(accept): return ', '.join(data) - def get_accept(): language = getdefaultlocale()[0] if language is None: @@ -150,14 +143,12 @@ def get_accept(): return AcceptLanguageType.decode(language) - def select_language(languages=None): return get_accept().select_language(languages) - def init_language_selector(language_selector=select_language): - __builtin__.__dict__['select_language'] = language_selector + builtins.__dict__['select_language'] = language_selector # Set default language selector diff --git a/itools/i18n/languages.py b/itools/i18n/languages.py old mode 100644 new mode 100755 index c4bf4c6c4..ba69a87f0 --- a/itools/i18n/languages.py +++ b/itools/i18n/languages.py @@ -19,211 +19,211 @@ languages = { - 'aa': u'Afar', - 'ab': u'Abkhazian', - 'ae': u'Avestan', - 'af': u'Afrikaans', - 'ak': u'Akan', - 'am': u'Amharic', - 'an': u'Aragonese', - 'ar': u'Arabic', - 'as': u'Assamese', - 'av': u'Avaric', - 'ay': u'Aymara', - 'az': u'Azerbaijani', - 'ba': u'Bashkir', - 'be': u'Belarusian', - 'bg': u'Bulgarian', - 'bh': u'Bihari', - 'bi': u'Bislama', - 'bm': u'Bambara', - 'bn': u'Bengali', - 'bo': u'Tibetan', - 'br': u'Breton', - 'bs': u'Bosnian', - 'ca': u'Catalan', - 'ce': u'Chechen', - 'ch': u'Chamorro', - 'co': u'Corsican', - 'cr': u'Cree', - 'cs': u'Czech', - 'cu': u'Church Slavic', - 'cv': u'Chuvash', - 'cy': u'Welsh', - 'da': u'Danish', - 'de': u'German', - 'de-AU': u'German/Austria', - 'de-DE': u'German/Germany', - 'de-CH': u'German/Switzerland', - 'dv': u'Divehi; Dhivehi; Maldivian', - 'dz': u'Dzongkha', - 'ee': u'Ewe', - 'el': u'Greek', - 'en': u'English', - 'en-GB': u'English/United Kingdom', - 'en-US': u'English/United States', - 'eo': u'Esperanto', - 'es': u'Spanish', - 'es-AR': u'Spanish/Argentina', - 'es-CO': u'Spanish/Colombia', - 'es-MX': u'Spanish/Mexico', - 'es-ES': u'Spanish/Spain', - 'et': u'Estonian', - 'eu': u'Basque', - 'fa': u'Persian', - 'ff': u'Fulah', - 'fi': u'Finnish', - 'fj': u'Fijian', - 'fo': u'Faroese', - 'fr': u'French', - 'fr-BE': u'French/Belgium', - 'fr-CA': u'French/Canada', - 'fr-FR': u'French/France', - 'fr-CH': u'French/Switzerland', - 'fy': u'Frisian', - 'ga': u'Irish', - 'gd': u'Gaelic', - 'gl': u'Galician', - 'gn': u'Guarani', - 'gu': u'Gujarati', - 'gv': u'Manx', - 'ha': u'Hausa', - 'he': u'Hebrew', - 'hi': u'Hindi', - 'ho': u'Hiri Motu', - 'hr': u'Croatian', - 'ht': u'Haitian', - 'hu': u'Hungarian', - 'hy': u'Armenian', - 'hz': u'Herero', - 'ia': u'Interlingua', - 'id': u'Indonesian', - 'ie': u'Interlingue; Occidental', - 'ig': u'Igbo', - 'ii': u'Sichuan Yi; Nuosu', - 'ik': u'Inupiak', - 'io': u'Ido', - 'is': u'Icelandic', - 'it': u'Italian', - 'iu': u'Inuktitut', - 'ja': u'Japanese', - 'jv': u'Javanese', - 'ka': u'Georgian', - 'kg': u'Kongo', - 'ki': u'Kikuyu; Gikuyu', - 'kj': u'Kuanyama; Kwanyama', - 'kk': u'Kazakh', - 'kl': u'Kalaallisut; Greenlandic', - 'km': u'Khmer', - 'kn': u'Kannada', - 'ko': u'Korean', - 'kr': u'Kanuri', - 'ks': u'Kashmiri', - 'ku': u'Kurdish', - 'kv': u'Komi', - 'kw': u'Cornish', - 'ky': u'Kirghiz; Kyrgyz', - 'la': u'Latin', - 'lb': u'Luxembourgish', - 'lg': u'Ganda', - 'li': u'Limburgan', - 'ln': u'Lingala', - 'lo': u'Lao', - 'lt': u'Lithuanian', - 'lu': u'Luba-Katanga', - 'lv': u'Latvian', - 'mg': u'Malagasy', - 'mh': u'Marshallese', - 'mi': u'Maori', - 'mk': u'Macedonian', - 'ml': u'Malayalam', - 'mn': u'Mongolian', - 'mr': u'Marathi', - 'ms': u'Malay', - 'mt': u'Maltese', - 'my': u'Burmese', - 'na': u'Nauru', - 'nb': u'Norwegian Bokmal', - 'nd': u'Ndebele, North', - 'ne': u'Nepali', - 'ng': u'Ndonga', - 'nl': u'Dutch', - 'nl-BE': u'Dutch/Belgium', - 'nn': u'Norwegian Nyrnosk', - 'no': u'Norwegian', - 'nr': u'Ndebele, South', - 'nv': u'Navajo; Navaho', - 'ny': u'Chichewa; Chewa; Nyanja', - 'oc': u'Occitan', - 'oj': u'Ojibwa', - 'om': u'Oromo', - 'or': u'Oriya', - 'os': u'Ossetian; Ossetic', - 'pa': u'Panjabi; Punjabi', - 'pi': u'Pali', - 'pl': u'Polish', - 'ps': u'Pushto; Pashto', - 'pt': u'Portuguese', - 'pt-BR': u'Portuguese/Brazil', - 'qu': u'Quechua', - 'rm': u'Romansh', - 'rn': u'Rundi', - 'ro': u'Romanian; Moldavian', - 'ru': u'Russian', - 'rw': u'Kinyarwanda', - 'sa': u'Sanskrit', - 'sc': u'Sardinian', - 'sd': u'Sindhi', - 'se': u'Sami', - 'sg': u'Sango', - 'si': u'Sinhala; Sinhalese', - 'sk': u'Slovak', - 'sl': u'Slovenian', - 'sm': u'Samoan', - 'sn': u'Shona', - 'so': u'Somali', - 'sq': u'Albanian', - 'sr': u'Serbian', - 'ss': u'Swati', - 'st': u'Sotho', - 'su': u'Sundanese', - 'sv': u'Swedish', - 'sw': u'Swahili', - 'ta': u'Tamil', - 'te': u'Telugu', - 'tg': u'Tajik', - 'th': u'Thai', - 'ti': u'Tigrinya', - 'tk': u'Turkmen', - 'tl': u'Tagalog', - 'tn': u'Tswana', - 'to': u'Tonga', - 'tr': u'Turkish', - 'ts': u'Tsonga', - 'tt': u'Tatar', - 'tw': u'Twi', - 'ty': u'Tahitian', - 'ug': u'Uighur; Uyghur', - 'uk': u'Ukrainian', - 'ur': u'Urdu', - 'uz': u'Uzbek', - 've': u'Venda', - 'vi': u'Vietnamese', - 'vo': u'Volapuk', - 'wa': u'Walloon', - 'wo': u'Wolof', - 'xh': u'Xhosa', - 'yi': u'Yiddish', - 'yo': u'Yoruba', - 'za': u'Zhuang; Chuang', - 'zh': u'Chinese', - 'zh-CN': u'Chinese/China', - 'zh-TW': u'Chinese/Taiwan', - 'zu': u'Zulu', + 'aa': 'Afar', + 'ab': 'Abkhazian', + 'ae': 'Avestan', + 'af': 'Afrikaans', + 'ak': 'Akan', + 'am': 'Amharic', + 'an': 'Aragonese', + 'ar': 'Arabic', + 'as': 'Assamese', + 'av': 'Avaric', + 'ay': 'Aymara', + 'az': 'Azerbaijani', + 'ba': 'Bashkir', + 'be': 'Belarusian', + 'bg': 'Bulgarian', + 'bh': 'Bihari', + 'bi': 'Bislama', + 'bm': 'Bambara', + 'bn': 'Bengali', + 'bo': 'Tibetan', + 'br': 'Breton', + 'bs': 'Bosnian', + 'ca': 'Catalan', + 'ce': 'Chechen', + 'ch': 'Chamorro', + 'co': 'Corsican', + 'cr': 'Cree', + 'cs': 'Czech', + 'cu': 'Church Slavic', + 'cv': 'Chuvash', + 'cy': 'Welsh', + 'da': 'Danish', + 'de': 'German', + 'de-AU': 'German/Austria', + 'de-DE': 'German/Germany', + 'de-CH': 'German/Switzerland', + 'dv': 'Divehi; Dhivehi; Maldivian', + 'dz': 'Dzongkha', + 'ee': 'Ewe', + 'el': 'Greek', + 'en': 'English', + 'en-GB': 'English/United Kingdom', + 'en-US': 'English/United States', + 'eo': 'Esperanto', + 'es': 'Spanish', + 'es-AR': 'Spanish/Argentina', + 'es-CO': 'Spanish/Colombia', + 'es-MX': 'Spanish/Mexico', + 'es-ES': 'Spanish/Spain', + 'et': 'Estonian', + 'eu': 'Basque', + 'fa': 'Persian', + 'ff': 'Fulah', + 'fi': 'Finnish', + 'fj': 'Fijian', + 'fo': 'Faroese', + 'fr': 'French', + 'fr-BE': 'French/Belgium', + 'fr-CA': 'French/Canada', + 'fr-FR': 'French/France', + 'fr-CH': 'French/Switzerland', + 'fy': 'Frisian', + 'ga': 'Irish', + 'gd': 'Gaelic', + 'gl': 'Galician', + 'gn': 'Guarani', + 'gu': 'Gujarati', + 'gv': 'Manx', + 'ha': 'Hausa', + 'he': 'Hebrew', + 'hi': 'Hindi', + 'ho': 'Hiri Motu', + 'hr': 'Croatian', + 'ht': 'Haitian', + 'hu': 'Hungarian', + 'hy': 'Armenian', + 'hz': 'Herero', + 'ia': 'Interlingua', + 'id': 'Indonesian', + 'ie': 'Interlingue; Occidental', + 'ig': 'Igbo', + 'ii': 'Sichuan Yi; Nuosu', + 'ik': 'Inupiak', + 'io': 'Ido', + 'is': 'Icelandic', + 'it': 'Italian', + 'iu': 'Inuktitut', + 'ja': 'Japanese', + 'jv': 'Javanese', + 'ka': 'Georgian', + 'kg': 'Kongo', + 'ki': 'Kikuyu; Gikuyu', + 'kj': 'Kuanyama; Kwanyama', + 'kk': 'Kazakh', + 'kl': 'Kalaallisut; Greenlandic', + 'km': 'Khmer', + 'kn': 'Kannada', + 'ko': 'Korean', + 'kr': 'Kanuri', + 'ks': 'Kashmiri', + 'ku': 'Kurdish', + 'kv': 'Komi', + 'kw': 'Cornish', + 'ky': 'Kirghiz; Kyrgyz', + 'la': 'Latin', + 'lb': 'Luxembourgish', + 'lg': 'Ganda', + 'li': 'Limburgan', + 'ln': 'Lingala', + 'lo': 'Lao', + 'lt': 'Lithuanian', + 'lu': 'Luba-Katanga', + 'lv': 'Latvian', + 'mg': 'Malagasy', + 'mh': 'Marshallese', + 'mi': 'Maori', + 'mk': 'Macedonian', + 'ml': 'Malayalam', + 'mn': 'Mongolian', + 'mr': 'Marathi', + 'ms': 'Malay', + 'mt': 'Maltese', + 'my': 'Burmese', + 'na': 'Nauru', + 'nb': 'Norwegian Bokmal', + 'nd': 'Ndebele, North', + 'ne': 'Nepali', + 'ng': 'Ndonga', + 'nl': 'Dutch', + 'nl-BE': 'Dutch/Belgium', + 'nn': 'Norwegian Nyrnosk', + 'no': 'Norwegian', + 'nr': 'Ndebele, South', + 'nv': 'Navajo; Navaho', + 'ny': 'Chichewa; Chewa; Nyanja', + 'oc': 'Occitan', + 'oj': 'Ojibwa', + 'om': 'Oromo', + 'or': 'Oriya', + 'os': 'Ossetian; Ossetic', + 'pa': 'Panjabi; Punjabi', + 'pi': 'Pali', + 'pl': 'Polish', + 'ps': 'Pushto; Pashto', + 'pt': 'Portuguese', + 'pt-BR': 'Portuguese/Brazil', + 'qu': 'Quechua', + 'rm': 'Romansh', + 'rn': 'Rundi', + 'ro': 'Romanian; Moldavian', + 'ru': 'Russian', + 'rw': 'Kinyarwanda', + 'sa': 'Sanskrit', + 'sc': 'Sardinian', + 'sd': 'Sindhi', + 'se': 'Sami', + 'sg': 'Sango', + 'si': 'Sinhala; Sinhalese', + 'sk': 'Slovak', + 'sl': 'Slovenian', + 'sm': 'Samoan', + 'sn': 'Shona', + 'so': 'Somali', + 'sq': 'Albanian', + 'sr': 'Serbian', + 'ss': 'Swati', + 'st': 'Sotho', + 'su': 'Sundanese', + 'sv': 'Swedish', + 'sw': 'Swahili', + 'ta': 'Tamil', + 'te': 'Telugu', + 'tg': 'Tajik', + 'th': 'Thai', + 'ti': 'Tigrinya', + 'tk': 'Turkmen', + 'tl': 'Tagalog', + 'tn': 'Tswana', + 'to': 'Tonga', + 'tr': 'Turkish', + 'ts': 'Tsonga', + 'tt': 'Tatar', + 'tw': 'Twi', + 'ty': 'Tahitian', + 'ug': 'Uighur; Uyghur', + 'uk': 'Ukrainian', + 'ur': 'Urdu', + 'uz': 'Uzbek', + 've': 'Venda', + 'vi': 'Vietnamese', + 'vo': 'Volapuk', + 'wa': 'Walloon', + 'wo': 'Wolof', + 'xh': 'Xhosa', + 'yi': 'Yiddish', + 'yo': 'Yoruba', + 'za': 'Zhuang; Chuang', + 'zh': 'Chinese', + 'zh-CN': 'Chinese/China', + 'zh-TW': 'Chinese/Taiwan', + 'zu': 'Zulu', } -langs = [ {'code': x, 'name': languages[x]} for x in sorted(languages.keys()) ] +langs = [{'code': x, 'name': languages[x]} for x in sorted(languages.keys())] ########################################################################### @@ -236,8 +236,7 @@ def has_language(code): def get_languages(): """Returns a list of tuples with the code and the name of each language. """ - return [ x.copy() for x in langs ] - + return [x.copy() for x in langs] def get_language_name(code): @@ -247,5 +246,5 @@ def get_language_name(code): # from the itools.gettext module, which is higher level than itools.i18n if code in languages: return languages[code] - return u'???' + return '???' diff --git a/itools/i18n/locale_.py b/itools/i18n/locale_.py old mode 100644 new mode 100755 index 2d6248acc..7a42ac0cf --- a/itools/i18n/locale_.py +++ b/itools/i18n/locale_.py @@ -23,7 +23,7 @@ from decimal import Decimal # Import from itools -from accept import get_accept +from .accept import get_accept def get_format(source, accept): @@ -32,7 +32,7 @@ def get_format(source, accept): accept = get_accept() # Negotiate - available_languages = source.keys() + available_languages = list(source.keys()) language = accept.select_language(available_languages) if language is None: language = 'en' @@ -41,11 +41,9 @@ def get_format(source, accept): return source[language] - # # Date and Time # - def format_date(x, accept=None): format = get_format(date_formats, accept)[0] return x.strftime(format) @@ -68,8 +66,8 @@ def format_datetime(x, accept=None): # http://docs.python.org/library/decimal.html#recipes # Modified for unicode and trailing currency -def moneyfmt(value, places=2, curr=u'', sep=u',', dp=u'.', pos=u'', - neg=u'-', trailneg=u''): +def moneyfmt(value, places=2, curr='', sep=',', dp='.', pos='', + neg='-', trailneg=''): """Convert Decimal to a money formatted unicode. places: required number of places after the decimal point @@ -97,17 +95,17 @@ def moneyfmt(value, places=2, curr=u'', sep=u',', dp=u'.', pos=u'', q = Decimal(10) ** -places # 2 places --> '0.01' sign, digits, exp = value.quantize(q).as_tuple() result = [] - digits = map(unicode, digits) + digits = list(map(str, digits)) build, next = result.append, digits.pop if curr: build(curr) if sign: build(trailneg) for i in range(places): - build(next() if digits else u'0') + build(next() if digits else '0') build(dp) if not digits: - build(u'0') + build('0') i = 0 while digits: build(next()) @@ -116,10 +114,10 @@ def moneyfmt(value, places=2, curr=u'', sep=u',', dp=u'.', pos=u'', i = 0 build(sep) build(neg if sign else pos) - return u''.join(reversed(result)) + return ''.join(reversed(result)) -def format_number(x, places=2, curr='', pos=u'', neg=u'-', trailneg=u"", +def format_number(x, places=2, curr='', pos='', neg='-', trailneg="", accept=None): """Convert Decimal to a number formatted unicode. @@ -136,7 +134,6 @@ def format_number(x, places=2, curr='', pos=u'', neg=u'-', trailneg=u"", trailneg=trailneg, **format) - ########################################################################### # Initialize the module ########################################################################### @@ -151,7 +148,7 @@ def format_number(x, places=2, curr='', pos=u'', neg=u'-', trailneg=u"", number_formats = { # See "moneyfmt" docstring for help - 'en': {'sep': u',', 'dp': u'.'}, - 'es': {'sep': u'.', 'dp': u','}, - 'fr': {'sep': u' ', 'dp': u','}, + 'en': {'sep': ',', 'dp': '.'}, + 'es': {'sep': '.', 'dp': ','}, + 'fr': {'sep': ' ', 'dp': ','}, } diff --git a/itools/i18n/oracle.py b/itools/i18n/oracle.py old mode 100644 new mode 100755 index 65fb58b9b..b9d28698b --- a/itools/i18n/oracle.py +++ b/itools/i18n/oracle.py @@ -144,7 +144,7 @@ def guess_language(text): n_words = 0 # Look for special chars and words in the given text - word = u'' + word = '' for c in text: n_chars += 1 c = c.lower() @@ -165,7 +165,7 @@ def guess_language(text): for language in negative_words.get(word, []): words.setdefault(language, 0) words[language] -= 2 - word = u'' + word = '' # Check limit n_words += 1 if n_words >= MAX_WORDS: @@ -226,112 +226,112 @@ def guess_language(text): ########################################################################### positive_chars = { - u'¡': ['es'], - u'¿': ['es'], - u'ä': ['de'], - u'ß': ['de'], - u'ç': ['fr'], - u'ê': ['fr'], - u'í': ['es'], - u'ñ': ['es'], - u'ö': ['de'], - u'ó': ['es'], - u'ü': ['de'], - u'ú': ['es'], + '¡': ['es'], + '¿': ['es'], + 'ä': ['de'], + 'ß': ['de'], + 'ç': ['fr'], + 'ê': ['fr'], + 'í': ['es'], + 'ñ': ['es'], + 'ö': ['de'], + 'ó': ['es'], + 'ü': ['de'], + 'ú': ['es'], # Asian languages # Japanese : based on particles (hiragana) - u'の': ['ja'], - u'は': ['ja'], - u'で': ['ja'], - u'に': ['ja'], - u'が': ['ja'], - u'へ': ['ja'], - u'を': ['ja'], - u'や': ['ja'], - u'と': ['ja'], + 'の': ['ja'], + 'は': ['ja'], + 'で': ['ja'], + 'に': ['ja'], + 'が': ['ja'], + 'へ': ['ja'], + 'を': ['ja'], + 'や': ['ja'], + 'と': ['ja'], # Japanese : punctuation - u'、': ['ja'], - u'。': ['ja'], + '、': ['ja'], + '。': ['ja'], } negative_chars = {} positive_words = { - u'à': ['fr'], - u'al': ['es'], - u'an': ['en'], - u'and': ['en'], - u'are': ['en'], - u'as': ['en'], - u'aux': ['fr'], - u'but': ['en'], - u'como': ['es'], - u'con': ['es'], - u'de': ['es', 'fr'], - u'del': ['es'], - u'des': ['fr'], - u'donc': ['fr'], - u'du': ['fr'], - u'el': ['es'], - u'elle': ['fr'], - u'elles': ['fr'], - u'es': ['es'], - u'est': ['fr'], - u'está': ['es'], - u'et': ['fr'], - u'from': ['en'], - u'hay': ['es'], - u'he': ['en', 'es'], - u'i': ['en'], - u'il': ['fr'], - u'ils': ['fr'], - u'in': ['en'], - u'is': ['en'], - u'it': ['en'], - u'je': ['fr'], - u'las': ['es'], - u'le': ['es', 'fr'], - u'lo': ['es'], - u'les': ['es', 'fr'], - u'los': ['es'], - u'mais': ['fr'], - u'no': ['en', 'es'], - u'nous': ['fr'], - u'nueva': ['es'], - u'o': ['es'], - u'of': ['en'], - u'on': ['en'], - u'or': ['en'], - u'où': ['fr'], - u'para': ['es'], - u'pero': ['es'], - u'por': ['es'], - u'que': ['es', 'fr'], - u'qué': ['es'], - u'she': ['en'], - u'su': ['es'], - u'sur': ['fr'], - u'that': ['en'], - u'the': ['en'], - u'their': ['en'], - u'this': ['en'], - u'to': ['en'], - u'tu': ['es', 'fr'], - u'un': ['es', 'fr'], - u'una': ['es'], - u'une': ['fr'], - u'vous': ['fr'], - u'when': ['en'], - u'where': ['en'], - u'y': ['es'], - u'you': ['en'], - u'your': ['en'], + 'à': ['fr'], + 'al': ['es'], + 'an': ['en'], + 'and': ['en'], + 'are': ['en'], + 'as': ['en'], + 'aux': ['fr'], + 'but': ['en'], + 'como': ['es'], + 'con': ['es'], + 'de': ['es', 'fr'], + 'del': ['es'], + 'des': ['fr'], + 'donc': ['fr'], + 'du': ['fr'], + 'el': ['es'], + 'elle': ['fr'], + 'elles': ['fr'], + 'es': ['es'], + 'est': ['fr'], + 'está': ['es'], + 'et': ['fr'], + 'from': ['en'], + 'hay': ['es'], + 'he': ['en', 'es'], + 'i': ['en'], + 'il': ['fr'], + 'ils': ['fr'], + 'in': ['en'], + 'is': ['en'], + 'it': ['en'], + 'je': ['fr'], + 'las': ['es'], + 'le': ['es', 'fr'], + 'lo': ['es'], + 'les': ['es', 'fr'], + 'los': ['es'], + 'mais': ['fr'], + 'no': ['en', 'es'], + 'nous': ['fr'], + 'nueva': ['es'], + 'o': ['es'], + 'of': ['en'], + 'on': ['en'], + 'or': ['en'], + 'où': ['fr'], + 'para': ['es'], + 'pero': ['es'], + 'por': ['es'], + 'que': ['es', 'fr'], + 'qué': ['es'], + 'she': ['en'], + 'su': ['es'], + 'sur': ['fr'], + 'that': ['en'], + 'the': ['en'], + 'their': ['en'], + 'this': ['en'], + 'to': ['en'], + 'tu': ['es', 'fr'], + 'un': ['es', 'fr'], + 'una': ['es'], + 'une': ['fr'], + 'vous': ['fr'], + 'when': ['en'], + 'where': ['en'], + 'y': ['es'], + 'you': ['en'], + 'your': ['en'], } negative_words = { - u'du': ['es'], + 'du': ['es'], } diff --git a/itools/ical/__init__.py b/itools/ical/__init__.py index 6df0b0931..3e6f730c9 100644 --- a/itools/ical/__init__.py +++ b/itools/ical/__init__.py @@ -23,8 +23,8 @@ """ # Import from itools -from icalendar import iCalendar -from datatypes import DateTime, Time +from .icalendar import iCalendar +from .datatypes import DateTime, Time __all__ = [ # DataTypes diff --git a/itools/ical/datatypes.py b/itools/ical/datatypes.py index a6fa524cb..2137a60c6 100644 --- a/itools/ical/datatypes.py +++ b/itools/ical/datatypes.py @@ -39,7 +39,6 @@ def encode(value, seconds=False): return value.strftime('%H:%M:%S') - class DateTime(DataType): """iCalendar Date format: A special case of itools.datatypes.DateTime, focused on RFC5545 needs @@ -74,7 +73,6 @@ def decode(value): return datetime(year, month, day, hour, min, sec, tzinfo=tzinfo) - @staticmethod def encode(value, type=None): if value is None: @@ -88,7 +86,6 @@ def encode(value, type=None): return value.strftime(fmt) - # data types for each property # --> TO VERIFY AND COMPLETE record_properties = freeze({ @@ -151,7 +148,6 @@ def encode(value, type=None): }) - record_parameters = freeze({ 'ALTREP': URI(multiple=False), # TODO Finish the list diff --git a/itools/ical/icalendar.py b/itools/ical/icalendar.py old mode 100644 new mode 100755 index 471f897c9..e52358665 --- a/itools/ical/icalendar.py +++ b/itools/ical/icalendar.py @@ -29,7 +29,7 @@ from itools.csv import property_to_str from itools.datatypes import String, Unicode from itools.handlers import guess_encoding, TextFile -from datatypes import DateTime, record_properties, record_parameters, Time +from .datatypes import DateTime, record_properties, record_parameters, Time class Component(object): @@ -58,15 +58,12 @@ def __init__(self, c_type, uid): self.uid = uid self.versions = {} - ####################################################################### # API / Private ####################################################################### def get_sequences(self): sequences = self.versions.keys() - sequences.sort() - return sequences - + return sorted(list(sequences)) def add_version(self, properties): # Sequence in properties only if just loading file @@ -76,7 +73,7 @@ def add_version(self, properties): else: sequences = self.get_sequences() if sequences: - sequence = sequences[-1] + 1 + sequence = list(sequences)[-1] + 1 else: sequence = 0 @@ -93,7 +90,7 @@ def get_version(self, sequence=None): """Return the last version of current component or the sequence's one. """ if sequence is None: - sequence = self.get_sequences()[-1] + sequence = list(self.get_sequences())[-1] return self.versions[sequence] @@ -114,7 +111,6 @@ def get_property(self, name=None): get_property_values = get_property - # TODO Move this: with Components that are not VEVENT, it will fail def get_ns_event(self, day, resource_name=None, conflicts_list=freeze([]), timetable=None, grid=False, starts_on=True, ends_on=True, @@ -206,7 +202,6 @@ def get_ns_event(self, day, resource_name=None, conflicts_list=freeze([]), return ns - class iCalendar(TextFile): """icalendar structure : @@ -226,7 +221,6 @@ class iCalendar(TextFile): class_mimetypes = ['text/calendar'] class_extension = 'ics' - record_properties = record_properties record_parameters = record_parameters @@ -238,13 +232,11 @@ def get_record_datatype(cls, name): # Default return String(multiple=True) - def generate_uid(self, c_type='UNKNOWN'): """Generate a uid based on c_type and current datetime. """ return ' '.join([c_type, datetime.now().isoformat()]) - def encode_property(self, name, property_values, encoding='utf-8'): if type(property_values) is not list: property_values = [property_values] @@ -252,8 +244,7 @@ def encode_property(self, name, property_values, encoding='utf-8'): datatype = self.get_record_datatype(name) return [ property_to_str(name, x, datatype, {}, encoding) - for x in property_values ] - + for x in property_values] ######################################################################### # New @@ -263,18 +254,16 @@ def reset(self): self.timezones = {} self.components = {} - def new(self): properties = ( - ('VERSION', u'2.0'), - ('PRODID', u'-//hforge.org/NONSGML ikaaro icalendar V1.0//EN')) + ('VERSION', '2.0'), + ('PRODID', '-//hforge.org/NONSGML ikaaro icalendar V1.0//EN')) for name, value in properties: self.properties[name] = Property(value) # The encoding self.encoding = 'UTF-8' - ######################################################################### # Load State ######################################################################### @@ -303,7 +292,7 @@ def _load_state_from_file(self, file): first = lines[0] if (first[0] != 'BEGIN' or first[1].value != 'VCALENDAR' or first[1].parameters): - raise ValueError, 'icalendar must begin with BEGIN:VCALENDAR' + raise ValueError('icalendar must begin with BEGIN:VCALENDAR') lines = lines[1:] @@ -317,10 +306,10 @@ def _load_state_from_file(self, file): break elif name == 'VERSION': if 'VERSION' in self.properties: - raise ValueError, 'VERSION can appear only one time' + raise ValueError('VERSION can appear only one time') elif name == 'PRODID': if 'PRODID' in self.properties: - raise ValueError, 'PRODID can appear only one time' + raise ValueError('PRODID can appear only one time') # Add the property self.properties[name] = value n_line += 1 @@ -328,7 +317,7 @@ def _load_state_from_file(self, file): # The properties VERSION and PRODID are mandatory if ('VERSION' not in self.properties or 'PRODID' not in self.properties): - raise ValueError, 'PRODID or VERSION parameter missing' + raise ValueError('PRODID or VERSION parameter missing') lines = lines[n_line:] @@ -340,8 +329,8 @@ def _load_state_from_file(self, file): for prop_name, prop_value in lines[:-1]: if prop_name in ('PRODID', 'VERSION'): - raise ValueError, 'PRODID and VERSION must appear before '\ - 'any component' + raise ValueError('PRODID and VERSION must appear before ' \ + 'any component') if prop_name == 'BEGIN': if c_type is None: c_type = prop_value.value @@ -355,7 +344,7 @@ def _load_state_from_file(self, file): value = prop_value.value if value == c_type: if uid is None: - raise ValueError, 'UID is not present' + raise ValueError('UID is not present') if c_type == 'VTIMEZONE': timezone = VTimezone(uid, c_inner_components) @@ -377,8 +366,8 @@ def _load_state_from_file(self, file): c_inner_components.append(inner_component) c_inner_type = None else: - raise ValueError, 'Component %s found, %s expected' \ - % (value, c_inner_type) + raise ValueError('Component %s found, %s expected' \ + % (value, c_inner_type)) else: datatype = self.get_record_datatype(prop_name) if c_inner_type is None: @@ -393,7 +382,7 @@ def _load_state_from_file(self, file): if prop_name in c_properties: msg = ('the property %s can be assigned only ' 'one value' % prop_name) - raise ValueError, msg + raise ValueError(msg) # Set the property c_properties[prop_name] = prop_value else: @@ -406,12 +395,11 @@ def _load_state_from_file(self, file): if prop_name in c_inner_properties: msg = ('the property %s can be assigned only one' ' value' % prop_name) - raise ValueError, msg + raise ValueError(msg) value = prop_value # Set the property c_inner_properties[prop_name] = value - ######################################################################### # Save State ######################################################################### @@ -419,7 +407,7 @@ def to_str(self, encoding='UTF-8'): lines = ['BEGIN:VCALENDAR\n'] # 1. Calendar properties - for key, value in self.properties.iteritems(): + for key, value in self.properties.items(): line = self.encode_property(key, value, encoding) lines.extend(line) @@ -430,7 +418,7 @@ def to_str(self, encoding='UTF-8'): lines.append('BEGIN:VTIMEZONE\n') # Properties lines.append('TZID:%s\n' % tzid) - for key, value in timezone.content.iteritems(): + for key, value in timezone.content.items(): line = self.encode_property(key, value, encoding) lines.extend(line) # Insert inner components @@ -441,7 +429,7 @@ def to_str(self, encoding='UTF-8'): # Begin lines.append('BEGIN:%s\n' % c_inner_type) # Properties - for key, value in version.iteritems(): + for key, value in version.items(): line = self.encode_property(key, value, encoding) lines.extend(line) # End @@ -460,7 +448,7 @@ def to_str(self, encoding='UTF-8'): # Properties lines.append('UID:%s\n' % uid) lines.append('SEQUENCE:%s\n' % sequence) - for key, value in version.iteritems(): + for key, value in version.items(): line = self.encode_property(key, value, encoding) lines.extend(line) # End @@ -470,7 +458,6 @@ def to_str(self, encoding='UTF-8'): lines.append('END:VCALENDAR\n') return ''.join(lines) - def to_text(self): text = [] for uid in self.components: @@ -480,7 +467,6 @@ def to_text(self): text.append(version[key].value) return ' '.join(text) - ####################################################################### # API ####################################################################### @@ -494,14 +480,13 @@ def check_properties(self, properties): if datatype.multiple is False: if isinstance(value, list): msg = 'property "%s" requires only one value' % name - raise TypeError, msg + raise TypeError(msg) else: if not isinstance(value, list): properties[name] = [value] return properties - def add_component(self, c_type, **kw): """Add a new component of type c_type. It generates a uid and a new version with the given properties if any. @@ -523,7 +508,6 @@ def add_component(self, c_type, **kw): return uid - def update_component(self, uid, **kw): """Update component with given uid with properties given as kw, creating a new version based on the previous one. @@ -551,7 +535,6 @@ def update_component(self, uid, **kw): # Unindex the component, add new version, index again component.add_version(version) - def remove(self, uid): """Definitely remove from the calendar an existant component with all its versions. @@ -559,7 +542,6 @@ def remove(self, uid): self.set_changed() del self.components[uid] - def get_property_values(self, name=None): """Return Property[] for the given icalendar property name or Return icalendar property values as a dict @@ -571,7 +553,6 @@ def get_property_values(self, name=None): return self.properties.get(name, None) return self.properties - def set_property(self, name, values): """Set values to the given calendar property, removing previous ones. @@ -592,30 +573,26 @@ def set_property(self, name, values): self.set_changed() self.properties[name] = values - # Used to factorize code of cms ical between Calendar & CalendarTable def get_record(self, uid): return self.components.get(uid) - def get_component_by_uid(self, uid): """Return components with the given uid, None if it doesn't appear. """ return self.components.get(uid) - def get_components(self, type=None): """Return the list of components of the given type, or all components if no type is given. """ if type is None: - return self.components.items() + self.timezones.items() + return list(self.components.items()) + list(self.timezones.items()) if type == 'VTIMEZONE': - return [component for tzid, component in self.timezones.iteritems()] - return [ component for uid, component in self.components.iteritems() - if component.c_type == type ] - + return [component for tzid, component in self.timezones.items()] + return [component for uid, component in self.components.items() + if component.c_type == type] class TZProp(object): @@ -642,7 +619,7 @@ def __init__(self, type, properties): self.offset - offset_from) # Compute recurrency self.rec_dic = {} - if self.properties.has_key('RRULE'): + if 'RRULE' in self.properties: rrules = self.properties['RRULE'] # FIXME RRULE can be multiple ! rrule = rrules[0] @@ -650,11 +627,9 @@ def __init__(self, type, properties): name, value = prop.split('=') self.rec_dic[name] = value - def get_offset(self): return self.offset - def get_date(self, dt): iso_weekdays = {'MO': 1, 'TU': 2, 'WE': 3, 'TH': 4, 'FR': 5, 'SA': 6, 'SU': 7} @@ -665,11 +640,11 @@ def get_date(self, dt): delta = 0 # Compute period for this year rec_dic = self.rec_dic - freq = rec_dic['FREQ'] if rec_dic.has_key('FREQ') else None + freq = rec_dic['FREQ'] if 'FREQ' in rec_dic else None if freq == 'YEARLY': - if self.rec_dic.has_key('BYMONTH'): + if 'BYMONTH' in self.rec_dic: month = int(self.rec_dic['BYMONTH']) - if self.rec_dic.has_key('BYDAY'): + if 'BYDAY' in self.rec_dic: byday = self.rec_dic['BYDAY'] if byday[0] == '-': sign = -1 @@ -690,7 +665,7 @@ def get_date(self, dt): else: delta = (7 + byday - weekday) % 7 + weeks_delta return datetime.combine(day, time) + timedelta(delta) - elif self.properties.has_key('RDATE'): + elif 'RDATE' in self.properties: dates = [self.properties['DTSTART'].value] for rdate in self.properties['RDATE']: dates.append(DateTime.decode(rdate.value)) @@ -703,27 +678,14 @@ def get_date(self, dt): else: raise NotImplementedError('We only implement FREQ in (YEARLY, )') - def get_begin(self): return self.properties['DTSTART'].value - def get_names(self): for name in self.properties['TZNAME']: yield (name.value, name.parameters) - def __cmp__(self, other): - self_b = self.get_begin() - other_b = other.get_begin() - if self_b > other_b: - return 1 - if self_b < other_b: - return -1 - return 0 - - - class VTimezone(tzinfo): """This class represent a Timezone with TZProps builded from an ICS file""" @@ -734,8 +696,7 @@ def __init__(self, tzid, tz_props): if len(tz_props) < 1: raise ValueError('A VTIMEZONE MUST contain at least one TZPROP') self.tz_props = tz_props - self.tz_props.sort() - + self.tz_props = sorted(tz_props, key=lambda x: x.get_begin()) def get_tz_prop(self, dt): props = self.tz_props @@ -777,19 +738,16 @@ def get_tz_prop(self, dt): else: return std - def tzname(self, dt): tz_prop = self.get_tz_prop(dt) # FIXME TZNAME property is multiple and can have language property - value, parameters = tz_prop.get_names().next() + value, parameters = next(tz_prop.get_names()) return str(value) - def utcoffset(self, dt): tz_prop = self.get_tz_prop(dt) return tz_prop.offset - def dst(self, dt): if dt is None or dt.tzinfo is None: return timedelta(0) diff --git a/itools/log/__init__.py b/itools/log/__init__.py deleted file mode 100644 index 848db04a2..000000000 --- a/itools/log/__init__.py +++ /dev/null @@ -1,40 +0,0 @@ -# -*- coding: UTF-8 -*- -# Copyright (C) 2009-2010 J. David Ibáñez -# Copyright (C) 2010 David Versmisse -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -# Import from itools -from log import Logger, register_logger -from log import FATAL, ERROR, WARNING, INFO, DEBUG -from log import log_fatal, log_error, log_warning, log_info, log_debug - - -__all__ = [ - 'Logger', - 'register_logger', - # Log levels - 'FATAL', - 'ERROR', - 'WARNING', - 'INFO', - 'DEBUG', - # Log functions - 'log_fatal', - 'log_error', - 'log_warning', - 'log_info', - 'log_debug', - ] - diff --git a/itools/log/log.py b/itools/log/log.py deleted file mode 100644 index 0831dcb05..000000000 --- a/itools/log/log.py +++ /dev/null @@ -1,264 +0,0 @@ -# -*- coding: UTF-8 -*- -# Copyright (C) 2009-2012 J. David Ibáñez -# Copyright (C) 2010 David Versmisse -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -"""This package provides a simple programming interface for logging (in -contrast of the ridiculously complex 'logging' package of the Standard -Library). - -It is inspired by the logging facilities of the Glib library, and will -eventually become just a wrapper of it (once pygobject exposes that part -of the Glib API). -""" - -# Import from the Standard Library -from datetime import datetime, timedelta -from glob import glob -from gzip import open as gz_open -from os import getpid, remove -from os.path import exists -from socket import gethostname -from shutil import move -from sys import exc_info, exit, stdout, stderr -from time import strftime -from traceback import format_exception - -# Import from itools -from itools.loop import cron - - - -# Log levels -FATAL = (1 << 4) -ERROR = (1 << 3) -WARNING = (1 << 2) -INFO = (1 << 1) -DEBUG = (1 << 0) - -# Number of files for the logrotate -LOG_FILES_NUMBER = 4 - - -########################################################################### -# Log functions -########################################################################### -def log(domain, level, message): - logger = registry.get(domain) - if logger is None: - logger = registry[None] - - logger.log(domain, level, message) - - - -def log_fatal(message, domain=None): - log(domain, FATAL, message) - - -def log_error(message, domain=None): - log(domain, ERROR, message) - - -def log_warning(message, domain=None): - log(domain, WARNING, message) - - -def log_info(message, domain=None): - log(domain, INFO, message) - - -def log_debug(message, domain=None): - log(domain, DEBUG, message) - - -########################################################################### -# Loggers -########################################################################### -registry = {} - - -def register_logger(logger, *args): - for domain in args: - registry[domain] = logger - - -class Logger(object): - - def __init__(self, log_file=None, min_level=INFO, rotate=None): - self.log_file = log_file - self.min_level = min_level - self.rotate_interval = rotate - if rotate: - self.launch_rotate() - - - def format_header(self, domain, level, message): - # []: - date = strftime('%Y-%m-%d %H:%M:%S') - host = gethostname() - domain = domain or '' - pid = getpid() - header = '{0} {1} {2}[{3}]: {4}\n' - return header.format(date, host, domain, pid, message) - - - def get_body(self): - type, value, traceback = exc_info() - if type is None: - return [] - - return format_exception(type, value, traceback) - - - def format_body(self): - body = self.get_body() - if not body: - return '' - body = ''.join([ ' %s' % x for x in body ]) - body += '\n' - return body - - - def format(self, domain, level, message): - header = self.format_header(domain, level, message) - body = self.format_body() - return header + body - - - def log(self, domain, level, message): - if level < self.min_level: - return - - message = self.format(domain, level, message) - - # Case 1: log file - if self.log_file: - self.write(message) - - # Case 2: standard output & error - elif level & (FATAL | ERROR | WARNING): - stderr.write(message) - else: - stdout.write(message) - - # Exit on fatal errors - if level & FATAL: - exit() - - - def write(self, message): - with open(self.log_file, 'a') as log_file: - log_file.write(message) - - - def clear(self): - """Empty log file""" - with open(self.log_file, 'w', 0) as log_file: - log_file.close() - - - def get_lines(self): - try: - with open(self.log_file, 'r') as log_file: - return log_file.read().splitlines() - except IOError: - return [] - - - def launch_rotate(self): - log_file = self.log_file - - # We save in a file ? - if log_file is None: - return - - # Find the more recent date - dates = [] - n2 = '[0-9][0-9]' - date_pattern = n2 + n2 + '-' + n2 + '-' + n2 + '_' + n2 + n2 - for name in glob(log_file + '.' + date_pattern + '.gz'): - try: - date = datetime.strptime(name[-18:-3], '%Y-%m-%d_%H%M') - except ValueError: - continue - dates.append(date) - if dates: - dates.sort() - last = dates[-1] - else: - # If here, there is no rotated files => so, we create one - self.rotate() - last = datetime.now() - - # Compute the next call - next_call = last + self.rotate_interval - datetime.now() - if next_call <= timedelta(0): - next_call = timedelta(seconds=1) - - # Call cron - cron(self.rotate, next_call) - - - def rotate(self): - log_file = self.log_file - - # We save in a file ? - if log_file is None: - return False - - # Save the current log - # XXX In a multithreads context, we must add a lock here - new_name = log_file + '.' + strftime('%Y-%m-%d_%H%M') - # We don't delete an existing file - if exists(new_name + '.gz'): - # If here, interval < 1min - return self.rotate_interval - # Yet a log file ? - if exists(log_file): - # Yes, we move it - move(log_file, new_name) - else: - # No, we create an empty one - open(new_name, 'w').close() - - # Compress it - f_in = open(new_name, 'rb') - f_out = gz_open(new_name + '.gz', 'wb') - f_out.writelines(f_in) - f_out.close() - f_in.close() - remove(new_name) - - # Suppress the old files - files = [] - n2 = '[0-9][0-9]' - date_pattern = n2 + n2 + '-' + n2 + '-' + n2 + '_' + n2 + n2 - for name in glob(log_file + '.' + date_pattern + '.gz'): - try: - date = datetime.strptime(name[-18:-3], '%Y-%m-%d_%H%M') - except ValueError: - continue - files.append( (date, name) ) - files.sort(reverse=True) - for a_file in files[LOG_FILES_NUMBER:]: - remove(a_file[1]) - - # We return always True to be "cron" compliant - return self.rotate_interval - - -# Register -register_logger(Logger(), None) diff --git a/itools/loop/__init__.py b/itools/loop/__init__.py index c801002fc..c76a859be 100644 --- a/itools/loop/__init__.py +++ b/itools/loop/__init__.py @@ -16,7 +16,7 @@ # along with this program. If not, see . # Import from itools -from loop import cron +from .loop import cron __all__ = [ diff --git a/itools/loop/loop.py b/itools/loop/loop.py index af165342d..b96d2110d 100644 --- a/itools/loop/loop.py +++ b/itools/loop/loop.py @@ -37,7 +37,6 @@ def total_seconds(td): return int(td.total_seconds()) - def _cron(callback, interval): while True: interval = total_seconds(interval) @@ -47,7 +46,6 @@ def _cron(callback, interval): break - def cron(callback, interval): """Add new cronjob. callback -- the callable to run. diff --git a/itools/odf/__init__.py b/itools/odf/__init__.py index 2e64a2512..64456661a 100644 --- a/itools/odf/__init__.py +++ b/itools/odf/__init__.py @@ -17,9 +17,9 @@ # along with this program. If not, see . # Import from itools -from odf import ODFFile, ODTFile, ODPFile, ODSFile -from oo import SXWFile, SXCFile, SXIFile -import schema +from .odf import ODFFile, ODTFile, ODPFile, ODSFile +from .oo import SXWFile, SXCFile, SXIFile +from . import schema __all__ = [ diff --git a/itools/odf/odf.py b/itools/odf/odf.py index a7f47906d..bcc330d31 100644 --- a/itools/odf/odf.py +++ b/itools/odf/odf.py @@ -21,7 +21,7 @@ # along with this program. If not, see . # Import from the Standard Library -from cStringIO import StringIO +from io import StringIO, BytesIO from os.path import splitext from random import choice from zipfile import ZipFile @@ -42,36 +42,35 @@ PILImage = None - def zip_data(source, modified_files): - file = StringIO() - outzip = ZipFile(file, 'w') - zip = ZipFile(StringIO(source)) - for info in zip.infolist(): - # Replace the data from the map - if info.filename in modified_files: - data = modified_files[info.filename] - if data is None: - continue - else: - data = zip.read(info.filename) - - # Section 17.4 says the mimetype file shall not include an extra - # field. So we remove it even if present in the source. - if info.filename == 'mimetype': - info.extra = '' - - # Ok - outzip.writestr(info, data) + assert isinstance(source, bytes) + inzip = ZipFile(BytesIO(source)) + + file = BytesIO() + with ZipFile(file, 'w') as outzip: + for info in inzip.infolist(): + # Replace the data from the map + if info.filename in modified_files: + data = modified_files[info.filename] + if data is None: + continue + else: + data = inzip.read(info.filename) + + # Section 17.4 says the mimetype file shall not include an extra + # field. So we remove it even if present in the source. + if info.filename == 'mimetype': + info.extra = b'' + + # Ok + outzip.writestr(info, data) # Ok - outzip.close() content = file.getvalue() file.close() return content - def stl_to_odt(model_odt, namespace): # STL events = list(model_odt.get_events('content.xml')) @@ -81,7 +80,6 @@ def stl_to_odt(model_odt, namespace): return zip_data(model_odt.data, modified_files) - class GreekCatalog(object): """A stupid translator. """ @@ -112,13 +110,12 @@ def f(c): new_unit = [] for x, s in unit: - if type(s) in (str, unicode): - s = ''.join([ f(c) for c in s ]) + if type(s) is str: + s = ''.join([f(c) for c in s]) new_unit.append((x, s)) return new_unit - class OOFile(ZIPFile): """SuperClass of OpenDocumentFormat 1.0 & 2.0 """ @@ -128,7 +125,6 @@ def to_text(self): return xml_to_text(content) - class ODFFile(OOFile): """Format ODF : OpenDocumentFormat 2.0 """ @@ -158,16 +154,17 @@ def get_meta(self): meta[previous_tag_name] = value return meta - def get_events(self, filename): content = self.get_file(filename) + if isinstance(content, bytes): + content = content.decode("utf-8") + for event in XMLParser(content): if event == XML_DECL: pass else: yield event - def get_units(self, srx_handler=None): for filename in ['content.xml', 'meta.xml', 'styles.xml']: events = self.get_events(filename) @@ -189,14 +186,13 @@ def translate(self, catalog, srx_handler=None): # Zip return zip_data(self.data, modified_files) - def greek(self): """Anonymize the ODF file. """ # Verify PIL is installed if PILImage is None: err = 'The greeking feature requires the Python Imaging Library' - raise ImportError, err + raise ImportError(err) folder = lfs.open(get_abspath('.')) err = 'Unexpected "%s" file will be omitted from the greeked document' @@ -226,7 +222,7 @@ def greek(self): else: # Unexpected (TODO use the logging system) modified_files[filename] = None - print err % filename + print(err % filename) # SVM files (usually they are in the Pictures folder) elif extension == '.svm': @@ -257,10 +253,9 @@ def greek(self): # Unexpected (TODO use the logging system) else: modified_files[filename] = None - print err % filename - - return zip_data(self.data, modified_files) + print(err % filename) + return zip_data(self.data, modified_files) class ODTFile(ODFFile): @@ -270,7 +265,6 @@ class ODTFile(ODFFile): namespace = 'urn:oasis:names:tc:opendocument:xmlns:text:1.0' - class ODSFile(ODFFile): class_mimetypes = ['application/vnd.oasis.opendocument.spreadsheet'] @@ -278,7 +272,6 @@ class ODSFile(ODFFile): namespace = 'urn:oasis:names:tc:opendocument:xmlns:spreadsheet:1.0' - class ODPFile(ODFFile): class_mimetypes = ['application/vnd.oasis.opendocument.presentation'] @@ -286,7 +279,6 @@ class ODPFile(ODFFile): namespace = 'urn:oasis:names:tc:opendocument:xmlns:presentation:1.0' - # Register handler and mimetypes for handler in [ODTFile, ODSFile, ODPFile]: add_type(handler.class_mimetypes[0], '.%s' % handler.class_extension) diff --git a/itools/odf/oo.py b/itools/odf/oo.py index 9b7210a98..a67020ad4 100644 --- a/itools/odf/oo.py +++ b/itools/odf/oo.py @@ -23,7 +23,7 @@ # Import from itools from itools.core import add_type from itools.handlers import register_handler_class -from odf import OOFile +from .odf import OOFile class SXWFile(OOFile): @@ -32,14 +32,12 @@ class SXWFile(OOFile): class_extension = 'sxw' - class SXCFile(OOFile): class_mimetypes = ['application/vnd.sun.xml.calc'] class_extension = 'sxc' - class SXIFile(OOFile): class_mimetypes = ['application/vnd.sun.xml.impress'] diff --git a/itools/odf/schema.py b/itools/odf/schema.py old mode 100644 new mode 100755 diff --git a/itools/office/__init__.py b/itools/office/__init__.py index 3fb43bebf..3ce413813 100644 --- a/itools/office/__init__.py +++ b/itools/office/__init__.py @@ -16,10 +16,10 @@ # along with this program. If not, see . # Import from itools -from office import MSWord, MSExcel -from ooxml import MSWordX, MSExcelX, MSPowerPointX -from ppt import MSPowerPoint -from rtf import RTF +from .office import MSWord, MSExcel +from .ooxml import MSWordX, MSExcelX, MSPowerPointX +from .ppt import MSPowerPoint +from .rtf import RTF diff --git a/itools/office/office.py b/itools/office/office.py old mode 100644 new mode 100755 index 6b45adc10..a863a91da --- a/itools/office/office.py +++ b/itools/office/office.py @@ -24,22 +24,20 @@ # Import from itools from itools.handlers import File, register_handler_class -from rtf import rtf_to_text +from .rtf import rtf_to_text try: from doctotext import doc_to_text, DocRtfException except ImportError: doc_to_text = None - class MSWord(File): class_mimetypes = ['application/msword'] class_extension = 'doc' - def to_text(self): if doc_to_text is None: - return u"" + return "" data = self.to_str() try: return doc_to_text(data) @@ -47,15 +45,13 @@ def to_text(self): return rtf_to_text(data) - class MSExcel(File): class_mimetypes = ['application/vnd.ms-excel'] class_extension = 'xls' - def to_text(self): if open_workbook is None: - return u"" + return "" data = self.to_str() @@ -68,15 +64,13 @@ def to_text(self): for sheet in book.sheets(): for idx in range(sheet.nrows): for value in sheet.row_values(idx): - if type(value) is not unicode: + if type(value) is not str: try: - value = unicode(value) + value = str(value) except UnicodeError: continue text.append(value) - return u' '.join(text) - - + return ' '.join(text) # Register diff --git a/itools/office/ole.py b/itools/office/ole.py old mode 100644 new mode 100755 index 9b1d65629..fb9aee7e5 --- a/itools/office/ole.py +++ b/itools/office/ole.py @@ -24,11 +24,11 @@ SEEK_CUR = 1 SEEK_END = 2 -BBD_BLOCK_SIZE = 512 -SBD_BLOCK_SIZE = 64 +BBD_BLOCK_SIZE = 512 +SBD_BLOCK_SIZE = 64 PROP_BLOCK_SIZE = 128 -OLENAMELENGHT = 32 -MSAT_ORIG_SIZE = 436 +OLENAMELENGHT = 32 +MSAT_ORIG_SIZE = 436 TYPE_OLE_DIR = 1 TYPE_OLE_STREAM = 2 @@ -58,12 +58,12 @@ def getulong(buffer, offset): def convert_char(uc): if uc == 0x001C: - return u"\t" + return "\t" elif uc == 0x001E: - return u"\n" + return "\n" elif uc == 0x00AD: - return u"" - return unichr(uc) + return "" + return chr(uc) def getOleType(oleBuf): @@ -75,14 +75,12 @@ def rightOleType(oleBuf): TYPE_OLE_ROOTDIR, TYPE_OLE_UNKNOWN) - class OleEntry(object): __slots__ = ['ole', 'file', 'name', 'startBlock', 'curBlock', 'length', 'ole_offset', 'file_offset', 'dirPos', 'type', 'numOfBlocks', 'blocks', 'isBigBlock'] - def __init__(self, ole): self.ole = ole @@ -97,7 +95,6 @@ def open(self): self.file_offset = self.file.tell() return 0 - def calcFileBlockOffset(self, blk): block = self.blocks[blk] ole = self.ole @@ -114,7 +111,6 @@ def calcFileBlockOffset(self, blk): + sbdSecMod * shortSectorSize) return res - def read(self, size): """Reads block from open ole stream interface-compatible with read @@ -178,7 +174,6 @@ def read(self, size): def is_eof(self): return self.ole_offset >= self.length - def seek(self, offset, whence): ole = self.ole new_ole_offset = 0 @@ -189,7 +184,7 @@ def seek(self, offset, whence): elif whence == SEEK_END: new_ole_offset = self.length + offset else: - raise ValueError, "Invalid whence!" + raise ValueError("Invalid whence!") if new_ole_offset < 0: new_ole_offset = 0 if new_ole_offset > self.length: @@ -234,9 +229,9 @@ def __init__(self, file): file.seek(0, SEEK_SET) oleBuf = file.read(BBD_BLOCK_SIZE) if len(oleBuf) != BBD_BLOCK_SIZE: - raise ValueError, "bad BBD_BLOCK_SIZE" + raise ValueError("bad BBD_BLOCK_SIZE") if not oleBuf.startswith(ole_sign): - raise ValueError, "not a PPT file" + raise ValueError("not a PPT file") self.file = file self.sectorSize = sectorSize = 1 << getshort(oleBuf, 0x1e) self.shortSectorSize = shortSectorSize = 1 << getshort(oleBuf, 0x20) @@ -253,7 +248,7 @@ def __init__(self, file): file.seek(512 + mblock * sectorSize, SEEK_SET) newbuf = file.read(sectorSize) if len(newbuf) != sectorSize: - raise ValueError, "Error read MSAT" + raise ValueError("Error read MSAT") tmpBuf = tmpBuf[:MSAT_ORIG_SIZE + (sectorSize - 4) * i] + newbuf i += 1 mblock = getlong(tmpBuf, MSAT_ORIG_SIZE + (sectorSize - 4) * i) @@ -261,11 +256,11 @@ def __init__(self, file): for i in range(bbdNumBlocks): bbdSector = getlong(tmpBuf, 4 * i) if bbdSector >= (fileLength / sectorSize) or bbdSector < 0: - raise ValueError, "Bad BBD entry!" + raise ValueError("Bad BBD entry!") file.seek(512 + bbdSector * sectorSize, SEEK_SET) newbuf = file.read(sectorSize) if len(newbuf) != sectorSize: - raise ValueError, "Can't read BBD!" + raise ValueError("Can't read BBD!") BBD.append(newbuf) self.BBD = ''.join(BBD) @@ -278,7 +273,7 @@ def __init__(self, file): file.seek(512 + sbdCurrent * sectorSize, SEEK_SET) newbuf = file.read(sectorSize) if len(newbuf) != sectorSize: - raise ValueError, "Can't read SBD!" + raise ValueError("Can't read SBD!") SBD.append(newbuf) sbdLen += 1 sbdCurrent = getlong(self.BBD, sbdCurrent * 4) @@ -298,7 +293,7 @@ def __init__(self, file): file.seek(512 + propCurrent * sectorSize, SEEK_SET) newbuf = file.read(sectorSize) if len(newbuf) != sectorSize: - raise ValueError, "Bad property entry!" + raise ValueError("Bad property entry!") self.properties += newbuf propLen += 1 propCurrent = getlong(self.BBD, propCurrent * 4) @@ -307,7 +302,7 @@ def __init__(self, file): self.propNumber = (propLen * sectorSize) / PROP_BLOCK_SIZE self.propCurNumber = 0 else: - raise ValueError, "Bad properties!" + raise ValueError("Bad properties!") # Find Root Entry self.rootEntry = None @@ -318,8 +313,8 @@ def __init__(self, file): self.propCurNumber = 0 file.seek(0, SEEK_SET) if self.rootEntry is None: - raise ValueError, ("Broken OLE structure. " - "Cannot find root entry in this file!") + raise ValueError(("Broken OLE structure. " + "Cannot find root entry in this file!")) def readdir(self): diff --git a/itools/office/ooxml.py b/itools/office/ooxml.py index 0120c8042..7cb4ee661 100644 --- a/itools/office/ooxml.py +++ b/itools/office/ooxml.py @@ -26,19 +26,16 @@ class MSWordX(File): class_extension = 'docx' - class MSExcelX(File): class_mimetypes = [prefix + 'spreadsheetml.sheet'] class_extension = 'xlsx' - class MSPowerPointX(File): class_mimetypes = [prefix + 'presentationml.presentation'] class_extension = 'pptx' - # Register register_handler_class(MSWordX) register_handler_class(MSExcelX) diff --git a/itools/office/ppt.py b/itools/office/ppt.py index af389b981..3c3457e03 100644 --- a/itools/office/ppt.py +++ b/itools/office/ppt.py @@ -18,11 +18,11 @@ # along with this program. If not, see . # Import from the Standard Library -from cStringIO import StringIO +from io import StringIO # Import from itools from itools.handlers import File, register_handler_class -from ole import SEEK_CUR, getshort, getulong, convert_char, Ole +from .ole import SEEK_CUR, getshort, getulong, convert_char, Ole DOCUMENT = 1000 @@ -50,8 +50,8 @@ def process_item(entry, rectype, reclen): if ord(buf) != 0x0d: yield convert_char(ord(buf)) else: - yield u"\n" - yield u"\n" + yield "\n" + yield "\n" elif rectype == TEXT_CHARS_ATOM or rectype == CSTRING: text_len = reclen / 2 for i in range(text_len): @@ -60,13 +60,12 @@ def process_item(entry, rectype, reclen): if u != 0x0d: yield convert_char(u) else: - yield u"\n" - yield u"\n" + yield "\n" + yield "\n" else: entry.seek(reclen, SEEK_CUR) - def do_ppt(entry): itemsread = 1 @@ -87,7 +86,6 @@ def do_ppt(entry): yield char - def ppt_to_text(data): buffer = [] file = StringIO(data) @@ -98,8 +96,7 @@ def ppt_to_text(data): if entry.name == 'PowerPoint Document': for text in do_ppt(entry): buffer.append(text) - return u"".join(buffer) - + return "".join(buffer) ########################################################################### @@ -110,7 +107,6 @@ class MSPowerPoint(File): class_mimetypes = ['application/vnd.ms-powerpoint'] class_extension = 'ppt' - def to_text(self): return ppt_to_text(self.to_str()) diff --git a/itools/office/rtf.py b/itools/office/rtf.py old mode 100644 new mode 100755 index a3f8c9ae2..183c434ac --- a/itools/office/rtf.py +++ b/itools/office/rtf.py @@ -19,7 +19,6 @@ from itools.handlers import File, register_handler_class - def rtf_parse(data): # 0 = default # 1 = keyword @@ -71,14 +70,13 @@ def rtf_parse(data): buffer.append(c) - def rtf_to_text(data): if not isinstance(data, str): - raise ValueError, "string data is expected" + raise ValueError("string data is expected") if not data: - raise ValueError, "data is empty" + raise ValueError("data is empty") if not data.startswith('{\\rtf'): - raise ValueError, "data is not RTF" + raise ValueError("data is not RTF") parser = rtf_parse(data) text = [] @@ -86,7 +84,7 @@ def rtf_to_text(data): # Read header for word in parser: if word in ('\\title', '\\author', '\\operator', '\\company'): - text.append(parser.next()) + text.append(next(parser)) text.append('\n') elif word in ('\\sectd', '\\pard', '\\plain'): break @@ -95,7 +93,7 @@ def rtf_to_text(data): for word in parser: if word == '\\pntxta' or word == '\\pntxtb': # Skip noise - parser.next() + next(arser) elif word[0] not in '\\{}': text.append(word) elif word == '\\par': @@ -114,12 +112,10 @@ def rtf_to_text(data): text = ''.join(text) text = text.decode('quopri_codec') - text = unicode(text, 'cp1252') + text = str(text, 'cp1252') return text - - ########################################################################### # Handler ########################################################################### @@ -128,7 +124,6 @@ class RTF(File): class_mimetypes = ['text/rtf'] class_extension = 'rtf' - def to_text(self): return rtf_to_text(self.to_str()) diff --git a/itools/pdf/__init__.py b/itools/pdf/__init__.py index cbeb46a61..eec9760ed 100644 --- a/itools/pdf/__init__.py +++ b/itools/pdf/__init__.py @@ -16,7 +16,7 @@ # along with this program. If not, see . # Import from itools -from pdf import PDFFile +from .pdf import PDFFile __all__ = ['PDFFile'] diff --git a/itools/pdf/pdf.py b/itools/pdf/pdf.py index 138a4a4a3..d174adaa2 100644 --- a/itools/pdf/pdf.py +++ b/itools/pdf/pdf.py @@ -24,16 +24,14 @@ pdf_to_text = None - class PDFFile(File): class_mimetypes = ['application/pdf'] class_extension = 'pdf' - def to_text(self): if pdf_to_text is None: - return u"" + return "" return pdf_to_text(self.to_str()) diff --git a/itools/pkg/__init__.py b/itools/pkg/__init__.py index e962a3a51..51cc922d2 100644 --- a/itools/pkg/__init__.py +++ b/itools/pkg/__init__.py @@ -18,11 +18,11 @@ # along with this program. If not, see . # Import from itools -from handlers import SetupConf -from build import get_manifest -from update_locale import update_locale -from utils import get_compile_flags -from utils import setup, get_config, OptionalExtension +from .handlers import SetupConf +from .build import get_manifest +from .update_locale import update_locale +from .utils import get_compile_flags +from .utils import setup, get_config, OptionalExtension __all__ = [ @@ -36,7 +36,7 @@ # Git: optional try: - from git import open_worktree + from .git import open_worktree __all__.append('open_worktree') except ImportError: pass diff --git a/itools/pkg/build.py b/itools/pkg/build.py index 5b3b72758..dfdd5a82c 100644 --- a/itools/pkg/build.py +++ b/itools/pkg/build.py @@ -29,15 +29,16 @@ # Import from itools from itools.fs import lfs from itools.gettext import POFile - +from itools.html import XHTMLFile, HTMLFile +from itools.xmlfile.errors import TranslationError # Import from here -from build_gulp import GulpBuilder -from git import open_worktree +from .build_gulp import GulpBuilder +from .git import open_worktree def get_manifest(): worktree = open_worktree('.') - return [ x for x in worktree.get_filenames() if not x.startswith('.')] + return [x for x in worktree.get_filenames() if not x.startswith('.')] def make(worktree, rules, manifest, package_root, po_files): @@ -63,11 +64,6 @@ def po2mo(package_root, source, target, handler_cls, po_files): # Translate templates def make_template(package_root, source, target, handler_cls, po_files): - # Import some packages so we can compile templates - from itools.xmlfile.errors import TranslationError - import itools.gettext - import itools.stl - import itools.pdf # Get file source_handler = handler_cls(source) language = target.rsplit('.', 1)[1] @@ -150,21 +146,20 @@ def build(path, config, environment): manifest.add('MANIFEST') # Write version open(path + version_txt, 'w').write(version) - print '**'*30 - print '* Version:', version + print("**"*30) + print("* Version: {}".format(version)) manifest.add(version_txt) # Write environment.json file environment_json = get_file_path(package_root, 'environment.json') environment_kw = {'build_path': path, 'environment': environment} open(path + environment_json, 'w').write(dumps(environment_kw)) manifest.add(environment_json) - print '* Build environment.json' + print("* Build environment.json") # Run gulp if environment == 'production': gulp_builder = GulpBuilder(package_root, worktree, manifest) gulp_builder.run() # Rules - from itools.html import XHTMLFile, HTMLFile rules = [('.po', '.mo', po2mo, None)] # Pre-load PO files po_files = {} @@ -183,8 +178,8 @@ def build(path, config, environment): # Make make(worktree, rules, manifest, package_root, po_files) # Write the manifest - lines = [ x + '\n' for x in sorted(manifest) ] + lines = [x + '\n' for x in sorted(manifest)] open(path + 'MANIFEST', 'w').write(''.join(lines)) - print '* Build MANIFEST file (list of files to install)' - print '**'*30 + print('* Build MANIFEST file (list of files to install)') + print('**'*30) return version diff --git a/itools/pkg/build_gulp.py b/itools/pkg/build_gulp.py index ea9457eac..37d929282 100644 --- a/itools/pkg/build_gulp.py +++ b/itools/pkg/build_gulp.py @@ -32,7 +32,6 @@ class GulpBuilder(object): That allow to avoid commit compiled JS/CSS files into GIT. """ - def __init__(self, package_root, worktree, manifest): self.package_root = package_root if self.package_root != '.': @@ -43,9 +42,7 @@ def __init__(self, package_root, worktree, manifest): self.manifest = manifest self.fs = LocalFolder('.') if self.fs.is_folder(self.ui_path): - self.dist_folders = tuple(['{0}{1}'.format(self.ui_path, x) - for x in LocalFolder(self.ui_path).get_names()]) - + self.dist_folders = tuple(['{0}{1}'.format(self.ui_path, x) for x in LocalFolder(self.ui_path).get_names()]) def run(self): npm_done = self.launch_npm_install() @@ -59,62 +56,59 @@ def run(self): relative_path.startswith(self.dist_folders) and self.fs.is_file(path)): self.manifest.add(relative_path) - def launch_npm_install(self): done = False for path in self.manifest: filename = get_uri_name(path) if filename == 'package.json': - print '***'*25 - print '*** Run $ npm install on ', path - print '***'*25 + print("***"*25) + print("*** Run $ npm install on {}".format(path)) + print("***"*25) path = str(Path(path)[:-1]) + '/' p = Popen(['npm', 'install'], cwd=path) p.wait() if p.returncode == 1: - print '***'*25 - print '*** Error running npm install ', path - print '***'*25 + print("***"*25) + print("*** Error running npm install {}".format(path)) + print("***"*25) sys.exit(1) done = True return done - def launch_gulp_build(self): done = False for path in self.manifest: filename = get_uri_name(path) if filename == 'gulpfile.js': - print '***'*25 - print '*** Run $ gulp build on ', path - print '***'*25 + print("***"*25) + print("*** Run $ gulp build on ".format(path)) + print("***"*25) path = str(Path(path)[:-1]) + '/' p = Popen(['gulp', 'build'], cwd=path) p.wait() if p.returncode == 1: - print '***'*25 - print '*** Error running gulp ', path - print '***'*25 + print("***"*25) + print("*** Error running gulp ".format(path)) + print("***"*25) sys.exit(1) done = True return done - def launch_webpack(self): done = False for path in self.manifest: filename = get_uri_name(path) if filename == 'webpack.config.js': - print '***'*25 - print '*** Run $ webpack ', path - print '***'*25 + print("***"*25) + print("*** Run $ webpack ".format(path)) + print("***"*25) path = str(Path(path)[:-1]) + '/' p = Popen(['webpack', '--mode=production'], cwd=path) p.wait() if p.returncode == 1: - print '***'*25 - print '*** Error running webpack ', path - print '***'*25 + print("***"*25) + print("*** Error running webpack ".format(path)) + print("***"*25) sys.exit(1) done = True return done diff --git a/itools/pkg/git.py b/itools/pkg/git.py index b21633e89..3c8bd3762 100644 --- a/itools/pkg/git.py +++ b/itools/pkg/git.py @@ -58,8 +58,8 @@ def _call(self, command): popen = Popen(command, stdout=PIPE, stderr=PIPE, cwd=self.path) stdoutdata, stderrdata = popen.communicate() if popen.returncode != 0: - raise EnvironmentError, (popen.returncode, stderrdata) - return stdoutdata + raise EnvironmentError((popen.returncode, stderrdata)) + return stdoutdata.decode("utf-8") def _resolve_reference(self, reference): @@ -106,7 +106,7 @@ def index(self): index = self.repo.index # Bare repository if index is None: - raise RuntimeError, 'expected standard repository, not bare' + raise RuntimeError('expected standard repository, not bare') return index @@ -125,6 +125,7 @@ def git_describe(self): return None # Parse + print(data) tag, n, commit = data.rsplit('-', 2) return tag, int(n), commit diff --git a/itools/pkg/update_locale.py b/itools/pkg/update_locale.py index f22db4bd9..0fde0aad6 100644 --- a/itools/pkg/update_locale.py +++ b/itools/pkg/update_locale.py @@ -29,21 +29,19 @@ from itools.gettext import POFile from itools.handlers import register_handler_class from itools.database.ro import ro_database -import itools.html -import itools.python -import itools.stl -import itools.pdf from itools.stl import STLFile +from itools.python import Python from itools.uri import Path from itools.fs import lfs, WRITE # Import from here -from utils import get_config +from .utils import get_config # FIXME We register STLFile to override get_units of XHTMLFile handler # (See bug #864) register_handler_class(STLFile) +register_handler_class(Python) def write(text): @@ -98,32 +96,29 @@ def update_locale(srx_handler, exclude_folders, no_wrap=False): try: handler = ro_database.get_handler(path) except Exception: - print - print '*' - print '* Error:', path - print '*' + print("*") + print("* Error: {}".format(path)) + print("*") raise try: units = handler.get_units(srx_handler=srx_handler) units = list(units) except Exception: - print - print '*' - print '* Error:', path - print '*' + print("*") + print("* Error: {}".format(path)) + print("*") raise relative_path = locale_folder_path.get_pathto(path) for source, context, line in units: po.add_unit(relative_path, source, context, line) - print write('* Update PO template ') data = po.to_str() # Write the po into the locale.pot try: - locale_pot = locale_folder.open('locale.pot', WRITE) + locale_pot = locale_folder.open('locale.pot', WRITE, text=True) except IOError: # The locale.pot file does not exist create and open locale_pot = locale_folder.make_file('locale.pot') @@ -139,7 +134,7 @@ def update_locale(srx_handler, exclude_folders, no_wrap=False): filenames = list(filenames) filenames.sort() - print '* Update PO files:' + print("* Update PO files:") locale_pot_path = locale_folder.get_absolute_path('locale.pot') for filename in filenames: if locale_folder.exists(filename): @@ -150,7 +145,6 @@ def update_locale(srx_handler, exclude_folders, no_wrap=False): else: call(['msgmerge', '-U', '-s', file_path, locale_pot_path]) else: - print ' %s (new)' % filename + print(" %s (new)" % filename) file_path = locale_folder.get_absolute_path(filename) lfs.copy(locale_pot_path, file_path) - print diff --git a/itools/pkg/utils.py b/itools/pkg/utils.py old mode 100644 new mode 100755 index eccdfe247..7f68cb308 --- a/itools/pkg/utils.py +++ b/itools/pkg/utils.py @@ -31,9 +31,8 @@ from itools.core import get_pipe # Import from itools.pkg -from build import build, get_package_version -from handlers import SetupConf - +from .build import build, get_package_version +from .handlers import SetupConf class OptionalExtension(Extension): @@ -48,7 +47,6 @@ class OptionalExtension(Extension): """ - class OptionalBuildExt(build_ext): """Internal class to support OptionalExtension. """ @@ -59,13 +57,12 @@ def build_extension(self, ext): try: build_ext.build_extension(self, ext) except LinkError: - print "" - print " '%s' module will not be available." % ext.name - print " Make sure the following libraries are installed:", - print ", ".join(ext.libraries) - print " This error is not fatal, continuing build..." - print "" - + print("") + print(" '%s' module will not be available." % ext.name) + print(" Make sure the following libraries are installed:") + print(", ".join(ext.libraries)) + print(" This error is not fatal, continuing build...") + print("") def get_compile_flags(command): @@ -96,12 +93,10 @@ def get_compile_flags(command): 'libraries': libraries} - def get_config(): return SetupConf('setup.conf') - def setup(path, ext_modules=None): ext_modules = ext_modules or [] config = get_config() @@ -119,8 +114,9 @@ def setup(path, ext_modules=None): version = get_package_version(package_root) # Initialize variables + package_name = config.get_value('package_name') - if package_name is None: + if not package_name: package_name = config.get_value('name') packages = [package_name] package_data = {package_name: []} @@ -134,8 +130,8 @@ def setup(path, ext_modules=None): subpackages = [] # Python files are included by default - filenames = [ x.strip() for x in open(path + 'MANIFEST').readlines() ] - filenames = [ x for x in filenames if not x.endswith('.py') ] + filenames = [x.strip() for x in open(path + 'MANIFEST').readlines()] + filenames = [x for x in filenames if not x.endswith('.py')] # The data files prefix = '' if package_root == '.' else package_root + '/' @@ -154,9 +150,10 @@ def setup(path, ext_modules=None): package_data[package_name].append(line) # The scripts + if config.has_value('scripts'): scripts = config.get_value('scripts') - scripts = [ join_path(*['scripts', x]) for x in scripts ] + scripts = [join_path(*['scripts', x]) for x in scripts] else: scripts = [] @@ -170,6 +167,7 @@ def setup(path, ext_modules=None): author_name = config.get_value('author_name') # Requires install_requires = [] + if exists('requirements.txt'): install_requires = parse_requirements( 'requirements.txt', session='xxx') @@ -177,26 +175,26 @@ def setup(path, ext_modules=None): # XXX Workaround buggy distutils ("sdist" don't likes unicode strings, # and "register" don't likes normal strings). if 'register' in argv: - author_name = unicode(author_name, 'utf-8') - classifiers = [ x for x in config.get_value('classifiers') if x ] - core.setup(name = package_name, - version = version, + author_name = str(author_name, 'utf-8') + classifiers = [x for x in config.get_value('classifiers') if x] + core.setup(name=package_name, + version=version, # Metadata - author = author_name, - author_email = config.get_value('author_email'), - license = config.get_value('license'), - url = config.get_value('url'), - description = config.get_value('title'), - long_description = long_description, - classifiers = classifiers, + author=author_name, + author_email=config.get_value('author_email'), + license=config.get_value('license'), + url=config.get_value('url'), + description=config.get_value('title'), + long_description=long_description, + classifiers=classifiers, # Packages - package_dir = {package_name: package_root}, - packages = packages, - package_data = package_data, + package_dir={package_name: package_root}, + packages=packages, + package_data=package_data, # Requires / Provides install_requires=install_requires, # Scripts - scripts = scripts, - cmdclass = {'build_ext': OptionalBuildExt}, + scripts=scripts, + cmdclass={'build_ext': OptionalBuildExt}, # C extensions ext_modules=ext_modules) diff --git a/itools/python/__init__.py b/itools/python/__init__.py index 99df9698a..4c20a4b05 100644 --- a/itools/python/__init__.py +++ b/itools/python/__init__.py @@ -16,7 +16,7 @@ # along with this program. If not, see . # Import from itools -from python import Python +from .python import Python __all__ = [ diff --git a/itools/python/python.py b/itools/python/python.py index e0a223762..d0e326095 100644 --- a/itools/python/python.py +++ b/itools/python/python.py @@ -28,13 +28,11 @@ from itools.srx import TEXT - class VisitorMSG(NodeVisitor): def __init__(self): self.messages = [] - def visit_Call(self, node): if isinstance(node.func, Attribute): try: @@ -49,33 +47,29 @@ def visit_Call(self, node): self.visit(e) for e in node.keywords: self.visit(e) - if node.starargs: - self.visit(node.starargs) - if node.kwargs: - self.visit(node.kwargs) # Check names if isinstance(func, Name): if func.id in ('MSG', 'INFO', 'ERROR'): text = node.args[0] if isinstance(text, Str): - if type(text.s) is unicode and text.s.strip(): + if type(text.s) is str and text.s.strip(): # Context = None msg = ((TEXT, text.s),), None, node.lineno self.messages.append(msg) - class Python(TextFile): class_mimetypes = ['text/x-python'] class_extension = 'py' - def get_units(self, srx_handler=None): data = self.to_str() # Make it work with Windows files (the parser expects '\n' ending # lines) - data = ''.join([ x + '\n' for x in data.splitlines() ]) + if type(data) is bytes: + data = data.decode("utf-8") + data = ''.join([x + '\n' for x in data.splitlines()]) # Parse and Walk ast = parse(data) visitor = VisitorMSG() diff --git a/itools/relaxng/__init__.py b/itools/relaxng/__init__.py index 694bf6da9..04a9d7e05 100644 --- a/itools/relaxng/__init__.py +++ b/itools/relaxng/__init__.py @@ -17,7 +17,7 @@ # Import from itools from itools.core import add_type -from relaxng import RelaxNGFile +from .relaxng import RelaxNGFile __all__ = [ 'RelaxNGFile', diff --git a/itools/relaxng/relaxng.py b/itools/relaxng/relaxng.py old mode 100644 new mode 100755 index 4b91af64c..a74c04525 --- a/itools/relaxng/relaxng.py +++ b/itools/relaxng/relaxng.py @@ -16,17 +16,19 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +from logging import getLogger + # Import from itools from itools.datatypes import Boolean, Date, DateTime, Decimal, Integer from itools.datatypes import QName, String, Time, URI from itools.fs import lfs from itools.handlers import TextFile, register_handler_class -from itools.log import log_warning from itools.uri import get_reference from itools.xml import XMLNamespace, ElementSchema, xml_uri, xmlns_uri from itools.xml import XMLParser, XML_DECL, START_ELEMENT, END_ELEMENT, TEXT from itools.xml import register_namespace, has_namespace +log = getLogger("itools.core") # Namespace of Relax NG rng_uri = 'http://relaxng.org/ns/structure/1.0' @@ -48,9 +50,9 @@ def read_common_attrs(tag_uri, attrs, context): context['default_ns'] = value elif name == 'datatypeLibrary': if value != 'http://www.w3.org/2001/XMLSchema-datatypes': - raise NotImplementedError, ('relax NG: we implement only ' - 'the "http://www.w3.org/2001/XMLSchema-datatypes" ' - 'for datatypes') + raise NotImplementedError(('relax NG: we implement only ' + 'the "http://www.w3.org/2001/XMLSchema-datatypes" ' + 'for datatypes')) def read_name_class(context, event, stream): @@ -65,15 +67,15 @@ def read_name_class(context, event, stream): # ... if tag_name == 'name': # Read the content - type, value, line = stream.next() + type, value, line = next(stream) if type != TEXT: - raise ValueError, 'your relax NG file is malformed' + raise ValueError('your relax NG file is malformed') name = value # Read "" - type, value, line = stream.next() + type, value, line = next(stream) if (type != END_ELEMENT or value[0] != rng_uri or value[1] != 'name'): - raise ValueError, 'your relax NG file is malformed' + raise ValueError('your relax NG file is malformed') # Make Qname if ':' in name: prefix, trash, name = name.partition(':') @@ -108,9 +110,9 @@ def read_name_class(context, event, stream): if level == 0: return None else: - raise ValueError, 'your relax NG file is malformed' + raise ValueError('your relax NG file is malformed') else: - event = stream.next() + event = next(stream) def read_qnames(attrs, context, stream): @@ -126,7 +128,7 @@ def read_qnames(attrs, context, stream): return [(uri, name)] # In the next events, ... else: - return read_name_class(context, stream.next(), stream) + return read_name_class(context, next(stream), stream) def read_file(context, uri, file): @@ -137,16 +139,19 @@ def read_file(context, uri, file): references = context['references'] # XML stream - stream = XMLParser(file.read()) - + data_file = file.read() + if isinstance(data_file, bytes): + data_file = data_file.decode("utf-8") + stream = XMLParser(data_file) # Parse ! stack = [] - for type, value, line in stream: + for xml_type, value, line in stream: + # Set the good encoding - if type == XML_DECL: + if xml_type == XML_DECL: context['encoding'] = value[1] - elif type == START_ELEMENT: + elif xml_type == START_ELEMENT: tag_uri, tag_name, attrs = value # Set your context variable @@ -187,13 +192,13 @@ def read_file(context, uri, file): # elif tag_name == 'data': - type = attrs[None, 'type'] + xml_type = attrs[None, 'type'] last = stack[-1] if last['data'] is not None: last['data'] = '' else: - last['data'] = type + last['data'] = xml_type # elif tag_name == 'ref': @@ -204,7 +209,7 @@ def read_file(context, uri, file): # elif tag_name == 'define': - name = attrs[None, 'name'] + name = attrs[None, 'name'] # New or merge ? if name in references and (None, 'combine') in attrs: @@ -247,9 +252,9 @@ def read_file(context, uri, file): # Tags not implemented else: - raise NotImplementedError, ('relax NG: <%s> not ' - 'implemented') % tag_name - elif type == END_ELEMENT: + raise NotImplementedError(('relax NG: <%s> not ' + 'implemented') % tag_name) + elif xml_type == END_ELEMENT: tag_uri, tag_name = value if (tag_uri == rng_uri and tag_name in ['element', 'attribute', 'define']): @@ -307,14 +312,15 @@ def read_file(context, uri, file): 'gYear': String, 'gMonthDay': String, 'gDay': String, - 'gMonth': String } + 'gMonth': String} + def convert_type(data): datatype = convert_type_data.get(data) if datatype is not None: return datatype else: - raise ValueError, 'relax NG: unexpected datatype "%s"' % data + raise ValueError('relax NG: unexpected datatype "%s"' % data) def split_attributes(uri, attributes): @@ -353,8 +359,8 @@ def make_namespaces(context): try: ref = references[ref] except KeyError: - raise KeyError, ('the define "%s" is missing in your ' - 'relax NG file') % ref + raise KeyError(('the define "%s" is missing in your ' + 'relax NG file') % ref) attributes.extend(ref['attributes']) new_refs.extend(ref['refs']) is_empty = is_empty and ref['is_empty'] @@ -381,8 +387,8 @@ def make_namespaces(context): try: ref = references[ref] except KeyError: - raise KeyError, ('the define "%s" is missing in your ' - 'relax NG file') % ref + raise KeyError(('the define "%s" is missing in your ' + 'relax NG file') % ref) new_refs.extend(ref['refs']) ref_data = ref['data'] @@ -411,27 +417,26 @@ def make_namespaces(context): namespace['elements'][name] = element # Free attributes - for (uri, name), datatype in free.iteritems(): + for (uri, name), datatype in free.items(): namespace = namespaces.setdefault(uri, {'elements': {}, 'free_attributes': {}}) namespace['free_attributes'][name] = datatype result = {} prefix2uri = context['prefix'] - for namespace, data in namespaces.iteritems(): + for namespace, data in namespaces.items(): # Find the prefix - for prefix, uri in prefix2uri.iteritems(): + for prefix, uri in prefix2uri.items(): if uri == namespace: result[uri] = XMLNamespace( uri=uri, prefix=prefix, - elements=data['elements'].values(), + elements=list(data['elements'].values()), free_attributes=data['free_attributes'], default_datatype=String) break else: - log_warning('relaxng: namespace "%s" not found' % namespace) - + log.warning('relaxng: namespace "%s" not found' % namespace) return result @@ -451,11 +456,11 @@ class RelaxNGFile(TextFile): def _load_state_from_file(self, file): # A new context - context = {'encoding' : 'utf-8', - 'current_ns' : None, + context = {'encoding': 'utf-8', + 'current_ns': None, 'elements': [], 'references': {}, - 'prefix' : {}} + 'prefix': {}} context['prefix']['xml'] = xml_uri # Parse the file @@ -479,7 +484,7 @@ def _load_state_from_file(self, file): # API Public ######################################################################### def auto_register(self): - for uri, namespace in self.namespaces.iteritems(): + for uri, namespace in self.namespaces.items(): if not has_namespace(uri): register_namespace(namespace) diff --git a/itools/rss/__init__.py b/itools/rss/__init__.py index 0231d2f9b..be1a3ebbd 100644 --- a/itools/rss/__init__.py +++ b/itools/rss/__init__.py @@ -17,7 +17,7 @@ # Import from itools from itools.core import add_type -from rss import RSSFile +from .rss import RSSFile __all__ = ['RSSFile'] diff --git a/itools/rss/rss.py b/itools/rss/rss.py old mode 100644 new mode 100755 index 37e9f2896..4c5c66534 --- a/itools/rss/rss.py +++ b/itools/rss/rss.py @@ -86,13 +86,11 @@ def encode_element(name, value, encoding='UTF-8'): return XMLContent.encode(result) - class RSSFile(TextFile): class_mimetypes = ['application/rss+xml'] class_extension = 'rss' - def new(self): # Channel API self.channel = { @@ -109,7 +107,6 @@ def new(self): # Item API is a list of dictionaries similar to the channel self.items = [] - def _load_state_from_file(self, file): # Default values encoding = 'UTF-8' @@ -153,7 +150,6 @@ def _load_state_from_file(self, file): # Others are ignored version, encoding, standalone = value - def to_str(self, encoding='UTF-8'): s = [] @@ -218,5 +214,4 @@ def to_str(self, encoding='UTF-8'): return '\n'.join(s) - register_handler_class(RSSFile) diff --git a/itools/srx/__init__.py b/itools/srx/__init__.py index b989a0274..7d9118afc 100644 --- a/itools/srx/__init__.py +++ b/itools/srx/__init__.py @@ -18,9 +18,9 @@ # Import from itools from itools.core import add_type -from srx import SRXFile -from segment import Message, get_segments, translate_message -from segment import TEXT, START_FORMAT, END_FORMAT +from .srx import SRXFile +from .segment import Message, get_segments, translate_message +from .segment import TEXT, START_FORMAT, END_FORMAT __all__ = [ 'SRXFile', diff --git a/itools/srx/segment.py b/itools/srx/segment.py old mode 100644 new mode 100755 index f0a1e94e8..0e7b8f200 --- a/itools/srx/segment.py +++ b/itools/srx/segment.py @@ -18,10 +18,10 @@ # Import from itools from itools.core import get_abspath -from srx import SRXFile +from .srx import SRXFile # Constants -TEXT, START_FORMAT, END_FORMAT = range(3) +TEXT, START_FORMAT, END_FORMAT = list(range(3)) SPACE = ' \t\r\n' @@ -56,7 +56,6 @@ def collapse(text): return ''.join(collapsed_text) - def _remove_spaces(left, center, right, keep_spaces): # (1) Move only "spaces" surrounding the center to left and right if center: @@ -83,7 +82,7 @@ def _remove_spaces(left, center, right, keep_spaces): text, context = value new_end = len(text) - moved_text = u'' + moved_text = '' for c in reversed(text): if isspace(c): # Move the character @@ -111,13 +110,13 @@ def _remove_spaces(left, center, right, keep_spaces): # Begin and End if i > 0 and text and isspace(text[0]): - begin = u' ' + begin = ' ' else: - begin = u'' + begin = '' if i < len(center) - 1 and isspace(text[-1]): - end = u' ' + end = ' ' else: - end = u'' + end = '' # Compute the new "line" argument for c in text: @@ -176,16 +175,19 @@ def _clean_message(message, keep_spaces): return left, center, right -default_srx_handler = get_abspath('srx/default.srx', 'itools') -default_srx_handler = SRXFile(default_srx_handler) +default_srx_handler = None def _split_message(message, srx_handler=None): # Concatenation! + global default_srx_handler + if default_srx_handler is None: + default_srx_handler = get_abspath('srx/default.srx', 'itools') + default_srx_handler = SRXFile(default_srx_handler) + concat_text = [] for type, value, line in message: if type == TEXT: concat_text.append(value[0]) - concat_text = u''.join(concat_text) - + concat_text = ''.join(concat_text) # Get the rules if srx_handler is None: srx_handler = default_srx_handler @@ -203,12 +205,14 @@ def _split_message(message, srx_handler=None): if not break_value and pos not in breaks: no_breaks.add(pos) breaks = list(breaks) - breaks.sort() + breaks = sorted(breaks) + # And now cut the message forbidden_break_level = 0 current_message = Message() current_length = 0 + for type, value, line in message: if type == TEXT: text, context = value @@ -263,8 +267,8 @@ def _translate_message(message, catalog): id2tags = {} for type, value, line in message: if type != TEXT: - id = value[1] - id2tags[type, id] = (type, value, line) + _id = value[1] + id2tags[type, _id] = (type, value, line) # Translation translation = catalog.gettext(message.to_unit(), message.get_context()) @@ -284,9 +288,7 @@ def _translate_message(message, catalog): def get_segments(message, keep_spaces=False, srx_handler=None): for sub_message in _split_message(message, srx_handler): left, center, right = _clean_message(sub_message, keep_spaces) - todo = left+right - if center != sub_message: for value, context, line in get_segments(center, keep_spaces, srx_handler): @@ -357,18 +359,15 @@ def append_text(self, text, line=1, context=None): else: list.append(self, (TEXT, (text, context), line)) - - def append_start_format(self, content, id, line=1): + def append_start_format(self, content, _id, line=1): """value=[(u'...', translatable, context), ...] """ - self.append((START_FORMAT, (content, id), line)) - + self.append((START_FORMAT, (content, _id), line)) - def append_end_format(self, content, id, line=1): + def append_end_format(self, content, _id, line=1): """value=idem as start_format """ - self.append((END_FORMAT, (content, id), line)) - + self.append((END_FORMAT, (content, _id), line)) def get_line(self): if self: @@ -376,7 +375,6 @@ def get_line(self): else: return None - def get_context(self): # Return the first context != None # or None @@ -386,7 +384,6 @@ def get_context(self): return value[1] return None - def to_str(self): result = [] for type, value, line in self: @@ -395,8 +392,7 @@ def to_str(self): else: for text, translatable, context in value[0]: result.append(text) - return u''.join(result) - + return ''.join(result) def to_unit(self): result = [] @@ -406,4 +402,3 @@ def to_unit(self): else: result.append((type, value[1])) return tuple(result) - diff --git a/itools/srx/srx.py b/itools/srx/srx.py old mode 100644 new mode 100755 index 9ce9a52fc..197fd4bb6 --- a/itools/srx/srx.py +++ b/itools/srx/srx.py @@ -23,6 +23,7 @@ from itools.handlers import TextFile, register_handler_class + class SRXFile(TextFile): """ A handler for the Segmentation Rules eXchange format (SRX) """ @@ -30,7 +31,6 @@ class SRXFile(TextFile): class_mimetypes = ['text/x-srx'] class_extension = 'srx' - def _load_state_from_file(self, file): # Default values encoding = 'utf-8' @@ -43,11 +43,14 @@ def _load_state_from_file(self, file): self.map_rules = [] srx_uri = 'http://www.lisa.org/srx20' + data_file = file.read() + if isinstance(data_file, bytes): + data_file = data_file.decode("utf-8") - for type, value, line in XMLParser(file.read()): - if type == XML_DECL: + for xml_type, value, line in XMLParser(data_file): + if xml_type == XML_DECL: encoding = value[1] - elif type == START_ELEMENT: + elif xml_type == START_ELEMENT: tag_uri, tag_name, attrs = value if tag_uri == srx_uri: # header @@ -67,9 +70,7 @@ def _load_state_from_file(self, file): self.header['formathandle_'+type_value] = include # languagerule elif tag_name == 'languagerule': - languagerulename = unicode( - attrs[None, 'languagerulename'], - encoding) + languagerulename = attrs[None, 'languagerulename'] current_language =\ self.language_rules[languagerulename] = [] # rule @@ -82,16 +83,14 @@ def _load_state_from_file(self, file): current_break = break_value.lower() != 'no' # languagemap elif tag_name == 'languagemap': - languagepattern = unicode( - attrs[None, 'languagepattern'], encoding) - languagerulename= unicode( - attrs[None, 'languagerulename'], encoding) + languagepattern = attrs[None, 'languagepattern'] + languagerulename= attrs[None, 'languagerulename'] self.map_rules.append((languagepattern, languagerulename)) - current_text = u'' - elif type == TEXT: - current_text = unicode(value, encoding) - elif type == END_ELEMENT: + current_text = '' + elif xml_type == TEXT: + current_text = value + elif xml_type == END_ELEMENT: tag_uri, tag_name = value if tag_uri == srx_uri: # beforebreak @@ -119,10 +118,8 @@ def get_compiled_rules(self, language): result.append((break_value, regexp)) return result - def get_languages(self): - return self.language_rules.keys() - + return list(self.language_rules.keys()) def get_rules(self, language): language_rules = self.language_rules diff --git a/itools/stl/__init__.py b/itools/stl/__init__.py index 18262a6cd..e52d51bd6 100644 --- a/itools/stl/__init__.py +++ b/itools/stl/__init__.py @@ -17,8 +17,8 @@ # along with this program. If not, see . # Import from itools -from stl import STLError, STLFile, STLTemplate, stl -from stl import rewrite_uris, set_prefix +from .stl import STLError, STLFile, STLTemplate, stl +from .stl import rewrite_uris, set_prefix stl_namespaces = { diff --git a/itools/stl/schema.py b/itools/stl/schema.py old mode 100644 new mode 100755 diff --git a/itools/stl/stl.py b/itools/stl/stl.py old mode 100644 new mode 100755 index e7775854d..e1916e5f4 --- a/itools/stl/stl.py +++ b/itools/stl/stl.py @@ -27,12 +27,12 @@ from functools import partial from re import compile from types import GeneratorType, MethodType +from logging import getLogger # Import from itools from itools.core import freeze, prototype from itools.datatypes import Boolean from itools.gettext import MSG -from itools.log import log_error from itools.uri import Path, Reference, get_reference from itools.xml import XMLParser, find_end, get_attr_datatype, stream_to_str from itools.xml import DOCUMENT_TYPE, START_ELEMENT, END_ELEMENT, TEXT @@ -41,14 +41,15 @@ from itools.xmlfile import XMLFile, get_units from itools.html import xhtml_uri from itools.html import stream_to_str_as_html, stream_to_str_as_xhtml -from schema import stl_uri +from .schema import stl_uri +log = getLogger("itools.stl") ######################################################################## # Exceptions ######################################################################## -class STLError(StandardError): +class STLError(Exception): pass @@ -86,18 +87,17 @@ def evaluate(expression, stack, repeat_stack): try: value = stack.lookup(path[0]) except STLError: - raise STLError, err % (expression, path[0]) + raise STLError(err % (expression, path[0])) for name in path[1:]: try: value = lookup(value, name) except STLError: - raise STLError, err % (expression, name) + raise STLError(err % (expression, name)) return value - def evaluate_if(expression, stack, repeat_stack): # stl:if="expression1 and expression2" # stl:if="expression1 or expression2" @@ -125,7 +125,6 @@ def evaluate_if(expression, stack, repeat_stack): return evaluate(expression, stack, repeat_stack) - def evaluate_repeat(expression, stack, repeat_stack): name, expression = expression.split(' ', 1) values = evaluate(expression, stack, repeat_stack) @@ -140,17 +139,17 @@ def lookup(namespace, name): # Case 1: dict if type(namespace) is dict: if name not in namespace: - err = u"name '{}' not found in the namespace" + err = "name '{}' not found in the namespace" raise STLError(err.format(name)) return namespace[name] # Case 2: instance try: value = getattr(namespace, name) - except AttributeError: - log_error('lookup failed', domain='itools.stl') - err = "name '{}' not found in the namespace" - raise STLError, err.format(name) + except AttributeError as e: + err = "Lookup failed : name '{}' not found in the namespace".format(name) + log.error(err, exc_info=True) + raise STLError(err.format(name)) if type(value) is MethodType: value = value() return value @@ -168,8 +167,7 @@ def lookup(self, name): except STLError: pass - raise STLError, 'name "%s" not found in the namespace' % name - + raise STLError('name "%s" not found in the namespace' % name) def __getslice__(self, a, b): return self.__class__(list.__getslice__(self, a, b)) @@ -195,7 +193,7 @@ def substitute_boolean(data, stack, repeat_stack, encoding='utf-8'): return bool(value) -def substitute_attribute(data, stack, repeat_stack, encoding='utf-8'): +def substitute_attribute(data, stack, repeat_stack): """Interprets the given data as a substitution string with the "${expr}" format, where the expression within the brackets is an STL expression. @@ -203,7 +201,7 @@ def substitute_attribute(data, stack, repeat_stack, encoding='utf-8'): substitutions done. """ if type(data) is not str: - raise ValueError, 'byte string expected, not %s' % type(data) + raise ValueError('byte string expected, not %s' % type(data)) # Solo, preserve the value None match = subs_expr_solo.match(data) if match is not None: @@ -214,9 +212,9 @@ def substitute_attribute(data, stack, repeat_stack, encoding='utf-8'): return None, 1 # Send the string if isinstance(value, MSG): - return value.gettext().encode(encoding), 1 - elif type(value) is unicode: - return value.encode(encoding), 1 + return value.gettext(), 1 + elif type(value) is str: + return value, 1 return str(value), 1 # A little more complex def repl(match): @@ -227,14 +225,13 @@ def repl(match): return '' # Send the string if isinstance(value, MSG): - return value.gettext().encode(encoding) - elif type(value) is unicode: - return value.encode(encoding) + return value.gettext() + elif type(value) is str: + return value return str(value) return subs_expr.subn(repl, data) - def substitute(data, stack, repeat_stack, encoding='utf-8'): """Interprets the given data as a substitution string with the "${expr}" format, where the expression within the brackets is an STL expression. @@ -243,7 +240,7 @@ def substitute(data, stack, repeat_stack, encoding='utf-8'): substitutions done. """ if type(data) is not str: - raise ValueError, 'byte string expected, not %s' % type(data) + raise ValueError('byte string expected, not %s' % type(data)) segments = subs_expr.split(data) for i, segment in enumerate(segments): @@ -263,12 +260,12 @@ def substitute(data, stack, repeat_stack, encoding='utf-8'): value = value.gettext() # Yield - if type(value) is unicode: + if type(value) is str: yield TEXT, value.encode(encoding), 0 elif is_xml_stream(value): for x in value: if type(x) is not tuple: - raise STLError, ERR_EXPR_XML % (type(x), segment) + raise STLError(ERR_EXPR_XML % (type(x), segment)) yield x else: yield TEXT, str(value), 0 @@ -276,7 +273,6 @@ def substitute(data, stack, repeat_stack, encoding='utf-8'): yield TEXT, segment, 0 - def stl(document=None, namespace=freeze({}), prefix=None, events=None, mode='events', skip=(DOCUMENT_TYPE,)): # Input @@ -309,16 +305,15 @@ def stl(document=None, namespace=freeze({}), prefix=None, events=None, return stream_to_str_as_xhtml(stream, encoding) elif mode == 'html': return stream_to_str_as_html(stream, encoding) - except STLError, e: + except STLError as e: error = 'Error in generation of {0}\n'.format(mode) if document: error += 'Template {0}\n'.format(document.key) - raise STLError(error + e.message) + raise STLError(error + str(e)) # Unknow mode raise ValueError('unexpected mode "{0}"'.format(mode)) - stl_repeat = stl_uri, 'repeat' stl_if = stl_uri, 'if' stl_omit_tag = stl_uri, 'omit-tag' @@ -356,7 +351,7 @@ def process_start_tag(tag_uri, tag_name, attributes, stack, repeat, encoding): aux[(attr_uri, attr_name)] = attr_name continue # Non Boolean attributes - value, n = substitute_attribute(value, stack, repeat, encoding) + value, n = substitute_attribute(value, stack, repeat) # Output only values different than None if value is None: continue @@ -443,11 +438,11 @@ def process(events, start, end, stack, re_stack, encoding, skip_events): i += 1 - ######################################################################## # Set prefix ######################################################################## -css_uri_expr = compile (r"url\(([a-zA-Z0-9\./%\-\_]*/%3[bB]{1}download)\);") +css_uri_expr = compile(r"url\(([a-zA-Z0-9\./%\-\_]*/%3[bB]{1}download)\);") + def set_prefix(stream, prefix, ns_uri=xhtml_uri, uri=None): if isinstance(prefix, str): @@ -462,7 +457,6 @@ def set_prefix(stream, prefix, ns_uri=xhtml_uri, uri=None): return rewrite_uris(stream, rewrite, ns_uri) - def resolve_pointer(offset, reference, value): # FIXME Exception for STL if value[:2] == '${': @@ -490,7 +484,6 @@ def resolve_pointer(offset, reference, value): return str(value) - def rewrite_uris(stream, rewrite, ns_uri=xhtml_uri): for event in stream: type, value, line = event @@ -537,10 +530,10 @@ def rewrite_uris(stream, rewrite, ns_uri=xhtml_uri): else: yield event + ########################################################################### # STLFile ########################################################################### - class STLFile(XMLFile): # FIXME To be changed once we have our own extension and mimetype (#864) @@ -561,14 +554,12 @@ class STLTemplate(prototype): template = None show = True - def get_template(self): if type(self.template) is list: return self.template error = 'template variable of unexpected type "%s"' - raise TypeError, error % type(self.template).__name__ - + raise TypeError(error % type(self.template).__name__) def render(self, mode='events'): if not self.show: diff --git a/itools/tmx/__init__.py b/itools/tmx/__init__.py index 0c1e25b1d..07b91ce26 100644 --- a/itools/tmx/__init__.py +++ b/itools/tmx/__init__.py @@ -17,7 +17,7 @@ # Import from itools from itools.core import add_type, get_abspath -from tmx import TMXFile, Sentence, TMXUnit, TMXNote +from .tmx import TMXFile, Sentence, TMXUnit, TMXNote from itools.xml import register_dtd diff --git a/itools/tmx/tmx.py b/itools/tmx/tmx.py old mode 100644 new mode 100755 index c059c3d6b..990afd85d --- a/itools/tmx/tmx.py +++ b/itools/tmx/tmx.py @@ -22,7 +22,6 @@ from itools.xml import XMLParser, START_ELEMENT, END_ELEMENT, COMMENT, TEXT - # FIXME TMXNote and XLFNote are the same class TMXNote(object): @@ -48,7 +47,6 @@ def to_str(self): return '%s\n' % (attributes, self.text) - class Sentence(object): def __init__(self, attributes): @@ -56,7 +54,6 @@ def __init__(self, attributes): self.text = '' self.notes = [] - def to_str(self): s = [] attributes = ['xml:lang="%s"' % self.attributes['lang']] @@ -74,7 +71,6 @@ def to_str(self): return ''.join(s) - class TMXUnit(object): def __init__(self, attributes): @@ -82,7 +78,6 @@ def __init__(self, attributes): self.msgstr = {} self.notes = [] - def to_str(self): s = [] if self.attributes != {}: @@ -96,8 +91,7 @@ def to_str(self): for l in self.notes: s.append(l.to_str()) - languages = self.msgstr.keys() - languages.sort() + languages = sorted(list(self.msgstr.keys())) for language in languages: s.append(self.msgstr[language].to_str()) @@ -105,13 +99,11 @@ def to_str(self): return ''.join(s) - class TMXFile(TextFile): class_mimetypes = ['application/x-tmx'] class_extension = 'tmx' - def new(self): self.version = '1.4' self.header = {'o-encoding': 'utf-8', 'srclang': 'en'} @@ -161,8 +153,7 @@ def _load_state_from_file(self, file): tu.notes = notes srclang = tu.attributes.get('srclang', default_srclang) if srclang == '*all*': - raise NotImplementedError, \ - 'no support for "*all*" in srclang attribute.' + raise NotImplementedError('no support for "*all*" in srclang attribute.') msgid = tu.msgstr[srclang].text messages[msgid] = tu elif local_name == 'tuv': @@ -175,7 +166,9 @@ def _load_state_from_file(self, file): elif event == COMMENT: pass elif event == TEXT: - text = unicode(value, 'UTF-8') + if isinstance(value, bytes): + value = value.decode("utf-8") + text = value self.messages = messages @@ -202,8 +195,7 @@ def to_str(self, encoding=None): # TMX body output.append('\n') messages = self.messages - msgids = messages.keys() - msgids.sort() + msgids = sorted(list(messages.keys())) for msgid in msgids: output.append(messages[msgid].to_str()) output.append('\n') @@ -212,7 +204,6 @@ def to_str(self, encoding=None): output.append('\n') return ''.join(output) - ####################################################################### # API ####################################################################### @@ -224,10 +215,8 @@ def get_languages(self): languages.append(l) return languages - def get_srclang(self): - return u'%s' % self.header['srclang'] - + return '%s' % self.header['srclang'] def add_unit(self, filename, source, context, line): # XXX Context must be used!! @@ -241,5 +230,4 @@ def add_unit(self, filename, source, context, line): return unit - register_handler_class(TMXFile) diff --git a/itools/uri/__init__.py b/itools/uri/__init__.py index ebd7228a8..9b772d50c 100644 --- a/itools/uri/__init__.py +++ b/itools/uri/__init__.py @@ -19,13 +19,13 @@ """ # Import from itools -import mailto -from generic import Path, Reference, decode_query, encode_query -from generic import normalize_path -from uri import get_reference, get_uri_name, get_uri_path -from uri import resolve_uri, resolve_uri2, resolve_name -from uri import get_host_from_authority -from registry import register_scheme, get_scheme +from . import mailto +from .generic import Path, Reference, decode_query, encode_query +from .generic import normalize_path +from .uri import get_reference, get_uri_name, get_uri_path +from .uri import resolve_uri, resolve_uri2, resolve_name +from .uri import get_host_from_authority +from .registry import register_scheme, get_scheme diff --git a/itools/uri/generic.py b/itools/uri/generic.py old mode 100644 new mode 100755 index 52857bd29..6a80d159a --- a/itools/uri/generic.py +++ b/itools/uri/generic.py @@ -48,9 +48,7 @@ # Import from the Standard Library from copy import copy -from urlparse import urlsplit, urlunsplit -from urllib import quote_plus, unquote, unquote_plus - +from urllib.parse import urlsplit, urlunsplit, quote_plus, unquote, unquote_plus # Import from itools from itools.core import freeze @@ -61,7 +59,7 @@ def _normalize_path(path): if type(path) is not str: - raise TypeError, 'path must be an string, not a %s' % type(path) + raise TypeError('path must be an string, not a %s' % type(path)) # Does the path start and/or end with an slash? startswith_slash = endswith_slash = False @@ -123,7 +121,6 @@ def normalize_path(path): return '' - class Path(list): """A path is a sequence of segments. A segment is has a name and, optionally one or more parameters. @@ -135,8 +132,10 @@ class Path(list): __slots__ = ['startswith_slash', 'endswith_slash'] - def __init__(self, path): + if type(path) is bytes: + path = path.decode() + if type(path) is str: startswith_slash, path, endswith_slash = _normalize_path(path) self.startswith_slash = startswith_slash @@ -144,6 +143,8 @@ def __init__(self, path): elif type(path) is Path: self.startswith_slash = path.startswith_slash self.endswith_slash = path.endswith_slash + + else: # XXX Here the path is not normalized: # @@ -151,27 +152,25 @@ def __init__(self, path): # a/../b self.startswith_slash = False self.endswith_slash = False - path = [ str(x) for x in path ] + path = [str(x) for x in path] list.__init__(self, path) + def __getitem__(self, val): + if isinstance(val, int): + return super().__getitem__(val) - def __getslice__(self, a, b): - slice = Path(list.__getslice__(self, a, b)) + slice = Path(super().__getitem__(val)) slice.startswith_slash = self.startswith_slash return slice - def __add__(self, path): - raise NotImplementedError, \ - 'paths can not be added, use resolve2 instead' - + raise NotImplementedError('paths can not be added, use resolve2 instead') ########################################################################## # API def __repr__(self): - return '' % hex(id(self)) - + return "Path({path!r})".format(path=str(self)) def __str__(self): path = '/' if self.startswith_slash else '' @@ -182,39 +181,32 @@ def __str__(self): return path if path else '.' - def __ne__(self, other): if isinstance(other, str): other = Path(other) return str(self) != str(other) - def __eq__(self, other): if isinstance(other, str): other = Path(other) return str(self) == str(other) - def __hash__(self): return hash(str(self)) - def is_absolute(self): return self.startswith_slash - def is_relative(self): return not self.startswith_slash - def get_name(self): if len(self) > 0: return self[-1] return '' - def resolve(self, path): """Resolve the path following the standard (RFC2396). This is to say, it takes into account the trailing slash, so: @@ -237,7 +229,6 @@ def resolve(self, path): return Path('%s/../%s' % (self_str, path)) - def resolve2(self, path): """This method provides an alternative to the standards resolution algorithm. The difference is that it not takes into account the @@ -269,13 +260,12 @@ def resolve2(self, path): new_path.append(name) return new_path - def resolve_name(self, name): """This is a particular case of the 'resolve2' method, where the reference is known to be a relative path of length = 1. """ if not isinstance(name, str): - raise TypeError, 'unexpected value "%s"' % repr(name) + raise TypeError('unexpected value "%s"' % repr(name)) # Relative path path = copy(self) @@ -283,23 +273,21 @@ def resolve_name(self, name): path.endswith_slash = False return path - def get_prefix(self, path): """Returns the common prefix of two paths, for example: - >>> print Path('a/b/c').get_prefix(Path('a/b/d/e')) + >>> print(Path('a/b/c').get_prefix(Path('a/b/d/e'))) a/b XXX When there are parameters (e.g. a/b;lang=es/c) it is undefined. """ if type(path) is not Path: path = Path(path) - i = 0 while i < len(self) and i < len(path) and self[i] == path[i]: i = i + 1 - return self[:i] + return self[:i] def get_pathto(self, path): """Returns the relative path from 'self' to 'path'. This operation is @@ -313,14 +301,12 @@ def get_pathto(self, path): i = len(prefix) return Path(((['..'] * len(self[i:])) + path[i:]) or []) - def get_pathtoroot(self): """Returns the path from the tail to the head, for example: '../../..' """ return Path('../' * (len(self) - 1)) - ######################################################################### # Query ########################################################################## @@ -345,6 +331,8 @@ def decode_query(data, schema=None): See http://www.w3.org/TR/REC-html40/interact/forms.html#h-17.13.4.1 for details. """ + if isinstance(data, bytes): + data = data.decode("utf-8") query = {} if data: if schema is None: @@ -374,7 +362,6 @@ def decode_query(data, schema=None): return query - def encode_query(query, schema=None): """This method encodes a query as defined by the "application/x-www-form-urlencoded" content type (see @@ -422,7 +409,6 @@ def encode_query(query, schema=None): return '&'.join(line) - ########################################################################## # Generic references ########################################################################## @@ -451,7 +437,6 @@ class Reference(object): __slots__ = ['scheme', 'authority', 'path', 'query', 'fragment'] - def __init__(self, scheme, authority, path, query, fragment=None): self.scheme = scheme self.authority = authority @@ -464,7 +449,6 @@ def __init__(self, scheme, authority, path, query, fragment=None): ## def netpath(self): ## return NetPath('//%s/%s' % (self.authority, self.path)) - def __str__(self): path = str(self.path) if path == '.': @@ -478,19 +462,15 @@ def __str__(self): return '.' return reference - def __eq__(self, other): return str(self) == str(other) - def __ne__(self, other): return str(self) != str(other) - def __hash__(self): return hash(str(self)) - def resolve(self, reference): """Resolve the given relative URI, this URI (self) is considered to be the base. @@ -538,7 +518,6 @@ def resolve(self, reference): copy(reference.query), reference.fragment) - def resolve2(self, reference): """This is much like 'resolve', but uses 'Path.resolve2' method instead. @@ -586,7 +565,6 @@ def resolve2(self, reference): copy(reference.query), reference.fragment) - def resolve_name(self, name): """This is a particular case of the 'resolve2' method, where the reference is known to be a relative path of length = 1. @@ -594,7 +572,6 @@ def resolve_name(self, name): path = self.path.resolve_name(name) return Reference(self.scheme, self.authority, path, {}) - def replace(self, **kw): """This method returns a new uri reference, equal to this one, but with the given keyword parameters set in the query. @@ -611,10 +588,10 @@ def replace(self, **kw): value_type = type(value) if value_type is int: value = str(value) - elif value_type is unicode: + elif value_type is str: value = value.encode('utf-8') elif value_type is not str: - raise TypeError, 'unexepected %s value' % type + raise TypeError('unexepected %s value' % value_type) # Update query[key] = value # Ok @@ -622,7 +599,6 @@ def replace(self, **kw): self.fragment) - class EmptyReference(Reference): scheme = None @@ -656,7 +632,7 @@ def decode(data): return Reference('', '', data, {}, None) if data_type is not str: - raise TypeError, 'unexpected %s' % type(data) + raise TypeError('unexpected %s' % type(data)) # Special case, the empty reference if data == '': diff --git a/itools/uri/mailto.py b/itools/uri/mailto.py index 12f612dbf..d6e1f48f9 100644 --- a/itools/uri/mailto.py +++ b/itools/uri/mailto.py @@ -17,7 +17,7 @@ # along with this program. If not, see . # Import from itools -from registry import register_scheme +from .registry import register_scheme class Mailto(object): @@ -28,25 +28,21 @@ class Mailto(object): def __init__(self, address): self.address = address - @property def username(self): if '@' in self.address: return self.address.split('@', 1)[0] return None - @property def host(self): if '@' in self.address: return self.address.split('@', 1)[1] return None - def __str__(self): return 'mailto:%s' % self.address - def __eq__(self, other): return str(self) == str(other) @@ -60,7 +56,6 @@ def decode(data): # given string. return Mailto(data[7:]) - @staticmethod def encode(value): return 'mailto:%s' % value.address diff --git a/itools/uri/registry.py b/itools/uri/registry.py index 659003bcd..31e0ecec9 100644 --- a/itools/uri/registry.py +++ b/itools/uri/registry.py @@ -15,7 +15,7 @@ # along with this program. If not, see . # Import from itools -from generic import GenericDataType +from .generic import GenericDataType _schemes = {} diff --git a/itools/uri/uri.py b/itools/uri/uri.py index e6bb42d99..6ba597ccf 100644 --- a/itools/uri/uri.py +++ b/itools/uri/uri.py @@ -17,15 +17,13 @@ # Import from itools from itools.core import LRUCache -from generic import GenericDataType -from registry import get_scheme - +from .generic import GenericDataType +from .registry import get_scheme cache = LRUCache(200) - def get_reference(reference): """Returns a URI reference of the good type from the given string. """ @@ -46,14 +44,12 @@ def get_reference(reference): return parsed_reference - def get_uri_name(uri): uri = get_reference(uri) return str(uri.path[-1]) - def get_uri_path(uri): uri = get_reference(uri) @@ -76,7 +72,6 @@ def resolve_uri(base, reference): return str(uri) - def resolve_uri2(base, reference): base = get_reference(base) reference = get_reference(reference) @@ -85,7 +80,6 @@ def resolve_uri2(base, reference): return str(uri) - def resolve_name(base, name): base = get_reference(base) diff --git a/itools/validators/__init__.py b/itools/validators/__init__.py index de196d0fc..f1db7b35e 100644 --- a/itools/validators/__init__.py +++ b/itools/validators/__init__.py @@ -15,12 +15,10 @@ # along with this program. If not, see . # Import from itools -from base import BaseValidator -from exceptions import ValidationError -from registry import register_validator, validator -import database -import files -import password +from .base import BaseValidator +from .exceptions import ValidationError +from .registry import register_validator, validator +from . import database, files, password __all__ = [ 'BaseValidator', diff --git a/itools/validators/base.py b/itools/validators/base.py index a65376ec1..c87127d4a 100644 --- a/itools/validators/base.py +++ b/itools/validators/base.py @@ -22,8 +22,8 @@ from itools.gettext import MSG # Import from here -from exceptions import ValidationError -from registry import register_validator +from .exceptions import ValidationError +from .registry import register_validator class BaseValidatorMetaclass(prototype_type): @@ -35,15 +35,15 @@ def __new__(mcs, name, bases, dict): return cls -class validator_prototype(prototype): +class validator_prototype(prototype, metaclass=BaseValidatorMetaclass): - __metaclass__ = BaseValidatorMetaclass + pass class BaseValidator(validator_prototype): validator_id = None - errors = {'invalid': MSG(u'Enter a valid value')} + errors = {'invalid': MSG('Enter a valid value')} def is_valid(self, value): try: @@ -52,20 +52,16 @@ def is_valid(self, value): return False return True - def check(self, value): raise NotImplementedError('Validator is not configured') - def get_error_msg(self): return self.msg - def raise_default_error(self, kw={}): - code, msg = self.errors.items()[0] + code, msg = list(self.errors.items())[0] raise ValidationError(msg, code, kw) - def raise_errors(self, errors, kw={}): l = [] for code in errors: @@ -73,17 +69,15 @@ def raise_errors(self, errors, kw={}): l.append((msg, code, kw)) raise ValidationError(l) - def __call__(self, value): return self.check(value) - class EqualsValidator(BaseValidator): validator_id = 'equals-to' base_value = None - errors = {'not_equals': MSG(u'The value should be equals to {base_value}')} + errors = {'not_equals': MSG('The value should be equals to {base_value}')} def check(self, value): if value != self.base_value: @@ -91,7 +85,6 @@ def check(self, value): self.raise_default_error(kw) - class RegexValidator(BaseValidator): regex = None @@ -104,20 +97,17 @@ def check(self, value): self.raise_default_error() - - class HexadecimalValidator(RegexValidator): validator_id = 'hexadecimal' regex = '^#[A-Fa-f0-9]+$' - errors = {'invalid': MSG(u'Enter a valid value.')} - + errors = {'invalid': MSG('Enter a valid value.')} class PositiveIntegerValidator(BaseValidator): validator_id = 'integer-positive' - errors = {'integer_positive': MSG(u'Ensure this value is positive.')} + errors = {'integer_positive': MSG('Ensure this value is positive.')} def check(self, value): if value < 0: @@ -125,11 +115,10 @@ def check(self, value): self.raise_default_error(kw) - class PositiveIntegerNotNullValidator(BaseValidator): validator_id = 'integer-positive-not-null' - errors = {'integer_positive_not_null': MSG(u'Ensure this value is greater than 0.')} + errors = {'integer_positive_not_null': MSG('Ensure this value is greater than 0.')} def check(self, value): if value <= 0: @@ -137,11 +126,10 @@ def check(self, value): self.raise_default_error(kw) - class MaxValueValidator(BaseValidator): validator_id = 'max-value' - errors = {'max_value': MSG(u'Ensure this value is less than or equal to {max_value}.')} + errors = {'max_value': MSG('Ensure this value is less than or equal to {max_value}.')} max_value = None def check(self, value): @@ -150,11 +138,10 @@ def check(self, value): self.raise_default_error(kw) - class MinValueValidator(BaseValidator): validator_id = 'min-value' - errors = {'min_value': MSG(u'Ensure this value is greater than or equal to {min_value}.')} + errors = {'min_value': MSG('Ensure this value is greater than or equal to {min_value}.')} min_value = None def check(self, value): @@ -163,13 +150,12 @@ def check(self, value): self.raise_default_error(kw) - class MinMaxValueValidator(BaseValidator): validator_id = 'min-max-value' errors = {'min_max_value': MSG( - u'Ensure this value is greater than or equal to {min_value} ' - u'and value is less than or equal to {max_value}.')} + 'Ensure this value is greater than or equal to {min_value} ' + 'and value is less than or equal to {max_value}.')} min_value = None max_value = None @@ -180,13 +166,11 @@ def check(self, value): self.raise_default_error(kw) - - class MinLengthValidator(BaseValidator): validator_id = 'min-length' min_length = 0 - errors = {'min_length': MSG(u'Ensure this value has at least {min_length} characters.')} + errors = {'min_length': MSG('Ensure this value has at least {min_length} characters.')} def check(self, value): if len(value) < self.min_length: @@ -196,12 +180,11 @@ def check(self, value): self.raise_default_error(kw) - class MaxLengthValidator(BaseValidator): validator_id = 'max-length' max_length = 0 - errors = {'max_length': MSG(u'Ensure this value has at most {max_length} characters.')} + errors = {'max_length': MSG('Ensure this value has at most {max_length} characters.')} def check(self, value): if len(value) > self.max_length: diff --git a/itools/validators/database.py b/itools/validators/database.py index 05222bc3e..052503e40 100644 --- a/itools/validators/database.py +++ b/itools/validators/database.py @@ -18,13 +18,13 @@ from itools.gettext import MSG # Import from here -from base import BaseValidator +from .base import BaseValidator class UniqueValidator(BaseValidator): validator_id = 'unique' - errors = {'unique': MSG(u'The field should be unique.')} + errors = {'unique': MSG('The field should be unique.')} field_name = None base_query = None diff --git a/itools/validators/exceptions.py b/itools/validators/exceptions.py index 6f6e164f2..5eeaca371 100644 --- a/itools/validators/exceptions.py +++ b/itools/validators/exceptions.py @@ -30,7 +30,6 @@ def __init__(self, msg=None, code=None, msg_params=None): errors.append((msg, code, msg_params)) self.errors = errors - def get_messages(self, field): l = [] for msg, code, msg_params in self.errors: @@ -39,7 +38,6 @@ def get_messages(self, field): l.append(msg.gettext(**msg_params)) return l - def get_message(self, field=None, mode='html'): messages = self.get_messages(field) if mode == 'html': @@ -47,6 +45,5 @@ def get_message(self, field=None, mode='html'): return MSG(msg, format='html') return MSG('\n'.join(messages)) - def __str__(self): return self.get_message() diff --git a/itools/validators/files.py b/itools/validators/files.py index 7f0b0c0d9..94885d467 100644 --- a/itools/validators/files.py +++ b/itools/validators/files.py @@ -15,7 +15,7 @@ # along with this program. If not, see . # Import from standard library -from cStringIO import StringIO +from io import StringIO # Import from PIL from PIL import Image as PILImage @@ -24,9 +24,8 @@ from itools.gettext import MSG # Import from here -from base import BaseValidator -from exceptions import ValidationError - +from .base import BaseValidator +from .exceptions import ValidationError class FileExtensionValidator(BaseValidator): @@ -34,9 +33,8 @@ class FileExtensionValidator(BaseValidator): validator_id = 'file-extension' allowed_extensions = [] errors = {'invalid_extension': MSG( - u"File extension '{extension}' is not allowed. " - u"Allowed extensions are: '{allowed_extensions}'.")} - + "File extension '{extension}' is not allowed. " + "Allowed extensions are: '{allowed_extensions}'.")} def check(self, value): extension = self.get_extension(value) @@ -45,27 +43,24 @@ def check(self, value): 'allowed_extensions': ','.join(self.allowed_extensions)} self.raise_default_error(kw) - def get_extension(self, value): filename, mimetype, body = value return filename.split('.')[-1] - class ImageExtensionValidator(FileExtensionValidator): validator_id = 'image-extension' allowed_extensions = ['jpeg', 'png', 'gif'] - class MimetypesValidator(BaseValidator): validator_id = 'file-mimetypes' allowed_mimetypes = [] errors = {'bad_mimetype': MSG( - u"File mimetype '{mimetype}' is not allowed. " - u"Allowed mimetypes are: '{allowed_mimetypes}'.")} + "File mimetype '{mimetype}' is not allowed. " + "Allowed mimetypes are: '{allowed_mimetypes}'.")} def check(self, value): @@ -76,19 +71,17 @@ def check(self, value): self.raise_default_error(kw) - class ImageMimetypesValidator(MimetypesValidator): validator_id = 'image-mimetypes' allowed_mimetypes = ['image/jpeg', 'image/png', 'image/gif'] - class FileSizeValidator(BaseValidator): validator_id = 'file-size' max_size = 1024*1024*10 - errors = {'too_big': MSG(u'Your file is too big. ({size})')} + errors = {'too_big': MSG('Your file is too big. ({size})')} def check(self, value): filename, mimetype, body = value @@ -98,29 +91,27 @@ def check(self, value): 'max_size': self.pretty_bytes(self.max_size)} self.raise_default_error(kw) - def pretty_bytes(self, b): # 1 Byte = 8 Bits # 1 Kilobyte = 1024 Bytes # 1 Megabyte = 1048576 Bytes # 1 Gigabyte = 1073741824 Bytes if b < 1024: - return u'%.01f Bytes' % b + return '%.01f Bytes' % b elif b < 1048576: - return u'%.01f KB' % (b / 1024) + return '%.01f KB' % (b / 1024) elif b < 1073741824: - return u'%.01f MB' % (b / 1048576) + return '%.01f MB' % (b / 1048576) return u'%.01f GB' % (b / 1073741824) - class ImagePixelsValidator(BaseValidator): validator_id = 'image-pixels' max_pixels = 2000*2000 - errors = {'too_much_pixels': MSG(u"Image is too big."), - 'image_has_errors': MSG(u"Image contains errors.")} + errors = {'too_much_pixels': MSG("Image is too big."), + 'image_has_errors': MSG("Image contains errors.")} def check(self, value): filename, mimetype, body = value diff --git a/itools/validators/password.py b/itools/validators/password.py index e655a959e..2dc4ceca3 100644 --- a/itools/validators/password.py +++ b/itools/validators/password.py @@ -21,7 +21,7 @@ from itools.gettext import MSG # Import from here -from base import BaseValidator +from .base import BaseValidator class StrongPasswordValidator(BaseValidator): @@ -36,10 +36,10 @@ class StrongPasswordValidator(BaseValidator): min_length = 5 errors = { - 'too_short': MSG(u"This password is too short. It must contain at least {min_length} characters."), - 'need_character': MSG(u"This password should contains at least one character."), - 'need_number': MSG(u"This password should contains at least one number."), - 'need_special_character': MSG(u"This password should contains at least one special character."), + 'too_short': MSG("This password is too short. It must contain at least {min_length} characters."), + 'need_character': MSG("This password should contains at least one character."), + 'need_number': MSG("This password should contains at least one number."), + 'need_special_character': MSG("This password should contains at least one special character."), } def check(self, value): diff --git a/itools/validators/registry.py b/itools/validators/registry.py index c6b6421af..dafebb8ec 100644 --- a/itools/validators/registry.py +++ b/itools/validators/registry.py @@ -17,6 +17,7 @@ validators_registry = {} + def register_validator(cls): validators_registry[cls.validator_id] = cls diff --git a/itools/validators/test_view.py b/itools/validators/test_view.py old mode 100644 new mode 100755 index 4f09d961a..c07a2c686 --- a/itools/validators/test_view.py +++ b/itools/validators/test_view.py @@ -26,60 +26,60 @@ class TestValidators(AutoEdit): access = True - title = MSG(u"Test validators") + title = MSG("Test validators") fields = ['field_1', 'field_2', 'field_3', 'field_4', 'field_5', 'field_6', 'field_7', 'field_8', 'field_9', 'field_10', 'field_11', 'field_12', 'field_13', 'field_14', 'field_15'] field_1 = Integer_Field( - title=MSG(u'5+5 equals to ?'), + title=MSG('5+5 equals to ?'), validators=[validator('equals-to', base_value=10)], error_messages={'not_equals': MSG(u'Give me a 10 ;)')} ) field_2 = Char_Field( - title=MSG(u'Hexadecimal color'), + title=MSG('Hexadecimal color'), validators=[validator('hexadecimal')]) field_3 = Integer_Field( - title=MSG(u'Give a positive number'), + title=MSG('Give a positive number'), validators=[validator('integer-positive')]) field_4 = Integer_Field( - title=MSG(u'Give a strict positive number'), + title=MSG('Give a strict positive number'), validators=[validator('integer-positive-not-null')]) field_5 = Integer_Field( - title=MSG(u'Give a number (max value 10)'), + title=MSG('Give a number (max value 10)'), validators=[validator('max-value', max_value=10)]) field_6 = Integer_Field( - title=MSG(u'Give a number (min value 10)'), + title=MSG('Give a number (min value 10)'), validators=[validator('min-value', min_value=10)]) field_7 = Integer_Field( - title=MSG(u'Give a number (>=10 and <=20)'), + title=MSG('Give a number (>=10 and <=20)'), validators=[validator('min-max-value', min_value=10, max_value=20)]) field_8 = Char_Field( - title=MSG(u'Give text (min length: 3 characters)'), + title=MSG('Give text (min length: 3 characters)'), validators=[validator('min-length', min_length=3)]) field_9 = Char_Field( - title=MSG(u'Give text (max length: 5 characters)'), + title=MSG('Give text (max length: 5 characters)'), validators=[validator('max-length', max_length=5)]) field_10 = Email_Field( - title=MSG(u'Give an email (unique in DB)'), + title=MSG('Give an email (unique in DB)'), validators=[validator('unique', field_name='email')], error_messages={'invalid': MSG(u'Give be an email address !!!'), 'unique': MSG(u'This address is already used')}) field_11 = File_Field( - title=MSG(u'File extension (png)'), + title=MSG('File extension (png)'), validators=[validator('file-extension', allowed_extensions=['png'])]) field_12 = File_Field( - title=MSG(u'File mimetypes (image/png)'), + title=MSG('File mimetypes (image/png)'), validators=[validator('file-mimetypes', allowed_mimetypes=['image/png'])]) field_13 = File_Field( - title=MSG(u'Image max pixels'), + title=MSG('Image max pixels'), validators=[validator('image-pixels', max_pixels=10*10)]) field_14 = Char_Field( - title=MSG(u'Strong password'), + title=MSG('Strong password'), validators=[validator('strong-password')]) field_15 = Integer_Field( - title=MSG(u'Number >=5 and equals to 10'), + title=MSG('Number >=5 and equals to 10'), validators=[ validator('min-value', min_value=5), validator('equals-to', base_value=10), @@ -91,4 +91,4 @@ def _get_datatype(self, resource, context, name): return field(resource=resource) def action(self, resource, context, form): - print form + print(form) diff --git a/itools/web/__init__.py b/itools/web/__init__.py index 133703ca1..16af77672 100644 --- a/itools/web/__init__.py +++ b/itools/web/__init__.py @@ -18,22 +18,20 @@ # along with this program. If not, see . # Import from itools -from context import get_context, set_context -from context import select_language -from context import WebLogger -from exceptions import HTTPError, ClientError, ServerError -from exceptions import NotModified -from exceptions import BadRequest, Unauthorized, Forbidden, NotFound -from exceptions import InternalServerError, NotImplemented, BadGateway -from exceptions import ServiceUnavailable, MethodNotAllowed, Conflict -from exceptions import FormError -from headers import Cookie, SetCookieDataType -from messages import INFO, ERROR, MSG_MISSING_OR_INVALID -from utils import NewJSONEncoder -from views import ItoolsView, BaseView, STLView +from .context import get_context, set_context +from .context import select_language +from .exceptions import HTTPError, ClientError, ServerError +from .exceptions import NotModified +from .exceptions import BadRequest, Unauthorized, Forbidden, NotFound +from .exceptions import InternalServerError, NotImplemented, BadGateway +from .exceptions import ServiceUnavailable, MethodNotAllowed, Conflict +from .exceptions import FormError +from .headers import Cookie, SetCookieDataType +from .messages import INFO, ERROR, MSG_MISSING_OR_INVALID +from .utils import NewJSONEncoder +from .views import ItoolsView, BaseView, STLView __all__ = [ - 'WebLogger', # Context 'set_context', 'get_context', diff --git a/itools/web/context.py b/itools/web/context.py old mode 100644 new mode 100755 index 0f877bb87..189769e0d --- a/itools/web/context.py +++ b/itools/web/context.py @@ -26,11 +26,10 @@ # Import from itools from itools.database.fields import get_field_and_datatype from itools.datatypes import String -from itools.log import Logger from itools.validators import ValidationError # Local imports -from exceptions import FormError +from .exceptions import FormError ########################################################################### @@ -40,7 +39,7 @@ def set_context(ctx): - if ctx and get_context() != None: + if ctx and get_context() is not None: raise ValueError('Cannot set context') g.context = ctx @@ -86,7 +85,7 @@ def _get_form_value(form, name, type=String, default=None): if not isinstance(value, list): value = [value] try: - values = [ datatype.decode(x) for x in value ] + values = [datatype.decode(x) for x in value] except Exception: raise FormError(invalid_msg, invalid=True) # Check the values are valid @@ -112,7 +111,7 @@ def _get_form_value(form, name, type=String, default=None): return default # We consider a blank string to be a missing value (FIXME not reliable). - is_blank = isinstance(value, (str, unicode)) and not value.strip() + is_blank = isinstance(value, (str, str)) and not value.strip() if is_blank: if is_mandatory: raise FormError(required_msg, missing=True) @@ -130,7 +129,7 @@ def check_form_value(field, value): validator = validator(title=field.title, context=context) try: validator.check(value) - except ValidationError, e: + except ValidationError as e: errors.extend(e.get_messages(field)) if errors: raise FormError(messages=errors, invalid=True) @@ -146,37 +145,10 @@ def get_form_value(form, name, type=String, default=None): return value # Multilingual values = {} - for key, value in form.iteritems(): + for key, value in form.items(): if key.startswith('%s:' % name): x, lang = key.split(':', 1) - value =_get_form_value(form, key, type, default) + value = _get_form_value(form, key, type, default) values[lang] = value check_form_value(field, values) return values - - - -class WebLogger(Logger): - - def get_body(self): - context = get_context() - if context is None: - return Logger.get_body(self) - - # The URI and user - if context.user: - lines = ['%s (user: %s)\n\n' % (context.uri, context.user.name)] - else: - lines = ['%s\n\n' % context.uri] - - # Request header - lines.append(context.get_request_line() + '\n') - headers = context.get_headers() - for key, value in headers: - lines.append('%s: %s\n' % (key, value)) - lines.append('\n') - - # Ok - body = Logger.get_body(self) - lines.extend(body) - return lines diff --git a/itools/web/dispatcher.py b/itools/web/dispatcher.py index 111bf6cbc..5d5159297 100644 --- a/itools/web/dispatcher.py +++ b/itools/web/dispatcher.py @@ -96,7 +96,7 @@ def resolve(self, path): Path resolution, look for a corresponding registered pattern and return associated data along with the extracted parameters. """ - for pattern, values in self.patterns.iteritems(): + for pattern, values in self.patterns.items(): compiled_regex, data = values match = compiled_regex.search(path) if match: diff --git a/itools/web/entities.py b/itools/web/entities.py index 6168cb780..e1705c04b 100644 --- a/itools/web/entities.py +++ b/itools/web/entities.py @@ -14,11 +14,13 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +from logging import getLogger + # Import from itools from itools.handlers import File -from itools.log import log_warning -from headers import get_type +from .headers import get_type +log = getLogger("itools.web") def read_headers(file): @@ -36,8 +38,7 @@ def read_headers(file): try: value = datatype.decode(value) except Exception: - log_warning("Failed to parse the '%s' header" % name, - domain='itools.web') + log.warning("Failed to parse the '%s' header" % name, exc_info=True) else: entity_headers[name] = value # Next diff --git a/itools/web/exceptions.py b/itools/web/exceptions.py old mode 100644 new mode 100755 index 978331303..2a08558c6 --- a/itools/web/exceptions.py +++ b/itools/web/exceptions.py @@ -17,11 +17,11 @@ # Import from itools from itools.core import freeze from itools.gettext import MSG -from messages import ERROR +from .messages import ERROR -class HTTPError(StandardError): +class HTTPError(Exception): """Base class for all errors, client or server side. """ @@ -54,6 +54,33 @@ class Unauthorized(ClientError): title = 'Unauthorized' +class TokenAuthorizationException(Unauthorized): + + code = 401 + message = None + error = "token_error" + + def to_dict(self): + return { + "error": self.error, + "message": self.message, + "code": self.code + } + + +class InvalidJWTSignatureException(TokenAuthorizationException): + + message = MSG("Signature du jeton invalide") + error = "invalid_token_signature" + + +class JWTExpiredException(TokenAuthorizationException): + + message = MSG("Jeton expiré") + error = "token_expired" + + + class Forbidden(ClientError): code = 403 title = 'Forbidden' @@ -99,13 +126,13 @@ class ServiceUnavailable(ServerError): title = 'Service Unavailable' -class FormError(StandardError): +class FormError(Exception): """Raised when a form is invalid (missing or invalid fields). """ def __init__(self, message=None, missing=False, invalid=False, missings=freeze([]), invalids=freeze([]), - messages=freeze([]), code=400): + messages=freeze([]), values=freeze([]), code=400, **kw): self.msg = message self.missing = missing self.invalid = invalid @@ -113,6 +140,8 @@ def __init__(self, message=None, missing=False, invalid=False, self.invalids = invalids self.messages = messages self.code = code + self.values = values + self.kw = kw def get_messages(self): @@ -148,10 +177,13 @@ def __str__(self): def to_dict(self): - return { - 'msg': self.get_message(), + namespace = { + 'msg': self.msg, 'messages': self.messages, 'missing': self.missing, 'invalid': self.invalid, 'missings': self.missings, - 'invalids': self.invalids} + 'invalids': self.invalids, + } + namespace.update(self.kw) + return namespace diff --git a/itools/web/headers.py b/itools/web/headers.py index 3b770cd0f..5f4612bf2 100644 --- a/itools/web/headers.py +++ b/itools/web/headers.py @@ -34,7 +34,7 @@ UNEXPECTED_CHAR = 'unexpected character "%s"' -ctls = set([ chr(x) for x in range(32) ] + [chr(127)]) +ctls = set([chr(x) for x in range(32)] + [chr(127)]) tspecials = set('()<>@,;:\\"/[]?={} \t') ctls_tspecials = ctls | tspecials white_space = ' \t' @@ -44,7 +44,6 @@ def lookup_char(char, data): return (data and data[0] == char) - def read_char(char, data): if not data: raise ValueError('unexpected end-of-data') @@ -53,7 +52,6 @@ def read_char(char, data): return data[1:] - def read_opaque(data, delimiters): index = 0 n = len(data) @@ -68,7 +66,6 @@ def read_opaque(data, delimiters): return data, '' - def read_white_space(data): index = 0 n = len(data) @@ -84,7 +81,6 @@ def read_white_space(data): return data, '' - def read_token(data): n = len(data) if n == 0: @@ -106,7 +102,6 @@ def read_token(data): return data, '' - def read_quoted_string(data): """Implementes quoted-strings as defined by RFC 2068 Section 2.2. Returns the value of the quoted string, and the continuation. @@ -131,14 +126,12 @@ def read_quoted_string(data): raise ValueError('expected double-quote (") not found') - def read_token_or_quoted_string(data): if data[0] == '"': return read_quoted_string(data) return read_token(data) - def read_parameter(data): # name name, data = read_token(data) @@ -152,7 +145,6 @@ def read_parameter(data): return (name, value), data - def read_parameters(data, read_parameter=read_parameter): parameters = {} while data: @@ -173,7 +165,6 @@ def read_parameters(data, read_parameter=read_parameter): return parameters, '' - def read_media_type(data): type, data = read_token(data) data = read_char('/', data) @@ -245,7 +236,6 @@ def read_cookie_parameter(data): return (name, value), data - class Cookie(object): __hash__ = None @@ -268,7 +258,6 @@ def __init__(self, name, value, comment=None, domain=None, max_age=None, # Not standard self.expires = expires - def __eq__(self, other): names = ['value', 'comment', 'domain', 'max_age', 'path', 'secure', 'version', 'commenturl', 'discard', 'port', 'expires'] @@ -277,7 +266,6 @@ def __eq__(self, other): return False return True - def __str__(self): output = ['"%s"' % self.value] if self.path is not None: @@ -287,7 +275,6 @@ def __str__(self): return '; '.join(output) - CACHE_COOKIES = LRUCache(200) class CookieDataType(DataType): @@ -476,7 +463,7 @@ def decode(data): @staticmethod def encode(value): value, parameters = value - parameters = [ '; %s="%s"' % x for x in parameters.items() ] + parameters = ['; %s="%s"' % x for x in parameters.items()] parameters = ''.join(parameters) return '%s%s' % (value, parameters) @@ -485,12 +472,14 @@ def encode(value): class Authorization(DataType): """RFC 7617 The 'Basic' HTTP Authentication Scheme""" - @staticmethod - def decode(data): + auth_types = ['bearer', 'basic'] + + @classmethod + def decode(cls, data): auth_type, credentials = read_token(data) if not credentials or not auth_type: raise ValueError('missing value') - if auth_type.lower() != 'basic': + if auth_type.lower() not in cls.auth_types: raise ValueError('unexpected authorization type "%s"' % auth_type) return auth_type, credentials diff --git a/itools/web/router.py b/itools/web/router.py old mode 100644 new mode 100755 index 38434586e..112318e8b --- a/itools/web/router.py +++ b/itools/web/router.py @@ -16,21 +16,20 @@ # Import from the Standard Library from copy import copy -from datetime import timedelta -from sys import exc_clear +from logging import getLogger from types import FunctionType, MethodType # Import from itools from itools.core import local_tz from itools.database import ReadonlyError -from itools.log import log_error from itools.uri import decode_query, Reference # Local imports -from exceptions import ClientError, NotModified, Forbidden, NotFound -from exceptions import Unauthorized, FormError, ServiceUnavailable -from exceptions import MethodNotAllowed +from .exceptions import ClientError, NotModified, Forbidden, NotFound +from .exceptions import Unauthorized, FormError, ServiceUnavailable +from .exceptions import MethodNotAllowed +log = getLogger("itools.web") ########################################################################### @@ -49,7 +48,6 @@ class RequestMethod(object): - @classmethod def check_access(cls, context): """Tell whether the user is allowed to access the view @@ -66,7 +64,6 @@ def check_access(cls, context): raise Forbidden - @classmethod def check_cache(cls, context): """Implement cache if your method supports it. @@ -108,11 +105,11 @@ def commit_transaction(cls, context): # Warning: Context.commit on GET is not recommended if context.method not in ('POST', 'DELETE', 'PUT', 'PATCH'): # Warning in case of commiting on a GET - print('WARNING: context.commit=True is not recommended') + log.debug("WARNING: context.commit=True is not recommended") # Save changes try: database.save_changes() - except Exception: + except Exception as e: cls.internal_server_error(context) @@ -192,20 +189,17 @@ def handle_request(cls, context): context.view_method = getattr(context.view, context.method, None) # Check the client's cache cls.check_cache(context) - except ClientError, error: + except ClientError as error: has_error = True cls.handle_client_error(error, context) except NotModified: context.http_not_modified() return - except Exception: + except Exception as e: has_error = True cls.internal_server_error(context) finally: - # Fucking Python. Clear the exception, otherwise a later call - # to the logging system will print an exception that has been - # handled already. - exc_clear() + pass # Deserialize the query and the form view = context.view @@ -217,7 +211,7 @@ def handle_request(cls, context): context.path_query = view.get_path_query(context) # Uri query context.query = view.get_query(context) - except FormError, error: + except FormError as error: # If the query is invalid we consider that URL do not exist. # Otherwise anybody can create many empty webpages, # which is very bad for SEO. @@ -235,7 +229,7 @@ def handle_request(cls, context): try: cls.get_form(context) method = cls.check_subview_method(context, method) - except FormError, error: + except FormError as error: context.form_error = error method = context.view.on_form_error except Exception: @@ -245,18 +239,18 @@ def handle_request(cls, context): if not has_error and method: try: context.entity = method(context.resource, context) - except ClientError, error: + except ClientError as error: cls.handle_client_error(error, context) except ReadonlyError: error = ServiceUnavailable cls.handle_client_error(error, context) - except FormError, error: + except FormError as error: context.form_error = error cls.handle_client_error(error, context) except NotModified: context.http_not_modified() return - except Exception: + except Exception as e: cls.internal_server_error(context) else: # Ok: set status @@ -274,7 +268,7 @@ def handle_request(cls, context): except NotModified: context.http_not_modified() return - except Exception: + except Exception as e: cls.internal_server_error(context) else: cls.set_status_from_entity(context) @@ -283,15 +277,9 @@ def handle_request(cls, context): # (6) After Traverse hook try: context.site_root.after_traverse(context) - except Exception: + except Exception as e: cls.internal_server_error(context) - # Cookies for authentification - session_timeout = context.get_session_timeout() - if context.user and session_timeout != timedelta(0): - cookie = context.get_cookie('iauth') - context._set_auth_cookie(cookie) - # (7) Build and return the response context.set_response_from_context() @@ -303,7 +291,7 @@ def handle_client_error(cls, error, context): accept = context.get_header('accept') if content_type: content_type, type_parameters = content_type - accept_json = accept == 'application/json' + accept_json = 'application/json' in accept # Manage error code if error.code == 405: # Add allow methods in case of 405 @@ -339,10 +327,9 @@ def set_status_from_entity(cls, context): context.status = 200 - @classmethod def internal_server_error(cls, context): - log_error('Internal Server Error', domain='itools.web') + log.error("Internal Server Error", exc_info=True) context.status = 500 context.set_content_type('text/html', charset='UTF-8') context.entity = context.root.internal_server_error(context) @@ -381,4 +368,3 @@ def check_subview_method(cls, context, method): # Return subview method view = context.view.get_action_view(context, context.form_action) return getattr(view, context.method) - diff --git a/itools/web/server.py b/itools/web/server.py deleted file mode 100644 index 0e9d4b52b..000000000 --- a/itools/web/server.py +++ /dev/null @@ -1,29 +0,0 @@ -# -*- coding: UTF-8 -*- -# Copyright (C) 2005-2012 J. David Ibáñez -# Copyright (C) 2006-2009 Hervé Cauwelier -# Copyright (C) 2007-2008 Henry Obein -# Copyright (C) 2008 Gautier Hayoun -# Copyright (C) 2008, 2010-2011 David Versmisse -# Copyright (C) 2009 Sylvain Taverne -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -# Import from itools -from itools.log import Logger - - -class AccessLogger(Logger): - - def format(self, domain, level, message): - return message diff --git a/itools/web/static.py b/itools/web/static.py old mode 100644 new mode 100755 index 76621a2ec..ffa2dbe1a --- a/itools/web/static.py +++ b/itools/web/static.py @@ -22,7 +22,8 @@ from itools.core import fixed_offset from itools.fs.common import get_mimetype from itools.uri import Path -from itools.web import BaseView, NotModified +from .exceptions import NotModified +from .views import BaseView class StaticView(BaseView): diff --git a/itools/web/utils.py b/itools/web/utils.py old mode 100644 new mode 100755 index 75f147ddd..8593d3a61 --- a/itools/web/utils.py +++ b/itools/web/utils.py @@ -18,10 +18,12 @@ from datetime import datetime, date, time from decimal import Decimal from json import JSONEncoder +import types # Import from itools from itools.gettext import MSG -from itools.html import stream_to_str_as_html +from itools.html import stream_to_str_as_html, XHTMLFile +from itools.uri import Reference from itools.xml import XMLParser @@ -87,7 +89,7 @@ def fix_json(obj): TODO Use a custom JSONDecoder instead. """ obj_type = type(obj) - if obj_type is unicode: + if obj_type is str: return obj.encode('utf-8') if obj_type is list: return [ fix_json(x) for x in obj ] @@ -115,4 +117,12 @@ def default(self, o): return o.gettext() elif isinstance(o, XMLParser): return stream_to_str_as_html(o) + elif isinstance(o, XHTMLFile): + return stream_to_str_as_html(o.events) + elif isinstance(o, types.GeneratorType): + return list(o) + elif isinstance(o, set): + return list(o) + elif isinstance(o, Reference): + return str(o) return JSONEncoder.default(self, o) diff --git a/itools/web/views.py b/itools/web/views.py index d77127ac2..3fb24a8ea 100644 --- a/itools/web/views.py +++ b/itools/web/views.py @@ -33,38 +33,41 @@ from itools.uri import Reference # Import from here -from exceptions import FormError, Conflict, MethodNotAllowed -from messages import ERROR +from .exceptions import FormError, Conflict, MethodNotAllowed +from .messages import ERROR def process_form(get_value, schema, error_msg=None): missings = [] invalids = [] - messages = [] + messages = {} unknow = [] values = {} for name in schema: datatype = schema[name] try: values[name] = get_value(name, type=datatype) - except FormError, e: + except FormError as e: if e.missing: missings.append(name) elif e.invalid: invalids.append(name) else: unknow.append(name) - messages.extend(e.messages) + messages[name] = e.get_messages() + values[name] = None if missings or invalids or unknow: - error_msg = error_msg or ERROR(u'Form values are invalid') + error_msg = error_msg or ERROR('Form values are invalid') raise FormError( message=error_msg, - missing=len(missings)>0, - invalid=len(invalids)>0, + missing=len(missings) > 0, + invalid=len(invalids) > 0, messages=messages, missings=missings, - invalids=invalids) + invalids=invalids, + values=values + ) return values @@ -184,7 +187,7 @@ def get_schema(self, resource, context): return self.schema - form_error_message = ERROR(u'There are errors, check below') + form_error_message = ERROR('There are errors, check below') def _get_form(self, resource, context): """Form checks the request form and collect inputs consider the schema. This method also checks the request form and raise an @@ -192,8 +195,8 @@ def _get_form(self, resource, context): or a value is not valid) or None if everything is ok. Its input data is a list (fields) that defines the form variables to - {'toto': Unicode(mandatory=True, multiple=False, default=u'toto'), - 'tata': Unicode(mandatory=True, multiple=False, default=u'tata')} + {'toto': Unicode(mandatory=True, multiple=False, default='toto'), + 'tata': Unicode(mandatory=True, multiple=False, default='tata')} """ get_value = context.get_form_value schema = self.get_schema(resource, context) @@ -235,7 +238,7 @@ def on_query_error(self, resource, context): def on_query_error_default(self, resource, context): - message = MSG(u'The query could not be processed.').gettext() + message = MSG('The query could not be processed.').gettext() return message.encode('utf-8') @@ -282,7 +285,7 @@ def get_canonical_uri(self, context): # Remove noise from query parameters canonical_query_parameters = self.canonical_query_parameters - for parameter in query.keys(): + for parameter in list(query.keys()): if parameter not in canonical_query_parameters: del query[parameter] uri.query = query @@ -363,7 +366,7 @@ def get_template(self, resource, context): template = self.template if template is None: msg = "%s is missing the 'template' variable" - raise NotImplementedError, msg % repr(self.__class__) + raise NotImplementedError(msg % repr(self.__class__)) # Case 1: a path to a file somewhere template_type = type(template) @@ -380,7 +383,7 @@ def get_template(self, resource, context): # Error error = 'unexpected type "%s" for the template' % template_type - raise TypeError, error + raise TypeError(error) def GET(self, resource, context): @@ -405,8 +408,8 @@ def get_namespace(self, resource, context, query=None): an HTML form. Its input data is a dictionnary that defines the form variables to consider: - {'toto': Unicode(mandatory=True, multiple=False, default=u'toto'), - 'tata': Unicode(mandatory=True, multiple=False, default=u'tata')} + {'toto': Unicode(mandatory=True, multiple=False, default='toto'), + 'tata': Unicode(mandatory=True, multiple=False, default='tata')} Every element specifies the datatype of the field. The output is like: @@ -431,7 +434,7 @@ def get_namespace(self, resource, context, query=None): if submit and not is_readonly: try: value = context.get_form_value(name, type=datatype) - except FormError, err: + except FormError as err: error = err.get_message(mode='text') if issubclass(datatype, Enumerate): value = datatype.get_namespace(None) @@ -450,7 +453,7 @@ def get_namespace(self, resource, context, query=None): else: try: value = self.get_value(resource, context, name, datatype) - except FormError, err: + except FormError as err: error = err.get_message(mode='text') if issubclass(datatype, Enumerate): value = datatype.get_namespace(None) diff --git a/itools/workflow/__init__.py b/itools/workflow/__init__.py index 3710cc979..48fc920e3 100644 --- a/itools/workflow/__init__.py +++ b/itools/workflow/__init__.py @@ -15,7 +15,7 @@ # along with this program. If not, see . # Import from itools -from workflow import WorkflowError, Workflow, WorkflowAware +from .workflow import WorkflowError, Workflow, WorkflowAware __all__ = ['WorkflowError', 'Workflow', 'WorkflowAware'] diff --git a/itools/workflow/workflow.py b/itools/workflow/workflow.py old mode 100644 new mode 100755 index cffa4e241..7337045b7 --- a/itools/workflow/workflow.py +++ b/itools/workflow/workflow.py @@ -83,7 +83,7 @@ def set_initstate(self, name): """Sets the default initial state. """ if name not in self.states: - raise WorkflowError, "invalid initial state: '%s'" % name + raise WorkflowError("invalid initial state: '%s'" % name) self.initstate = name @@ -100,11 +100,11 @@ def add_trans(self, name, state_from, state_to, **kw): try: state_from = self.states[state_from] except KeyError: - raise WorkflowError, "unregistered state: '%s'" % state_from + raise WorkflowError("unregistered state: '%s'" % state_from) try: state_to = self.states[state_to] except KeyError: - raise WorkflowError, "unregistered state: '%s'" % state_to + raise WorkflowError("unregistered state: '%s'" % state_to) state_from.add_trans(name, transition) @@ -213,10 +213,10 @@ def enter_workflow(self, workflow=None, initstate=None, *args, **kw): initstate = self.workflow.initstate if not initstate: - raise WorkflowError, 'undefined initial state' + raise WorkflowError('undefined initial state') if not initstate in self.workflow.states: - raise WorkflowError, "invalid initial state: '%s'" % initstate + raise WorkflowError("invalid initial state: '%s'" % initstate) self.workflow_state = initstate @@ -240,7 +240,7 @@ def do_trans(self, transname, *args, **kw): state = workflow.states[self.workflow_state] if transname not in state.transitions: error = "transition '%s' is invalid from state '%s'" - raise WorkflowError, error % (transname, self.workflow_state) + raise WorkflowError(error % (transname, self.workflow_state)) # Get the new state name state = state.transitions[transname].state_to diff --git a/itools/xliff/__init__.py b/itools/xliff/__init__.py index 5933d3664..f52f96d17 100644 --- a/itools/xliff/__init__.py +++ b/itools/xliff/__init__.py @@ -18,7 +18,7 @@ # Import from itools from itools.core import add_type, get_abspath from itools.xml import register_dtd -from xliff import XLFFile, XLFUnit, XLFNote, File +from .xliff import XLFFile, XLFUnit, XLFNote, File __all__ = ['XLFFile', 'XLFUnit', 'XLFNote', 'File'] diff --git a/itools/xliff/schema.py b/itools/xliff/schema.py old mode 100644 new mode 100755 diff --git a/itools/xliff/xliff.py b/itools/xliff/xliff.py old mode 100644 new mode 100755 index 22fa5f5fa..e86e8628a --- a/itools/xliff/xliff.py +++ b/itools/xliff/xliff.py @@ -73,7 +73,7 @@ def to_str(self): att = ['%s="%s"' % (k, self.attributes[k]) for k in self.attributes.keys() if k != 'space'] s.append(' \n') else: @@ -227,7 +227,7 @@ def _load_state_from_file(self, file): elif event == COMMENT: pass elif event == TEXT: - text = unicode(value, 'UTF-8') + text = value if phrase is not None: phrase.append((srx_TEXT, text)) diff --git a/itools/xml/__init__.py b/itools/xml/__init__.py index 6f1739795..ba39270ab 100644 --- a/itools/xml/__init__.py +++ b/itools/xml/__init__.py @@ -17,18 +17,17 @@ # along with this program. If not, see . # Import from itools -import dublin_core -from namespaces import XMLNamespace, xml_uri, xmlns_uri -from namespaces import register_namespace, get_namespace, has_namespace -from namespaces import ElementSchema, get_element_schema, get_attr_datatype -from namespaces import is_empty -from parser import XMLParser, DocType, register_dtd, XMLError, XML_DECL -from parser import DOCUMENT_TYPE, START_ELEMENT, END_ELEMENT, TEXT, COMMENT -from parser import PI, CDATA -from utils import is_xml_stream, xml_to_text -from xml import Element, stream_to_str, get_element, find_end -from xml import get_qname, get_attribute_qname, get_end_tag, get_doctype - +from . import dublin_core +from .namespaces import XMLNamespace, xml_uri, xmlns_uri +from .namespaces import register_namespace, get_namespace, has_namespace +from .namespaces import ElementSchema, get_element_schema, get_attr_datatype +from .namespaces import is_empty +from .parser import XMLParser, DocType, register_dtd, XMLError, XML_DECL +from .parser import DOCUMENT_TYPE, START_ELEMENT, END_ELEMENT, TEXT, COMMENT +from .parser import PI, CDATA +from .utils import is_xml_stream, xml_to_text +from .xml import Element, stream_to_str, get_element, find_end +from .xml import get_qname, get_attribute_qname, get_end_tag, get_doctype __all__ = [ diff --git a/itools/xml/dublin_core.py b/itools/xml/dublin_core.py index c7c161a21..f8416aa60 100644 --- a/itools/xml/dublin_core.py +++ b/itools/xml/dublin_core.py @@ -18,7 +18,7 @@ # Import from itools from itools.datatypes import Unicode, String, ISODateTime -from namespaces import XMLNamespace, register_namespace, ElementSchema +from .namespaces import XMLNamespace, register_namespace, ElementSchema dc_attributes = { diff --git a/itools/xml/namespaces.py b/itools/xml/namespaces.py index 0109ba78e..f502c910b 100644 --- a/itools/xml/namespaces.py +++ b/itools/xml/namespaces.py @@ -22,7 +22,7 @@ # Import from itools from itools.core import proto_lazy_property, prototype from itools.datatypes import String -from parser import XMLError +from .parser import XMLError """ diff --git a/itools/xml/parser.c b/itools/xml/parser.c index b637418f0..eed17a2fd 100644 --- a/itools/xml/parser.c +++ b/itools/xml/parser.c @@ -57,7 +57,7 @@ struct _Parser gint source_type; union { - gchar *cursor; + const gchar *cursor; FILE *file; } source; gint source_row; @@ -332,10 +332,10 @@ _parser_error (Parser * parser, ErrorEvent * event, gchar * msg) gchar * -intern_string (gchar * str) +intern_string (const gchar * str) { HStrTree *node; - gchar *cursor; + const gchar *cursor; node = intern_strings_tree; @@ -401,7 +401,7 @@ parser_search_namespace (Parser * parser, gchar * prefix) void -parser_push_namespace (Parser * parser, gchar * prefix, gchar * uri) +parser_push_namespace (Parser * parser, gchar * prefix, const gchar * uri) { Namespace *namespace; @@ -1310,7 +1310,7 @@ parser_read_BOM (Parser * parser) *************************************************************************/ Parser * -parser_new (gchar * data, FILE * file, DocType * doctype) +parser_new (const gchar * data, FILE * file, DocType * doctype) { Parser *parser; @@ -1409,7 +1409,7 @@ parser_free (Parser * parser) void -parser_add_namespace (Parser * parser, gchar * prefix, gchar * uri) +parser_add_namespace (Parser * parser, const gchar * prefix, const gchar * uri) { parser_push_namespace (parser, intern_string (prefix), uri); } diff --git a/itools/xml/parser.h b/itools/xml/parser.h index 2b301b942..ad6e35dd7 100644 --- a/itools/xml/parser.h +++ b/itools/xml/parser.h @@ -159,14 +159,14 @@ typedef union /* if data != NULL source = data * else source = file */ -Parser *parser_new (gchar * data, FILE * file, DocType * doctype); +Parser *parser_new (const gchar * data, FILE * file, DocType * doctype); void parser_free (Parser * parser); /********************************************** * Add a prefix/namespace in namespaces table * **********************************************/ -void parser_add_namespace (Parser * parser, gchar * prefix, gchar * uri); +void parser_add_namespace (Parser * parser, const gchar * prefix, const gchar * uri); /********************* diff --git a/itools/xml/pyparser.c b/itools/xml/pyparser.c index 134da048c..55d0a4cc9 100644 --- a/itools/xml/pyparser.c +++ b/itools/xml/pyparser.c @@ -67,7 +67,7 @@ PyDocType_dealloc (PyDocType * self) if (self->doctype) doctype_free (self->doctype); - self->ob_type->tp_free ((PyObject *) self); + Py_TYPE(self)->tp_free ((PyObject *) self); } @@ -110,7 +110,7 @@ PyDocType_init (PyDocType * self, PyObject * args, PyObject * kwds) static PyObject * PyDocType_to_str (PyDocType * self, PyObject * trash1, PyObject * trash2) { - return PyString_FromString (doctype_to_str (self->doctype)); + return PyUnicode_FromString (doctype_to_str (self->doctype)); } @@ -135,8 +135,7 @@ static PyMethodDef PyDocType_methods[] = { }; static PyTypeObject PyDocTypeType = { - PyObject_HEAD_INIT - (NULL) 0, /* ob_size */ + PyVarObject_HEAD_INIT(NULL, 0) /* ob_size */ "itools.xml.parser.DocType", /* tp_name */ sizeof (PyDocType), /* tp_basicsize */ 0, /* tp_itemsize */ @@ -199,7 +198,7 @@ XMLParser_intern_string (gchar * str) return Py_None; } - return PyString_FromString ((char *) str); + return PyUnicode_FromString ((char *) str); } @@ -226,12 +225,12 @@ XMLParser_translate_Decl (XMLParser * self) PyObject *version, *encoding, *standalone, *result; /* Version */ - version = PyString_FromString (event->version); + version = PyUnicode_FromString (event->version); if (version == NULL) return NULL; /* Encoding */ - encoding = PyString_FromString (event->encoding); + encoding = PyUnicode_FromString (event->encoding); if (encoding == NULL) { Py_DECREF (version); @@ -246,7 +245,7 @@ XMLParser_translate_Decl (XMLParser * self) } else { - standalone = PyString_FromString (event->standalone); + standalone = PyUnicode_FromString (event->standalone); if (standalone == NULL) { Py_DECREF (version); @@ -277,7 +276,7 @@ XMLParser_translate_DocType (XMLParser * self) PyObject *name, *result; /* Name */ - name = PyString_FromString (event->name); + name = PyUnicode_FromString (event->name); if (!name) return NULL; @@ -346,7 +345,7 @@ XMLParser_translate_STag (XMLParser * self) return NULL; } /* The value */ - value = PyString_FromString ((char *) (attribute->value->str)); + value = PyUnicode_FromString ((char *) (attribute->value->str)); if (value == NULL) { Py_DECREF (attributes); @@ -470,7 +469,7 @@ static void XMLParser_dealloc (XMLParser * self) { XMLParser_reset (self); - self->ob_type->tp_free ((PyObject *) self); + Py_TYPE(self)->tp_free ((PyObject *) self); } @@ -491,7 +490,7 @@ XMLParser_init (XMLParser * self, PyObject * args, PyObject * kwds) Parser *parser; PyObject *py_prefix, *py_uri; - char *prefix, *uri; + const char *prefix, *uri; Py_ssize_t pos = 0; @@ -518,23 +517,18 @@ XMLParser_init (XMLParser * self, PyObject * args, PyObject * kwds) } /* Check the source */ - if (PyString_CheckExact (source)) + if (PyUnicode_CheckExact (source)) { /* Create the parser object */ - parser = parser_new (PyString_AsString (source), NULL, doctype); - } - else if (PyFile_CheckExact (source)) - { - /* Set the buffer size */ - PyFile_SetBufSize (source, BUFFER_SIZE); - - /* Create the parser object */ - parser = parser_new (NULL, PyFile_AsFile (source), doctype); + parser = parser_new (PyUnicode_AsUTF8 (source), NULL, doctype); } else { - PyErr_SetString (PyExc_TypeError, "argument 1 must be string or file"); - return -1; + int fd = PyObject_AsFileDescriptor(source); + if (fd == -1) + return -1; + + parser = parser_new (NULL, fdopen(fd, "w"), doctype); } /* End of the creation of the parser object */ @@ -563,10 +557,10 @@ XMLParser_init (XMLParser * self, PyObject * args, PyObject * kwds) if (py_prefix == Py_None) prefix = ""; else - prefix = PyString_AsString (py_prefix); + prefix = PyUnicode_AsUTF8 (py_prefix); /* Keep the URI. */ - uri = PyString_AsString (py_uri); + uri = PyUnicode_AsUTF8 (py_uri); /* prefix and uri should be two strings */ if (!prefix || !uri) @@ -615,7 +609,7 @@ XMLParser_iternext (XMLParser * self) case TEXT: case COMMENT: case CDATA: - value = PyString_FromString (self->event.text_event.text); + value = PyUnicode_FromString (self->event.text_event.text); break; case PI: value = XMLParser_translate_PI (self); @@ -623,7 +617,7 @@ XMLParser_iternext (XMLParser * self) case END_DOCUMENT: return NULL; default: - value = PyString_FromString ("Not implemented"); + value = PyUnicode_FromString ("Not implemented"); } if (value == NULL) @@ -646,8 +640,7 @@ XMLParser_iternext (XMLParser * self) * Declaration of XMLParser * ****************************/ static PyTypeObject XMLParserType = { - PyObject_HEAD_INIT - (NULL) 0, /* ob_size */ + PyVarObject_HEAD_INIT(NULL, 0) /* ob_size */ "itools.xml.parser.XMLParser", /* tp_name */ sizeof (XMLParser), /* tp_basicsize */ 0, /* tp_itemsize */ @@ -736,9 +729,17 @@ static PyMethodDef module_methods[] = { #define PyMODINIT_FUNC void #endif +static struct PyModuleDef moduledef = { + PyModuleDef_HEAD_INIT, + "parser", /* name of module */ + "usage: Combinations.uniqueCombinations(lstSortableItems, comboSize)\n", /* module documentation, may be NULL */ + -1, /* size of per-interpreter state of the module, or -1 if the module keeps state in global variables. */ + module_methods +}; + /* Declaration */ PyMODINIT_FUNC -initparser (void) +PyInit_parser(void) { /* TODO Make verifications / destructions ... */ PyObject *module; @@ -747,26 +748,26 @@ initparser (void) XMLParserType.tp_iter = PyObject_SelfIter; /* Register parser */ - module = Py_InitModule3 ("parser", module_methods, "Low-level XML parser"); + module = PyModule_Create(&moduledef); if (module == NULL) - return; + return NULL; /* Register XMLParser */ if (PyType_Ready (&XMLParserType) != 0) - return; + return NULL; Py_INCREF (&XMLParserType); PyModule_AddObject (module, "XMLParser", (PyObject *) & XMLParserType); /* Register DocType (PyDocType) */ if (PyType_Ready (&PyDocTypeType) != 0) - return; + return NULL; Py_INCREF (&PyDocTypeType); PyModule_AddObject (module, "DocType", (PyObject *) & PyDocTypeType); /* Register exceptions */ XMLError = PyErr_NewException ("itools.xml.parser.XMLError", - PyExc_StandardError, NULL); + PyExc_Exception, NULL); Py_INCREF (XMLError); PyModule_AddObject (module, "XMLError", XMLError); @@ -779,4 +780,6 @@ initparser (void) PyModule_AddIntConstant (module, "COMMENT", COMMENT); PyModule_AddIntConstant (module, "PI", PI); PyModule_AddIntConstant (module, "CDATA", CDATA); + + return module; } diff --git a/itools/xml/utils.py b/itools/xml/utils.py old mode 100644 new mode 100755 index b4e07d46b..e0b3ec392 --- a/itools/xml/utils.py +++ b/itools/xml/utils.py @@ -19,7 +19,7 @@ from types import GeneratorType # Import from itools -from parser import XMLParser, TEXT, XML_DECL +from .parser import XMLParser, TEXT, XML_DECL @@ -39,4 +39,4 @@ def xml_to_text(stream): text.append(value) elif event == XML_DECL: encoding = value[1] - return unicode(' '.join(text), encoding) + return str(' '.join(text), encoding) diff --git a/itools/xml/xml.py b/itools/xml/xml.py index 30956e85e..2f69f4084 100644 --- a/itools/xml/xml.py +++ b/itools/xml/xml.py @@ -21,8 +21,8 @@ # Import from itools from itools.datatypes import XMLAttribute, XMLContent -from namespaces import get_namespace, is_empty -from parser import START_ELEMENT, END_ELEMENT +from .namespaces import get_namespace, is_empty +from .parser import START_ELEMENT, END_ELEMENT # Serialize @@ -116,7 +116,7 @@ def stream_to_str_xmldecl(value): # XXX encoding is not used def stream_to_str(stream, encoding='UTF-8', map=stream_to_str_map): - return ''.join( map[x](y) for x, y, z in stream ) + return ''.join(map[x](y) for x, y, z in stream) diff --git a/itools/xmlfile/__init__.py b/itools/xmlfile/__init__.py index 9e73e9f2b..9edf5b0fd 100644 --- a/itools/xmlfile/__init__.py +++ b/itools/xmlfile/__init__.py @@ -17,8 +17,8 @@ # along with this program. If not, see . # Import from itools -from i18n import get_units, translate -from xmlfile import XMLFile +from .i18n import get_units, translate +from .xmlfile import XMLFile __all__ = [ diff --git a/itools/xmlfile/i18n.py b/itools/xmlfile/i18n.py index e8a337bfb..8e6ba3b72 100644 --- a/itools/xmlfile/i18n.py +++ b/itools/xmlfile/i18n.py @@ -28,7 +28,7 @@ from itools.xml import XMLParser, XMLError, DOCUMENT_TYPE, XML_DECL from itools.xml import START_ELEMENT, END_ELEMENT, TEXT, COMMENT from itools.xml import stream_to_str -from itools.xmlfile.errors import TranslationError +from .errors import TranslationError @@ -51,10 +51,11 @@ def _get_attr_context(datatype, tag_name, attr_name): def _make_start_format(tag_uri, tag_name, attributes, encoding): # We must search for translatable attributes - result = [(u'<%s' % get_qname(tag_uri, tag_name), False, None)] + result = [('<%s' % get_qname(tag_uri, tag_name), False, None)] for attr_uri, attr_name in attributes: qname = get_attribute_qname(attr_uri, attr_name) + qname = Unicode.decode(qname, encoding=encoding) value = attributes[(attr_uri, attr_name)] value = Unicode.decode(value, encoding=encoding) @@ -63,17 +64,17 @@ def _make_start_format(tag_uri, tag_name, attributes, encoding): datatype = get_attr_datatype(tag_uri, tag_name, attr_uri, attr_name, attributes) if issubclass(datatype, Unicode): - result[-1] = (result[-1][0] + u' %s="' % qname, False, None) + result[-1] = (result[-1][0] + ' %s="' % qname, False, None) context = _get_attr_context(datatype, tag_name, attr_name) result.append((value, True, context)) - result.append((u'"', False, None)) + result.append(('"', False, None)) else: - result[-1] = (result[-1][0] + u' %s="%s"' % (qname, value), False, None) + result[-1] = (result[-1][0] + ' %s="%s"' % (qname, value), False, None) # Close the start tag if is_empty(tag_uri, tag_name): - result[-1] = (result[-1][0] + u'/>', False, None) + result[-1] = (result[-1][0] + '/>', False, None) else: - result[-1] = (result[-1][0] + u'>', False, None) + result[-1] = (result[-1][0] + '>', False, None) return result @@ -92,13 +93,12 @@ def _get_translatable_blocks(events): message = Message() skip_level = 0 for event in events: - type, value, line = event - + xml_type, value, line = event # Set the good encoding - if type == XML_DECL: + if xml_type == XML_DECL: encoding = value[1] # And now, we catch only the good events - elif type == START_ELEMENT: + elif xml_type == START_ELEMENT: if skip_level > 0: skip_level += 1 if stream: @@ -107,7 +107,6 @@ def _get_translatable_blocks(events): else: tag_uri, tag_name, attributes = value schema = get_element_schema(tag_uri, tag_name) - # Context management if schema.context is not None: context_stack.append(schema.context) @@ -131,7 +130,7 @@ def _get_translatable_blocks(events): skip_level = 1 stream = [event] continue - elif type == END_ELEMENT: + elif xml_type == END_ELEMENT: if skip_level > 0: skip_level -= 1 if stream: @@ -139,7 +138,6 @@ def _get_translatable_blocks(events): if skip_level == 0: id += 1 aux = stream_to_str(stream, encoding) - aux = unicode(aux, encoding) aux = [(aux, False, context_stack[-1])] message.append_start_format(aux, id, line) message.append_end_format([], id, line) @@ -158,25 +156,22 @@ def _get_translatable_blocks(events): message.append_end_format([(get_end_tag(value), False, None)], id_stack.pop(), line) continue - elif type == TEXT: + elif xml_type == TEXT: # Not empty ? if stream: stream.append(event) continue elif skip_level == 0 and (value.strip() != '' or message): value = XMLContent.encode(value) - value = unicode(value, encoding) message.append_text(value, line, context_stack[-1]) continue - elif type == COMMENT: + elif xml_type == COMMENT: if stream: stream.append(event) continue elif message: id += 1 - if isinstance(value, str): - value = unicode(value, encoding) - value = u'' % value + value = '' % value message.append_start_format([(value, False, None)], id, line) message.append_end_format([], id, line) continue @@ -198,8 +193,8 @@ def _get_translatable_blocks(events): def get_units(events, srx_handler=None): keep_spaces = False keep_spaces_level = 0 - for type, value, line in _get_translatable_blocks(events): - if type == START_ELEMENT: + for xml_type, value, line in _get_translatable_blocks(events): + if xml_type == START_ELEMENT: tag_uri, tag_name, attributes = value # Attributes for attr_uri, attr_name in attributes: @@ -219,7 +214,7 @@ def get_units(events, srx_handler=None): if schema.keep_spaces: keep_spaces = True keep_spaces_level += 1 - elif type == END_ELEMENT: + elif xml_type == END_ELEMENT: # Keep spaces ? tag_uri, tag_name = value schema = get_element_schema(tag_uri, tag_name) @@ -227,7 +222,7 @@ def get_units(events, srx_handler=None): keep_spaces_level -= 1 if keep_spaces_level == 0: keep_spaces = False - elif type == MESSAGE: + elif xml_type == MESSAGE: # Segmentation for segment in get_segments(value, keep_spaces, srx_handler): yield segment @@ -246,18 +241,17 @@ def translate(events, catalog, srx_handler=None): namespaces = {} for event in _get_translatable_blocks(events): - type, value, line = event - + xml_type, value, line = event # Set the good encoding - if type == XML_DECL: + if xml_type == XML_DECL: encoding = value[1] yield event # Store the current DTD - elif type == DOCUMENT_TYPE: + elif xml_type == DOCUMENT_TYPE: name, doctype = value yield event # GO ! - elif type == START_ELEMENT: + elif xml_type == START_ELEMENT: tag_uri, tag_name, attributes = value # Attributes (translate) for attr_uri, attr_name in attributes: @@ -286,7 +280,7 @@ def translate(events, catalog, srx_handler=None): if schema.keep_spaces: keep_spaces = True keep_spaces_level += 1 - elif type == END_ELEMENT: + elif xml_type == END_ELEMENT: yield event # Keep spaces ? tag_uri, tag_name = value @@ -295,7 +289,7 @@ def translate(events, catalog, srx_handler=None): keep_spaces_level -= 1 if keep_spaces_level == 0: keep_spaces = False - elif type == MESSAGE: + elif xml_type == MESSAGE: try: translation = translate_message(value, catalog, keep_spaces, srx_handler) @@ -303,11 +297,11 @@ def translate(events, catalog, srx_handler=None): # translate_message can raise an KeyError in case of translations mistake raise TranslationError(line=line) try: - for event in XMLParser(translation.encode(encoding), + for event in XMLParser(translation, namespaces, doctype=doctype): yield event except XMLError: - raise XMLError, ('please have a look in your source file, ' - 'line ~ %d:\n%s') % (line, value.to_str()) + raise XMLError(('please have a look in your source file, ' + 'line ~ %d:\n%s') % (line, value.to_str())) else: yield event diff --git a/itools/xmlfile/xmlfile.py b/itools/xmlfile/xmlfile.py old mode 100644 new mode 100755 index 356fddc17..33752afa0 --- a/itools/xmlfile/xmlfile.py +++ b/itools/xmlfile/xmlfile.py @@ -20,12 +20,12 @@ # along with this program. If not, see . # Import from the Standard Library -from cStringIO import InputType +from io import StringIO # Import from itools from itools.handlers import TextFile, register_handler_class from itools.xml import XMLParser, stream_to_str, xml_to_text -from i18n import get_units, translate +from .i18n import get_units, translate @@ -61,6 +61,8 @@ def _load_state_from_file(self, file): def load_state_from_string(self, string): self.reset() + if isinstance(string, bytes): + string = string.decode("utf-8") stream = XMLParser(string) self.events = list(stream) @@ -81,10 +83,8 @@ def set_events(self, events): self.events = events - def __cmp__(self, other): - if not isinstance(other, self.__class__): - return 1 - return cmp(self.events, other.events) + def __eq__(self, other): + return isinstance(other, self.__class__) and self.events == other.events def to_text(self): diff --git a/requirements.txt b/requirements.txt old mode 100644 new mode 100755 index 2a6144aed..933d750fb --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,8 @@ -psutil==5.6.6 -pytz==2010h -xlrd==0.6.1 -pygit2==0.28.2 -python-magic==0.4.10 -junitxml -gevent +cryptography +gevent==21.12.0 +junitxml==0.7 +pillow +pygit2==1.9.0 +python-magic==0.4.25 +pytz +xlrd==2.0.1 diff --git a/scripts/idb-inspect.py b/scripts/idb-inspect.py index 7b2115f69..a6b758cab 100644 --- a/scripts/idb-inspect.py +++ b/scripts/idb-inspect.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # -*- coding: UTF-8 -*- # Copyright (C) 2009-2010 David Versmisse # Copyright (C) 2010 J. David Ibáñez @@ -34,7 +33,7 @@ def get_db(path): try: return Database(path) except DatabaseOpeningError: - print 'Bad DB, sorry' + print('Bad DB, sorry') exit(1) @@ -57,16 +56,16 @@ def get_regexp(regexp): if regexp is not None: try: return compile(regexp) - except Exception, error: - print 'Your regexp "%s" is invalid: %s' % (regexp, str(error)) + except Exception as e: + print('Your regexp "%s" is invalid: %s' % (regexp, str(e))) exit(1) return None def dump_summary(db, metadata): - print 'Summary' - print '=======' - print + print('Summary') + print('=======') + print() print (' * You have %d document(s) stocked in your ' 'database. ') % db.get_doccount() @@ -77,40 +76,40 @@ def dump_summary(db, metadata): stored += 1 if 'prefix' in info: indexed += 1 - print ' * %d field(s) (%d stored, %d indexed).' % (total, stored, indexed) - print ' * key field: "abspath"' + print(' * %d field(s) (%d stored, %d indexed).' % (total, stored, indexed)) + print(' * key field: "abspath"') def dump_fields(db, metadata, docs, only_field, show_values, show_terms): - print 'FIELDS' - print '======' - print + print('FIELDS') + print('======') + print() for name, info in metadata.iteritems(): if only_field is not None and not only_field.match(name): continue - print name - print '-'*len(name) + print(name) + print('-'*len(name)) # Info if name == 'abspath': - print ' * key field' + print(' * key field') if 'value' in info: - print ' * stored (%d)' % info['value'] + print(' * stored (%d)' % info['value']) else: - print ' * not stored' + print(' * not stored') if 'prefix' in info: - print ' * indexed (%s)' % info['prefix'] + print(' * indexed (%s)' % info['prefix']) else: - print ' * not indexed' + print(' * not indexed') # Values if 'value' in info and show_values: value = info['value'] - print ' * raw values:' + print(' * raw values:') for doc in docs: - print ' "%s"' % doc.get_value(value) + print(' "%s"' % doc.get_value(value)) # Terms if 'prefix' in info and show_terms: @@ -118,18 +117,18 @@ def dump_fields(db, metadata, docs, only_field, show_values, show_terms): prefix_size = len(prefix) terms = set([ t.term[prefix_size:] for t in db.allterms(prefix) ]) - print ' * raw terms:' + print(' * raw terms:') for term in terms: - print ' "%s"' % term + print(' "%s"' % term) - print + print() def dump_docs(db, metadata, docs, only_doc, only_field, show_values, show_terms): - print 'DOCUMENTS' - print '=========' - print + print('DOCUMENTS') + print('=========') + print() # Prepare the good docs if only_doc is not None: @@ -146,8 +145,8 @@ def dump_docs(db, metadata, docs, only_doc, only_field, show_values, # Show the documents for doc in show_docs: title = 'document id#%d' % doc.get_docid() - print title - print '-'*len(title) + print(title) + print('-'*len(title)) terms = [term.term for term in doc] for name, info in metadata.iteritems(): @@ -155,21 +154,21 @@ def dump_docs(db, metadata, docs, only_doc, only_field, show_values, continue if show_values or show_terms: - print ' * %s:' % name + print(' * %s:' % name) # Value if 'value' in info and show_values: - print ' - raw value: "%s"' % doc.get_value(info['value']) + print(' - raw value: "%s"' % doc.get_value(info['value'])) # Terms if 'prefix' in info and show_terms: prefix = info['prefix'] prefix_size = len(prefix) - print ' - raw terms:' + print(' - raw terms:') for term in terms: if term.startswith(prefix): - print ' "%s"' % term[prefix_size:] - print + print(' "%s"' % term[prefix_size:]) + print() diff --git a/scripts/igettext-build.py b/scripts/igettext-build.py index 518a83021..0fdc0955c 100644 --- a/scripts/igettext-build.py +++ b/scripts/igettext-build.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # -*- coding: UTF-8 -*- # Copyright (C) 2003-2007, 2010 J. David Ibáñez # Copyright (C) 2007 Sylvain Taverne @@ -55,14 +54,14 @@ def build(parser): try: translate = handler.translate except AttributeError: - print 'Error: Unable to translate "%s", unsupported format.' % source + print('Error: Unable to translate "%s", unsupported format.' % source) return # Load the Catalog handler (check the API) catalog = ro_database.get_handler(catalog_name) try: catalog.gettext except AttributeError: - print ('Error: The file "%s" is not a supported ' + print('Error: The file "%s" is not a supported ' 'catalog.') % catalog_name return # Translate diff --git a/scripts/igettext-extract.py b/scripts/igettext-extract.py index bc4eae51c..3dd478ecb 100644 --- a/scripts/igettext-extract.py +++ b/scripts/igettext-extract.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # -*- coding: UTF-8 -*- # Copyright (C) 2003-2008, 2010 J. David Ibáñez # Copyright (C) 2007 Sylvain Taverne @@ -90,6 +89,6 @@ # Output if options.output is None: - print data + print(data) else: open(options.output, 'w').write(data) diff --git a/scripts/igettext-merge.py b/scripts/igettext-merge.py index c0d61cb99..f0a52fb2a 100644 --- a/scripts/igettext-merge.py +++ b/scripts/igettext-merge.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # -*- coding: UTF-8 -*- # Copyright (C) 2003-2009, 2011 J. David Ibáñez # diff --git a/scripts/ipkg-docs.py b/scripts/ipkg-docs.py index d7d53e458..d23d6a1e2 100644 --- a/scripts/ipkg-docs.py +++ b/scripts/ipkg-docs.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # -*- coding: UTF-8 -*- # Copyright (C) 2010 J. David Ibáñez # @@ -34,7 +33,7 @@ def run(command): else: command_str = ' '.join(command) # Print - print command_str + print(command_str) # Call return call(command) @@ -84,9 +83,9 @@ def make_html(): make_figures('png') # HTML command = sphinx.format(mode='html') - print run(command) + print(run(command)) # Ok - print 'The HTML pages are in docs/_build/html' + print('The HTML pages are in docs/_build/html') def make_pdf(): @@ -94,11 +93,11 @@ def make_pdf(): make_figures('eps') # Latex command = sphinx.format(mode='latex') - print run(command) + print(run(command)) # PDF chdir('_build/latex') - print run('make all-pdf') - print 'The PDF is available in docs/_build/latex' + print(run('make all-pdf')) + print('The PDF is available in docs/_build/latex') def make_release(): @@ -109,7 +108,7 @@ def make_release(): chdir('_build/html') call('zip -r %s.zip *' % pkgname, shell=True) call('mv %s.zip /tmp/' % pkgname, shell=True) - print 'The tarball is available in /tmp/%s.zip' % pkgname + print('The tarball is available in /tmp/%s.zip' % pkgname) if __name__ == '__main__': diff --git a/scripts/ipkg-quality.py b/scripts/ipkg-quality.py index fa1712e1e..cdcf3d63f 100644 --- a/scripts/ipkg-quality.py +++ b/scripts/ipkg-quality.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # -*- coding: utf-8 -*- # Copyright (C) 2007 David Versmisse # Copyright (C) 2007-2009, 2011 J. David Ibáñez @@ -237,10 +236,10 @@ def analyse_file_by_tokens(filename, ignore_errors): for plugin in plugins: if plugin.analyse_token(token, value, srow, scol): stats[plugin.key].append(srow) - except TokenError, e: + except TokenError as e: if ignore_errors is False: raise e - print e + print(e) return {'tokens': 0} return stats @@ -273,10 +272,10 @@ def analyse_file_by_ast(filename, ignore_errors): """ try: ast = parse(open(filename).read()) - except (SyntaxError, IndentationError), e: + except (SyntaxError, IndentationError) as e: if ignore_errors is False: raise e - print e + print(e) return None visitor = Visitor() visitor.generic_visit(ast) @@ -341,10 +340,10 @@ def analyse(filenames, ignore_errors=False): def print_list(title, string_list): if len(string_list) != 0: - print title - print '-'*len(title) + print(title) + print('-'*len(title)) for line in string_list: - print line + print(line) print @@ -363,11 +362,11 @@ def print_worses(db, worse, criteria): for f in db[:number]: if sort_key(f) != 0: if first: - print 'Worse files:' + print('Worse files:') first = False - print '- %s (%d)' % (f['filename'], sort_key(f)) + print('- %s (%d)' % (f['filename'], sort_key(f))) if not first: - print + print() def show_lines(filenames): @@ -376,7 +375,7 @@ def show_lines(filenames): # We get statistics stats, files_db = analyse(filenames) # We show lines - print + print() comments = [] infos = files_db[0] for problem in problems.keys(): @@ -387,9 +386,9 @@ def show_lines(filenames): comments.append('%s +%d' % (filenames[0], line)) comments.append('\n') if comments: - print '\n'.join(comments) + print('\n'.join(comments)) else: - print u'This file is perfect !' + print('This file is perfect !') def show_stats(filenames, worse): @@ -398,10 +397,10 @@ def show_stats(filenames, worse): # We get statistics stats, files_db = analyse(filenames) # Show quality summary - print - print 'Code length: %d lines, %d tokens' % (stats['lines'], - stats['tokens']) - print + print() + print('Code length: %d lines, %d tokens' % (stats['lines'], + stats['tokens'])) + print() # Aesthetics (and readibility) show_comments = [] @@ -524,9 +523,9 @@ def fix(filenames): # (2) Fix elif options.fix is True: show_stats(filenames, options.worse) - print 'FIXING...' + print('FIXING...') fix(filenames) - print 'DONE' + print('DONE') show_stats(filenames, options.worse) # (3) Analyse diff --git a/scripts/ipkg-update-locale.py b/scripts/ipkg-update-locale.py index c3fcd56fb..9517946ff 100644 --- a/scripts/ipkg-update-locale.py +++ b/scripts/ipkg-update-locale.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python # -*- coding: UTF-8 -*- # Copyright (C) 2006, 2010 Hervé Cauwelier # Copyright (C) 2006-2010 J. David Ibáñez diff --git a/setup.conf b/setup.conf old mode 100644 new mode 100755 index 9b79a0055..d00141c19 --- a/setup.conf +++ b/setup.conf @@ -35,7 +35,7 @@ classifiers = " # Packages package_root = itools packages = "core csv database database/backends datatypes fs gettext handlers html i18n ical - log loop odf office pdf pkg python relaxng rss srx stl tmx uri validators + loop odf office pdf pkg python relaxng rss srx stl tmx uri validators web workflow xliff xml xmlfile" # Requires diff --git a/setup.py b/setup.py old mode 100644 new mode 100755 index 26c8b15a2..13d9d1a57 --- a/setup.py +++ b/setup.py @@ -15,13 +15,16 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see . +from __future__ import print_function + +from setuptools import setup # Import from the Standard Library from distutils.core import Extension -from distutils.core import setup from os.path import join as join_path from pip._internal.req import parse_requirements from sys import stderr +import sys from subprocess import Popen, PIPE @@ -31,7 +34,7 @@ def get_pipe(command, cwd=None): popen = Popen(command, stdout=PIPE, stderr=PIPE, cwd=cwd) stdoutdata, stderrdata = popen.communicate() if popen.returncode != 0: - raise EnvironmentError, (popen.returncode, stderrdata) + raise EnvironmentError((popen.returncode, stderrdata)) return stdoutdata @@ -47,7 +50,7 @@ def get_compile_flags(command): for line in data.splitlines(): for token in line.split(): - flag, value = token[:2], token[2:] + flag, value = token[:2].decode("utf-8"), token[2:].decode("utf-8") if flag == '-I': include_dirs.append(value) elif flag == '-f': @@ -78,7 +81,7 @@ def generate_mo_files(po_file_names): Popen(['msgfmt', po_file, '-o', mo_file]) except OSError: # Check msgfmt is properly installed - print >> stderr, "[ERROR] 'msgfmt' not found, aborting..." + print("[ERROR] 'msgfmt' not found, aborting...", file=stderr) return [] mo_files.append(mo_file) return mo_files @@ -86,9 +89,12 @@ def generate_mo_files(po_file_names): if __name__ == '__main__': itools_is_available = False + #from itools.core import get_abspath + #from itools.pkg.utils import setup as itools_setup try: + # TODO FIXME Try Except hide import errors with Python 3 from itools.core import get_abspath - from itools.pkg import setup as itools_setup + from itools.pkg.utils import setup as itools_setup itools_is_available = True print('[OK] itools is available') except ImportError: @@ -96,7 +102,7 @@ def generate_mo_files(po_file_names): pass ext_modules = [] - filenames = [x.strip() for x in open('MANIFEST').readlines() ] + filenames = [x.strip() for x in open('MANIFEST').readlines()] if not itools_is_available: # In case itools is not yet install, build won't work # thus we need to make sure mo files will be generated @@ -109,18 +115,19 @@ def generate_mo_files(po_file_names): # XML Parser try: flags = get_compile_flags('pkg-config --cflags --libs glib-2.0') + print(flags) except OSError: - print >> stderr, "[ERROR] 'pkg-config' not found, aborting..." + print("[ERROR] 'pkg-config' not found, aborting...", file=stderr) raise - except EnvironmentError: - err = '[ERROR] Glib 2.0 library or headers not found, aborting...' - print >> stderr, err + except Exception: + print("[ERROR] Glib 2.0 library or headers not found, aborting...", file=stderr) raise else: sources = [ 'itools/xml/parser.c', 'itools/xml/doctype.c', 'itools/xml/arp.c', 'itools/xml/pyparser.c'] - extension = Extension('itools.xml.parser', sources, **flags) + print("flags", sources, flags) + extension = Extension('itools.xml.parser', sources=sources, **flags) ext_modules.append(extension) # PDF indexation @@ -128,8 +135,7 @@ def generate_mo_files(po_file_names): flags = get_compile_flags( 'pkg-config --cflags --libs "poppler >= 0.20.0" fontconfig') except EnvironmentError: - err = "[WARNING] poppler headers not found, PDF indexation won't work" - print >> stderr, err + print("[WARNING] poppler headers not found, PDF indexation won't work", file=stderr) else: sources = ['itools/pdf/pdftotext.cc'] extension = Extension('itools.pdf.pdftotext', sources, **flags) @@ -139,13 +145,15 @@ def generate_mo_files(po_file_names): try: flags = get_compile_flags('wv2-config --cflags --libs') except EnvironmentError: - err = "[WARNING] wv2 not found, DOC indexation won't work" - print >> stderr, err + print("[WARNING] wv2 not found, DOC indexation won't work", file=stderr) else: sources = ['itools/office/doctotext.cc'] extension = Extension('itools.office.doctotext', sources, **flags) ext_modules.append(extension) + # if sys.version_info[0] == 3: + # ext_modules = [] + # Ok if itools_is_available: itools_setup(get_abspath(''), ext_modules=ext_modules) @@ -185,7 +193,6 @@ def generate_mo_files(po_file_names): "itools.html", "itools.i18n", "itools.ical", - "itools.log", "itools.loop", "itools.odf", "itools.office", @@ -236,8 +243,8 @@ def generate_mo_files(po_file_names): license="GNU General Public License (GPL)", url="http://www.hforge.org/itools", description=description, - long_description=None, - classifiers = classifiers, + long_description="", + classifiers=classifiers, install_requires=install_requires, # Packages packages=packages, diff --git a/test/bench_xml.py b/test/bench_xml.py old mode 100644 new mode 100755 index 87d1765d3..bd8d503a8 --- a/test/bench_xml.py +++ b/test/bench_xml.py @@ -99,11 +99,11 @@ def get_test_filenames(test_path, force_download): If the test files does'nt exists, we download it """ - uris = {'http://download.wikimedia.org/qualitywiki/latest': + uris = {'https://dumps.wikimedia.org/qualitywiki/latest/': [('qualitywiki-latest-stub-articles.xml', '.gz'), #~ 3.1 KB ('qualitywiki-latest-stub-meta-current.xml', '.gz'), #~ 11.0 KB ('qualitywiki-latest-stub-meta-history.xml', '.gz')], #~ 28.9 KB - 'http://download.wikimedia.org/tawiki/latest': + 'https://dumps.wikimedia.org/tawiki/latest/': [('tawiki-latest-stub-articles.xml', '.gz'), #~ 1.2 MB ('tawiki-latest-stub-meta-history.xml', '.gz')], #~ 7.3 MB 'http://www.w3.org/XML/Test/': [('xmlts20080205', '.tar.gz')] @@ -112,13 +112,13 @@ def get_test_filenames(test_path, force_download): if force_download is True: if lfs.exists(compressed_dir_path): - print 'Remove compressed directory ', compressed_dir_path + print('Remove compressed directory ', compressed_dir_path) lfs.remove(compressed_dir_path) for names in uris.itervalues(): for (name, ext) in names: path = join(test_path, name) if lfs.exists(path): - print 'Remove %s file' % path + print('Remove %s file' % path) lfs.remove(path) # test directory @@ -132,7 +132,7 @@ def get_test_filenames(test_path, force_download): lfs.open(compressed_dir_path) test_dir_filenames = lfs.get_names(test_path) - for base_uri, names in uris.iteritems(): + for base_uri, names in uris.items(): for (name, ext) in names: if test_dir_filenames.count(name): continue @@ -140,9 +140,9 @@ def get_test_filenames(test_path, force_download): # check if tarball already exists if lfs.exists(compressed_dest) is False: src = join(base_uri, '%s%s' % (name, ext)) - print 'GET %s file' % src + print('GET %s file' % src) if lfs.exists(src) is False: - print "%s uri does not exists" % src + print("%s uri does not exists" % src) continue src_file = lfs.open(src) # save Gzip file @@ -150,7 +150,7 @@ def get_test_filenames(test_path, force_download): compressed_dest_file.write(src_file.read()) compressed_dest_file.close() src_file.close() - print 'Extract file %s' % compressed_dest + print('Extract file %s' % compressed_dest) # Uncompressed File Path if name == 'xmlts20080205': # uncompress only xmlconf.xml file @@ -189,11 +189,11 @@ def get_test_filenames(test_path, force_download): ########################################################################### def output_init(parsers_name): - print u'-' * 78 + print(u'-' * 78) # 30c | 23c | 23c -> 78c - print u' %s|%s|%s' % (center(u'file', 30), center(parser_names[0], 23), - center(parser_names[1], 23)) - print u'-' * 78 + print(u' %s|%s|%s' % (center(u'file', 30), center(parser_names[0], 23), + center(parser_names[1], 23))) + print(u'-' * 78) @@ -225,16 +225,16 @@ def output_result(results, file): # time_spent ok already like ___.___ ms or s or mn output2 = rjust(u'%s / %s' % (time_spent, memory), 21) - print '%s | %s | %s ' % (file_string, output1, output2) + print('%s | %s | %s ' % (file_string, output1, output2)) ##################################################################### # MAIN ##################################################################### parser_scripts = { - 'expat': './bench_xml_expat.py', - 'itools': './bench_xml_itools.py', - 'itools_c': './a.out', + 'expat': 'test/bench_xml_expat.py', + 'itools': 'test/bench_xml_itools.py', + 'itools_c': 'test/a.out', } @@ -254,7 +254,7 @@ def output_result(results, file): parser.add_option( '-d', '--directory', help=('An optional directory to which to extract files.'), - default='/tmp/itools_bench', dest='test_dir') + default='test/bench', dest='test_dir') parser.add_option( '', '--force-download', help='Force the test files download.', @@ -263,7 +263,7 @@ def output_result(results, file): options, args = parser.parse_args() # Garbage collector if options.no_gc is True: - print u'DISABLE GARBAGE COLLECTOR' + print('DISABLE GARBAGE COLLECTOR') gc.disable() # Get test files @@ -275,7 +275,7 @@ def output_result(results, file): else: parser_names = ['itools', 'expat'] # parser_names = ['itools_c', 'itools'] - output_init(parser_names); + output_init(parser_names) # Go for real_path, filename, file_bytes, file_size in filenames: @@ -300,5 +300,5 @@ def output_result(results, file): else: test_results.append((parser_name, None)) output_result(test_results, (filename, file_bytes)) - print + print() diff --git a/test/handlers/test.tar.gz b/test/handlers/test.tar.gz old mode 100644 new mode 100755 diff --git a/test/test.py b/test/test.py old mode 100644 new mode 100755 index b0d685753..c51039041 --- a/test/test.py +++ b/test/test.py @@ -29,7 +29,8 @@ import test_core import test_csv import test_dispatcher -import test_database +# Test database commentés par l'auteur +# import test_database import test_datatypes import test_gettext import test_handlers @@ -50,11 +51,12 @@ import test_xml import test_xmlfile -test_modules = [test_core, test_csv, test_database, test_datatypes, test_dispatcher, +test_modules = [test_core, test_csv, test_datatypes, test_dispatcher, test_gettext, test_handlers, test_html, test_i18n, test_ical, test_odf, test_rss, test_srx, test_stl, test_tmx, test_uri, test_fs, test_validators, test_web, test_workflow, test_xliff, test_xml, test_xmlfile] +#test_modules = [test_core, test_csv, test_dispatcher, test_datatypes] loader = TestLoader() @@ -72,7 +74,7 @@ elif options.mode == 'junitxml': path = get_abspath('./junit.xml') print('Result is here: %s' % path) - f = file(path, 'wb') + f = open(path, 'wb') result = JUnitXmlResult(f) result.startTestRun() ret = suite.run(result) diff --git a/test/test_core.py b/test/test_core.py old mode 100644 new mode 100755 index 733c03eec..92b5c6078 --- a/test/test_core.py +++ b/test/test_core.py @@ -15,14 +15,15 @@ # along with this program. If not, see . # Import from the Standard Library -from string import lowercase +from string import ascii_lowercase from unittest import TestCase, main - +import sys # Import from itools from itools.core import freeze, frozenlist, frozendict from itools.core import LRUCache + ########################################################################### # Freeze ########################################################################### @@ -32,21 +33,21 @@ def test_freeze_list(self): a_list = [1, 2, 3] a_frozen_list = freeze(a_list) self.assertEqual(a_frozen_list, a_list) - self.assert_(isinstance(a_frozen_list, frozenlist)) + self.assertTrue(isinstance(a_frozen_list, frozenlist)) def test_freeze_dict(self): a_dict = {'a': 5, 'b': 3} a_frozen_dict = freeze(a_dict) self.assertEqual(a_frozen_dict, a_dict) - self.assert_(isinstance(a_frozen_dict, frozendict)) + self.assertTrue(isinstance(a_frozen_dict, frozendict)) def test_freeze_set(self): a_set = set('abc') a_frozen_set = freeze(a_set) self.assertEqual(a_frozen_set, a_set) - self.assert_(isinstance(a_frozen_set, frozenset)) + self.assertTrue(isinstance(a_frozen_set, frozenset)) @@ -59,11 +60,11 @@ def test_freeze_set(self): class FrozenlistTestCase(TestCase): def test_inheritance(self): - self.assert_(isinstance(a_frozen_list, list)) + self.assertTrue(isinstance(a_frozen_list, list)) def test_identity(self): - self.assert_(freeze(a_frozen_list) is a_frozen_list) + self.assertTrue(freeze(a_frozen_list) is a_frozen_list) ####################################################################### @@ -86,7 +87,7 @@ def test_delitem(self): except TypeError: pass else: - self.assert_(False) + self.assertTrue(False) def test_del_slice(self): @@ -95,7 +96,7 @@ def test_del_slice(self): except TypeError: pass else: - self.assert_(False) + self.assertTrue(False) def test_incremental_add(self): @@ -105,7 +106,7 @@ def test_incremental_add(self): except TypeError: pass else: - self.assert_(False) + self.assertTrue(False) def test_incremental_mul(self): @@ -115,7 +116,7 @@ def test_incremental_mul(self): except TypeError: pass else: - self.assert_(False) + self.assertTrue(False) def test_setitem(self): @@ -124,7 +125,7 @@ def test_setitem(self): except TypeError: pass else: - self.assert_(False) + self.assertTrue(False) def test_setslice(self): @@ -133,7 +134,7 @@ def test_setslice(self): except TypeError: pass else: - self.assert_(False) + self.assertTrue(False) def test_insert(self): @@ -164,13 +165,13 @@ def test_concatenation(self): """ # frozenlist + frozenlist alist = freeze([]) + freeze([]) - self.assert_(isinstance(alist, frozenlist)) + self.assertTrue(isinstance(alist, frozenlist)) # frozenlist + list alist = freeze([]) + [] - self.assert_(isinstance(alist, frozenlist)) + self.assertTrue(isinstance(alist, frozenlist)) # list + frozenlist alist = [] + freeze([]) - self.assert_(not isinstance(alist, frozenlist)) + self.assertTrue(not isinstance(alist, frozenlist)) def test_equality(self): @@ -180,11 +181,11 @@ def test_equality(self): def test_multiplication(self): # frozenlist * n alist = freeze([1, 2]) * 2 - self.assert_(isinstance(alist, frozenlist)) + self.assertTrue(isinstance(alist, frozenlist)) self.assertEqual(alist, [1, 2, 1, 2]) # n * frozenlist alist = 2 * freeze([1, 2]) - self.assert_(isinstance(alist, frozenlist)) + self.assertTrue(isinstance(alist, frozenlist)) self.assertEqual(alist, [1, 2, 1, 2]) @@ -193,20 +194,20 @@ def test_representation(self): -########################################################################### -# Frozen dicts -########################################################################### +# ########################################################################### +# # Frozen dicts +# ########################################################################### a_frozen_dict = freeze({'a': 5, 'b': 3}) class FrozendictTestCase(TestCase): def test_inheritance(self): - self.assert_(isinstance(a_frozen_dict, dict)) + self.assertTrue(isinstance(a_frozen_dict, dict)) def test_identity(self): - self.assert_(freeze(a_frozen_dict) is a_frozen_dict) + self.assertTrue(freeze(a_frozen_dict) is a_frozen_dict) ####################################################################### @@ -221,7 +222,7 @@ def test_delitem(self): except TypeError: pass else: - self.assert_(False) + self.assertTrue(False) def test_setitem(self): @@ -230,7 +231,7 @@ def test_setitem(self): except TypeError: pass else: - self.assert_(False) + self.assertTrue(False) def test_clear(self): @@ -263,15 +264,15 @@ def test_representation(self): self.assertEqual(repr(a_frozen_dict), "frozendict({'a': 5, 'b': 3})") -########################################################################### -# Cache -########################################################################### +# ########################################################################### +# # Cache +# ########################################################################### class CacheTestCase(TestCase): def setUp(self): self.cache = LRUCache(3) - for c in lowercase: + for c in ascii_lowercase: self.cache[c] = c.upper() @@ -313,8 +314,8 @@ def test_delitem(self): def test_in(self): cache = self.cache - self.assert_('x' in cache) - self.assert_('n' not in cache) + self.assertTrue('x' in cache) + self.assertTrue('n' not in cache) def test_clear(self): @@ -349,21 +350,21 @@ def test_items(self): def test_iteritems(self): cache = self.cache - items = cache.iteritems() + items = cache.items() items = list(items) self.assertEqual(items, [('x', 'X'), ('y', 'Y'), ('z', 'Z')]) def test_iterkeys(self): cache = self.cache - keys = cache.iterkeys() + keys = cache.keys() keys = list(keys) self.assertEqual(keys, list('xyz')) def test_itervalues(self): cache = self.cache - values = cache.itervalues() + values = cache.values() values = list(values) self.assertEqual(values, list('XYZ')) diff --git a/test/test_csv.py b/test/test_csv.py old mode 100644 new mode 100755 index 3f10ee848..70bf5f646 --- a/test/test_csv.py +++ b/test/test_csv.py @@ -37,8 +37,6 @@ TEST_SYNTAX_ERROR = '"one",,\n,"two",,\n,,"three"' - - class Languages(CSVFile): columns = ['name', 'url', 'number', 'date'] @@ -54,7 +52,6 @@ class Numbers(CSVFile): schema = {'one': Unicode, 'two': Unicode, 'three': Unicode} - class CSVTestCase(TestCase): def test_unicode(self): @@ -64,13 +61,11 @@ def test_unicode(self): self.assertEqual(rows, [["Martin von Löwis", "Marc André Lemburg", "Guido van Rossum"]]) - def test_num_of_lines(self): handler = CSVFile(string=TEST_DATA_2) rows = list(handler.get_rows()) self.assertEqual(len(rows), 3) - def test_num_of_lines_with_last_new_line(self): data = TEST_DATA_2 + '\r\n' handler = CSVFile(string=data) diff --git a/test/test_database.py b/test/test_database.py old mode 100644 new mode 100755 index 1eb1b074e..662328022 --- a/test/test_database.py +++ b/test/test_database.py @@ -31,7 +31,6 @@ from itools.datatypes import String, Unicode, Boolean, Integer from itools.fs import lfs, FileName from itools.handlers import TextFile -from itools.log.log import register_logger, Logger, FATAL # Import from xapian from xapian import Document as XapianDocument @@ -50,8 +49,6 @@ def to_str(self): # def setUp(self): # self.tearDown() # # Silence the log system -# logger = Logger(min_level=FATAL) -# register_logger(logger, 'itools.database') # # Make database # self.database = make_git_database('fables', 20, 20) # self.database.worktree.git_add('.') @@ -61,7 +58,6 @@ def to_str(self): # # def tearDown(self): # # Restore logging -# register_logger(None, 'itools.database') # # Clean file-system # paths = ['fables/catalog', # 'fables/database/.git', diff --git a/test/test_datatypes.py b/test/test_datatypes.py old mode 100644 new mode 100755 index 4f9425907..8ef2b1154 --- a/test/test_datatypes.py +++ b/test/test_datatypes.py @@ -25,6 +25,7 @@ import random from unittest import TestCase, main + # Import from itools from itools.core import fixed_offset from itools.datatypes import ISOTime, ISOCalendarDate, ISODateTime, HTTPDate @@ -51,20 +52,17 @@ def test_Integer(self): data = Integer.encode(x) self.assertEqual(x, Integer.decode(data)) - def test_Decimal(self): for x in [random.uniform(-100,100) for _ in range(10)]: x = decimal.Decimal(str(x)) data = Decimal.encode(x) self.assertEqual(x, Decimal.decode(data)) - def test_Unicode(self): x = u'العربيه 中文 Español Français' data = Unicode.encode(x) self.assertEqual(x, Unicode.decode(data)) - def test_Boolean(self): for x in [True, False]: data = Boolean.encode(x) diff --git a/test/test_dispatcher.py b/test/test_dispatcher.py old mode 100644 new mode 100755 diff --git a/test/test_fs.py b/test/test_fs.py old mode 100644 new mode 100755 index 598bb153a..7672438cc --- a/test/test_fs.py +++ b/test/test_fs.py @@ -40,7 +40,7 @@ def test_FileName(self): 'toto.gz': ('toto', 'gz', None), 'toto.Z': ('toto', 'Z', None), } - for name, result in map.iteritems(): + for name, result in map.items(): self.assertEqual(FileName.decode(name), result) self.assertEqual(FileName.encode(result), name) @@ -145,14 +145,14 @@ def test_remove_folder(self): def test_open_file(self): - file = lfs.open('tests/hello.txt') + file = lfs.open('tests/hello.txt', text=True) self.assertEqual(file.read(), 'hello world\n') def test_move_file(self): lfs.copy('tests/hello.txt', 'tests/hello.txt.bak') lfs.move('tests/hello.txt.bak', 'tests/hello.txt.old') - file = lfs.open('tests/hello.txt.old') + file = lfs.open('tests/hello.txt.old', text=True) self.assertEqual(file.read(), 'hello world\n') self.assertEqual(lfs.exists('tests/hello.txt.bak'), False) lfs.remove('tests/hello.txt.old') @@ -166,16 +166,15 @@ def test_traverse(self): for x in lfs.traverse('.'): self.assertEqual(lfs.exists(x), True) - def test_append(self): # Initialize - file = lfs.make_file('tests/toto.txt') + file = lfs.make_file('tests/toto.txt', text=True) try: file.write('hello\n') finally: file.close() # Test - file = lfs.open('tests/toto.txt', APPEND) + file = lfs.open('tests/toto.txt', APPEND, text=True) try: file.write('bye\n') finally: @@ -187,13 +186,13 @@ def test_append(self): def test_write_and_truncate(self): # Initialize - file = lfs.make_file('tests/toto.txt') + file = lfs.make_file('tests/toto.txt', text=True) try: file.write('hello\n') finally: file.close() # Test - file = lfs.open('tests/toto.txt', WRITE) + file = lfs.open('tests/toto.txt', WRITE, text=True) try: file.write('bye\n') finally: @@ -250,7 +249,7 @@ def tearDown(self): def test_copy_file(self): lfs.copy('tests/hello.txt', 'tmp/hello.txt.bak') - file = lfs.open('tmp/hello.txt.bak') + file = lfs.open('tmp/hello.txt.bak', text=True) try: self.assertEqual(file.read(), 'hello world\n') finally: @@ -259,7 +258,7 @@ def test_copy_file(self): def test_copy_file_to_folder(self): lfs.copy('tests/hello.txt', 'tmp') - file = lfs.open('tmp/hello.txt') + file = lfs.open('tmp/hello.txt', text=True) try: self.assertEqual(file.read(), 'hello world\n') finally: @@ -268,7 +267,7 @@ def test_copy_file_to_folder(self): def test_copy_folder(self): lfs.copy('tests', 'tmp/xxx') - file = lfs.open('tmp/xxx/hello.txt') + file = lfs.open('tmp/xxx/hello.txt', text=True) try: self.assertEqual(file.read(), 'hello world\n') finally: @@ -277,7 +276,7 @@ def test_copy_folder(self): def test_copy_folder_to_folder(self): lfs.copy('tests', 'tmp') - file = lfs.open('tmp/tests/hello.txt') + file = lfs.open('tmp/tests/hello.txt', text=True) try: self.assertEqual(file.read(), 'hello world\n') finally: diff --git a/test/test_gettext.py b/test/test_gettext.py old mode 100644 new mode 100755 index 03c1910d1..7df097303 --- a/test/test_gettext.py +++ b/test/test_gettext.py @@ -24,7 +24,6 @@ from itools.srx import TEXT - class POTestCase(TestCase): def test_case1(self): @@ -58,7 +57,6 @@ def test_case3(self): self.assertEqual(po.get_msgstr('test'), u'Esto es una "prueba"') - def test_output(self): """Test output.""" content = '# Comment\n' \ @@ -72,7 +70,6 @@ def test_output(self): self.assertEqual((po.get_units())[0].to_str(), content) - def test_fuzzy(self): """Test fuzzy.""" content = '# Comment\n' \ @@ -87,7 +84,6 @@ def test_fuzzy(self): translation = po.gettext(((TEXT, 'Hello'),)) self.assertEqual(translation, ((TEXT, 'Hello'),)) - def test_end_comment(self): """Test end comment.""" content = '#, fuzzy\n' \ @@ -97,7 +93,7 @@ def test_end_comment(self): po.load_state_from_string(content) translation = po.gettext(((TEXT, 'Hello'),)) - self.assertEqual(translation,((TEXT, 'Hello'),)) + self.assertEqual(translation, ((TEXT, 'Hello'),)) diff --git a/test/test_handlers.py b/test/test_handlers.py old mode 100644 new mode 100755 index c04c63349..3b824317b --- a/test/test_handlers.py +++ b/test/test_handlers.py @@ -26,17 +26,15 @@ from itools.fs import lfs - class StateTestCase(TestCase): def test_abort(self): handler = File('tests/hello.txt') - self.assertEqual(handler.data, u'hello world\n') - handler.set_data(u'bye world\n') - self.assertEqual(handler.data, u'bye world\n') + self.assertEqual(handler.data, b'hello world\n') + handler.set_data(b'bye world\n') + self.assertEqual(handler.data, b'bye world\n') handler.abort_changes() - self.assertEqual(handler.data, u'hello world\n') - + self.assertEqual(handler.data, b'hello world\n') class ConfigFileTestCase(TestCase): @@ -47,23 +45,20 @@ def setUp(self): if lfs.exists(self.config_path): lfs.remove(self.config_path) - def tearDown(self): if lfs.exists(self.config_path): lfs.remove(self.config_path) - def _init_test(self, value): # Init data if not lfs.exists(self.config_path): - lfs.make_file(self.config_path) - + f = lfs.make_file(self.config_path, text=True) + f.close() # Write data config = ConfigFile(self.config_path) config.set_value("test", value) config.save_state() - def test_simple_value(self): # Init data value = "HELLO, WORLD!" @@ -77,7 +72,6 @@ def test_simple_value(self): # Test data self.assertEqual(config2_value, value) - def test_long_value(self): # Init data value = "HELLO, WORLD!\n\nHELLO WORLD2222" @@ -87,7 +81,7 @@ def test_long_value(self): config2 = ConfigFile(self.config_path) try: config2_value = config2.get_value("test") - except SyntaxError, e: + except SyntaxError as e: self.fail(e) finally: lfs.remove(self.config_path) @@ -95,7 +89,6 @@ def test_long_value(self): # Test data self.assertEqual(config2_value, value) - def test_last_line_empty(self): # Init data value = "HELLO, WORLD!\n\n" @@ -114,7 +107,6 @@ def test_last_line_empty(self): # Test data self.assertEqual(config2_value, value) - def test_quote_value(self): # Init data value = "HELLO, \"WORLD\"!" @@ -124,7 +116,7 @@ def test_quote_value(self): config = ConfigFile(self.config_path) try: config.set_value("test", value) - except SyntaxError, e: + except SyntaxError as e: self.fail(e) config.save_state() @@ -132,7 +124,7 @@ def test_quote_value(self): config2 = ConfigFile(self.config_path) try: config2_value = config2.get_value("test") - except SyntaxError, e: + except SyntaxError as e: self.fail(e) finally: lfs.remove(self.config_path) @@ -140,8 +132,6 @@ def test_quote_value(self): # Test data self.assertEqual(config2_value, value) - - def test_wrapped_quote_value(self): # Init data value = "\"HELLO, WORLD!\"" @@ -151,7 +141,7 @@ def test_wrapped_quote_value(self): config = ConfigFile(self.config_path) try: config.set_value("test", value) - except SyntaxError, e: + except SyntaxError as e: self.fail(e) config.save_state() @@ -159,7 +149,7 @@ def test_wrapped_quote_value(self): config2 = ConfigFile(self.config_path) try: config2_value = config2.get_value("test") - except SyntaxError, e: + except SyntaxError as e: self.fail(e) finally: lfs.remove(self.config_path) @@ -168,7 +158,6 @@ def test_wrapped_quote_value(self): self.assertEqual(config2_value, value) - ########################################################################### # Archive files ########################################################################### diff --git a/test/test_html.py b/test/test_html.py old mode 100644 new mode 100755 index 94c6838f3..4150c2592 --- a/test/test_html.py +++ b/test/test_html.py @@ -99,7 +99,7 @@ def test_case1(self): doc = HTMLFile(string='

hello world

') messages = [unit[0] for unit in doc.get_units()] - self.assertEqual(messages, [((TEXT, u'hello world'),)]) + self.assertEqual(messages, [((TEXT, 'hello world'),)]) def test_case2(self): @@ -107,7 +107,7 @@ def test_case2(self): doc = HTMLFile(string='The beach') messages = [unit[0] for unit in doc.get_units()] - self.assertEqual(messages, [((TEXT, u'The beach'),)]) + self.assertEqual(messages, [((TEXT, 'The beach'),)]) def test_case3(self): @@ -119,7 +119,7 @@ def test_case3(self): '') messages = [unit[0] for unit in doc.get_units()] - self.assertEqual(messages, [((TEXT, u'Change'),)]) + self.assertEqual(messages, [((TEXT, 'Change'),)]) def test_case4(self): @@ -145,9 +145,8 @@ def test_case5(self): string = doc.translate(po) output = HTMLFile(string=string) - expected = HTMLFile(string='La playa') - self.assertEqual(output, expected) + self.assertEqual(output.to_str(), expected.to_str()) def test_case6(self): @@ -160,7 +159,6 @@ def test_case6(self): 'msgctxt "button"\n' 'msgid "Change"\n' 'msgstr "Cambiar"') - output = HTMLFile(string=doc.translate(p)) expected = HTMLFile(string= diff --git a/test/test_i18n.py b/test/test_i18n.py old mode 100644 new mode 100755 diff --git a/test/test_ical.py b/test/test_ical.py old mode 100644 new mode 100755 index 0214def9e..8dda2a7b8 --- a/test/test_ical.py +++ b/test/test_ical.py @@ -105,29 +105,30 @@ def property_to_string(prop_name, prop): prop_value = prop.value if type(prop.value) is datetime: params = prop.parameters - if params:# and prop.parameters.has_key('VALUE'): - t = params['VALUE'][0] if params.has_key('VALUE') else None + if params: + t = params['VALUE'][0] if 'VALUE' in params else None else: t = None prop_value = DateTime.encode(prop.value, type=t) # Simple case if not prop.parameters: - return u'%s:%s' % (prop_name, prop_value) + return '%s:%s' % (prop_name, prop_value) # Params params = '' for p_name in prop.parameters: p_value = prop.parameters[p_name] - p_value = [ encode_param_value(p_name, x, String) for x in p_value ] + p_value = [encode_param_value(p_name, x, String) for x in p_value] param = ';%s=%s' % (p_name, ','.join(p_value)) params = params + param - return u'%s%s:%s' % (prop_name, params, prop_value) + return '%s%s:%s' % (prop_name, params, prop_value) class icalTestCase(TestCase): def setUp(self): + self.maxDiff = None self.cal1 = iCalendar(string=content) self.cal2 = iCalendar(string=content2) @@ -144,8 +145,8 @@ def test_new(self): # Test properties expected_properties = [ - u'VERSION;None:2.0', - u'PRODID;None:-//hforge.org/NONSGML ikaaro icalendar V1.0//EN'] + 'VERSION;None:2.0', + 'PRODID;None:-//hforge.org/NONSGML ikaaro icalendar V1.0//EN'] self.assertEqual(properties, expected_properties) # Test components @@ -240,11 +241,11 @@ def test_load(self): value = property_value.value property = '%s;%s:%s' % (name, params, value) properties.append(property) - expected_properties = [ - u'VERSION;None:2.0', - u'METHOD;None:PUBLISH', - u'PRODID;None:-//Mozilla.org/NONSGML Mozilla Calendar V1.0//EN' ] + 'VERSION;None:2.0', + 'PRODID;None:-//Mozilla.org/NONSGML Mozilla Calendar V1.0//EN', + 'METHOD;None:PUBLISH', + ] self.assertEqual(properties, expected_properties) # Test component properties @@ -263,20 +264,19 @@ def test_load(self): properties.append(property) expected_event_properties = [ - u'STATUS:TENTATIVE', - u'DTSTAMP:20050601T074604Z', - u'DESCRIPTION:all all all', - u'ATTENDEE;MEMBER="mailto:DEV-GROUP@host2.com"' - ';RSVP=TRUE:mailto:jdoe@itaapy.com', - u'ATTENDEE;MEMBER="mailto:DEV-GROUP@host2.com"' - ':mailto:jsmith@itaapy.com', - u'SUMMARY:Résumé', - u'PRIORITY:1', - u'LOCATION:France', - u'X-MOZILLA-RECUR-DEFAULT-INTERVAL:0', - u'DTEND;VALUE=DATE:20050531', - u'DTSTART;VALUE=DATE:20050530', - u'CLASS:PRIVATE'] + 'SUMMARY:Résumé', + 'DESCRIPTION:all all all', + 'LOCATION:France', + 'STATUS:TENTATIVE', + 'CLASS:PRIVATE', + 'X-MOZILLA-RECUR-DEFAULT-INTERVAL:0', + 'DTSTART;VALUE=DATE:20050530', + 'DTEND;VALUE=DATE:20050531', + 'DTSTAMP:20050601T074604Z', + 'ATTENDEE;RSVP=TRUE;MEMBER="mailto:DEV-GROUP@host2.com":mailto:jdoe@itaapy.com', + 'ATTENDEE;MEMBER="mailto:DEV-GROUP@host2.com":mailto:jsmith@itaapy.com', + 'PRIORITY:1', + ] self.assertEqual(event.uid, '581361a0-1dd2-11b2-9a42-bd3958eeac9a') self.assertEqual(properties, expected_event_properties) @@ -308,9 +308,10 @@ def test_load_2(self): # Test properties expected_properties = [ - u'VERSION;None:2.0', - u'METHOD;None:PUBLISH', - u'PRODID;None:-//Mozilla.org/NONSGML Mozilla Calendar V1.0//EN' ] + 'VERSION;None:2.0', + 'PRODID;None:-//Mozilla.org/NONSGML Mozilla Calendar V1.0//EN', + 'METHOD;None:PUBLISH', + ] self.assertEqual(properties, expected_properties) events = [] @@ -335,24 +336,26 @@ def test_load_2(self): # Test events expected_events = [ - [u'ATTENDEE;MEMBER="mailto:DEV-GROUP@host2.com";RSVP=TRUE' - u':mailto:jdoe@itaapy.com', - u'SUMMARY:222222222', - u'PRIORITY:2', - u'DTEND;VALUE=DATE:20050701', - u'DTSTART;VALUE=DATE:20050701'], - [u'STATUS:TENTATIVE', - u'DESCRIPTION:all all all', - u'ATTENDEE;MEMBER="mailto:DEV-GROUP@host2.com"' - u';RSVP=TRUE:mailto:jdoe@itaapy.com', - u'SUMMARY:Refound', - u'PRIORITY:1', - u'LOCATION:France', - u'X-MOZILLA-RECUR-DEFAULT-INTERVAL:0', - u'DTEND;VALUE=DATE:20050531', - u'DTSTART;VALUE=DATE:20050530', - u'CLASS:PRIVATE'], - ] + [ + 'SUMMARY:Refound', + 'DESCRIPTION:all all all', + 'LOCATION:France', + 'STATUS:TENTATIVE', + 'CLASS:PRIVATE', + 'X-MOZILLA-RECUR-DEFAULT-INTERVAL:0', + 'DTSTART;VALUE=DATE:20050530', + 'DTEND;VALUE=DATE:20050531', + 'ATTENDEE;RSVP=TRUE;MEMBER="mailto:DEV-GROUP@host2.com":mailto:jdoe@itaapy.com', + 'PRIORITY:1', + ], + [ + 'SUMMARY:222222222', + 'DTSTART;VALUE=DATE:20050701', + 'DTEND;VALUE=DATE:20050701', + 'ATTENDEE;RSVP=TRUE;MEMBER="mailto:DEV-GROUP@host2.com":mailto:jdoe@itaapy.com', + 'PRIORITY:2', + ], + ] self.assertEqual(events, expected_events) self.assertEqual(len(cal.get_components('VEVENT')), 2) diff --git a/test/test_odf.py b/test/test_odf.py old mode 100644 new mode 100755 index acd0b63f1..325316e3e --- a/test/test_odf.py +++ b/test/test_odf.py @@ -37,14 +37,14 @@ def setUp(self): def test_get_msg(self): messages = [unit[0] for unit in self.doc.get_units()] - expected = [((TEXT, u'Hello '), (START_FORMAT, 1), (TEXT, u'world'), - (END_FORMAT, 1), (TEXT, u' !')), - ((TEXT, u'Hello world Document'),), - ((TEXT, u"it's a very good document"),), - ((TEXT, u'Itools test'),), - ((TEXT, u'itools'),), - ((TEXT, u'odt'),), - ((TEXT, u'odf'),)] + expected = [((TEXT, 'Hello '), (START_FORMAT, 1), (TEXT, 'world'), + (END_FORMAT, 1), (TEXT, ' !')), + ((TEXT, 'Hello world Document'),), + ((TEXT, "it's a very good document"),), + ((TEXT, 'Itools test'),), + ((TEXT, 'itools'),), + ((TEXT, 'odt'),), + ((TEXT, 'odf'),)] self.assertEqual(messages, expected) @@ -62,29 +62,29 @@ def test_translation(self): # Check if allright expected = [ # content.xml - ((TEXT, u'Hello '), (START_FORMAT, 1), (TEXT, u'world'), - (END_FORMAT, 1), (TEXT, u' !')), + ((TEXT, 'Hello '), (START_FORMAT, 1), (TEXT, 'world'), + (END_FORMAT, 1), (TEXT, ' !')), # meta.xml - ((TEXT, u'Hello world Document'),), - ((TEXT, u"it's a very good document"),), - ((TEXT, u'Itools test'),), - ((TEXT, u'itools'),), - ((TEXT, u'odt'),), - ((TEXT, u'odf'),)] + ((TEXT, 'Hello world Document'),), + ((TEXT, "it's a very good document"),), + ((TEXT, 'Itools test'),), + ((TEXT, 'itools'),), + ((TEXT, 'odt'),), + ((TEXT, 'odf'),)] self.assertEqual(messages, expected) def test_meta(self): - expected_meta = {'initial-creator': u'sylvain', - 'description': u"it's a very good document", - 'keyword': u'itools\nodt\nodf', - 'creator': u'sylvain', - 'title': u'Hello world Document', - 'language': u'fr-FR', - 'creation-date': u'2007-06-01T11:25:20', - 'date': u'2007-06-03T21:26:04', - 'subject': u'Itools test'} + expected_meta = {'initial-creator': 'sylvain', + 'description': "it's a very good document", + 'keyword': 'itools\nodt\nodf', + 'creator': 'sylvain', + 'title': 'Hello world Document', + 'language': 'fr-FR', + 'creation-date': '2007-06-01T11:25:20', + 'date': '2007-06-03T21:26:04', + 'subject': 'Itools test'} meta = self.doc.get_meta() self.assertEqual(expected_meta, meta) @@ -99,10 +99,10 @@ def setUp(self): def test_get_msg(self): messages = [unit[0] for unit in self.doc.get_units()] - expected = [((START_FORMAT, 1), (TEXT, u'Hello '), (END_FORMAT, 1), - (START_FORMAT, 2), (TEXT, u'World'), (END_FORMAT, 2), - (START_FORMAT, 3), (TEXT, u' !'), (END_FORMAT, 3)), - ((TEXT, u'Welcome'),)] + expected = [((START_FORMAT, 1), (TEXT, 'Hello '), (END_FORMAT, 1), + (START_FORMAT, 2), (TEXT, 'World'), (END_FORMAT, 2), + (START_FORMAT, 3), (TEXT, ' !'), (END_FORMAT, 3)), + ((TEXT, 'Welcome'),)] self.assertEqual(messages, expected) @@ -115,30 +115,30 @@ def setUp(self): def test_get_msg(self): messages = [unit[0] for unit in self.doc.get_units()] - expected = [((TEXT, u'Chocolate'),), - ((TEXT, u'Coffee'),), - ((TEXT, u'Tea'),), - ((TEXT, u'Price'),), - ((TEXT, u'80'),), - ((TEXT, u'20'),), - ((TEXT, u'40'),), - ((TEXT, u'Quantity'),), - ((TEXT, u'20'),), - ((TEXT, u'30'),), - ((TEXT, u'20'),), - ((TEXT, u'Quality'),), - ((TEXT, u'0'),), - ((TEXT, u'50'),), - ((TEXT, u'40'),), - ((TEXT, u'-'),), - ((TEXT, u'-'),), - ((TEXT, u'Page'),), - ((TEXT, u'('),), - ((TEXT, u'???'),), - ((TEXT, u')'),), - ((TEXT, u','),), - ((TEXT, u'Page'),), - ((TEXT, u'/'),)] + expected = [((TEXT, 'Chocolate'),), + ((TEXT, 'Coffee'),), + ((TEXT, 'Tea'),), + ((TEXT, 'Price'),), + ((TEXT, '80'),), + ((TEXT, '20'),), + ((TEXT, '40'),), + ((TEXT, 'Quantity'),), + ((TEXT, '20'),), + ((TEXT, '30'),), + ((TEXT, '20'),), + ((TEXT, 'Quality'),), + ((TEXT, '0'),), + ((TEXT, '50'),), + ((TEXT, '40'),), + ((TEXT, '-'),), + ((TEXT, '-'),), + ((TEXT, 'Page'),), + ((TEXT, '('),), + ((TEXT, '???'),), + ((TEXT, ')'),), + ((TEXT, ','),), + ((TEXT, 'Page'),), + ((TEXT, '/'),)] self.assertEqual(messages, expected) @@ -232,12 +232,12 @@ def test_table(self): content = odt_template % content messages = XMLParser(content) messages = [unit[0] for unit in get_units(messages)] - expected= [((TEXT, u'A'),), - ((TEXT, u'B'),), - ((TEXT, u'C'),), - ((TEXT, u'D'),), - ((TEXT, u'E'),), - ((TEXT, u'F'),)] + expected= [((TEXT, 'A'),), + ((TEXT, 'B'),), + ((TEXT, 'C'),), + ((TEXT, 'D'),), + ((TEXT, 'E'),), + ((TEXT, 'F'),)] self.assertEqual(messages, expected) @@ -258,7 +258,7 @@ def test_translation_paragraph(self): messages = XMLParser(content) messages = translate(messages, po) messages = [unit[0] for unit in get_units(messages)] - self.assertEqual(messages, [((TEXT, u'hola mundo'),)]) + self.assertEqual(messages, [((TEXT, 'hola mundo'),)]) if __name__ == '__main__': diff --git a/test/test_rss.py b/test/test_rss.py old mode 100644 new mode 100755 index e26c05595..1577f7a06 --- a/test/test_rss.py +++ b/test/test_rss.py @@ -61,7 +61,7 @@ def test_item(self): self.assertEqual(str(link), 'http://liftoff.msfc.nasa.gov/news/2003/news-starcity.asp') expected = u"How do Americans get ready to work with Russians" - self.assert_(item['description'].startswith(expected)) + self.assertTrue(item['description'].startswith(expected)) def test_serialize(self): diff --git a/test/test_srx.py b/test/test_srx.py old mode 100644 new mode 100755 diff --git a/test/test_stl.py b/test/test_stl.py old mode 100644 new mode 100755 diff --git a/test/test_tmx.py b/test/test_tmx.py old mode 100644 new mode 100755 diff --git a/test/test_uri.py b/test/test_uri.py old mode 100644 new mode 100755 index b96f28a69..2ca8675a5 --- a/test/test_uri.py +++ b/test/test_uri.py @@ -17,6 +17,8 @@ # along with this program. If not, see . # Import from the Standard Library +from __future__ import print_function + from unittest import TestCase, main # Import from itools @@ -224,6 +226,20 @@ def test_relnorm(self): path = Path('../../a//.//b/c') self.assertEqual(str(path), '../../a/b/c') + def test_slice(self): + path = Path('/a/b/c') + self.assertEqual(str(path[1:3]), '/b/c') + self.assertEqual(path[0], 'a') + self.assertEqual(path[1], 'b') + self.assertEqual(path[-1], 'c') + + def test_get_name(self): + path = Path('/a/b/c/;x') + self.assertEqual(path.get_name(), ';x') + self.assertEqual(path.get_name()[0], ';') + + + class ParseTestCase(TestCase): @@ -344,11 +360,11 @@ def test_standard(self): try: self.assertEqual(x, expected) except AssertionError: - print '\n%s + %s = %s != %s' \ - % (self.base, reference, x, expected) + print('\n%s + %s = %s != %s' \ + % (self.base, reference, x, expected)) failure += 1 if failure: - raise AssertionError, '%s uri resolutions failed' % failure + raise AssertionError('%s uri resolutions failed' % failure) def test_others(self): @@ -431,7 +447,7 @@ def test_compare(self): """Compare two Mailto objects with same parameters.""" ob = Mailto(self.address) copy = MailtoDataType.decode(self.uri) - self.assert_(type(ob) is type(copy)) + self.assertTrue(type(ob) is type(copy)) self.assertEqual(ob.username, copy.username) self.assertEqual(ob.host, copy.host) self.assertEqual(str(ob), str(copy)) diff --git a/test/test_validators.py b/test/test_validators.py old mode 100644 new mode 100755 index 8891b3486..784e36f03 --- a/test/test_validators.py +++ b/test/test_validators.py @@ -23,32 +23,27 @@ class ValidatorsTestCase(TestCase): - def test_hexadecimal(self): v = validator('hexadecimal') self.assertEqual(True, v.is_valid('#000000')) - def test_equals(self): v = validator('equals-to', base_value=2) self.assertEqual(True, v.is_valid(2)) self.assertEqual(False, v.is_valid(3)) - def test_integer_positive(self): v = validator('integer-positive') self.assertEqual(True, v.is_valid(0)) self.assertEqual(True, v.is_valid(2)) self.assertEqual(False, v.is_valid(-1)) - def test_integer_positive_not_null(self): v = validator('integer-positive-not-null') self.assertEqual(True, v.is_valid(2)) self.assertEqual(False, v.is_valid(-1)) self.assertEqual(False, v.is_valid(0)) - def test_image_mimetypes(self): v = validator('image-mimetypes') image1 = 'image.png', 'image/png', None diff --git a/test/test_web.py b/test/test_web.py old mode 100644 new mode 100755 diff --git a/test/test_workflow.py b/test/test_workflow.py old mode 100644 new mode 100755 diff --git a/test/test_xliff.py b/test/test_xliff.py old mode 100644 new mode 100755 diff --git a/test/test_xml.py b/test/test_xml.py old mode 100644 new mode 100755 index ca70c32f8..45343c9a9 --- a/test/test_xml.py +++ b/test/test_xml.py @@ -32,7 +32,8 @@ def test_xml_decl(self): data = '' token = XML_DECL value = '1.0', 'UTF-8', None - self.assertEqual(XMLParser(data).next(), (token, value, 1)) + self.assertEqual(next(XMLParser(data)), (token, value, 1)) + ####################################################################### @@ -41,19 +42,19 @@ def test_char_ref(self): data = 'ñ' token = TEXT value = "ñ" - self.assertEqual(XMLParser(data).next(), (token, value, 1)) + self.assertEqual(next(XMLParser(data)), (token, value, 1)) def test_char_ref_hex(self): data = 'ñ' token = TEXT value = "ñ" - self.assertEqual(XMLParser(data).next(), (token, value, 1)) + self.assertEqual(next(XMLParser(data)), (token, value, 1)) def test_char_ref_empty(self): data = '&#;' - self.assertRaises(XMLError, XMLParser(data).next) + self.assertRaises(XMLError, XMLParser(data).__next__) ####################################################################### @@ -62,38 +63,38 @@ def test_element(self): data = '' token = START_ELEMENT value = None, 'a', {} - self.assertEqual(XMLParser(data).next(), (token, value, 1)) + self.assertEqual(next(XMLParser(data)), (token, value, 1)) def test_attributes(self): data = '' token = START_ELEMENT value = None, 'a', {(None, 'href'): 'http://www.hforge.org'} - self.assertEqual(XMLParser(data).next(), (token, value, 1)) + self.assertEqual(next(XMLParser(data)), (token, value, 1)) def test_attributes_single_quote(self): data = "" token = START_ELEMENT value = None, 'a', {(None, 'href'): 'http://www.hforge.org'} - self.assertEqual(XMLParser(data).next(), (token, value, 1)) + self.assertEqual(next(XMLParser(data)), (token, value, 1)) def test_attributes_no_quote(self): data = "" - self.assertRaises(XMLError, XMLParser(data).next) + self.assertRaises(XMLError, XMLParser(data).__next__) def test_attributes_forbidden_char(self): data = '' - self.assertRaises(XMLError, XMLParser(data).next) + self.assertRaises(XMLError, XMLParser(data).__next__) def test_attributes_entity_reference(self): data = '' token = START_ELEMENT value = None, 'img', {(None, 'title'): 'Black & White'} - self.assertEqual(XMLParser(data).next(), (token, value, 1)) + self.assertEqual(next(XMLParser(data)), (token, value, 1)) ####################################################################### @@ -102,7 +103,7 @@ def test_cdata(self): data = '' token = CDATA value = 'Black & White' - self.assertEqual(XMLParser(data).next(), (token, value, 1)) + self.assertEqual(next(XMLParser(data)), (token, value, 1)) ####################################################################### diff --git a/test/test_xmlfile.py b/test/test_xmlfile.py old mode 100644 new mode 100755 diff --git a/test/tests/sample-rss-2.xml b/test/tests/sample-rss-2.xml old mode 100644 new mode 100755 diff --git a/test/tests/test.csv b/test/tests/test.csv old mode 100644 new mode 100755 diff --git a/test/tests/test_adv.csv b/test/tests/test_adv.csv old mode 100644 new mode 100755 diff --git a/test/tests/test_vtimezone.ics b/test/tests/test_vtimezone.ics old mode 100644 new mode 100755