From 8215a7ba34d8e93deea8326cb932725757c1e887 Mon Sep 17 00:00:00 2001 From: goodboy Date: Mon, 9 Feb 2026 12:20:17 -0500 Subject: [PATCH 1/3] Hide private fields in `Struct.pformat()` output Skip fields starting with `_` in pretty-printed struct output to avoid cluttering displays with internal/private state (and/or accessing private properties which have errors Bp). Deats, - add `if k[0] == '_': continue` check to skip private fields - change nested `if isinstance(v, Struct)` to `elif` since we now have early-continue for private fields - mv `else:` comment to clarify it handles top-level fields - fix indentation of `yield` statement to only output non-private, non-nested fields (this commit msg was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-code --- tractor/msg/pretty_struct.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/tractor/msg/pretty_struct.py b/tractor/msg/pretty_struct.py index 169cb461..dfc8a944 100644 --- a/tractor/msg/pretty_struct.py +++ b/tractor/msg/pretty_struct.py @@ -126,13 +126,17 @@ def iter_struct_ppfmt_lines( str(ft) ).replace(' ', '') + if k[0] == '_': + continue + # recurse to get sub-struct's `.pformat()` output Bo - if isinstance(v, Struct): + elif isinstance(v, Struct): yield from iter_struct_ppfmt_lines( struct=v, field_indent=field_indent+field_indent, ) - else: + + else: # top-level field val_str: str = repr(v) # XXX LOL, below just seems to be f#$%in causing @@ -149,10 +153,10 @@ def iter_struct_ppfmt_lines( # raise # return _Struct.__repr__(struct) - yield ( - ' '*field_indent, # indented ws prefix - f'{k}: {typ_name} = {val_str},', # field's repr line content - ) + yield ( + ' '*field_indent, # indented ws prefix + f'{k}: {typ_name} = {val_str},', # field's repr line content + ) def pformat( From 1529095c32f01658bb6f7bb555843a5837dcdf0a Mon Sep 17 00:00:00 2001 From: goodboy Date: Thu, 12 Mar 2026 18:32:31 -0400 Subject: [PATCH 2/3] Add `tests/msg/` sub-pkg, audit `pformat()` filtering Reorganize existing msg-related test suites under a new `tests/msg/` subdir (matching `tests/devx/` and `tests/ipc/` convention) and add unit tests for the `_`-prefixed field filtering in `pformat()`. Deats, - `git mv` `test_ext_types_msgspec` and `test_pldrx_limiting` into `tests/msg/`. - add `__init__.py` + `conftest.py` for the new test sub-pkg. - add new `test_pretty_struct.py` suite with 8 unit tests: - parametrized field visibility (public shown, `_`-private hidden, mixed) - direct `iter_struct_ppfmt_lines()` assertion - nested struct recursion filtering - empty struct edge case - real `MsgDec` via `mk_dec()` hiding `_dec` - `repr()` integration via `Struct.__repr__()` (this patch was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-code --- tests/msg/__init__.py | 4 + tests/msg/conftest.py | 4 + tests/{ => msg}/test_ext_types_msgspec.py | 0 tests/{ => msg}/test_pldrx_limiting.py | 0 tests/msg/test_pretty_struct.py | 244 ++++++++++++++++++++++ 5 files changed, 252 insertions(+) create mode 100644 tests/msg/__init__.py create mode 100644 tests/msg/conftest.py rename tests/{ => msg}/test_ext_types_msgspec.py (100%) rename tests/{ => msg}/test_pldrx_limiting.py (100%) create mode 100644 tests/msg/test_pretty_struct.py diff --git a/tests/msg/__init__.py b/tests/msg/__init__.py new file mode 100644 index 00000000..19f1d863 --- /dev/null +++ b/tests/msg/__init__.py @@ -0,0 +1,4 @@ +''' +`tractor.msg.*` sub-sys test suite. + +''' diff --git a/tests/msg/conftest.py b/tests/msg/conftest.py new file mode 100644 index 00000000..4775480c --- /dev/null +++ b/tests/msg/conftest.py @@ -0,0 +1,4 @@ +''' +`tractor.msg.*` test sub-pkg conf. + +''' diff --git a/tests/test_ext_types_msgspec.py b/tests/msg/test_ext_types_msgspec.py similarity index 100% rename from tests/test_ext_types_msgspec.py rename to tests/msg/test_ext_types_msgspec.py diff --git a/tests/test_pldrx_limiting.py b/tests/msg/test_pldrx_limiting.py similarity index 100% rename from tests/test_pldrx_limiting.py rename to tests/msg/test_pldrx_limiting.py diff --git a/tests/msg/test_pretty_struct.py b/tests/msg/test_pretty_struct.py new file mode 100644 index 00000000..4b13d72a --- /dev/null +++ b/tests/msg/test_pretty_struct.py @@ -0,0 +1,244 @@ +''' +Unit tests for `tractor.msg.pretty_struct` +private-field filtering in `pformat()`. + +''' +from typing import ( + Any, +) + +import pytest + +from tractor.msg.pretty_struct import ( + Struct, + pformat, + iter_struct_ppfmt_lines, +) +from tractor.msg._codec import ( + MsgDec, + mk_dec, +) + + +# ------ test struct definitions ------ # + +class PublicOnly(Struct): + ''' + All-public fields for baseline testing. + + ''' + name: str = 'alice' + age: int = 30 + + +class PrivateOnly(Struct): + ''' + Only underscore-prefixed (private) fields. + + ''' + _secret: str = 'hidden' + _internal: int = 99 + + +class MixedFields(Struct): + ''' + Mix of public and private fields. + + ''' + name: str = 'bob' + _hidden: int = 42 + value: float = 3.14 + _meta: str = 'internal' + + +class Inner( + Struct, + frozen=True, +): + ''' + Frozen inner struct with a private field, + for nesting tests. + + ''' + x: int = 1 + _secret: str = 'nope' + + +class Outer(Struct): + ''' + Outer struct nesting an `Inner`. + + ''' + label: str = 'outer' + inner: Inner = Inner() + + +class EmptyStruct(Struct): + ''' + Struct with zero fields. + + ''' + pass + + +# ------ tests ------ # + +@pytest.mark.parametrize( + 'struct_and_expected', + [ + ( + PublicOnly(), + { + 'shown': ['name', 'age'], + 'hidden': [], + }, + ), + ( + MixedFields(), + { + 'shown': ['name', 'value'], + 'hidden': ['_hidden', '_meta'], + }, + ), + ( + PrivateOnly(), + { + 'shown': [], + 'hidden': ['_secret', '_internal'], + }, + ), + ], + ids=[ + 'all-public', + 'mixed-pub-priv', + 'all-private', + ], +) +def test_field_visibility_in_pformat( + struct_and_expected: tuple[ + Struct, + dict[str, list[str]], + ], +): + ''' + Verify `pformat()` shows public fields + and hides `_`-prefixed private fields. + + ''' + ( + struct, + expected, + ) = struct_and_expected + output: str = pformat(struct) + + for field_name in expected['shown']: + assert field_name in output, ( + f'{field_name!r} should appear in:\n' + f'{output}' + ) + + for field_name in expected['hidden']: + assert field_name not in output, ( + f'{field_name!r} should NOT appear in:\n' + f'{output}' + ) + + +def test_iter_ppfmt_lines_skips_private(): + ''' + Directly verify `iter_struct_ppfmt_lines()` + never yields tuples with `_`-prefixed field + names. + + ''' + struct = MixedFields() + lines: list[tuple[str, str]] = list( + iter_struct_ppfmt_lines( + struct, + field_indent=2, + ) + ) + # should have lines for public fields only + assert len(lines) == 2 + + for _prefix, line_content in lines: + field_name: str = ( + line_content.split(':')[0].strip() + ) + assert not field_name.startswith('_'), ( + f'private field leaked: {field_name!r}' + ) + + +def test_nested_struct_filters_inner_private(): + ''' + Verify that nested struct's private fields + are also filtered out during recursion. + + ''' + outer = Outer() + output: str = pformat(outer) + + # outer's public field + assert 'label' in output + + # inner's public field (recursed into) + assert 'x' in output + + # inner's private field must be hidden + assert '_secret' not in output + + +def test_empty_struct_pformat(): + ''' + An empty struct should produce a valid + `pformat()` result with no field lines. + + ''' + output: str = pformat(EmptyStruct()) + assert 'EmptyStruct(' in output + assert output.rstrip().endswith(')') + + # no field lines => only struct header+footer + lines: list[tuple[str, str]] = list( + iter_struct_ppfmt_lines( + EmptyStruct(), + field_indent=2, + ) + ) + assert lines == [] + + +def test_real_msgdec_pformat_hides_private(): + ''' + Verify `pformat()` on a real `MsgDec` + hides the `_dec` internal field. + + NOTE: `MsgDec.__repr__` is custom and does + NOT call `pformat()`, so we call it directly. + + ''' + dec: MsgDec = mk_dec(spec=int) + output: str = pformat(dec) + + # the private `_dec` field should be filtered + assert '_dec' not in output + + # but the struct type name should be present + assert 'MsgDec(' in output + + +def test_pformat_repr_integration(): + ''' + Verify that `Struct.__repr__()` (which calls + `pformat()`) also hides private fields for + custom structs that do NOT override `__repr__`. + + ''' + mixed = MixedFields() + output: str = repr(mixed) + + assert 'name' in output + assert 'value' in output + assert '_hidden' not in output + assert '_meta' not in output From abbb4a79c811cd78335015e2cf5001ac3bc9a12b Mon Sep 17 00:00:00 2001 From: goodboy Date: Fri, 13 Mar 2026 11:52:18 -0400 Subject: [PATCH 3/3] Drop unused import noticed by `copilot` --- tests/msg/test_pretty_struct.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/msg/test_pretty_struct.py b/tests/msg/test_pretty_struct.py index 4b13d72a..3bf4eefe 100644 --- a/tests/msg/test_pretty_struct.py +++ b/tests/msg/test_pretty_struct.py @@ -3,10 +3,6 @@ Unit tests for `tractor.msg.pretty_struct` private-field filtering in `pformat()`. ''' -from typing import ( - Any, -) - import pytest from tractor.msg.pretty_struct import (