Skip to content

Implement support for DLR get/set#2706

Merged
filmor merged 5 commits into
masterfrom
dlr
May 15, 2026
Merged

Implement support for DLR get/set#2706
filmor merged 5 commits into
masterfrom
dlr

Conversation

@filmor
Copy link
Copy Markdown
Member

@filmor filmor commented Apr 18, 2026

Implements #72 and should thus fix #2696.

@spkl Could you try out this branch?

@filmor filmor requested a review from lostmsu April 18, 2026 13:51
@filmor filmor force-pushed the dlr branch 3 times, most recently from 34eff30 to ac40869 Compare April 18, 2026 17:58
@filmor filmor marked this pull request as ready for review April 20, 2026 07:23
@spkl
Copy link
Copy Markdown

spkl commented Apr 20, 2026

@filmor If I understand this correctly, one wouldn't even need the dynamic.py script anymore for DynamicObject access to work, correct? That seems great :)
To test this in our setup, can you point me to a wheel that I can install, or provide documentation how to build it?

@filmor
Copy link
Copy Markdown
Member Author

filmor commented Apr 20, 2026

@spkl Yes, the dynamic support would be integrated. To build a wheel, the simplest way is to get uv and the .NET SDK installed on your machine, check out the repo and run uv build. That builds the wheel in dist/.

@spkl
Copy link
Copy Markdown

spkl commented Apr 20, 2026

OK so I did a little digging and found out that I can pip install . in the root directory of the pythonnet repository to install this version into my venv.
Edit: Is this wrong? Should I try again with uv?

Getting the value of dynamic properties seems to work, but I encountered a problem when setting dynamic properties. This is what I observed when running a script like this:

x = obj.MyProp
obj.MyProp = None
y = obj.MyProp

For line 1, TryGetMember is called for name "MyProp" and x will correctly be assigned the value returned from there.
For line 2, TrySetMember is NOT called.
For line 3, TryGetMember is NOT called, y will be assigned the value from line 2 (None), but that value is unknown to the .NET implementation.

Comment thread src/runtime/Types/ClassBase.cs Outdated
Comment thread src/runtime/Types/ClassBase.cs Outdated
Comment thread src/runtime/Types/ClassBase.cs Outdated
Comment thread src/runtime/Types/ClassBase.cs Outdated
Comment thread src/runtime/Types/ClassBase.cs Outdated
@filmor filmor force-pushed the dlr branch 4 times, most recently from fdfa219 to 6d1572e Compare May 5, 2026 19:56
@filmor
Copy link
Copy Markdown
Member Author

filmor commented May 6, 2026

@spkl Could you test again whether this works for you?

@spkl
Copy link
Copy Markdown

spkl commented May 6, 2026

Will do ASAP and let you know

@spkl
Copy link
Copy Markdown

spkl commented May 7, 2026

OK @filmor, this is pretty great. With the current version from this branch and with all previous workarounds removed, all of our test cases pass. 🎉

Performance is also improved, which I could see in our two dedicated performance tests. These tests don't strictly measure pythonnet performance, but that is a significant part of them. Numbers are normalized to fractions, lower is better.

Variant Test 1 Test 2
Python 3.12, pythonnet 3.0.5 + our own adaptation of filmors old DynamicObject workaround 1.0 1.0
Python 3.14, pythonnet 3.1.0rc0 + our own (problematic) workaround using codecs 1.18 1.04
Python 3.14, pythonnet with DLR support (this branch) 0.77 0.96

There is one significant behavioral difference that I observed: With past solutions, there was always an AttributeError when setting an attribute for which TrySetMember returns false (= which does not exist). With this branch, the attribute is now set on the Python side without any error. This can be problematic, because it can mask errors / typos in scripts and make troubleshooting harder. I don't know if this was intended. If it is, we can probably find a different solution.

I tried to find a workaround for that behavior and discovered something else where I'm not sure if it's correct: When throwing an exception in TrySetMember, the process crashes with a .NET exception, so there is no Python traceback to pinpoint the error location. In contrast, when throwing an exception in TryGetMember, it comes up as an AttributeError with Python traceback output.

Because we support running in different environments and Python versions, we probably need to implement automatic switching between DynamicObject workaround and first-class DLR support in pythonnet. Is there a way to find out if the currently loaded pythonnet module has DLR support? An information about the version would also work, but pythonnet currently has no __version__ attribute.

@filmor
Copy link
Copy Markdown
Member Author

filmor commented May 7, 2026

I'll try to handle these cases, definitely not intentional :)

If you need to get a package's version in your code, please use importlib.metadata.version().

@filmor filmor requested a review from lostmsu May 7, 2026 17:02
greateggsgreg added a commit to greateggsgreg/pythonnet that referenced this pull request May 9, 2026
The dynamic getter swallowed any exception from TryGetMember and
returned default to Python with the prior AttributeError still set,
so user code observed a misleading AttributeError instead of the real
failure.

Set a Python exception in the catch arm. We use RuntimeError with the
message string rather than Converter.ToPython(e) because wrapping the
CLR exception object can trigger type initialisation that re-enters
this same slot on the live dynamic object, producing infinite
recursion.

Mirrors the symmetry already present in the setter (pythonnet#2706 review,
@lostmsu) and adds a regression test alongside the existing
ThrowingSetDynamicObject coverage.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
greateggsgreg added a commit to greateggsgreg/pythonnet that referenced this pull request May 9, 2026
The dynamic getter swallowed any exception from TryGetMember and
returned default to Python with the prior AttributeError still set,
so user code observed a misleading AttributeError instead of the real
failure.

Set a Python exception in the catch arm. We use RuntimeError with the
message string rather than Converter.ToPython(e) because wrapping the
CLR exception object can trigger type initialisation that re-enters
this same slot on the live dynamic object, producing infinite
recursion.

Mirrors the symmetry already present in the setter (pythonnet#2706 review,
@lostmsu) and adds a regression test alongside the existing
ThrowingSetDynamicObject coverage.
@greateggsgreg
Copy link
Copy Markdown
Contributor

greateggsgreg commented May 10, 2026

#2718 should fix the TryGetMember issue highlighted above.

filmor pushed a commit that referenced this pull request May 11, 2026
The dynamic getter swallowed any exception from TryGetMember and
returned default to Python with the prior AttributeError still set,
so user code observed a misleading AttributeError instead of the real
failure.

Set a Python exception in the catch arm. We use RuntimeError with the
message string rather than Converter.ToPython(e) because wrapping the
CLR exception object can trigger type initialisation that re-enters
this same slot on the live dynamic object, producing infinite
recursion.

Mirrors the symmetry already present in the setter (#2706 review,
@lostmsu) and adds a regression test alongside the existing
ThrowingSetDynamicObject coverage.
@alexchandel
Copy link
Copy Markdown

You want to rebase this and retest with the TryGetMember fix?

greateggsgreg added a commit to greateggsgreg/pythonnet that referenced this pull request May 12, 2026
- Cache HasClrMember reflection per (Type, name) so tp_getattro_dlr_proxy
  / tp_setattro_dlr_proxy avoid repeated GetMember() calls on every
  attribute access of DLR-aware objects.

- Mirror tp_setattro_dlr_proxy's catch arm to the getter's safer
  SetError(RuntimeError, e.Message) shape instead of SetError(Exception),
  keeping both slots re-entry-safe on live dynamic objects.

Related to pythonnet#2706.
filmor pushed a commit that referenced this pull request May 12, 2026
The dynamic getter swallowed any exception from TryGetMember and
returned default to Python with the prior AttributeError still set,
so user code observed a misleading AttributeError instead of the real
failure.

Set a Python exception in the catch arm. We use RuntimeError with the
message string rather than Converter.ToPython(e) because wrapping the
CLR exception object can trigger type initialisation that re-enters
this same slot on the live dynamic object, producing infinite
recursion.

Mirrors the symmetry already present in the setter (#2706 review,
@lostmsu) and adds a regression test alongside the existing
ThrowingSetDynamicObject coverage.
filmor pushed a commit that referenced this pull request May 12, 2026
- Cache HasClrMember reflection per (Type, name) so tp_getattro_dlr_proxy
  / tp_setattro_dlr_proxy avoid repeated GetMember() calls on every
  attribute access of DLR-aware objects.

- Mirror tp_setattro_dlr_proxy's catch arm to the getter's safer
  SetError(RuntimeError, e.Message) shape instead of SetError(Exception),
  keeping both slots re-entry-safe on live dynamic objects.

Related to #2706.
filmor pushed a commit that referenced this pull request May 12, 2026
The dynamic getter swallowed any exception from TryGetMember and
returned default to Python with the prior AttributeError still set,
so user code observed a misleading AttributeError instead of the real
failure.

Set a Python exception in the catch arm. We use RuntimeError with the
message string rather than Converter.ToPython(e) because wrapping the
CLR exception object can trigger type initialisation that re-enters
this same slot on the live dynamic object, producing infinite
recursion.

Mirrors the symmetry already present in the setter (#2706 review,
@lostmsu) and adds a regression test alongside the existing
ThrowingSetDynamicObject coverage.
filmor pushed a commit that referenced this pull request May 12, 2026
- Cache HasClrMember reflection per (Type, name) so tp_getattro_dlr_proxy
  / tp_setattro_dlr_proxy avoid repeated GetMember() calls on every
  attribute access of DLR-aware objects.

- Mirror tp_setattro_dlr_proxy's catch arm to the getter's safer
  SetError(RuntimeError, e.Message) shape instead of SetError(Exception),
  keeping both slots re-entry-safe on live dynamic objects.

Related to #2706.
filmor pushed a commit that referenced this pull request May 13, 2026
The dynamic getter swallowed any exception from TryGetMember and
returned default to Python with the prior AttributeError still set,
so user code observed a misleading AttributeError instead of the real
failure.

Set a Python exception in the catch arm. We use RuntimeError with the
message string rather than Converter.ToPython(e) because wrapping the
CLR exception object can trigger type initialisation that re-enters
this same slot on the live dynamic object, producing infinite
recursion.

Mirrors the symmetry already present in the setter (#2706 review,
@lostmsu) and adds a regression test alongside the existing
ThrowingSetDynamicObject coverage.
filmor pushed a commit that referenced this pull request May 13, 2026
- Cache HasClrMember reflection per (Type, name) so tp_getattro_dlr_proxy
  / tp_setattro_dlr_proxy avoid repeated GetMember() calls on every
  attribute access of DLR-aware objects.

- Mirror tp_setattro_dlr_proxy's catch arm to the getter's safer
  SetError(RuntimeError, e.Message) shape instead of SetError(Exception),
  keeping both slots re-entry-safe on live dynamic objects.

Related to #2706.
@filmor
Copy link
Copy Markdown
Member Author

filmor commented May 13, 2026

@spkl Can you please test out the latest version?
@greateggsgreg I stole your pending commit that includes the caching, I hope that's fine :)
@lostmsu One last round of review? Once this is through, I would merge this and publish it as 3.1.0-rc2, we can discuss on a separate issue (after I update the Changelog to reflect the actual changes) whether we want to release it as 4.0. I don't want to delay the release any further :)

@spkl
Copy link
Copy Markdown

spkl commented May 13, 2026

@filmor All good from my side 👍

filmor and others added 5 commits May 15, 2026 21:57
- Catch exceptions in TrySet/DeleteMember
- Convert the exceptions into Python exceptions
- Add tests for the remaining cases
- Add a note on why the field has to be lazily initialized (general
  issue with derived classes)
The dynamic getter swallowed any exception from TryGetMember and
returned default to Python with the prior AttributeError still set,
so user code observed a misleading AttributeError instead of the real
failure.

Set a Python exception in the catch arm. We use RuntimeError with the
message string rather than Converter.ToPython(e) because wrapping the
CLR exception object can trigger type initialisation that re-enters
this same slot on the live dynamic object, producing infinite
recursion.

Mirrors the symmetry already present in the setter (#2706 review,
@lostmsu) and adds a regression test alongside the existing
ThrowingSetDynamicObject coverage.
- Cache HasClrMember reflection per (Type, name) so tp_getattro_dlr_proxy
  / tp_setattro_dlr_proxy avoid repeated GetMember() calls on every
  attribute access of DLR-aware objects.

- Mirror tp_setattro_dlr_proxy's catch arm to the getter's safer
  SetError(RuntimeError, e.Message) shape instead of SetError(Exception),
  keeping both slots re-entry-safe on live dynamic objects.

Related to #2706.
@filmor filmor merged commit 27caf5c into master May 15, 2026
29 of 30 checks passed
@filmor filmor deleted the dlr branch May 15, 2026 19:58
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.

DynamicObject support script no longer working since version 3.1.0rc0

5 participants