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. 24
      pygal/graph/frenchmap.py
  7. 9
      pygal/graph/supranationalworldmap.py
  8. 9
      pygal/graph/worldmap.py
  9. 44
      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 half pie (thanks philt2001)
Add render_table (WIP)
Make lxml an optionnal dependency
V 1.4.6
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/>.
from pygal import CHARTS_NAMES, CHARTS_BY_NAME
from pygal import CHARTS, CHARTS_BY_NAME
from pygal.test import adapt
from random import sample
@ -82,7 +82,7 @@ if '--mem' in sys.argv:
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:
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
# along with pygal. If not, see <http://www.gnu.org/licenses/>.
import os
import sys
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:
base = (str, bytes)
coerce = str
@ -41,6 +52,12 @@ def to_str(string):
return string
def to_unicode(string):
if not isinstance(string, coerce):
return string.decode('utf-8')
return string
def u(s):
if sys.version_info[0] == 2:
return s.decode('utf-8')

2
pygal/ghost.py

@ -123,7 +123,7 @@ class Ghost(object):
def render_pyquery(self):
"""Render the graph, and return a pyquery wrapped tree"""
from pyquery import PyQuery as pq
return pq(self.render_tree())
return pq(self.render(), parser='html')
def render_in_browser(self):
"""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)
def render_tree(self):
"""Render the graph, and return lxml tree"""
"""Render the graph, and return (l)xml etree"""
svg = self.svg.root
for f in self.xml_filters:
svg = f(svg)

24
pygal/graph/frenchmap.py

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

9
pygal/graph/supranationalworldmap.py

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

44
pygal/svg.py

@ -22,13 +22,12 @@ Svg helper
"""
from __future__ import division
from pygal._compat import to_str, u
from pygal._compat import to_str, u, etree
import io
import os
import json
from datetime import date, datetime
from numbers import Number
from lxml import etree
from math import cos, sin, pi
from pygal.util import template, coord_format, minify_css
from pygal import __version__
@ -37,6 +36,7 @@ from pygal import __version__
class Svg(object):
"""Svg object"""
ns = 'http://www.w3.org/2000/svg'
xlink_ns = 'http://www.w3.org/1999/xlink'
def __init__(self, graph):
self.graph = graph
@ -45,19 +45,30 @@ class Svg(object):
else:
self.id = ''
self.processing_instructions = [
etree.PI(u('xml'), u("version='1.0' encoding='utf-8'"))]
self.root = etree.Element(
"{%s}svg" % self.ns,
nsmap={
None: self.ns,
'xlink': 'http://www.w3.org/1999/xlink',
})
etree.ProcessingInstruction(
u('xml'), u("version='1.0' encoding='utf-8'"))]
if etree.lxml:
attrs = {
'nsmap': {
None: self.ns,
'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['class'] = 'pygal-chart'
self.root.append(
etree.Comment(u(
'Generated with pygal %s ©Kozea 2011-2014 on %s' % (
__version__, date.today().isoformat()))))
'Generated with pygal %s (%s) ©Kozea 2011-2014 on %s' % (
__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://github.com/Kozea/pygal')))
self.defs = self.node(tag='defs')
@ -227,14 +238,17 @@ class Svg(object):
"""Last thing to do before rendering"""
for f in self.graph.xml_filters:
self.root = f(self.root)
args = {
'encoding': 'utf-8'
}
if etree.lxml:
args['pretty_print'] = pretty_print
svg = etree.tostring(
self.root, pretty_print=pretty_print,
xml_declaration=False,
encoding='utf-8')
self.root, **args)
if not self.graph.disable_xml_declaration:
svg = b'\n'.join(
[etree.tostring(
pi, encoding='utf-8', pretty_print=pretty_print)
pi, **args)
for pi in self.processing_instructions]
) + b'\n' + svg
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 pygal.i18n import COUNTRIES
from pygal.graph.frenchmap import DEPARTMENTS, REGIONS
from pygal._compat import etree
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/>.
from pygal import Bar
class ChangeBarsXMLFilter(object):
def __init__(self, a, b):
self.data = [b[i] - a[i] for i in range(len(a))]
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 = subplot.render_tree()
subplot = subplot.xpath("g")[0]
subplot = subplot.findall("g")[0]
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
def test_xml_filters_round_trip():
plot = Bar()
plot.add("A", [60, 75, 80, 78, 83, 90])
@ -40,13 +43,16 @@ def test_xml_filters_round_trip():
after = plot.render()
assert before == after
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]
B = [92, 87, 81, 73, 68, 55]
plot.add("A", A)
plot.add("B", B)
plot.add_xml_filter(ChangeBarsXMLFilter(A,B))
plot.add_xml_filter(ChangeBarsXMLFilter(A, B))
q = plot.render_tree()
assert len(q.xpath("g")) == 2
assert q.xpath("g")[1].attrib["transform"] == "translate(0,150), scale(1,0.75)"
assert len(q.findall("g")) == 2
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 pygal._compat import to_str, u, is_list_like
from pygal._compat import u, is_list_like, to_unicode
import re
from decimal import Decimal
from math import floor, pi, log, log10, ceil
@ -241,7 +241,7 @@ def decorate(svg, node, metadata):
if key == 'xlink' and isinstance(value, dict):
value = value.get('href', 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
@ -328,7 +328,7 @@ def prepare_values(raw, config, cls):
from pygal.graph.worldmap import Worldmap
from pygal.graph.frenchmap import FrenchMapDepartments
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)):
config.zero = 1

8
setup.py

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

Loading…
Cancel
Save