diff --git a/setup.cfg b/setup.cfg index 7a38d72..82b6f28 100644 --- a/setup.cfg +++ b/setup.cfg @@ -2,4 +2,7 @@ universal = 1 [metadata] -license_file = LICENSE.txt \ No newline at end of file +license_file = LICENSE.txt + +[pydocstyle] +convention = pep257 \ No newline at end of file diff --git a/setup.py b/setup.py index b90fe80..07b678f 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ import os -VERSION = '1.7.5' +VERSION = '1.8.0' AUTHOR_NAME = 'Andy Port' AUTHOR_EMAIL = 'AndyAPort@gmail.com' GITHUB = 'https://github.com/mathandy/svgpathtools' diff --git a/svgpathtools/__init__.py b/svgpathtools/__init__.py index 7e5da65..abe9f3d 100644 --- a/svgpathtools/__init__.py +++ b/svgpathtools/__init__.py @@ -7,7 +7,7 @@ is_bezier_path, concatpaths, poly2bez, bpoints2bezier, closest_point_in_path, farthest_point_in_path, path_encloses_pt, bbox2path, polygon, polyline) -from .parser import parse_path +from .parser import parse_path, parse_transform, SVGSyntaxWarning from .paths2svg import disvg, wsvg, paths2Drawing from .polytools import polyroots, polyroots01, rational_limit, real, imag from .misctools import hex2rgb, rgb2hex diff --git a/svgpathtools/document.py b/svgpathtools/document.py index d9f477a..6ac8e93 100644 --- a/svgpathtools/document.py +++ b/svgpathtools/document.py @@ -84,7 +84,8 @@ def flattened_paths(group, group_filter=lambda x: True, path_filter=lambda x: True, path_conversions=CONVERSIONS, - group_search_xpath=SVG_GROUP_TAG): + group_search_xpath=SVG_GROUP_TAG, + strict_transform_parsing=False): """Returns the paths inside a group (recursively), expressing the paths in the base coordinates. @@ -102,6 +103,10 @@ def flattened_paths(group, group_filter=lambda x: True, dictionary will be ignored (including the `path` tag). To only convert explicit path elements, pass in `path_conversions=CONVERT_ONLY_PATHS`. + strict_transform_parsing (bool): If true, a ValueError is + raised when a transform attribute contains invalid syntax; + by default invalid transform substrings are skipped with + an SVGSyntaxWarning. """ if not isinstance(group, Element): raise TypeError('Must provide an xml.etree.Element object. ' @@ -123,7 +128,8 @@ def flattened_paths(group, group_filter=lambda x: True, def new_stack_element(element, last_tf): return StackElement(element, last_tf.dot( - parse_transform(element.get('transform')))) + parse_transform(element.get('transform'), + strict=strict_transform_parsing))) def get_relevant_children(parent, last_tf): children = [] @@ -145,7 +151,8 @@ def get_relevant_children(parent, last_tf): for path_elem in filter(path_filter, top.group.iterfind( 'svg:'+key, SVG_NAMESPACE)): path_tf = top.transform.dot( - parse_transform(path_elem.get('transform'))) + parse_transform(path_elem.get('transform'), + strict=strict_transform_parsing)) path = transform(parse_path(converter(path_elem)), path_tf) path.element = path_elem path.transform = path_tf @@ -160,12 +167,17 @@ def flattened_paths_from_group(group_to_flatten, root, recursive=True, group_filter=lambda x: True, path_filter=lambda x: True, path_conversions=CONVERSIONS, - group_search_xpath=SVG_GROUP_TAG): + group_search_xpath=SVG_GROUP_TAG, + strict_transform_parsing=False): """Flatten all the paths in a specific group. The paths will be flattened into the 'root' frame. Note that root needs to be an ancestor of the group that is being flattened. - Otherwise, no paths will be returned.""" + Otherwise, no paths will be returned. + + If `strict_transform_parsing` is true, a ValueError is raised when + a transform attribute contains invalid syntax; by default invalid + transform substrings are skipped with an SVGSyntaxWarning.""" if not any(group_to_flatten is descendant for descendant in root.iter()): warnings.warn('The requested group_to_flatten is not a ' @@ -226,24 +238,33 @@ def desired_path_filter(x): return (id(x) not in ignore_paths) and path_filter(x) return flattened_paths(root, desired_group_filter, desired_path_filter, - path_conversions, group_search_xpath) + path_conversions, group_search_xpath, + strict_transform_parsing=strict_transform_parsing) class Document: - def __init__(self, filepath=None): - """A container for a DOM-style SVG document. + def __init__(self, filepath=None, strict_transform_parsing=False): + """ + A container for a DOM-style SVG document. - The `Document` class provides a simple interface to modify and analyze - the path elements in a DOM-style document. The DOM-style document is + The `Document` class provides a simple interface to modify and analyze + the path elements in a DOM-style document. The DOM-style document is parsed into an ElementTree object (stored in the `tree` attribute). This class provides functions for extracting SVG data into Path objects. The output Path objects will be transformed based on their parent groups. - + Args: filepath (str or file-like): The filepath of the DOM-style object or a file-like object containing it. + strict_transform_parsing (bool): If true, a ValueError is + raised when a transform attribute containing invalid + syntax is parsed (transforms are parsed lazily, by + `paths()` and `paths_from_group()`); by default invalid + transform substrings are skipped with an + SVGSyntaxWarning. """ + self.strict_transform_parsing = strict_transform_parsing # strings are interpreted as file location everything else is treated as # file-like object and passed to the xml parser directly @@ -259,12 +280,13 @@ def __init__(self, filepath=None): self.root = self.tree.getroot() @classmethod - def from_svg_string(cls, svg_string): + def from_svg_string(cls, svg_string, strict_transform_parsing=False): """Constructor for creating a Document object from a string.""" # wrap string into StringIO object svg_file_obj = StringIO(svg_string) # create document from file object - return Document(svg_file_obj) + return Document(svg_file_obj, + strict_transform_parsing=strict_transform_parsing) def paths(self, group_filter=lambda x: True, path_filter=lambda x: True, path_conversions=CONVERSIONS): @@ -273,8 +295,9 @@ def paths(self, group_filter=lambda x: True, Note that any transform attributes are applied before returning the paths. """ - return flattened_paths(self.tree.getroot(), group_filter, - path_filter, path_conversions) + return flattened_paths( + self.tree.getroot(), group_filter, path_filter, path_conversions, + strict_transform_parsing=self.strict_transform_parsing) def paths_from_group(self, group, recursive=True, group_filter=lambda x: True, path_filter=lambda x: True, path_conversions=CONVERSIONS): @@ -292,8 +315,10 @@ def paths_from_group(self, group, recursive=True, group_filter=lambda x: True, warnings.warn("Could not find the requested group!") return [] - return flattened_paths_from_group(group, self.tree.getroot(), recursive, - group_filter, path_filter, path_conversions) + return flattened_paths_from_group( + group, self.tree.getroot(), recursive, group_filter, path_filter, + path_conversions, + strict_transform_parsing=self.strict_transform_parsing) def add_path(self, path, attribs=None, group=None): """Add a new path to the SVG.""" diff --git a/svgpathtools/parser.py b/svgpathtools/parser.py index 955c671..12f6b63 100644 --- a/svgpathtools/parser.py +++ b/svgpathtools/parser.py @@ -1,66 +1,102 @@ -"""This submodule contains the path_parse() function used to convert SVG path -element d-strings into svgpathtools Path objects. -Note: This file was taken (nearly) as is from the svg.path module (v 2.0).""" +""" +Parse SVG path element d-strings into svgpathtools Path objects. + +This submodule contains the parse_path() function. Note: this file was +taken (nearly) as is from the svg.path module (v 2.0). +""" # External dependencies from __future__ import division, absolute_import, print_function +from typing import Optional, Sequence, TYPE_CHECKING +import math +import re import numpy as np import warnings # Internal dependencies from .path import Path +if TYPE_CHECKING: + # Imported for type annotations only; no XML is parsed here. + from xml.etree.ElementTree import Element + +# The SVG number grammar. Stricter than float(), which also accepts +# e.g. '1_0', 'nan', and non-ASCII digits. (The one deliberate +# looseness: '1.' is accepted, though the grammar technically wants a +# digit after the point.) +_NUMBER_RE = re.compile(r'[+-]?([0-9]+(\.[0-9]*)?|\.[0-9]+)([eE][+-]?[0-9]+)?\Z') + -def parse_path(pathdef, current_pos=0j, tree_element=None): +class SVGSyntaxWarning(UserWarning): + """Category for warnings about invalid SVG syntax handled leniently.""" + + +def parse_path(pathdef: str, current_pos: complex = 0j, + tree_element: Optional['Element'] = None) -> Path: + """Convert an SVG path element d-string into a Path object.""" return Path(pathdef, current_pos=current_pos, tree_element=tree_element) -def _check_num_parsed_values(values, allowed): +def _check_num_parsed_values(values: Sequence[float], allowed: Sequence[int], + transform_substr: str) -> None: if not any(num == len(values) for num in allowed): if len(allowed) > 1: - warnings.warn('Expected one of the following number of values {0}, but found {1} values instead: {2}' - .format(allowed, len(values), values)) + raise ValueError('Expected one of the following number of values {0}, ' + 'but found {1} values in {2!r}: {3}' + .format(allowed, len(values), transform_substr, values)) elif allowed[0] != 1: - warnings.warn('Expected {0} values, found {1}: {2}'.format(allowed[0], len(values), values)) + raise ValueError('Expected {0} values in {1!r}, found {2}: {3}' + .format(allowed[0], transform_substr, len(values), values)) else: - warnings.warn('Expected 1 value, found {0}: {1}'.format(len(values), values)) - return False - return True + raise ValueError('Expected 1 value in {0!r}, found {1}: {2}' + .format(transform_substr, len(values), values)) + +def _parse_transform_substr(transform_substr: str) -> np.ndarray: + """ + Convert a single SVG transform substring into a 3x3 matrix. -def _parse_transform_substr(transform_substr): + A well-formed transform substring has the form `type(v1 v2 ...)`. + Raises a ValueError on invalid transform syntax. + """ + if transform_substr.count('(') != 1: + raise ValueError('Invalid SVG transform substring: {0!r}'.format(transform_substr)) type_str, value_str = transform_substr.split('(') - value_str = value_str.replace(',', ' ') - values = list(map(float, filter(None, value_str.split(' ')))) + # Any leading commas/whitespace are the separator from the preceding + # transform in the list, e.g. 'translate(1), rotate(30)'. + type_str = type_str.strip(', \t\n\r') + tokens = value_str.replace(',', ' ').split() + if not all(_NUMBER_RE.match(t) for t in tokens): + raise ValueError('Invalid SVG transform substring: {0!r}'.format(transform_substr)) + values = [float(t) for t in tokens] + # A grammar-valid value can still overflow float, e.g. '1e999'. + if not all(math.isfinite(v) for v in values): + raise ValueError('Non-finite value in SVG transform substring: {0!r}'.format(transform_substr)) transform = np.identity(3) - if 'matrix' in type_str: - if not _check_num_parsed_values(values, [6]): - return transform + if type_str == 'matrix': + _check_num_parsed_values(values, [6], transform_substr) transform[0:2, 0:3] = np.array([values[0:6:2], values[1:6:2]]) - elif 'translate' in transform_substr: - if not _check_num_parsed_values(values, [1, 2]): - return transform + elif type_str == 'translate': + _check_num_parsed_values(values, [1, 2], transform_substr) transform[0, 2] = values[0] if len(values) > 1: transform[1, 2] = values[1] - elif 'scale' in transform_substr: - if not _check_num_parsed_values(values, [1, 2]): - return transform + elif type_str == 'scale': + _check_num_parsed_values(values, [1, 2], transform_substr) x_scale = values[0] y_scale = values[1] if (len(values) > 1) else x_scale transform[0, 0] = x_scale transform[1, 1] = y_scale - elif 'rotate' in transform_substr: - if not _check_num_parsed_values(values, [1, 3]): - return transform + elif type_str == 'rotate': + _check_num_parsed_values(values, [1, 3], transform_substr) angle = values[0] * np.pi / 180.0 if len(values) == 3: @@ -76,35 +112,63 @@ def _parse_transform_substr(transform_substr): transform = tf_offset.dot(tf_rotate).dot(tf_offset_neg) - elif 'skewX' in transform_substr: - if not _check_num_parsed_values(values, [1]): - return transform + elif type_str == 'skewX': + _check_num_parsed_values(values, [1], transform_substr) transform[0, 1] = np.tan(values[0] * np.pi / 180.0) - elif 'skewY' in transform_substr: - if not _check_num_parsed_values(values, [1]): - return transform + elif type_str == 'skewY': + _check_num_parsed_values(values, [1], transform_substr) transform[1, 0] = np.tan(values[0] * np.pi / 180.0) else: - # Return an identity matrix if the type of transform is unknown, and warn the user - warnings.warn('Unknown SVG transform type: {0}'.format(type_str)) + raise ValueError('Unknown SVG transform type: {0}'.format(type_str)) return transform -def parse_transform(transform_str): - """Converts a valid SVG transformation string into a 3x3 matrix. - If the string is empty or null, this returns a 3x3 identity matrix""" - if not transform_str: +def parse_transform(transform_str: Optional[str], strict: bool = False) -> np.ndarray: + """ + Convert a valid SVG transformation string into a 3x3 matrix. + + If the string is empty, null, or 'none', this returns a 3x3 + identity matrix. By default each invalid transform substring is + skipped (i.e. contributes an identity matrix) with a warning. If + `strict` is true, a ValueError is raised on invalid transform + syntax instead. + + The warnings use the SVGSyntaxWarning category, so they can be + silenced or escalated selectively, e.g. + `warnings.simplefilter('error', SVGSyntaxWarning)`. + """ + if transform_str is None or transform_str == '': return np.identity(3) elif not isinstance(transform_str, str): raise TypeError('Must provide a string to parse') + # 'none' is valid (SVG 2 / CSS transform syntax) and means no transform. + if transform_str.strip() == 'none': + return np.identity(3) + + transform_substrs = transform_str.split(')') + # Anything after the last ')' (e.g. a stray 'matrix' with no + # parentheses) is invalid syntax, not a transform to apply -- but a + # trailing list-separator comma is harmless. + trailing = transform_substrs.pop() + if trailing.strip(', \t\n\r'): + if strict: + raise ValueError('Invalid SVG transform substring: {0!r}'.format(trailing)) + warnings.warn('Skipping invalid SVG transform substring: {0!r}'.format(trailing), + SVGSyntaxWarning) + total_transform = np.identity(3) - transform_substrs = transform_str.split(')')[:-1] # Skip the last element, because it should be empty for substr in transform_substrs: - total_transform = total_transform.dot(_parse_transform_substr(substr)) + try: + total_transform = total_transform.dot(_parse_transform_substr(substr)) + except ValueError as e: + if strict: + raise + warnings.warn('Skipping invalid SVG transform substring {0!r}: {1}'.format(substr, e), + SVGSyntaxWarning) return total_transform diff --git a/svgpathtools/svg_io_sax.py b/svgpathtools/svg_io_sax.py index 7faced1..4992a09 100644 --- a/svgpathtools/svg_io_sax.py +++ b/svgpathtools/svg_io_sax.py @@ -44,14 +44,20 @@ class SaxDocument: - def __init__(self, filename): - """A container for a SAX SVG light tree objects document. + def __init__(self, filename, strict_transform_parsing=False): + """ + A container for a SAX SVG light tree objects document. This class provides functions for extracting SVG data into Path objects. Args: filename (str): The filename of the SVG file + strict_transform_parsing (bool): If true, a ValueError is + raised when a transform attribute contains invalid + syntax; by default invalid transform substrings are + skipped with an SVGSyntaxWarning. """ + self.strict_transform_parsing = strict_transform_parsing self.root_values = {} self.tree = [] # remember location of original svg file @@ -69,57 +75,63 @@ def sax_parse(self, filename): stack = [] values = {} matrix = None - for event, elem in iterparse(filename, events=('start', 'end')): - if event == 'start': - stack.append((values, matrix)) - if matrix is not None: - matrix = matrix.copy() # copy of matrix - current_values = values - values = {} - values.update(current_values) # copy of dictionary - attrs = elem.attrib - values.update(attrs) - name = elem.tag[28:] - if "style" in attrs: - for equate in attrs["style"].split(";"): - equal_item = equate.split(":") - values[equal_item[0]] = equal_item[1] - if "transform" in attrs: - transform_matrix = parse_transform(attrs["transform"]) - if matrix is None: - matrix = np.identity(3) - matrix = transform_matrix.dot(matrix) - if "svg" == name: + # Open the file ourselves (rather than letting iterparse do it) + # so the handle is closed even if parsing raises; otherwise the + # file stays locked on Windows until garbage collection. + with open(filename, 'rb') as svg_file: + for event, elem in iterparse(svg_file, events=('start', 'end')): + if event == 'start': + stack.append((values, matrix)) + if matrix is not None: + matrix = matrix.copy() # copy of matrix current_values = values values = {} - values.update(current_values) - self.root_values = current_values - continue - elif "g" == name: - continue - elif 'path' == name: - values['d'] = path2pathd(values) - elif 'circle' == name: - values["d"] = ellipse2pathd(values) - elif 'ellipse' == name: - values["d"] = ellipse2pathd(values) - elif 'line' == name: - values["d"] = line2pathd(values) - elif 'polyline' == name: - values["d"] = polyline2pathd(values) - elif 'polygon' == name: - values["d"] = polygon2pathd(values) - elif 'rect' == name: - values["d"] = rect2pathd(values) + values.update(current_values) # copy of dictionary + attrs = elem.attrib + values.update(attrs) + name = elem.tag[28:] + if "style" in attrs: + for equate in attrs["style"].split(";"): + equal_item = equate.split(":") + values[equal_item[0]] = equal_item[1] + if "transform" in attrs: + transform_matrix = parse_transform( + attrs["transform"], + strict=self.strict_transform_parsing) + if matrix is None: + matrix = np.identity(3) + matrix = transform_matrix.dot(matrix) + if "svg" == name: + current_values = values + values = {} + values.update(current_values) + self.root_values = current_values + continue + elif "g" == name: + continue + elif 'path' == name: + values['d'] = path2pathd(values) + elif 'circle' == name: + values["d"] = ellipse2pathd(values) + elif 'ellipse' == name: + values["d"] = ellipse2pathd(values) + elif 'line' == name: + values["d"] = line2pathd(values) + elif 'polyline' == name: + values["d"] = polyline2pathd(values) + elif 'polygon' == name: + values["d"] = polygon2pathd(values) + elif 'rect' == name: + values["d"] = rect2pathd(values) + else: + continue + values["matrix"] = matrix + values["name"] = name + self.tree.append(values) else: - continue - values["matrix"] = matrix - values["name"] = name - self.tree.append(values) - else: - v = stack.pop() - values = v[0] - matrix = v[1] + v = stack.pop() + values = v[0] + matrix = v[1] def flatten_all_paths(self): flat = [] diff --git a/test/test_parsing.py b/test/test_parsing.py index 6ef5c9d..90392e1 100644 --- a/test/test_parsing.py +++ b/test/test_parsing.py @@ -1,6 +1,9 @@ # Note: This file was taken mostly as is from the svg.path module (v 2.0) from __future__ import division, absolute_import, print_function +import os +import tempfile import unittest +import warnings from svgpathtools import Path, Line, QuadraticBezier, CubicBezier, Arc, parse_path import svgpathtools @@ -295,6 +298,152 @@ def test_transform(self): scale(10 0.5)""") )) + def test_transform_whitespace(self): + # Values may be separated by any whitespace, not just spaces. + expected_tf_matrix = np.identity(3) + expected_tf_matrix[0:2, 0:3] = np.array([[1.0, 3.0, 5.0], + [2.0, 4.0, 6.0]]) + tf_matrix = svgpathtools.parser.parse_transform( + 'matrix(1, 2,\n3 4\t5 6)') + self.assertTrue(np.array_equal(expected_tf_matrix, tf_matrix)) + + def test_transform_malformed(self): + bad_transforms = ('matrix(1 x 3 4 5 6)', # non-numeric value + 'translate(a)', # non-numeric value + 'scale()', # wrong number of values + 'rotate(1 2 z)', # non-numeric value + 'bogus(5)', # unknown transform type + 'notmatrix(1 0 0 1 0 0)', # unknown transform type + 'translate(nan)', # non-finite value + 'scale(1 inf)', # non-finite value + 'translate(1_0)', # not an SVG number + 'scale(1e999)', # overflows to inf + 'foo(1', # missing closing paren + 'matrix', # no parens at all + 'matrix(1(2)', # extra opening paren + 'rotate(30))') # stray closing paren + + # By default, each invalid substring warns and contributes an + # identity matrix. ('rotate(30))' is excluded because its valid + # 'rotate(30)' part still applies.) + identity = np.identity(3) + for bad in bad_transforms[:-1]: + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter('always') + tf = svgpathtools.parser.parse_transform(bad) + self.assertTrue(caught, msg=bad) + self.assertTrue(np.array_equal(identity, tf), msg=bad) + + # Valid substrings still apply alongside skipped invalid ones. + expected_tf_translate = np.identity(3) + expected_tf_translate[0, 2] = 10 + expected_tf_translate[1, 2] = 20 + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter('always') + tf = svgpathtools.parser.parse_transform( + 'translate(10 20) matrix(1 x 3 4 5 6)') + self.assertTrue(caught) + self.assertTrue(np.array_equal(expected_tf_translate, tf)) + + # With strict=True, invalid transform syntax raises a ValueError. + for bad in bad_transforms: + with self.assertRaises(ValueError, msg=bad): + svgpathtools.parser.parse_transform(bad, strict=True) + + # Error messages identify the offending substring. + with self.assertRaisesRegex(ValueError, r'rotate\(1 2'): + svgpathtools.parser.parse_transform('rotate(1 2)', strict=True) + + # None, '' and 'none' yield identity silently, even in strict + # mode; other non-strings raise TypeError. + for empty in (None, '', 'none', ' none '): + with warnings.catch_warnings(): + warnings.simplefilter('error') + tf = svgpathtools.parser.parse_transform(empty, strict=True) + self.assertTrue(np.array_equal(identity, tf), msg=repr(empty)) + for bad_type in (0, False, [], 0.5): + with self.assertRaises(TypeError, msg=repr(bad_type)): + svgpathtools.parser.parse_transform(bad_type) + + def test_transform_separators(self): + # Transforms in a list may be separated by whitespace, commas, or + # (leniently) nothing at all; all should parse identically, in + # strict mode too. + expected = svgpathtools.parser.parse_transform( + 'translate(10 20) rotate(30)', strict=True) + for tf_str in ('translate(10 20),rotate(30)', + 'translate(10 20) , rotate(30)', + 'translate(10 20)\nrotate(30)', + 'translate(10 20)rotate(30)', + # A trailing list-separator comma is harmless. + 'translate(10 20),rotate(30),', + 'translate(10 20),rotate(30) , '): + with warnings.catch_warnings(): + warnings.simplefilter('error') + tf = svgpathtools.parser.parse_transform(tf_str, strict=True) + self.assertTrue(np.array_equal(expected, tf), msg=tf_str) + + def test_transform_warning_category(self): + # Lenient-mode warnings use SVGSyntaxWarning, a UserWarning + # subclass, so they can be filtered or escalated selectively + # while existing UserWarning filters keep working. + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter('always') + svgpathtools.parse_transform('translate(a)') + self.assertTrue(caught) + for w in caught: + self.assertTrue(issubclass(w.category, + svgpathtools.SVGSyntaxWarning)) + self.assertTrue(issubclass(w.category, UserWarning)) + + def test_document_strict_transform_parsing(self): + svg = ('' + '') + + # Default: lenient, warns with SVGSyntaxWarning. + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter('always') + paths = svgpathtools.Document.from_svg_string(svg).paths() + self.assertEqual(len(paths), 1) + self.assertTrue(any(issubclass(w.category, + svgpathtools.SVGSyntaxWarning) + for w in caught)) + + # The warning category can be escalated to an error. + with warnings.catch_warnings(): + warnings.simplefilter('error', svgpathtools.SVGSyntaxWarning) + with self.assertRaises(svgpathtools.SVGSyntaxWarning): + svgpathtools.Document.from_svg_string(svg).paths() + + # Opt-in strict parsing raises ValueError. + doc = svgpathtools.Document.from_svg_string( + svg, strict_transform_parsing=True) + with self.assertRaises(ValueError): + doc.paths() + + def test_sax_document_strict_transform_parsing(self): + svg = ('' + '') + fd, fname = tempfile.mkstemp(suffix='.svg') + try: + with os.fdopen(fd, 'w') as f: + f.write(svg) + + # Default: lenient, warns with SVGSyntaxWarning. + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter('always') + svgpathtools.SaxDocument(fname) + self.assertTrue(any(issubclass(w.category, + svgpathtools.SVGSyntaxWarning) + for w in caught)) + + # Opt-in strict parsing raises ValueError. + with self.assertRaises(ValueError): + svgpathtools.SaxDocument(fname, + strict_transform_parsing=True) + finally: + os.remove(fname) + def test_pathd_init(self): path0 = Path('') path1 = parse_path("M 100 100 L 300 100 L 200 300 z")