Skip to content

refactor: consistent printline() across pose and quaternion classes - #200

Open
petercorke wants to merge 2 commits into
rai-opensource:masterfrom
petercorke:refactor/printline
Open

refactor: consistent printline() across pose and quaternion classes#200
petercorke wants to merge 2 commits into
rai-opensource:masterfrom
petercorke:refactor/printline

Conversation

@petercorke

Copy link
Copy Markdown
Collaborator

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.

…Quaternion, UnitQuaternion types. In `base` now have separate functions to convert type to a single line string or to print it. Fixed problem where stdout redirection failed using `contextlib.redirect_stdout`.
Fixes discovered while packaging up old WIP work (commit 9dc38f9,
"Consistent operation of printline()...") for review. Three separate
bugs, all in that same unreleased WIP, none of it previously merged
or published:

1. trprint()/qprint() silently shifted `file` into the 2nd positional
   argument slot (`def trprint(T, file=False, **kwargs)`), when the
   original, published, external-user-facing signatures had `orient`/
   `delim` there instead. `trprint(T, 'angvec')` or the library's own
   documented `x.printline('angvec')` example put 'angvec' into `file`
   instead of `orient`, crashing with
   `AttributeError: 'str' object has no attribute 'write'`.
   Fixed by restoring the full original explicit parameter order for
   trprint/qprint (trprint2 was already fine here - its parameter
   order happened to be unaffected).

2. trprint2() captured `label` as an explicit parameter but its body
   never forwarded it to tr2str2() - `trprint2(T, label='T')` silently
   dropped the label from the output.

3. All three (trprint/trprint2/qprint) stopped returning the compact
   string entirely, only printing it - breaking any caller using the
   deprecated-but-still-supported `file=None` -> return string instead
   of printing usage, which several existing tests rely on
   (test_transforms3d.py::test_print, test_transforms2d.py::test_print2).

While fixing (3): the `file=None` "give me a string instead" pattern
is a wart worth phasing out rather than perpetuating silently - each
of these now has a real *2str()/q2str() sibling that does that
directly. So `file=None` is kept working (still returns the string,
still doesn't print) but now consistently raises a DeprecationWarning
and says so explicitly in the docstring via `.. deprecated::`,
pointing at tr2str()/tr2str2()/q2str() as the replacement.

All three now follow one consistent shape: always build the string via
the *2str()/q2str() sibling, always return it, print only when
file is not None (file=False, the default, resolves to None passed to
print() so contextlib.redirect_stdout() sees the current sys.stdout at
call time rather than whatever was captured when the function was
defined - the actual bug this WIP originally set out to fix).

Verified: file=None still returns the correct string with a
DeprecationWarning and no printing; the default path both prints
(confirmed via contextlib.redirect_stdout) and returns the same
string; full test suite green (338 passed, 4 skipped);
black --check clean at 23.10.0.
@codecov-commenter

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 86.95652% with 3 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
spatialmath/baseposematrix.py 0.00% 2 Missing ⚠️
spatialmath/quaternion.py 50.00% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@petercorke

Copy link
Copy Markdown
Collaborator Author

Merge-order suggestion across the currently open PRs, based on a full file-overlap check across all of them. Nothing here is a conflict with anything already merged — these are parallel PRs sharing some source files with each other, so a rebase will likely be needed wherever paths cross, regardless of order.

Recommended order:

  1. Fixes to base.plot_box() #179 (fix/base-plot-box) — already approved, CI green, ready now
  2. Fix/twist pitch clean #196 (fix/twist-pitch-clean) — isolated to twist.py
  3. test: force headless Matplotlib backend for local + CI test runs #197 (fix/headless-tests-locally) — isolated to test files only
  4. docs: fix Sphinx docstring formatting and reST syntax errors #195 (docs/sphinx-docstring-fixes) — touches ~18 files, overlaps most of the PRs below; docstring-only/low-risk, so merging it here means the functional PRs below each only rebase against it once
  5. feat(pose,quaternion): add __imatmul__ (@=) operator #199 (feat/imatmul) — only conflicts with refactor: consistent printline() across pose and quaternion classes #200
  6. fix(base): export tr2pos2, pos2tr2, tr2adjoint2; rename tradjoint2 #194 (fix/export-base-functions) — conflicts with perf: defer Matplotlib import until something actually plots #198 and refactor: consistent printline() across pose and quaternion classes #200
  7. perf: defer Matplotlib import until something actually plots #198 (perf/lazy-matplotlib-import) — conflicts with fix(base): export tr2pos2, pos2tr2, tr2adjoint2; rename tradjoint2 #194 and refactor: consistent printline() across pose and quaternion classes #200
  8. refactor: consistent printline() across pose and quaternion classes #200 (refactor/printline) — touches the most shared surface (conflicts with fix(base): export tr2pos2, pos2tr2, tr2adjoint2; rename tradjoint2 #194, perf: defer Matplotlib import until something actually plots #198, feat(pose,quaternion): add __imatmul__ (@=) operator #199); merging it last means it absorbs one final rebase instead of three others rebasing against it (this one)

Since this one lands last and touches the most shared surface, it'll very likely need a rebase once #194/#198/#199 are in — happy to do that rebase myself once they land, just flagging it now so it's not a surprise.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants