Browse Source

Make lxml an optionnal dependency

pull/130/head
Florian Mounier 11 years ago
parent
commit
ad3464a682
  1. 1
      CHANGELOG
  2. 4
      perf.py
  3. 17
      pygal/_compat.py
  4. 2
      pygal/ghost.py
  5. 2
      pygal/graph/base.py
  6. 22
      pygal/graph/frenchmap.py
  7. 9
      pygal/graph/supranationalworldmap.py
  8. 9
      pygal/graph/worldmap.py
  9. 42
      pygal/svg.py
  10. 1
      pygal/test/__init__.py
  11. 20
      pygal/test/test_xml_filters.py
  12. 6
      pygal/util.py
  13. 8
      setup.py

1
CHANGELOG

@ -2,6 +2,7 @@ V 1.5.0 UNRELEASED
Add per serie configuration Add per serie configuration
Add half pie (thanks philt2001) Add half pie (thanks philt2001)
Add render_table (WIP) Add render_table (WIP)
Make lxml an optionnal dependency
V 1.4.6 V 1.4.6
Add support for \n separated multiline titles (thanks sirlark) Add support for \n separated multiline titles (thanks sirlark)

4
perf.py

@ -18,7 +18,7 @@
# along with pygal. If not, see <http://www.gnu.org/licenses/>. # along with pygal. If not, see <http://www.gnu.org/licenses/>.
from pygal import CHARTS_NAMES, CHARTS_BY_NAME from pygal import CHARTS, CHARTS_BY_NAME
from pygal.test import adapt from pygal.test import adapt
from random import sample from random import sample
@ -82,7 +82,7 @@ if '--mem' in sys.argv:
sys.exit(0) sys.exit(0)
charts = CHARTS_NAMES if '--all' in sys.argv else 'Line', charts = CHARTS if '--all' in sys.argv else 'Line',
for chart in charts: for chart in charts:
prt('%s\n' % chart) prt('%s\n' % chart)

17
pygal/_compat.py

@ -16,9 +16,20 @@
# #
# You should have received a copy of the GNU Lesser General Public License # You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>. # along with pygal. If not, see <http://www.gnu.org/licenses/>.
import os
import sys import sys
from collections import Iterable from collections import Iterable
try:
if os.getenv('NO_LXML', None):
raise ImportError('Explicit lxml bypass')
from lxml import etree
etree.lxml = True
except ImportError:
from xml.etree import ElementTree as etree
etree.lxml = False
if sys.version_info[0] == 3: if sys.version_info[0] == 3:
base = (str, bytes) base = (str, bytes)
coerce = str coerce = str
@ -41,6 +52,12 @@ def to_str(string):
return string return string
def to_unicode(string):
if not isinstance(string, coerce):
return string.decode('utf-8')
return string
def u(s): def u(s):
if sys.version_info[0] == 2: if sys.version_info[0] == 2:
return s.decode('utf-8') return s.decode('utf-8')

2
pygal/ghost.py

@ -123,7 +123,7 @@ class Ghost(object):
def render_pyquery(self): def render_pyquery(self):
"""Render the graph, and return a pyquery wrapped tree""" """Render the graph, and return a pyquery wrapped tree"""
from pyquery import PyQuery as pq from pyquery import PyQuery as pq
return pq(self.render_tree()) return pq(self.render(), parser='html')
def render_in_browser(self): def render_in_browser(self):
"""Render the graph, open it in your browser with black magic""" """Render the graph, open it in your browser with black magic"""

2
pygal/graph/base.py

@ -293,7 +293,7 @@ class BaseGraph(object):
is_unicode=is_unicode, pretty_print=self.pretty_print) is_unicode=is_unicode, pretty_print=self.pretty_print)
def render_tree(self): def render_tree(self):
"""Render the graph, and return lxml tree""" """Render the graph, and return (l)xml etree"""
svg = self.svg.root svg = self.svg.root
for f in self.xml_filters: for f in self.xml_filters:
svg = f(svg) svg = f(svg)

22
pygal/graph/frenchmap.py

@ -26,9 +26,8 @@ from collections import defaultdict
from pygal.ghost import ChartCollection from pygal.ghost import ChartCollection
from pygal.util import cut, cached_property, decorate from pygal.util import cut, cached_property, decorate
from pygal.graph.graph import Graph from pygal.graph.graph import Graph
from pygal._compat import u from pygal._compat import u, etree
from numbers import Number from numbers import Number
from lxml import etree
import os import os
@ -190,6 +189,7 @@ class FrenchMapDepartments(Graph):
x_labels = list(DEPARTMENTS.keys()) x_labels = list(DEPARTMENTS.keys())
area_names = DEPARTMENTS area_names = DEPARTMENTS
area_prefix = 'z' area_prefix = 'z'
kind = 'departement'
svg_map = DPT_MAP svg_map = DPT_MAP
@ -222,9 +222,10 @@ class FrenchMapDepartments(Graph):
ratio = 1 ratio = 1
else: else:
ratio = .3 + .7 * (value - min_) / (max_ - min_) ratio = .3 + .7 * (value - min_) / (max_ - min_)
areae = map.xpath( areae = map.findall(
"//*[contains(concat(' ', normalize-space(@class), ' ')," ".//*[@class='%s%s %s map-element']" % (
" ' %s%s ')]" % (self.area_prefix, area_code)) self.area_prefix, area_code,
self.kind))
if not areae: if not areae:
continue continue
@ -236,14 +237,16 @@ class FrenchMapDepartments(Graph):
metadata = serie.metadata.get(j) metadata = serie.metadata.get(j)
if metadata: if metadata:
parent = area.getparent()
node = decorate(self.svg, area, metadata) node = decorate(self.svg, area, metadata)
if node != area: if node != area:
area.remove(node) area.remove(node)
index = parent.index(area) for g in map:
parent.remove(area) if area not in g:
continue
index = list(g).index(area)
g.remove(area)
node.append(area) node.append(area)
parent.insert(index, node) g.insert(index, node)
last_node = len(area) > 0 and area[-1] last_node = len(area) > 0 and area[-1]
if last_node is not None and last_node.tag == 'title': if last_node is not None and last_node.tag == 'title':
@ -265,6 +268,7 @@ class FrenchMapRegions(FrenchMapDepartments):
area_names = REGIONS area_names = REGIONS
area_prefix = 'a' area_prefix = 'a'
svg_map = REG_MAP svg_map = REG_MAP
kind = 'region'
class FrenchMap(ChartCollection): class FrenchMap(ChartCollection):

9
pygal/graph/supranationalworldmap.py

@ -25,7 +25,7 @@ from __future__ import division
from pygal.graph.worldmap import Worldmap from pygal.graph.worldmap import Worldmap
from pygal.i18n import SUPRANATIONAL from pygal.i18n import SUPRANATIONAL
from pygal.util import cut, decorate from pygal.util import cut, decorate
from lxml import etree from pygal._compat import etree
import os import os
with open(os.path.join( with open(os.path.join(
@ -68,14 +68,13 @@ class SupranationalWorldmap(Worldmap):
metadata = serie.metadata.get(j) metadata = serie.metadata.get(j)
if metadata: if metadata:
parent = country.getparent()
node = decorate(self.svg, country, metadata) node = decorate(self.svg, country, metadata)
if node != country: if node != country:
country.remove(node) country.remove(node)
index = parent.index(country) index = list(map).index(country)
parent.remove(country) map.remove(country)
node.append(country) node.append(country)
parent.insert(index, node) map.insert(index, node)
last_node = len(country) > 0 and country[-1] last_node = len(country) > 0 and country[-1]
if last_node is not None and last_node.tag == 'title': if last_node is not None and last_node.tag == 'title':

9
pygal/graph/worldmap.py

@ -25,7 +25,7 @@ from __future__ import division
from pygal.util import cut, cached_property, decorate from pygal.util import cut, cached_property, decorate
from pygal.graph.graph import Graph from pygal.graph.graph import Graph
from pygal.i18n import COUNTRIES from pygal.i18n import COUNTRIES
from lxml import etree from pygal._compat import etree
import os import os
with open(os.path.join( with open(os.path.join(
@ -86,14 +86,13 @@ class Worldmap(Graph):
metadata = serie.metadata.get(j) metadata = serie.metadata.get(j)
if metadata: if metadata:
parent = country.getparent()
node = decorate(self.svg, country, metadata) node = decorate(self.svg, country, metadata)
if node != country: if node != country:
country.remove(node) country.remove(node)
index = parent.index(country) index = list(map).index(country)
parent.remove(country) map.remove(country)
node.append(country) node.append(country)
parent.insert(index, node) map.insert(index, node)
last_node = len(country) > 0 and country[-1] last_node = len(country) > 0 and country[-1]
if last_node is not None and last_node.tag == 'title': if last_node is not None and last_node.tag == 'title':

42
pygal/svg.py

@ -22,13 +22,12 @@ Svg helper
""" """
from __future__ import division from __future__ import division
from pygal._compat import to_str, u from pygal._compat import to_str, u, etree
import io import io
import os import os
import json import json
from datetime import date, datetime from datetime import date, datetime
from numbers import Number from numbers import Number
from lxml import etree
from math import cos, sin, pi from math import cos, sin, pi
from pygal.util import template, coord_format, minify_css from pygal.util import template, coord_format, minify_css
from pygal import __version__ from pygal import __version__
@ -37,6 +36,7 @@ from pygal import __version__
class Svg(object): class Svg(object):
"""Svg object""" """Svg object"""
ns = 'http://www.w3.org/2000/svg' ns = 'http://www.w3.org/2000/svg'
xlink_ns = 'http://www.w3.org/1999/xlink'
def __init__(self, graph): def __init__(self, graph):
self.graph = graph self.graph = graph
@ -45,19 +45,30 @@ class Svg(object):
else: else:
self.id = '' self.id = ''
self.processing_instructions = [ self.processing_instructions = [
etree.PI(u('xml'), u("version='1.0' encoding='utf-8'"))] etree.ProcessingInstruction(
self.root = etree.Element( u('xml'), u("version='1.0' encoding='utf-8'"))]
"{%s}svg" % self.ns, if etree.lxml:
nsmap={ attrs = {
'nsmap': {
None: self.ns, None: self.ns,
'xlink': 'http://www.w3.org/1999/xlink', 'xlink': self.xlink_ns
}) }
}
else:
attrs = {
'xmlns': self.ns
}
etree.register_namespace('xlink', self.xlink_ns)
self.root = etree.Element('svg', **attrs)
self.root.attrib['id'] = self.id.lstrip('#').rstrip() self.root.attrib['id'] = self.id.lstrip('#').rstrip()
self.root.attrib['class'] = 'pygal-chart' self.root.attrib['class'] = 'pygal-chart'
self.root.append( self.root.append(
etree.Comment(u( etree.Comment(u(
'Generated with pygal %s ©Kozea 2011-2014 on %s' % ( 'Generated with pygal %s (%s) ©Kozea 2011-2014 on %s' % (
__version__, date.today().isoformat())))) __version__,
'lxml' if etree.lxml else 'etree',
date.today().isoformat()))))
self.root.append(etree.Comment(u('http://pygal.org'))) self.root.append(etree.Comment(u('http://pygal.org')))
self.root.append(etree.Comment(u('http://github.com/Kozea/pygal'))) self.root.append(etree.Comment(u('http://github.com/Kozea/pygal')))
self.defs = self.node(tag='defs') self.defs = self.node(tag='defs')
@ -227,14 +238,17 @@ class Svg(object):
"""Last thing to do before rendering""" """Last thing to do before rendering"""
for f in self.graph.xml_filters: for f in self.graph.xml_filters:
self.root = f(self.root) self.root = f(self.root)
args = {
'encoding': 'utf-8'
}
if etree.lxml:
args['pretty_print'] = pretty_print
svg = etree.tostring( svg = etree.tostring(
self.root, pretty_print=pretty_print, self.root, **args)
xml_declaration=False,
encoding='utf-8')
if not self.graph.disable_xml_declaration: if not self.graph.disable_xml_declaration:
svg = b'\n'.join( svg = b'\n'.join(
[etree.tostring( [etree.tostring(
pi, encoding='utf-8', pretty_print=pretty_print) pi, **args)
for pi in self.processing_instructions] for pi in self.processing_instructions]
) + b'\n' + svg ) + b'\n' + svg
if self.graph.disable_xml_declaration or is_unicode: if self.graph.disable_xml_declaration or is_unicode:

1
pygal/test/__init__.py

@ -22,6 +22,7 @@ from pygal.util import cut
from datetime import datetime from datetime import datetime
from pygal.i18n import COUNTRIES from pygal.i18n import COUNTRIES
from pygal.graph.frenchmap import DEPARTMENTS, REGIONS from pygal.graph.frenchmap import DEPARTMENTS, REGIONS
from pygal._compat import etree
def get_data(i): def get_data(i):

20
pygal/test/test_xml_filters.py

@ -18,19 +18,22 @@
# along with pygal. If not, see <http://www.gnu.org/licenses/>. # along with pygal. If not, see <http://www.gnu.org/licenses/>.
from pygal import Bar from pygal import Bar
class ChangeBarsXMLFilter(object): class ChangeBarsXMLFilter(object):
def __init__(self, a, b): def __init__(self, a, b):
self.data = [b[i] - a[i] for i in range(len(a))] self.data = [b[i] - a[i] for i in range(len(a))]
def __call__(self, T): def __call__(self, T):
subplot = Bar(legend_at_bottom=True, explicit_size=True, width=800, height=150) subplot = Bar(legend_at_bottom=True, explicit_size=True,
width=800, height=150)
subplot.add("Difference", self.data) subplot.add("Difference", self.data)
subplot = subplot.render_tree() subplot = subplot.render_tree()
subplot = subplot.xpath("g")[0] subplot = subplot.findall("g")[0]
T.insert(2, subplot) T.insert(2, subplot)
T.xpath("g")[1].set('transform', 'translate(0,150), scale(1,0.75)') T.findall("g")[1].set('transform', 'translate(0,150), scale(1,0.75)')
return T return T
def test_xml_filters_round_trip(): def test_xml_filters_round_trip():
plot = Bar() plot = Bar()
plot.add("A", [60, 75, 80, 78, 83, 90]) plot.add("A", [60, 75, 80, 78, 83, 90])
@ -40,13 +43,16 @@ def test_xml_filters_round_trip():
after = plot.render() after = plot.render()
assert before == after assert before == after
def test_xml_filters_change_bars(): def test_xml_filters_change_bars():
plot = Bar(legend_at_bottom=True, explicit_size=True, width=800, height=600) plot = Bar(legend_at_bottom=True, explicit_size=True,
width=800, height=600)
A = [60, 75, 80, 78, 83, 90] A = [60, 75, 80, 78, 83, 90]
B = [92, 87, 81, 73, 68, 55] B = [92, 87, 81, 73, 68, 55]
plot.add("A", A) plot.add("A", A)
plot.add("B", B) plot.add("B", B)
plot.add_xml_filter(ChangeBarsXMLFilter(A,B)) plot.add_xml_filter(ChangeBarsXMLFilter(A, B))
q = plot.render_tree() q = plot.render_tree()
assert len(q.xpath("g")) == 2 assert len(q.findall("g")) == 2
assert q.xpath("g")[1].attrib["transform"] == "translate(0,150), scale(1,0.75)" assert q.findall("g")[1].attrib[
"transform"] == "translate(0,150), scale(1,0.75)"

6
pygal/util.py

@ -21,7 +21,7 @@ Various utils
""" """
from __future__ import division from __future__ import division
from pygal._compat import to_str, u, is_list_like from pygal._compat import u, is_list_like, to_unicode
import re import re
from decimal import Decimal from decimal import Decimal
from math import floor, pi, log, log10, ceil from math import floor, pi, log, log10, ceil
@ -241,7 +241,7 @@ def decorate(svg, node, metadata):
if key == 'xlink' and isinstance(value, dict): if key == 'xlink' and isinstance(value, dict):
value = value.get('href', value) value = value.get('href', value)
if value: if value:
svg.node(node, 'desc', class_=key).text = to_str(value) svg.node(node, 'desc', class_=key).text = to_unicode(value)
return node return node
@ -328,7 +328,7 @@ def prepare_values(raw, config, cls):
from pygal.graph.worldmap import Worldmap from pygal.graph.worldmap import Worldmap
from pygal.graph.frenchmap import FrenchMapDepartments from pygal.graph.frenchmap import FrenchMapDepartments
if config.x_labels is None and hasattr(cls, 'x_labels'): if config.x_labels is None and hasattr(cls, 'x_labels'):
config.x_labels = cls.x_labels config.x_labels = list(map(to_unicode, cls.x_labels))
if config.zero == 0 and issubclass(cls, (Worldmap, FrenchMapDepartments)): if config.zero == 0 and issubclass(cls, (Worldmap, FrenchMapDepartments)):
config.zero = 1 config.zero = 1

8
setup.py

@ -32,7 +32,7 @@ class PyTest(TestCommand):
self.test_suite = True self.test_suite = True
def run_tests(self): def run_tests(self):
#import here, cause outside the eggs aren't loaded # import here, cause outside the eggs aren't loaded
import pytest import pytest
errno = pytest.main(self.test_args) errno = pytest.main(self.test_args)
sys.exit(errno) sys.exit(errno)
@ -65,7 +65,11 @@ setup(
tests_require=["pytest", "pyquery", "flask", "cairosvg"], tests_require=["pytest", "pyquery", "flask", "cairosvg"],
cmdclass={'test': PyTest}, cmdclass={'test': PyTest},
package_data={'pygal': ['css/*', 'graph/*.svg']}, package_data={'pygal': ['css/*', 'graph/*.svg']},
install_requires=['lxml'], extra_requires={
'lxml': ['lxml'],
'png': ['cairosvg']
},
classifiers=[ classifiers=[
"Development Status :: 4 - Beta", "Development Status :: 4 - Beta",
"Environment :: Console", "Environment :: Console",

Loading…
Cancel
Save