From 0075cedf7ac65a5c90f19bc08dded10196f5db68 Mon Sep 17 00:00:00 2001 From: pulsar17 Date: Tue, 16 Mar 2021 18:23:50 +0530 Subject: [PATCH 1/4] Fix for issue #345 --- inkex/extensions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inkex/extensions.py b/inkex/extensions.py index 05e96bd1..a4d27b4d 100644 --- a/inkex/extensions.py +++ b/inkex/extensions.py @@ -125,7 +125,7 @@ class CallExtension(TempDirMixin, InputExtension): with open(document, 'r') as fhl: document = fhl.read() if '<' in document: - document = load_svg(document) + document = load_svg(document.encode('utf-8')) else: with open(document, 'rb') as fhl: document = fhl.read() -- GitLab From 24112f9388082adc52a88ff6218611df68757f73 Mon Sep 17 00:00:00 2001 From: pulsar17 <5243241-pulsar17@users.noreply.gitlab.com> Date: Fri, 26 Mar 2021 03:38:35 +0530 Subject: [PATCH 2/4] Drop Python 2 support and add f-strings --- inkex/__init__.py | 1 - inkex/base.py | 63 +++++++++++---------------- inkex/colors.py | 22 ++++------ inkex/command.py | 34 +++++---------- inkex/elements/_base.py | 27 +++++------- inkex/elements/_filters.py | 5 +-- inkex/elements/_groups.py | 4 +- inkex/elements/_meta.py | 4 +- inkex/elements/_polygons.py | 8 ++-- inkex/elements/_svg.py | 6 +-- inkex/extensions.py | 17 ++++---- inkex/inx.py | 16 +++---- inkex/paths.py | 36 +++++++-------- inkex/ports.py | 2 +- inkex/styles.py | 41 ++++++++--------- inkex/tester/__init__.py | 37 ++++++---------- inkex/tester/filters.py | 2 +- inkex/tester/mock.py | 27 +++++------- inkex/tester/svg.py | 4 +- inkex/tester/xmldiff.py | 14 +++--- inkex/transforms.py | 75 ++++++++++++++------------------ inkex/turtle.py | 2 +- inkex/tween.py | 11 ++--- inkex/units.py | 2 +- inkex/utils.py | 31 +++---------- tests/test_inkex_bounding_box.py | 2 +- 26 files changed, 198 insertions(+), 295 deletions(-) diff --git a/inkex/__init__.py b/inkex/__init__.py index 36884fc7..ca0827a3 100644 --- a/inkex/__init__.py +++ b/inkex/__init__.py @@ -6,7 +6,6 @@ This provides the basis from which you can develop your inkscape extension. """ # pylint: disable=wildcard-import -from __future__ import print_function from .extensions import * from .utils import * diff --git a/inkex/base.py b/inkex/base.py index a23d7b7d..eb31f505 100644 --- a/inkex/base.py +++ b/inkex/base.py @@ -19,36 +19,24 @@ """ The ultimate base functionality for every Inkscape extension. """ -from __future__ import absolute_import, print_function, unicode_literals import os import sys import copy -import shutil + +from typing import List, Tuple, Type, Optional, Callable, Any, Union, IO, TYPE_CHECKING, cast from argparse import ArgumentParser, Namespace from lxml import etree -from .utils import PY3, filename_arg, AbortExtension, ABORT_STATUS, errormsg, do_nothing +from .utils import filename_arg, AbortExtension, ABORT_STATUS, errormsg, do_nothing from .elements._base import load_svg, BaseElement # pylint: disable=unused-import from .localization import localize -stdout = sys.stdout - -try: - from typing import (List, Tuple, Type, Optional, Callable, Any, Union, IO, - TYPE_CHECKING, cast) -except ImportError: - cast = lambda x, y: y - TYPE_CHECKING = False +stdout = sys.stdout.buffer # type: ignore -if PY3: - unicode = str # pylint: disable=redefined-builtin,invalid-name - basestring = str # pylint: disable=redefined-builtin,invalid-name - stdout = sys.stdout.buffer # type: ignore - -class InkscapeExtension(object): +class InkscapeExtension: """ The base class extension, provides argument parsing and basic variable handling features. @@ -59,7 +47,7 @@ class InkscapeExtension(object): # type: () -> None self.file_io = None # type: Optional[IO] self.options = Namespace() - self.document = None # type: Union[None, bytes, str, unicode, etree] + self.document = None # type: Union[None, bytes, str, etree] self.arg_parser = ArgumentParser(description=self.__doc__) self.arg_parser.add_argument( @@ -108,13 +96,13 @@ class InkscapeExtension(object): except AttributeError: if name.startswith('_'): return do_nothing - raise AbortExtension("Can not find method {}".format(name)) + raise AbortExtension(f"Can not find method {name}") return _inner def debug(self, msg): # type: (str) -> None """Write a debug message""" - errormsg("DEBUG<{}> {}\n".format(type(self).__name__, msg)) + errormsg(f"DEBUG<{type(self).__name__}> {msg}\n") @staticmethod def msg(msg): @@ -134,7 +122,6 @@ class InkscapeExtension(object): self.options.input_file = sys.stdin if self.options.output is None: - # assert output self.options.output = output self.load_raw() @@ -148,7 +135,7 @@ class InkscapeExtension(object): def load_raw(self): # type: () -> None """Load the input stream or filename, save everything to self""" - if isinstance(self.options.input_file, (str, unicode)): + if isinstance(self.options.input_file, str): self.file_io = open(self.options.input_file, 'rb') document = self.load(self.file_io) else: @@ -159,7 +146,7 @@ class InkscapeExtension(object): # type: (Any) -> None """Save to the output stream, use everything from self""" if self.has_changed(ret): - if isinstance(self.options.output, (str, unicode)): + if isinstance(self.options.output, str): with open(self.options.output, 'wb') as stream: self.save(stream) else: @@ -168,17 +155,17 @@ class InkscapeExtension(object): def load(self, stream): # type: (IO) -> str """Takes the input stream and creates a document for parsing""" - raise NotImplementedError("No input handle for {}".format(self.name)) + raise NotImplementedError(f"No input handle for {self.name}") def save(self, stream): # type: (IO) -> None """Save the given document to the output file""" - raise NotImplementedError("No output handle for {}".format(self.name)) + raise NotImplementedError(f"No output handle for {self.name}") def effect(self): # type: () -> Any """Apply some effects on the document or local context""" - raise NotImplementedError("No effect handle for {}".format(self.name)) + raise NotImplementedError(f"No effect handle for {self.name}") def has_changed(self, ret): # pylint: disable=no-self-use # type: (Any) -> bool @@ -254,22 +241,23 @@ class TempDirMixin(_Base): def __init__(self, *args, **kwargs): self.tempdir = None - super(TempDirMixin, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) def load_raw(self): # type: () -> None """Create the temporary directory""" - from tempfile import mkdtemp - self.tempdir = os.path.realpath( - mkdtemp(self.dir_suffix, self.dir_prefix, None)) - super(TempDirMixin, self).load_raw() + from tempfile import TemporaryDirectory + # Need to hold a reference to the Directory object or else it might get GC'd + self._tempdir = TemporaryDirectory(prefix=self.dir_prefix, suffix=self.dir_suffix) + self.tempdir = self._tempdir.name + super().load_raw() def clean_up(self): # type: () -> None """Delete the temporary directory""" - if self.tempdir and os.path.isdir(self.tempdir): - shutil.rmtree(self.tempdir) - super(TempDirMixin, self).clean_up() + self.tempdir = None + self._tempdir.cleanup() + super().clean_up() class SvgInputMixin(_Base): # pylint: disable=too-few-public-methods @@ -280,7 +268,7 @@ class SvgInputMixin(_Base): # pylint: disable=too-few-public-methods select_all = () # type: Tuple[Type[BaseElement], ...] def __init__(self): - super(SvgInputMixin, self).__init__() + super().__init__() self.arg_parser.add_argument( "--id", action="append", type=str, dest="ids", default=[], @@ -328,15 +316,14 @@ class SvgOutputMixin(_Base): # pylint: disable=too-few-public-methods def save(self, stream): # type: (IO) -> None """Save the svg document to the given stream""" - if isinstance(self.document, (bytes, str, unicode)): + if isinstance(self.document, (bytes, str)): document = self.document elif 'Element' in type(self.document).__name__: # isinstance can't be used here because etree is broken doc = cast(etree, self.document) document = doc.getroot().tostring() else: - raise ValueError("Unknown type of document: {} can not save."\ - .format(type(self.document).__name__)) + raise ValueError(f"Unknown type of document: {type(self.document).__name__} can not save.") try: stream.write(document) diff --git a/inkex/colors.py b/inkex/colors.py index 5ae9403a..7db26dc9 100644 --- a/inkex/colors.py +++ b/inkex/colors.py @@ -22,15 +22,11 @@ Basic color controls """ -from .utils import PY3 from .tween import interpcoord # All the names that get added to the inkex API itself. __all__ = ('Color', 'ColorError', 'ColorIdError') -if PY3: - unicode = str # pylint: disable=redefined-builtin,invalid-name - SVG_COLOR = { 'aliceblue': '#f0f8ff', 'antiquewhite': '#faebd7', @@ -221,11 +217,11 @@ class Color(list): lightness = lightness.setter(lambda self, value: self._set(2, value, ('hsl',))) def __init__(self, color=None, space='rgb'): - super(Color, self).__init__() + super().__init__() if isinstance(color, Color): space, color = color.space, list(color) - if isinstance(color, (str, unicode)): + if isinstance(color, str): # String from xml or css attributes space, color = self.parse_str(color.strip()) @@ -274,7 +270,7 @@ class Color(list): if len(self) == len(self.space): raise ValueError("Can't add any more values to color.") - if isinstance(val, (unicode, str)): + if isinstance(val, str): val = val.strip() if val.endswith('%'): val = float(val.strip('%')) / 100 @@ -289,7 +285,7 @@ class Color(list): val *= 255 if isinstance(val, (int, float)): - super(Color, self).append(max(end_type(val), 0)) + super().append(max(end_type(val), 0)) @staticmethod def parse_str(color): @@ -316,7 +312,7 @@ class Color(list): try: return 'rgb', (int(col[1:3], 16), int(col[3:5], 16), int(col[5:], 16)) except ValueError: - raise ColorError("Bad RGB hex color value {}".format(col)) + raise ColorError(f"Bad RGB hex color value {col}") # Handle other css color values elif '(' in color and ')' in color: @@ -328,7 +324,7 @@ class Color(list): except ValueError: pass - raise ColorError("Unknown color format: {}".format(color)) + raise ColorError(f"Unknown color format: {color}") @staticmethod def parse_int(color): @@ -363,7 +359,7 @@ class Color(list): return 'rgba({:g}, {:g}, {:g}, {:g})'.format(*self) elif self.space == 'hsl': return 'hsl({0:g}, {1:g}, {2:g})'.format(*self) - raise ColorError("Can't print colour space '{}'".format(self.space)) + raise ColorError(f"Can't print colour space '{self.space}'") def __int__(self): """int array to large integer""" @@ -384,7 +380,7 @@ class Color(list): return self elif self.space == 'rgb': return Color(rgb_to_hsl(*self.to_floats()), space='hsl') - raise ColorError("Unknown color conversion {}->hsl".format(self.space)) + raise ColorError(f"Unknown color conversion {self.space}->hsl") def to_rgb(self): """Turn this color into a Red/Green/Blue colour space""" @@ -396,7 +392,7 @@ class Color(list): return Color(self[:3], space='rgb') elif self.space == 'hsl': return Color(hsl_to_rgb(*self.to_floats()), space='rgb') - raise ColorError("Unknown color conversion {}->rgb".format(self.space)) + raise ColorError(f"Unknown color conversion {self.space}->rgb") def to_rgba(self, alpha=1.0): """Turn this color isn't an RGB with Alpha colour space""" diff --git a/inkex/command.py b/inkex/command.py index cfce7841..c7d2ecf3 100644 --- a/inkex/command.py +++ b/inkex/command.py @@ -33,9 +33,9 @@ it yourself, to take advantage of the security settings and testing functions. import os from subprocess import Popen, PIPE +from tempfile import TemporaryDirectory from lxml.etree import ElementTree -from .utils import TemporaryDirectory, PY3 from .elements import SvgDocumentElement INKSCAPE_EXECUTABLE_NAME = os.environ.get('INKSCAPE_COMMAND', 'inkscape') @@ -54,29 +54,16 @@ def which(program): """ if os.path.isabs(program) and os.path.isfile(program): return program - try: - # Python2 and python3, but must have distutils and may not always - # work on windows versions (depending on the version) - from distutils.spawn import find_executable - prog = find_executable(program) - if prog: - return prog - except ImportError: - pass - try: - # Python3 only version of which - from shutil import which as warlock - prog = warlock(program) - if prog: + from shutil import which as warlock + prog = warlock(program) + if prog: return prog - except ImportError: - pass # python2 # There may be other methods for doing a `which` command for other # operating systems; These should go here as they are discovered. - raise CommandNotFound("Can not find the command: '{}'".format(program)) + raise CommandNotFound(f"Can not find the command: '{program}'") def write_svg(svg, *filename): """Writes an svg to the given filename""" @@ -107,7 +94,7 @@ def to_arg(arg, oldie=False): return arg if val is False: return None - return '{}={}'.format(arg, str(val)) + return f"{arg}={str(val)}" return str(arg) def to_args(prog, *positionals, **arguments): @@ -159,7 +146,7 @@ def to_args(prog, *positionals, **arguments): def _call(program, *args, **kwargs): stdin = kwargs.pop('stdin', None) - if PY3 and isinstance(stdin, str): + if isinstance(stdin, str): stdin = stdin.encode('utf-8') inpipe = PIPE if stdin else None @@ -174,8 +161,7 @@ def _call(program, *args, **kwargs): (stdout, stderr) = process.communicate(input=stdin) if process.returncode == 0: return stdout - raise ProgramRunError("Return Code: {}: {}\n{}\nargs: {}".format( - process.returncode, stderr, stdout, args)) + raise ProgramRunError(f"Return Code: {process.returncode}: {stderr}\n{stdout}\nargs: {args}") def call(program, *args, **kwargs): """ @@ -199,8 +185,8 @@ def inkscape_command(svg, select=None, verbs=()): inkscape_command('', ('verb', 'VerbName'), ...) """ - with TemporaryDirectory(prefix='inkscape-command') as dirname: - svg_file = write_svg(svg, dirname, 'input.svg') + with TemporaryDirectory(prefix='inkscape-command') as tmpdir: + svg_file = write_svg(svg, tmpdir, 'input.svg') select = ('select', select) if select else None verbs += ('FileSave', 'FileQuit') inkscape(svg_file, select, batch_process=True, verb=';'.join(verbs)) diff --git a/inkex/elements/_base.py b/inkex/elements/_base.py index 0c73e5b8..874f5774 100644 --- a/inkex/elements/_base.py +++ b/inkex/elements/_base.py @@ -33,12 +33,9 @@ from lxml import etree from ..paths import Path from ..styles import Style, AttrFallbackStyle, Classes from ..transforms import Transform, BoundingBox -from ..utils import PY3, NSS, addNS, removeNS, splitNS, FragmentError +from ..utils import NSS, addNS, removeNS, splitNS, FragmentError -try: - from typing import overload, DefaultDict, Type, Any, List, Tuple, Union, Optional # pylint: disable=unused-import -except ImportError: - overload = lambda x: x +from typing import overload, DefaultDict, Type, Any, List, Tuple, Union, Optional # pylint: disable=unused-import class NodeBasedLookup(etree.PythonElementClassLookup): """ @@ -96,7 +93,7 @@ class BaseElement(etree.ElementBase): def TAG(self): # pylint: disable=invalid-name """Return the tag_name without NS""" if not self.tag_name: - return removeNS(super(etree.ElementBase, self).tag)[-1] + return removeNS(super().tag)[-1] return removeNS(self.tag_name)[-1] @classmethod @@ -148,7 +145,7 @@ class BaseElement(etree.ElementBase): value = cls(self.attrib.get(attr, None), callback=_set_attr) setattr(self, name, value) return value - raise AttributeError("Can't find attribute {}.{}".format(self.typename, name)) + raise AttributeError(f"Can't find attribute {self.typename}.{name}") def __setattr__(self, name, value): """Set the attribute, update it if needed""" @@ -162,7 +159,7 @@ class BaseElement(etree.ElementBase): else: self.attrib.pop(attr, None) # pylint: disable=no-member else: - super(BaseElement, self).__setattr__(name, value) + super().__setattr__(name, value) def get(self, attr, default=None): """Get element attribute named, with addNS support.""" @@ -173,7 +170,7 @@ class BaseElement(etree.ElementBase): # transformations and style attributes are equiv to not-existing ret = str(value) if value else (default or None) return ret - return super(BaseElement, self).get(addNS(attr), default) + return super().get(addNS(attr), default) def set(self, attr, value): """Set element attribute named, with addNS support""" @@ -187,8 +184,8 @@ class BaseElement(etree.ElementBase): if value is None: self.attrib.pop(addNS(attr), None) # pylint: disable=no-member else: - value = str(value) if PY3 else unicode(value) # pylint: disable=undefined-variable - super(BaseElement, self).set(addNS(attr), value) + value = str(value) + super().set(addNS(attr), value) def update(self, **kwargs): """ @@ -333,11 +330,11 @@ class BaseElement(etree.ElementBase): def xpath(self, pattern, namespaces=NSS): # pylint: disable=dangerous-default-value """Wrap xpath call and add svg namespaces""" - return super(BaseElement, self).xpath(pattern, namespaces=namespaces) + return super().xpath(pattern, namespaces=namespaces) def findall(self, pattern, namespaces=NSS): # pylint: disable=dangerous-default-value """Wrap findall call and add svg namespaces""" - return super(BaseElement, self).findall(pattern, namespaces=namespaces) + return super().findall(pattern, namespaces=namespaces) def findone(self, xpath): """Gets a single element from the given xpath or returns None""" @@ -444,12 +441,12 @@ class ShapeElement(BaseElement): def get_path(self): """Generate a path for this object which can inform the bounding box""" - raise NotImplementedError("Path should be provided by svg elem {}.".format(self.typename)) + raise NotImplementedError(f"Path should be provided by svg elem {self.typename}.") def set_path(self, path): """Set the path for this object (if possible)""" raise AttributeError( - "Path can not be set on this element: {} <- {}.".format(self.typename, path)) + f"Path can not be set on this element: {self.typename} <- {path}.") def to_path_element(self): """Replace this element with a path element""" diff --git a/inkex/elements/_filters.py b/inkex/elements/_filters.py index ff6883f3..7871c678 100644 --- a/inkex/elements/_filters.py +++ b/inkex/elements/_filters.py @@ -35,10 +35,7 @@ from ..styles import Style from ._base import BaseElement -try: - from typing import overload, Iterable, List, Tuple, Union, Optional # pylint: disable=unused-import -except ImportError: - overload = lambda x: x +from typing import overload, Iterable, List, Tuple, Union, Optional # pylint: disable=unused-import class Filter(BaseElement): diff --git a/inkex/elements/_groups.py b/inkex/elements/_groups.py index 52bd073f..7b0ac9c8 100644 --- a/inkex/elements/_groups.py +++ b/inkex/elements/_groups.py @@ -63,7 +63,7 @@ class Group(GroupBase): @classmethod def new(cls, label, *children, **attrs): attrs['inkscape:label'] = label - return super(Group, cls).new(*children, **attrs) + return super().new(*children, **attrs) def effective_style(self): @@ -98,7 +98,7 @@ class Anchor(GroupBase): @classmethod def new(cls, href, *children, **attrs): attrs['xlink:href'] = href - return super(Anchor, cls).new(*children, **attrs) + return super().new(*children, **attrs) class ClipPath(GroupBase): diff --git a/inkex/elements/_meta.py b/inkex/elements/_meta.py index 157d3d42..b90c504f 100644 --- a/inkex/elements/_meta.py +++ b/inkex/elements/_meta.py @@ -104,7 +104,7 @@ class Guide(BaseElement): @classmethod def new(cls, pos_x, pos_y, angle, **attrs): - guide = super(Guide, cls).new(**attrs) + guide = super().new(**attrs) guide.move_to(pos_x, pos_y, angle=angle) return guide @@ -116,7 +116,7 @@ class Guide(BaseElement): it may be a pair of numbers (tuple) which will set the orientation directly. If not given at all, the orientation remains unchanged. """ - self.set('position', "{:g},{:g}".format(float(pos_x), float(pos_y))) + self.set('position', f"{float(pos_x):g},{float(pos_y):g}") if isinstance(angle, str): if ',' not in angle: angle = float(angle) diff --git a/inkex/elements/_polygons.py b/inkex/elements/_polygons.py index e362793e..65185c85 100644 --- a/inkex/elements/_polygons.py +++ b/inkex/elements/_polygons.py @@ -36,7 +36,7 @@ class PathElementBase(ShapeElement): @classmethod def new(cls, path, **attrs): - return super(PathElementBase, cls).new(d=Path(path), **attrs) + return super().new(d=Path(path), **attrs) def set_path(self, path): """Set the given data as a path as the 'd' attribute""" @@ -104,7 +104,7 @@ class Polyline(ShapeElement): return Path('M' + self.get('points')) def set_path(self, path): - points = ['{:g},{:g}'.format(x, y) for x, y in Path(path).end_points] + points = [f'{x:g},{y:g}' for x, y in Path(path).end_points] self.set('points', ' '.join(points)) @@ -123,7 +123,7 @@ class Line(ShapeElement): def new(cls, start, end, **attrs): start = Vector2d(start) end = Vector2d(end) - return super(Line, cls).new(x1=start.x, y1=start.y, + return super().new(x1=start.x, y1=start.y, x2=end.x, y2=end.y, **attrs) @@ -190,7 +190,7 @@ class EllipseBase(ShapeElement): @classmethod def new(cls, center, radius, **attrs): - circle = super(EllipseBase, cls).new(**attrs) + circle = super().new(**attrs) circle.center = center circle.radius = radius return circle diff --git a/inkex/elements/_svg.py b/inkex/elements/_svg.py index 50f71ad5..2bd82c27 100644 --- a/inkex/elements/_svg.py +++ b/inkex/elements/_svg.py @@ -106,14 +106,14 @@ class SvgDocumentElement(DeprecatedSvgMixin, BaseElement): def getElementsByHref(self, eid): # pylint: disable=invalid-name """Get elements by their href xlink attribute""" - return self.xpath('//*[@xlink:href="#{}"]'.format(eid)) + return self.xpath(f'//*[@xlink:href="#{eid}"]') def getElementsByStyleUrl(self, eid, style=None): # pylint: disable=invalid-name """Get elements by a style attribute url""" - url = "url(#{})".format(eid) + url = f"url(#{eid})" if style is not None: url = style + ":" + url - return self.xpath('//*[contains(@style,"{}")]'.format(url)) + return self.xpath(f'//*[contains(@style,"{url}")]') @property def name(self): diff --git a/inkex/extensions.py b/inkex/extensions.py index a4d27b4d..949ac255 100644 --- a/inkex/extensions.py +++ b/inkex/extensions.py @@ -28,7 +28,7 @@ import re import sys import types -from .utils import errormsg, Boolean, CloningVat, PY3 +from .utils import errormsg, Boolean, CloningVat from .colors import Color, ColorIdError, ColorError from .elements import load_svg, BaseElement, ShapeElement, Group, Layer, Grid, \ TextElement, FlowPara, FlowDiv @@ -41,8 +41,7 @@ __all__ = ('EffectExtension', 'GenerateExtension', 'InputExtension', 'CallExtension', 'TemplateExtension', 'ColorExtension', 'TextExtension') stdout = sys.stdout -if PY3: - unicode = str # pylint: disable=redefined-builtin,invalid-name + class EffectExtension(SvgThroughMixin, InkscapeExtension): """ @@ -109,7 +108,7 @@ class CallExtension(TempDirMixin, InputExtension): TempDirMixin.load_raw(self) input_file = self.options.input_file - if not isinstance(input_file, (unicode, str)): + if not isinstance(input_file, str): data = input_file.read() input_file = os.path.join(self.tempdir, 'input.' + self.input_ext) with open(input_file, 'wb') as fhl: @@ -117,9 +116,9 @@ class CallExtension(TempDirMixin, InputExtension): output_file = os.path.join(self.tempdir, 'output.' + self.output_ext) document = self.call(input_file, output_file) or output_file - if isinstance(document, (str, unicode)): + if isinstance(document, str): if not os.path.isfile(document): - raise IOError("Can't find generated document: {}".format(document)) + raise IOError(f"Can't find generated document: {document}") if self.output_ext == 'svg': with open(document, 'r') as fhl: @@ -200,7 +199,7 @@ class TemplateExtension(EffectExtension): template_id = "SVGRoot" def __init__(self): - super(TemplateExtension, self).__init__() + super().__init__() # Arguments added on after add_arguments so it can be overloaded cleanly. self.arg_parser.add_argument("--size", type=self.arg_size(), dest="size") self.arg_parser.add_argument("--width", type=int, default=800) @@ -250,7 +249,7 @@ class TemplateExtension(EffectExtension): self.svg.set("id", self.template_id) self.svg.set("width", str(width) + width_unit) self.svg.set("height", str(height) + height_unit) - self.svg.set("viewBox", "0 0 {} {}".format(width, height)) + self.svg.set("viewBox", f"0 0 {width} {height}") self.set_namedview(width_px, height_px, width_unit) def set_namedview(self, width, height, unit): @@ -309,7 +308,7 @@ class ColorExtension(EffectExtension): def _ref_cloned(self, old_id, new_id, style, name): self._renamed[old_id] = new_id - style[name] = "url(#{})".format(new_id) + style[name] = f"url(#{new_id})" def _xlink_cloned(self, old_id, new_id, linker): lid = linker.get('id') diff --git a/inkex/inx.py b/inkex/inx.py index 4753d1b4..d9c8cb19 100644 --- a/inkex/inx.py +++ b/inkex/inx.py @@ -21,14 +21,10 @@ Parsing inx files for checking and generating. """ import os +from inspect import isclass +from importlib import util from lxml import etree -try: - from inspect import isclass - from importlib import util -except ImportError: - util = None # type: ignore - from .base import InkscapeExtension from .utils import Boolean @@ -48,7 +44,7 @@ class InxLookup(etree.CustomElementClassLookup): INX_PARSER = etree.XMLParser() INX_PARSER.set_element_class_lookup(InxLookup()) -class InxFile(object): +class InxFile: """Open an INX file and provide useful functions""" name = property(lambda self: self.xml._text('name')) ident = property(lambda self: self.xml._text('id')) @@ -69,7 +65,7 @@ class InxFile(object): self.xml.warnings = [] def __repr__(self): - return "".format(self) + return f"" @property def script(self): @@ -87,7 +83,7 @@ class InxFile(object): def extension_class(self): """Attempt to get the extension class""" script = self.script.get('script', None) - if script is not None and util is not None: + if script is not None: name = script[:-3].replace('/', '.') spec = util.spec_from_file_location(name, script) mod = util.module_from_spec(spec) @@ -165,7 +161,7 @@ class InxElement(etree.ElementBase): if '}' in tag: (url, tag) = tag[1:].split('}', 1) return SSN.get(url, 'inx') - self.set_warning(f"No inx xml prefix.") + self.set_warning("No inx xml prefix.") return None # no default prefix def apply_nss(self, xpath, nss=None): diff --git a/inkex/paths.py b/inkex/paths.py index 72dae771..7ac5feb9 100644 --- a/inkex/paths.py +++ b/inkex/paths.py @@ -28,13 +28,10 @@ from math import atan2, cos, pi, sin, sqrt, acos, tan from .transforms import Transform, BoundingBox, Vector2d from .utils import classproperty, strargs -try: # pylint: disable=using-constant-test - from typing import overload, Any, Type, Dict, Optional, Union, Tuple, List, Iterator, Generator # pylint: disable=unused-import - from typing import TypeVar - Pathlike = TypeVar('Pathlike', bound="PathCommand") - AbsolutePathlike = TypeVar('AbsolutePathlike', bound="AbsolutePathCommand") -except ImportError: - overload = lambda x: x +from typing import overload, Any, Type, Dict, Optional, Union, Tuple, List, Iterator, Generator # pylint: disable=unused-import +from typing import TypeVar +Pathlike = TypeVar('Pathlike', bound="PathCommand") +AbsolutePathlike = TypeVar('AbsolutePathlike', bound="AbsolutePathCommand") # All the names that get added to the inkex API itself. __all__ = ( @@ -62,7 +59,7 @@ class InvalidPath(ValueError): """Raised when given an invalid path string""" -class PathCommand(object): +class PathCommand: """ Base class of all path commands """ @@ -164,14 +161,14 @@ class PathCommand(object): :param (list of tuple) last_two_points: list with last two control points in abs coords. :param (BoundingBox) bbox: bounding box to update """ - raise NotImplementedError("Bounding box is not implemented for {}".format(self.name)) + raise NotImplementedError(f"Bounding box is not implemented for {self.name}") def to_curve(self, prev, prev_prev=Vector2d()): # type: (Vector2d, Vector2d) -> Curve """Convert command to :py:class:`Curve` Curve().to_curve() returns a copy """ - raise NotImplementedError("To curve not supported for {}".format(self.name)) + raise NotImplementedError(f"To curve not supported for {self.name}") def to_curves(self, prev, prev_prev=Vector2d()): # type: (Vector2d, Vector2d) -> List[Curve] @@ -1105,7 +1102,7 @@ PathCommand._letter_to_class = { class Path(list): """A list of segment commands which combine to draw a shape""" - class PathCommandProxy(object): + class PathCommandProxy: """ A handy class for Path traverse and coordinate access @@ -1166,7 +1163,7 @@ class Path(list): return "<" + self.__class__.__name__ + ">" + repr(self.command) def __init__(self, path_d=None): - super(Path, self).__init__() + super().__init__() if isinstance(path_d, str): # Returns a generator returning PathCommand objects path_d = self.parse_string(path_d) @@ -1182,8 +1179,7 @@ class Path(list): else: self.append(Line(*item)) else: - raise TypeError("Bad path type: {}({}, ...): {}".format( - type(path_d).__name__, type(item).__name__, item)) + raise TypeError(f"Bad path type: {type(path_d).__name__}({type(item).__name__}, ...): {item}") @classmethod def parse_string(cls, path_d): @@ -1444,7 +1440,7 @@ class CubicSuperPath(list): """ def __init__(self, items): - super(CubicSuperPath, self).__init__() + super().__init__() self._closed = True self._prev = Vector2d() self._prev_prev = Vector2d() @@ -1470,7 +1466,7 @@ class CubicSuperPath(list): if isinstance(item, PathCommand): if isinstance(item, Move): if self._closed is False: - super(CubicSuperPath, self).append([]) + super().append([]) item = [list(item.args), list(item.args), list(item.args)] elif isinstance(item, ZoneClose) and self and self[-1]: # This duplicates the first segment to 'close' the path, it's appended directly @@ -1501,22 +1497,22 @@ class CubicSuperPath(list): item = item.to_bez() if not isinstance(item, list): - raise ValueError("Unknown super curve item type: {}".format(item)) + raise ValueError(f"Unknown super curve item type: {item}") if len(item) != 3 or not all([len(bit) == 2 for bit in item]): # The item is already a subpath (usually from some other process) if len(item[0]) == 3 and all([len(bit) == 2 for bit in item[0]]): - super(CubicSuperPath, self).append(self._clean(item)) + super().append(self._clean(item)) self._prev_prev = Vector2d(self[-1][-1][0]) self._prev = Vector2d(self[-1][-1][1]) return - raise ValueError("Unknown super curve list format: {}".format(item)) + raise ValueError(f"Unknown super curve list format: {item}") if self._closed: # Closed means that the previous segment is closed so we need a new one # We always append to the last open segment. CSP starts out closed. self._closed = False - super(CubicSuperPath, self).append([]) + super().append([]) if self[-1]: # The last tuple is replaced, it's the coords of where the next segment will land. diff --git a/inkex/ports.py b/inkex/ports.py index f7c5f5d7..f8e31091 100644 --- a/inkex/ports.py +++ b/inkex/ports.py @@ -31,7 +31,7 @@ try: except ImportError: serial = None -class Serial(object): +class Serial: """ Attempt to get access to the computer's serial port. diff --git a/inkex/styles.py b/inkex/styles.py index c8196bd7..e9868259 100644 --- a/inkex/styles.py +++ b/inkex/styles.py @@ -24,20 +24,17 @@ and some color handling on top. import re from collections import OrderedDict -from .utils import PY3 from .colors import Color, ColorIdError from .tween import interpcoord, interpunit -if PY3: - unicode = str # pylint: disable=redefined-builtin,invalid-name class Classes(list): """A list of classes applied to an element (used in css and js)""" def __init__(self, classes=None, callback=None): self.callback = None - if isinstance(classes, (str, unicode)): + if isinstance(classes, str): classes = classes.split() - super(Classes, self).__init__(classes or ()) + super().__init__(classes or ()) self.callback = callback def __str__(self): @@ -48,19 +45,19 @@ class Classes(list): self.callback(self) def __setitem__(self, index, value): - super(Classes, self).__setitem__(index, value) + super().__setitem__(index, value) self._callback() def append(self, value): value = str(value) if value not in self: - super(Classes, self).append(value) + super().append(value) self._callback() def remove(self, value): value = str(value) if value in self: - super(Classes, self).remove(value) + super().remove(value) self._callback() def toggle(self, value): @@ -81,13 +78,13 @@ class Style(OrderedDict): self.callback = None # Either a string style or kwargs (with dashes as underscores). style = style or [(k.replace('_', '-'), v) for k, v in kw.items()] - if isinstance(style, (str, unicode)): + if isinstance(style, str): style = self.parse_str(style) # Order raw dictionaries so tests can be made reliable if isinstance(style, dict) and not isinstance(style, OrderedDict): style = [(name, style[name]) for name in sorted(style)] # Should accept dict, Style, parsed string, list etc. - super(Style, self).__init__(style) + super().__init__(style) # Now after the initial data, the callback makes sense. self.callback = callback @@ -145,12 +142,12 @@ class Style(OrderedDict): def update(self, other): """Make sure callback is called when updating""" - super(Style, self).update(Style(other)) + super().update(Style(other)) if self.callback is not None: self.callback(self) def __setitem__(self, key, value): - super(Style, self).__setitem__(key, value) + super().__setitem__(key, value) if self.callback is not None: self.callback(self) @@ -169,8 +166,8 @@ class Style(OrderedDict): def update_urls(self, old_id, new_id): """Find urls in this style and replace them with the new id""" for (name, value) in self.items(): - if value == 'url(#{})'.format(old_id): - self[name] = 'url(#{})'.format(new_id) + if value == f"url(#{old_id})": + self[name] = f"url(#{new_id})" def interpolate_prop(self, other, fraction, prop, svg=None): """Interpolate specific property.""" @@ -205,7 +202,7 @@ class Style(OrderedDict): return style -class AttrFallbackStyle(object): +class AttrFallbackStyle: """ A container for a style and an element that may have competing styles @@ -263,7 +260,7 @@ class StyleSheets(list): re-created on the fly by lxml so lookups have to be centralised. """ def __init__(self, svg=None): - super(StyleSheets, self).__init__() + super().__init__() self.svg = svg def lookup(self, element_id, svg=None): @@ -286,7 +283,7 @@ class StyleSheet(list): comment_strip = re.compile(r"//.*?\n") def __init__(self, content=None, callback=None): - super(StyleSheet, self).__init__() + super().__init__() self.callback = None # Remove comments content = self.comment_strip.sub('', (content or '')) @@ -314,7 +311,7 @@ class StyleSheet(list): return # Warning? rules, style = other.strip('}').split('{', 1) other = ConditionalStyle(rules=rules, style=style.strip(), callback=self._callback) - super(StyleSheet, self).append(other) + super().append(other) self._callback() def lookup(self, element_id, svg): @@ -331,7 +328,7 @@ class ConditionalStyle(Style): rather than being an attribute style. """ def __init__(self, rules='*', style=None, callback=None, **kwargs): - super(ConditionalStyle, self).__init__(style=style, callback=callback, **kwargs) + super().__init__(style=style, callback=callback, **kwargs) self.rules = [ConditionalRule(rule) for rule in rules.split(',')] def __str__(self): @@ -339,8 +336,8 @@ class ConditionalStyle(Style): content = self.to_str(";\n ") rules = ",\n".join(str(rule) for rule in self.rules) if content: - return "{0} {{\n {1};\n}}".format(rules, content) - return "{0} {{}}".format(rules) + return f"{rules} {{\n {content};\n}}" + return f"{rules} {{}}" def to_xpath(self): """Convert all rules to an xpath""" @@ -349,7 +346,7 @@ class ConditionalStyle(Style): # this xpath transform and provides no extra functionality for reverse lookups. return '|'.join([rule.to_xpath() for rule in self.rules]) -class ConditionalRule(object): +class ConditionalRule: """A single css rule""" step_to_xpath = [ (re.compile(r'\[(\w+)\^=([^\]]+)\]'), r'[starts-with(@\1,\2)]'), # Starts With diff --git a/inkex/tester/__init__.py b/inkex/tester/__init__.py index f33f6950..3362489e 100644 --- a/inkex/tester/__init__.py +++ b/inkex/tester/__init__.py @@ -66,8 +66,6 @@ of the `.export` suffix. pytest should then be re-run to confirm before committing to the repository. """ -from __future__ import absolute_import, print_function, unicode_literals - import os import re import sys @@ -83,7 +81,7 @@ import xml.etree.ElementTree as xml from unittest import TestCase as BaseCase from inkex.base import InkscapeExtension -from ..utils import PY3, to_bytes +from ..utils import to_bytes from .xmldiff import xmldiff from .mock import MockCommandMixin, Capture @@ -115,29 +113,20 @@ class TestCase(MockCommandMixin, BaseCase): stderr_output = False stdout_protect = True stderr_protect = True - python3_only = False def __init__(self, *args, **kw): - super(TestCase, self).__init__(*args, **kw) + super().__init__(*args, **kw) self._temp_dir = None self._effect = None def setUp(self): # pylint: disable=invalid-name """Make sure every test is seeded the same way""" self._effect = None - super(TestCase, self).setUp() - if self.python3_only and not PY3: - self.skipTest("No available in python2") - try: - # python3, with version 1 to get the same numbers - # as in python2 during tests. - random.seed(0x35f, version=1) - except TypeError: - # But of course this kwarg doesn't exist in python2 - random.seed(0x35f) + super().setUp() + random.seed(0x35f) def tearDown(self): - super(TestCase, self).tearDown() + super().tearDown() if self._temp_dir and os.path.isdir(self._temp_dir): shutil.rmtree(self._temp_dir) @@ -186,7 +175,7 @@ class TestCase(MockCommandMixin, BaseCase): full_path = os.path.join(cls.datadir(), filename, *parts) if not os.path.isfile(full_path): - raise IOError("Can't find test data file: {}".format(full_path)) + raise IOError(f"Can't find test data file: {full_path}") return full_path @property @@ -256,7 +245,7 @@ class TestCase(MockCommandMixin, BaseCase): self._effect = self.effect_class() return self._effect -class InkscapeExtensionTestMixin(object): +class InkscapeExtensionTestMixin: """Automatically setup self.effect for each test and test with an empty svg""" def setUp(self): # pylint: disable=invalid-name """Check if there is an effect_class set and create self.effect if it is""" @@ -268,7 +257,7 @@ class InkscapeExtensionTestMixin(object): """Extension works with empty svg file""" self.effect.run([self.empty_svg]) -class ComparisonMixin(object): +class ComparisonMixin: """ Add comparison tests to any existing test suite. """ @@ -320,7 +309,7 @@ class ComparisonMixin(object): outfile = self.get_compare_outfile(args) if not os.path.isfile(outfile): - raise IOError("Comparison file {} not found".format(outfile)) + raise IOError(f"Comparison file {outfile} not found") data_a = effect.test_output.getvalue() if os.environ.get('EXPORT_COMPARE', False): @@ -328,7 +317,7 @@ class ComparisonMixin(object): if sys.version_info[0] == 3 and isinstance(data_a, str): data_a = data_a.encode('utf-8') fhl.write(self._apply_compare_filters(data_a, True)) - print("Written output: {}.export".format(outfile)) + print(f"Written output: {outfile}.export") data_a = self._apply_compare_filters(data_a) @@ -343,7 +332,7 @@ class ComparisonMixin(object): print('The XML is different, you can save the output using the EXPORT_COMPARE=1'\ ' envionment variable. This will save the compared file as a ".output" file'\ ' next to the reference file used in the test.\n') - diff = 'SVG Differences: {}\n\n'.format(outfile) + diff = f"SVG Differences: {outfile}\n\n" if os.environ.get('XML_DIFF', False): diff = '<- ' + diff_xml else: @@ -352,7 +341,7 @@ class ComparisonMixin(object): # Take advantage of better text diff in testcase's own asserts. self.assertEqual(value_a, value_b) except AssertionError as err: - diff += " {}. {}\n".format(x, str(err)) + diff += f" {x}. {str(err)}\n" self.assertTrue(delta, diff) else: # compare any content (non svg) @@ -381,4 +370,4 @@ class ComparisonMixin(object): # avoid filename-too-long error opstr = hashlib.md5(opstr.encode('latin1')).hexdigest() opstr = '__' + opstr - return self.data_file("refs", "{}{}.out".format(self.effect_name, opstr)) + return self.data_file("refs", f"{self.effect_name}{opstr}.out") diff --git a/inkex/tester/filters.py b/inkex/tester/filters.py index 6fd4f0ab..c6703716 100644 --- a/inkex/tester/filters.py +++ b/inkex/tester/filters.py @@ -33,7 +33,7 @@ filters that are being used. import re from ..utils import to_bytes -class Compare(object): +class Compare: """ Comparison base class, this acts as a passthrough unless the filter staticmethod is overwritten. diff --git a/inkex/tester/mock.py b/inkex/tester/mock.py index 2df87919..c6507fe4 100644 --- a/inkex/tester/mock.py +++ b/inkex/tester/mock.py @@ -43,7 +43,7 @@ if False: # pylint: disable=using-constant-test FIXED_BOUNDARY = '--CALLDATA--//--CALLDATA--' -class Capture(object): +class Capture: """Capture stdout or stderr. Used as `with Capture('stdout') as stream:`""" def __init__(self, io_name='stdout', swap=True): self.io_name = io_name @@ -52,8 +52,6 @@ class Capture(object): self.swap = swap def __enter__(self): - # We can't control python2 correctly (unicode vs. bytes-like) but - # we don't need it, so we're ignore python2 as if it doesn't exist. if self.swap: setattr(sys, self.io_name, self.stream) return self.stream @@ -64,7 +62,7 @@ class Capture(object): self.original.write(self.stream.getvalue()) setattr(sys, self.io_name, self.original) -class ManualVerbosity(object): +class ManualVerbosity: """Change the verbosity of the test suite manually""" result = property(lambda self: self.test._current_result) @@ -82,7 +80,7 @@ class ManualVerbosity(object): __exit__ = flip -class MockMixin(object): +class MockMixin: """ Add mocking ability to any test base class, will set up mock on setUp and remove it on tearDown. @@ -118,7 +116,7 @@ class MockMixin(object): def setUp(self): # pylint: disable=invalid-name """For each mock instruction, set it up and store the return""" - super(MockMixin, self).setUp() + super().setUp() for x, mock in enumerate(self.mocks): if len(mock) == 4: logging.error("Mock was already set up, so it wasn't cleared previously!") @@ -127,7 +125,7 @@ class MockMixin(object): def tearDown(self): # pylint: disable=invalid-name """For each returned stored, tear it down and restore mock instruction""" - super(MockMixin, self).tearDown() + super().tearDown() try: for x, (owner, name, old, _) in enumerate(self.mocks): self.mocks[x] = (owner, name, getattr(owner, name)) @@ -155,7 +153,7 @@ class MockCommandMixin(MockMixin): recorded_tempdirs = [] # type:List[str] def setUp(self): # pylint: disable=invalid-name - super(MockCommandMixin, self).setUp() + super().setUp() # This is a the daftest thing I've ever seen, when in the middle # of a mock, the 'self' variable magically turns from a FooTest # into a TestCase, this makes it impossible to find the datadir. @@ -198,7 +196,7 @@ class MockCommandMixin(MockMixin): path = os.path.join(fdir, fname) # We store the modified time so if a program modifies # the input file in-place, it will look different. - ret.add(path + ';{}'.format(os.path.getmtime(path))) + ret.add(path + f";{os.path.getmtime(path)}") return ret @@ -273,9 +271,9 @@ class MockCommandMixin(MockMixin): return self.load_call(program, key, outputs) except IOError: self.save_key(program, key, keystr, 'bad-key') - raise IOError("Problem loading call: {}/{} use the environment variable "\ + raise IOError(f"Problem loading call: {program}/{key} use the environment variable "\ "NO_MOCK_COMMANDS=1 to call out to the external program and generate "\ - "the mock call file.".format(program, key)) + "the mock call file.") def add_call_files(self, msg, args, kwargs): """ @@ -331,7 +329,7 @@ class MockCommandMixin(MockMixin): path = self.get_call_path(program, create=create) fname = os.path.join(path, key + '.msg') if not create and not os.path.isfile(fname): - raise IOError("Attempted to find call test data {}".format(key)) + raise IOError(f"Attempted to find call test data {key}") return fname def get_program_name(self, program): @@ -348,9 +346,8 @@ class MockCommandMixin(MockMixin): os.makedirs(command_dir) else: raise IOError("A test is attempting to use an external program in a test:"\ - " {}; but there is not a command data directory which should"\ - " contain the results of the command here: {}"\ - .format(program, command_dir)) + f" {program}; but there is not a command data directory which should"\ + f" contain the results of the command here: {command_dir}") return command_dir def load_call(self, program, key, files): diff --git a/inkex/tester/svg.py b/inkex/tester/svg.py index d7ef569e..b1bd20e4 100644 --- a/inkex/tester/svg.py +++ b/inkex/tester/svg.py @@ -32,7 +32,7 @@ def svg(svg_attrs=''): """ return etree.fromstring(str.encode( '' - ''.format(svg_attrs)), parser=SVG_PARSER) + f''), parser=SVG_PARSER) def uu_svg(user_unit): @@ -40,7 +40,7 @@ def uu_svg(user_unit): It's based on the ratio between the SVG width and the viewBox width. """ - return svg('width="1{}" viewBox="0 0 1 1"'.format(user_unit)) + return svg(f'width="1{user_unit}" viewBox="0 0 1 1"') def svg_file(filename): """Parse an svg file and return it's document root""" diff --git a/inkex/tester/xmldiff.py b/inkex/tester/xmldiff.py index 17f43a4f..40cf5557 100644 --- a/inkex/tester/xmldiff.py +++ b/inkex/tester/xmldiff.py @@ -27,9 +27,9 @@ class DeltaLogger(list): def append_tag(self, tag_a, tag_b): """Record a tag difference""" if tag_a: - tag_a = "<{}.../>".format(tag_a) + tag_a = f"<{tag_a}.../>" if tag_b: - tag_b = "<{}.../>".format(tag_b) + tag_b = f"<{tag_b}.../>" self.append((tag_a, tag_b)) def append_attr(self, attr, value_a, value_b): @@ -55,7 +55,7 @@ class DeltaLogger(list): def __repr__(self): if self: return "No differences detected" - return "{} xml differences".format(len(self)) + return f"{len(self)} xml differences" def to_xml(data): """Convert string or bytes to xml parsed root node""" @@ -74,7 +74,7 @@ def xmldiff(data1, data2): def _xmldiff(xml1, xml2, delta): if xml1.tag != xml2.tag: - xml1.tag = '{}XXX{}'.format(xml1.tag, xml2.tag) + xml1.tag = f"{xml1.tag}XXX{xml2.tag}" delta.append_tag(xml1.tag, xml2.tag) for name, value in xml1.attrib.items(): if name not in xml2.attrib: @@ -82,17 +82,17 @@ def _xmldiff(xml1, xml2, delta): xml1.attrib[name] += "XXX" elif xml2.attrib.get(name) != value: delta.append_attr(name, xml1.attrib.get(name), xml2.attrib.get(name)) - xml1.attrib[name] = "{}XXX{}".format(xml1.attrib.get(name), xml2.attrib.get(name)) + xml1.attrib[name] = f"{xml1.attrib.get(name)}XXX{xml2.attrib.get(name)}" for name, value in xml2.attrib.items(): if name not in xml1.attrib: delta.append_attr(name, None, value) xml1.attrib[name] = "XXX" + value if not text_compare(xml1.text, xml2.text): delta.append_text(xml1.text, xml2.text) - xml1.text = "{}XXX{}".format(xml1.text, xml2.text) + xml1.text = f"{xml1.text}XXX{xml2.text}" if not text_compare(xml1.tail, xml2.tail): delta.append_text(xml1.tail, xml2.tail) - xml1.tail = "{}XXX{}".format(xml1.tail, xml2.tail) + xml1.tail = f"{xml1.tail}XXX{xml2.tail}" # Get children and pad with nulls children_a = list(xml1) diff --git a/inkex/transforms.py b/inkex/transforms.py index 1f7cac9f..a253ed83 100644 --- a/inkex/transforms.py +++ b/inkex/transforms.py @@ -31,17 +31,13 @@ from decimal import Decimal from math import cos, radians, sin, sqrt, tan, fabs, atan2, hypot, pi, isfinite from .tween import interpcoord -from .utils import strargs, KeyDict, PY3 +from .utils import strargs, KeyDict -try: - from typing import overload, cast, List, Any, Callable, Generator, Iterator, Tuple, Union, Optional, Sequence # pylint: disable=unused-import +from typing import overload, cast, List, Any, Callable, Generator, Iterator, Tuple, Union, Optional, Sequence # pylint: disable=unused-import - VectorLike = Union["ImmutableVector2d", Tuple[float, float]] # pylint: disable=invalid-name - MatrixLike = Union[str, Tuple[Tuple[float,float,float], Tuple[float,float,float]], Tuple[float,float,float,float,float,float], "Transform"] - BoundingIntervalArgs = Union['BoundingInterval', Tuple[float, float], float] # pylint: disable=invalid-name -except ImportError: - overload = lambda x: x - cast = lambda x, y: y +VectorLike = Union["ImmutableVector2d", Tuple[float, float]] # pylint: disable=invalid-name +MatrixLike = Union[str, Tuple[Tuple[float,float,float], Tuple[float,float,float]], Tuple[float,float,float,float,float,float], "Transform"] +BoundingIntervalArgs = Union['BoundingInterval', Tuple[float, float], float] # pylint: disable=invalid-name # All the names that get added to the inkex API itself. __all__ = ( @@ -52,8 +48,6 @@ __all__ = ( 'Vector2d', ) -if PY3: - unicode = str # pylint: disable=redefined-builtin,invalid-name # Old settings, supported because users click 'ok' without looking. XAN = KeyDict({'l': 'left', 'r': 'right', 'm': 'center_x'}) @@ -63,7 +57,7 @@ CUSTOM_DIRECTION = {270: 'tb', 90: 'bt', 0: 'lr', 360: 'lr', 180: 'rl'} DIRECTION = ['tb', 'bt', 'lr', 'rl', 'ro', 'ri'] -class ImmutableVector2d(object): +class ImmutableVector2d: """Represents an immutable element of 2-dimensional Euclidean space""" _x = 0.0 _y = 0.0 @@ -107,7 +101,7 @@ class ImmutableVector2d(object): elif isinstance(point, str) and point.count(',') == 1: x, y = map(float, point.split(',')) else: - raise ValueError("Can't parse {}".format(repr(point))) + raise ValueError(f"Can't parse {repr(point)}") return x, y def __add__(self, other): @@ -164,11 +158,11 @@ class ImmutableVector2d(object): def __repr__(self): # type: () -> str - return "Vector2d({:.6g}, {:.6g})".format(self.x, self.y) + return f"Vector2d({self.x:.6g}, {self.y:.6g})" def __str__(self): # type: () -> str - return "{:.6g}, {:.6g}".format(self.x, self.y) + return f"{self.x:.6g}, {self.y:.6g}" def __iter__(self): # type: () -> Generator[float, None, None] @@ -303,7 +297,7 @@ class Vector2d(ImmutableVector2d): -class Transform(object): +class Transform: """A transformation object which will always reduce to a matrix and can then be used in combination with other transformations for reducing finding a point and printing svg ready output. @@ -345,7 +339,7 @@ class Transform(object): def _set_matrix(self, matrix): # type: (MatrixLike) -> None """Parse a given string as an svg transformation instruction.""" - if isinstance(matrix, (str, unicode)): + if isinstance(matrix, str): for func, values in self.TRM.findall(matrix.strip()): getattr(self, 'add_' + func.lower())(*strargs(values)) elif isinstance(matrix, Transform): @@ -359,18 +353,18 @@ class Transform(object): row2 = cast("Tuple[float, float, float]", tuple(map(float, row2))) self.matrix = row1, row2 else: - raise ValueError("Matrix '{}' is not a valid transformation matrix".format(matrix)) + raise ValueError(f"Matrix '{matrix}' is not a valid transformation matrix") else: - raise ValueError("Matrix '{}' is not a valid transformation matrix".format(matrix)) + raise ValueError(f"Matrix '{matrix}' is not a valid transformation matrix") elif isinstance(matrix, (list, tuple)) and len(matrix) == 6: tmatrix = cast("Union[List[float], Tuple[float,float,float,float,float,float]]", matrix) row1 = (float(tmatrix[0]), float(tmatrix[2]), float(tmatrix[4])) row2 = (float(tmatrix[1]), float(tmatrix[3]), float(tmatrix[5])) self.matrix = row1, row2 elif not isinstance(matrix, (list, tuple)): - raise ValueError("Invalid transform type: {}".format(type(matrix).__name__)) + raise ValueError(f"Invalid transform type: {type(matrix).__name__}") else: - raise ValueError("Matrix '{}' is not a valid transformation matrix".format(matrix)) + raise ValueError(f"Matrix '{matrix}' is not a valid transformation matrix") # These provide quick access to the svg matrix: @@ -413,7 +407,7 @@ class Transform(object): elif len(args) == 2 or len(args) == 6: self.__imul__(Transform(args)) else: - raise ValueError("Invalid number of arguments {}".format(args)) + raise ValueError(f"Invalid number of arguments {args}") def add_kwargs(self, **kwargs): """Add translations, scales, rotations etc using key word arguments""" @@ -527,20 +521,20 @@ class Transform(object): if self.is_translate(): if not self: return "" - return "translate({:.6g}, {:.6g})".format(self.e, self.f) + return f"translate({self.e:.6g}, {self.f:.6g})" elif self.is_scale(): - return "scale({:.6g}, {:.6g})".format(self.a, self.d) + return f"scale({self.a:.6g}, {self.d:.6g})" elif self.is_rotate(): - return "rotate({:.6g})".format(self.rotation_degrees()) - return "matrix({})".format(" ".join(format(var, '.6g') for var in hexad)) + return f"rotate({self.rotation_degrees():.6g})" + return "matrix({})".format(" ".join(f"{var:.6g}" for var in hexad)) def __repr__(self): # type: () -> str """String representation of this object""" return "{}((({}), ({})))".format( type(self).__name__, - ', '.join(format(var, '.6g') for var in self.matrix[0]), - ', '.join(format(var, '.6g') for var in self.matrix[1])) + ', '.join(f"{var:.6g}" for var in self.matrix[0]), + ', '.join(f"{var:.6g}" for var in self.matrix[1])) def __eq__(self, matrix): # typing this requires writing a proof for mypy that matrix is really @@ -593,7 +587,7 @@ class Transform(object): # type: (VectorLike) -> Vector2d """Transform a tuple (X, Y)""" if isinstance(point, str): - raise ValueError("Will not transform string '{}'".format(point)) + raise ValueError(f"Will not transform string '{point}'") point = Vector2d(point) return Vector2d(self.a * point.x + self.c * point.y + self.e, self.b * point.x + self.d * point.y + self.f) @@ -621,7 +615,7 @@ class Transform(object): interpcoord(self.f, other.f, fraction))) -class BoundingInterval(object): # pylint: disable=too-few-public-methods +class BoundingInterval: # pylint: disable=too-few-public-methods """A pair of numbers that represent the minimum and maximum values.""" @overload @@ -650,8 +644,7 @@ class BoundingInterval(object): # pylint: disable=too-few-public-methods self.minimum = x self.maximum = y else: - raise ValueError("Not a number for scaling: {} ({},{})" - .format(str((x, y)), type(x).__name__, type(y).__name__)) + raise ValueError(f"Not a number for scaling: {str((x, y))} ({type(x).__name__},{type(y).__name__})") else: value = x @@ -666,8 +659,7 @@ class BoundingInterval(object): # pylint: disable=too-few-public-methods elif isinstance(value, (int, float, Decimal)): self.minimum = self.maximum = value else: - raise ValueError("Not a number for scaling: {} ({})" - .format(str(value), type(value).__name__)) + raise ValueError(f"Not a number for scaling: {str(value)} ({type(value).__name__})") def __bool__(self): # type: () -> bool @@ -751,7 +743,7 @@ class BoundingInterval(object): # pylint: disable=too-few-public-methods def __repr__(self): # type: () -> str - return "BoundingInterval({}, {})".format(self.minimum, self.maximum) + return f"BoundingInterval({self.minimum}, {self.maximum})" @property def center(self): @@ -766,7 +758,7 @@ class BoundingInterval(object): # pylint: disable=too-few-public-methods return self.maximum - self.minimum -class BoundingBox(object): # pylint: disable=too-few-public-methods +class BoundingBox: # pylint: disable=too-few-public-methods """ Some functions to compute a rough bbox of a given list of objects. @@ -802,8 +794,7 @@ class BoundingBox(object): # pylint: disable=too-few-public-methods elif isinstance(x, BoundingBox): x, y = x.x, x.y else: - raise ValueError("Not a number for scaling: {} ({})" - .format(str(x), type(x).__name__)) + raise ValueError(f"Not a number for scaling: {str(x)} ({type(x).__name__})") self.x = BoundingInterval(x) self.y = BoundingInterval(y) @@ -893,7 +884,7 @@ class BoundingBox(object): # pylint: disable=too-few-public-methods def __repr__(self): # type: () -> str - return "BoundingBox({},{})".format(tuple(self.x), tuple(self.y)) + return f"BoundingBox({tuple(self.x)},{tuple(self.y)})" @property def center(self): @@ -932,7 +923,7 @@ class BoundingBox(object): # pylint: disable=too-few-public-methods return [y, -y, x, -x, rot, -rot][DIRECTION.index(direction)] -class DirectedLineSegment(object): +class DirectedLineSegment: """ A directed line segment @@ -973,7 +964,7 @@ class DirectedLineSegment(object): elif len(args) == 2: # overload 2 start, end = args else: - raise ValueError("DirectedLineSegment() can't be constructed from {}".format(args)) + raise ValueError(f"DirectedLineSegment() can't be constructed from {args}") self.start = Vector2d(start) self.end = Vector2d(end) @@ -1064,7 +1055,7 @@ class DirectedLineSegment(object): def __repr__(self): # type: () -> str - return "DirectedLineSegment(({0.start}), ({0.end}))".format(self) + return f"DirectedLineSegment(({self.start}), ({self.end}))" def cubic_extrema(py0, py1, py2, py3): diff --git a/inkex/turtle.py b/inkex/turtle.py index cee7dadb..f2869b94 100644 --- a/inkex/turtle.py +++ b/inkex/turtle.py @@ -21,7 +21,7 @@ import math import random -class pTurtle(object): +class pTurtle: """A Python path turtle""" def __init__(self, home=(0, 0)): diff --git a/inkex/tween.py b/inkex/tween.py index 31c165b1..b3e76fdb 100644 --- a/inkex/tween.py +++ b/inkex/tween.py @@ -23,13 +23,10 @@ from bisect import bisect_left from .utils import X, Y from .units import convert_unit, parse_unit, render_unit -try: - from typing import Union, Tuple, List, TypeVar, Callable, overload - hasTypes = True - Value = TypeVar('Value') - Number = TypeVar('Number', int, float) -except ImportError: - pass +from typing import Union, Tuple, List, TypeVar, Callable, overload +hasTypes = True +Value = TypeVar('Value') +Number = TypeVar('Number', int, float) def interpcoord( diff --git a/inkex/units.py b/inkex/units.py index cd69a395..2dad5b8a 100644 --- a/inkex/units.py +++ b/inkex/units.py @@ -102,6 +102,6 @@ def render_unit(value, unit): try: if isinstance(value, str): (value, unit) = parse_unit(value, default_unit=unit) - return "{:.6g}{:s}".format(value, unit) + return f"{value:.6g}{ unit:s}" except TypeError: return '' diff --git a/inkex/utils.py b/inkex/utils.py index b0985f4c..0302fb67 100644 --- a/inkex/utils.py +++ b/inkex/utils.py @@ -20,11 +20,8 @@ """ Basic common utility functions for calculated things """ -from __future__ import absolute_import, print_function, unicode_literals - import os import sys -import shutil import random import math @@ -32,9 +29,6 @@ from itertools import tee from collections import defaultdict from argparse import ArgumentTypeError -# When python2 support is gone, enable tempfile's version -# from tempfile import TemporaryDirectory - # All the names that get added to the inkex API itself. __all__ = ('AbortExtension', 'DependencyError', 'Boolean', 'errormsg', 'addNS', 'NSS') @@ -43,9 +37,6 @@ ABORT_STATUS = -5 (X, Y) = range(2) PY3 = sys.version_info[0] == 3 -if PY3: - unicode = str # pylint: disable=redefined-builtin,invalid-name - # a dictionary of all of the xmlns prefixes in a standard inkscape doc NSS = { 'sodipodi': 'http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd', @@ -83,6 +74,7 @@ def get_inkscape_directory(): if os.path.isdir(os.path.join(pth, 'inkex')): return pth + class KeyDict(dict): """ A normal dictionary, except asking for anything not in the dictionary @@ -90,23 +82,10 @@ class KeyDict(dict): """ def __getitem__(self, key): try: - return super(KeyDict, self).__getitem__(key) + return super().__getitem__(key) except KeyError: return key -class TemporaryDirectory(object): # pylint: disable=too-few-public-methods - """Tiny replacement for python3's version.""" - def __init__(self, suffix="", prefix="tmp"): - self.suffix = suffix - self.prefix = prefix - self.path = None - def __enter__(self): - from tempfile import mkdtemp - self.path = mkdtemp(self.suffix, self.prefix, None) - return self.path - def __exit__(self, exc, value, traceback): - if os.path.isdir(self.path): - shutil.rmtree(self.path) def Boolean(value): """ArgParser function to turn a boolean string into a python boolean""" @@ -147,7 +126,7 @@ def errormsg(msg): try: sys.stderr.write(msg) except TypeError: - sys.stderr.write(unicode(msg)) + sys.stderr.write(str(msg)) except UnicodeEncodeError: # Python 2: # Fallback for cases where sys.stderr.encoding is not Unicode. @@ -232,7 +211,7 @@ def splitNS(name): # pylint: disable=invalid-name (prefix, tag) = removeNS(name) return (NSS[prefix], tag) -class classproperty(object): # pylint: disable=invalid-name, too-few-public-methods +class classproperty: # pylint: disable=invalid-name, too-few-public-methods """Combine classmethod and property decorators""" def __init__(self, func): @@ -257,7 +236,7 @@ def pairwise(iterable, start=True): starter = [] return starter + list(zip(first, then)) -class CloningVat(object): +class CloningVat: """ When modifying defs, sometimes we want to know if every backlink would have needed changing, or it was just some of them. diff --git a/tests/test_inkex_bounding_box.py b/tests/test_inkex_bounding_box.py index e074476c..a44dacc8 100644 --- a/tests/test_inkex_bounding_box.py +++ b/tests/test_inkex_bounding_box.py @@ -17,7 +17,7 @@ from inkex import ( Style ) from inkex.tester import TestCase -from inkex.utils import TemporaryDirectory +from tempfile import TemporaryDirectory from inkex.command import is_inkscape_available from inkex.tester.decorators import requires_inkscape try: -- GitLab From df0c5ff487f2e4b6245b87c66f8b585944fb4cd7 Mon Sep 17 00:00:00 2001 From: pulsar17 <5243241-pulsar17@users.noreply.gitlab.com> Date: Fri, 26 Mar 2021 04:18:53 +0530 Subject: [PATCH 3/4] Fix missing Dict type --- inkex/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inkex/base.py b/inkex/base.py index 988e59ad..a09543ac 100644 --- a/inkex/base.py +++ b/inkex/base.py @@ -24,7 +24,7 @@ import os import sys import copy -from typing import List, Tuple, Type, Optional, Callable, Any, Union, IO, TYPE_CHECKING, cast +from typing import Dict, List, Tuple, Type, Optional, Callable, Any, Union, IO, TYPE_CHECKING, cast from argparse import ArgumentParser, Namespace from lxml import etree -- GitLab From 6b348535bc079089470794cbd7dc4f23d312216a Mon Sep 17 00:00:00 2001 From: pulsar17 <5243241-pulsar17@users.noreply.gitlab.com> Date: Fri, 26 Mar 2021 22:13:02 +0530 Subject: [PATCH 4/4] Rebase drop-py to master --- inkex/utils.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/inkex/utils.py b/inkex/utils.py index 7f2eacaa..ca1a4a3e 100644 --- a/inkex/utils.py +++ b/inkex/utils.py @@ -36,20 +36,6 @@ ABORT_STATUS = -5 (X, Y) = range(2) PY3 = sys.version_info[0] == 3 -# a dictionary of all of the xmlns prefixes in a standard inkscape doc -NSS = { - 'sodipodi': 'http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd', - 'cc': 'http://creativecommons.org/ns#', - 'ccOLD': 'http://web.resource.org/cc/', - 'svg': 'http://www.w3.org/2000/svg', - 'dc': 'http://purl.org/dc/elements/1.1/', - 'rdf': 'http://www.w3.org/1999/02/22-rdf-syntax-ns#', - 'inkscape': 'http://www.inkscape.org/namespaces/inkscape', - 'xlink': 'http://www.w3.org/1999/xlink', - 'xml': 'http://www.w3.org/XML/1998/namespace' -} -SSN = dict((b, a) for (a, b) in NSS.items()) - def _pythonpath(): for pth in os.environ.get('PYTHONPATH', '').split(':'): if os.path.isdir(pth): -- GitLab