Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

THRIFT-5715: Python non-user defined fields mutable with slots #2816

Merged
merged 1 commit into from Jul 7, 2023

Conversation

KTAtkinson
Copy link
Contributor

@KTAtkinson KTAtkinson commented Jun 12, 2023

In Python 3.11 exceptions generated by the compiler can't be used with a
context manager because they are immutable. As of Python 3.11
contextlib.contextmanager sets exc.__traceback__ in the event that
the code in the context manager errors.

As of Thrift v0.18.1 exceptions are generated as immutable by default.
See PR#1835 for more information about why exceptions were made
immutable by default.

This change makes all non-Thrift fields mutable when slots is used without dynamic. This
will allow exceptions to be re-raised properly by the contextmanager in
Python 3.11.

With tutorial code generated from head this raises a TypeError not
an InvalidOperation:

import contextlib
import sys

sys.path.append('gen-py')
from tutorial.ttypes import InvalidOperation

@contextlib.contextmanager
def example():
    yield
    return

def main():
    with example():
        raise InvalidOperation

if __name__ == "__main__":
    main()

It raises this error:

root@b81016c354b4:/src/thrift/tutorial/py# python3 contextmanager.py 
Traceback (most recent call last):
  File "/src/thrift/tutorial/py/contextmanager.py", line 5, in <module>
    from tutorial.ttypes import InvalidOperation
ModuleNotFoundError: No module named 'tutorial.ttypes'
root@b81016c354b4:/src/thrift/tutorial/py# python3 contextmanager.py 
Traceback (most recent call last):
  File "/usr/local/lib/python3.11/contextlib.py", line 155, in __exit__
    self.gen.throw(typ, value, traceback)
  File "/src/thrift/tutorial/py/contextmanager.py", line 9, in example
    yield
  File "/src/thrift/tutorial/py/contextmanager.py", line 14, in main
    raise InvalidOperation
tutorial.ttypes.InvalidOperation: InvalidOperation(whatOp=None, why=None)

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/src/thrift/tutorial/py/contextmanager.py", line 17, in <module>
    main()
  File "/src/thrift/tutorial/py/contextmanager.py", line 13, in main
    with example():
  File "/usr/local/lib/python3.11/contextlib.py", line 188, in __exit__
    exc.__traceback__ = traceback
    ^^^^^^^^^^^^^^^^^
  File "/src/thrift/tutorial/py/gen-py/tutorial/ttypes.py", line 160, in __setattr__
    raise TypeError("can't modify immutable instance")
TypeError: can't modify immutable instance

The error after this change without slots, this error still happens:

root@daf9a41282a7:/src/thrift/tutorial/py# python3 ./contextmanager.py 
Traceback (most recent call last):
  File "/src/thrift/tutorial/py/./contextmanager.py", line 17, in <module>
    main()
  File "/src/thrift/tutorial/py/./contextmanager.py", line 14, in main
    raise InvalidOperation
tutorial.ttypes.InvalidOperation: InvalidOperation(whatOp=None, why=None)
root@daf9a41282a7:/src/thrift/tutorial/py# python3 ./contextmanager.py 
Traceback (most recent call last):
  File "/usr/local/lib/python3.11/contextlib.py", line 155, in __exit__
    self.gen.throw(typ, value, traceback)
  File "/src/thrift/tutorial/py/./contextmanager.py", line 9, in example
    yield
  File "/src/thrift/tutorial/py/./contextmanager.py", line 14, in main
    raise InvalidOperation
tutorial.ttypes.InvalidOperation: InvalidOperation(whatOp=None, why=None)

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/src/thrift/tutorial/py/./contextmanager.py", line 17, in <module>
    main()
  File "/src/thrift/tutorial/py/./contextmanager.py", line 13, in main
    with example():
  File "/usr/local/lib/python3.11/contextlib.py", line 188, in __exit__
    exc.__traceback__ = traceback
    ^^^^^^^^^^^^^^^^^
  File "/src/thrift/tutorial/py/gen-py/tutorial/ttypes.py", line 160, in __setattr__
    raise TypeError("can't modify immutable instance")
TypeError: can't modify immutable instance

After change with slots:

root@b81016c354b4:/src/thrift/tutorial/py# python3 contextmanager.py 
Traceback (most recent call last):
  File "/src/thrift/tutorial/py/contextmanager.py", line 17, in <module>
    main()
  File "/src/thrift/tutorial/py/contextmanager.py", line 14, in main
    raise InvalidOperation
tutorial.ttypes.InvalidOperation: InvalidOperation(whatOp=None, why=None)

There is no diff in the generated code when slots is not used.

Here is the diff between thrift at head and this branch generating with slots:

src# diff -C 5 -r tutorial/gen-py-pr/head-slots/ tutorial/gen-py-pr/dev-slots/
diff -C 5 -r tutorial/gen-py-pr/head-slots/tutorial/ttypes.py tutorial/gen-py-pr/dev-slots/tutorial/ttypes.py
*** tutorial/gen-py-pr/head-slots/tutorial/ttypes.py    Thu Jun 29 16:03:16 2023
--- tutorial/gen-py-pr/dev-slots/tutorial/ttypes.py     Thu Jun 29 16:19:11 2023
***************
*** 174,186 ****
--- 174,192 ----
      def __init__(self, whatOp=None, why=None,):
          super(InvalidOperation, self).__setattr__('whatOp', whatOp)
          super(InvalidOperation, self).__setattr__('why', why)
  
      def __setattr__(self, *args):
+         if args[0] not in self.__slots__:
+             super().__setattr__(*args)
+             return
          raise TypeError("can't modify immutable instance")
  
      def __delattr__(self, *args):
+         if args[0] not in self.__slots__:
+             super().__delattr__(*args)
+             return
          raise TypeError("can't modify immutable instance")
  
      def __hash__(self):
          return hash(self.__class__) ^ hash((self.whatOp, self.why, ))
  • Did you create an Apache Jira ticket? (Request account here, not required for trivial changes)
  • If a ticket exists: Does your pull request title follow the pattern "THRIFT-NNNN: describe my issue"?
  • Did you squash your changes to a single commit? (not required, but preferred)
  • Did you do your best to avoid breaking changes? If one was needed, did you label the Jira ticket with "Breaking-Change"?
  • If your change does not involve any code, include [skip ci] anywhere in the commit message to free up build resources.

@KTAtkinson KTAtkinson changed the title THRIFT-5715 Python flag to generate mutible exceptions THRIFT-5715: Python flag to generate mutible exceptions Jun 12, 2023
@KTAtkinson KTAtkinson force-pushed the katie-atkinson/py-mutable-errs branch 2 times, most recently from 3d1ba71 to 6bd6ace Compare June 14, 2023 21:49
@KTAtkinson KTAtkinson changed the title THRIFT-5715: Python flag to generate mutible exceptions THRIFT-5715: Python flag to generate mutable exceptions Jun 14, 2023
@KTAtkinson
Copy link
Contributor Author

appvayor is currently failing, but it's also failing on all other PRs. Is this expected?

@KTAtkinson KTAtkinson marked this pull request as ready for review June 14, 2023 21:51
@@ -105,7 +106,9 @@ class t_py_generator : public t_generator {
}
if( import_dynbase_.empty()) {
import_dynbase_ = "from thrift.protocol.TBase import TBase, TFrozenBase, TExceptionBase, TFrozenExceptionBase, TTransport\n";
}
}
} else if( iter->first.compare("immutable_exc") == 0) {
Copy link
Member

@fishy fishy Jun 14, 2023

Choose a reason for hiding this comment

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

shouldn't this be called mutable_exc or allow_mutable_exc instead? otherwise you are making gen_immutable_exc_ to false when user pass in immutable_exc arg to the compiler.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yea this naming is a bit confusing, I'll update it :)

@fishy
Copy link
Member

fishy commented Jun 14, 2023

appvayor is currently failing, but it's also failing on all other PRs. Is this expected?

kind of.

but I think a better fix is to limit the scope of immutability of exceptions. we only need to limit the fields used by __hash__ and __eq__ to be immutable, and allow mutations of all other fields.

@KTAtkinson KTAtkinson force-pushed the katie-atkinson/py-mutable-errs branch 2 times, most recently from 402f505 to ec45983 Compare June 16, 2023 19:44
@KTAtkinson
Copy link
Contributor Author

I updated this so that the generated python only considers fields in the __dict__ or __slots__ field when Thrift types are immutable.

@KTAtkinson KTAtkinson requested a review from fishy June 16, 2023 20:43
Copy link
Member

@fishy fishy left a comment

Choose a reason for hiding this comment

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

@KTAtkinson can you please also paste what the new generated python exception code look like (with and without slots)?

@@ -256,6 +257,19 @@ def testMultiException(self):
y = self.client.testMultiException('success', 'foobar')
self.assertEqual(y.string_thing, 'foobar')

def testExceptionInContextManager(self):
Copy link
Member

Choose a reason for hiding this comment

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

We don't really run python 3.11 in github actions so this test doesn't really verify that it fixes the issue you are seeing right now, but it will eventually.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think there should be some sort of test here. I originally wrote the test testing the functionality directly, assigning and deleting particular attributes. I wasn't able to write a test to ensure that user defined attributes are not editable as in some cases they are editable.

My thinking with this test case is that it will at least catch the specific failure when Thrift is updated to use Python 3.11. I did try to run this test with 3.11 but there are parts of the test script that aren't compatible with 3.11.

Thoughts on how to effectively write at test here?

if (gen_slots_) {
out << indent() << "if attr not in self.__slots__:" << endl;
} else {
out << indent() << "if not self.__dict__.has_key(attr):" << endl;
Copy link
Member

Choose a reason for hiding this comment

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

this might not work as expected:

$ python3
Python 3.11.4 (main, Jun  7 2023, 10:13:09) [GCC 12.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> class Foo:
...   _bar = None
... 
>>> foo = Foo()
>>> foo.__dict__
{}
>>> foo._bar = 1
>>> foo.__dict__
{'_bar': 1}

it seems that the __dict__ would only have the key if it's used at least once?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Currently, it looks like each use defined field is explicitly set in the __init__. I believe this would avoid this issue. Is that not correct?

Alternately I could keep track of user defined fields similar to how we do for __slots__, thoughts?

Copy link
Member

Choose a reason for hiding this comment

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

my main concern here is that after contextmanager changes the __traceback__ it will start to appear in __dict__ and then it can no longer be changed again

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Long story short, __traceback__ does not appear in __dict__ from what I could track down it's because the implementation of Exception is in C and __traceback__ is not technically an official property.

Fishy and I discussed off PR and decided that it was acceptable to only support context managers (in Python 3.11) with when using slots. This means any user that runs into this issue should use slots to get around their issue.

@Jens-G Jens-G added the python label Jun 21, 2023
@KTAtkinson KTAtkinson requested a review from fishy June 22, 2023 17:09
@KTAtkinson KTAtkinson force-pushed the katie-atkinson/py-mutable-errs branch from 6d12bc0 to 409b4f9 Compare June 28, 2023 22:59
@KTAtkinson
Copy link
Contributor Author

@fishy Have time to take a look at this again?

<< endl;
out << indent() << "def __setattr__(self, *args):" << endl;
indent_up();
if (gen_slots_) {
Copy link
Member

Choose a reason for hiding this comment

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

please add some comments inside this if block, with link to the jira ticket, to explain why we have this check.

out << endl;
out << indent() << "def __delattr__(self, *args):" << endl;
indent_up();
if (gen_slots_) {
Copy link
Member

Choose a reason for hiding this comment

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

same here

)
except Exception as e:
self.assertTrue(
isinstance(e, TypeError),
Copy link
Member

Choose a reason for hiding this comment

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

this should be isinstance(e, TypeError) and not uses_slots instead?

Copy link
Member

Choose a reason for hiding this comment

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

or probably better split into 2 separated asserts.

@fishy
Copy link
Member

fishy commented Jul 6, 2023

it was acceptable to only support context managers (in Python 3.11) with when using slots.

also to clarify, I said it's acceptable to limit the scope of this PR to only fix the slots use-case, but that does not fully fix the issue (so we cannot close the issue with this PR merged) and we should still fix the non-slots use-case later.

@fishy
Copy link
Member

fishy commented Jul 6, 2023

please also rebase your change on top of latest master. there are 2 commits related to py compiler merged recently and I want to make sure that this does not break those changes.

@fishy
Copy link
Member

fishy commented Jul 6, 2023

Please also change the title of this PR accordingly. You are no longer adding a flag to allow mutable exceptions.

@KTAtkinson KTAtkinson changed the title THRIFT-5715: Python flag to generate mutable exceptions THRIFT-5715: Python non-user defined fields mutable with slots Jul 7, 2023
@KTAtkinson KTAtkinson force-pushed the katie-atkinson/py-mutable-errs branch from 409b4f9 to 906ff2a Compare July 7, 2023 21:18
@KTAtkinson KTAtkinson requested a review from fishy July 7, 2023 21:19
compiler/cpp/src/thrift/generate/t_py_generator.cc Outdated Show resolved Hide resolved
compiler/cpp/src/thrift/generate/t_py_generator.cc Outdated Show resolved Hide resolved
Comment on lines 917 to 919
// Not user-provided fields should be editable so that the Python Standard L`ibrary can edit
// internal fields of std library base classes. For example, in Python 3.11 context managers
// edit the `__traceback__` field on Exceptions. Allowing this to work with `__slots__` is
Copy link
Member

Choose a reason for hiding this comment

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

same here

In Python 3.11 exceptions generated by the compiler can't be used with a
context manager because they are immutable. As of Python 3.11
`contextlib.contextmanager` sets `exc.__traceback__` in the event that
the code in the context manager errors.

As of Thrift v0.18.1 exceptions are generated as immutable by default.
See [PR#1835](apache#1835) for more
information about why exceptions were made immutable by default.

This change makes all non-Thrift fields mutable when slots is used
without dynamic. This will allow exceptions to be re-raised properly by
the contextmanager in Python 3.11.
@KTAtkinson KTAtkinson force-pushed the katie-atkinson/py-mutable-errs branch from 23e94f8 to d3d8fd7 Compare July 7, 2023 22:31
@KTAtkinson KTAtkinson requested a review from fishy July 7, 2023 22:32
@fishy fishy merged commit ff9850e into apache:master Jul 7, 2023
10 of 12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
3 participants