Skip to content

Commit b3f687d

Browse files
committed
Apply ruff, add Makefile, revamp tox, update hooks
1 parent 868183b commit b3f687d

File tree

13 files changed

+600
-133
lines changed

13 files changed

+600
-133
lines changed

.pre-commit-config.yaml

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,22 @@
11
# See https://pre-commit.com for more information
22
# See https://pre-commit.com/hooks.html for more hooks
3+
34
repos:
4-
- repo: https://github.com/pre-commit/pre-commit-hooks
5-
rev: v6.0.0
6-
hooks:
7-
- id: trailing-whitespace
8-
- repo: https://github.com/asottile/pyupgrade
9-
rev: v3.20.0
10-
hooks:
11-
- id: pyupgrade
5+
- repo: https://github.com/pre-commit/pre-commit-hooks
6+
rev: v6.0.0
7+
hooks:
8+
- id: trailing-whitespace
9+
10+
- repo: https://github.com/asottile/pyupgrade
11+
rev: v3.20.0
12+
hooks:
13+
- id: pyupgrade
14+
15+
- repo: https://github.com/astral-sh/ruff-pre-commit
16+
rev: v0.13.0
17+
hooks:
18+
- id: ruff-format
19+
types_or: ["python"]
20+
- id: ruff
21+
types_or: ["python"]
22+
args: ["--fix"]

Makefile

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
.PHONY: help
2+
3+
# Default to 'help' when no target is given
4+
DEFAULT_GOAL := help
5+
.DEFAULT_GOAL := $(DEFAULT_GOAL)
6+
7+
help: ## Display this help screen
8+
@echo "Usage: make [target] ..."
9+
@echo
10+
@echo "Available targets:"
11+
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
12+
13+
.PHONY: install
14+
install: ## Install dependencies and pre-commit hooks.
15+
uv pip install -e ".[dev]"
16+
uv run pre-commit install
17+
18+
.PHONY: uninstall
19+
uninstall: ## Uninstall the package from the local virtual environment.
20+
uv pip uninstall svglib -y
21+
22+
.PHONY: test
23+
test: ## Run the test suite with coverage.
24+
uv run pytest -s -v -rs --cov=src/svglib --cov-report=term-missing --cov-report=html tests
25+
uv run coverage html
26+
27+
.PHONY: lint
28+
lint: ## Run the formatter and linter.
29+
uv run ruff format .
30+
uv run ruff check --fix .
31+
32+
.PHONY: hooks
33+
hooks: ## Run all pre-commit hooks.
34+
uv run pre-commit run --all-files
35+
36+
.PHONY: all
37+
all: install lint hooks test ## Run all checks (install, lint, test).
38+
39+
.PHONY: dist
40+
dist: ## Build the package for distribution.
41+
uv build
42+
43+
.PHONY: test-dist
44+
test-dist: ## Build and test the distribution wheel.
45+
@./scripts/test-dist.sh
46+
47+
.PHONY: clean
48+
clean: ## Remove temporary build files.
49+
@find . -type f -name "*~" -delete
50+
@find . -type f -name "*.py[co]" -delete
51+
@find . -type f -name ".coverage" -delete
52+
@find . -type f -name "coverage.xml" -delete
53+
@find . -type d -name "__pycache__" -delete
54+
@find . -type d -name ".mypy_cache" -delete
55+
@find . -type d -name ".pytest_cache" -delete
56+
@find . -type d -name ".rust_cache" -delete
57+
@find . -type d -name "*.egg-info" -exec rm -rf {} +
58+
@rm -rf .pytest_cache .ruff_cache .mypy_cache .tox
59+
60+
.PHONY: testclean
61+
testclean: ## Delete PDF files generated by the test suite.
62+
@find tests/samples -type f -name "*-svglib.pdf" -delete
63+
64+
.PHONY: distclean
65+
distclean: clean ## Delete all temporary files and the virtual environment.
66+
@rm -rf .venv

pyproject.toml

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,11 +60,30 @@ svg2pdf = "svglib:main"
6060
requires = ["hatchling"]
6161
build-backend = "hatchling.build"
6262

63-
[tool.uv]
64-
dev-dependencies = [
63+
[project.optional-dependencies]
64+
dev = [
65+
"pre-commit>=4.3.0",
6566
"pytest>=8.3.5",
67+
"pytest-cov>=7.0.0",
6668
"pytest-runner>=6.0.1",
69+
"ruff>=0.13.0",
70+
"tox>=4.0.0",
6771
]
6872

69-
[tool.flake8]
70-
max-line-length = "99"
73+
[tool.ruff]
74+
line-length = 88
75+
76+
[tool.ruff.lint]
77+
select = [
78+
"E", # pycodestyle errors (essential syntax and style issues)
79+
"F", # Pyflakes messages (e.g., undefined names, unused imports)
80+
"I", # isort rules for enforcing import order
81+
]
82+
83+
[tool.ruff.format]
84+
quote-style = "double"
85+
86+
[dependency-groups]
87+
dev = [
88+
"tox>=4.30.2",
89+
]

requirements.txt

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

src/svglib/__init__.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import sys
88
import textwrap
99
from datetime import datetime
10-
from os.path import dirname, basename, splitext, exists
10+
from os.path import basename, dirname, exists, splitext
1111

1212
from reportlab.graphics import renderPDF
1313

@@ -88,9 +88,7 @@ def main():
8888
https://github.com/deeplook/svglib
8989
9090
Copyleft by {author}, 2008-{copyleft_year} ({license}):
91-
https://www.gnu.org/licenses/lgpl-3.0.html""".format(
92-
**args
93-
)
91+
https://www.gnu.org/licenses/lgpl-3.0.html""".format(**args)
9492
)
9593
p = argparse.ArgumentParser(
9694
description=desc,

src/svglib/fonts.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
from reportlab.pdfbase.pdfmetrics import registerFont
1010
from reportlab.pdfbase.ttfonts import TTFError, TTFont
1111

12-
1312
STANDARD_FONT_NAMES = (
1413
"Times-Roman",
1514
"Times-Italic",
@@ -42,8 +41,8 @@ def __init__(self):
4241
"""
4342
The map has the form:
4443
'internal_name': {
45-
'svg_family': 'family_name', 'svg_weight': 'font-weight', 'svg_style': 'font-style',
46-
'rlgFont': 'rlgFontName'
44+
'svg_family': 'family_name', 'svg_weight': 'font-weight',
45+
'svg_style': 'font-style', 'rlgFont': 'rlgFontName'
4746
}
4847
for faster searching we use internal keys for finding the matching font
4948
"""
@@ -63,7 +62,7 @@ def build_internal_name(family, weight="normal", style="normal"):
6362
if weight != "normal" or style != "normal":
6463
result_name += "-"
6564
if weight != "normal":
66-
if type(weight) is int:
65+
if isinstance(weight, int):
6766
result_name += f"{weight}"
6867
else:
6968
result_name += weight.lower().capitalize()

src/svglib/svglib.py

Lines changed: 57 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
#!/usr/bin/env python
21
"""A library for reading and converting SVG.
32
43
This is a converter from SVG to RLG (ReportLab Graphics) drawings.
@@ -24,14 +23,10 @@
2423
import shlex
2524
import shutil
2625
from collections import defaultdict, namedtuple
27-
from io import BytesIO
2826
from importlib.metadata import version
27+
from io import BytesIO
2928

3029
from PIL import Image as PILImage
31-
32-
from reportlab.pdfbase.pdfmetrics import stringWidth
33-
from reportlab.pdfgen.canvas import FILL_EVEN_ODD, FILL_NON_ZERO
34-
from reportlab.pdfgen.pdfimages import PDFImage
3530
from reportlab.graphics.shapes import (
3631
_CLOSEPATH,
3732
Circle,
@@ -41,43 +36,48 @@
4136
Image,
4237
Line,
4338
Path,
44-
PolyLine,
4539
Polygon,
40+
PolyLine,
4641
Rect,
4742
SolidShape,
4843
String,
4944
)
5045
from reportlab.lib import colors
5146
from reportlab.lib.units import pica, toLength
47+
from reportlab.pdfbase.pdfmetrics import stringWidth
48+
from reportlab.pdfgen.canvas import FILL_EVEN_ODD, FILL_NON_ZERO
49+
from reportlab.pdfgen.pdfimages import PDFImage
5250

5351
try:
5452
from reportlab.graphics.transform import mmult
5553
except ImportError:
5654
# Before Reportlab 3.5.61
5755
from reportlab.graphics.shapes import mmult
5856

59-
from lxml import etree
6057
import cssselect2
6158
import tinycss2
62-
63-
from .utils import (
64-
bezier_arc_from_end_points,
65-
convert_quadratic_to_cubic_path,
66-
normalise_svg_path,
67-
)
59+
from lxml import etree
6860

6961
from .fonts import (
70-
get_global_font_map,
7162
DEFAULT_FONT_NAME,
72-
DEFAULT_FONT_WEIGHT,
73-
DEFAULT_FONT_STYLE,
7463
DEFAULT_FONT_SIZE,
64+
DEFAULT_FONT_STYLE,
65+
DEFAULT_FONT_WEIGHT,
66+
get_global_font_map,
67+
)
68+
from .fonts import (
69+
find_font as _fonts_find_font,
7570
)
7671

77-
# To keep backward compatibility, since those functions where previously part of the svglib module
72+
# To keep backward compatibility, since those functions where previously part of
73+
# the svglib module
7874
from .fonts import (
7975
register_font as _fonts_register_font,
80-
find_font as _fonts_find_font,
76+
)
77+
from .utils import (
78+
bezier_arc_from_end_points,
79+
convert_quadratic_to_cubic_path,
80+
normalise_svg_path,
8181
)
8282

8383

@@ -625,7 +625,8 @@ def renderNode(self, node, parent=None):
625625
# and simplify further analyses of generated document
626626
item.setProperties({"svgid": nid})
627627
# labels are used in inkscape to name specific groups as layers
628-
# preserving them simplify extraction of feature from the generated document
628+
# preserving them simplify extraction of feature from the generated
629+
# document
629630
label_attrs = [v for k, v in node.attrib.items() if "label" in k]
630631
if len(label_attrs) == 1:
631632
(label,) = label_attrs
@@ -905,7 +906,8 @@ def renderUse(self, node, group=None, clipping=None):
905906
if clipping:
906907
group.add(clipping)
907908
if len(node.getchildren()) == 0:
908-
# Append a copy of the referenced node as the <use> child (if not already done)
909+
# Append a copy of the referenced node as the <use> child (if not
910+
# already done)
909911
node.append(copy.deepcopy(item))
910912
self.renderNode(list(node.iter_children())[-1], parent=group)
911913
self.apply_node_attr_to_group(node, group)
@@ -1089,7 +1091,8 @@ def convertText(self, node):
10891091

10901092
frag_lengths.append(stringWidth(text, ff, fs))
10911093

1092-
# When x, y, dx, or dy is a list, we calculate position for each char of text.
1094+
# When x, y, dx, or dy is a list, we calculate position for each char of
1095+
# text.
10931096
if any(isinstance(val, list) for val in (x1, y1, dx, dy)):
10941097
if has_x:
10951098
xlist = x1 if isinstance(x1, list) else [x1]
@@ -1225,9 +1228,12 @@ def convertPath(self, node):
12251228
x0, y0 = points[-2:]
12261229
x1, y1, xn, yn = nums
12271230
last_quadratic_cp = (x1, y1)
1228-
(x0, y0), (x1, y1), (x2, y2), (xn, yn) = (
1229-
convert_quadratic_to_cubic_path((x0, y0), (x1, y1), (xn, yn))
1230-
)
1231+
(
1232+
(x0, y0),
1233+
(x1, y1),
1234+
(x2, y2),
1235+
(xn, yn),
1236+
) = convert_quadratic_to_cubic_path((x0, y0), (x1, y1), (xn, yn))
12311237
path.curveTo(x1, y1, x2, y2, xn, yn)
12321238
elif op == "T":
12331239
if last_quadratic_cp is not None:
@@ -1238,9 +1244,12 @@ def convertPath(self, node):
12381244
xi, yi = x0 + (x0 - xp), y0 + (y0 - yp)
12391245
last_quadratic_cp = (xi, yi)
12401246
xn, yn = nums
1241-
(x0, y0), (x1, y1), (x2, y2), (xn, yn) = (
1242-
convert_quadratic_to_cubic_path((x0, y0), (xi, yi), (xn, yn))
1243-
)
1247+
(
1248+
(x0, y0),
1249+
(x1, y1),
1250+
(x2, y2),
1251+
(xn, yn),
1252+
) = convert_quadratic_to_cubic_path((x0, y0), (xi, yi), (xn, yn))
12441253
path.curveTo(x1, y1, x2, y2, xn, yn)
12451254

12461255
# quadratic bezier, relative
@@ -1249,9 +1258,12 @@ def convertPath(self, node):
12491258
x1, y1, xn, yn = nums
12501259
x1, y1, xn, yn = x0 + x1, y0 + y1, x0 + xn, y0 + yn
12511260
last_quadratic_cp = (x1, y1)
1252-
(x0, y0), (x1, y1), (x2, y2), (xn, yn) = (
1253-
convert_quadratic_to_cubic_path((x0, y0), (x1, y1), (xn, yn))
1254-
)
1261+
(
1262+
(x0, y0),
1263+
(x1, y1),
1264+
(x2, y2),
1265+
(xn, yn),
1266+
) = convert_quadratic_to_cubic_path((x0, y0), (x1, y1), (xn, yn))
12551267
path.curveTo(x1, y1, x2, y2, xn, yn)
12561268
elif op == "t":
12571269
if last_quadratic_cp is not None:
@@ -1263,9 +1275,12 @@ def convertPath(self, node):
12631275
xn, yn = x0 + xn, y0 + yn
12641276
xi, yi = x0 + (x0 - xp), y0 + (y0 - yp)
12651277
last_quadratic_cp = (xi, yi)
1266-
(x0, y0), (x1, y1), (x2, y2), (xn, yn) = (
1267-
convert_quadratic_to_cubic_path((x0, y0), (xi, yi), (xn, yn))
1268-
)
1278+
(
1279+
(x0, y0),
1280+
(x1, y1),
1281+
(x2, y2),
1282+
(xn, yn),
1283+
) = convert_quadratic_to_cubic_path((x0, y0), (xi, yi), (xn, yn))
12691284
path.curveTo(x1, y1, x2, y2, xn, yn)
12701285

12711286
# elliptical arc
@@ -1342,7 +1357,8 @@ def applyTransformOnGroup(self, transform, group):
13421357
group.scale(*values)
13431358
elif op == "translate":
13441359
if isinstance(values, (int, float)):
1345-
# From the SVG spec: If <ty> is not provided, it is assumed to be zero.
1360+
# From the SVG spec: If <ty> is not provided, it is assumed to
1361+
# be zero.
13461362
values = values, 0
13471363
group.translate(*values)
13481364
elif op == "rotate":
@@ -1441,11 +1457,11 @@ def applyStyleOnShape(self, shape, node, only_explicit=False):
14411457
shape.fillColor.alpha = shape.fillOpacity
14421458
if getattr(shape, "strokeWidth", None) == 0:
14431459
# Quoting from the PDF 1.7 spec:
1444-
# A line width of 0 denotes the thinnest line that can be rendered at device
1445-
# resolution: 1 device pixel wide. However, some devices cannot reproduce 1-pixel
1446-
# lines, and on high-resolution devices, they are nearly invisible. Since the
1447-
# results of rendering such zero-width lines are device-dependent, their use
1448-
# is not recommended.
1460+
# A line width of 0 denotes the thinnest line that can be rendered at
1461+
# device resolution: 1 device pixel wide. However, some devices cannot
1462+
# reproduce 1-pixel lines, and on high-resolution devices, they are
1463+
# nearly invisible. Since the results of rendering such zero-width
1464+
# lines are device-dependent, their use is not recommended.
14491465
shape.strokeColor = None
14501466

14511467

@@ -1584,8 +1600,8 @@ def monkeypatch_reportlab():
15841600
ReportLab always use 'Even-Odd' filling mode for paths, this patch forces
15851601
RL to honor the path fill rule mode (possibly 'Non-Zero Winding') instead.
15861602
"""
1587-
from reportlab.pdfgen.canvas import Canvas
15881603
from reportlab.graphics import shapes
1604+
from reportlab.pdfgen.canvas import Canvas
15891605

15901606
original_renderPath = shapes._renderPath
15911607

0 commit comments

Comments
 (0)