Skip to content

Commit 51ef2a1

Browse files
authored
Merge pull request #234 from mathandy/update-ci
Drop python 2.7 and legacy support; Update CI
2 parents fcb648b + e3ca96a commit 51ef2a1

File tree

9 files changed

+108
-159
lines changed

9 files changed

+108
-159
lines changed

.github/workflows/github-ci-legacy.yml

Lines changed: 0 additions & 34 deletions
This file was deleted.

.github/workflows/github-ci.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ jobs:
1111
continue-on-error: true
1212
strategy:
1313
matrix:
14-
os: [ubuntu-latest, macos-latest, windows-latest]
15-
python-version: [3.7, 3.8, 3.9, "3.10", "3.11"]
14+
os: [ubuntu-24.04, macos-15, windows-2025]
15+
python-version: [3.8, 3.9, "3.10", "3.11", "3.12", "3.13"]
1616
steps:
1717
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
1818
- uses: actions/checkout@v2

.github/workflows/publish-on-pypi-test.yml

Lines changed: 19 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,33 +2,32 @@ name: Publish to TestPyPI
22

33
on:
44
push:
5-
branches:
6-
- master
5+
branches: [master]
6+
pull_request:
7+
branches: [master]
8+
workflow_dispatch:
79

810
jobs:
911
build-n-publish:
1012
name: Build and publish to TestPyPI
11-
runs-on: ubuntu-latest
13+
runs-on: ubuntu-24.04
1214
steps:
13-
- uses: actions/checkout@master
14-
- name: Set up Python 3
15-
uses: actions/setup-python@v1
15+
- uses: actions/checkout@v4
16+
17+
- name: Set up Python
18+
uses: actions/setup-python@v4
1619
with:
17-
python-version: 3
18-
- name: Install pypa/build
19-
run: >-
20-
python -m
21-
pip install
22-
build
23-
--user
20+
python-version: '3.8'
21+
22+
- name: Upgrade pip
23+
run: python -m pip install --upgrade pip
24+
25+
- name: Install build tool
26+
run: python -m pip install build
27+
2428
- name: Build a binary wheel and a source tarball
25-
run: >-
26-
python -m
27-
build
28-
--sdist
29-
--wheel
30-
--outdir dist/
31-
.
29+
run: python -m build --sdist --wheel --outdir dist/
30+
3231
- name: Publish to Test PyPI
3332
uses: pypa/gh-action-pypi-publish@release/v1
3433
with:

.github/workflows/publish-on-pypi.yml

Lines changed: 17 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,35 +8,34 @@ on:
88
jobs:
99
build-n-publish:
1010
name: Build and publish to TestPyPI and PyPI
11-
runs-on: ubuntu-latest
11+
runs-on: ubuntu-24.04
1212
steps:
13-
- uses: actions/checkout@master
14-
- name: Set up Python 3
15-
uses: actions/setup-python@v1
13+
- uses: actions/checkout@v4
14+
15+
- name: Set up Python
16+
uses: actions/setup-python@v4
1617
with:
17-
python-version: 3
18-
- name: Install pypa/build
19-
run: >-
20-
python -m
21-
pip install
22-
build
23-
--user
18+
python-version: '3.8'
19+
20+
- name: Upgrade pip
21+
run: python -m pip install --upgrade pip
22+
23+
- name: Install build tool
24+
run: python -m pip install build
25+
2426
- name: Build a binary wheel and a source tarball
25-
run: >-
26-
python -m
27-
build
28-
--sdist
29-
--wheel
30-
--outdir dist/
31-
.
27+
run: python -m build --sdist --wheel --outdir dist/
28+
3229
- name: Publish to Test PyPI
3330
uses: pypa/gh-action-pypi-publish@release/v1
3431
with:
3532
skip_existing: true
3633
password: ${{ secrets.TESTPYPI_API_TOKEN }}
3734
repository_url: https://test.pypi.org/legacy/
35+
3836
- name: Publish to PyPI
3937
if: startsWith(github.ref, 'refs/tags')
4038
uses: pypa/gh-action-pypi-publish@release/v1
4139
with:
40+
skip_existing: true
4241
password: ${{ secrets.PYPI_API_TOKEN }}

setup.py

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import os
44

55

6-
VERSION = '1.6.1'
6+
VERSION = '1.7.0'
77
AUTHOR_NAME = 'Andy Port'
88
AUTHOR_EMAIL = 'AndyAPort@gmail.com'
99
GITHUB = 'https://github.com/mathandy/svgpathtools'
@@ -27,27 +27,25 @@ def read(relative_path):
2727
author=AUTHOR_NAME,
2828
author_email=AUTHOR_EMAIL,
2929
url=GITHUB,
30-
download_url='{}/releases/download/{}/svgpathtools-{}-py2.py3-none-any.whl'
30+
download_url='{}/releases/download/{}/svgpathtools-{}-py3-none-any.whl'
3131
''.format(GITHUB, VERSION, VERSION),
3232
license='MIT',
3333
install_requires=['numpy', 'svgwrite', 'scipy'],
34+
python_requires='>=3.8',
3435
platforms="OS Independent",
3536
keywords=['svg', 'svg path', 'svg.path', 'bezier', 'parse svg path', 'display svg'],
3637
classifiers=[
3738
"Development Status :: 4 - Beta",
3839
"Intended Audience :: Developers",
3940
"License :: OSI Approved :: MIT License",
4041
"Operating System :: OS Independent",
41-
"Programming Language :: Python :: 2",
4242
"Programming Language :: Python :: 3",
43-
"Programming Language :: Python :: 2.7",
44-
"Programming Language :: Python :: 3.5",
45-
"Programming Language :: Python :: 3.6",
46-
"Programming Language :: Python :: 3.7",
4743
"Programming Language :: Python :: 3.8",
4844
"Programming Language :: Python :: 3.9",
4945
"Programming Language :: Python :: 3.10",
5046
"Programming Language :: Python :: 3.11",
47+
"Programming Language :: Python :: 3.12",
48+
"Programming Language :: Python :: 3.13",
5149
"Topic :: Multimedia :: Graphics :: Editors :: Vector-Based",
5250
"Topic :: Scientific/Engineering",
5351
"Topic :: Scientific/Engineering :: Image Recognition",

svgpathtools/path.py

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
Arc."""
44

55
# External dependencies
6-
from __future__ import division, absolute_import, print_function
6+
from __future__ import annotations
77
import re
88
try:
99
from collections.abc import MutableSequence # noqa
@@ -40,6 +40,7 @@
4040
except NameError:
4141
pass
4242

43+
4344
COMMANDS = set('MmZzLlHhVvCcSsQqTtAa')
4445
UPPERCASE = set('MZLHVCSQTA')
4546

@@ -602,15 +603,15 @@ def __init__(self, start, end):
602603
self.start = start
603604
self.end = end
604605

605-
def __hash__(self):
606+
def __hash__(self) -> int:
606607
return hash((self.start, self.end))
607608

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

611612
def __eq__(self, other):
612613
if not isinstance(other, Line):
613-
return NotImplemented
614+
return False
614615
return self.start == other.start and self.end == other.end
615616

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

877-
def __hash__(self):
878+
def __hash__(self) -> int:
878879
return hash((self.start, self.control, self.end))
879880

880881
def __repr__(self):
@@ -883,7 +884,7 @@ def __repr__(self):
883884

884885
def __eq__(self, other):
885886
if not isinstance(other, QuadraticBezier):
886-
return NotImplemented
887+
return False
887888
return self.start == other.start and self.end == other.end \
888889
and self.control == other.control
889890

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

1148-
def __hash__(self):
1149+
def __hash__(self) -> int:
11491150
return hash((self.start, self.control1, self.control2, self.end))
11501151

11511152
def __repr__(self):
@@ -1154,7 +1155,7 @@ def __repr__(self):
11541155

11551156
def __eq__(self, other):
11561157
if not isinstance(other, CubicBezier):
1157-
return NotImplemented
1158+
return False
11581159
return self.start == other.start and self.end == other.end \
11591160
and self.control1 == other.control1 \
11601161
and self.control2 == other.control2
@@ -1493,8 +1494,12 @@ def __init__(self, start, radius, rotation, large_arc, sweep, end,
14931494
# Derive derived parameters
14941495
self._parameterize()
14951496

1496-
def __hash__(self):
1497-
return hash((self.start, self.radius, self.rotation, self.large_arc, self.sweep, self.end))
1497+
def apoints(self) -> tuple[complex, complex, float, bool, bool, complex]:
1498+
"""Analog of the Bezier path method, .bpoints(), for Arc objects."""
1499+
return self.start, self.radius, self.rotation, self.large_arc, self.sweep, self.end
1500+
1501+
def __hash__(self) -> int:
1502+
return hash(self.apoints())
14981503

14991504
def __repr__(self):
15001505
params = (self.start, self.radius, self.rotation,
@@ -1504,7 +1509,7 @@ def __repr__(self):
15041509

15051510
def __eq__(self, other):
15061511
if not isinstance(other, Arc):
1507-
return NotImplemented
1512+
return False
15081513
return self.start == other.start and self.end == other.end \
15091514
and self.radius == other.radius \
15101515
and self.rotation == other.rotation \
@@ -2494,8 +2499,13 @@ def __init__(self, *segments, **kw):
24942499
if 'tree_element' in kw:
24952500
self._tree_element = kw['tree_element']
24962501

2497-
def __hash__(self):
2498-
return hash((tuple(self._segments), self._closed))
2502+
def __hash__(self) -> int:
2503+
2504+
def _pointify(segment):
2505+
return segment.apoints() if isinstance(segment, Arc) else segment.bpoints()
2506+
2507+
pts = tuple(x for segment in self._segments for x in _pointify(segment))
2508+
return hash(pts + (self._closed,))
24992509

25002510
def __getitem__(self, index):
25012511
return self._segments[index]
@@ -2543,7 +2553,7 @@ def __repr__(self):
25432553

25442554
def __eq__(self, other):
25452555
if not isinstance(other, Path):
2546-
return NotImplemented
2556+
return False
25472557
if len(self) != len(other):
25482558
return False
25492559
for s, o in zip(self._segments, other._segments):

test/test_bezier.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@
55
from svgpathtools.path import bpoints2bezier
66

77

8+
seed = 2718
9+
np.random.seed(seed)
10+
11+
812
class HigherOrderBezier:
913
def __init__(self, bpoints):
1014
self.bpts = bpoints

test/test_generation.py

Lines changed: 28 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,31 @@
22
#------------------------------------------------------------------------------
33
from __future__ import division, absolute_import, print_function
44
import unittest
5+
import re
6+
from typing import Optional
7+
8+
import numpy as np
9+
510
from svgpathtools import parse_path
611

712

13+
_space_or_comma_pattern = re.compile(r'[,\s]+')
14+
15+
16+
def _assert_d_strings_are_almost_equal(d1: str, d2: str, test_case=unittest.TestCase, msg: Optional[str] = None) -> bool:
17+
"""Slight differences are expected on different platforms, check each part is approx. as expected."""
18+
19+
parts1 = _space_or_comma_pattern.split(d1)
20+
parts2 = _space_or_comma_pattern.split(d2)
21+
test_case.assertEqual(len(parts1), len(parts2), msg=msg)
22+
for p1, p2 in zip(parts1, parts2):
23+
if p1.isalpha():
24+
test_case.assertEqual(p1, p2, msg=msg)
25+
else:
26+
test_case.assertTrue(np.isclose(float(p1), float(p2)), msg=msg)
27+
28+
29+
830
class TestGeneration(unittest.TestCase):
931

1032
def test_path_parsing(self):
@@ -41,32 +63,16 @@ def test_path_parsing(self):
4163
'M 200.0,300.0 Q 400.0,50.0 600.0,300.0 Q 800.0,550.0 1000.0,300.0',
4264
'M -3.4e+38,3.4e+38 L -3.4e-38,3.4e-38',
4365
'M 0.0,0.0 L 50.0,20.0 L 200.0,100.0 L 50.0,20.0',
44-
('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
45-
'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
66+
'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'
4667
]
4768

48-
for path, flpath in zip(paths[::-1], float_paths[::-1]):
49-
# Note: Python 3 and Python 2 differ in the number of digits
50-
# truncated when returning a string representation of a float
69+
for path, expected in zip(paths, float_paths):
5170
parsed_path = parse_path(path)
5271
res = parsed_path.d()
53-
if isinstance(flpath, tuple):
54-
option3 = res == flpath[1] # Python 3
55-
flpath = flpath[0]
56-
else:
57-
option3 = False
58-
option1 = res == path
59-
option2 = res == flpath
60-
61-
msg = ('\npath =\n {}\nflpath =\n {}\nparse_path(path).d() =\n {}'
62-
''.format(path, flpath, res))
63-
self.assertTrue(option1 or option2 or option3, msg=msg)
64-
65-
for flpath in float_paths[:-1]:
66-
res = parse_path(flpath).d()
67-
msg = ('\nflpath =\n {}\nparse_path(path).d() =\n {}'
68-
''.format(flpath, res))
69-
self.assertTrue(res == flpath, msg=msg)
72+
msg = ('\npath =\n {}\nexpected =\n {}\nparse_path(path).d() =\n {}'
73+
''.format(path, expected, res))
74+
_assert_d_strings_are_almost_equal(res, expected, self, msg)
75+
7076

7177
def test_normalizing(self):
7278
# Relative paths will be made absolute, subpaths merged if they can,

0 commit comments

Comments
 (0)