svg-ultralight 0.44.0__tar.gz → 0.45.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of svg-ultralight might be problematic. Click here for more details.

Files changed (61) hide show
  1. {svg_ultralight-0.44.0 → svg_ultralight-0.45.0}/.pre-commit-config.yaml +5 -6
  2. {svg_ultralight-0.44.0/src/svg_ultralight.egg-info → svg_ultralight-0.45.0}/PKG-INFO +1 -1
  3. {svg_ultralight-0.44.0 → svg_ultralight-0.45.0}/pyproject.toml +5 -2
  4. svg_ultralight-0.45.0/src/svg_ultralight/attrib_hints.py +14 -0
  5. {svg_ultralight-0.44.0 → svg_ultralight-0.45.0}/src/svg_ultralight/bounding_boxes/bound_helpers.py +5 -4
  6. {svg_ultralight-0.44.0 → svg_ultralight-0.45.0}/src/svg_ultralight/bounding_boxes/padded_text_initializers.py +16 -7
  7. {svg_ultralight-0.44.0 → svg_ultralight-0.45.0}/src/svg_ultralight/constructors/new_element.py +6 -4
  8. {svg_ultralight-0.44.0 → svg_ultralight-0.45.0}/src/svg_ultralight/font_tools/comp_results.py +5 -5
  9. {svg_ultralight-0.44.0 → svg_ultralight-0.45.0}/src/svg_ultralight/font_tools/font_info.py +3 -1
  10. {svg_ultralight-0.44.0 → svg_ultralight-0.45.0}/src/svg_ultralight/main.py +8 -2
  11. {svg_ultralight-0.44.0 → svg_ultralight-0.45.0}/src/svg_ultralight/root_elements.py +8 -2
  12. {svg_ultralight-0.44.0 → svg_ultralight-0.45.0}/src/svg_ultralight/string_conversion.py +8 -4
  13. {svg_ultralight-0.44.0 → svg_ultralight-0.45.0/src/svg_ultralight.egg-info}/PKG-INFO +1 -1
  14. {svg_ultralight-0.44.0 → svg_ultralight-0.45.0}/src/svg_ultralight.egg-info/SOURCES.txt +2 -1
  15. {svg_ultralight-0.44.0 → svg_ultralight-0.45.0}/tests/conftest.py +7 -2
  16. {svg_ultralight-0.44.0 → svg_ultralight-0.45.0}/tests/test_bounding.py +14 -13
  17. {svg_ultralight-0.44.0 → svg_ultralight-0.45.0}/tests/test_layout.py +16 -19
  18. {svg_ultralight-0.44.0 → svg_ultralight-0.45.0}/tests/test_matrices.py +3 -2
  19. {svg_ultralight-0.44.0 → svg_ultralight-0.45.0}/tests/test_metadata.py +4 -2
  20. {svg_ultralight-0.44.0 → svg_ultralight-0.45.0}/tests/test_padded_text_initializers.py +3 -2
  21. {svg_ultralight-0.44.0 → svg_ultralight-0.45.0}/tests/test_queries.py +2 -7
  22. {svg_ultralight-0.44.0 → svg_ultralight-0.45.0}/tests/test_root_elements.py +8 -13
  23. {svg_ultralight-0.44.0 → svg_ultralight-0.45.0}/tests/test_svg_ultralight.py +39 -26
  24. {svg_ultralight-0.44.0 → svg_ultralight-0.45.0}/.gitignore +0 -0
  25. {svg_ultralight-0.44.0 → svg_ultralight-0.45.0}/README.md +0 -0
  26. {svg_ultralight-0.44.0 → svg_ultralight-0.45.0}/dev-requirements.txt +0 -0
  27. /svg_ultralight-0.44.0/experiments/encode_fonts3.py → /svg_ultralight-0.45.0/experiments/encode_fonts3.txt +0 -0
  28. {svg_ultralight-0.44.0 → svg_ultralight-0.45.0}/experiments/font_css.py +0 -0
  29. {svg_ultralight-0.44.0 → svg_ultralight-0.45.0}/setup.cfg +0 -0
  30. {svg_ultralight-0.44.0 → svg_ultralight-0.45.0}/src/svg_ultralight/__init__.py +0 -0
  31. {svg_ultralight-0.44.0 → svg_ultralight-0.45.0}/src/svg_ultralight/animate.py +0 -0
  32. {svg_ultralight-0.44.0 → svg_ultralight-0.45.0}/src/svg_ultralight/bounding_boxes/__init__.py +0 -0
  33. {svg_ultralight-0.44.0 → svg_ultralight-0.45.0}/src/svg_ultralight/bounding_boxes/supports_bounds.py +0 -0
  34. {svg_ultralight-0.44.0 → svg_ultralight-0.45.0}/src/svg_ultralight/bounding_boxes/type_bound_collection.py +0 -0
  35. {svg_ultralight-0.44.0 → svg_ultralight-0.45.0}/src/svg_ultralight/bounding_boxes/type_bound_element.py +0 -0
  36. {svg_ultralight-0.44.0 → svg_ultralight-0.45.0}/src/svg_ultralight/bounding_boxes/type_bounding_box.py +0 -0
  37. {svg_ultralight-0.44.0 → svg_ultralight-0.45.0}/src/svg_ultralight/bounding_boxes/type_padded_text.py +0 -0
  38. {svg_ultralight-0.44.0 → svg_ultralight-0.45.0}/src/svg_ultralight/constructors/__init__.py +0 -0
  39. {svg_ultralight-0.44.0 → svg_ultralight-0.45.0}/src/svg_ultralight/font_tools/__init__.py +0 -0
  40. {svg_ultralight-0.44.0 → svg_ultralight-0.45.0}/src/svg_ultralight/font_tools/globs.py +0 -0
  41. {svg_ultralight-0.44.0 → svg_ultralight-0.45.0}/src/svg_ultralight/image_ops.py +0 -0
  42. {svg_ultralight-0.44.0 → svg_ultralight-0.45.0}/src/svg_ultralight/inkscape.py +0 -0
  43. {svg_ultralight-0.44.0 → svg_ultralight-0.45.0}/src/svg_ultralight/layout.py +0 -0
  44. {svg_ultralight-0.44.0 → svg_ultralight-0.45.0}/src/svg_ultralight/metadata.py +0 -0
  45. {svg_ultralight-0.44.0 → svg_ultralight-0.45.0}/src/svg_ultralight/nsmap.py +0 -0
  46. {svg_ultralight-0.44.0 → svg_ultralight-0.45.0}/src/svg_ultralight/py.typed +0 -0
  47. {svg_ultralight-0.44.0 → svg_ultralight-0.45.0}/src/svg_ultralight/query.py +0 -0
  48. {svg_ultralight-0.44.0 → svg_ultralight-0.45.0}/src/svg_ultralight/strings/__init__.py +0 -0
  49. {svg_ultralight-0.44.0 → svg_ultralight-0.45.0}/src/svg_ultralight/strings/svg_strings.py +0 -0
  50. {svg_ultralight-0.44.0 → svg_ultralight-0.45.0}/src/svg_ultralight/transformations.py +0 -0
  51. {svg_ultralight-0.44.0 → svg_ultralight-0.45.0}/src/svg_ultralight/unit_conversion.py +0 -0
  52. {svg_ultralight-0.44.0 → svg_ultralight-0.45.0}/src/svg_ultralight.egg-info/dependency_links.txt +0 -0
  53. {svg_ultralight-0.44.0 → svg_ultralight-0.45.0}/src/svg_ultralight.egg-info/requires.txt +0 -0
  54. {svg_ultralight-0.44.0 → svg_ultralight-0.45.0}/src/svg_ultralight.egg-info/top_level.txt +0 -0
  55. {svg_ultralight-0.44.0 → svg_ultralight-0.45.0}/tests/__init__.py +0 -0
  56. {svg_ultralight-0.44.0 → svg_ultralight-0.45.0}/tests/resources/arrow.svg +0 -0
  57. {svg_ultralight-0.44.0 → svg_ultralight-0.45.0}/tests/test_inkscape.py +0 -0
  58. {svg_ultralight-0.44.0 → svg_ultralight-0.45.0}/tests/test_new_element.py +0 -0
  59. {svg_ultralight-0.44.0 → svg_ultralight-0.45.0}/tests/test_padding.py +0 -0
  60. {svg_ultralight-0.44.0 → svg_ultralight-0.45.0}/tests/test_string_conversion.py +0 -0
  61. {svg_ultralight-0.44.0 → svg_ultralight-0.45.0}/tox.ini +0 -0
@@ -1,7 +1,7 @@
1
1
  ci:
2
2
  skip: [pyright]
3
3
 
4
- exclude: "experiments"
4
+ exclude: "tests|experiments"
5
5
 
6
6
  repos:
7
7
 
@@ -42,7 +42,7 @@ repos:
42
42
  # files: .pre-commit-config.yaml
43
43
 
44
44
  - repo: https://github.com/pre-commit/mirrors-mypy
45
- rev: v1.15.0
45
+ rev: v1.16.1
46
46
  hooks:
47
47
  - id: mypy
48
48
  name: mypy
@@ -89,14 +89,13 @@ repos:
89
89
  # S101 Use of `assert` detected
90
90
  # S603 `subprocess` call: check for execution of untrusted input
91
91
  # PLR2004 Magic value used in comparison
92
- # S320 Using `lxml` to parse untrusted data is known to be vulnerable to XML attacks
93
92
  # S301 don't use pickle
94
93
  # B028 wants explicit stacklevel on warn
95
94
  # BLE001 Use of `except Exception:` detected
96
95
  # ANN401 Any type disallowed
97
96
  # FLY002 Consider f-string instead of string join
98
97
  # S311 Standard pseudo-random generator used
99
- rev: 'v0.11.11'
98
+ rev: 'v0.12.2'
100
99
  hooks:
101
100
  - id: ruff
102
101
  name: "ruff-lint"
@@ -104,7 +103,7 @@ repos:
104
103
  args:
105
104
  - --target-version=py39
106
105
  - --select=ALL
107
- - --ignore=ANN201,ANN202,B905,COM812,D203,D213,I001,ISC003,N802,N806,PGH003,PLR0913,PTH108,S101,S603,PLR2004,S320,S301,B028,BLE001,ANN401,FLY002,SIM108,S311
106
+ - --ignore=ANN201,ANN202,B905,COM812,D203,D213,I001,ISC003,N802,N806,PGH003,PLR0913,PTH108,S101,S603,PLR2004,S301,B028,BLE001,ANN401,FLY002,SIM108,S311
108
107
  - --fix
109
108
  - --fixable=RUF022
110
109
  - id: ruff
@@ -114,6 +113,6 @@ repos:
114
113
 
115
114
  # reads pyproject.toml for additional config
116
115
  - repo: https://github.com/RobertCraigie/pyright-python
117
- rev: v1.1.398
116
+ rev: v1.1.403
118
117
  hooks:
119
118
  - id: pyright
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: svg-ultralight
3
- Version: 0.44.0
3
+ Version: 0.45.0
4
4
  Summary: a sensible way to create svg files with Python
5
5
  Author-email: Shay Hill <shay_public@hotmail.com>
6
6
  License: MIT
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "svg-ultralight"
3
- version = "0.44.0"
3
+ version = "0.45.0"
4
4
  description = "a sensible way to create svg files with Python"
5
5
  authors = [{ name = "Shay Hill", email = "shay_public@hotmail.com" }]
6
6
  license = { text = "MIT" }
@@ -48,9 +48,12 @@ legacy_tox_ini = """
48
48
  [tool.ruff.lint.pydocstyle]
49
49
  convention = "pep257"
50
50
 
51
+ [tool.ruff.lint.per-file-ignores]
52
+ "tests/*.py" = ["S101", "D", "F401"] # Ignore assertions, docstrings, unused imports in test files
53
+
51
54
  [tool.commitizen]
52
55
  name = "cz_conventional_commits"
53
- version = "0.44.0"
56
+ version = "0.45.0"
54
57
  tag_format = "$version"
55
58
  version_files = ["pyproject.toml:^version"]
56
59
  annotated_tag = true
@@ -0,0 +1,14 @@
1
+ """Type hints for pass-through arguments to lxml constructors.
2
+
3
+ :author: Shay Hill
4
+ :created: 2025-07-09
5
+ """
6
+
7
+ from collections.abc import Mapping
8
+ from typing import Union
9
+
10
+ # Types svg_ultralight can format to pass through to lxml constructors.
11
+ ElemAttrib = Union[str, float, None]
12
+
13
+ # Type for an optional dictionary of element attributes.
14
+ OptionalElemAttribMapping = Union[Mapping[str, ElemAttrib], None]
@@ -8,6 +8,7 @@ from __future__ import annotations
8
8
 
9
9
  from typing import TYPE_CHECKING
10
10
 
11
+ from lxml import etree
11
12
  from lxml.etree import _Element as EtreeElement # pyright: ignore[reportPrivateUsage]
12
13
 
13
14
  from svg_ultralight.bounding_boxes.supports_bounds import SupportsBounds
@@ -19,14 +20,14 @@ from svg_ultralight.constructors import new_element
19
20
  if TYPE_CHECKING:
20
21
  import os
21
22
 
23
+ from svg_ultralight.attrib_hints import ElemAttrib
22
24
  from svg_ultralight.bounding_boxes.supports_bounds import SupportsBounds
23
- from lxml import etree
24
25
 
25
26
  _Matrix = tuple[float, float, float, float, float, float]
26
27
 
27
28
 
28
29
  def new_element_union(
29
- *elems: EtreeElement | SupportsBounds, **attributes: float | str
30
+ *elems: EtreeElement | SupportsBounds, **attributes: ElemAttrib
30
31
  ) -> EtreeElement:
31
32
  """Get the union of any elements found in the given arguments.
32
33
 
@@ -186,13 +187,13 @@ def _get_view_box(elem: EtreeElement) -> tuple[float, float, float, float]:
186
187
  return x, y, width, height
187
188
 
188
189
 
189
- def parse_bound_element(svg_fil: str | os.PathLike[str]) -> BoundElement:
190
+ def parse_bound_element(svg_file: str | os.PathLike[str]) -> BoundElement:
190
191
  """Import an element as a BoundElement.
191
192
 
192
193
  :param elem: the element to import.
193
194
  :return: a BoundElement instance.
194
195
  """
195
- tree = etree.parse(svg_fil)
196
+ tree = etree.parse(svg_file)
196
197
  root = tree.getroot()
197
198
  elem = new_element("g")
198
199
  elem.extend(list(root))
@@ -27,10 +27,7 @@ from svg_ultralight.font_tools.font_info import (
27
27
  )
28
28
  from svg_ultralight.font_tools.globs import DEFAULT_FONT_SIZE
29
29
  from svg_ultralight.query import get_bounding_boxes
30
- from svg_ultralight.string_conversion import (
31
- format_attr_dict,
32
- format_number,
33
- )
30
+ from svg_ultralight.string_conversion import format_attr_dict, format_number
34
31
 
35
32
  if TYPE_CHECKING:
36
33
  import os
@@ -39,6 +36,8 @@ if TYPE_CHECKING:
39
36
  _Element as EtreeElement, # pyright: ignore[reportPrivateUsage]
40
37
  )
41
38
 
39
+ from svg_ultralight.attrib_hints import ElemAttrib, OptionalElemAttribMapping
40
+
42
41
  DEFAULT_Y_BOUNDS_REFERENCE = "{[|gjpqyf"
43
42
 
44
43
 
@@ -99,7 +98,8 @@ def pad_text_ft(
99
98
  descent: float | None = None,
100
99
  *,
101
100
  y_bounds_reference: str | None = None,
102
- **attributes: str | float,
101
+ attrib: OptionalElemAttribMapping = None,
102
+ **attributes: ElemAttrib,
103
103
  ) -> PaddedText:
104
104
  """Create a new PaddedText instance using fontTools.
105
105
 
@@ -115,11 +115,15 @@ def pad_text_ft(
115
115
  extents of the capline reference. This argument is provided to mimic the
116
116
  behavior of the query module's `pad_text` function. `pad_text` does no
117
117
  inspect font files and relies on Inkscape to measure reference characters.
118
+ :param attrib: optionally pass additional attributes as a mapping instead of as
119
+ anonymous kwargs. This is useful for pleasing the linter when unpacking a
120
+ dictionary into a function call.
118
121
  :param attributes: additional attributes to set on the text element. There is a
119
122
  chance these will cause the font element to exceed the BoundingBox of the
120
123
  PaddedText instance.
121
124
  :return: a PaddedText instance with a line_gap defined.
122
125
  """
126
+ attributes.update(attrib or {})
123
127
  attributes_ = format_attr_dict(**attributes)
124
128
  attributes_.update(get_svg_font_attributes(font))
125
129
 
@@ -145,7 +149,8 @@ def pad_text_mix(
145
149
  descent: float | None = None,
146
150
  *,
147
151
  y_bounds_reference: str | None = None,
148
- **attributes: str | float,
152
+ attrib: OptionalElemAttribMapping = None,
153
+ **attributes: ElemAttrib,
149
154
  ) -> PaddedText:
150
155
  """Use Inkscape text bounds and fill missing with fontTools.
151
156
 
@@ -161,11 +166,15 @@ def pad_text_mix(
161
166
  extents of the capline reference. This argument is provided to mimic the
162
167
  behavior of the query module's `pad_text` function. `pad_text` does no
163
168
  inspect font files and relies on Inkscape to measure reference characters.
169
+ :param attrib: optionally pass additional attributes as a mapping instead of as
170
+ anonymous kwargs. This is useful for pleasing the linter when unpacking a
171
+ dictionary into a function call.
164
172
  :param attributes: additional attributes to set on the text element. There is a
165
173
  chance these will cause the font element to exceed the BoundingBox of the
166
174
  PaddedText instance.
167
175
  :return: a PaddedText instance with a line_gap defined.
168
176
  """
177
+ attributes.update(attrib or {})
169
178
  elem = new_element("text", text=text, **attributes)
170
179
  padded_inkscape = pad_text(inkscape, elem, y_bounds_reference, font=font)
171
180
  padded_fonttools = pad_text_ft(
@@ -175,7 +184,7 @@ def pad_text_mix(
175
184
  ascent,
176
185
  descent,
177
186
  y_bounds_reference=y_bounds_reference,
178
- **attributes,
187
+ attrib=attributes,
179
188
  )
180
189
  bbox = padded_inkscape.unpadded_bbox
181
190
  rpad = padded_inkscape.rpad
@@ -25,8 +25,10 @@ if TYPE_CHECKING:
25
25
  _Element as EtreeElement, # pyright: ignore[reportPrivateUsage]
26
26
  )
27
27
 
28
+ from svg_ultralight.attrib_hints import ElemAttrib
28
29
 
29
- def new_element(tag: str | QName, **attributes: str | float) -> EtreeElement:
30
+
31
+ def new_element(tag: str | QName, **attributes: ElemAttrib) -> EtreeElement:
30
32
  """Create an etree.Element, make every kwarg value a string.
31
33
 
32
34
  :param tag: element tag
@@ -62,7 +64,7 @@ def new_element(tag: str | QName, **attributes: str | float) -> EtreeElement:
62
64
 
63
65
 
64
66
  def new_sub_element(
65
- parent: EtreeElement, tag: str | QName, **attributes: str | float
67
+ parent: EtreeElement, tag: str | QName, **attributes: ElemAttrib
66
68
  ) -> EtreeElement:
67
69
  """Create an etree.SubElement, make every kwarg value a string.
68
70
 
@@ -81,7 +83,7 @@ def new_sub_element(
81
83
  return elem
82
84
 
83
85
 
84
- def update_element(elem: EtreeElement, **attributes: str | float) -> EtreeElement:
86
+ def update_element(elem: EtreeElement, **attributes: ElemAttrib) -> EtreeElement:
85
87
  """Update an existing etree.Element with additional params.
86
88
 
87
89
  :param elem: at etree element
@@ -94,7 +96,7 @@ def update_element(elem: EtreeElement, **attributes: str | float) -> EtreeElemen
94
96
  return elem
95
97
 
96
98
 
97
- def deepcopy_element(elem: EtreeElement, **attributes: str | float) -> EtreeElement:
99
+ def deepcopy_element(elem: EtreeElement, **attributes: ElemAttrib) -> EtreeElement:
98
100
  """Create a deepcopy of an element. Optionally pass additional params.
99
101
 
100
102
  :param elem: at etree element or list of elements
@@ -281,11 +281,11 @@ def _test_every_font_on_my_system(
281
281
 
282
282
  if __name__ == "__main__":
283
283
  _INKSCAPE = Path(r"C:\Program Files\Inkscape\bin\inkscape")
284
- # _FONT_DIRS = [
285
- # Path(r"C:\Windows\Fonts"),
286
- # Path(r"C:\Users\shaya\AppData\Local\Microsoft\Windows\Fonts"),
287
- # ]
288
- # _test_every_font_on_my_system(_INKSCAPE, _FONT_DIRS)
284
+ _FONT_DIRS = [
285
+ Path(r"C:\Windows\Fonts"),
286
+ Path(r"C:\Users\shaya\AppData\Local\Microsoft\Windows\Fonts"),
287
+ ]
288
+ _test_every_font_on_my_system(_INKSCAPE, _FONT_DIRS)
289
289
 
290
290
  font = Path(r"C:\Windows\Fonts\arial.ttf")
291
291
  font = Path("C:/Windows/Fonts/Aptos-Display-Bold.ttf")
@@ -119,6 +119,8 @@ if TYPE_CHECKING:
119
119
 
120
120
  from lxml.etree import _Element as EtreeElement
121
121
 
122
+ from svg_ultralight.attrib_hints import ElemAttrib
123
+
122
124
  logging.getLogger("fontTools").setLevel(logging.ERROR)
123
125
 
124
126
 
@@ -520,7 +522,7 @@ class FTTextInfo:
520
522
  """
521
523
  return self.font_size / self.font.units_per_em
522
524
 
523
- def new_element(self, **attributes: str | float) -> EtreeElement:
525
+ def new_element(self, **attributes: ElemAttrib) -> EtreeElement:
524
526
  """Return an svg text element with the appropriate font attributes."""
525
527
  matrix_vals = (self.scale, 0, 0, -self.scale, 0, 0)
526
528
  matrix = f"matrix({' '.join(format_numbers(matrix_vals))})"
@@ -36,6 +36,7 @@ if TYPE_CHECKING:
36
36
  _Element as EtreeElement, # pyright: ignore[reportPrivateUsage]
37
37
  )
38
38
 
39
+ from svg_ultralight.attrib_hints import ElemAttrib, OptionalElemAttribMapping
39
40
  from svg_ultralight.layout import PadArg
40
41
 
41
42
 
@@ -68,7 +69,8 @@ def new_svg_root(
68
69
  print_height_: float | str | None = None,
69
70
  dpu_: float = 1,
70
71
  nsmap: dict[str | None, str] | None = None,
71
- **attributes: float | str,
72
+ attrib: OptionalElemAttribMapping | None = None,
73
+ **attributes: ElemAttrib,
72
74
  ) -> EtreeElement:
73
75
  """Create an svg root element from viewBox style parameters.
74
76
 
@@ -87,6 +89,9 @@ def new_svg_root(
87
89
  different from print_width_ and print_height_ in that dpu_ scales the
88
90
  *padded* output.
89
91
  :param nsmap: optionally pass a namespace map of your choosing
92
+ :param attrib: optionally pass additional attributes as a mapping instead of as
93
+ anonymous kwargs. This is useful for pleasing the linter when unpacking a
94
+ dictionary into a function call.
90
95
  :param attributes: element attribute names and values
91
96
  :return: root svg element
92
97
 
@@ -97,10 +102,11 @@ def new_svg_root(
97
102
  will be passed to ``etree.Element`` as element parameters. These will
98
103
  supercede any parameters inferred from the trailing underscore parameters.
99
104
  """
105
+ attributes.update(attrib or {})
100
106
  if nsmap is None:
101
107
  nsmap = NSMAP
102
108
 
103
- inferred_attribs: dict[str, float | str] = {}
109
+ inferred_attribs: dict[str, ElemAttrib] = {}
104
110
  view_box_args = (x_, y_, width_, height_)
105
111
  if _is_four_floats(view_box_args):
106
112
  assert isinstance(x_, (float, int))
@@ -17,6 +17,7 @@ if TYPE_CHECKING:
17
17
  _Element as EtreeElement, # pyright: ignore[reportPrivateUsage]
18
18
  )
19
19
 
20
+ from svg_ultralight.attrib_hints import ElemAttrib, OptionalElemAttribMapping
20
21
  from svg_ultralight.bounding_boxes.supports_bounds import SupportsBounds
21
22
  from svg_ultralight.layout import PadArg
22
23
 
@@ -43,7 +44,8 @@ def new_svg_root_around_bounds(
43
44
  print_height_: float | str | None = None,
44
45
  dpu_: float = 1,
45
46
  nsmap: dict[str | None, str] | None = None,
46
- **attributes: float | str,
47
+ attrib: OptionalElemAttribMapping = None,
48
+ **attributes: ElemAttrib,
47
49
  ) -> EtreeElement:
48
50
  """Create svg root around BoundElements.
49
51
 
@@ -61,10 +63,14 @@ def new_svg_root_around_bounds(
61
63
  different from print_width_ and print_height_ in that dpu_ scales the
62
64
  *padded* output.
63
65
  :param nsmap: optionally pass a namespace map of your choosing
66
+ :param attrib: optionally pass additional attributes as a mapping instead of as
67
+ anonymous kwargs. This is useful for pleasing the linter when unpacking a
68
+ dictionary into a function call.
64
69
  :param attributes: element attribute names and values
65
70
  :return: root svg element
66
71
  :raise ValueError: if no bounding boxes are found in bounded
67
72
  """
73
+ attributes.update(attrib or {})
68
74
  bbox = bound.new_bbox_union(*bounded)
69
75
  viewbox = _viewbox_args_from_bboxes(bbox)
70
76
  return new_svg_root(
@@ -77,5 +83,5 @@ def new_svg_root_around_bounds(
77
83
  print_height_=print_height_,
78
84
  dpu_=dpu_,
79
85
  nsmap=nsmap,
80
- **attributes,
86
+ attrib=attributes,
81
87
  )
@@ -26,6 +26,8 @@ if TYPE_CHECKING:
26
26
  _Element as EtreeElement, # pyright: ignore[reportPrivateUsage]
27
27
  )
28
28
 
29
+ from svg_ultralight.attrib_hints import ElemAttrib
30
+
29
31
 
30
32
  def format_number(num: float | str, resolution: int | None = 6) -> str:
31
33
  """Format a number into an svg-readable float string with resolution = 6.
@@ -50,7 +52,7 @@ def format_numbers(
50
52
  return [format_number(num) for num in nums]
51
53
 
52
54
 
53
- def _fix_key_and_format_val(key: str, val: str | float) -> tuple[str, str]:
55
+ def _fix_key_and_format_val(key: str, val: ElemAttrib) -> tuple[str, str]:
54
56
  """Format one key, value pair for an svg element.
55
57
 
56
58
  :param key: element attribute name
@@ -82,7 +84,9 @@ def _fix_key_and_format_val(key: str, val: str | float) -> tuple[str, str]:
82
84
  else:
83
85
  key_ = key.rstrip("_").replace("_", "-")
84
86
 
85
- if isinstance(val, (int, float)):
87
+ if val is None:
88
+ val_ = "none"
89
+ elif isinstance(val, (int, float)):
86
90
  val_ = format_number(val)
87
91
  else:
88
92
  val_ = val
@@ -90,7 +94,7 @@ def _fix_key_and_format_val(key: str, val: str | float) -> tuple[str, str]:
90
94
  return key_, val_
91
95
 
92
96
 
93
- def format_attr_dict(**attributes: str | float) -> dict[str, str]:
97
+ def format_attr_dict(**attributes: ElemAttrib) -> dict[str, str]:
94
98
  """Use svg_ultralight key / value fixer to create a dict of attributes.
95
99
 
96
100
  :param attributes: element attribute names and values.
@@ -99,7 +103,7 @@ def format_attr_dict(**attributes: str | float) -> dict[str, str]:
99
103
  return dict(_fix_key_and_format_val(key, val) for key, val in attributes.items())
100
104
 
101
105
 
102
- def set_attributes(elem: EtreeElement, **attributes: str | float) -> None:
106
+ def set_attributes(elem: EtreeElement, **attributes: ElemAttrib) -> None:
103
107
  """Set name: value items as element attributes. Make every value a string.
104
108
 
105
109
  :param elem: element to receive element.set(keyword, str(value)) calls
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: svg-ultralight
3
- Version: 0.44.0
3
+ Version: 0.45.0
4
4
  Summary: a sensible way to create svg files with Python
5
5
  Author-email: Shay Hill <shay_public@hotmail.com>
6
6
  License: MIT
@@ -4,10 +4,11 @@ README.md
4
4
  dev-requirements.txt
5
5
  pyproject.toml
6
6
  tox.ini
7
- experiments/encode_fonts3.py
7
+ experiments/encode_fonts3.txt
8
8
  experiments/font_css.py
9
9
  src/svg_ultralight/__init__.py
10
10
  src/svg_ultralight/animate.py
11
+ src/svg_ultralight/attrib_hints.py
11
12
  src/svg_ultralight/image_ops.py
12
13
  src/svg_ultralight/inkscape.py
13
14
  src/svg_ultralight/layout.py
@@ -13,16 +13,21 @@ if TYPE_CHECKING:
13
13
  import os
14
14
 
15
15
 
16
- def pytest_assertrepr_compare(config: Any, op: str, left: str, right: str):
16
+ def pytest_assertrepr_compare(
17
+ config: Any, op: str, left: str, right: str
18
+ ) -> list[str] | None:
17
19
  """See full error diffs"""
20
+ del config
18
21
  if op in ("==", "!="):
19
- return ["{0} {1} {2}".format(left, op, right)]
22
+ return [f"{left} {op} {right}"]
23
+ return None
20
24
 
21
25
 
22
26
  TEST_RESOURCES = Path(__file__).parent / "resources"
23
27
 
24
28
  INKSCAPE = Path(r"C:\Program Files\Inkscape\bin\inkscape")
25
29
 
30
+
26
31
  def has_inkscape(inkscape: str | os.PathLike[str]) -> bool:
27
32
  """Check if Inkscape is available at a path.
28
33
 
@@ -4,22 +4,24 @@
4
4
  :created: 2024-05-05
5
5
  """
6
6
 
7
- import pytest
7
+ import copy
8
8
  import math
9
+
10
+ import pytest
9
11
  from conftest import TEST_RESOURCES
10
- from svg_ultralight.bounding_boxes.type_bound_element import BoundElement
11
- from svg_ultralight.bounding_boxes.type_padded_text import PaddedText
12
12
  from lxml import etree
13
- from svg_ultralight.bounding_boxes.type_bounding_box import BoundingBox
14
- from svg_ultralight.bounding_boxes.type_bound_collection import BoundCollection
13
+
15
14
  from svg_ultralight.bounding_boxes.bound_helpers import (
16
- pad_bbox,
17
- cut_bbox,
18
- parse_bound_element,
19
15
  bbox_dict,
16
+ cut_bbox,
20
17
  new_bbox_rect,
18
+ pad_bbox,
19
+ parse_bound_element,
21
20
  )
22
- import copy
21
+ from svg_ultralight.bounding_boxes.type_bound_collection import BoundCollection
22
+ from svg_ultralight.bounding_boxes.type_bound_element import BoundElement
23
+ from svg_ultralight.bounding_boxes.type_bounding_box import BoundingBox
24
+ from svg_ultralight.bounding_boxes.type_padded_text import PaddedText
23
25
  from svg_ultralight.constructors import new_element
24
26
 
25
27
 
@@ -126,7 +128,7 @@ class TestPaddedText:
126
128
  assert bound_element.cy == 148.0
127
129
 
128
130
  def test_cx(self, bound_element: PaddedText):
129
- bbox_x = bound_element.x
131
+ bbox_x = bound_element.x
130
132
  bbox_x2 = bound_element.x2
131
133
  bbox_cx = bound_element.cx
132
134
  bound_element.cx += 100.0
@@ -135,13 +137,13 @@ class TestPaddedText:
135
137
  assert bound_element.cx == bbox_cx + 100.0
136
138
 
137
139
  def test_cy(self, bound_element: PaddedText):
138
- bbox_y = bound_element.y
140
+ bbox_y = bound_element.y
139
141
  bbox_y2 = bound_element.y2
140
142
  bbox_cy = bound_element.cy
141
143
  bound_element.cy += 100.0
142
144
  assert bound_element.y == bbox_y + 100.0
143
145
  assert bound_element.y2 == bbox_y2 + 100.0
144
- assert bound_element.cy == bbox_cy + 100.0
146
+ assert bound_element.cy == bbox_cy + 100.0
145
147
 
146
148
  def test_width(self, bound_element: PaddedText):
147
149
  assert bound_element.width == 106.0
@@ -188,7 +190,6 @@ class TestPaddedText:
188
190
 
189
191
 
190
192
  class TestBoundCollection:
191
-
192
193
  @pytest.fixture
193
194
  def bound_collection(self) -> BoundCollection:
194
195
  elem = new_element("rect", x=0, y=0, width=100, height=200)
@@ -11,7 +11,6 @@
11
11
  # pyright: reportMissingParameterType = false
12
12
  # pyright: reportPrivateUsage = false
13
13
 
14
-
15
14
  import itertools as it
16
15
  import math
17
16
  from collections.abc import Iterator
@@ -39,13 +38,13 @@ INKSCAPE_SCALARS = {
39
38
  }
40
39
 
41
40
 
42
- @pytest.fixture(scope="function", params=Unit)
43
- def unit(request) -> Unit:
41
+ @pytest.fixture(params=Unit)
42
+ def unit(request: pytest.FixtureRequest) -> Unit:
44
43
  return request.param
45
44
 
46
45
 
47
- @pytest.fixture(scope="function", params=it.product(Unit, Unit))
48
- def unit_pair(request) -> Iterator[Unit]:
46
+ @pytest.fixture(params=it.product(Unit, Unit))
47
+ def unit_pair(request: pytest.FixtureRequest) -> Iterator[Unit]:
49
48
  return request.param
50
49
 
51
50
 
@@ -80,17 +79,17 @@ class TestParseUnit:
80
79
 
81
80
 
82
81
  class TestMeasurement:
83
- def test_unit_identified(self, unit):
82
+ def test_unit_identified(self, unit: Unit):
84
83
  """Test that unit is identified correctly."""
85
84
  assert Measurement(f"1{unit.value[0]}").native_unit == unit
86
85
 
87
- def test_value_scaled(self, unit):
86
+ def test_value_scaled(self, unit: Unit):
88
87
  """Value is scaled per Inkscape conversion values."""
89
88
  assert math.isclose(
90
89
  Measurement(f"1{unit.value[0]}").value, INKSCAPE_SCALARS[unit.value[0]]
91
90
  )
92
91
 
93
- def test_conversion(self, unit_pair):
92
+ def test_conversion(self, unit_pair: tuple[Unit, Unit]):
94
93
  """Test that value is converted to other units."""
95
94
  unit_a, unit_b = unit_pair
96
95
  a_unit = Measurement(f"1{unit_a.value[0]}")
@@ -98,27 +97,27 @@ class TestMeasurement:
98
97
  b_unit = Measurement(f"{a_as_b}{unit_b.value[0]}")
99
98
  assert math.isclose(b_unit.value, a_unit.value)
100
99
 
101
- def test_add(self, unit):
100
+ def test_add(self, unit: Unit):
102
101
  """Test that values are added."""
103
102
  a_unit = Measurement(f"1{unit.value[0]}")
104
103
  b_unit = Measurement(f"2{unit.value[0]}")
105
104
  assert (a_unit + b_unit).value == Measurement(f"3{unit.value[0]}").value
106
105
 
107
- def test_subtract(self, unit):
106
+ def test_subtract(self, unit: Unit):
108
107
  """Test that values are subtracted."""
109
108
  a_unit = Measurement(f"1{unit.value[0]}")
110
109
  b_unit = Measurement(f"2{unit.value[0]}")
111
110
  assert (a_unit - b_unit).value == Measurement(f"-1{unit.value[0]}").value
112
111
 
113
- def test_multiply(self, unit):
112
+ def test_multiply(self, unit: Unit):
114
113
  """Test that values are multiplied."""
115
114
  assert (Measurement((1, unit)) * 4).value == Measurement((4, unit)).value
116
115
 
117
- def test_rmultiply(self, unit):
116
+ def test_rmultiply(self, unit: Unit):
118
117
  """Test that values are multiplied."""
119
118
  assert (4 * Measurement((1, unit))).value == Measurement((4, unit)).value
120
119
 
121
- def test_divide(self, unit):
120
+ def test_divide(self, unit: Unit):
122
121
  """Test that values are multiplied."""
123
122
  assert (Measurement((1, unit)) / 4).value == Measurement((1 / 4, unit)).value
124
123
 
@@ -144,6 +143,7 @@ class TestExpandPadArg:
144
143
  """Test that a single value is expanded to a 4-tuple per css rules."""
145
144
  assert layout.expand_pad_arg((1, 2, 3, 4)) == (1, 2, 3, 4)
146
145
 
146
+
147
147
  class TestLayout:
148
148
  def test_standard(self):
149
149
  """No print dimensions give expanded pad argument
@@ -194,9 +194,8 @@ class TestLayout:
194
194
  def test_infinite_width(self):
195
195
  """Raise ValueError if no non-infinite scale can be inferred."""
196
196
  viewbox = (0, 0, 0, 0)
197
- with pytest.raises(ValueError) as excinfo:
197
+ with pytest.raises(ValueError, match="infinite"):
198
198
  _ = layout.pad_and_scale(viewbox, "0.25in", "2in")
199
- assert "infinite" in str(excinfo.value)
200
199
 
201
200
  def test_pad_print_at_input_scale(self):
202
201
  """Test that padding is applied at the input scale.
@@ -229,7 +228,7 @@ class TestLayout:
229
228
  assert width_attribs == {"width": "12in", "height": "22in"}
230
229
 
231
230
  padded, width_attribs = layout.pad_and_scale(viewbox, "1in", "100in")
232
- assert [format_number(x) for x in padded] == ['-.1', '-.1', '10.2', '20.2']
231
+ assert [format_number(x) for x in padded] == ["-.1", "-.1", "10.2", "20.2"]
233
232
  assert width_attribs == {"width": "102in", "height": "202in"}
234
233
 
235
234
  def test_dpu_(self):
@@ -248,6 +247,4 @@ class TestLayout:
248
247
  viewbox = (0, 0, 1, 1)
249
248
  padded, width_attribs = layout.pad_and_scale(viewbox, 1, dpu=2)
250
249
  assert padded == (-1, -1, 3, 3)
251
- assert width_attribs == {'width': '6', 'height': '6'}
252
-
253
-
250
+ assert width_attribs == {"width": "6", "height": "6"}
@@ -4,11 +4,12 @@
4
4
  :created: 2024-05-05
5
5
  """
6
6
 
7
- from svg_ultralight.transformations import mat_dot, mat_apply, mat_invert
8
- import random
9
7
  import math
8
+ import random
10
9
  from contextlib import suppress
11
10
 
11
+ from svg_ultralight.transformations import mat_apply, mat_dot, mat_invert
12
+
12
13
 
13
14
  class TestMat:
14
15
  def test_explicit(self):
@@ -4,10 +4,11 @@
4
4
  :created: 2024-01-30
5
5
  """
6
6
 
7
- from svg_ultralight.metadata import new_metadata
8
- from svg_ultralight.main import new_svg_root
9
7
  from lxml import etree
10
8
 
9
+ from svg_ultralight.main import new_svg_root
10
+ from svg_ultralight.metadata import new_metadata
11
+
11
12
  _inkscape_output = """
12
13
  <metadata id="metadata1">
13
14
  <rdf:RDF>
@@ -53,6 +54,7 @@ _inkscape_output = """
53
54
 
54
55
  _inkscape_output = "".join([x.strip() for x in _inkscape_output.splitlines()])
55
56
 
57
+
56
58
  class TestMetedata:
57
59
  def test_inkscape_explicit(self):
58
60
  """Output matches Inkscape output.
@@ -3,14 +3,14 @@
3
3
  :author: Shay Hill
4
4
  :created: 2025-06-09
5
5
  """
6
+
6
7
  from pathlib import Path
7
8
 
8
9
  import pytest
10
+ from conftest import INKSCAPE, has_inkscape
9
11
 
10
12
  from svg_ultralight.bounding_boxes.padded_text_initializers import pad_text, pad_text_ft
11
13
  from svg_ultralight.constructors import new_element
12
- from conftest import has_inkscape, INKSCAPE
13
-
14
14
 
15
15
 
16
16
  class TestPadText:
@@ -57,6 +57,7 @@ class TestPadText:
57
57
  padded.line_gap = 5
58
58
  assert padded.leading == padded.height + 5
59
59
 
60
+
60
61
  class TestPadTextFt:
61
62
  def test_has_line_gap(self) -> None:
62
63
  """Test pad_text_ft with a font file."""
@@ -8,11 +8,10 @@ your system.
8
8
  """
9
9
 
10
10
  from dataclasses import dataclass
11
- from pathlib import Path
12
11
 
13
12
  import pytest
14
-
15
13
  from conftest import INKSCAPE, has_inkscape
14
+
16
15
  from svg_ultralight import BoundingBox, new_svg_root
17
16
  from svg_ultralight.constructors import new_sub_element
18
17
  from svg_ultralight.query import (
@@ -22,7 +21,6 @@ from svg_ultralight.query import (
22
21
  )
23
22
 
24
23
 
25
-
26
24
  class TestMergeBoundingBoxes:
27
25
  def test_new_merged_bbox(self):
28
26
  bbox_a = BoundingBox(-2, -4, 10, 20)
@@ -223,6 +221,7 @@ class TestMapElemsToBoundingBoxes:
223
221
  result = get_bounding_boxes(INKSCAPE, *elems)
224
222
  assert result == tuple(get_bounding_box(INKSCAPE, e) for e in elems)
225
223
 
224
+
226
225
  class TestAlterBoundingBox:
227
226
  def test_reverse_width(self) -> None:
228
227
  """adjust width one way then the other returns to original box."""
@@ -232,7 +231,3 @@ class TestAlterBoundingBox:
232
231
  bbox.height = 200
233
232
  bbox.height = 40
234
233
  assert bbox.transformation == (1, 0, 0, 1, 90, 180)
235
-
236
-
237
-
238
-
@@ -5,29 +5,26 @@
5
5
  """
6
6
 
7
7
  import pytest
8
+ from lxml.etree import _Element as EtreeElement # pyright: ignore[reportPrivateUsage]
8
9
 
9
- from svg_ultralight.bounding_boxes.type_bounding_box import BoundingBox
10
+ from svg_ultralight.bounding_boxes.bound_helpers import new_bound_union
10
11
  from svg_ultralight.bounding_boxes.type_bound_element import BoundElement
12
+ from svg_ultralight.bounding_boxes.type_bounding_box import BoundingBox
11
13
  from svg_ultralight.bounding_boxes.type_padded_text import PaddedText
12
14
  from svg_ultralight.constructors import new_element
13
15
  from svg_ultralight.root_elements import new_svg_root_around_bounds
14
- from svg_ultralight.bounding_boxes.bound_helpers import new_bound_union
15
- from lxml.etree import _Element as EtreeElement # pyright: ignore[reportPrivateUsage]
16
16
 
17
17
 
18
18
  class TestNewSvgRootAroundBounds:
19
-
20
19
  def test_empty(self):
21
20
  """Raise ValueError if no bounding boxes found."""
22
- with pytest.raises(ValueError) as excinfo:
21
+ with pytest.raises(ValueError, match="At least one argument"):
23
22
  _ = new_svg_root_around_bounds()
24
- assert "At least one argument" in str(excinfo.value)
25
23
 
26
24
  def test_no_bound_elements(self):
27
25
  """Raise ValueError if no BoundElements found."""
28
- with pytest.raises(ValueError) as excinfo:
26
+ with pytest.raises(ValueError, match="At least one argument"):
29
27
  _ = new_svg_root_around_bounds(new_element("g"))
30
- assert "At least one argument" in str(excinfo.value)
31
28
 
32
29
  def test_bounding_boxes(self):
33
30
  """Create svg root element from bounding boxes."""
@@ -52,21 +49,19 @@ class TestNewSvgRootAroundBounds:
52
49
  assert isinstance(result, EtreeElement)
53
50
  assert result.attrib["viewBox"] == "0 0 201 201"
54
51
 
55
- class TestNewBoundUnion:
56
52
 
53
+ class TestNewBoundUnion:
57
54
  def test_bounding_boxes_only(self):
58
55
  """Raise an error if no elements found."""
59
56
  bboxes = [BoundingBox(0, 0, 100, 100), BoundingBox(50, 50, 150, 150)]
60
- with pytest.raises(ValueError) as excinfo:
57
+ with pytest.raises(ValueError, match="must be a BoundElement, PaddedText"):
61
58
  _ = new_bound_union(*bboxes)
62
- assert "must be a BoundElement, PaddedText, or Etree" in str(excinfo.value)
63
59
 
64
60
  def test_elements_only(self):
65
61
  """Raise an error if no elements found."""
66
62
  elems = [new_element("g"), new_element("g")]
67
- with pytest.raises(ValueError) as excinfo:
63
+ with pytest.raises(ValueError, match="must be a BoundElement, BoundingBox"):
68
64
  _ = new_bound_union(*elems)
69
- assert "must be a BoundElement, BoundingBox, or Padded" in str(excinfo.value)
70
65
 
71
66
  def test_bound_elements(self):
72
67
  """Create svg root element from BoundElements."""
@@ -8,13 +8,14 @@ No tests for png writing.
8
8
  """
9
9
 
10
10
  from __future__ import annotations
11
+
11
12
  import os
12
13
  import tempfile
13
14
  from pathlib import Path
15
+ from typing import TYPE_CHECKING
14
16
 
15
17
  import pytest
16
18
  from lxml import etree
17
- from lxml.etree import _Element as EtreeElement # pyright: ignore[reportPrivateUsage]
18
19
 
19
20
  from svg_ultralight import NSMAP
20
21
 
@@ -22,8 +23,13 @@ from svg_ultralight import NSMAP
22
23
  from svg_ultralight.main import new_svg_root, write_svg
23
24
  from svg_ultralight.string_conversion import svg_tostring
24
25
 
26
+ if TYPE_CHECKING:
27
+ from lxml.etree import (
28
+ _Element as EtreeElement, # pyright: ignore[reportPrivateUsage]
29
+ )
30
+
25
31
 
26
- @pytest.fixture()
32
+ @pytest.fixture
27
33
  def css_source():
28
34
  """Temporary css file object with meaningless contents."""
29
35
  with tempfile.NamedTemporaryFile(mode="w", delete=False) as css_source:
@@ -32,24 +38,29 @@ def css_source():
32
38
  os.unlink(css_source.name)
33
39
 
34
40
 
35
- @pytest.fixture()
41
+ @pytest.fixture
36
42
  def temp_filename(mode: str = "w"):
37
43
  """Temporary file object to capture test output."""
38
- svg_output = tempfile.NamedTemporaryFile(mode=mode, delete=False)
39
- svg_output.close()
40
- yield svg_output.name
44
+ with tempfile.NamedTemporaryFile(mode=mode, delete=False) as svg_output:
45
+ yield svg_output.name
41
46
  os.unlink(svg_output.name)
42
47
 
43
48
 
44
49
  class TestWriteSvg:
45
- def test_linked(self, css_source, temp_filename) -> None:
50
+ def test_linked(
51
+ self, css_source: str | os.PathLike[str], temp_filename: str | os.PathLike[str]
52
+ ) -> None:
46
53
  """Insert stylesheet reference."""
47
54
  blank = etree.Element("blank")
48
- write_svg(
49
- temp_filename, blank, css_source, do_link_css=True, xml_declaration=True
55
+ _ = write_svg(
56
+ Path(temp_filename),
57
+ blank,
58
+ css_source,
59
+ do_link_css=True,
60
+ xml_declaration=True,
50
61
  )
51
- with open(temp_filename, "rb") as svg_binary:
52
- svg_lines = [x.decode() for x in svg_binary.readlines()]
62
+ with Path(temp_filename).open("rb") as svg_binary:
63
+ svg_lines = [x.decode() for x in svg_binary]
53
64
 
54
65
  relative_css_path = Path(css_source).relative_to(Path(temp_filename).parent)
55
66
  assert svg_lines == [
@@ -60,12 +71,14 @@ class TestWriteSvg:
60
71
  "<blank/>\n",
61
72
  ]
62
73
 
63
- def test_not_linked(self, css_source, temp_filename) -> None:
74
+ def test_not_linked(
75
+ self, css_source: str | os.PathLike[str], temp_filename: str | os.PathLike[str]
76
+ ) -> None:
64
77
  """Copy css_source contents into svg file."""
65
78
  blank = etree.Element("blank")
66
- write_svg(temp_filename, blank, css_source)
67
- with open(temp_filename, "rb") as svg_binary:
68
- svg_lines = [x.decode() for x in svg_binary.readlines()]
79
+ _ = write_svg(Path(temp_filename), blank, css_source)
80
+ with Path(temp_filename).open("rb") as svg_binary:
81
+ svg_lines = [x.decode() for x in svg_binary]
69
82
 
70
83
  assert svg_lines == [
71
84
  "<blank>\n",
@@ -75,19 +88,19 @@ class TestWriteSvg:
75
88
  "</blank>\n",
76
89
  ]
77
90
 
78
- def test_css_none(self, temp_filename) -> None:
91
+ def test_css_none(self, temp_filename: str | os.PathLike[str]) -> None:
79
92
  """Do not link or copy in css if no css_source is passed."""
80
93
  blank = etree.Element("blank")
81
- write_svg(temp_filename, blank, do_link_css=True)
82
- with open(temp_filename, "rb") as svg_binary:
83
- svg_lines = [x.decode() for x in svg_binary.readlines()]
94
+ _ = write_svg(Path(temp_filename), blank, do_link_css=True)
95
+ with Path(temp_filename).open("rb") as svg_binary:
96
+ svg_lines = [x.decode() for x in svg_binary]
84
97
 
85
98
  assert svg_lines == ["<blank/>\n"]
86
99
 
87
100
  # test with do_link_css = False
88
- write_svg(temp_filename, blank)
89
- with open(temp_filename, "rb") as svg_binary:
90
- svg_lines_false = [x.decode() for x in svg_binary.readlines()]
101
+ _ = write_svg(Path(temp_filename), blank)
102
+ with Path(temp_filename).open("rb") as svg_binary:
103
+ svg_lines_false = [x.decode() for x in svg_binary]
91
104
  assert svg_lines_false == svg_lines
92
105
 
93
106
 
@@ -106,7 +119,7 @@ def svg_root(**kwargs: str | float) -> EtreeElement:
106
119
  xmlns = [f'xmlns="{namespace[0][1]}"']
107
120
  xmlns += [f'xmlns:{k}="{v}"' for k, v in namespace[1:]]
108
121
  attributes = " ".join([f'{k}="{v}"' for k, v in kwargs.items()])
109
- return etree.fromstring(f'<svg {" ".join(xmlns)} {attributes}/>'.encode())
122
+ return etree.fromstring(f"<svg {' '.join(xmlns)} {attributes}/>".encode())
110
123
 
111
124
 
112
125
  class TestNewSvgRoot:
@@ -120,19 +133,19 @@ class TestNewSvgRoot:
120
133
 
121
134
  Build the svg element namespace from NSMAP and compare to output
122
135
  """
123
- expect = svg_root(**{"viewBox": "0 1 2 3"})
136
+ expect = svg_root(viewBox="0 1 2 3")
124
137
  result = new_svg_root(0, 1, 2, 3)
125
138
  assert result.attrib == expect.attrib
126
139
 
127
140
  def test_additional_params(self) -> None:
128
141
  """Pass additional params."""
129
- expect = svg_root(**{"attr": "value", "viewBox": "0 1 2 3"})
142
+ expect = svg_root(attr="value", viewBox="0 1 2 3")
130
143
  result = new_svg_root(0, 1, 2, 3, attr="value")
131
144
  assert result.attrib == expect.attrib
132
145
 
133
146
  def test_conflicting_params(self) -> None:
134
147
  """Explicit params overwrite trailing-underscore-inferred params."""
135
- expect = svg_root(**{"viewBox": "0 1 2 3", "height": "30"})
148
+ expect = svg_root(viewBox="0 1 2 3", height="30")
136
149
  result = new_svg_root(0, 1, 2, 3, height=30)
137
150
  assert result.attrib == expect.attrib
138
151
 
File without changes