.project
.pydevproject
.settings
+/.tox
+/nosetests.xml
+/htmlcov
Authors
-------
-Originally written by Marek Stępniowski <marek@stepniowski>
-
-Later contributions:
+List of people who have contributed to the project, in chronological order:
+
+* Marek Stępniowski
+* Łukasz Rekucki
+* Radek Czajka
+* Łukasz Anwajler
+* Adam Twardoch
+* Marcin Koziej
+* Michał Górny
+* Aleksander Łukasz
+* Robert Błaut
+* Jan Szejko
+
- * Łukasz Rekucki <lrekucki@gmail.com>
- * Radek Czajka <radek.czajka@gmail.com>
--- /dev/null
+# Change Log
+
+This document records all notable changes to Librarian.
+
+## 1.7 (2019-02-27)
+
+### Added
+- Python 3.4+ support, to existing Python 2.7 support.
+- `coverter_path` argument in `mobi.transform`.
+- Proper packaging info.
+- This changelog.
+- Tox configuration for tests.
+
+### Changed
+- `from_bytes` methods replaced all `from_string` methods,
+ i.e. on: OutputFile, WorkInfo, BookInfo, WLDocument, WLPicture.
+- `get_bytes` replaced `get_string` on OutputFile.
+
+### Removed
+- Shims for Python < 2.7.
--- /dev/null
+include *.md
+include LICENSE
+include NOTICE
+include tox.ini
+recursive-include scripts *.py *.css
+recursive-include tests *.py *.xml *.html *.out *.txt *.jpeg
+include librarian/xslt/*.xslt
+include librarian/xslt/*.xml
+include librarian/epub/*
+include librarian/pdf/*
+include librarian/fb2/*
+include librarian/fonts/*
+graft librarian/res
+graft librarian/font-optimizer
+
![AGPL Logo](http://www.gnu.org/graphics/agplv3-155x51.png)
- Copyright © 2008,2009,2010 Fundacja Nowoczesna Polska <fundacja@nowoczesnapolska.org.pl>
+ Copyright © 2008-2019 Fundacja Nowoczesna Polska <fundacja@nowoczesnapolska.org.pl>
- For full list of contributors see AUTHORS section at the end.
+ For full list of contributors see AUTHORS file.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
Currently we support:
- * HTML4, XHTML 1.0
+ * HTML4, XHTML 1.0 (?)
* Plain text
* EPUB (XHTML based)
+ * MOBI
* print-ready PDF
+ * FB2
Other features:
To extract book fragments marked as "theme":
bookfragments file1.xml [file2.xml ...]
-
-
-Authors
--------
-Originally written by Marek Stępniowski <marek@stepniowski.com>
-
-Later contributions:
-
- * Łukasz Rekucki <lrekucki@gmail.com>
- * Radek Czajka <radek.czajka@gmail.com>
\ No newline at end of file
# This file is part of Librarian, licensed under GNU Affero GPLv3 or later.
# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
#
-from __future__ import with_statement
+from __future__ import print_function, unicode_literals
import os
import re
import shutil
+from tempfile import NamedTemporaryFile
import urllib
-
-from util import makedirs
+from lxml import etree
+import six
+from six.moves.urllib.request import FancyURLopener
+from .util import makedirs
+@six.python_2_unicode_compatible
class UnicodeException(Exception):
def __str__(self):
- """ Dirty workaround for Python Unicode handling problems. """
- return unicode(self).encode('utf-8')
-
- def __unicode__(self):
""" Dirty workaround for Python Unicode handling problems. """
args = self.args[0] if len(self.args) == 1 else self.args
try:
- message = unicode(args)
+ message = six.text_type(args)
except UnicodeDecodeError:
- message = unicode(args, encoding='utf-8', errors='ignore')
+ message = six.text_type(args, encoding='utf-8', errors='ignore')
return message
class ParseError(UnicodeException):
WLNS = EmptyNamespace()
+@six.python_2_unicode_compatible
class WLURI(object):
"""Represents a WL URI. Extracts slug from it."""
slug = None
'(?P<slug>[-a-z0-9]+)/?$')
def __init__(self, uri):
- uri = unicode(uri)
+ uri = six.text_type(uri)
self.uri = uri
self.slug = uri.rstrip('/').rsplit('/', 1)[-1]
def from_slug(cls, slug):
"""Contructs an URI from slug.
- >>> WLURI.from_slug('a-slug').uri
- u'http://wolnelektury.pl/katalog/lektura/a-slug/'
+ >>> print(WLURI.from_slug('a-slug').uri)
+ http://wolnelektury.pl/katalog/lektura/a-slug/
"""
uri = 'http://wolnelektury.pl/katalog/lektura/%s/' % slug
return cls(uri)
- def __unicode__(self):
- return self.uri
-
def __str__(self):
return self.uri
def by_slug(self, slug):
fname = slug + '.xml'
- return open(os.path.join(self.dir, fname))
+ return open(os.path.join(self.dir, fname), 'rb')
-import lxml.etree as etree
-import dcparser
+from . import dcparser
DEFAULT_BOOKINFO = dcparser.BookInfo(
{ RDFNS('about'): u'http://wiki.wolnepodreczniki.pl/Lektury:Template'},
def xinclude_forURI(uri):
e = etree.Element(XINS("include"))
e.set("href", uri)
- return etree.tostring(e, encoding=unicode)
+ return etree.tostring(e, encoding='unicode')
def wrap_text(ocrtext, creation_date, bookinfo=DEFAULT_BOOKINFO):
"""Wrap the text within the minimal XML structure with a DC template."""
bookinfo.created_at = creation_date
dcstring = etree.tostring(bookinfo.to_etree(), \
- method='xml', encoding=unicode, pretty_print=True)
+ method='xml', encoding='unicode', pretty_print=True)
return u'<utwor>\n' + dcstring + u'\n<plain-text>\n' + ocrtext + \
u'\n</plain-text>\n</utwor>'
b = u'' + (element.text or '')
for child in element.iterchildren():
- e = etree.tostring(child, method='xml', encoding=unicode,
+ e = etree.tostring(child, method='xml', encoding='unicode',
pretty_print=True)
b += e
class OutputFile(object):
"""Represents a file returned by one of the converters."""
- _string = None
+ _bytes = None
_filename = None
def __del__(self):
os.unlink(self._filename)
def __nonzero__(self):
- return self._string is not None or self._filename is not None
+ return self._bytes is not None or self._filename is not None
@classmethod
- def from_string(cls, string):
+ def from_bytes(cls, bytestring):
"""Converter returns contents of a file as a string."""
instance = cls()
- instance._string = string
+ instance._bytes = bytestring
return instance
@classmethod
instance._filename = filename
return instance
- def get_string(self):
- """Get file's contents as a string."""
+ def get_bytes(self):
+ """Get file's contents as a bytestring."""
if self._filename is not None:
- with open(self._filename) as f:
+ with open(self._filename, 'rb') as f:
return f.read()
else:
- return self._string
+ return self._bytes
def get_file(self):
"""Get file as a file-like object."""
- if self._string is not None:
- from StringIO import StringIO
- return StringIO(self._string)
+ if self._bytes is not None:
+ return six.BytesIO(self._bytes)
elif self._filename is not None:
- return open(self._filename)
+ return open(self._filename, 'rb')
def get_filename(self):
"""Get file as a fs path."""
if self._filename is not None:
return self._filename
- elif self._string is not None:
- from tempfile import NamedTemporaryFile
+ elif self._bytes is not None:
temp = NamedTemporaryFile(prefix='librarian-', delete=False)
- temp.write(self._string)
+ temp.write(self._bytes)
temp.close()
self._filename = temp.name
return self._filename
shutil.copy(self.get_filename(), path)
-class URLOpener(urllib.FancyURLopener):
+class URLOpener(FancyURLopener):
version = 'FNP Librarian (http://github.com/fnp/librarian)'
urllib._urlopener = URLOpener()
# This file is part of Librarian, licensed under GNU Affero GPLv3 or later.
# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
#
+from __future__ import print_function, unicode_literals
+
import os.path
import optparse
-
+import six
from librarian import DirDocProvider, ParseError
from librarian.parser import WLDocument
from librarian.cover import make_cover
try:
for main_input in input_filenames:
if options.verbose:
- print main_input
+ print(main_input)
+
+ if isinstance(main_input, six.binary_type):
+ main_input = main_input.decode('utf-8')
# Where to find input?
if cls.uses_provider:
doc.save_output_file(output, output_file, options.output_dir, options.make_dir, cls.ext)
- except ParseError, e:
- print '%(file)s:%(name)s:%(message)s' % {
+ except ParseError as e:
+ print('%(file)s:%(name)s:%(message)s' % {
'file': main_input,
'name': e.__class__.__name__,
'message': e
- }
+ })
# This file is part of Librarian, licensed under GNU Affero GPLv3 or later.
# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
#
+from __future__ import unicode_literals
+
import re
from PIL import Image, ImageFont, ImageDraw, ImageFilter
-from StringIO import StringIO
+from six import BytesIO
from librarian import get_resource, OutputFile, URLOpener
line_width = self.draw.textsize(line, font=font)[0]
line = line.strip() + ' '
- pos_x = (self.max_width - line_width) / 2
+ pos_x = (self.max_width - line_width) // 2
if shadow_color:
self.shadow_draw.text(
if format is not None:
self.format = format
if width and height:
- self.height = height * self.width / width
+ self.height = int(round(height * self.width / width))
scale = max(float(width or 0) / self.width, float(height or 0) / self.height)
if scale >= 1:
self.scale = scale
# WL logo
if metr.logo_width:
logo = Image.open(get_resource('res/wl-logo.png'))
- logo = logo.resize((metr.logo_width, logo.size[1] * metr.logo_width / logo.size[0]))
- img.paste(logo, ((metr.width - metr.logo_width) / 2, img.size[1] - logo.size[1] - metr.logo_bottom))
+ logo = logo.resize((metr.logo_width, int(round(logo.size[1] * metr.logo_width / logo.size[0]))))
+ img.paste(logo, ((metr.width - metr.logo_width) // 2, img.size[1] - logo.size[1] - metr.logo_bottom))
top = metr.author_top
tbox = TextBox(
return self.final_image().save(*args, **default_kwargs)
def output_file(self, *args, **kwargs):
- imgstr = StringIO()
+ imgstr = BytesIO()
self.save(imgstr, *args, **kwargs)
- return OutputFile.from_string(imgstr.getvalue())
+ return OutputFile.from_bytes(imgstr.getvalue())
class WLCover(Cover):
elif self.box_position == 'bottom':
box_top = metr.height - metr.box_bottom_margin - box_img.size[1]
else: # Middle.
- box_top = (metr.height - box_img.size[1]) / 2
+ box_top = (metr.height - box_img.size[1]) // 2
- box_left = metr.bar_width + (metr.width - metr.bar_width - box_img.size[0]) / 2
+ box_left = metr.bar_width + (metr.width - metr.bar_width - box_img.size[0]) // 2
# Draw the white box.
ImageDraw.Draw(img).rectangle(
if src.size[0] * trg_size[1] < src.size[1] * trg_size[0]:
resized = (
trg_size[0],
- src.size[1] * trg_size[0] / src.size[0]
+ int(round(src.size[1] * trg_size[0] / src.size[0]))
)
- cut = (resized[1] - trg_size[1]) / 2
+ cut = (resized[1] - trg_size[1]) // 2
src = src.resize(resized, Image.ANTIALIAS)
src = src.crop((0, cut, src.size[0], src.size[1] - cut))
else:
resized = (
- src.size[0] * trg_size[1] / src.size[1],
+ int(round(src.size[0] * trg_size[1] / src.size[1])),
trg_size[1],
)
- cut = (resized[0] - trg_size[0]) / 2
+ cut = (resized[0] - trg_size[0]) // 2
src = src.resize(resized, Image.ANTIALIAS)
src = src.crop((cut, 0, src.size[0] - cut, src.size[1]))
img.paste(gradient, (metr.bar_width, metr.height - metr.gradient_height), mask=gradient_mask)
cursor = metr.width - metr.gradient_logo_margin_right
- logo_top = metr.height - metr.gradient_height / 2 - metr.gradient_logo_height / 2 - metr.bleed / 2
+ logo_top = int(metr.height - metr.gradient_height / 2 - metr.gradient_logo_height / 2 - metr.bleed / 2)
for logo_path in self.gradient_logos[::-1]:
logo = Image.open(get_resource(logo_path))
logo = logo.resize(
- (logo.size[0] * metr.gradient_logo_height / logo.size[1], metr.gradient_logo_height),
+ (int(round(logo.size[0] * metr.gradient_logo_height / logo.size[1])), metr.gradient_logo_height),
Image.ANTIALIAS)
cursor -= logo.size[0]
img.paste(logo, (cursor, logo_top), mask=logo)
# This file is part of Librarian, licensed under GNU Affero GPLv3 or later.
# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
#
+from __future__ import unicode_literals
+
from xml.parsers.expat import ExpatError
from datetime import date
+from functools import total_ordering
import time
import re
+import six
from librarian.util import roman_to_int
from librarian import (ValidationError, NoDublinCore, ParseError, DCNS, RDFNS,
from lxml.etree import XMLSyntaxError
-class TextPlus(unicode):
+class TextPlus(six.text_type):
pass
# ==============
# = Converters =
# ==============
+@six.python_2_unicode_compatible
+@total_ordering
class Person(object):
"""Single person with last name and a list of first names."""
def __init__(self, last_name, *first_names):
def __eq__(self, right):
return self.last_name == right.last_name and self.first_names == right.first_names
- def __cmp__(self, other):
- return cmp((self.last_name, self.first_names), (other.last_name, other.first_names))
+ def __lt__(self, other):
+ return (self.last_name, self.first_names) < (other.last_name, other.first_names)
def __hash__(self):
return hash((self.last_name, self.first_names))
- def __unicode__(self):
+ def __str__(self):
if len(self.first_names) > 0:
return '%s, %s' % (self.last_name, ' '.join(self.first_names))
else:
"""
try:
# check out the "N. poł X w." syntax
- if isinstance(text, str):
+ if isinstance(text, six.binary_type):
text = text.decode("utf-8")
century_format = u"(?:([12]) *poł[.]? +)?([MCDXVI]+) *w[.,]*(?: *l[.]? *([0-9]+))?"
if m:
half = m.group(1)
decade = m.group(3)
- century = roman_to_int(str(m.group(2)))
+ century = roman_to_int(m.group(2))
if half is not None:
if decade is not None:
raise ValueError("Bad date format. Cannot specify both half and decade of century")
raise ValueError
return DatePlus(t[0], t[1], t[2])
- except ValueError, e:
+ except ValueError as e:
raise ValueError("Unrecognized date format. Try YYYY-MM-DD or YYYY.")
def as_unicode(text):
- if isinstance(text, unicode):
+ if isinstance(text, six.text_type):
return text
else:
return TextPlus(text.decode('utf-8'))
if hasattr(val[0], 'lang'):
setattr(nv, 'lang', val[0].lang)
return nv
- except ValueError, e:
+ except ValueError as e:
raise ValidationError("Field '%s' - invald value: %s" % (self.uri, e.message))
def validate(self, fdict, fallbacks=None, strict=False):
return super(DCInfo, mcs).__new__(mcs, classname, bases, class_dict)
-class WorkInfo(object):
- __metaclass__ = DCInfo
-
+class WorkInfo(six.with_metaclass(DCInfo, object)):
FIELDS = (
Field(DCNS('creator'), 'authors', as_person, salias='author', multiple=True),
Field(DCNS('title'), 'title'),
)
@classmethod
- def from_string(cls, xml, *args, **kwargs):
- from StringIO import StringIO
- return cls.from_file(StringIO(xml), *args, **kwargs)
+ def from_bytes(cls, xml, *args, **kwargs):
+ return cls.from_file(six.BytesIO(xml), *args, **kwargs)
@classmethod
def from_file(cls, xmlfile, *args, **kwargs):
# extract data from the element and make the info
return cls.from_element(desc_tag, *args, **kwargs)
- except XMLSyntaxError, e:
+ except XMLSyntaxError as e:
raise ParseError(e)
- except ExpatError, e:
+ except ExpatError as e:
raise ParseError(e)
@classmethod
fv = field_dict.get(e.tag, [])
if e.text is not None:
text = e.text
- if not isinstance(text, unicode):
+ if not isinstance(text, six.text_type):
text = text.decode('utf-8')
val = TextPlus(text)
val.lang = e.attrib.get(XMLNS('lang'), lang)
for x in v:
e = etree.Element(field.uri)
if x is not None:
- e.text = unicode(x)
+ e.text = six.text_type(x)
description.append(e)
else:
e = etree.Element(field.uri)
- e.text = unicode(v)
+ e.text = six.text_type(v)
description.append(e)
return root
if field.multiple:
if len(v) == 0:
continue
- v = [unicode(x) for x in v if x is not None]
+ v = [six.text_type(x) for x in v if x is not None]
else:
- v = unicode(v)
+ v = six.text_type(v)
dc[field.name] = {'uri': field.uri, 'value': v}
rdf['fields'] = dc
if field.multiple:
if len(v) == 0:
continue
- v = [unicode(x) for x in v if x is not None]
+ v = [six.text_type(x) for x in v if x is not None]
else:
- v = unicode(v)
+ v = six.text_type(v)
result[field.name] = v
if field.salias:
v = getattr(self, field.salias)
if v is not None:
- result[field.salias] = unicode(v)
+ result[field.salias] = six.text_type(v)
return result
+from __future__ import unicode_literals
+
import importlib
from lxml import etree
# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
import os
import shutil
from subprocess import call, PIPE
class LaTeX(DataEmbed):
@downgrades_to('image/png')
def to_png(self):
- tmpl = open(get_resource('res/embeds/latex/template.tex')).read().decode('utf-8')
+ tmpl = open(get_resource('res/embeds/latex/template.tex'), 'rb').read().decode('utf-8')
tempdir = mkdtemp('-librarian-embed-latex')
fpath = os.path.join(tempdir, 'doc.tex')
- with open(fpath, 'w') as f:
+ with open(fpath, 'wb') as f:
f.write((tmpl % {'code': self.data}).encode('utf-8'))
call(['xelatex', '-interaction=batchmode', '-output-directory', tempdir, fpath], stdout=PIPE, stderr=PIPE)
call(['convert', '-density', '150', os.path.join(tempdir, 'doc.pdf'), '-trim',
os.path.join(tempdir, 'doc.png')])
- pngdata = open(os.path.join(tempdir, 'doc.png')).read()
+ pngdata = open(os.path.join(tempdir, 'doc.png'), 'rb').read()
shutil.rmtree(tempdir)
return create_embed('image/png', data=pngdata)
# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
from lxml import etree
+import six
from librarian import get_resource
from . import TreeEmbed, create_embed, downgrades_to
def to_latex(self):
xslt = etree.parse(get_resource('res/embeds/mathml/mathml2latex.xslt'))
output = self.tree.xslt(xslt)
- return create_embed('application/x-latex', data=unicode(output))
+ return create_embed('application/x-latex', data=six.text_type(output))
# This file is part of Librarian, licensed under GNU Affero GPLv3 or later.
# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
#
-from __future__ import with_statement
+from __future__ import print_function, unicode_literals
import os
import os.path
import re
import subprocess
-from StringIO import StringIO
+from six import BytesIO
from copy import deepcopy
from mimetypes import guess_type
def squeeze_whitespace(s):
- return re.sub(r'\s+', ' ', s)
+ return re.sub(b'\\s+', b' ', s)
def set_hyph_language(source_tree):
result = ''
text = ''.join(text)
with open(get_resource('res/ISO-639-2_8859-1.txt'), 'rb') as f:
- for line in f:
+ for line in f.read().decode('latin1').split('\n'):
list = line.strip().split('|')
if list[0] == text:
result = list[2]
def inner_xml(node):
""" returns node's text and children as a string
- >>> print inner_xml(etree.fromstring('<a>x<b>y</b>z</a>'))
+ >>> print(inner_xml(etree.fromstring('<a>x<b>y</b>z</a>')))
x<b>y</b>z
"""
nt = node.text if node.text is not None else ''
- return ''.join([nt] + [etree.tostring(child) for child in node])
+ return ''.join([nt] + [etree.tostring(child, encoding='unicode') for child in node])
def set_inner_xml(node, text):
>>> e = etree.fromstring('<a>b<b>x</b>x</a>')
>>> set_inner_xml(e, 'x<b>y</b>z')
- >>> print etree.tostring(e)
+ >>> print(etree.tostring(e, encoding='unicode'))
<a>x<b>y</b>z</a>
"""
def node_name(node):
""" Find out a node's name
- >>> print node_name(etree.fromstring('<a>X<b>Y</b>Z</a>'))
+ >>> print(node_name(etree.fromstring('<a>X<b>Y</b>Z</a>')))
XYZ
"""
xml = etree.ElementTree(xml)
with open(sheet) as xsltf:
transform = etree.XSLT(etree.parse(xsltf))
- params = dict((key, transform.strparam(value)) for key, value in kwargs.iteritems())
+ params = dict((key, transform.strparam(value)) for key, value in kwargs.items())
return transform(xml, **params)
>>> s = etree.fromstring("<strofa>a <b>c</b> <b>c</b>/\\nb<x>x/\\ny</x>c/ \\nd</strofa>")
>>> Stanza(s).versify()
- >>> print etree.tostring(s)
- <strofa><wers_normalny>a <b>c</b> <b>c</b></wers_normalny><wers_normalny>b<x>x/
+ >>> print(etree.tostring(s, encoding='unicode'))
+ <strofa><wers_normalny>a <b>c</b><b>c</b></wers_normalny><wers_normalny>b<x>x/
y</x>c</wers_normalny><wers_normalny>d</wers_normalny></strofa>
"""
return "\n".join(texts)
def html(self):
- with open(get_resource('epub/toc.html')) as f:
- t = unicode(f.read(), 'utf-8')
+ with open(get_resource('epub/toc.html'), 'rb') as f:
+ t = f.read().decode('utf-8')
return t % self.html_part()
mime = zipfile.ZipInfo()
mime.filename = 'mimetype'
mime.compress_type = zipfile.ZIP_STORED
- mime.extra = ''
- zip.writestr(mime, 'application/epub+zip')
+ mime.extra = b''
+ zip.writestr(mime, b'application/epub+zip')
zip.writestr(
'META-INF/container.xml',
- '<?xml version="1.0" ?>'
- '<container version="1.0" '
- 'xmlns="urn:oasis:names:tc:opendocument:xmlns:container">'
- '<rootfiles><rootfile full-path="OPS/content.opf" '
- 'media-type="application/oebps-package+xml" />'
- '</rootfiles></container>'
+ b'<?xml version="1.0" ?>'
+ b'<container version="1.0" '
+ b'xmlns="urn:oasis:names:tc:opendocument:xmlns:container">'
+ b'<rootfiles><rootfile full-path="OPS/content.opf" '
+ b'media-type="application/oebps-package+xml" />'
+ b'</rootfiles></container>'
)
zip.write(get_resource('res/wl-logo-small.png'),
os.path.join('OPS', 'logo_wolnelektury.png'))
if cover is True:
cover = make_cover
- cover_file = StringIO()
+ cover_file = BytesIO()
bound_cover = cover(document.book_info)
bound_cover.save(cover_file)
cover_name = 'cover.%s' % bound_cover.ext()
annotations = etree.Element('annotations')
toc_file = etree.fromstring(
- '<?xml version="1.0" encoding="utf-8"?><!DOCTYPE ncx PUBLIC '
- '"-//NISO//DTD ncx 2005-1//EN" '
- '"http://www.daisy.org/z3986/2005/ncx-2005-1.dtd">'
- '<ncx xmlns="http://www.daisy.org/z3986/2005/ncx/" xml:lang="pl" '
- 'version="2005-1"><head></head><docTitle></docTitle><navMap>'
- '</navMap></ncx>'
+ b'<?xml version="1.0" encoding="utf-8"?><!DOCTYPE ncx PUBLIC '
+ b'"-//NISO//DTD ncx 2005-1//EN" '
+ b'"http://www.daisy.org/z3986/2005/ncx-2005-1.dtd">'
+ b'<ncx xmlns="http://www.daisy.org/z3986/2005/ncx/" xml:lang="pl" '
+ b'version="2005-1"><head></head><docTitle></docTitle><navMap>'
+ b'</navMap></ncx>'
)
nav_map = toc_file[-1]
'<item id="support" href="support.html" media-type="application/xhtml+xml" />'))
spine.append(etree.fromstring(
'<itemref idref="support" />'))
- html_string = open(get_resource('epub/support.html')).read()
+ html_string = open(get_resource('epub/support.html'), 'rb').read()
chars.update(used_chars(etree.fromstring(html_string)))
zip.writestr('OPS/support.html', squeeze_whitespace(html_string))
os.path.join(tmpdir, fname)]
env = {"PERL_USE_UNSAFE_INC": "1"}
if verbose:
- print "Running font-optimizer"
+ print("Running font-optimizer")
subprocess.check_call(optimizer_call, env=env)
else:
dev_null = open(os.devnull, 'w')
# This file is part of Librarian, licensed under GNU Affero GPLv3 or later.
# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
#
+from __future__ import unicode_literals
+
import os.path
from copy import deepcopy
from lxml import etree
+import six
from librarian import functions, OutputFile
from .epub import replace_by_verse
result = document.transform(style)
- return OutputFile.from_string(unicode(result).encode('utf-8'))
+ return OutputFile.from_bytes(six.text_type(result).encode('utf-8'))
# vim:et
# This file is part of Librarian, licensed under GNU Affero GPLv3 or later.
# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
#
+from __future__ import unicode_literals
+
from lxml import etree
import re
result = ''
text = ''.join(text)
with open(get_resource('res/ISO-639-2_8859-1.txt'), 'rb') as f:
- for line in f:
+ for line in f.read().decode('latin1').split('\n'):
list = line.strip().split('|')
if list[0] == text:
result = list[2]
# This file is part of Librarian, licensed under GNU Affero GPLv3 or later.
# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
#
+from __future__ import print_function, unicode_literals
+
import os
import re
-import cStringIO
import copy
from lxml import etree
from librarian import functions
from lxml.etree import XMLSyntaxError, XSLTApplyError
+import six
+
functions.reg_substitute_entities()
functions.reg_person_name()
def transform_abstrakt(abstrakt_element):
- from cStringIO import StringIO
style_filename = get_stylesheet('legacy')
style = etree.parse(style_filename)
xml = etree.tostring(abstrakt_element)
- document = etree.parse(StringIO(xml.replace('abstrakt', 'dlugi_cytat'))) # HACK
+ document = etree.parse(six.BytesIO(xml.replace('abstrakt', 'dlugi_cytat'))) # HACK
result = document.xslt(style)
html = re.sub('<a name="sec[0-9]*"/>', '', etree.tostring(result))
return re.sub('</?blockquote[^>]*>', '', html)
add_table_of_themes(result.getroot())
add_table_of_contents(result.getroot())
- return OutputFile.from_string(etree.tostring(
+ return OutputFile.from_bytes(etree.tostring(
result, method='html', xml_declaration=False, pretty_print=True, encoding='utf-8'))
else:
return None
except KeyError:
raise ValueError("'%s' is not a valid stylesheet.")
- except (XMLSyntaxError, XSLTApplyError), e:
+ except (XMLSyntaxError, XSLTApplyError) as e:
raise ParseError(e)
+@six.python_2_unicode_compatible
class Fragment(object):
def __init__(self, id, themes):
super(Fragment, self).__init__()
try:
stack.pop()
except IndexError:
- print 'CLOSED NON-OPEN TAG:', element
+ print('CLOSED NON-OPEN TAG:', element)
stack.reverse()
return self.events + stack
return ''.join(result)
- def __unicode__(self):
+ def __str__(self):
return self.to_string()
# iterparse would die on a HTML document
parser = etree.HTMLParser(encoding='utf-8')
- buf = cStringIO.StringIO()
+ buf = six.BytesIO()
buf.write(etree.tostring(etree.parse(input_filename, parser).getroot()[0][0], encoding='utf-8'))
buf.seek(0)
try:
fragment = open_fragments[element.get('fid')]
except KeyError:
- print '%s:closed not open fragment #%s' % (input_filename, element.get('fid'))
+ print('%s:closed not open fragment #%s' % (input_filename, element.get('fid')))
else:
closed_fragments[fragment.id] = fragment
del open_fragments[fragment.id]
link_text = prefix
anchor = etree.Element('a', href='#%s' % prefix)
anchor.set('class', 'anchor')
- anchor.text = unicode(link_text)
+ anchor.text = six.text_type(link_text)
parent.insert(index, anchor)
if with_target:
for e in working.findall('a'):
if e.get('class') in ('annotation', 'theme-begin'):
e.text = ''
- return etree.tostring(working, method='text', encoding=unicode).strip()
+ return etree.tostring(working, method='text', encoding='unicode').strip()
def add_table_of_contents(root):
theme_names = [s.strip() for s in fragment.text.split(',')]
for theme_name in theme_names:
book_themes.setdefault(theme_name, []).append(fragment.get('name'))
- book_themes = book_themes.items()
+ book_themes = list(book_themes.items())
book_themes.sort(key=lambda s: sortify(s[0]))
themes_div = etree.Element('div', id="themes")
themes_ol = etree.SubElement(themes_div, 'ol')
parser = etree.HTMLParser(encoding='utf-8')
tree = etree.parse(html_path, parser)
footnotes = tree.find('//*[@id="footnotes"]')
- re_qualifier = re.compile(ur'[^\u2014]+\s+\(([^\)]+)\)\s+\u2014')
+ re_qualifier = re.compile(r'[^\u2014]+\s+\(([^\)]+)\)\s+\u2014')
if footnotes is not None:
for footnote in footnotes.findall('div'):
fn_type = footnote.get('class').split('-')[1]
footnote.text = None
if len(footnote) and footnote[-1].tail == '\n':
footnote[-1].tail = None
- text_str = etree.tostring(footnote, method='text', encoding=unicode).strip()
- html_str = etree.tostring(footnote, method='html', encoding=unicode).strip()
+ text_str = etree.tostring(footnote, method='text', encoding='unicode').strip()
+ html_str = etree.tostring(footnote, method='html', encoding='unicode').strip()
match = re_qualifier.match(text_str)
if match:
License: LGPL.
"""
+from __future__ import print_function, unicode_literals
import sys
import re
h = Hyphenator(dict_file, left=1, right=1)
for i in h(word):
- print i
+ print(i)
# This file is part of Librarian, licensed under GNU Affero GPLv3 or later.
# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
#
+from __future__ import unicode_literals
from copy import deepcopy
import os
def transform(wldoc, verbose=False, sample=None, cover=None,
- use_kindlegen=False, flags=None, hyphenate=True, ilustr_path=''):
+ use_kindlegen=False, flags=None, hyphenate=True, ilustr_path='',
+ converter_path=None):
""" produces a MOBI file
wldoc: a WLDocument
sample=n: generate sample e-book (with at least n paragraphs)
cover: a cover.Cover factory overriding default
flags: less-advertising,
+ converter_path: override path to MOBI converter,
+ either ebook-convert or kindlegen
"""
document = deepcopy(wldoc)
if use_kindlegen:
output_file_basename = os.path.basename(output_file.name)
- subprocess.check_call(['kindlegen', '-c2', epub.get_filename(),
- '-o', output_file_basename], **kwargs)
+ subprocess.check_call([converter_path or 'kindlegen',
+ '-c2', epub.get_filename(),
+ '-o', output_file_basename], **kwargs)
else:
- subprocess.check_call(['ebook-convert', epub.get_filename(),
+ subprocess.check_call([converter_path or 'ebook-convert',
+ epub.get_filename(),
output_file.name, '--no-inline-toc',
'--mobi-file-type=both',
'--mobi-ignore-margins'], **kwargs)
# This file is part of Librarian, licensed under GNU Affero GPLv3 or later.
# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
#
+from __future__ import print_function, unicode_literals
+
import os
from librarian import pdf, epub, mobi, DirDocProvider, ParseError
from librarian.parser import WLDocument
-from util import makedirs
+from .util import makedirs
class Packager(object):
try:
for main_input in input_filenames:
if verbose:
- print main_input
+ print(main_input)
cls.prepare_file(main_input, output_dir, verbose, overwrite)
- except ParseError, e:
- print '%(file)s:%(name)s:%(message)s' % {
+ except ParseError as e:
+ print('%(file)s:%(name)s:%(message)s' % {
'file': main_input,
'name': e.__class__.__name__,
'message': e.message
- }
+ })
class EpubPackager(Packager):
# This file is part of Librarian, licensed under GNU Affero GPLv3 or later.
# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
#
+from __future__ import unicode_literals
+
from librarian import ValidationError, NoDublinCore, ParseError, NoProvider
from librarian import RDFNS
from librarian.cover import make_cover
import os
import re
-from StringIO import StringIO
+import six
class WLDocument(object):
self.book_info = None
@classmethod
- def from_string(cls, xml, *args, **kwargs):
- return cls.from_file(StringIO(xml), *args, **kwargs)
+ def from_bytes(cls, xml, *args, **kwargs):
+ return cls.from_file(six.BytesIO(xml), *args, **kwargs)
@classmethod
def from_file(cls, xmlfile, *args, **kwargs):
# first, prepare for parsing
- if isinstance(xmlfile, basestring):
+ if isinstance(xmlfile, six.text_type):
file = open(xmlfile, 'rb')
try:
data = file.read()
else:
data = xmlfile.read()
- if not isinstance(data, unicode):
+ if not isinstance(data, six.text_type):
data = data.decode('utf-8')
data = data.replace(u'\ufeff', '')
try:
parser = etree.XMLParser(remove_blank_text=False)
- tree = etree.parse(StringIO(data.encode('utf-8')), parser)
+ tree = etree.parse(six.BytesIO(data.encode('utf-8')), parser)
return cls(tree, *args, **kwargs)
- except (ExpatError, XMLSyntaxError, XSLTApplyError), e:
+ except (ExpatError, XMLSyntaxError, XSLTApplyError) as e:
raise ParseError(e)
def swap_endlines(self):
def serialize(self):
self.update_dc()
- return etree.tostring(self.edoc, encoding=unicode, pretty_print=True)
+ return etree.tostring(self.edoc, encoding='unicode', pretty_print=True)
def merge_chunks(self, chunk_dict):
unmerged = []
node = self.edoc.xpath(xpath)[0]
repl = etree.fromstring(u"<%s>%s</%s>" % (node.tag, data, node.tag))
node.getparent().replace(node, repl)
- except Exception, e:
+ except Exception as e:
unmerged.append(repr((key, xpath, e)))
return unmerged
if output_dir_path:
save_path = output_dir_path
if make_author_dir:
- save_path = os.path.join(save_path, unicode(self.book_info.author).encode('utf-8'))
+ save_path = os.path.join(save_path, six.text_type(self.book_info.author).encode('utf-8'))
save_path = os.path.join(save_path, self.book_info.url.slug)
if ext:
save_path += '.%s' % ext
New partners shouldn't be added here, but in the partners repository.
"""
+from __future__ import print_function, unicode_literals
from librarian import packagers, cover
-from util import makedirs
+from .util import makedirs
class GandalfEpub(packagers.EpubPackager):
try:
for main_input in input_filenames:
if verbose:
- print main_input
+ print(main_input)
path, fname = os.path.realpath(main_input).rsplit('/', 1)
provider = DirDocProvider(path)
slug, ext = os.path.splitext(fname)
doc.save_output_file(
doc.as_mobi(doc, cover=cover.VirtualoCover, sample=25),
output_path=outfile_sample)
- except ParseError, e:
- print '%(file)s:%(name)s:%(message)s' % {
+ except ParseError as e:
+ print('%(file)s:%(name)s:%(message)s' % {
'file': main_input,
'name': e.__class__.__name__,
'message': e.message
- }
+ })
xml_file = open(os.path.join(output_dir, 'import_products.xml'), 'w')
- xml_file.write(etree.tostring(xml, pretty_print=True, encoding=unicode).encode('utf-8'))
+ xml_file.write(etree.tostring(xml, pretty_print=True, encoding='unicode').encode('utf-8'))
xml_file.close()
with TeXML, then runs it by XeLaTeX.
"""
-from __future__ import with_statement
+from __future__ import print_function, unicode_literals
+
import os
import os.path
import shutil
-from StringIO import StringIO
from tempfile import mkdtemp, NamedTemporaryFile
import re
from copy import deepcopy
from Texml.processor import process
from lxml import etree
from lxml.etree import XMLSyntaxError, XSLTApplyError
+import six
from librarian.dcparser import Person
from librarian.parser import WLDocument
>>> t = etree.fromstring('<a><b>A-B-C</b>X-Y-Z</a>')
>>> insert_tags(t, re.compile('-'), 'd')
- >>> print etree.tostring(t)
+ >>> print(etree.tostring(t, encoding='unicode'))
<a><b>A<d/>B<d/>C</b>X<d/>Y<d/>Z</a>
"""
tempdir = mkdtemp('-wl2pdf-test')
fpath = os.path.join(tempdir, 'test.tex')
f = open(fpath, 'w')
- f.write(r"""
- \documentclass{wl}
- \usepackage[%s]{%s}
- \begin{document}
- \end{document}
+ f.write("""
+ \\documentclass{wl}
+ \\usepackage[%s]{%s}
+ \\begin{document}
+ \\end{document}
""" % (args, package))
f.close()
if verbose:
del document # no longer needed large object :)
tex_path = os.path.join(temp, 'doc.tex')
- fout = open(tex_path, 'w')
- process(StringIO(texml), fout, 'utf-8')
+ fout = open(tex_path, 'wb')
+ process(six.BytesIO(texml), fout, 'utf-8')
fout.close()
del texml
# some things work better when compiled twice
# (table of contents, [line numbers - disabled])
- for run in xrange(2):
+ for run in range(2):
if verbose:
p = call(['xelatex', tex_path])
else:
shutil.rmtree(temp)
return OutputFile.from_filename(output_file.name)
- except (XMLSyntaxError, XSLTApplyError), e:
+ except (XMLSyntaxError, XSLTApplyError) as e:
raise ParseError(e)
text = f.read().decode('utf-8')
f.close()
elif wldoc is not None:
- text = etree.tostring(wldoc.edoc, encoding=unicode)
+ text = etree.tostring(wldoc.edoc, encoding='unicode')
provider = wldoc.provider
else:
raise ValueError('Neither a WLDocument, nor provider and URI were provided.')
- text = re.sub(ur"([\u0400-\u04ff]+)", ur"<alien>\1</alien>", text)
+ text = re.sub(r"([\u0400-\u04ff]+)", r"<alien>\1</alien>", text)
- document = WLDocument.from_string(text, parse_dublincore=True, provider=provider)
+ document = WLDocument.from_bytes(text.encode('utf-8'), parse_dublincore=True, provider=provider)
document.swap_endlines()
for child_uri in document.book_info.parts:
# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
from operator import and_
-from dcparser import Field, WorkInfo, DCNS
+from .dcparser import Field, WorkInfo, DCNS
from librarian import (RDFNS, ValidationError, NoDublinCore, ParseError, WLURI)
from xml.parsers.expat import ExpatError
from os import path
-from StringIO import StringIO
from lxml import etree
from lxml.etree import (XMLSyntaxError, XSLTApplyError, Element)
import re
+import six
class WLPictureURI(WLURI):
self.frame = None
@classmethod
- def from_string(cls, xml, *args, **kwargs):
- return cls.from_file(StringIO(xml), *args, **kwargs)
+ def from_bytes(cls, xml, *args, **kwargs):
+ return cls.from_file(six.BytesIO(xml), *args, **kwargs)
@classmethod
def from_file(cls, xmlfile, parse_dublincore=True, image_store=None):
# first, prepare for parsing
- if isinstance(xmlfile, basestring):
+ if isinstance(xmlfile, six.text_type):
file = open(xmlfile, 'rb')
try:
data = file.read()
else:
data = xmlfile.read()
- if not isinstance(data, unicode):
+ if not isinstance(data, six.text_type):
data = data.decode('utf-8')
data = data.replace(u'\ufeff', '')
try:
parser = etree.XMLParser(remove_blank_text=False)
- tree = etree.parse(StringIO(data.encode('utf-8')), parser)
+ tree = etree.parse(six.BytesIO(data.encode('utf-8')), parser)
me = cls(tree, parse_dublincore=parse_dublincore, image_store=image_store)
me.load_frame_info()
return me
- except (ExpatError, XMLSyntaxError, XSLTApplyError), e:
+ except (ExpatError, XMLSyntaxError, XSLTApplyError) as e:
raise ParseError(e)
@property
pd['coords'] = coords
def want_unicode(x):
- if not isinstance(x, unicode):
+ if not isinstance(x, six.text_type):
return x.decode('utf-8')
else:
return x
# This file is part of Librarian, licensed under GNU Affero GPLv3 or later.
# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
#
+from __future__ import unicode_literals
+
from librarian import get_resource
# This file is part of Librarian, licensed under GNU Affero GPLv3 or later.
# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
#
+from __future__ import unicode_literals
+
import copy
from librarian import functions, OutputFile
from lxml import etree
import os
+import six
functions.reg_substitute_entities()
'description': description,
'url': url,
'license_description': license_description,
- 'text': unicode(result),
+ 'text': six.text_type(result),
'source': source,
'contributors': contributors,
'funders': funders,
'isbn': isbn,
}).encode('utf-8')
else:
- result = unicode(result).encode('utf-8')
- return OutputFile.from_string("\r\n".join(result.splitlines()) + "\r\n")
+ result = six.text_type(result).encode('utf-8')
+ return OutputFile.from_bytes(b"\r\n".join(result.splitlines()) + b"\r\n")
# by Paul Winkler
# http://code.activestate.com/recipes/81611-roman-numerals/
# PSFL (GPL compatible)
+from __future__ import print_function, unicode_literals
+
import os
Traceback (most recent call last):
ValueError: Argument must be between 1 and 3999
- >>> int_to_roman(1.5)
+ >>> int_to_roman(1.5) # doctest: +IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last):
TypeError: expected integer, got <type 'float'>
- >>> for i in range(1, 21): print int_to_roman(i)
+ >>> for i in range(1, 21): print(int_to_roman(i))
...
I
II
XVIII
XIX
XX
- >>> print int_to_roman(2000)
+ >>> print(int_to_roman(2000))
MM
- >>> print int_to_roman(1999)
+ >>> print(int_to_roman(1999))
MCMXCIX
"""
if type(input) != type(1):
- raise TypeError, "expected integer, got %s" % type(input)
+ raise TypeError("expected integer, got %s" % type(input))
if not 0 < input < 4000:
- raise ValueError, "Argument must be between 1 and 3999"
+ raise ValueError("Argument must be between 1 and 3999")
ints = (1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1)
nums = ('M', 'CM', 'D', 'CD','C', 'XC','L','XL','X','IX','V','IV','I')
result = ""
"""
Convert a roman numeral to an integer.
- >>> r = range(1, 4000)
+ >>> r = list(range(1, 4000))
>>> nums = [int_to_roman(i) for i in r]
>>> ints = [roman_to_int(n) for n in nums]
- >>> print r == ints
+ >>> print(r == ints)
1
>>> roman_to_int('VVVIV')
Traceback (most recent call last):
...
ValueError: input is not a valid roman numeral: VVVIV
- >>> roman_to_int(1)
+ >>> roman_to_int(1) # doctest: +IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last):
...
TypeError: expected string, got <type 'int'>
ValueError: input is not a valid roman numeral: IL
"""
if type(input) != type(""):
- raise TypeError, "expected string, got %s" % type(input)
+ raise TypeError("expected string, got %s" % type(input))
input = input.upper()
nums = ['M', 'D', 'C', 'L', 'X', 'V', 'I']
ints = [1000, 500, 100, 50, 10, 5, 1]
places = []
for c in input:
if not c in nums:
- raise ValueError, "input is not a valid roman numeral: %s" % input
+ raise ValueError("input is not a valid roman numeral: %s" % input)
for i in range(len(input)):
c = input[i]
value = ints[nums.index(c)]
if int_to_roman(sum) == input:
return sum
else:
- raise ValueError, 'input is not a valid roman numeral: %s' % input
+ raise ValueError('input is not a valid roman numeral: %s' % input)
def makedirs(path):
if not os.path.isdir(path):
- os.makedirs(path)
\ No newline at end of file
+ os.makedirs(path)
# This file is part of Librarian, licensed under GNU Affero GPLv3 or later.
# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
#
-from StringIO import StringIO
-from librarian import OutputFile
+from __future__ import unicode_literals
+
from librarian.book2anything import Book2Anything, Option
# This file is part of Librarian, licensed under GNU Affero GPLv3 or later.
# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
#
+from __future__ import unicode_literals
+
from librarian.book2anything import Book2Anything, Option
# This file is part of Librarian, licensed under GNU Affero GPLv3 or later.
# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
#
+from __future__ import unicode_literals
+
from librarian.book2anything import Book2Anything
# This file is part of Librarian, licensed under GNU Affero GPLv3 or later.
# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
#
+from __future__ import unicode_literals
+
from librarian.book2anything import Book2Anything, Option
# This file is part of Librarian, licensed under GNU Affero GPLv3 or later.
# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
#
+from __future__ import unicode_literals
+
from librarian.book2anything import Book2Anything, Option
# This file is part of Librarian, licensed under GNU Affero GPLv3 or later.
# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
#
+from __future__ import print_function, unicode_literals
+
+from collections import OrderedDict
import inspect
import optparse
import os
import sys
from librarian import packagers
-try:
- from collections import OrderedDict
-except ImportError:
- try:
- from django.utils.datastructures import SortedDict
- OrderedDict = SortedDict
- except ImportError:
- OrderedDict = dict
if __name__ == '__main__':
if inspect.isclass(package) and issubclass(package, packagers.Packager):
packages[package_name] = package
if not packages:
- print 'No packages found!'
+ print('No packages found!')
if options.list_packages:
- print 'Available packages:'
+ print('Available packages:')
for package_name, package in packages.items():
- print ' ', package_name
+ print(' ', package_name)
exit(0)
if len(input_filenames) < 1 or not options.packages:
used_packages = [packages[p] for p in options.packages.split(',')]
for package in used_packages:
if options.verbose:
- print 'Package:', package.__name__
+ print('Package:', package.__name__)
package.prepare(input_filenames,
options.output_dir, options.verbose, options.overwrite)
# This file is part of Librarian, licensed under GNU Affero GPLv3 or later.
# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
#
+from __future__ import unicode_literals
+
from librarian.book2anything import Book2Anything, Option
# This file is part of Librarian, licensed under GNU Affero GPLv3 or later.
# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
#
+from __future__ import unicode_literals
+
from librarian.book2anything import Book2Anything, Option
from librarian.parser import WLDocument
# This file is part of Librarian, licensed under GNU Affero GPLv3 or later.
# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
#
+from __future__ import print_function, unicode_literals
+
import os
import optparse
# Do some real work
for input_filename in input_filenames:
if options.verbose:
- print input_filename
+ print(input_filename)
output_filename = os.path.splitext(input_filename)[0] + '.fragments.html'
closed_fragments, open_fragments = html.extract_fragments(input_filename)
for fragment_id in open_fragments:
- print '%s:warning:unclosed fragment #%s' % (input_filename, fragment_id)
+ print('%s:warning:unclosed fragment #%s' % (input_filename, fragment_id))
output_file = open(output_filename, 'w')
output_file.write("""
This scripts reads the table of footnote qualifiers from Redmine
and produces contents of fn_qualifiers.py – a list of valid qualifiers.
"""
+from __future__ import print_function, unicode_literals
from lxml import etree
-from urllib2 import urlopen
+from six.moves.urllib.request import urlopen
url = 'http://redmine.nowoczesnapolska.org.pl/projects/wl-publikacje/wiki/Lista_skr%C3%B3t%C3%B3w'
parser = etree.HTMLParser()
tree = etree.parse(urlopen(url), parser)
-print """\
+print("""\
# -*- coding: utf-8
\"""
List of standard footnote qualifiers.
from __future__ import unicode_literals
-FN_QUALIFIERS = {""".encode('utf-8')
+FN_QUALIFIERS = {""")
for td in tree.findall('//td'):
- print (" '%s': '%s'," % (
+ print((" '%s': '%s'," % (
td[0].text.replace('\\', '\\\\').replace("'", "\\'"),
td[0].tail.strip(' -').replace('\\', '\\\\').replace("'", "\\'")
- )).encode('utf-8')
+ )))
-print """ }""".encode('utf-8')
+print(""" }""")
# This file is part of Librarian, licensed under GNU Affero GPLv3 or later.
# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
#
+from __future__ import print_function, unicode_literals
+
import os
import optparse
# Do some real work
for input_filename in input_filenames:
if options.verbose:
- print input_filename
+ print(input_filename)
doc = etree.parse(input_filename)
try:
title = doc.find('//{http://purl.org/dc/elements/1.1/}title').text
except AttributeError:
- print '%s:error:Book title not found. Skipping.' % input_filename
+ print('%s:error:Book title not found. Skipping.' % input_filename)
continue
parent = ''
except AttributeError:
pass
except IndexError:
- print '%s:error:Invalid parent URL "%s". Skipping.' % (input_filename, parent_url)
+ print('%s:error:Invalid parent URL "%s". Skipping.' % (input_filename, parent_url))
book_url = doc.find('//{http://purl.org/dc/elements/1.1/}identifier.url')
if book_url is None:
book_description = doc.find('//{http://www.w3.org/1999/02/22-rdf-syntax-ns#}Description')
book_url = etree.SubElement(book_description, '{http://purl.org/dc/elements/1.1/}identifier.url')
if not options.force and book_url.text.startswith('http://'):
- print '%s:Notice:Book already has identifier URL "%s". Skipping.' % (input_filename, book_url.text)
+ print('%s:Notice:Book already has identifier URL "%s". Skipping.' % (input_filename, book_url.text))
continue
book_url.text = BOOK_URL + slughifi(parent + title)[:60]
#
import os
import os.path
-from distutils.core import setup
+from setuptools import setup
def whole_tree(prefix, path):
files = []
setup(
name='librarian',
- version='1.6',
+ version='1.7',
description='Converter from WolneLektury.pl XML-based language to XHTML, TXT and other formats',
author="Marek Stępniowski",
author_email='marek@stepniowski.com',
maintainer_email='radoslaw.czajka@nowoczesnapolska.org.pl',
url='http://github.com/fnp/librarian',
packages=['librarian', 'librarian.embeds'],
- package_data={'librarian': ['xslt/*.xslt', 'epub/*', 'mobi/*', 'pdf/*', 'fb2/*', 'fonts/*'] +
+ package_data={'librarian': ['xslt/*.xslt', 'xslt/*.xml', 'epub/*', 'pdf/*', 'fb2/*', 'fonts/*'] +
whole_tree(os.path.join(os.path.dirname(__file__), 'librarian'), 'res') +
whole_tree(os.path.join(os.path.dirname(__file__), 'librarian'), 'font-optimizer')},
include_package_data=True,
install_requires=[
- 'lxml>=2.2',
+ 'lxml>=2.2,<=4.3',
'Pillow',
+ 'six',
+ 'texml',
],
scripts=['scripts/book2html',
'scripts/book2txt',
'scripts/book2cover',
'scripts/bookfragments',
'scripts/genslugs'],
- tests_require=['nose>=0.11', 'coverage>=3.0.1'],
)
{
- 'publisher': u'Fundacja Nowoczesna Polska',
+ 'publisher': [u'Fundacja Nowoczesna Polska'],
'about': u'http://wiki.wolnepodreczniki.pl/Lektury:Andersen/Brzydkie_kaczątko',
'source_name': u'Andersen, Hans Christian (1805-1875), Baśnie, Gebethner i Wolff, wyd. 7, Kraków, 1925',
'author': u'Andersen, Hans Christian',
{
'editors': [u'Sekuła, Aleksandra'],
- 'publisher': u'Fundacja Nowoczesna Polska',
+ 'publisher': [u'Fundacja Nowoczesna Polska'],
'about': 'http://wiki.wolnepodreczniki.pl/Lektury:Biedrzycki/Akslop',
'source_name': u'Miłosz Biedrzycki, * ("Gwiazdka"), Fundacja "brulion", Kraków-Warszawa, 1993',
'author': u'Biedrzycki, Miłosz',
{
- 'publisher': u'Fundacja Nowoczesna Polska',
+ 'publisher': [u'Fundacja Nowoczesna Polska'],
'about': u'http://wiki.wolnepodreczniki.pl/Lektury:Kochanowski/Pieśni/Pieśń_VII_(1)',
'source_name': u'Kochanowski, Jan (1530-1584), Dzieła polskie, tom 1, oprac. Julian Krzyżanowski, wyd. 8, Państwowy Instytut Wydawniczy, Warszawa, 1976',
'author': u'Kochanowski, Jan',
{
'editors': [u'Sekuła, Aleksandra', u'Kallenbach, Józef'],
- 'publisher': u'Fundacja Nowoczesna Polska',
+ 'publisher': [u'Fundacja Nowoczesna Polska'],
'about': 'http://wiki.wolnepodreczniki.pl/Lektury:Mickiewicz/Ballady/Rybka',
'source_name': u'Mickiewicz, Adam (1798-1855), Poezje, tom 1 (Wiersze młodzieńcze - Ballady i romanse - Wiersze do r. 1824), Krakowska Spółdzielnia Wydawnicza, wyd. 2 zwiększone, Kraków, 1922',
'author': u'Mickiewicz, Adam',
{
'editors': [u'Sekuła, Aleksandra'],
- 'publisher': u'Fundacja Nowoczesna Polska',
+ 'publisher': [u'Fundacja Nowoczesna Polska'],
'about': 'http://wiki.wolnepodreczniki.pl/Lektury:Sofokles/Antygona',
'source_name': u'Sofokles (496-406 a.C.), Antygona, Zakład Narodowy im. Ossolińskich, wyd. 7, Lwów, 1939',
'author': u'Sofokles',
--- /dev/null
+<?xml version="1.0"?>
+<FictionBook xmlns="http://www.gribuser.ru/xml/fictionbook/2.0" xmlns:wl="http://wolnelektury.pl/functions" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:l="http://www.w3.org/1999/xlink">
+ <body>
+ <title>
+ <p>Adam Asnyk</p>
+ <p>Między nami nic nie było</p>
+ </title>
+ <epigraph>
+ <p>
+ Utwór opracowany został w ramach projektu
+ <a l:href="http://www.wolnelektury.pl/">Wolne Lektury</a>
+ przez <a l:href="http://www.nowoczesnapolska.org.pl/">fundację
+ Nowoczesna Polska</a>.
+ </p>
+ </epigraph>
+ <section>
+ <poem>
+ <stanza>
+ <v>Między nami nic nie było!</v>
+ <v>Żadnych zwierzeń, wyznań żadnych!</v>
+ <v>Nic nas z sobą nie łączyło —</v>
+ <v>Prócz wiosennych marzeń zdradnych;</v>
+ </stanza>
+ <stanza>
+ <v>Prócz tych woni, barw i blasków,</v>
+ <v>Unoszących się w przestrzeni;</v>
+ <v>Prócz szumiących śpiewem lasków</v>
+ <v>I tej świeżej łąk zieleni;</v>
+ </stanza>
+ <stanza>
+ <v>Prócz tych kaskad i potoków,</v>
+ <v>Zraszających każdy parów,</v>
+ <v>Prócz girlandy tęcz, obłoków,</v>
+ <v>Prócz natury słodkich czarów;</v>
+ </stanza>
+ <stanza>
+ <v>Prócz tych wspólnych, jasnych zdrojów,</v>
+ <v>Z których serce zachwyt piło;</v>
+ <v>Prócz pierwiosnków i powojów,—</v>
+ <v>Między nami nic nie było!</v>
+ </stanza>
+ </poem>
+ </section>
+ </body>
+ <body name="notes"/>
+</FictionBook>
\r
Tekst opracowany na podstawie: (Asnyk, Adam) El...y (1838-1897), Poezye, t. 3, Gebethner i Wolff, wyd. nowe poprzedzone słowem wstępnym St. Krzemińskiego, Warszawa, 1898\r
\r
+Wydawca: Fundacja Nowoczesna Polska\r
+\r
Publikacja zrealizowana w ramach projektu Wolne Lektury (http://wolnelektury.pl). Reprodukcja cyfrowa wykonana przez Bibliotekę Narodową z egzemplarza pochodzącego ze zbiorów BN.\r
\r
Opracowanie redakcyjne i przypisy: Adam Fikcyjny, Aleksandra Sekuła, Olga Sutkowska.\r
--- /dev/null
+\r
+\r
+Między nami nic nie było!\r
+Żadnych zwierzeń, wyznań żadnych!\r
+Nic nas z sobą nie łączyło —\r
+Prócz wiosennych marzeń zdradnych;\r
+\r
+Prócz tych woni, barw i blasków,\r
+Unoszących się w przestrzeni;\r
+Prócz szumiących śpiewem lasków\r
+I tej świeżej łąk zieleni;\r
+\r
+Prócz tych kaskad i potoków,\r
+Zraszających każdy parów,\r
+Prócz girlandy tęcz, obłoków,\r
+Prócz natury słodkich czarów;\r
+\r
+Prócz tych wspólnych, jasnych zdrojów,\r
+Z których serce zachwyt piło;\r
+Prócz pierwiosnków i powojów,—\r
+Między nami nic nie było!\r
+\r
# This file is part of Librarian, licensed under GNU Affero GPLv3 or later.
# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
#
+from __future__ import unicode_literals
+
from librarian import dcparser
from lxml import etree
from nose.tools import *
def check_dcparser(xml_file, result_file):
- xml = file(xml_file).read()
+ xml = open(xml_file, 'rb').read()
result = codecs.open(result_file, encoding='utf-8').read()
- info = dcparser.BookInfo.from_string(xml).to_dict()
+ info = dcparser.BookInfo.from_bytes(xml).to_dict()
should_be = eval(result)
for key in should_be:
assert_equals(info[key], should_be[key])
def check_serialize(xml_file):
- xml = file(xml_file).read()
- info = dcparser.BookInfo.from_string(xml)
+ xml = open(xml_file, 'rb').read()
+ info = dcparser.BookInfo.from_bytes(xml)
# serialize
- serialized = etree.tostring(info.to_etree(), encoding=unicode).encode('utf-8')
+ serialized = etree.tostring(info.to_etree(), encoding='unicode').encode('utf-8')
# then parse again
- info_bis = dcparser.BookInfo.from_string(serialized)
+ info_bis = dcparser.BookInfo.from_bytes(serialized)
# check if they are the same
for key in vars(info):
def test_asdate():
- assert_equals(dcparser.as_date(u"2010-10-03"), date(2010, 10, 03))
+ assert_equals(dcparser.as_date(u"2010-10-03"), date(2010, 10, 3))
assert_equals(dcparser.as_date(u"2011"), date(2011, 1, 1))
assert_equals(dcparser.as_date(u"2 poł. XIX w."), date(1950, 1, 1))
assert_equals(dcparser.as_date(u"XVII w., l. 20"), date(1720, 1, 1))
# This file is part of Librarian, licensed under GNU Affero GPLv3 or later.
# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
#
+from __future__ import unicode_literals
+
from zipfile import ZipFile
from lxml import html
from nose.tools import *
u'Opracowanie redakcyjne i przypisy: '
u'Adam Fikcyjny, Aleksandra Sekuła, Olga Sutkowska.')
assert_true(editors_attribution)
+
+
+def test_transform_hyphenate():
+ epub = WLDocument.from_file(
+ get_fixture('text', 'asnyk_zbior.xml'),
+ provider=DirDocProvider(get_fixture('text', ''))
+ ).as_epub(
+ flags=['without_fonts'],
+ hyphenate=True
+ ).get_file()
--- /dev/null
+# -*- coding: utf-8 -*-
+#
+# This file is part of Librarian, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+#
+from __future__ import unicode_literals
+
+from librarian import NoDublinCore
+from librarian.parser import WLDocument
+from nose.tools import *
+from .utils import get_fixture
+
+
+def test_transform():
+ expected_output_file_path = get_fixture('text', 'asnyk_miedzy_nami_expected.fb2')
+
+ text = WLDocument.from_file(
+ get_fixture('text', 'miedzy-nami-nic-nie-bylo.xml')
+ ).as_fb2().get_bytes()
+
+ assert_equal(text, open(expected_output_file_path, 'rb').read())
+
# This file is part of Librarian, licensed under GNU Affero GPLv3 or later.
# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
#
+from __future__ import unicode_literals
+
from librarian import NoDublinCore
from librarian.parser import WLDocument
from nose.tools import *
-from utils import get_fixture
+from .utils import get_fixture
def test_transform():
html = WLDocument.from_file(
get_fixture('text', 'miedzy-nami-nic-nie-bylo.xml')
- ).as_html().get_string()
+ ).as_html().get_bytes()
- assert_equal(html, file(expected_output_file_path).read())
+ assert_equal(html, open(expected_output_file_path, 'rb').read())
@raises(NoDublinCore)
def test_empty():
- assert not WLDocument.from_string(
- '<utwor />',
+ assert not WLDocument.from_bytes(
+ b'<utwor />',
parse_dublincore=False,
).as_html()
('<pe/>', (
'pe',
- [],
- '',
- '<p></p>'
+ [],
+ '[przypis edytorski]',
+ '<p> [przypis edytorski]</p>'
),
'Empty footnote'),
('<pr>Definiendum --- definiens.</pr>', (
'pr',
- [],
- 'Definiendum \u2014 definiens.',
- '<p>Definiendum \u2014 definiens.</p>'
+ [],
+ 'Definiendum \u2014 definiens. [przypis redakcyjny]',
+ '<p>Definiendum \u2014 definiens. [przypis redakcyjny]</p>'
),
'Plain footnote.'),
('<pt><slowo_obce>Definiendum</slowo_obce> --- definiens.</pt>', (
'pt',
- [],
- 'Definiendum \u2014 definiens.',
- '<p><em class="foreign-word">Definiendum</em> \u2014 definiens.</p>'
+ [],
+ 'Definiendum \u2014 definiens. [przypis tłumacza]',
+ '<p><em class="foreign-word">Definiendum</em> \u2014 definiens. [przypis tłumacza]</p>'
),
'Standard footnote.'),
('<pr>Definiendum (łac.) --- definiens.</pr>', (
'pr',
- ['łac.'],
- 'Definiendum (łac.) \u2014 definiens.',
- '<p>Definiendum (łac.) \u2014 definiens.</p>'
+ ['łac.'],
+ 'Definiendum (łac.) \u2014 definiens. [przypis redakcyjny]',
+ '<p>Definiendum (łac.) \u2014 definiens. [przypis redakcyjny]</p>'
),
'Plain footnote with qualifier'),
('<pe><slowo_obce>Definiendum</slowo_obce> (łac.) --- definiens.</pe>', (
'pe',
- ['łac.'],
- 'Definiendum (łac.) \u2014 definiens.',
- '<p><em class="foreign-word">Definiendum</em> (łac.) \u2014 definiens.</p>'
+ ['łac.'],
+ 'Definiendum (łac.) \u2014 definiens. [przypis edytorski]',
+ '<p><em class="foreign-word">Definiendum</em> (łac.) \u2014 definiens. [przypis edytorski]</p>'
),
'Standard footnote with qualifier.'),
('<pt> <slowo_obce>Definiendum</slowo_obce> (daw.) --- definiens.</pt>', (
'pt',
- ['daw.'],
- 'Definiendum (daw.) \u2014 definiens.',
- '<p> <em class="foreign-word">Definiendum</em> (daw.) \u2014 definiens.</p>'
+ ['daw.'],
+ 'Definiendum (daw.) \u2014 definiens. [przypis tłumacza]',
+ '<p> <em class="foreign-word">Definiendum</em> (daw.) \u2014 definiens. [przypis tłumacza]</p>'
),
'Standard footnote with leading whitespace and qualifier.'),
('<pr>Definiendum (łac.) --- <slowo_obce>definiens</slowo_obce>.</pr>', (
'pr',
- ['łac.'],
- 'Definiendum (łac.) \u2014 definiens.',
- '<p>Definiendum (łac.) \u2014 <em class="foreign-word">definiens</em>.</p>'
+ ['łac.'],
+ 'Definiendum (łac.) \u2014 definiens. [przypis redakcyjny]',
+ '<p>Definiendum (łac.) \u2014 <em class="foreign-word">definiens</em>. [przypis redakcyjny]</p>'
),
'Plain footnote with qualifier and some emphasis.'),
('<pe><slowo_obce>Definiendum</slowo_obce> (łac.) --- <slowo_obce>definiens</slowo_obce>.</pe>', (
'pe',
['łac.'],
- 'Definiendum (łac.) \u2014 definiens.',
- '<p><em class="foreign-word">Definiendum</em> (łac.) \u2014 <em class="foreign-word">definiens</em>.</p>'
+ 'Definiendum (łac.) \u2014 definiens. [przypis edytorski]',
+ '<p><em class="foreign-word">Definiendum</em> (łac.) \u2014 <em class="foreign-word">definiens</em>. [przypis edytorski]</p>'
),
'Standard footnote with qualifier and some emphasis.'),
('<pe>Definiendum (łac.) --- definiens (some) --- more text.</pe>', (
'pe',
['łac.'],
- 'Definiendum (łac.) \u2014 definiens (some) \u2014 more text.',
- '<p>Definiendum (łac.) \u2014 definiens (some) \u2014 more text.</p>',
+ 'Definiendum (łac.) \u2014 definiens (some) \u2014 more text. [przypis edytorski]',
+ '<p>Definiendum (łac.) \u2014 definiens (some) \u2014 more text. [przypis edytorski]</p>',
),
'Footnote with a second parentheses and mdash.'),
'pe',
['daw.', 'niem.'],
'gemajna (daw., z niem. gemein: zwykły) \u2014 częściej: gemajn, '
- 'szeregowiec w wojsku polskim cudzoziemskiego autoramentu.',
+ 'szeregowiec w wojsku polskim cudzoziemskiego autoramentu. [przypis edytorski]',
'<p><em class="foreign-word">gemajna</em> (daw., z niem. <em class="foreign-word">gemein</em>: zwykły) '
- '\u2014 częściej: gemajn, szeregowiec w wojsku polskim cudzoziemskiego autoramentu.</p>'
+ '\u2014 częściej: gemajn, szeregowiec w wojsku polskim cudzoziemskiego autoramentu. [przypis edytorski]</p>'
),
'Footnote with multiple and qualifiers and emphasis.'),
xml_src = '''<utwor><akap> %s </akap></utwor>''' % "".join(
t[0] for t in annotations)
- html = WLDocument.from_string(xml_src, parse_dublincore=False).as_html().get_file()
+ html = WLDocument.from_bytes(
+ xml_src.encode('utf-8'),
+ parse_dublincore=False).as_html().get_file()
res_annotations = list(extract_annotations(html))
for i, (src, expected, name) in enumerate(annotations):
# This file is part of Librarian, licensed under GNU Affero GPLv3 or later.
# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
#
+from __future__ import unicode_literals
+
from librarian.html import extract_fragments
from nose.tools import *
-from utils import get_fixture
+from .utils import get_fixture
def test_fragments():
closed_fragments, open_fragments = extract_fragments(
get_fixture('text', 'asnyk_miedzy_nami_expected.html'))
assert not open_fragments
- fragments_text = u"\n\n".join(u"%s: %s\n%s" % (f.id, f.themes, f) for f in closed_fragments.values())
- assert_equal(fragments_text, file(expected_output_file_path).read().decode('utf-8'))
+ fragments_text = u"\n\n".join(u"%s: %s\n%s" % (f.id, f.themes, f) for f in sorted(closed_fragments.values(), key=lambda f: f.id))
+ assert_equal(fragments_text, open(expected_output_file_path, 'rb').read().decode('utf-8'))
--- /dev/null
+# -*- coding: utf-8 -*-
+#
+# This file is part of Librarian, licensed under GNU Affero GPLv3 or later.
+# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+#
+from __future__ import unicode_literals
+
+from zipfile import ZipFile
+from lxml import html
+from nose.tools import *
+from librarian import DirDocProvider
+from librarian.parser import WLDocument
+from tests.utils import get_fixture
+
+
+def test_transform():
+ mobi = WLDocument.from_file(
+ get_fixture('text', 'asnyk_zbior.xml'),
+ provider=DirDocProvider(get_fixture('text', ''))
+ ).as_mobi(converter_path='true').get_file()
# This file is part of Librarian, licensed under GNU Affero GPLv3 or later.
# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
#
+from __future__ import unicode_literals
+
+import re
from tempfile import NamedTemporaryFile
from nose.tools import *
from librarian import DirDocProvider
from librarian.parser import WLDocument
-from utils import get_fixture
+from .utils import get_fixture
def test_transform():
get_fixture('text', 'asnyk_zbior.xml'),
provider=DirDocProvider(get_fixture('text', ''))
).as_pdf(save_tex=temp.name)
- tex = open(temp.name).read().decode('utf-8')
- print tex
+ tex = open(temp.name, 'rb').read().decode('utf-8')
# Check contributor list.
- editors = re.search(ur'\\def\\editors\{Opracowanie redakcyjne i przypisy: ([^}]*?)\.\s*\}', tex)
+ editors = re.search(r'\\def\\editors\{Opracowanie redakcyjne i przypisy: ([^}]*?)\.\s*\}', tex)
assert_equal(editors.group(1), u"Adam Fikcyjny, Aleksandra Sekuła, Olga Sutkowska")
# This file is part of Librarian, licensed under GNU Affero GPLv3 or later.
# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
#
+from __future__ import unicode_literals
+
from librarian import picture, dcparser
from tests.utils import get_all_fixtures, get_fixture
from os import path
motifs = set()
names = set()
- print parts
for p in parts:
for m in p['themes']:
motifs.add(m)
# This file is part of Librarian, licensed under GNU Affero GPLv3 or later.
# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
#
+from __future__ import unicode_literals
+
from librarian import NoDublinCore
from librarian.parser import WLDocument
from nose.tools import *
-from utils import get_fixture
+from .utils import get_fixture
def test_transform():
text = WLDocument.from_file(
get_fixture('text', 'miedzy-nami-nic-nie-bylo.xml')
- ).as_text().get_string()
+ ).as_text().get_bytes()
+
+ assert_equal(text, open(expected_output_file_path, 'rb').read())
+
+
+def test_transform_raw():
+ expected_output_file_path = get_fixture('text', 'asnyk_miedzy_nami_expected_raw.txt')
+
+ text = WLDocument.from_file(
+ get_fixture('text', 'miedzy-nami-nic-nie-bylo.xml')
+ ).as_text(flags=['raw-text']).get_bytes()
- assert_equal(text, file(expected_output_file_path).read())
+ assert_equal(text, open(expected_output_file_path, 'rb').read())
@raises(NoDublinCore)
# This file is part of Librarian, licensed under GNU Affero GPLv3 or later.
# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
#
-from __future__ import with_statement
from os.path import realpath, join, dirname
import glob
--- /dev/null
+[tox]
+envlist =
+ clean,
+ py{27,34,35,36,37},
+ stats
+
+[testenv]
+deps =
+ nose
+ coverage
+passenv = HOME ; Needed to find locally installed fonts when testing PDF production.
+commands =
+ nosetests --with-coverage --cover-package=librarian -d --with-doctest --with-xunit --exe
+install_command = pip install --extra-index-url https://py.mdrn.pl/simple {packages}
+
+[testenv:clean]
+basepython = python2
+commands =
+ coverage erase
+deps = coverage
+
+[testenv:stats]
+basepython = python2
+commands =
+ coverage report
+ coverage html
+deps = coverage
+