From 40c0d99cbbe8456e87a47adca9930f4018b63c94 Mon Sep 17 00:00:00 2001 From: Martin Owens Date: Thu, 19 Sep 2024 09:09:10 -0400 Subject: [PATCH] Automatically register namespaces when using custom elements Previous we required extension authors to pre-register their xml namespaces in order to use them in their inkex svg parsing. But this is cumbersom as it required the namespace registeration to happen once before the class is created and then once again after the svg has been created. We tidy up our registerNS so it handles the etree registration. We rewrite addNS since it was doing problematic things, and one of the tests was even wrong. Then we allow custom elements to register thier custom namespaces on initalisation. Finally we make sure our namepspaces are registered with the svg when elements are added to the document allowing namespace prefixes can be renamed if they are in the ns%f format lxml will create sometimes. --- docs/tutorial/document-namespaces.rst | 187 ++++++++++++++++++ ...ort-extension.rst => import-extension.rst} | 2 +- docs/tutorial/index.rst | 5 +- ...-text-extension.rst => text-extension.rst} | 2 +- inkex/elements/__init__.py | 2 +- inkex/elements/_base.py | 22 ++- inkex/elements/_parser.py | 11 +- inkex/elements/_svg.py | 56 ++++-- inkex/elements/_utils.py | 165 ++++++++++++++-- jessyink_install.py | 3 +- jessyink_mouse_handler.py | 1 + tests/test_inkex_elements_base.py | 83 +++++++- tests/test_inkex_svg.py | 14 ++ tests/test_inkex_utils.py | 29 +-- 14 files changed, 533 insertions(+), 49 deletions(-) create mode 100644 docs/tutorial/document-namespaces.rst rename docs/tutorial/{my-first-import-extension.rst => import-extension.rst} (99%) rename docs/tutorial/{my-first-text-extension.rst => text-extension.rst} (99%) diff --git a/docs/tutorial/document-namespaces.rst b/docs/tutorial/document-namespaces.rst new file mode 100644 index 000000000..a1d2e221e --- /dev/null +++ b/docs/tutorial/document-namespaces.rst @@ -0,0 +1,187 @@ +Document Namespaces +========================= + +Introduction +------------ + +This article will teach you about document namespaces, what they are and +how they are used in the ``SVG`` document. + +A namespace is a feature of XML documents which allows two distinct formats +to co-exist in the same XML tree without getting in each other's way. It's +defined as a ``url`` and a ``prefix`` and can be identified at the root element tag +like this: + +.. code:: xml + + + + +If the ``prefix`` is blank, then this is considered to be the default namespace +for all the elements that will be in the document and any element without +a namespace will be assumed to belong to that default. + +The ``URL`` is not a real website and doesn't need to exist. You may choose to +invent your own urls and prefixes for your own custom data. Although because of +improper processing by other programs, the domain name such as ``example.com`` +should be in your control just in case someone requests the url by mistake. + +If you see a prefix such as ns0, ns1, etc, this prefix was generated when +elements from a never before seen namespace were added without registering +a prefix. See below for details of registering a namespace. + +Existing Namespaces +------------------- + +The default namespace for SVG documents is ``http://www.w3.org/2000/svg`` and +usually does not have a prefix. Although sometimes you will see the prefix +``svg:`` used too. On top of the default is the now deprecated ``xlink`` +namespace with the url ``http://www.w3.org/1999/xlink`` used for linking +objects by their id. + +Additionally to SVG, there is a sub document called ``RDF`` which is a standard +way of including metadata into all sorts of different documents such as ``PDF`` +files. These have their own namespaces which you can see by the use of the +prefixes ``rdf`` ``dc`` and ``cc``. + +Finally Inkscape has it's own set of namespaces where it stores non-standard +information that helps inkscape remember how to edit objects. These are the +``inkscape`` namespace and the older ``sodipodi`` namespace. You will find both +in use and any new use of the ``inkscape`` namespace should be agreed with the +Inkscape developer team. It should not be used for your own customisations. + +See below for creating and using your own namespaces. + +Reading Namespaces +------------------ + +Each elements in a document will have it's own namespace, this can be accessed +using the ``element.prefix`` property. Because this is a prefix, in order to +get the url you must query ``element.nsmap`` which is a dictionary generated by +the document of all it's available namespaces. + +Attributes inside an element are formatted as ``{url}attribute`` but the ``inkex`` +library reforms these into a standard ``prefix:attribute`` format in order +to make attributes and elements the same. Although you are free to use the url +format if you need to. + +Using Namespaces +---------------- + +If you try and use a prefix in an attribute or element without it being +registered your document will reject the new information and an error will be +raised. If you use a ``url`` instead of a prefix, the attribute or element will +be added but ``lxml`` will generate a prefix such as ``ns0`` which may not be +what you want. + +Always register your namespaces first. + +When requesting attributes you may use the format ``prefix:name`` in all +setters, searchers and getters within inkex. There are translated for you into +``{url}name``. + +You normally wouldn't ever need to worry about namespaces on elements because +all elements are defined as ``classes`` in ``inkex`` which define their own +namespaces and tag names. See below Creating Custom Elements. + +Registering a Namespace +----------------------- + +There are three places where namespaces are registered. Each place does +functionally the same thing for a different bit of context. + +Firstly let's talk about ``inkex``'s namespace dicationaries in +``inkex.elements._utils`` called ``NSS`` and it's inverse ``SSN``. These are +simple global dictionaries which are used when calling ``addNS``. You are +not expected to use these directly, they are used internally to automatically +register elements and attributes as they are added to documents that have +not seen that namespace before. + +This should mean most extensions authors never have to know about namespaces. + +The second set is the global dictionary in ``lxml``, this is used by ``lxml`` +to translate unknown prefixes to urls and to add prefixes to urls being used. + +Both global dictionaries above are modified by calling ``registerNS``. + +The final namespace dictionary is the document doc.nsmap which is the main way +we know what namespaces are being used in a document. This map is the primary +translation between prefixes and urls and if your url has already been given +a prefix in this map, it doesn't matter how many times you call ``registerNS`` +above, it won't change the mapping inside an existing document. + +In order to add to the document map, you should use ``doc.add_namespace`` which +will register the namespace globally and inside the document you intend to use. + +Both ``registerNS`` and ``doc.add_namespace`` are automatically called when you +create custom elements and should not be needed for most cases. + +Creating Custom Elements +------------------------ + +When creating your own custom document data, you should think about creating a +custom element. These are defined just in the same way all SVG elements are +defined in inkex, by creating a class that inherits from ``BaseElement`` and +specifing the ``tag_name``, this name allows lxml to load your custom class +any time a document with your element is loaded. It is automatically registered +and doesn't need to be manually added to the svg loading process once defined:: + + class MyCustomElement(inkex.elements.BaseElement): + tag_name = "myprefix:mycustomelement" + namespace = "https://example.com/my-custom-namespace" + +The tag name will be paired with a prefix separated by a colon for example +``prefix:elementname`` this prefix will be mapped to a url before being used +to identify the element. The second property you should defined is ``namespace`` +which defines the url this element will use as a namespace. When both are +defined the prefix and url will be registered to all global namespaces and will +be registered to the document namespace map when appended to the document for +the first time:: + + doc.append(MyCustomElement()) + +Cleaning Namespaces +------------------- + +Sometimes you will have a document which has the wrong namespaces, or has +had child elements added which have their own local custom namespaces. You +should clean these in order to produce cleaner output:: + + doc.clean_namespaces([url, or prefixes, to remove]) + +If you don't specify any prefixes to remove, all the root prefixes will be +kept, but the element namespaces should be cleaned up. Only namespaces which +are not being used will be removed, if you need to remove something in use +see ``Removing a Namespace`` below. + +Renaming a Namespace +-------------------- + +Sometimes your document will have the wrong namespace prefix and you will want +to replace it. This is actually very dificult to do, but you can use the util +``inkex.elements._utils.renameNamespacePrefix`` which makes a best effort to +clean all the namespaces that are in use before readding everything back into +your document. + +It should be considered an intensive process. + +Removing a Namespace +-------------------- + +If you have a bunch of data which is no longer needed in a document and it all +has the same namespace, you can use the ``NamespaceRemover()`` object. This +is a special object that normally works by removing elements and then putting +them back when used in a ``with`` statement:: + + with NamespaceRemover(doc, "https://example.com/bad-namespace"): + # All elements and attributes in the namespace are gone in + # doc, in this code block. + # Now we are out of the code block, all those elements have been + # Added back into the document. + +To remove the namespace elements without adding them back, call ``remove()``:: + + NamespaceRemover(doc, "https://example.com/bad-namespace").remove() + diff --git a/docs/tutorial/my-first-import-extension.rst b/docs/tutorial/import-extension.rst similarity index 99% rename from docs/tutorial/my-first-import-extension.rst rename to docs/tutorial/import-extension.rst index 6c296bdd9..167d5fe85 100644 --- a/docs/tutorial/my-first-import-extension.rst +++ b/docs/tutorial/import-extension.rst @@ -1,4 +1,4 @@ -My first import extension +Import extension Tutorial ========================= Resources diff --git a/docs/tutorial/index.rst b/docs/tutorial/index.rst index 934ee9979..9132337ef 100644 --- a/docs/tutorial/index.rst +++ b/docs/tutorial/index.rst @@ -44,7 +44,8 @@ will be covered in the more advanced topics later. my-first-effect-extension simple-path-extension - my-first-text-extension - my-first-import-extension + text-extension + import-extension object-editing + document-namespaces diff --git a/docs/tutorial/my-first-text-extension.rst b/docs/tutorial/text-extension.rst similarity index 99% rename from docs/tutorial/my-first-text-extension.rst rename to docs/tutorial/text-extension.rst index 7fb9a314b..7d4225657 100644 --- a/docs/tutorial/my-first-text-extension.rst +++ b/docs/tutorial/text-extension.rst @@ -1,4 +1,4 @@ -My first text extension +Text extension Tutorial ======================= This article will teach you the basics of writing a Text Extension for diff --git a/inkex/elements/__init__.py b/inkex/elements/__init__.py index d426ff663..67846482e 100644 --- a/inkex/elements/__init__.py +++ b/inkex/elements/__init__.py @@ -5,7 +5,7 @@ interact directly with the SVG xml interface. See the documentation for each of the elements for details on how it works. """ -from ._utils import addNS, NSS +from ._utils import addNS, NSS, registerNS from ._parser import SVG_PARSER, load_svg from ._base import ShapeElement, BaseElement from ._svg import SvgDocumentElement diff --git a/inkex/elements/_base.py b/inkex/elements/_base.py index 42b7e94e3..f87f00fa8 100644 --- a/inkex/elements/_base.py +++ b/inkex/elements/_base.py @@ -73,6 +73,14 @@ class BaseElement(IBaseElement): return True tag_name = "" + """The tag name this element uses in xml, including namespace prefix""" + + namespace = None + """The url of the used namespace if it's a custom namespace not already used + by svg or inkscape-svg xml documents. + + .. versionadded:: 1.5 + """ @property def TAG(self): # pylint: disable=invalid-name @@ -174,7 +182,7 @@ class BaseElement(IBaseElement): # transformations and style attributes are equiv to not-existing ret = str(value) if value else default return ret - return super().get(addNS(attr), default) + return super().get(addNS(attr, namespaces=self.nsmap), default) def set(self, attr, value): """Set element attribute named, with addNS support""" @@ -196,10 +204,10 @@ class BaseElement(IBaseElement): if not value: return if value is None: - self.attrib.pop(addNS(attr), None) # pylint: disable=no-member + self.attrib.pop(addNS(attr, namespaces=self.nsmap), None) # pylint: disable=no-member else: value = str(value) - super().set(addNS(attr), value) + super().set(addNS(attr, namespaces=self.nsmap), value) def update(self, **kwargs): """ @@ -223,7 +231,7 @@ class BaseElement(IBaseElement): value = getattr(self, prop) setattr(self, prop, cls(None)) return value - return self.attrib.pop(addNS(attr), default) # pylint: disable=no-member + return self.attrib.pop(addNS(attr, namespaces=self.nsmap), default) # pylint: disable=no-member @overload def add( @@ -705,6 +713,12 @@ class BaseElement(IBaseElement): self, element: BaseElement, add_func: Callable[[BaseElement], None] ): BaseElement._remove_from_tree_callback(element, element) + # Make sure the element's namespaces are available on this svg document + if element.namespace is not None: + self.root.add_namespace( + element.tag_name.split(":")[0], element.namespace, False + ) + # Make sure that we have an ID cache before adding the element, # otherwise we will try to add this element twice to the cache try: diff --git a/inkex/elements/_parser.py b/inkex/elements/_parser.py index 43eac796c..53796f0e7 100644 --- a/inkex/elements/_parser.py +++ b/inkex/elements/_parser.py @@ -31,7 +31,7 @@ from lxml import etree from ..interfaces.IElement import IBaseElement -from ._utils import splitNS, addNS +from ._utils import splitNS, addNS, registerNS from ..utils import errormsg from ..localization import inkex_gettext as _ @@ -50,6 +50,15 @@ class NodeBasedLookup(etree.PythonElementClassLookup): @classmethod def register_class(cls, klass): """Register the given class using it's attached tag name""" + + # Register the class' namespace so it can be used in the element register + if klass.namespace is not None: + if ":" not in klass.tag_name or "{" in klass.tag_name: + raise AttributeError( + "Custom Element namespace must define the tag_name as 'prefix:name'" + ) + registerNS(klass.tag_name.split(":", 1)[0], klass.namespace) + key = addNS(*splitNS(klass.tag_name)[::-1]) old = cls.lookup_table.get(key, []) old.append(klass) diff --git a/inkex/elements/_svg.py b/inkex/elements/_svg.py index f931764b5..362d5b923 100644 --- a/inkex/elements/_svg.py +++ b/inkex/elements/_svg.py @@ -41,7 +41,7 @@ from ..styles import StyleSheets, ConditionalStyle from ._base import BaseElement, ViewboxMixin from ._meta import StyleElement, NamedView -from ._utils import registerNS, addNS, splitNS +from ._utils import renameNamespacePrefix, registerNS, addNS, splitNS from typing import Optional, List, Tuple @@ -166,7 +166,7 @@ class SvgDocumentElement( return self return layer - def add_namespace(self, prefix, url): + def add_namespace(self, prefix, url, register=True): """Adds an xml namespace to the xml parser with the desired prefix. If the prefix or url are already in use with different values, this @@ -175,27 +175,59 @@ class SvgDocumentElement( .. versionadded:: 1.3 """ - if self.nsmap.get(prefix, None) == url: - registerNS(prefix, url) + nsmap = self.nsmap + + if nsmap.get(prefix, None) == url: + if register: + registerNS(prefix, url) return # Attempt to clean any existing namespaces - if prefix in self.nsmap or url in self.nsmap.values(): - nskeep = [k for k, v in self.nsmap.items() if k != prefix and v != url] - etree.cleanup_namespaces(self, keep_ns_prefixes=nskeep) - if prefix in self.nsmap: + if prefix in nsmap or url in nsmap.values(): + # If this works, the namespace wasn't being used + self.clean_namespaces((prefix, url)) + nsmap = self.nsmap + + # Fail here if the url is different, this is much more problematic + if prefix in nsmap: raise KeyError("ns prefix already used with a different url") - if url in self.nsmap.values(): + + # When the prefix already used is ns%d, we forcefully remove it + if url in self.nsmap.values() and self.nsmap: + used = dict(zip(nsmap.values(), nsmap.keys()))[url] + if used.startswith("ns") and used[2:].isdigit(): + renameNamespacePrefix(self, prefix, url) + register = False + + # This shouldn't happen if our agressive cleaning worked + if self.nsmap.get(prefix, None) != url and url in self.nsmap.values(): raise ValueError("ns url already used with a different prefix") - # These are globals, but both will overwrite previous uses. - registerNS(prefix, url) - etree.register_namespace(prefix, url) + # Register in globals + if register: + registerNS(prefix, url) # Set and unset an attribute to add the namespace to this root element. self.set(f"{prefix}:temp", "1") self.set(f"{prefix}:temp", None) + def clean_namespaces(self, remove=()): + """Clean any residual namespace issues when cloning or appending elements + + Args: + remove (list, optional): Remove these prefixes or urls while cleaning + + .. versionadded:: 1.5 + """ + nstop = dict( + [ + (prefix, url) + for prefix, url in self.nsmap.items() + if prefix and prefix not in remove and url not in remove + ] + ) + etree.cleanup_namespaces(self, top_nsmap=nstop, keep_ns_prefixes=list(nstop)) + def getElement(self, xpath): # pylint: disable=invalid-name """Gets a single element from the given xpath or returns None""" return self.findone(xpath) diff --git a/inkex/elements/_utils.py b/inkex/elements/_utils.py index 62acb72ea..659c7fccb 100644 --- a/inkex/elements/_utils.py +++ b/inkex/elements/_utils.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (c) 2021 Martin Owens +# Copyright (c) 2021-2024 Martin Owens # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -23,9 +23,12 @@ Useful utilities specifically for elements (that aren't base classes) Most of the methods in this module were moved from inkex.utils. """ +from lxml import etree from collections import defaultdict import re +from copy import deepcopy + # a dictionary of all of the xmlns prefixes in a standard inkscape doc NSS = { "sodipodi": "http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd", @@ -42,23 +45,66 @@ SSN = dict((b, a) for (a, b) in NSS.items()) def registerNS(prefix, url): - """Register the given prefix as a namespace url.""" + """Register the given prefix as a namespace url. + + Will overwrite previous uses in both inkex and lxml.etree dictionaries. + """ NSS[prefix] = url SSN[url] = prefix + etree.register_namespace(prefix, url) + + +def renameNamespacePrefix(doc, prefix, url): + """ + Attempt to clean up a broken namespace which is currently in use. + """ + with NamespaceRemover(doc, url): + # Then register it to make sure the prefix will be right + registerNS(prefix, url) + + # Mark the new url, the prefix will come from the registerNS + doc.set(f"{{{url}}}temp", "1") + + # Final clean up to remove any child-ns elements + doc.clean_namespaces((prefix, url)) + + # Reset the temporary attribute that created the prefix in nsmap + doc.set(f"{{{url}}}temp", None) def addNS(tag, ns=None, namespaces=NSS): # pylint: disable=invalid-name """Add a known namespace to a name for use with lxml""" - if tag.startswith("{") and ns: - _, tag = removeNS(tag) - if not tag.startswith("{"): - tag = tag.replace("__", ":") - if ":" in tag: - (ns, tag) = tag.rsplit(":", 1) - ns = namespaces.get(ns, None) or ns - if ns is not None: - return f"{{{ns}}}{tag}" - return tag + url = None + prefix = None + + if tag.startswith("{"): + url, tag = tag[1:].split("}") + elif ":" in tag: + prefix, tag = tag.split(":", 1) + elif "__" in tag: + # Prefix from attribute call that uses double underscore + prefix, tag = tag.split("__", 1) + + if prefix is not None and url is None: + url = namespaces.get(prefix, None) + + if ns: + ns_url = namespaces.get(ns, ns) + if url and ns_url != url: + raise ValueError( + f"Tag '{tag}' already has the namespace '{ns_url}' can't addNS '{url}'." + ) + else: + url = ns_url + + if prefix and not url: + if prefix not in NSS: # Globally registered, likely will work + raise ValueError( + f"Can not use prefix '{prefix}', namespace not in document yet." + ) + url = NSS[prefix] + + return f"{{{url}}}{tag}" if url else tag def removeNS(name, reverse_namespaces=SSN, default="svg"): # pylint: disable=invalid-name @@ -148,3 +194,98 @@ class CloningVat: for update, upkw in self.set_ids.get(elem_id, ()): update(elem.get("id"), clone.get("id"), **upkw) process(clone, **kwargs) + + +class NamespaceRemover: + """ + Removes all attributes and elements which belong to the given namespace url. + + When used in a `with NamespaceRemover(doc):` context, the elements and attributes + will be added back into the document when the code block is finished. + """ + + def __init__(self, doc, namespace): + self.doc = doc + self.namespace = namespace + self.elements = None + self.attributes = None + + def __enter__(self): + self.temporarilyRemoveElements() + return self + + def __exit__(self, exc, value, traceback): + self.addRemovedElements() + + def remove(self): + """ + Run the main action without saving any items NamespaceRemover(doc).remove() + """ + self.removeElementRecursive(self.doc) + + # Now the url is removed entirely, clean it up + self.doc.clean_namespaces((self.namespace)) + + def temporarilyRemoveElements(self): + """ + Removes the elements and stores all the information needed to + repair the removal when needed. See addRemovedElements() + """ + self.elements = defaultdict(list) + self.attributes = [] + self.remove() + + def removeElementRecursive(self, elem): + """Recursively remove elements that match the namespace, + + Returns False if the element should not be removed, True if it + should be removed. A clone will be added internally if the elements + property has been intialised by temporarilyRemoveElements(). + """ + if elem.nsmap.get(elem.prefix, None) == self.namespace: + # This element itself needs to be removed, no need + # to go into it's children or attributes. + return True + + # Process attributes for an elements we're keeping + for attr, value in elem.attrib.items(): + if attr.startswith(f"{{{self.namespace}}}"): + if self.attributes is not None: + self.attributes.append([elem, attr, value]) + elem.attrib.pop(attr) + + to_remove = [] + for child in elem: + # Recurse into the child for removals + if self.removeElementRecursive(child): + to_remove.append(child) + if self.elements is not None: + self.elements[(elem, None)].append(deepcopy(child)) + elif self.elements is not None and (elem, None) in self.elements: + # This sibling came after deletions, so use it as an anchor + self.elements[(elem, child)] = self.elements.pop((elem, None)) + + # Remove all the child elements, this will remove tail text too (be warned!) + from ._base import BaseElement + + for child in to_remove: + super(BaseElement, elem).remove(child) + + return False + + def addRemovedElements(self): + """ + Put all recorded elements and attributes back in the document. + """ + if self.attributes is not None: + # Now we attempt to put the elements and attributes back + for elem, attr, value in self.attributes: + elem.attrib[attr] = value + + if self.elements is not None: + for (parent, sibling), clones in self.elements.items(): + for clone in clones: + if sibling is None: + etree.ElementBase.append(parent, clone) + else: + etree.ElementBase.addprevious(sibling, clone) diff --git a/jessyink_install.py b/jessyink_install.py index da4cacac6..3798e2064 100755 --- a/jessyink_install.py +++ b/jessyink_install.py @@ -18,11 +18,12 @@ """Install jessyInk scripts""" import inkex +from inkex.elements import registerNS from inkex import Script from inkex.localization import inkex_gettext as _ -inkex.NSS["jessyink"] = "https://launchpad.net/jessyink" +registerNS("jessyink", "https://launchpad.net/jessyink") class JessyInkMixin(object): diff --git a/jessyink_mouse_handler.py b/jessyink_mouse_handler.py index edc370338..75a2ba95c 100755 --- a/jessyink_mouse_handler.py +++ b/jessyink_mouse_handler.py @@ -26,6 +26,7 @@ class MouseHandler(BaseElement): """jessyInk mouse handler""" tag_name = "jessyink:mousehandler" + namespace = "https://launchpad.net/jessyink" class AddMouseHandler(JessyInkMixin, inkex.EffectExtension): diff --git a/tests/test_inkex_elements_base.py b/tests/test_inkex_elements_base.py index 2cf14124d..b003498ef 100644 --- a/tests/test_inkex_elements_base.py +++ b/tests/test_inkex_elements_base.py @@ -20,6 +20,7 @@ from inkex.elements import ( Line, ) from inkex.elements._base import NodeBasedLookup, BaseElement +from inkex.elements._utils import NSS, SSN, renameNamespacePrefix, NamespaceRemover from inkex.transforms import Transform from inkex.styles import Style from inkex.utils import FragmentError @@ -43,6 +44,64 @@ class SvgTestCase(TestCase): self.svg = svg_file(self.data_file("svg", self.source_file)) +class NamespaceTestCase(SvgTestCase): + """Test namespace cleaning""" + + namespace = "http://example.com/x" + svg_text = """ + + + + Child + + tail + + + + + + +""" + + def test_namespace_removal(self): + doc = load_svg(self.svg_text).getroot() + NamespaceRemover(doc, self.namespace).remove() + self.assertEqual( + doc.tostring().decode("utf8"), + """ + + + + """, + ) + + def test_prefix_rename(self): + doc = load_svg(self.svg_text) + + svg = doc.getroot() + renameNamespacePrefix(svg, "x", self.namespace) + + self.assertIn("x", svg.nsmap) + self.assertNotIn("ns1", svg.nsmap) + + self.assertEqual( + svg.tostring().decode("utf8"), + """ + + + Child + + tail + + + + + + +""", + ) + + class OverridenElementTestCase(SvgTestCase): """Test element overriding functionality""" @@ -52,7 +111,7 @@ class OverridenElementTestCase(SvgTestCase): """ doc = load_svg( """ - + Unknown SVG tag @@ -67,6 +126,28 @@ class OverridenElementTestCase(SvgTestCase): ugly = svg.getElementById("ugly") self.assertEqual(ugly.TAG, "othertag") + def test_namespaces(self): + test_prefix = "velcro" + test_url = "http://example.com/velcro/" + + self.assertNotIn(test_prefix, NSS) + self.assertNotIn(test_url, SSN) + + class CustomElement(BaseElement): + tag_name = f"{test_prefix}:custom" + namespace = test_url + + self.assertIn(test_prefix, NSS) + self.assertIn(test_url, SSN) + + def capture_error(tag): + class BadCustomElement(BaseElement): + tag_name = tag + namespace = 1 + + self.assertRaises(AttributeError, capture_error, "noprefix") + self.assertRaises(AttributeError, capture_error, "{urlprefix}:custom") + def test_reference_count(self): """ Test inkex.element.BaseElement-derived object type is preserved on adding to group diff --git a/tests/test_inkex_svg.py b/tests/test_inkex_svg.py index cb38ae286..f4d261c33 100644 --- a/tests/test_inkex_svg.py +++ b/tests/test_inkex_svg.py @@ -25,9 +25,15 @@ from inkex.transforms import Vector2d from inkex import Guide, Rectangle from inkex.tester import TestCase from inkex.tester.svg import svg, svg_file, svg_unit_scaled +from inkex.elements import BaseElement from inkex import addNS +class CustomElement(BaseElement): + tag_name = "fruit:blueberry" + namespace = "http://example.com/fruit/xml/" + + class BasicSvgTest(TestCase): """Basic svg tests""" @@ -84,6 +90,14 @@ class BasicSvgTest(TestCase): root.add_namespace("hotel", "http://other.url/") self.assertEqual(root.nsmap["hotel"], "http://other.url/") + def test_register_ns_automatic(self): + """Test namespaces for custom elements are automatically added""" + root = svg() + self.assertNotIn("fruit", root.nsmap) + root.append(CustomElement()) + self.assertIn("fruit", root.nsmap) + root.append(CustomElement()) + def test_register_ns_children(self): """Test namespace registration when children are added before / after modification of the namespaces""" diff --git a/tests/test_inkex_utils.py b/tests/test_inkex_utils.py index 503a78e34..69569639b 100644 --- a/tests/test_inkex_utils.py +++ b/tests/test_inkex_utils.py @@ -28,9 +28,13 @@ from inkex.tester import TestCase from inkex import addNS -class TestInkexBasic(object): +class TestInkexBasic(TestCase): """Test basic utiltiies of inkex""" + @pytest.fixture(autouse=True) + def capsys(self, capsys): + self.capsys = capsys + def test_boolean(self): """Inkscape boolean input""" assert Boolean("TRUE") is True @@ -41,10 +45,10 @@ class TestInkexBasic(object): assert Boolean("False") is False assert Boolean("Banana") is None - def test_debug(self, capsys): + def test_debug(self): """Debug messages go to stderr""" debug("Hello World") - assert capsys.readouterr().err == "Hello World\n" + assert self.capsys.readouterr().err == "Hello World\n" def test_to(self): """Decorator for generators""" @@ -90,16 +94,15 @@ class TestInkexBasic(object): == "{http://www.inkscape.org/namespaces/inkscape}bar" ) assert ( - addNS("http://www.inkscape.org/namespaces/inkscape:bar") + addNS("{http://www.inkscape.org/namespaces/inkscape}bar") == "{http://www.inkscape.org/namespaces/inkscape}bar" ) assert ( addNS("car", "http://www.inkscape.org/namespaces/inkscape") == "{http://www.inkscape.org/namespaces/inkscape}car" ) - assert ( - addNS("{http://www.inkscape.org/namespaces/inkscape}bar", "rdf") - == "{http://www.w3.org/1999/02/22-rdf-syntax-ns#}bar" + self.assertRaises( + ValueError, addNS, "{http://www.inkscape.org/namespaces/inkscape}bar", "rdf" ) def test_strargs(self): @@ -139,22 +142,22 @@ class TestInkexBasic(object): ] assert strargs("1.E2+.3E+4") == [100, 3000] - def test_ascii(self, capsys): + def test_ascii(self): """Parse ABCabc""" errormsg("ABCabc") - assert capsys.readouterr().err == "ABCabc\n" + assert self.capsys.readouterr().err == "ABCabc\n" - def test_nonunicode_latin1(self, capsys): + def test_nonunicode_latin1(self): # Py2 has issues with unicode in docstrings. *sigh* # """Parse Àûïàèé""" errormsg("Àûïàèé") - assert capsys.readouterr().err, "Àûïàèé\n" + assert self.capsys.readouterr().err, "Àûïàèé\n" - def test_unicode_latin1(self, capsys): + def test_unicode_latin1(self): # Py2 has issues with unicode in docstrings. *sigh* # """Parse Àûïàèé (unicode)""" errormsg("Àûïàèé") - assert capsys.readouterr().err, "Àûïàèé\n" + assert self.capsys.readouterr().err, "Àûïàèé\n" def test_parse_percent(self): assert parse_percent("75%") == 0.75 -- GitLab