Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 0 additions & 34 deletions .github/workflows/github-ci-legacy.yml

This file was deleted.

4 changes: 2 additions & 2 deletions .github/workflows/github-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ jobs:
continue-on-error: true
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
python-version: [3.7, 3.8, 3.9, "3.10", "3.11"]
os: [ubuntu-24.04, macos-15, windows-2025]
python-version: [3.8, 3.9, "3.10", "3.11", "3.12", "3.13"]
steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- uses: actions/checkout@v2
Expand Down
39 changes: 19 additions & 20 deletions .github/workflows/publish-on-pypi-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,32 @@ name: Publish to TestPyPI

on:
push:
branches:
- master
branches: [master]
pull_request:
branches: [master]
workflow_dispatch:

jobs:
build-n-publish:
name: Build and publish to TestPyPI
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@master
- name: Set up Python 3
uses: actions/setup-python@v1
- uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: 3
- name: Install pypa/build
run: >-
python -m
pip install
build
--user
python-version: '3.8'

- name: Upgrade pip
run: python -m pip install --upgrade pip

- name: Install build tool
run: python -m pip install build

- name: Build a binary wheel and a source tarball
run: >-
python -m
build
--sdist
--wheel
--outdir dist/
.
run: python -m build --sdist --wheel --outdir dist/

- name: Publish to Test PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
Expand Down
35 changes: 17 additions & 18 deletions .github/workflows/publish-on-pypi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,35 +8,34 @@ on:
jobs:
build-n-publish:
name: Build and publish to TestPyPI and PyPI
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@master
- name: Set up Python 3
uses: actions/setup-python@v1
- uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: 3
- name: Install pypa/build
run: >-
python -m
pip install
build
--user
python-version: '3.8'

- name: Upgrade pip
run: python -m pip install --upgrade pip

- name: Install build tool
run: python -m pip install build

- name: Build a binary wheel and a source tarball
run: >-
python -m
build
--sdist
--wheel
--outdir dist/
.
run: python -m build --sdist --wheel --outdir dist/

- name: Publish to Test PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
skip_existing: true
password: ${{ secrets.TESTPYPI_API_TOKEN }}
repository_url: https://test.pypi.org/legacy/

- name: Publish to PyPI
if: startsWith(github.ref, 'refs/tags')
uses: pypa/gh-action-pypi-publish@release/v1
with:
skip_existing: true
password: ${{ secrets.PYPI_API_TOKEN }}
12 changes: 5 additions & 7 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import os


VERSION = '1.6.1'
VERSION = '1.7.0'
AUTHOR_NAME = 'Andy Port'
AUTHOR_EMAIL = 'AndyAPort@gmail.com'
GITHUB = 'https://github.com/mathandy/svgpathtools'
Expand All @@ -27,27 +27,25 @@ def read(relative_path):
author=AUTHOR_NAME,
author_email=AUTHOR_EMAIL,
url=GITHUB,
download_url='{}/releases/download/{}/svgpathtools-{}-py2.py3-none-any.whl'
download_url='{}/releases/download/{}/svgpathtools-{}-py3-none-any.whl'
''.format(GITHUB, VERSION, VERSION),
license='MIT',
install_requires=['numpy', 'svgwrite', 'scipy'],
python_requires='>=3.8',
platforms="OS Independent",
keywords=['svg', 'svg path', 'svg.path', 'bezier', 'parse svg path', 'display svg'],
classifiers=[
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
"Programming Language :: Python :: 2",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 2.7",
"Programming Language :: Python :: 3.5",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Topic :: Multimedia :: Graphics :: Editors :: Vector-Based",
"Topic :: Scientific/Engineering",
"Topic :: Scientific/Engineering :: Image Recognition",
Expand Down
36 changes: 23 additions & 13 deletions svgpathtools/path.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
Arc."""

# External dependencies
from __future__ import division, absolute_import, print_function
from __future__ import annotations
import re
try:
from collections.abc import MutableSequence # noqa
Expand Down Expand Up @@ -40,6 +40,7 @@
except NameError:
pass


COMMANDS = set('MmZzLlHhVvCcSsQqTtAa')
UPPERCASE = set('MZLHVCSQTA')

Expand Down Expand Up @@ -602,15 +603,15 @@ def __init__(self, start, end):
self.start = start
self.end = end

def __hash__(self):
def __hash__(self) -> int:
return hash((self.start, self.end))

def __repr__(self):
return 'Line(start=%s, end=%s)' % (self.start, self.end)

def __eq__(self, other):
if not isinstance(other, Line):
return NotImplemented
return False
return self.start == other.start and self.end == other.end

def __ne__(self, other):
Expand Down Expand Up @@ -874,7 +875,7 @@ def __init__(self, start, control, end):
# used to know if self._length needs to be updated
self._length_info = {'length': None, 'bpoints': None}

def __hash__(self):
def __hash__(self) -> int:
return hash((self.start, self.control, self.end))

def __repr__(self):
Expand All @@ -883,7 +884,7 @@ def __repr__(self):

def __eq__(self, other):
if not isinstance(other, QuadraticBezier):
return NotImplemented
return False
return self.start == other.start and self.end == other.end \
and self.control == other.control

Expand Down Expand Up @@ -1145,7 +1146,7 @@ def __init__(self, start, control1, control2, end):
self._length_info = {'length': None, 'bpoints': None, 'error': None,
'min_depth': None}

def __hash__(self):
def __hash__(self) -> int:
return hash((self.start, self.control1, self.control2, self.end))

def __repr__(self):
Expand All @@ -1154,7 +1155,7 @@ def __repr__(self):

def __eq__(self, other):
if not isinstance(other, CubicBezier):
return NotImplemented
return False
return self.start == other.start and self.end == other.end \
and self.control1 == other.control1 \
and self.control2 == other.control2
Expand Down Expand Up @@ -1493,8 +1494,12 @@ def __init__(self, start, radius, rotation, large_arc, sweep, end,
# Derive derived parameters
self._parameterize()

def __hash__(self):
return hash((self.start, self.radius, self.rotation, self.large_arc, self.sweep, self.end))
def apoints(self) -> tuple[complex, complex, float, bool, bool, complex]:
"""Analog of the Bezier path method, .bpoints(), for Arc objects."""
return self.start, self.radius, self.rotation, self.large_arc, self.sweep, self.end

def __hash__(self) -> int:
return hash(self.apoints())

def __repr__(self):
params = (self.start, self.radius, self.rotation,
Expand All @@ -1504,7 +1509,7 @@ def __repr__(self):

def __eq__(self, other):
if not isinstance(other, Arc):
return NotImplemented
return False
return self.start == other.start and self.end == other.end \
and self.radius == other.radius \
and self.rotation == other.rotation \
Expand Down Expand Up @@ -2494,8 +2499,13 @@ def __init__(self, *segments, **kw):
if 'tree_element' in kw:
self._tree_element = kw['tree_element']

def __hash__(self):
return hash((tuple(self._segments), self._closed))
def __hash__(self) -> int:

def _pointify(segment):
return segment.apoints() if isinstance(segment, Arc) else segment.bpoints()

pts = tuple(x for segment in self._segments for x in _pointify(segment))
return hash(pts + (self._closed,))

def __getitem__(self, index):
return self._segments[index]
Expand Down Expand Up @@ -2543,7 +2553,7 @@ def __repr__(self):

def __eq__(self, other):
if not isinstance(other, Path):
return NotImplemented
return False
if len(self) != len(other):
return False
for s, o in zip(self._segments, other._segments):
Expand Down
4 changes: 4 additions & 0 deletions test/test_bezier.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@
from svgpathtools.path import bpoints2bezier


seed = 2718
np.random.seed(seed)


class HigherOrderBezier:
def __init__(self, bpoints):
self.bpts = bpoints
Expand Down
50 changes: 28 additions & 22 deletions test/test_generation.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,31 @@
#------------------------------------------------------------------------------
from __future__ import division, absolute_import, print_function
import unittest
import re
from typing import Optional

import numpy as np

from svgpathtools import parse_path


_space_or_comma_pattern = re.compile(r'[,\s]+')


def _assert_d_strings_are_almost_equal(d1: str, d2: str, test_case=unittest.TestCase, msg: Optional[str] = None) -> bool:
"""Slight differences are expected on different platforms, check each part is approx. as expected."""

parts1 = _space_or_comma_pattern.split(d1)
parts2 = _space_or_comma_pattern.split(d2)
test_case.assertEqual(len(parts1), len(parts2), msg=msg)
for p1, p2 in zip(parts1, parts2):
if p1.isalpha():
test_case.assertEqual(p1, p2, msg=msg)
else:
test_case.assertTrue(np.isclose(float(p1), float(p2)), msg=msg)



class TestGeneration(unittest.TestCase):

def test_path_parsing(self):
Expand Down Expand Up @@ -41,32 +63,16 @@ def test_path_parsing(self):
'M 200.0,300.0 Q 400.0,50.0 600.0,300.0 Q 800.0,550.0 1000.0,300.0',
'M -3.4e+38,3.4e+38 L -3.4e-38,3.4e-38',
'M 0.0,0.0 L 50.0,20.0 L 200.0,100.0 L 50.0,20.0',
('M 600.0,350.0 L 650.0,325.0 A 27.9508497187,27.9508497187 -30.0 0,1 700.0,300.0 L 750.0,275.0', # Python 2
'M 600.0,350.0 L 650.0,325.0 A 27.95084971874737,27.95084971874737 -30.0 0,1 700.0,300.0 L 750.0,275.0') # Python 3
'M 600.0,350.0 L 650.0,325.0 A 27.9508497187,27.9508497187 -30.0 0,1 700.0,300.0 L 750.0,275.0'
]

for path, flpath in zip(paths[::-1], float_paths[::-1]):
# Note: Python 3 and Python 2 differ in the number of digits
# truncated when returning a string representation of a float
for path, expected in zip(paths, float_paths):
parsed_path = parse_path(path)
res = parsed_path.d()
if isinstance(flpath, tuple):
option3 = res == flpath[1] # Python 3
flpath = flpath[0]
else:
option3 = False
option1 = res == path
option2 = res == flpath

msg = ('\npath =\n {}\nflpath =\n {}\nparse_path(path).d() =\n {}'
''.format(path, flpath, res))
self.assertTrue(option1 or option2 or option3, msg=msg)

for flpath in float_paths[:-1]:
res = parse_path(flpath).d()
msg = ('\nflpath =\n {}\nparse_path(path).d() =\n {}'
''.format(flpath, res))
self.assertTrue(res == flpath, msg=msg)
msg = ('\npath =\n {}\nexpected =\n {}\nparse_path(path).d() =\n {}'
''.format(path, expected, res))
_assert_d_strings_are_almost_equal(res, expected, self, msg)


def test_normalizing(self):
# Relative paths will be made absolute, subpaths merged if they can,
Expand Down
Loading
Loading