Skip to content

Implement hasattr builtin function#66

Open
friendlymatthew wants to merge 2 commits intomainfrom
hasattr
Open

Implement hasattr builtin function#66
friendlymatthew wants to merge 2 commits intomainfrom
hasattr

Conversation

@friendlymatthew
Copy link
Copy Markdown
Member

This PR implements hasattr(object,name) which returns True if the object has the named attribute, False otherwise. Unlike #65, this function never raises an AttributeError

This implementation calls py_get_attr() directly and converts the result to a boolean rather than reusing builtin_getattr(), since the functions have different signatures to validate

@codspeed-hq
Copy link
Copy Markdown

codspeed-hq bot commented Jan 23, 2026

Merging this PR will not alter performance

✅ 15 untouched benchmarks


Comparing hasattr (0e8a4ba) with main (23ff7eb)

Open in CodSpeed

Copy link
Copy Markdown
Collaborator

@davidhewitt davidhewitt left a comment

Choose a reason for hiding this comment

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

Thanks!

@davidhewitt davidhewitt enabled auto-merge (squash) April 13, 2026 14:38
Copy link
Copy Markdown

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 9 potential issues.

Open in Devin Review

Comment on lines +38 to +40
let Value::InternString(name_id) = name else {
return Err(SimpleException::new_msg(ExcType::TypeError, "hasattr(): attribute name must be string").into());
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 hasattr rejects heap-allocated strings as attribute name, unlike getattr

hasattr uses let Value::InternString(name_id) = name (line 38) which only matches interned string literals. The equivalent getattr implementation uses name.as_either_str(vm.heap) (crates/monty/src/builtins/getattr.rs:41) which correctly handles both Value::InternString and heap-allocated strings (Value::RefHeapData::Str). This means dynamically constructed strings (e.g., from concatenation like 'st' + 'art') passed as the attribute name to hasattr will incorrectly raise TypeError: attribute name must be string even though the value is a valid Python string.

Prompt for agents
In crates/monty/src/builtins/hasattr.rs, lines 38-43, the attribute name is checked with `let Value::InternString(name_id) = name` which only handles interned strings. It should instead use `name.as_either_str(vm.heap)` like getattr does in crates/monty/src/builtins/getattr.rs:41-46. This would handle both InternString and heap-allocated Str values. The pattern should be:

let Some(attr) = name.as_either_str(vm.heap) else {
    let ty = name.py_type(vm);
    return Err(SimpleException::new_msg(ExcType::TypeError, format!("attribute name must be string, not '{ty}'")).into());
};

Then use `&attr` instead of `&EitherStr::Interned(*name_id)` in the py_getattr call. You'll need to import PyTrait for py_type, and adjust the import of EitherStr if it's no longer used directly.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

//
// TODO: might need to support this case?
return Err(
SimpleException::new_msg(ExcType::TypeError, "getattr(): attribute is not a simple value").into(),
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Error message says "getattr()" instead of "hasattr()" due to copy-paste

Line 54 contains the error message "getattr(): attribute is not a simple value" but this is inside the builtin_hasattr function. This is a copy-paste error from the getattr implementation. The message should say "hasattr()" to correctly identify the calling function.

Suggested change
SimpleException::new_msg(ExcType::TypeError, "getattr(): attribute is not a simple value").into(),
SimpleException::new_msg(ExcType::TypeError, "hasattr(): attribute is not a simple value").into(),
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

hasattr(s, 123)
assert False, 'hasattr() with non-string name should raise TypeError'
except TypeError as e:
assert 'attribute name must be string' in str(e), 'Error message should mention string requirement'
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 Test uses assert 'x' in str(e) pattern which violates CLAUDE.md rule

CLAUDE.md explicitly states: "Do NOT Write tests like assert 'thing' in msg it's lazy and inexact unless explicitly told to do so, instead write tests like assert msg == 'expected message'". Line 44 uses assert 'attribute name must be string' in str(e) which is exactly this prohibited pattern.

Suggested change
assert 'attribute name must be string' in str(e), 'Error message should mention string requirement'
assert str(e) == 'hasattr(): attribute name must be string', 'Error message should mention string requirement'
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +25 to +26
except TypeError:
pass
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 Test catches TypeError without checking exception message (no-args case)

CLAUDE.md explicitly states: "IMPORTANT: don't just check that an exception is raised, you should always check the exception message." The try/except block at lines 22-26 catches TypeError for hasattr() with no args but only does pass without verifying str(exc).

Suggested change
except TypeError:
pass
except TypeError as e:
assert str(e) == 'hasattr expected 2 arguments, got 0', 'hasattr() no-args error message'
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +31 to +32
except TypeError:
pass
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 Test catches TypeError without checking exception message (1-arg case)

CLAUDE.md explicitly states: "IMPORTANT: don't just check that an exception is raised, you should always check the exception message." The try/except block at lines 28-32 catches TypeError for hasattr(s) but only does pass without verifying str(exc).

Suggested change
except TypeError:
pass
except TypeError as e:
assert str(e) == 'hasattr expected 2 arguments, got 1', 'hasattr() 1-arg error message'
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +37 to +38
except TypeError:
pass
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 Test catches TypeError without checking exception message (3-args case)

CLAUDE.md explicitly states: "IMPORTANT: don't just check that an exception is raised, you should always check the exception message." The try/except block at lines 34-38 catches TypeError for hasattr(s, 'start', 'extra') but only does pass without verifying str(exc).

Suggested change
except TypeError:
pass
except TypeError as e:
assert str(e) == 'hasattr expected 2 arguments, got 3', 'hasattr() 3-args error message'
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +49 to +50
except TypeError:
pass
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 Test catches TypeError without checking exception message (None name case)

CLAUDE.md explicitly states: "IMPORTANT: don't just check that an exception is raised, you should always check the exception message." The try/except block at lines 46-50 catches TypeError for hasattr(s, None) but only does pass without verifying str(exc).

Suggested change
except TypeError:
pass
except TypeError as e:
assert str(e) == 'hasattr(): attribute name must be string', 'hasattr() None-name error message'
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

SimpleException::new_msg(ExcType::TypeError, "getattr(): attribute is not a simple value").into(),
);
}
Err(_) => false,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚩 hasattr swallows all exception types, not just AttributeError

In CPython, hasattr(obj, name) calls getattr(obj, name) and catches only AttributeError — any other exception type propagates to the caller. The Monty implementation at crates/monty/src/builtins/hasattr.rs:57 uses Err(_) => false which catches ALL error types indiscriminately and returns False. This means that if py_getattr raises something other than AttributeError (e.g., a RuntimeError from a custom __getattr__), hasattr would silently return False instead of propagating the exception. This is a semantic divergence from CPython's behavior. Currently this may not be triggerable since Monty doesn't support custom __getattr__ on user classes, but it's worth noting for when that support is added.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +48 to +56
Ok(_) => {
// hasattr() only tests attribute values — OS calls, external calls,
// method calls, and awaits are not supported here
//
// TODO: might need to support this case?
return Err(
SimpleException::new_msg(ExcType::TypeError, "getattr(): attribute is not a simple value").into(),
);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚩 Ok(_) branch in hasattr raises TypeError instead of returning False

The Ok(_) branch at crates/monty/src/builtins/hasattr.rs:48-56 handles cases where py_getattr returns a non-Value CallResult (e.g., an OS call or external function). In this case, hasattr raises a TypeError. This is debatable — since the attribute technically exists (getattr succeeded), returning True might be more correct Python semantics. The TODO comment acknowledges uncertainty. This is unlikely to be hit in practice for typical hasattr use cases, but if it does get hit, it would be surprising to get a TypeError from hasattr() which is documented as never raising exceptions other than for bad arguments.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@davidhewitt davidhewitt disabled auto-merge April 13, 2026 14:42
@github-actions
Copy link
Copy Markdown

Codecov Results 📊

✅ Patch coverage is 84.21%. Project has 4126 uncovered lines.
✅ Project coverage is 86.45%. Comparing base (base) to head (head).

Files with missing lines (1)
File Patch % Lines
crates/monty/src/builtins/hasattr.rs 83.33% ⚠️ 3 Missing and 1 partials
Coverage diff
@@            Coverage Diff             @@
##          main       #PR       +/-##
==========================================
+ Coverage    83.78%    86.45%    +2.67%
==========================================
  Files          118       119        +1
  Lines        31354     30456      -898
  Branches     71312     65491     -5821
==========================================
+ Hits         26269     26330       +61
- Misses        5085      4126      -959
- Partials      3408      1925     -1483

Generated by Codecov Action

@codecov
Copy link
Copy Markdown

codecov bot commented Apr 13, 2026

Codecov Report

❌ Patch coverage is 78.94737% with 4 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
crates/monty/src/builtins/hasattr.rs 77.77% 3 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

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