diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 00000000..56e4e57d --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,41 @@ +environment: + + matrix: + # For Python versions available on Appveyor, see + # http://www.appveyor.com/docs/installed-software#python + - PYTHON: "C:\\Python36-x64" + PYTHON_VERSION: "3.6" + PYTHON_ARCH: "64" + +init: + - "ECHO %PYTHON% %PYTHON_VERSION% %PYTHON_ARCH%" + + # Add Python binaries to the path + - "SET PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%" + - "python --version" + + +install: + # Upgrade PIP and setuptools to allow installation of pre-compiled wheels instead of compiling by ourselves + - "python -m pip install --upgrade pip" + - "pip install --upgrade setuptools" + + - "pip install -e .[test]" + +build: off + +test_script: + # Run test files via py.test and generate JUnit XML. Then push test results + # to appveyor. The plugin pytest-cov takes care of coverage. + - ps: | + & pytest -W ignore::DeprecationWarning --junitxml=.\unittests.xml pygal/test + $testsExitCode = $lastexitcode + + # Upload test results to AppVeyor + $wc = New-Object 'System.Net.WebClient' + $wc.UploadFile("https://ci.appveyor.com/api/testresults/junit/$($env:APPVEYOR_JOB_ID)", (Resolve-Path .\unittests.xml)) + + if ($testsExitCode -ne 0) {exit $testsExitCode} + +artifacts: + - path: .\unittests.xml diff --git a/pygal/svg.py b/pygal/svg.py index 5afdef6d..e828ea57 100644 --- a/pygal/svg.py +++ b/pygal/svg.py @@ -23,6 +23,7 @@ import io import json import os +import urllib from datetime import date, datetime from math import pi from numbers import Number @@ -84,6 +85,38 @@ def __init__(self, graph): for def_ in self.graph.defs: self.defs.append(etree.fromstring(def_)) + def file_uri_to_file_path(self, file_uri): + """ + Convert file URI ("file://...") to file path without "file://" (in a simplest case). + + On Windows, file uri "file:///c:/dir/file.ext" is converted to "c:/dir/file.ext". + On *nix, file uri "file:///dir/file.ext" is converted to "/dir/file.ext". + The escaped characters like "%20" (space) are unescaped. + + Examples: + + >>> Svg.file_uri_to_path('file:///home/user/file.ext') + '/home/user/file.ext' + + >>> Svg.file_uri_to_path('file:///c:/temp/file.ext') + 'c:/temp/file.ext' + + >>> Svg.file_uri_to_path('file:///c:/Program%20Files/file.ext') + 'c:/Program files/file.ext' + """ + + if not file_uri.startswith('file://'): + raise ValueError("Invalid file uri {}: should start with 'file://'".format(file_uri)) + + file_path = urllib.parse.unquote(file_uri[len('file://'):]) + + # Remove leading "/" character if a Windows-like drive letter is present, like: "/c:/dir/..." + if len(file_path) > 3 and file_path[0] == '/' and file_path[2] == ':': + file_path = file_path[1:] + + return file_path + + def add_styles(self): """Add the css to the svg""" colors = self.graph.style.get_colors(self.id, self.graph._order) @@ -102,7 +135,8 @@ def add_styles(self): if css.startswith('inline:'): css_text = css[len('inline:'):] elif css.startswith('file://'): - css = css[len('file://'):] + css = self.file_uri_to_file_path(css) + if not os.path.exists(css): css = os.path.join(os.path.dirname(__file__), 'css', css) @@ -168,7 +202,7 @@ def json_default(o): for js in self.graph.js: if js.startswith('file://'): script = self.node(self.defs, 'script', type='text/javascript') - with io.open(js[len('file://'):], encoding='utf-8') as f: + with io.open(self.file_uri_to_file_path(js), encoding='utf-8') as f: script.text = f.read() else: if js.startswith('//') and self.graph.force_uri_protocol: diff --git a/pygal/test/test_config.py b/pygal/test/test_config.py index 758a2eca..c89cc3e2 100644 --- a/pygal/test/test_config.py +++ b/pygal/test/test_config.py @@ -19,6 +19,8 @@ """Various config options tested on one chart type or more""" from tempfile import NamedTemporaryFile +import pathlib +import os from pygal import ( XY, Bar, Box, Config, DateLine, DateTimeLine, Dot, Funnel, Gauge, @@ -334,22 +336,28 @@ def test_include_x_axis(Chart): def test_css(Chart): """Test css file option""" css = "{{ id }}text { fill: #bedead; }\n" - with NamedTemporaryFile('w') as f: - f.write(css) - f.flush() + f = NamedTemporaryFile('w', suffix='.css', delete=False) + filename = f.name + f.write(css) + f.close() # close the file to avoid the permission issue on Window when opening it for reading below + + try: config = Config() - config.css.append('file://' + f.name) + print(f.name) + config.css.append(pathlib.Path(f.name).as_uri()) chart = Chart(config) chart.add('/', [10, 1, 5]) svg = chart.render().decode('utf-8') assert '#bedead' in svg - chart = Chart(css=(_ellipsis, 'file://' + f.name)) + chart = Chart(css=(_ellipsis, pathlib.Path(f.name).as_uri())) chart.add('/', [10, 1, 5]) svg = chart.render().decode('utf-8') assert '#bedead' in svg + finally: + os.remove(filename) def test_inline_css(Chart): diff --git a/pygal/test/test_graph.py b/pygal/test/test_graph.py index 6127dc4c..a3592412 100644 --- a/pygal/test/test_graph.py +++ b/pygal/test/test_graph.py @@ -32,7 +32,7 @@ try: import cairosvg -except ImportError: +except (ImportError, OSError): # OSError is raised on Windows if cairo's DLLs are missing cairosvg = None @@ -45,9 +45,9 @@ def test_multi_render(Chart, datas): assert svg == chart.render() -def test_render_to_file(Chart, datas): +def test_render_to_file(Chart, datas, tmpdir): """Test in file rendering""" - file_name = '/tmp/test_graph-%s.svg' % uuid.uuid4() + file_name = str(tmpdir.join('test_graph-%s.png' % uuid.uuid4())) if os.path.exists(file_name): os.remove(file_name) @@ -60,9 +60,9 @@ def test_render_to_file(Chart, datas): @pytest.mark.skipif(not cairosvg, reason="CairoSVG not installed") -def test_render_to_png(Chart, datas): +def test_render_to_png(Chart, datas, tmpdir): """Test in file png rendering""" - file_name = '/tmp/test_graph-%s.png' % uuid.uuid4() + file_name = str(tmpdir.join('test_graph-%s.png' % uuid.uuid4())) if os.path.exists(file_name): os.remove(file_name)