Skip to content

Conversation

@Andy-Jost
Copy link
Contributor

Summary

  • Add __repr__ methods to Stream, Event, Context, Buffer, ObjectCode, and Kernel
  • Create test_object_protocols.py consolidating tests from removed files
  • Remove test_hashable.py, test_comparable.py, test_weakref.py

Changes

  • __repr__ implementations: All core API classes now have informative repr output (e.g., Stream(handle=0x...), Buffer(ptr=0x..., size=64))
  • Consolidated test module with 152 tests covering:
    • Weak references (weakref.ref, WeakValueDict, WeakKeyDict, WeakSet)
    • Hash consistency, distinctness across types, and small value guards
    • Equality (reflexive, cross-type, same-type distinct objects)
    • Collection usage (dict, set, weak collections)
    • repr format validation with regex patterns

Test Coverage

  • 152 parameterized tests (148 pass, 4 skip on single-GPU machines)
  • All tests run locally and pass

@Andy-Jost Andy-Jost added the cuda.core Everything related to the cuda.core module label Jan 28, 2026
@Andy-Jost Andy-Jost self-assigned this Jan 28, 2026
@copy-pr-bot
Copy link
Contributor

copy-pr-bot bot commented Jan 28, 2026

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@Andy-Jost
Copy link
Contributor Author

/ok to test 4828262

@github-actions
Copy link

- Add __repr__ methods to Stream, Event, Context, Buffer, ObjectCode, Kernel
- Create test_object_protocols.py with comprehensive tests for:
  - Weak references (weakref.ref, WeakValueDict, WeakKeyDict, WeakSet)
  - Hash consistency, distinctness, and small value guards
  - Equality (reflexive, cross-type, same-type distinct objects)
  - Collection usage (dict, set, weak collections)
  - repr format validation with regex patterns
- Remove test_hashable.py, test_comparable.py, test_weakref.py (consolidated)
@Andy-Jost Andy-Jost force-pushed the object-protocol-tests branch from 4828262 to 197da96 Compare January 28, 2026 20:06
@Andy-Jost
Copy link
Contributor Author

/ok to test 197da96

1 similar comment
@Andy-Jost
Copy link
Contributor Author

/ok to test 197da96

@Andy-Jost
Copy link
Contributor Author

/ok to test 763005c

@rwgk
Copy link
Collaborator

rwgk commented Jan 28, 2026

Logging a Cursor prompt/response below. What do you think about the "middle ground" approach?


__repr__ vs __str__ - Mainstream Wisdom vs LSC Experience

Question

From my days working at Google on "Large Scale Changes", i.e. often touching thousands of files across a gigantic codebase, I developed the opinion: keep the default __repr__, but implement __str__. This is so that it's easy to see what type an object is when e.g. debug-printing a list or dict, or especially nested lists and dicts. What's the current mainstream wisdom in this regard?

Response

That's an interesting perspective. The mainstream Python wisdom actually goes the opposite direction:

The conventional advice (from Python docs, Raymond Hettinger, etc.) is:

  1. Always implement __repr__ - it should be unambiguous and ideally eval()-able to recreate the object
  2. __str__ is optional - implement it only if you want a "pretty" user-facing string that differs from __repr__

The rationale is that __repr__ is used in more contexts automatically (debuggers, logging, interactive REPL, and as the fallback for __str__), so it's the more important one to get right.

However, your concern is valid and points to a real tradeoff:

  • Default __repr__ gives you: <cuda.core._stream.Stream object at 0x7f...>
  • Custom __repr__ gives you: Stream(handle=0x7f...)

In nested structures, the default makes the type immediately obvious because of the full module path. The custom __repr__ loses that context - if you're looking at a list of mixed objects, you might see Stream(...) but not immediately know it's cuda.core.Stream vs some other Stream.

A middle ground some projects use:

def __repr__(self):
    return f"<{self.__class__.__module__}.{self.__class__.__name__} handle={...:#x}>"

This preserves the angle-bracket style and module path while adding useful state info.

That said, I think the mainstream has largely settled on "implement __repr__, let __str__ fall back to it" - your Google LSC experience may have given you a perspective that's more informed by large-scale debugging scenarios than typical library design.

Copy link
Collaborator

@rwgk rwgk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor noted:

Inconsistent __repr__ styles

  • Device: <Device 0 (NVIDIA GeForce...)> (angle brackets, no handle=)
  • Stream: Stream(handle=0x...) (constructor-style)
  • LaunchConfig: LaunchConfig(grid=..., cluster=..., ...) (constructor-style, all attrs)

Are these design choices? Could we make the fully, or maybe more, uniform?


def __repr__(self) -> str:
# Trigger lazy load to get the handle
self._lazy_load_module()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm ... this could trigger "interesting" error scenarios.

Is the lazy loading really needed? How was the handle generated (if it exists)? Didn't that trigger the import already? If the handle does not exist, maybe we should just show <not imported> or similar in the repr, because that's what the actual state is?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My 2c: trigger lazy execution on __repr__ is a relatively common pattern in Python where I think this makes sense.

"""Distinct objects of the same type have different hashes."""
obj_a = request.getfixturevalue(a_name)
obj_b = request.getfixturevalue(b_name)
assert hash(obj_a) != hash(obj_b), f"{a_name} and {b_name} have same hash but different handles"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general hash collisions are allowed by the hash contract - only equal objects must have equal hashes, not the converse. Are you deliberately making the requirements stronger for the cuda.core types? If yes, could you please add a comment to explain?

"""Hash should not be a small number (guards against returning IDs or indices)."""
obj = request.getfixturevalue(fixture_name)
h = hash(obj)
assert abs(h) >= 10, f"hash {h} is suspiciously small"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, what if someone has 10+ GPUs and tests Device(9), this would pass even if it were returning the device ID. Also, in general legitimate hash values can be small.

I'm thinking this test isn't really needed. I think it's better to limit testing to what we're sure must be true, e.g. the hash for objects that are equal must be equal, too, but I wouldn't assert anything on the hash value itself.

obj_b = request.getfixturevalue(b_name)
assert obj_a is not obj_b, f"{a_name} and {b_name} are the same object"
assert obj_a != obj_b, f"{a_name} == {b_name} but they have different handles"

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor suggested:

Missing test for equality of same objects created differently

The tests check that distinct objects are not equal, but don't verify that two wrapper objects pointing to the same underlying handle compare equal. For example:

stream = device.create_stream()
stream2 = Stream.from_handle(stream.handle)
assert stream == stream2  # This case isn't tested

@pytest.fixture
def sample_context_alt(sample_device_alt):
"""An alternate Context object (requires multi-GPU)."""
sample_device_alt.set_current()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This calls set_current() which changes global state. The fixture doesn't restore the previous current device. Is this future-safe (what comes to mind: multiple GPUs and running tests in parallel)?

return hash((as_intptr(self._h_ptr), self._size))

def __repr__(self) -> str:
return f"Buffer(ptr={as_intptr(self._h_ptr):#x}, size={self._size})"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given we have a stream associated to the Buffer that we ultimately deallocate with, it may be nice to include that in the repr?


def __repr__(self) -> str:
# Trigger lazy load to get the handle
self._lazy_load_module()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My 2c: trigger lazy execution on __repr__ is a relatively common pattern in Python where I think this makes sense.

return as_intptr(self._h_stream) == as_intptr((<Stream>other)._h_stream)

def __repr__(self) -> str:
return f"Stream(handle={as_intptr(self._h_stream):#x})"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because streams are tied to a specific context, does it make sense to capture the context here as well? Similar question for other types tied to a specific context.

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

Labels

cuda.core Everything related to the cuda.core module

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants