Split tango.utils into a package

TL;DR

  • tango/utils.py (2216 lines, 130 top-level symbols) becomes a tango/utils/ package of twelve private submodules grouped by concern, with __init__.py re-exporting the public surface.
  • No definition is edited. __all__ is unchanged, every documented function keeps its import path, and no caller in the package or the tests needed a change.

Follow-up to #789. Draft: opened for agreement on the grouping before review of the detail.

Key Facts

  • The concerns are not entangled, which is what makes this worth doing. Treating each group as a module leaves three dependency edges — _typeinfo -> _types, _convert -> _types, _db -> _types, _convert — and no cycles. Nine of the twelve groups depend on nothing else in the file.
  • The move is verbatim and provable. All 128 definitions compare equal by AST dump against tango/utils.py at the merge base. Each one is moved with the comments that preceded it.
  • The import cycle is unchanged. utils pulls 26 names out of the partially-initialised tango package at module level. Submodules execute while tango/utils/__init__.py is being imported, which is the same moment tango/__init__.py imported .utils before, so they see the same partially-initialised package.
  • wheel.packages needed tango.utils added. Subpackages are listed explicitly there — tango.databaseds already is — so without it the new package would not ship. This is the one change that a reader could easily miss and that would only fail at install time.
  • LICENSE.txt attributed PSF-licensed code to tango/utils.py. The path is updated to tango/utils/_concurrency.py, where _adjust_thread_count now lives, and the attribution comment travels with the code.
  • tango/utils/__init__.py carries a file-level # ruff: noqa: F401. A re-export facade is all unused imports by construction; the alternative was a shared per-file-ignore in pyproject.toml, which would have relaxed the rule for every __init__.py in the tree.

Behaviour change

One, and it is deliberate. Names that tango/utils.py merely imported were incidentally reachable as tango.utils.<name> — CmdArgType, np, sys, StdStringVector, Release and 53 others. They are not re-exported. Nothing in the package or the test suite imported any of them from tango.utils, and the tango names are canonically available from tango itself.

Two carve-outs:

  • __auto_die_callbacks_holder is re-exported, because tests/test_device_proxy.py imports it from tango.utils. It is a container mutated in place, so re-exporting the object is correct.
  • __device_classes and __TEST_CONTEXT_HOST_TRL are not re-exported. Functions rebind them at runtime, and a facade binds values at import time, so a copy here would silently go stale. They stay in the submodule that owns them, where the global statements still work.

Objects now report a __module__ of tango.utils._<group> rather than tango.utils. The submodules are importable, so pickling and autofunction by explicit path are unaffected.

How this was checked

  • Surface comparison against a snapshot of dir(tango.utils) and __all__ taken before the split: every defined name still resolves, __all__ is identical and all 44 names resolve, every object has the same kind, every callable the same signature. The only additions are __path__ and the twelve submodules.
  • AST equality of all 128 definitions, old versus new.
  • Full suite: 1514 passed, 98 skipped, 53 xfailed, 1 xpassed, 0 failures — the same numbers as the merge base. The xpassed is test_dev_encoded_memory_usage, which the project marks "too fragile"; it is the only @pytest.mark.xfail in the suite and flips independently of this change.

Caveats

  • Git records this as one delete and thirteen adds, not a rename: at 450 of 2216 lines the largest new file is below any sane similarity threshold. git log --follow -- tango/utils.py reaches the pre-split history and git blame -C -C attributes the moved lines, since the content is unchanged. Flagged because it is the usual objection to a split of this shape.
  • The __all__ inconsistencies reported in #789 are untouched here on purpose: get_enum_labels and is_bool are documented but missing from __all__, and five names that their own docstrings call "internal use only" are listed in it. Resolving those changes the public surface, so it does not belong in a move that changes nothing.
  • Moving the genuinely-internal helpers out of tango.utils into something like tango/_internal/ is the natural next step, and needs the __all__ question settled first.

🤖 Generated with Claude Code

Merge request reports

Loading
Loading