Skip to content

Commit c20408d

Browse files
authored
refactor: consistent printline() across pose and quaternion classes (#200)
## What Consistent `printline()` behaviour across `SO2`/`SE2`/`SO3`/`SE3`/ `Quaternion`/`UnitQuaternion`: - Adds `Quaternion.printline()` (previously only the pose classes had it). - Splits the string-building logic out of `trprint`/`trprint2`/`qprint` into dedicated `tr2str`/`tr2str2`/`q2str`-style functions (the latter two already existed; `tr2str`/`tr2str2` are new), so "give me the formatted string" and "print the formatted string" are two separate, clearly-named functions instead of one function trying to do both via a `file` flag. - Fixes the original bug this was written for: `contextlib.redirect_stdout()` didn't work with the old code, because `file=sys.stdout` as a default argument value is evaluated once at function-definition time and freezes whatever `sys.stdout` was *then* - it doesn't see a later redirect. Fixed by using a `file=False` sentinel, resolved to `None` inside the function body (so `print()` looks up the *current* `sys.stdout` at call time). ## Bugs found while packaging this up for review This started as old WIP work from a local branch, never previously reviewed or merged. Testing it before submission turned up three real bugs, all fixed here: 1. **`trprint()`/`qprint()` silently shifted `file` into the 2nd positional argument.** The original, already-published signatures had `orient` (`trprint`) / `delim` (`qprint`) there. `trprint(T, 'angvec')` or this library's *own documented* `x.printline('angvec')` example put `'angvec'` into `file` instead, crashing with `AttributeError: 'str' object has no attribute 'write'`. Fixed by restoring the original explicit parameter order for both (`trprint2` was unaffected - its parameter order happened to already put `file` in the same slot). 2. **`trprint2()` captured `label` but never forwarded it** to `tr2str2()` - `trprint2(T, label='T')` silently dropped the label from the output. 3. **All three stopped returning the formatted string**, only printing it - breaking the existing, still-supported `file=None` → "return the string instead of printing" usage that a couple of existing tests rely on (`test_transforms3d.py::test_print`, `test_transforms2d.py::test_print2`). While fixing (3), also formally deprecated that `file=None` pattern rather than silently perpetuating it: each function now has a real `tr2str()`/`tr2str2()`/`q2str()` sibling that returns the string directly, so `file=None` still works exactly as before (returns the string, doesn't print) but now raises a `DeprecationWarning` and says so explicitly in the docstring via `.. deprecated::`, pointing at the replacement function. All three (`trprint`/`trprint2`/`qprint`) now follow one consistent shape: build the string via the `*2str`/`q2str` sibling, always return it, print only when `file is not None`. ## Testing - Verified `file=None` still returns the correct string with a `DeprecationWarning` and no printing, for all three functions. - Verified the default path both prints (via `contextlib.redirect_stdout`, confirming the original bug is actually fixed) and returns the same string. - Verified positional-arg calls (`trprint(T, 'angvec')`, `x.printline('angvec')`) work correctly again. - Full suite: 338 passed, 4 skipped. - `black --check` clean at the pinned 23.10.0. ## Note on PR ordering This PR and #198 (lazy Matplotlib import) both touch adjacent code in `spatialmath/base/transforms3d.py` (right around `_vec2s`/ `_matplotlib_exists`/`trprint`/`trplot`) - not a conflict with anything already merged, just two open PRs based on the same `upstream/master` point. Whichever lands first, the other will likely need a rebase to resolve a real merge conflict before it can land.
1 parent 45ed832 commit c20408d

6 files changed

Lines changed: 238 additions & 57 deletions

File tree

‎spatialmath/base/__init__.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,7 @@
225225
"tradjoint2",
226226
"trinterp2",
227227
"trprint2",
228+
"tr2str2",
228229
"trplot2",
229230
"tranimate2",
230231
"xyt2tr",
@@ -268,6 +269,7 @@
268269
"exp2jac",
269270
"rot2jac",
270271
"trprint",
272+
"tr2str",
271273
"trplot",
272274
"tranimate",
273275
"tr2x",

‎spatialmath/base/quaternions.py‎

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1188,10 +1188,10 @@ def qprint(
11881188
q: Union[ArrayLike4, ArrayLike4],
11891189
delim: Optional[Tuple[str, str]] = ("<", ">"),
11901190
fmt: Optional[str] = "{: .4f}",
1191-
file: Optional[TextIO] = sys.stdout,
1192-
) -> None:
1191+
file: Optional[TextIO] = False,
1192+
) -> str:
11931193
"""
1194-
Format a quaternion to a file
1194+
Compact single-line display of a quaternion
11951195
11961196
:arg q: unit-quaternion
11971197
:type q: array_like(4)
@@ -1219,15 +1219,26 @@ def qprint(
12191219
>>> q = qrand() # a unit quaternion
12201220
>>> qprint(q, delim=('<<', '>>'))
12211221
1222+
.. deprecated:: 1.1.15
1223+
``file=None`` to get the string back without printing is
1224+
deprecated - call :func:`~q2str` directly instead.
1225+
12221226
:seealso: :meth:`q2str`
12231227
"""
12241228
q = smb.getvector(q, 4)
1229+
s = q2str(q, delim=delim, fmt=fmt)
12251230
if file is None:
12261231
warnings.warn(
12271232
"Usage: qprint(..., file=None) -> str is deprecated, use q2str() instead",
12281233
DeprecationWarning,
12291234
)
1230-
print(q2str(q, delim=delim, fmt=fmt), file=file)
1235+
else:
1236+
# file=False (the default) resolves to None here so print() looks
1237+
# up the *current* sys.stdout at call time, not whatever it was
1238+
# when this function was defined - that's what makes
1239+
# contextlib.redirect_stdout() work.
1240+
print(s, file=None if file is False else file)
1241+
return s
12311242

12321243

12331244
if __name__ == "__main__": # pragma: no cover

‎spatialmath/base/transforms2d.py‎

Lines changed: 80 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import math
1919
import warnings
2020
import numpy as np
21+
import warnings
2122

2223
try:
2324
import matplotlib.pyplot as plt
@@ -976,22 +977,19 @@ def trinterp2(start, end, s, shortest: bool = True):
976977
raise ValueError("Argument must be SO(2) or SE(2)")
977978

978979

979-
def trprint2(
980+
def tr2str2(
980981
T: Union[SO2Array, SE2Array],
981982
label: str = "",
982-
file: TextIO = sys.stdout,
983983
fmt: str = "{:.3g}",
984984
unit: str = "deg",
985985
) -> str:
986986
"""
987-
Compact display of SE(2) or SO(2) matrices
987+
Convert SO(2) or SE(3) matrices to compact single-line string
988988
989989
:param T: matrix to format
990990
:type T: ndarray(3,3) or ndarray(2,2)
991991
:param label: text label to put at start of line
992992
:type label: str
993-
:param file: file to write formatted string to
994-
:type file: file object
995993
:param fmt: conversion format for each number
996994
:type fmt: str
997995
:param unit: angular units: 'rad' [default], or 'deg'
@@ -1002,12 +1000,12 @@ def trprint2(
10021000
The matrix is formatted and written to ``file`` and the
10031001
string is returned. To suppress writing to a file, set ``file=None``.
10041002
1005-
- ``trprint2(R)`` displays the SO(2) rotation matrix in a compact
1003+
- ``tr2str2(R)`` displays the SO(2) rotation matrix in a compact
10061004
single-line format and returns the string::
10071005
10081006
[LABEL:] θ UNIT
10091007
1010-
- ``trprint2(T)`` displays the SE(2) homogoneous transform in a compact
1008+
- ``tr2str2(T)`` displays the SE(2) homogoneous transform in a compact
10111009
single-line format and returns the string::
10121010
10131011
[LABEL:] [t=X, Y;] θ UNIT
@@ -1016,17 +1014,18 @@ def trprint2(
10161014
10171015
>>> from spatialmath.base import *
10181016
>>> T = transl2(1,2) @ trot2(0.3)
1019-
>>> trprint2(T, file=None, label='T')
1020-
>>> trprint2(T, file=None, label='T', fmt='{:8.4g}')
1021-
1017+
>>> tr2str2(T, label='T')
1018+
>>> tr2str2(T, label='T', fmt='{:8.4g}')
10221019
10231020
.. note::
10241021
10251022
- Default formatting is for compact display of data
10261023
- For tabular data set ``fmt`` to a fixed width format such as
10271024
``fmt='{:.3g}'``
10281025
1029-
:seealso: trprint
1026+
.. versionadded:: 1.1.15
1027+
1028+
:seealso: :func:`~tr2str`
10301029
"""
10311030

10321031
s = ""
@@ -1045,8 +1044,6 @@ def trprint2(
10451044
else:
10461045
s += " {} rad".format(_vec2s(fmt, [angle]))
10471046

1048-
if file:
1049-
print(s, file=file)
10501047
return s
10511048

10521049

@@ -1069,6 +1066,76 @@ def _vec2s(fmt: str, v: ArrayLikePure, tol: float = 20) -> str:
10691066
return ", ".join([fmt.format(x) for x in v])
10701067

10711068

1069+
def trprint2(
1070+
T: Union[SO2Array, SE2Array],
1071+
label: str = "",
1072+
file: TextIO = False,
1073+
**kwargs,
1074+
) -> str:
1075+
"""
1076+
Compact single-line display of SE(2) or SO(2) matrices
1077+
1078+
:param T: matrix to format
1079+
:type T: ndarray(3,3) or ndarray(2,2)
1080+
:param label: text label to put at start of line
1081+
:type label: str
1082+
:param file: file to write formatted string to [default is stdout]
1083+
:type file: file object
1084+
:param fmt: conversion format for each number
1085+
:type fmt: str
1086+
:param unit: angular units: 'rad' [default], or 'deg'
1087+
:type unit: str
1088+
:return: formatted string
1089+
:rtype: str
1090+
1091+
The matrix is formatted and written to ``file`` and the
1092+
string is returned. To suppress writing to a file, set ``file=None``.
1093+
1094+
- ``trprint2(R)`` displays the SO(2) rotation matrix in a compact
1095+
single-line format and returns the string::
1096+
1097+
[LABEL:] θ UNIT
1098+
1099+
- ``trprint2(T)`` displays the SE(2) homogoneous transform in a compact
1100+
single-line format and returns the string::
1101+
1102+
[LABEL:] [t=X, Y;] θ UNIT
1103+
1104+
.. runblock:: pycon
1105+
1106+
>>> from spatialmath.base import *
1107+
>>> T = transl2(1,2) @ trot2(0.3)
1108+
>>> trprint2(T, label='T')
1109+
>>> trprint2(T, label='T', fmt='{:8.4g}')
1110+
1111+
1112+
.. note::
1113+
1114+
- Default formatting is for compact display of data
1115+
- For tabular data set ``fmt`` to a fixed width format such as
1116+
``fmt='{:.3g}'``
1117+
1118+
.. deprecated:: 1.1.15
1119+
``file=None`` to get the string back without printing is
1120+
deprecated - call :func:`~tr2str2` directly instead.
1121+
1122+
:seealso: :func:`~tr2str2` :func:`~trprint`
1123+
"""
1124+
s = tr2str2(T, label=label, **kwargs)
1125+
if file is None:
1126+
warnings.warn(
1127+
"Usage: trprint2(..., file=None) -> str is deprecated, use tr2str2() instead",
1128+
DeprecationWarning,
1129+
)
1130+
else:
1131+
# file=False (the default) resolves to None here so print() looks
1132+
# up the *current* sys.stdout at call time, not whatever it was
1133+
# when this function was defined - that's what makes
1134+
# contextlib.redirect_stdout() work.
1135+
print(s, file=None if file is False else file)
1136+
return s
1137+
1138+
10721139
def points2tr2(p1: NDArray, p2: NDArray) -> SE2Array:
10731140
"""
10741141
SE(2) transform from corresponding points

0 commit comments

Comments
 (0)