Skip to content

Commit 5d0d01a

Browse files
committed
safer GetAttr(name, default)
fixes pythonnet#1036
1 parent f857d59 commit 5d0d01a

File tree

3 files changed

+62
-11
lines changed

3 files changed

+62
-11
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ See [Mixins/collections.py](src/runtime/Mixins/collections.py).
7171
- BREAKING: When trying to convert Python `int` to `System.Object`, result will
7272
be of type `PyInt` instead of `System.Int32` due to possible loss of information.
7373
Python `float` will continue to be converted to `System.Double`.
74+
- BREAKING: `PyObject.GetAttr(name, default)` now only ignores `AttributeError` (previously ignored all exceptions).
7475
- BREAKING: `PyObject` no longer implements `IEnumerable<PyObject>`.
7576
Instead, `PyIterable` does that.
7677

src/embed_tests/TestPyObject.cs

+21
Original file line numberDiff line numberDiff line change
@@ -79,5 +79,26 @@ public void UnaryMinus_ThrowsOnBadType()
7979
var error = Assert.Throws<PythonException>(() => list = -list);
8080
Assert.AreEqual("TypeError", error.Type.Name);
8181
}
82+
83+
[Test]
84+
[Obsolete]
85+
public void GetAttrDefault_IgnoresAttributeErrorOnly()
86+
{
87+
var ob = new PyObjectTestMethods().ToPython();
88+
using var fallback = new PyList();
89+
var attrErrResult = ob.GetAttr(nameof(PyObjectTestMethods.RaisesAttributeError), fallback);
90+
Assert.IsTrue(PythonReferenceComparer.Instance.Equals(fallback, attrErrResult));
91+
92+
var typeErrResult = Assert.Throws<PythonException>(
93+
() => ob.GetAttr(nameof(PyObjectTestMethods.RaisesTypeError), fallback)
94+
);
95+
Assert.AreEqual(Exceptions.TypeError, typeErrResult.Type.Handle);
96+
}
97+
}
98+
99+
public class PyObjectTestMethods
100+
{
101+
public string RaisesAttributeError => throw new PythonException(new PyType(new BorrowedReference(Exceptions.AttributeError)), value: null, traceback: null);
102+
public string RaisesTypeError => throw new PythonException(new PyType(new BorrowedReference(Exceptions.TypeError)), value: null, traceback: null);
82103
}
83104
}

src/runtime/pyobject.cs

+40-11
Original file line numberDiff line numberDiff line change
@@ -309,21 +309,36 @@ public PyObject GetAttr(string name)
309309

310310

311311
/// <summary>
312-
/// GetAttr Method. Returns fallback value if getting attribute fails for any reason.
312+
/// Returns the named attribute of the Python object, or the given
313+
/// default object if the attribute access throws AttributeError.
313314
/// </summary>
314315
/// <remarks>
315-
/// Returns the named attribute of the Python object, or the given
316-
/// default object if the attribute access fails.
316+
/// This method ignores any AttrubiteError(s), even ones
317+
/// not raised due to missing requested attribute.
318+
///
319+
/// For example, if attribute getter calls other Python code, and
320+
/// that code happens to cause AttributeError elsewhere, it will be ignored
321+
/// and <paramref name="_default"/> value will be returned instead.
317322
/// </remarks>
323+
/// <param name="name">Name of the attribute.</param>
324+
/// <param name="_default">The object to return on AttributeError.</param>
325+
[Obsolete("See remarks")]
318326
public PyObject GetAttr(string name, PyObject _default)
319327
{
320328
if (name == null) throw new ArgumentNullException(nameof(name));
321329

322330
IntPtr op = Runtime.PyObject_GetAttrString(obj, name);
323331
if (op == IntPtr.Zero)
324332
{
325-
Runtime.PyErr_Clear();
326-
return _default;
333+
if (Exceptions.ExceptionMatches(Exceptions.AttributeError))
334+
{
335+
Runtime.PyErr_Clear();
336+
return _default;
337+
}
338+
else
339+
{
340+
throw PythonException.ThrowLastAsClrException();
341+
}
327342
}
328343
return new PyObject(op);
329344
}
@@ -351,22 +366,36 @@ public PyObject GetAttr(PyObject name)
351366

352367

353368
/// <summary>
354-
/// GetAttr Method
369+
/// Returns the named attribute of the Python object, or the given
370+
/// default object if the attribute access throws AttributeError.
355371
/// </summary>
356372
/// <remarks>
357-
/// Returns the named attribute of the Python object, or the given
358-
/// default object if the attribute access fails. The name argument
359-
/// is a PyObject wrapping a Python string or unicode object.
373+
/// This method ignores any AttrubiteError(s), even ones
374+
/// not raised due to missing requested attribute.
375+
///
376+
/// For example, if attribute getter calls other Python code, and
377+
/// that code happens to cause AttributeError elsewhere, it will be ignored
378+
/// and <paramref name="_default"/> value will be returned instead.
360379
/// </remarks>
380+
/// <param name="name">Name of the attribute. Must be of Python type 'str'.</param>
381+
/// <param name="_default">The object to return on AttributeError.</param>
382+
[Obsolete("See remarks")]
361383
public PyObject GetAttr(PyObject name, PyObject _default)
362384
{
363385
if (name == null) throw new ArgumentNullException(nameof(name));
364386

365387
IntPtr op = Runtime.PyObject_GetAttr(obj, name.obj);
366388
if (op == IntPtr.Zero)
367389
{
368-
Runtime.PyErr_Clear();
369-
return _default;
390+
if (Exceptions.ExceptionMatches(Exceptions.AttributeError))
391+
{
392+
Runtime.PyErr_Clear();
393+
return _default;
394+
}
395+
else
396+
{
397+
throw PythonException.ThrowLastAsClrException();
398+
}
370399
}
371400
return new PyObject(op);
372401
}

0 commit comments

Comments
 (0)