diff options
| author | Brett Stottlemyer <bstottle@ford.com> | 2024-12-18 10:33:56 -0500 |
|---|---|---|
| committer | Friedemann Kleint <Friedemann.Kleint@qt.io> | 2025-03-13 16:28:42 +0100 |
| commit | 19abd816e73bebdd489408d0a3b7676822bff39c (patch) | |
| tree | 8459ae9401f5e190995b3e24b6ae6968cf457baf /sources/pyside6/tests/QtRemoteObjects/test_shared.py | |
| parent | 3c66c456aeab597b7cb046f81c7f015433bb57a4 (diff) | |
Make Remote Objects usable beyond Models
While present, the Qt Remote Objects bindings to Python have not been
very useful. The only usable components were those based on
QAbstractItemModel, due to the lack of a way to interpret .rep files
from Python. This addresses that limitation.
Fixes: PYSIDE-862
Change-Id: Ice57c0c64f11c3c7e74d50ce3c48617bd9b422a3
Reviewed-by: Friedemann Kleint <Friedemann.Kleint@qt.io>
Reviewed-by: Brett Stottlemyer <brett.stottlemyer@gmail.com>
Diffstat (limited to 'sources/pyside6/tests/QtRemoteObjects/test_shared.py')
| -rw-r--r-- | sources/pyside6/tests/QtRemoteObjects/test_shared.py | 126 |
1 files changed, 126 insertions, 0 deletions
diff --git a/sources/pyside6/tests/QtRemoteObjects/test_shared.py b/sources/pyside6/tests/QtRemoteObjects/test_shared.py new file mode 100644 index 000000000..5b176ce9d --- /dev/null +++ b/sources/pyside6/tests/QtRemoteObjects/test_shared.py @@ -0,0 +1,126 @@ +# Copyright (C) 2025 Ford Motor Company +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 +from __future__ import annotations + +import gc +import sys +from functools import wraps + + +def _cleanup_local_variables(self, extra, debug): + """ + Function to clean up local variables after a unit test. + + This method will set any local variables defined in the test run to None. It also + sets variables of self to None, if they are provided in the extra list. + + The self argument is passed by the decorator, so we can access the instance variables. + """ + local_vars = self._locals + if debug: + print(f" Cleaning up locals: {local_vars.keys()} and member of self: {extra}", + file=sys.stderr) + exclude_vars = {'__builtins__', 'self', 'args', 'kwargs'} + for var in list(local_vars.keys()): + if var not in exclude_vars: + local_vars[var] = None + if debug: + print(f" Set {var} to None", file=sys.stderr) + # Remove variables added to 'self' during our test + for var in list(vars(self).keys()): + if var in extra: + setattr(self, var, None) + if debug: + print(f" Set self.{var} to None", file=sys.stderr) + gc.collect() + + +# This leverages the tip from # https://stackoverflow.com/a/9187022/169296 +# for capturing local variables using sys.setprofile and a tracer function +def wrap_tests_for_cleanup(extra: str | list[str] = None, debug: bool = False): + """ + Method that returns a decorator for setting variables used in a test to + None, thus allowing the garbage collection to clean up properly and ensure + destruction behavior is correct. Using a method to return the decorator + allows us to pass extra arguments to the decorator, in this case for extra + data members on `self` to set to None or whether to output additional debug + logging. + + It simply returns the class decorator to be used. + """ + def decorator(cls): + """ + This is a class decorator that finds and wraps all test methods in a + class. + + The provided extra is used to define a set() of variables that are set + to None on `self` after the test method has run. This is useful for + making sure the local and self variables can be garbage collected. + """ + _extra = set() + if extra: + if isinstance(extra, str): + _extra.add(extra) + else: + _extra.update(extra) + for name, attr in cls.__dict__.items(): + if name.startswith("test") and callable(attr): + """ + Only wrap methods that start with 'test' and are callable. + """ + def make_wrapper(method): + """ + This is the actual wrapper that will be used to wrap the + test methods. It will set a tracer function to capture the + local variables and then calls our cleanup function to set + the variables to None. + """ + @wraps(method) + def wrapper(self, *args, **kwargs): + if debug: + print(f"wrap_tests_for_cleanup - calling {method.__name__}", + file=sys.stderr) + + def tracer(frame, event, arg): + if event == 'return': + self._locals = frame.f_locals.copy() + + # tracer is activated on next call, return or exception + sys.setprofile(tracer) + try: + # trace the function call + return method(self, *args, **kwargs) + finally: + # disable tracer and replace with old one + sys.setprofile(None) + # call our cleanup function + _cleanup_local_variables(self, _extra, debug) + if debug: + print(f"wrap_tests_for_cleanup - done calling {method.__name__}", + file=sys.stderr) + return wrapper + setattr(cls, name, make_wrapper(attr)) + return cls + return decorator + + +if __name__ == "__main__": + # Set up example test class + @wrap_tests_for_cleanup(extra="name", debug=True) + class test: + def __init__(self): + self.name = "test" + + def testStuff(self): + value = 42 + raise ValueError("Test") + temp = 11 # noqa: F841 + return value + + t = test() + try: + t.testStuff() + except ValueError: + pass + # Should print that `value` and `self.name` are set to None, even with the + # exception being raised. |
