Skip to content

Commit b40b2b6

Browse files
committed
Move dimension parser to standalone function
1 parent 7363cfe commit b40b2b6

File tree

3 files changed

+114
-125
lines changed

3 files changed

+114
-125
lines changed

pgfutils.py

Lines changed: 68 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,71 @@ class ColorError(ValueError):
4545
pass
4646

4747

48+
# Recognise pieces of a dimension string.
49+
dimension_pieces = re.compile(r"^\s*(?P<size>\d+(?:\.\d*)?)\s*(?P<unit>.+?)?\s*$")
50+
51+
# Divisors to convert from a unit to inches.
52+
dimension_divisor = {
53+
"cm": 2.54,
54+
"centimetre": 2.54,
55+
"centimeter": 2.54,
56+
"centimetres": 2.54,
57+
"centimeters": 2.54,
58+
"mm": 25.4,
59+
"millimetre": 25.4,
60+
"millimeter": 25.4,
61+
"millimetres": 25.4,
62+
"millimeters": 25.4,
63+
"in": 1,
64+
"inch": 1,
65+
"inches": 1,
66+
"pt": 72.27, # Printers points, not the 1/72 Postscript/PDF points.
67+
"point": 72.27,
68+
"points": 72.27,
69+
}
70+
71+
72+
def parse_dimension(spec: str) -> float:
73+
"""Parse a dimension specification to TeX points.
74+
75+
Matplotlib uses inches for physical sizes. This function allows other units to be
76+
specified and converts them to inches. Note that points are assumed to be TeX points
77+
(72.27 per inch) rather than Postscript points (72 per inch).
78+
79+
Parameters
80+
----------
81+
spec
82+
A dimension specification. This should be in the format "<value><unit>" where
83+
the unit can be any of the keys from the `dimension_divisor` dictionary.
84+
Whitespace is allowed between the value and unit. If no unit is given, it is
85+
assumed to be inches.
86+
87+
Returns
88+
-------
89+
float
90+
The dimension in inches.
91+
92+
"""
93+
match = dimension_pieces.match(spec)
94+
if not match:
95+
raise DimensionError(f"could not parse {spec} as a dimension")
96+
97+
# Get the components.
98+
groups = match.groupdict()
99+
size = float(groups["size"])
100+
unit: str = (groups.get("unit") or "").lower()
101+
102+
# Assume already in inches.
103+
if not unit:
104+
return size
105+
106+
# Convert with the divisor.
107+
factor = dimension_divisor.get(unit)
108+
if factor is None:
109+
raise DimensionError(f"unknown unit in {spec}")
110+
return size / factor
111+
112+
48113
class PgfutilsParser(configparser.ConfigParser):
49114
"""Custom configuration parser with Matplotlib dimension and color support."""
50115

@@ -136,63 +201,6 @@ def read_kwargs(self, **kwargs):
136201
# And then read the dictionary in.
137202
return self.read_dict(d)
138203

139-
def parsedimension(self, dim):
140-
"""Convert a dimension to inches.
141-
142-
The dimension should be in the format '<value><unit>', where the unit can be
143-
'mm', 'cm', 'in', or 'pt'. If no unit is specified, it is assumed to be in
144-
inches. Note that points refer to TeX points (72.27 per inch) rather than
145-
Postscript points (72 per inch).
146-
147-
Parameters
148-
----------
149-
dim: string
150-
The dimension to parse.
151-
152-
Returns
153-
-------
154-
float: The dimension in inches.
155-
156-
Raises
157-
------
158-
DimensionError:
159-
The dimension is empty or not recognised.
160-
161-
"""
162-
# Need a string.
163-
if dim is None:
164-
raise DimensionError("Cannot be set to an empty value.")
165-
166-
# Check for an empty string.
167-
dim = dim.strip().lower()
168-
if not dim:
169-
raise DimensionError("Cannot be set to an empty value.")
170-
171-
# Try to parse it.
172-
m = self._dimre.match(dim)
173-
if not m:
174-
raise DimensionError(f"Could not parse {dim} as a dimension.")
175-
176-
# Pull out the pieces.
177-
groups = m.groupdict()
178-
size = float(groups["size"])
179-
unit = groups.get("unit", "") or ""
180-
unit = unit.lower()
181-
182-
# No unit: already in inches.
183-
if not unit:
184-
return size
185-
186-
# Pick out the divisor to convert into inches.
187-
factor = self._dimconv.get(unit, None)
188-
189-
# Unknown unit.
190-
if factor is None:
191-
raise DimensionError(f"Unknown unit {unit}.")
192-
193-
# Do the conversion.
194-
return size / factor
195-
196204
def getdimension(self, section, option, **kwargs):
197205
"""Return a configuration entry as a dimension in inches.
198206
@@ -222,7 +230,7 @@ def getdimension(self, section, option, **kwargs):
222230
# And parse it; modify any parsing exception to include
223231
# the section and option we were parsing.
224232
try:
225-
return self.parsedimension(dim)
233+
return parse_dimension(dim)
226234
except DimensionError as e:
227235
raise DimensionError(f"{section}.{option}: {e}") from None
228236

@@ -848,15 +856,15 @@ def setup_figure(
848856
try:
849857
w = float(width)
850858
except ValueError:
851-
w = _config.parsedimension(width)
859+
w = parse_dimension(width)
852860
else:
853861
w *= available_width
854862

855863
# And the figure height.
856864
try:
857865
h = float(height)
858866
except ValueError:
859-
h = _config.parsedimension(height)
867+
h = parse_dimension(height)
860868
else:
861869
h *= _config["tex"].getdimension("text_height")
862870

tests/test_config.py

Lines changed: 1 addition & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,8 @@
44
from pathlib import Path
55

66
import pytest
7-
from pytest import approx
87

9-
from pgfutils import DimensionError, _config, _config_reset
8+
from pgfutils import _config, _config_reset
109

1110
base = Path(__file__).parent
1211

@@ -41,69 +40,6 @@ def test_cfg_unknown_rcparams(self):
4140
with pytest.raises(KeyError):
4241
_config.read(base / "sources" / "extra_options_rcparams.cfg")
4342

44-
def test_dim_unknown_unit(self):
45-
"""Dimension with unknown unit is rejected..."""
46-
_config_reset()
47-
with pytest.raises(DimensionError):
48-
_config.parsedimension("1.2kg")
49-
with pytest.raises(DimensionError):
50-
_config.read_kwargs(text_width="1.2kg")
51-
_config["tex"].getdimension("text_width")
52-
53-
def test_dimension_empty(self):
54-
"""Dimension cannot be empty string..."""
55-
_config_reset()
56-
with pytest.raises(DimensionError):
57-
_config.parsedimension("")
58-
with pytest.raises(DimensionError):
59-
_config.parsedimension(" ")
60-
with pytest.raises(DimensionError):
61-
_config.parsedimension(None)
62-
with pytest.raises(DimensionError):
63-
_config.read_kwargs(text_width="")
64-
_config["tex"].getdimension("text_width")
65-
with pytest.raises(DimensionError):
66-
_config.read_kwargs(text_width=" ")
67-
_config["tex"].getdimension("text_width")
68-
69-
def test_dimension_not_parsing(self):
70-
"""Dimension rejects invalid strings..."""
71-
_config_reset()
72-
with pytest.raises(DimensionError):
73-
_config.parsedimension("cm1.2")
74-
with pytest.raises(DimensionError):
75-
_config.parsedimension("1.2.2cm")
76-
with pytest.raises(DimensionError):
77-
_config.read_kwargs(text_width="1.2.2cm")
78-
_config["tex"].getdimension("text_width")
79-
with pytest.raises(DimensionError):
80-
_config.read_kwargs(text_width="cm1.2")
81-
_config["tex"].getdimension("text_width")
82-
83-
def test_dimension_inches(self):
84-
"""Dimensions without units are treated as inches..."""
85-
_config_reset()
86-
assert _config.parsedimension("7") == approx(7)
87-
assert _config.parsedimension("2.7") == approx(2.7)
88-
_config.read_kwargs(text_width="5")
89-
assert _config["tex"].getdimension("text_width") == approx(5)
90-
_config.read_kwargs(text_width="5.451")
91-
assert _config["tex"].getdimension("text_width") == approx(5.451)
92-
93-
def test_dimension_negative(self):
94-
"""Negative dimensions are rejected..."""
95-
_config_reset()
96-
with pytest.raises(DimensionError):
97-
_config.parsedimension("-1.2")
98-
with pytest.raises(DimensionError):
99-
_config.parsedimension("-1.2cm")
100-
with pytest.raises(DimensionError):
101-
_config.read_kwargs(text_width="-1.2")
102-
_config["tex"].getdimension("text_width")
103-
with pytest.raises(DimensionError):
104-
_config.read_kwargs(text_width="-1.2cm")
105-
_config["tex"].getdimension("text_width")
106-
10743
def test_unknown_tracking_type(self):
10844
"""Unknown tracking types are rejected..."""
10945
_config_reset()

tests/test_dimension.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import pytest
2+
from pytest import approx
3+
4+
from pgfutils import DimensionError, parse_dimension
5+
6+
7+
class TestDimension:
8+
def test_parse(self):
9+
"""Dimension parser with valid inputs"""
10+
assert parse_dimension("1") == approx(1)
11+
assert parse_dimension("3.2") == approx(3.2)
12+
assert parse_dimension("2.54cm") == approx(1)
13+
assert parse_dimension("2.54 cm") == approx(1)
14+
assert parse_dimension("2.54\tcm") == approx(1)
15+
16+
def test_unknown_unit(self):
17+
"""Dimension parser rejects unknown units"""
18+
with pytest.raises(DimensionError):
19+
parse_dimension("1.2kg")
20+
21+
def test_dimension_empty(self):
22+
"""Dimension cannot be empty string"""
23+
with pytest.raises(DimensionError):
24+
parse_dimension("")
25+
with pytest.raises(DimensionError):
26+
parse_dimension(" ")
27+
28+
def test_dimension_not_parsing(self):
29+
"""Dimension parser rejects invalid strings..."""
30+
with pytest.raises(DimensionError):
31+
parse_dimension("cm1.2")
32+
with pytest.raises(DimensionError):
33+
parse_dimension("1.2.2cm")
34+
35+
def test_dimension_inches(self):
36+
"""Dimensions without units are treated as inches..."""
37+
assert parse_dimension("7") == approx(7)
38+
assert parse_dimension("2.7") == approx(2.7)
39+
40+
def test_dimension_negative(self):
41+
"""Negative dimensions are rejected..."""
42+
with pytest.raises(DimensionError):
43+
parse_dimension("-1.2")
44+
with pytest.raises(DimensionError):
45+
parse_dimension("-1.2cm")

0 commit comments

Comments
 (0)