././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1730689026.5452478 kgb-7.2/0000755000076500000240000000000014712034003011333 5ustar00chipx86staff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1730689021.0 kgb-7.2/AUTHORS0000644000076500000240000000011014712033775012412 0ustar00chipx86staffLead Developers: * Christian Hammond Contributors: * Todd Wolfson ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1730689021.0 kgb-7.2/LICENSE0000644000076500000240000000204114712033775012354 0ustar00chipx86staffCopyright (c) 2013 Beanbag, Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1730689021.0 kgb-7.2/MANIFEST.in0000644000076500000240000000025714712033775013114 0ustar00chipx86staffinclude AUTHORS include LICENSE include NEWS.rst include README.rst include conftest.py include setup.cfg include tox.ini include tests/runtests.py include *-requirements.txt ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1730689021.0 kgb-7.2/NEWS.rst0000644000076500000240000003030114712033775012655 0ustar00chipx86staff============ kgb Releases ============ kgb 7.2 (3-November-2024) ========================= * Added support for Python 3.13. We've fixed crashes with some changes made to how Python 3.13 builds and executes functions, and added explicit testing and support going forward. kgb 7.1.1 (6-August-2022) ========================= * Small packaging update to include the ``LICENSE`` file. No code changes. kgb 7.1 (4-August-2022) ======================= * Added support for Python 3.11. kgb 7 (20-January-2022) ======================= * Added explicit support for Python 3.10. * Dropped support for Python 2.6, 3.4, and 3.5. * kgb now works as a plugin for pytest_. Unit tests can use the ``spy_agency`` fixture to have a spy agency created and ready for use. Spies will be automatically unregistered when the test completes. * Added snake_case versions of all assertion methods in ``SpyAgency``. This includes: * ``assert_has_spy`` * ``assert_spy_call_count`` * ``assert_spy_called_with`` * ``assert_spy_called`` * ``assert_spy_last_called_with`` * ``assert_spy_last_raised_message`` * ``assert_spy_last_raised`` * ``assert_spy_last_returned`` * ``assert_spy_not_called_with`` * ``assert_spy_not_called`` * ``assert_spy_raised_message`` * ``assert_spy_raised`` * ``assert_spy_returned`` * Added standalone assertion methods in ``kgb.asserts``. This provides all the assertion methods shown above, but as standalone methods that can work in any test suite. * Added a ``func_name=`` argument when setting up spies, to avoid problems with bad decorators. When spying on an unbound method wrapped in a decorator that doesn't preserve the function name, errors could occur. In this case, you can pass ``func_name=`` when setting up the spy, telling kgb about the original function name it should use. This is a special situation. Most spies will not need to set this. * Updated ``SpyCall.__repr__`` to list keyword arguments in sorted order. * The package now lists the Python versions that are supported. This will help down the road when we begin deprecating older versions of Python, ensuring that ``pip`` will install the appropriate version of kgb for the version of Python. .. _pytest: https://pytest.org kgb 6.1 (24-August-2021) ======================== * Added new ``SpyOpReturnInOrder`` and ``SpyOpRaiseInOrder`` spy operations. ``SpyOpReturnInOrder`` takes a list of values to return. Each call made will return the next value from that list. An exception will be raised if any further calls are made once the list is exhausted. ``SpyOpRaiseInOrder`` is similar, but takes a list of exceptions to raise. Examples: .. code-block:: python spy_on(our_agent.get_identity, op=kgb.SpyOpReturnInOrder([ 'nobody...', 'who?', 'not telling...', ])) spy_on(pen.emit_poison, op=kgb.SpyOpRaiseInOrder([ PoisonEmptyError(), Kaboom(), MissingPenError(), ])) * ``SpyOpMatchInOrder`` and ``SpyOpMatchAny`` now accept operations in the expected calls. These can be set through an ``op`` key, instead of setting ``call_fake`` or ``call_original``. For example: .. code-block:: python spy_on(lockbox.enter_code, op=kgb.SpyOpMatchInOrder([ { 'args': (42, 42, 42, 42, 42, 42), 'op': kgb.SpyOpRaise(Kaboom()), 'call_original': True, }, ])) Any operation can be provided. This also allows for advanced, reusable rule sets by nesting, for example, ``SpyOpMatchInOrder`` inside ``SpyOpMatchAny``. * ``UnexpectedCallError`` now lists the call that was made in the error message. kgb 6.0 (3-September-2020) ========================== * Added a new ``@spy_for`` decorator. This is an alternative to defining a function and then calling ``spy_on(func, call_fake=...)``. It takes a function or method to spy on and an optional owner, much like ``spy_on()``. For example: .. code-block:: python def test_doomsday_device(self): dd = DoomsdayDevice() @self.spy_for(dd.kaboom) def _save_world(*args, **kwargs) print('Sprinkles and ponies!') * Added new support for Spy Operations. Spy Operations can be thought of as pre-packaged "fake functions" for a spy, which can perform some useful operations. There are a few built-in types: * ``SpyOpMatchAny`` allows a caller to provide a list of all possible sets of arguments that may be in one or more calls, triggering spy behavior for the particular match (allowing ``call_original``/``call_fake`` to be conditional on the arguments). Any call not provided in the list will raise an ``UnexpectedCallError`` assertion. * ``SpyOpMatchInOrder`` is similar to ``SpyOpMatchAny``, but the calls must be in the order specified (which is useful for ensuring an order of operations). * ``SpyOpRaise`` takes an exception instance and raises it when the function is called (preventing a caller from having to define a wrapping function). * ``SpyOpReturn`` takes a return value and returns it when the function is called (similar to defining a simple lambda, but better specifying the intent). These are set with an ``op=`` argument, instead of a ``call_fake=``. For example: .. code-block:: python spy_on(pen.emit_poison, op=kgb.SpyOpRaise(PoisonEmptyError())) Or, for one of the more complex examples: .. code-block:: python spy_on(traps.trigger, op=kgb.SpyOpMatchAny([ { 'args': ('hallway_lasers',), 'call_fake': _send_wolves, }, { 'args': ('trap_tile',), 'call_fake': _spill_hot_oil, }, { 'args': ('infrared_camera',), 'kwargs': { 'sector': 'underground_passage', }, 'call_original': False, }, ])) * Added an ``assertSpyNotCalledWith()`` assertion method. Like the name suggests, it asserts that a spy has not been called with the provided arguments. It's the inverse of ``assertSpyCalledWith()``. * ``SpyAgency``'s assertion methods can now be used even without mixing it into a ``TestCase``. * Fixed a crash in ``SpyAgency.unspy_all()``. * Fixed the grammar in an error message about slippery functions. kgb 5.0 (10-April-2020) ======================= * Added support for Python 3.8. Functions with positional-only arguments on Python 3.8 will now work correctly, and the positional-only arguments will factor into any spy matching. * Added several new unit test assertion methods: * ``assertHasSpy`` * ``assertSpyCalled`` * ``assertSpyNotCalled`` * ``assertSpyCallCount`` * ``assertSpyCalledWith`` * ``assertSpyLastCalledWith`` * ``assertSpyReturned`` * ``assertSpyLastReturned`` * ``assertSpyRaised`` * ``assertSpyLastRaised`` * ``assertSpyRaisedMessage`` * ``assertSpyLastRaisedMessage`` We recommend using these for unit tests instead of checking individual properties of calls, as they'll provide better output and help you find out why spies have gone rogue. * Added support for spying on "slippery" functions. A slippery function is defined (by us) as a function on an object that is actually a different function every time you access it. In other words, if you were to just reference a slippery function as an attribute two times, you'd end up with two separate copies of that function, each with their own ID. This can happen if the "function" is actually some decorator that returns a new function every time it's accessed. A real-world example would be the Python Stripe module's API functions, like ``stripe.Customer.delete``. In previous versions of kgb, you wouldn't be able to spy on these functions. With 5.0, you can spy on them just fine by passing ``owner=`` when setting up the spy: .. code-block:: python spy_on(myobj.slippery_func, owner=myobj) * Lots of internal changes to help keep the codebase organized and manageable, as Python support increases. kgb 4.0 (30-July-2019) ====================== * Added ``call_original()``, which calls the original spied-on function. The call will not be logged, and will invoke the original behavior of the function. This is useful when a spy simply needs to wrap another function. * Updated the Python 3 support to use the modern, non-deprecated support for inspecting and formatting function/method signatures. kgb 3.0 (23-March-2019) ======================= * Added an argument to ``spy_on()`` for specifying an explicit owner class for unbound methods, and warn if missing. Python 3.x doesn't have a real way of determining the owning class for unbound methods, and attempting to spy on an unbound method can end up causing a number of problems, potentially interfering with spies that are a subclass or superclass of the spied object. ``spy_on()`` now accepts an ``owner=`` parameter for unbound methods in order to explicitly specify the class. It will warn if this is missing, providing details on what it thinks the owner is and the recommended changes to make to the call. * Fixed spying on unbound methods originally defined on the parent class of a specified or determined owning class. * Fixed spying on old-syle classes (those not inheriting from ``object``) on Python 2.6 and early versions of 2.7. kgb 2.0.3 (18-August-2018) ========================== * Added a version classifier for Python 3.7. * Fixed a regression on Python 2.6. kgb 2.0.2 (9-July-2018) ======================= * Fixed spying on instances of classes with a custom ``__setattr__``. * Fixed spying on classmethods defined in the parent of a class. kgb 2.0.1 (12-March-2018) ========================= * Fixed a regression in spying on classmethods. * Fixed copying function annotations and keyword-only defaults in Python 3. * Fixed problems executing some types of functions on Python 3.6. kgb 2.0 (5-February-2018) ========================= * Added compatibility with Python 3.6. * Spy methods for standard functions no longer need to be accessed like: .. code-block:: python func.spy.last_call Now you can call them the same way you could with methods: .. code-block:: python func.last_call * The ``args`` and ``kwargs`` information recorded for a spy now correspond to the function signature and not the way the function was called. * ``called_with()`` now allows providing keyword arguments to check positional arguments by name. * When spying on a function fails for some reason, the error output is a lot more helpful. kgb 1.1 (5-December-2017) ========================= * Added ``returned()``, ``last_returned()``, ``raised()``, ``last_raised()``, ``raised_with_message()``, and ``last_raised_with_message()`` methods to function spies. See the README for how this works. * Added ``called_with()``, ``returned()``, ``raised()``, and ``raised_with_message()`` to the individual ``SpyCall`` objects. These are accessed through ``spy.calls``, and allow for more conveniently checking the results of specific calls in tests. * ``called_with()`` and ``last_called_with()`` now accept matching subsets of arguments. Any number of leading positional arguments and any subset of keyword arguments can be specified. Prior to 1.0, subsets of keyword arguments were supported, but 1.0 temporarily made this more strict. This is helpful when testing function calls containing many default arguments or when the function takes ``*args`` and ``**kwargs``. kgb 1.0 (31-October-2017) ========================= * Added support for Python 3, including keyword-only arguments. * Function signatures for spies now mimic that of the spied-on functions, allowing Python's ``getargspec()`` to work. kgb 0.5.3 (28-November-2015) ============================ * Objects that evaluate to false (such as objects inheriting from ``dict``) can now be spied upon. kgb 0.5.2 (17-March-2015) ========================= * Expose the spy when using ``spy_on`` as a context manager. Patch by Todd Wolfson. kgb 0.5.1 (2-June-2014) ======================= * Added support for spying on unbound member functions on classes. kgb 0.5.0 (23-May-2013) ======================= * First public release. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1730689026.5451531 kgb-7.2/PKG-INFO0000644000076500000240000005106714712034003012441 0ustar00chipx86staffMetadata-Version: 2.1 Name: kgb Version: 7.2 Summary: Utilities for spying on function calls in unit tests. Author-email: "Beanbag, Inc." License: MIT Project-URL: Homepage, https://github.com/beanbaginc/kgb Project-URL: Documentation, https://github.com/beanbaginc/kgb Project-URL: Repository, https://github.com/beanbaginc/kgb Keywords: pytest,unit tests,spies Classifier: Development Status :: 5 - Production/Stable Classifier: Environment :: Other Environment Classifier: Framework :: Pytest Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Programming Language :: Python :: 3.13 Classifier: Topic :: Software Development Classifier: Topic :: Software Development :: Libraries :: Python Modules Classifier: Topic :: Software Development :: Testing Requires-Python: !=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7 Description-Content-Type: text/x-rst License-File: LICENSE License-File: AUTHORS =============================== kgb - Function spies for Python =============================== Ever deal with a large test suite before, monkey patching functions to figure out whether it was called as expected? It's a dirty job. If you're not careful, you can make a mess of things. Leave behind evidence. kgb's spies will take care of that little problem for you. What are spies? =============== Spies intercept and record calls to functions. They can report on how many times a function was called and with what arguments. They can allow the function call to go through as normal, to block it, or to reroute it to another function. Spies are awesome. (If you've used Jasmine_, you know this.) Spies are like mocks, but better. You're not mocking the world. You're replacing very specific function logic, or listening to functions without altering them. (See the FAQ below.) .. _Jasmine: https://jasmine.github.io/ What test platforms are supported? ================================== Anything Python-based: * unittest_ * pytest_ * nose_ * nose2_ You can even use it outside of unit tests as part of your application. If you really want to. (Probably don't do that.) .. _unittest: https://docs.python.org/3/library/unittest.html .. _pytest: https://pytest.org .. _nose: https://nose.readthedocs.io/en/latest/ .. _nose2: https://docs.nose2.io/en/latest/ Where is kgb used? ================== * `liveswot-api `_ -- REST API Backend for liveswot * `phabricator-emails `_ -- Mozilla's utilities for converting Phabricator feeds to e-mails * `projector `_ -- Takes the overhead out of managing repositories and development environments * `ynab-sdk-python `_ -- Python implementation of the YNAB API Plus our own products: * `Django Evolution `_ -- An alternative approach to Django database migrations * `Djblets `_ -- An assortment of utilities and add-ons for managing large Django projects * `Review Board `_ -- Our open source, extensible code review product * `RBCommons `_ -- Our hosted code review service * `RBTools `_ -- Command line tools for Review Board * `Power Pack `_ -- Document review, reports, and enterprise SCM integrations for Review Board * `Review Bot `_ -- Automated code review add-on for Review Board If you use kgb, let us know and we'll add you! Installing kgb ============== Before you can use kgb, you need to install it. You can do this by typing:: $ pip install kgb kgb supports Python 2.7 and 3.6 through 3.11, both CPython and PyPy. Spying for fun and profit ========================= Spying is really easy. There are four main ways to initiate a spy. 1. Creating a SpyAgency ----------------------- A SpyAgency manages all your spies. You can create as many or as few as you want. Generally, you'll create one per unit test run. Then you'll call ``spy_on()``, passing in the function you want. .. code-block:: python from kgb import SpyAgency def test_mind_control_device(): mcd = MindControlDevice() agency = SpyAgency() agency.spy_on(mcd.assassinate, call_fake=give_hugs) 2. Mixing a SpyAgency into your tests ------------------------------------- A ``SpyAgency`` can be mixed into your unittest_-based test suite, making it super easy to spy all over the place, discretely, without resorting to a separate agency. (We call this the "inside job.") .. code-block:: python from kgb import SpyAgency # Using Python's unittest: class TopSecretTests(SpyAgency, unittest.TestCase): def test_weather_control(self): weather = WeatherControlDevice() self.spy_on(weather.start_raining) # Using pytest with the "spy_agency" fixture (kgb 7+): def test_weather_control(spy_agency): weather = WeatherControlDevice() spy_agency.spy_on(weather.start_raining) 3. Using a decorator -------------------- If you're creating a spy that calls a fake function, you can simplify some things by using the ``spy_for`` decorator: .. code-block:: python from kgb import SpyAgency # Using Python's unittest: class TopSecretTests(SpyAgency, unittest.TestCase): def test_doomsday_device(self): dd = DoomsdayDevice() @self.spy_for(dd.kaboom) def _save_world(*args, **kwargs) print('Sprinkles and ponies!') # Give it your best shot, doomsday device. dd.kaboom() # Using pytest: def test_doomsday_device(spy_agency): dd = DoomsdayDevice() @spy_agency.spy_for(dd.kaboom) def _save_world(*args, **kwargs) print('Sprinkles and ponies!') # Give it your best shot, doomsday device. dd.kaboom() 4. Using a context manager -------------------------- If you just want a spy for a quick job, without all that hassle of a full agency, just use the ``spy_on`` context manager, like so: .. code-block:: python from kgb import spy_on def test_the_bomb(self): bomb = Bomb() with spy_on(bomb.explode, call_original=False): # This won't explode. Phew. bomb.explode() A spy's abilities ================= A spy can do many things. The first thing you need to do is figure out how you want to use the spy. Creating a spy that calls the original function ----------------------------------------------- .. code-block:: python spy_agency.spy_on(obj.function) When your spy is called, the original function will be called as well. It won't even know you were there. Creating a spy that blocks the function call -------------------------------------------- .. code-block:: python spy_agency.spy_on(obj.function, call_original=False) Useful if you want to know that a function was called, but don't want the original function to actually get the call. Creating a spy that reroutes to a fake function ----------------------------------------------- .. code-block:: python def _my_fake_function(some_param, *args, **kwargs): ... spy_agency.spy_on(obj.function, call_fake=my_fake_function) # Or, in kgb 6+ @spy_agency.spy_for(obj.function) def _my_fake_function(some_param, *args, **kwargs): ... Fake the return values or operations without anybody knowing. Stopping a spy operation ------------------------ .. code-block:: python obj.function.unspy() Do your job and get out. Check the call history ---------------------- .. code-block:: python for call in obj.function.calls: print(calls.args, calls.kwargs) See how many times your spy's intercepted a function call, and what was passed. Check a specific call --------------------- .. code-block:: python # Check the latest call... print(obj.function.last_call.args) print(obj.function.last_call.kwargs) print(obj.function.last_call.return_value) print(obj.function.last_call.exception) # For an older call... print(obj.function.calls[0].args) print(obj.function.calls[0].kwargs) print(obj.function.calls[0].return_value) print(obj.function.calls[0].exception) Also a good way of knowing whether it's even been called. ``last_call`` will be ``None`` if nobody's called yet. Check if the function was ever called ------------------------------------- Mixing in ``SpyAgency`` into a unittest_-based test suite: .. code-block:: python # Either one of these is fine. self.assertSpyCalled(obj.function) self.assertTrue(obj.function.called) # Or the inverse: self.assertSpyNotCalled(obj.function) self.assertFalse(obj.function.called) Or using the pytest_ ``spy_agency`` fixture on kgb 7+: .. code-block:: python spy_agency.assert_spy_called(obj.function) spy_agency.assert_spy_not_called(obj.function) Or using standalone assertion methods on kgb 7+: .. code-block:: python from kgb.asserts import (assert_spy_called, assert_spy_not_called) assert_spy_called(obj.function) assert_spy_not_called(obj.function) If the function was ever called at all, this will let you know. Check if the function was ever called with certain arguments ------------------------------------------------------------ Mixing in ``SpyAgency`` into a unittest_-based test suite: .. code-block:: python # Check if it was ever called with these arguments... self.assertSpyCalledWith(obj.function, 'foo', bar='baz') self.assertTrue(obj.function.called_with('foo', bar='baz')) # Check a specific call... self.assertSpyCalledWith(obj.function.calls[0], 'foo', bar='baz') self.assertTrue(obj.function.calls[0].called_with('foo', bar='baz')) # Check the last call... self.assertSpyLastCalledWith(obj.function, 'foo', bar='baz') self.assertTrue(obj.function.last_called_with('foo', bar='baz')) # Or the inverse: self.assertSpyNotCalledWith(obj.function, 'foo', bar='baz') self.assertFalse(obj.function.called) Or using the pytest_ ``spy_agency`` fixture on kgb 7+: .. code-block:: python spy_agency.assert_spy_called_with(obj.function, 'foo', bar='baz') spy_agency.assert_spy_last_called_with(obj.function, 'foo', bar='baz') spy_agency.assert_spy_not_called_with(obj.function, 'foo', bar='baz') Or using standalone assertion methods on kgb 7+: .. code-block:: python from kgb.asserts import (assert_spy_called_with, assert_spy_last_called_with, assert_spy_not_called_with) assert_spy_called_with(obj.function, 'foo', bar='baz') assert_spy_last_called_with(obj.function, 'foo', bar='baz') assert_spy_not_called_with(obj.function, 'foo', bar='baz') The whole callkhistory will be searched. You can provide the entirety of the arguments passed to the function, or you can provide a subset. You can pass positional arguments as-is, or pass them by name using keyword arguments. Recorded calls always follow the function's original signature, so even if a keyword argument was passed a positional value, it will be recorded as a keyword argument. Check if the function ever returned a certain value --------------------------------------------------- Mixing in ``SpyAgency`` into a unittest_-based test suite: .. code-block:: python # Check if the function ever returned a certain value... self.assertSpyReturned(obj.function, 42) self.assertTrue(obj.function.returned(42)) # Check a specific call... self.assertSpyReturned(obj.function.calls[0], 42) self.assertTrue(obj.function.calls[0].returned(42)) # Check the last call... self.assertSpyLastReturned(obj.function, 42) self.assertTrue(obj.function.last_returned(42)) Or using the pytest_ ``spy_agency`` fixture on kgb 7+: .. code-block:: python spy_agency.assert_spy_returned(obj.function, 42) spy_agency.assert_spy_returned(obj.function.calls[0], 42) spy_agency.assert_spy_last_returned(obj.function, 42) Or using standalone assertion methods on kgb 7+: .. code-block:: python from kgb.asserts import (assert_spy_last_returned, assert_spy_returned) assert_spy_returned(obj.function, 42) assert_spy_returned(obj.function.calls[0], 42) assert_spy_last_returned(obj.function, 42) Handy for checking if some function ever returned what you expected it to, when you're not calling that function yourself. Check if a function ever raised a certain type of exception ----------------------------------------------------------- Mixing in ``SpyAgency`` into a unittest_-based test suite: .. code-block:: python # Check if the function ever raised a certain exception... self.assertSpyRaised(obj.function, TypeError) self.assertTrue(obj.function.raised(TypeError)) # Check a specific call... self.assertSpyRaised(obj.function.calls[0], TypeError) self.assertTrue(obj.function.calls[0].raised(TypeError)) # Check the last call... self.assertSpyLastRaised(obj.function, TypeError) self.assertTrue(obj.function.last_raised(TypeError)) Or using the pytest_ ``spy_agency`` fixture on kgb 7+: .. code-block:: python spy_agency.assert_spy_raised(obj.function, TypeError) spy_agency.assert_spy_raised(obj.function.calls[0], TypeError) spy_agency.assert_spy_last_raised(obj.function, TypeError) Or using standalone assertion methods on kgb 7+: .. code-block:: python from kgb.asserts import (assert_spy_last_raised, assert_spy_raised) assert_spy_raised(obj.function, TypeError) assert_spy_raised(obj.function.calls[0], TypeError) assert_spy_last_raised(obj.function, TypeError) You can also go a step further by checking the exception's message. .. code-block:: python # Check if the function ever raised an exception with a given message... self.assertSpyRaisedWithMessage( obj.function, TypeError, "'type' object is not iterable") self.assertTrue(obj.function.raised_with_message( TypeError, "'type' object is not iterable")) # Check a specific call... self.assertSpyRaisedWithMessage( obj.function.calls[0], TypeError, "'type' object is not iterable") self.assertTrue(obj.function.calls[0].raised_with_message( TypeError, "'type' object is not iterable")) # Check the last call... self.assertSpyLastRaisedWithMessage( obj.function, TypeError, "'type' object is not iterable") self.assertTrue(obj.function.last_raised_with_message( TypeError, "'type' object is not iterable")) Reset all the calls ------------------- .. code-block:: python obj.function.reset_calls() Wipe away the call history. Nobody will know. Call the original function -------------------------- .. code-block:: python result = obj.function.call_original('foo', bar='baz') Super, super useful if you want to use ``call_fake=`` or ``@spy_agency.spy_for`` to wrap a function and track or influence some part of it, but still want the original function to do its thing. For instance: .. code-block:: python stored_results = [] @spy_agency.spy_for(obj.function) def my_fake_function(*args, **kwargs): kwargs['bar'] = 'baz' result = obj.function.call_original(*args, **kwargs) stored_results.append(result) return result Plan a spy operation ==================== Why start from scratch when setting up a spy? Let's plan an operation. (Spy operations are only available in kgb 6 or higher.) Raise an exception when called ------------------------------ .. code-block:: python spy_on(pen.emit_poison, op=kgb.SpyOpRaise(PoisonEmptyError())) Or go nuts, have a different exception for each call (in kgb 6.1+): .. code-block:: python spy_on(pen.emit_poison, op=kgb.SpyOpRaiseInOrder([ PoisonEmptyError(), Kaboom(), MissingPenError(), ])) Or return a value ----------------- .. code-block:: python spy_on(our_agent.get_identity, op=kgb.SpyOpReturn('nobody...')) Maybe a different value for each call (in kgb 6.1+)? .. code-block:: python spy_on(our_agent.get_identity, op=kgb.SpyOpReturnInOrder([ 'nobody...', 'who?', 'not telling...', ])) Now for something more complicated. Handle a call based on the arguments used ----------------------------------------- If you're dealing with many calls to the same function, you may want to return different values or only call the original function depending on which arguments were passed in the call. That can be done with a ``SpyOpMatchAny`` operation. .. code-block:: python spy_on(traps.trigger, op=kgb.SpyOpMatchAny([ { 'args': ('hallway_lasers',), 'call_fake': _send_wolves, }, { 'args': ('trap_tile',), 'op': SpyOpMatchInOrder([ { 'call_fake': _spill_hot_oil, }, { 'call_fake': _drop_torch, }, ]), }, { 'args': ('infrared_camera',), 'kwargs': { 'sector': 'underground_passage', }, 'call_original': False, }, ])) Any unexpected calls will automatically assert. Or require those calls in a specific order ------------------------------------------ You can combine that with requiring the calls to be in the order you want using ``SpyOpMatchInOrder``. .. code-block:: python spy_on(lockbox.enter_code, op=kgb.SpyOpMatchInOrder([ { 'args': (1, 2, 3, 4, 5, 6), 'call_original': False, }, { 'args': (9, 0, 2, 1, 0, 0), 'call_fake': _start_countdown, }, { 'args': (42, 42, 42, 42, 42, 42), 'op': kgb.SpyOpRaise(Kaboom()), 'call_original': True, }, { 'args': (4, 8, 15, 16, 23, 42), 'kwargs': { 'secret_button_pushed': True, }, 'call_original': True, } ])) FAQ === Doesn't this just do what mock does? ------------------------------------ kgb's spies and mock_'s patching are very different from each other. When patching using mock, you're simply replacing a method on a class with something that looks like a method, and that works great except you're limited to methods on classes. You can't override a top-level function, like ``urllib2.urlopen``. kgb spies leave the function or method where it is. What it *does* do is replace the *bytecode* of the function, intercepting calls on a very low level, recording everything about it, and then passing on the call to the original function or your replacement function. It's pretty powerful, and allows you to listen to or override calls you normally would have no control over. .. _mock: https://pypi.python.org/pypi/mock What?! There's no way that's stable. ------------------------------------ It is! It really is! We've been using it for years across a wide variety of codebases. It's pretty amazing. Python actually allows this. We're not scanning your RAM and doing terrible things with it, or something like that. Every function or method in Python has a ``func_code`` (Python 2) or ``__code__`` (Python 3) attribute, which is mutable. We can go in and replace the bytecode with something compatible with the original function. How we actually do that, well, that's complicated, and you may not want to know. Does this work with PyPy? ------------------------- I'm going to level with you, I was going to say "hell no!", and then decided to give it a try. Hell yes! (But only accidentally. YMMV... We'll try to officially support this later.) What else do you build? ----------------------- Lots of things. Check out some of our other `open source projects`_. .. _open source projects: https://www.beanbaginc.com/opensource/ ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1730689021.0 kgb-7.2/README.rst0000644000076500000240000004601414712033775013046 0ustar00chipx86staff=============================== kgb - Function spies for Python =============================== Ever deal with a large test suite before, monkey patching functions to figure out whether it was called as expected? It's a dirty job. If you're not careful, you can make a mess of things. Leave behind evidence. kgb's spies will take care of that little problem for you. What are spies? =============== Spies intercept and record calls to functions. They can report on how many times a function was called and with what arguments. They can allow the function call to go through as normal, to block it, or to reroute it to another function. Spies are awesome. (If you've used Jasmine_, you know this.) Spies are like mocks, but better. You're not mocking the world. You're replacing very specific function logic, or listening to functions without altering them. (See the FAQ below.) .. _Jasmine: https://jasmine.github.io/ What test platforms are supported? ================================== Anything Python-based: * unittest_ * pytest_ * nose_ * nose2_ You can even use it outside of unit tests as part of your application. If you really want to. (Probably don't do that.) .. _unittest: https://docs.python.org/3/library/unittest.html .. _pytest: https://pytest.org .. _nose: https://nose.readthedocs.io/en/latest/ .. _nose2: https://docs.nose2.io/en/latest/ Where is kgb used? ================== * `liveswot-api `_ -- REST API Backend for liveswot * `phabricator-emails `_ -- Mozilla's utilities for converting Phabricator feeds to e-mails * `projector `_ -- Takes the overhead out of managing repositories and development environments * `ynab-sdk-python `_ -- Python implementation of the YNAB API Plus our own products: * `Django Evolution `_ -- An alternative approach to Django database migrations * `Djblets `_ -- An assortment of utilities and add-ons for managing large Django projects * `Review Board `_ -- Our open source, extensible code review product * `RBCommons `_ -- Our hosted code review service * `RBTools `_ -- Command line tools for Review Board * `Power Pack `_ -- Document review, reports, and enterprise SCM integrations for Review Board * `Review Bot `_ -- Automated code review add-on for Review Board If you use kgb, let us know and we'll add you! Installing kgb ============== Before you can use kgb, you need to install it. You can do this by typing:: $ pip install kgb kgb supports Python 2.7 and 3.6 through 3.11, both CPython and PyPy. Spying for fun and profit ========================= Spying is really easy. There are four main ways to initiate a spy. 1. Creating a SpyAgency ----------------------- A SpyAgency manages all your spies. You can create as many or as few as you want. Generally, you'll create one per unit test run. Then you'll call ``spy_on()``, passing in the function you want. .. code-block:: python from kgb import SpyAgency def test_mind_control_device(): mcd = MindControlDevice() agency = SpyAgency() agency.spy_on(mcd.assassinate, call_fake=give_hugs) 2. Mixing a SpyAgency into your tests ------------------------------------- A ``SpyAgency`` can be mixed into your unittest_-based test suite, making it super easy to spy all over the place, discretely, without resorting to a separate agency. (We call this the "inside job.") .. code-block:: python from kgb import SpyAgency # Using Python's unittest: class TopSecretTests(SpyAgency, unittest.TestCase): def test_weather_control(self): weather = WeatherControlDevice() self.spy_on(weather.start_raining) # Using pytest with the "spy_agency" fixture (kgb 7+): def test_weather_control(spy_agency): weather = WeatherControlDevice() spy_agency.spy_on(weather.start_raining) 3. Using a decorator -------------------- If you're creating a spy that calls a fake function, you can simplify some things by using the ``spy_for`` decorator: .. code-block:: python from kgb import SpyAgency # Using Python's unittest: class TopSecretTests(SpyAgency, unittest.TestCase): def test_doomsday_device(self): dd = DoomsdayDevice() @self.spy_for(dd.kaboom) def _save_world(*args, **kwargs) print('Sprinkles and ponies!') # Give it your best shot, doomsday device. dd.kaboom() # Using pytest: def test_doomsday_device(spy_agency): dd = DoomsdayDevice() @spy_agency.spy_for(dd.kaboom) def _save_world(*args, **kwargs) print('Sprinkles and ponies!') # Give it your best shot, doomsday device. dd.kaboom() 4. Using a context manager -------------------------- If you just want a spy for a quick job, without all that hassle of a full agency, just use the ``spy_on`` context manager, like so: .. code-block:: python from kgb import spy_on def test_the_bomb(self): bomb = Bomb() with spy_on(bomb.explode, call_original=False): # This won't explode. Phew. bomb.explode() A spy's abilities ================= A spy can do many things. The first thing you need to do is figure out how you want to use the spy. Creating a spy that calls the original function ----------------------------------------------- .. code-block:: python spy_agency.spy_on(obj.function) When your spy is called, the original function will be called as well. It won't even know you were there. Creating a spy that blocks the function call -------------------------------------------- .. code-block:: python spy_agency.spy_on(obj.function, call_original=False) Useful if you want to know that a function was called, but don't want the original function to actually get the call. Creating a spy that reroutes to a fake function ----------------------------------------------- .. code-block:: python def _my_fake_function(some_param, *args, **kwargs): ... spy_agency.spy_on(obj.function, call_fake=my_fake_function) # Or, in kgb 6+ @spy_agency.spy_for(obj.function) def _my_fake_function(some_param, *args, **kwargs): ... Fake the return values or operations without anybody knowing. Stopping a spy operation ------------------------ .. code-block:: python obj.function.unspy() Do your job and get out. Check the call history ---------------------- .. code-block:: python for call in obj.function.calls: print(calls.args, calls.kwargs) See how many times your spy's intercepted a function call, and what was passed. Check a specific call --------------------- .. code-block:: python # Check the latest call... print(obj.function.last_call.args) print(obj.function.last_call.kwargs) print(obj.function.last_call.return_value) print(obj.function.last_call.exception) # For an older call... print(obj.function.calls[0].args) print(obj.function.calls[0].kwargs) print(obj.function.calls[0].return_value) print(obj.function.calls[0].exception) Also a good way of knowing whether it's even been called. ``last_call`` will be ``None`` if nobody's called yet. Check if the function was ever called ------------------------------------- Mixing in ``SpyAgency`` into a unittest_-based test suite: .. code-block:: python # Either one of these is fine. self.assertSpyCalled(obj.function) self.assertTrue(obj.function.called) # Or the inverse: self.assertSpyNotCalled(obj.function) self.assertFalse(obj.function.called) Or using the pytest_ ``spy_agency`` fixture on kgb 7+: .. code-block:: python spy_agency.assert_spy_called(obj.function) spy_agency.assert_spy_not_called(obj.function) Or using standalone assertion methods on kgb 7+: .. code-block:: python from kgb.asserts import (assert_spy_called, assert_spy_not_called) assert_spy_called(obj.function) assert_spy_not_called(obj.function) If the function was ever called at all, this will let you know. Check if the function was ever called with certain arguments ------------------------------------------------------------ Mixing in ``SpyAgency`` into a unittest_-based test suite: .. code-block:: python # Check if it was ever called with these arguments... self.assertSpyCalledWith(obj.function, 'foo', bar='baz') self.assertTrue(obj.function.called_with('foo', bar='baz')) # Check a specific call... self.assertSpyCalledWith(obj.function.calls[0], 'foo', bar='baz') self.assertTrue(obj.function.calls[0].called_with('foo', bar='baz')) # Check the last call... self.assertSpyLastCalledWith(obj.function, 'foo', bar='baz') self.assertTrue(obj.function.last_called_with('foo', bar='baz')) # Or the inverse: self.assertSpyNotCalledWith(obj.function, 'foo', bar='baz') self.assertFalse(obj.function.called) Or using the pytest_ ``spy_agency`` fixture on kgb 7+: .. code-block:: python spy_agency.assert_spy_called_with(obj.function, 'foo', bar='baz') spy_agency.assert_spy_last_called_with(obj.function, 'foo', bar='baz') spy_agency.assert_spy_not_called_with(obj.function, 'foo', bar='baz') Or using standalone assertion methods on kgb 7+: .. code-block:: python from kgb.asserts import (assert_spy_called_with, assert_spy_last_called_with, assert_spy_not_called_with) assert_spy_called_with(obj.function, 'foo', bar='baz') assert_spy_last_called_with(obj.function, 'foo', bar='baz') assert_spy_not_called_with(obj.function, 'foo', bar='baz') The whole callkhistory will be searched. You can provide the entirety of the arguments passed to the function, or you can provide a subset. You can pass positional arguments as-is, or pass them by name using keyword arguments. Recorded calls always follow the function's original signature, so even if a keyword argument was passed a positional value, it will be recorded as a keyword argument. Check if the function ever returned a certain value --------------------------------------------------- Mixing in ``SpyAgency`` into a unittest_-based test suite: .. code-block:: python # Check if the function ever returned a certain value... self.assertSpyReturned(obj.function, 42) self.assertTrue(obj.function.returned(42)) # Check a specific call... self.assertSpyReturned(obj.function.calls[0], 42) self.assertTrue(obj.function.calls[0].returned(42)) # Check the last call... self.assertSpyLastReturned(obj.function, 42) self.assertTrue(obj.function.last_returned(42)) Or using the pytest_ ``spy_agency`` fixture on kgb 7+: .. code-block:: python spy_agency.assert_spy_returned(obj.function, 42) spy_agency.assert_spy_returned(obj.function.calls[0], 42) spy_agency.assert_spy_last_returned(obj.function, 42) Or using standalone assertion methods on kgb 7+: .. code-block:: python from kgb.asserts import (assert_spy_last_returned, assert_spy_returned) assert_spy_returned(obj.function, 42) assert_spy_returned(obj.function.calls[0], 42) assert_spy_last_returned(obj.function, 42) Handy for checking if some function ever returned what you expected it to, when you're not calling that function yourself. Check if a function ever raised a certain type of exception ----------------------------------------------------------- Mixing in ``SpyAgency`` into a unittest_-based test suite: .. code-block:: python # Check if the function ever raised a certain exception... self.assertSpyRaised(obj.function, TypeError) self.assertTrue(obj.function.raised(TypeError)) # Check a specific call... self.assertSpyRaised(obj.function.calls[0], TypeError) self.assertTrue(obj.function.calls[0].raised(TypeError)) # Check the last call... self.assertSpyLastRaised(obj.function, TypeError) self.assertTrue(obj.function.last_raised(TypeError)) Or using the pytest_ ``spy_agency`` fixture on kgb 7+: .. code-block:: python spy_agency.assert_spy_raised(obj.function, TypeError) spy_agency.assert_spy_raised(obj.function.calls[0], TypeError) spy_agency.assert_spy_last_raised(obj.function, TypeError) Or using standalone assertion methods on kgb 7+: .. code-block:: python from kgb.asserts import (assert_spy_last_raised, assert_spy_raised) assert_spy_raised(obj.function, TypeError) assert_spy_raised(obj.function.calls[0], TypeError) assert_spy_last_raised(obj.function, TypeError) You can also go a step further by checking the exception's message. .. code-block:: python # Check if the function ever raised an exception with a given message... self.assertSpyRaisedWithMessage( obj.function, TypeError, "'type' object is not iterable") self.assertTrue(obj.function.raised_with_message( TypeError, "'type' object is not iterable")) # Check a specific call... self.assertSpyRaisedWithMessage( obj.function.calls[0], TypeError, "'type' object is not iterable") self.assertTrue(obj.function.calls[0].raised_with_message( TypeError, "'type' object is not iterable")) # Check the last call... self.assertSpyLastRaisedWithMessage( obj.function, TypeError, "'type' object is not iterable") self.assertTrue(obj.function.last_raised_with_message( TypeError, "'type' object is not iterable")) Reset all the calls ------------------- .. code-block:: python obj.function.reset_calls() Wipe away the call history. Nobody will know. Call the original function -------------------------- .. code-block:: python result = obj.function.call_original('foo', bar='baz') Super, super useful if you want to use ``call_fake=`` or ``@spy_agency.spy_for`` to wrap a function and track or influence some part of it, but still want the original function to do its thing. For instance: .. code-block:: python stored_results = [] @spy_agency.spy_for(obj.function) def my_fake_function(*args, **kwargs): kwargs['bar'] = 'baz' result = obj.function.call_original(*args, **kwargs) stored_results.append(result) return result Plan a spy operation ==================== Why start from scratch when setting up a spy? Let's plan an operation. (Spy operations are only available in kgb 6 or higher.) Raise an exception when called ------------------------------ .. code-block:: python spy_on(pen.emit_poison, op=kgb.SpyOpRaise(PoisonEmptyError())) Or go nuts, have a different exception for each call (in kgb 6.1+): .. code-block:: python spy_on(pen.emit_poison, op=kgb.SpyOpRaiseInOrder([ PoisonEmptyError(), Kaboom(), MissingPenError(), ])) Or return a value ----------------- .. code-block:: python spy_on(our_agent.get_identity, op=kgb.SpyOpReturn('nobody...')) Maybe a different value for each call (in kgb 6.1+)? .. code-block:: python spy_on(our_agent.get_identity, op=kgb.SpyOpReturnInOrder([ 'nobody...', 'who?', 'not telling...', ])) Now for something more complicated. Handle a call based on the arguments used ----------------------------------------- If you're dealing with many calls to the same function, you may want to return different values or only call the original function depending on which arguments were passed in the call. That can be done with a ``SpyOpMatchAny`` operation. .. code-block:: python spy_on(traps.trigger, op=kgb.SpyOpMatchAny([ { 'args': ('hallway_lasers',), 'call_fake': _send_wolves, }, { 'args': ('trap_tile',), 'op': SpyOpMatchInOrder([ { 'call_fake': _spill_hot_oil, }, { 'call_fake': _drop_torch, }, ]), }, { 'args': ('infrared_camera',), 'kwargs': { 'sector': 'underground_passage', }, 'call_original': False, }, ])) Any unexpected calls will automatically assert. Or require those calls in a specific order ------------------------------------------ You can combine that with requiring the calls to be in the order you want using ``SpyOpMatchInOrder``. .. code-block:: python spy_on(lockbox.enter_code, op=kgb.SpyOpMatchInOrder([ { 'args': (1, 2, 3, 4, 5, 6), 'call_original': False, }, { 'args': (9, 0, 2, 1, 0, 0), 'call_fake': _start_countdown, }, { 'args': (42, 42, 42, 42, 42, 42), 'op': kgb.SpyOpRaise(Kaboom()), 'call_original': True, }, { 'args': (4, 8, 15, 16, 23, 42), 'kwargs': { 'secret_button_pushed': True, }, 'call_original': True, } ])) FAQ === Doesn't this just do what mock does? ------------------------------------ kgb's spies and mock_'s patching are very different from each other. When patching using mock, you're simply replacing a method on a class with something that looks like a method, and that works great except you're limited to methods on classes. You can't override a top-level function, like ``urllib2.urlopen``. kgb spies leave the function or method where it is. What it *does* do is replace the *bytecode* of the function, intercepting calls on a very low level, recording everything about it, and then passing on the call to the original function or your replacement function. It's pretty powerful, and allows you to listen to or override calls you normally would have no control over. .. _mock: https://pypi.python.org/pypi/mock What?! There's no way that's stable. ------------------------------------ It is! It really is! We've been using it for years across a wide variety of codebases. It's pretty amazing. Python actually allows this. We're not scanning your RAM and doing terrible things with it, or something like that. Every function or method in Python has a ``func_code`` (Python 2) or ``__code__`` (Python 3) attribute, which is mutable. We can go in and replace the bytecode with something compatible with the original function. How we actually do that, well, that's complicated, and you may not want to know. Does this work with PyPy? ------------------------- I'm going to level with you, I was going to say "hell no!", and then decided to give it a try. Hell yes! (But only accidentally. YMMV... We'll try to officially support this later.) What else do you build? ----------------------- Lots of things. Check out some of our other `open source projects`_. .. _open source projects: https://www.beanbaginc.com/opensource/ ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1730689021.0 kgb-7.2/conftest.py0000644000076500000240000000034414712033775013552 0ustar00chipx86staff"""Configures pytest for kgb. This will conditionally ignore Python 3 test files on Python 2. """ from __future__ import unicode_literals import sys if sys.version_info[0] < 3: collect_ignore_glob = ['kgb/tests/py3/*'] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1730689021.0 kgb-7.2/dev-requirements.txt0000644000076500000240000000005214712033775015407 0ustar00chipx86staffpytest unittest2; python_version == '2.7' ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1730689026.542453 kgb-7.2/kgb/0000755000076500000240000000000014712034003012076 5ustar00chipx86staff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1730689021.0 kgb-7.2/kgb/__init__.py0000644000076500000240000000411014712033775014222 0ustar00chipx86stafffrom __future__ import unicode_literals from kgb.agency import SpyAgency from kgb.contextmanagers import spy_on from kgb.ops import (SpyOpMatchAny, SpyOpMatchInOrder, SpyOpRaise, SpyOpRaiseInOrder, SpyOpReturn, SpyOpReturnInOrder) # The version of kgb # # This is in the format of: # # (Major, Minor, Micro, alpha/beta/rc/final, Release Number, Released) # VERSION = (7, 2, 0, 'final', 0, True) def get_version_string(): """Return the kgb version as a human-readable string. Returns: unicode: The kgb version. """ version = '%s.%s' % (VERSION[0], VERSION[1]) if VERSION[2]: version += ".%s" % VERSION[2] if VERSION[3] != 'final': if VERSION[3] == 'rc': version += ' RC%s' % VERSION[4] else: version += ' %s %s' % (VERSION[3], VERSION[4]) if not is_release(): version += " (dev)" return version def get_package_version(): """Return the kgb version as a Python package version string. Returns: unicode: The kgb package version. """ version = '%s.%s' % (VERSION[0], VERSION[1]) if VERSION[2]: version += '.%s' % VERSION[2] tag = VERSION[3] if tag != 'final': if tag == 'alpha': tag = 'a' elif tag == 'beta': tag = 'b' version = '%s%s%s' % (version, tag, VERSION[4]) return version def is_release(): """Return whether this is a released version of kgb. Returns: bool: ``True`` if the version is released. ``False`` if it is still in development. """ return VERSION[5] __version_info__ = VERSION[:-1] __version__ = get_package_version() __all__ = [ '__version__', '__version_info__', 'SpyAgency', 'SpyOpMatchAny', 'SpyOpMatchInOrder', 'SpyOpRaise', 'SpyOpRaiseInOrder', 'SpyOpReturn', 'SpyOpReturnInOrder', 'VERSION', 'get_package_version', 'get_version_string', 'is_release', 'spy_on', ] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1730689021.0 kgb-7.2/kgb/agency.py0000644000076500000240000007513414712033775013747 0ustar00chipx86staff"""A spy agency to manage spies.""" from __future__ import unicode_literals from pprint import pformat from unittest.util import safe_repr from kgb.signature import _UNSET_ARG from kgb.spies import FunctionSpy, SpyCall from kgb.utils import format_spy_kwargs class SpyAgency(object): """Manages spies. A SpyAgency can be instantiated or mixed into a :py:class:`unittest.TestCase` in order to provide spies. Every spy created through this agency will be tracked, and can be later be removed (individually or at once). Version Changed: 7.0: Added ``assert_`` versions of all the assertion methods (e.g., ``assert_spy_called_with`` as an alias of ``assertSpyCalledWith``. Attributes: spies (set of kgb.spies.FunctionSpy): All spies currently registered with this agency. """ def __init__(self, *args, **kwargs): """Initialize the spy agency. Args: *args (tuple): Positional arguments to pass on to any other class (if using this as a mixin). **kwargs (dict): Keyword arguments to pass on to any other class (if using this as a mixin). """ super(SpyAgency, self).__init__(*args, **kwargs) self.spies = set() def tearDown(self): """Tear down a test suite. This is used when SpyAgency is mixed into a TestCase. It will automatically remove all spies when tearing down. """ super(SpyAgency, self).tearDown() self.unspy_all() def spy_on(self, *args, **kwargs): """Spy on a function. By default, the spy will allow the call to go through to the original function. This can be disabled by passing ``call_original=False`` when initiating the spy. If disabled, the original function will never be called. This can also be passed a ``call_fake`` parameter pointing to another function to call instead of the original. If passed, this will take precedence over ``call_original``. See :py:class:`~kgb.spies.FunctionSpy` for more details on arguments. Args: *args (tuple): Positional arguments to pass to :py:class:`~kgb.spies.FunctionSpy`. **kwargs (dict): Keyword arguments to pass to :py:class:`~kgb.spies.FunctionSpy`. Returns: kgb.spies.FunctionSpy: The resulting spy. """ spy = FunctionSpy(self, *args, **kwargs) self.spies.add(spy) return spy def spy_for(self, func, owner=_UNSET_ARG): """Decorate a function that should be a spy for another function. This is a convenience over declaring a function and using :py:meth:`spy_on` with ``call_fake=``. It's used to quickly and easily create a fake function spy for another function. Version Added: 6.0 Args: func (callable): The function or method to spy on. owner (type or object, optional): The owner of the function or method. If spying on an unbound method, this **must** be set to the class that owns it. If spying on a bound method that identifies as a plain function (which may happen if the method is decorated and dynamically returns a new function on access), this should be the instance of the object you're spying on. Example: @self.spy_for(get_doomsday): def _fake_get_doomsday(): return datetime(year=2038, month=12, day=5, hour=1, minute=2, second=3) """ def _wrap(call_fake): self.spy_on(func, owner=owner, call_fake=call_fake) return call_fake return _wrap def unspy(self, func): """Stop spying on a function. Args: func (callable): The function to stop spying on. Raises: ValueError: The provided function was not spied on. """ try: spy = func.spy except AttributeError: raise ValueError('Function %r has not been spied on.' % func) assert spy in self.spies spy.unspy() def unspy_all(self): """Stop spying on all functions tracked by this agency.""" for spy in self.spies: spy.unspy(unregister=False) self.spies.clear() def assertHasSpy(self, spy): """Assert that a function has a spy. This also accepts a spy as an argument, which will always return ``True``. Args: spy (callable or kgb.spies.FunctionSpy): The function or spy to check. Raises: AssertionError: The function did not have a spy. """ if not hasattr(spy, 'spy') and not isinstance(spy, FunctionSpy): self._kgb_assert_fail('%s has not been spied on.' % self._format_spy_or_call(spy)) def assertSpyCalled(self, spy): """Assert that a function has been called at least once. This will imply :py:meth:`assertHasSpy`. Args: spy (callable or kgb.spies.FunctionSpy): The function or spy to check. Raises: AssertionError: The function was not called. """ self.assertHasSpy(spy) if not spy.called: self._kgb_assert_fail('%s was not called.' % self._format_spy_or_call(spy)) def assertSpyNotCalled(self, spy): """Assert that a function has not been called. This will imply :py:meth:`assertHasSpy`. Args: spy (callable or kgb.spies.FunctionSpy): The function or spy to check. Raises: AssertionError: The function was called. """ self.assertHasSpy(spy) if spy.called: call_count = len(spy.calls) if call_count == 1: msg = ( '%s was called 1 time:' % self._format_spy_or_call(spy) ) else: msg = ( '%s was called %d times:' % (self._format_spy_or_call(spy), call_count) ) self._kgb_assert_fail( '%s\n' '\n' '%s' % ( msg, self._format_spy_calls(spy, self._format_spy_call_args), )) def assertSpyCallCount(self, spy, count): """Assert that a function was called the given number of times. This will imply :py:meth:`assertHasSpy`. Args: spy (callable or kgb.spies.FunctionSpy): The function or spy to check. count (int): The number of times the function is expected to have been called. Raises: AssertionError: The function was not called the specified number of times. """ self.assertHasSpy(spy) call_count = len(spy.calls) if call_count != count: if call_count == 1: msg = '%s was called %d time, not %d.' else: msg = '%s was called %d times, not %d.' self._kgb_assert_fail(msg % (self._format_spy_or_call(spy), call_count, count)) def assertSpyCalledWith(self, spy_or_call, *expected_args, **expected_kwargs): """Assert that a function was called with the given arguments. If a spy is provided, all calls will be checked for a match. This will imply :py:meth:`assertHasSpy`. Args: spy_or_call (callable or kgb.spies.FunctionSpy): The function, spy, or call to check. *expected_args (tuple): Position arguments expected to be provided in any of the calls. **expected_kwargs (dict): Keyword arguments expected to be provided in any of the calls. Raises: AssertionError: The function was not called with the provided arguments. """ if isinstance(spy_or_call, FunctionSpy): self.assertSpyCalled(spy_or_call) if not spy_or_call.called_with(*expected_args, **expected_kwargs): if isinstance(spy_or_call, SpyCall): self._kgb_assert_fail( 'This call to %s was not passed args=%s, kwargs=%s.\n' '\n' 'It was called with:\n' '\n' '%s' % ( self._format_spy_or_call(spy_or_call), safe_repr(expected_args), format_spy_kwargs(expected_kwargs), self._format_spy_call_args(spy_or_call), )) else: self._kgb_assert_fail( 'No call to %s was passed args=%s, kwargs=%s.\n' '\n' 'The following calls were recorded:\n' '\n' '%s' % ( self._format_spy_or_call(spy_or_call), safe_repr(expected_args), format_spy_kwargs(expected_kwargs), self._format_spy_calls( spy_or_call, self._format_spy_call_args), )) def assertSpyNotCalledWith(self, spy_or_call, *expected_args, **expected_kwargs): """Assert that a function was not called with the given arguments. If a spy is provided, all calls will be checked for a match. This will imply :py:meth:`assertHasSpy`. Args: spy_or_call (callable or kgb.spies.FunctionSpy): The function, spy, or call to check. *expected_args (tuple): Position arguments not expected to be provided in any of the calls. **expected_kwargs (dict): Keyword arguments not expected to be provided in any of the calls. Raises: AssertionError: The function was called with the provided arguments. """ if isinstance(spy_or_call, FunctionSpy): self.assertSpyCalled(spy_or_call) if spy_or_call.called_with(*expected_args, **expected_kwargs): if isinstance(spy_or_call, SpyCall): self._kgb_assert_fail( 'This call to %s was unexpectedly passed args=%s, ' 'kwargs=%s.' % ( self._format_spy_or_call(spy_or_call), safe_repr(expected_args), format_spy_kwargs(expected_kwargs), )) else: self._kgb_assert_fail( 'A call to %s was unexpectedly passed args=%s, ' 'kwargs=%s.\n' '\n' 'The following calls were recorded:\n' '\n' '%s' % ( self._format_spy_or_call(spy_or_call), safe_repr(expected_args), format_spy_kwargs(expected_kwargs), self._format_spy_calls( spy_or_call, self._format_spy_call_args), )) def assertSpyLastCalledWith(self, spy, *expected_args, **expected_kwargs): """Assert that a function was last called with the given arguments. This will imply :py:meth:`assertHasSpy`. Args: spy (callable or kgb.spies.FunctionSpy): The function or spy to check. *expected_args (tuple): Position arguments expected to be provided in the last call. **expected_kwargs (dict): Keyword arguments expected to be provided in the last call. Raises: AssertionError: The function was not called last with the provided arguments. """ self.assertSpyCalled(spy) if not spy.last_called_with(*expected_args, **expected_kwargs): self._kgb_assert_fail( 'The last call to %s was not passed args=%s, kwargs=%s.\n' '\n' 'It was last called with:\n' '\n' '%s' % ( self._format_spy_or_call(spy), safe_repr(expected_args), format_spy_kwargs(expected_kwargs), self._format_spy_call_args(spy.last_call), )) def assertSpyReturned(self, spy_or_call, return_value): """Assert that a function call returned the given value. If a spy is provided, all calls will be checked for a match. This will imply :py:meth:`assertHasSpy`. Args: spy_or_call (callable or kgb.spies.FunctionSpy or kgb.spies.SpyCall): The function, spy, or call to check. return_value (object or type): The value expected to be returned by any of the calls. Raises: AssertionError: The function never returned the provided value. """ if isinstance(spy_or_call, FunctionSpy): self.assertSpyCalled(spy_or_call) if not spy_or_call.returned(return_value): if isinstance(spy_or_call, SpyCall): self._kgb_assert_fail( 'This call to %s did not return %s.\n' '\n' 'It returned:\n' '\n' '%s' % ( self._format_spy_or_call(spy_or_call), safe_repr(return_value), self._format_spy_call_returned(spy_or_call), )) else: self._kgb_assert_fail( 'No call to %s returned %s.\n' '\n' 'The following values have been returned:\n' '\n' '%s' % ( self._format_spy_or_call(spy_or_call), safe_repr(return_value), self._format_spy_calls( spy_or_call, self._format_spy_call_returned), )) def assertSpyLastReturned(self, spy, return_value): """Assert that the last function call returned the given value. This will imply :py:meth:`assertHasSpy`. Args: spy (callable or kgb.spies.FunctionSpy): The function or spy to check. return_value (object or type): The value expected to be returned by the last call. Raises: AssertionError: The function's last call did not return the provided value. """ self.assertSpyCalled(spy) if not spy.last_returned(return_value): self._kgb_assert_fail( 'The last call to %s did not return %s.\n' '\n' 'It last returned:\n' '\n' '%s' % ( self._format_spy_or_call(spy), safe_repr(return_value), self._format_spy_call_returned(spy.last_call), )) def assertSpyRaised(self, spy_or_call, exception_cls): """Assert that a function call raised the given exception type. If a spy is provided, all calls will be checked for a match. This will imply :py:meth:`assertHasSpy`. Args: spy_or_call (callable or kgb.spies.FunctionSpy or kgb.spies.SpyCall): The function, spy, or call to check. exception_cls (type): The exception type expected to be raised by one of the calls. Raises: AssertionError: The function never raised the provided exception type. """ if isinstance(spy_or_call, FunctionSpy): self.assertSpyCalled(spy_or_call) if not spy_or_call.raised(exception_cls): if isinstance(spy_or_call, SpyCall): if spy_or_call.exception is not None: self._kgb_assert_fail( 'This call to %s did not raise %s. It raised %s.' % ( self._format_spy_or_call(spy_or_call), exception_cls.__name__, self._format_spy_call_raised(spy_or_call), )) else: self._kgb_assert_fail( 'This call to %s did not raise an exception.' % self._format_spy_or_call(spy_or_call)) else: has_raised = any( call.exception is not None for call in spy_or_call.calls ) if has_raised: self._kgb_assert_fail( 'No call to %s raised %s.\n' '\n' 'The following exceptions have been raised:\n\n' '%s' % ( self._format_spy_or_call(spy_or_call), exception_cls.__name__, self._format_spy_calls( spy_or_call, self._format_spy_call_raised), )) else: self._kgb_assert_fail( 'No call to %s raised an exception.' % self._format_spy_or_call(spy_or_call)) def assertSpyLastRaised(self, spy, exception_cls): """Assert that the last function call raised the given exception type. This will imply :py:meth:`assertHasSpy`. Args: spy (callable or kgb.spies.FunctionSpy): The function or spy to check. exception_cls (type): The exception type expected to be raised by the last call. Raises: AssertionError: The last function call did not raise the provided exception type. """ self.assertSpyCalled(spy) if not spy.last_raised(exception_cls): if spy.last_call.exception is not None: self._kgb_assert_fail( 'The last call to %s did not raise %s. It last ' 'raised %s.' % ( self._format_spy_or_call(spy), exception_cls.__name__, self._format_spy_call_raised(spy.last_call), )) else: self._kgb_assert_fail( 'The last call to %s did not raise an exception.' % self._format_spy_or_call(spy)) def assertSpyRaisedMessage(self, spy_or_call, exception_cls, message): """Assert that a function call raised the given exception/message. If a spy is provided, all calls will be checked for a match. This will imply :py:meth:`assertHasSpy`. Args: spy_or_call (callable or kgb.spies.FunctionSpy or kgb.spies.SpyCall): The function, spy, or call to check. exception_cls (type): The exception type expected to be raised by one of the calls. message (bytes or unicode): The expected message in a matching extension. Raises: AssertionError: The function never raised the provided exception type with the expected message. """ if isinstance(spy_or_call, FunctionSpy): self.assertSpyCalled(spy_or_call) if not spy_or_call.raised_with_message(exception_cls, message): if isinstance(spy_or_call, SpyCall): if spy_or_call.exception is not None: self._kgb_assert_fail( 'This call to %s did not raise %s with message %r.\n' '\n' 'It raised:\n' '\n' '%s' % ( self._format_spy_or_call(spy_or_call), exception_cls.__name__, message, self._format_spy_call_raised_with_message( spy_or_call), )) else: self._kgb_assert_fail( 'This call to %s did not raise an exception.' % self._format_spy_or_call(spy_or_call)) else: has_raised = any( call.exception is not None for call in spy_or_call.calls ) if has_raised: self._kgb_assert_fail( 'No call to %s raised %s with message %r.\n' '\n' 'The following exceptions have been raised:\n' '\n' '%s' % ( self._format_spy_or_call(spy_or_call), exception_cls.__name__, message, self._format_spy_calls( spy_or_call, self._format_spy_call_raised_with_message), )) else: self._kgb_assert_fail( 'No call to %s raised an exception.' % self._format_spy_or_call(spy_or_call)) def assertSpyLastRaisedMessage(self, spy, exception_cls, message): """Assert that the function last raised the given exception/message. This will imply :py:meth:`assertHasSpy`. Args: spy (callable or kgb.spies.FunctionSpy): The function or spy to check. exception_cls (type): The exception type expected to be raised by the last call. message (bytes or unicode): The expected message in the matching extension. Raises: AssertionError: The last function call did not raise the provided exception type with the expected message. """ self.assertSpyCalled(spy) if not spy.last_raised_with_message(exception_cls, message): if spy.last_call.exception is not None: self._kgb_assert_fail( 'The last call to %s did not raise %s with message %r.\n' '\n' 'It last raised:\n' '\n' '%s' % ( self._format_spy_or_call(spy), exception_cls.__name__, message, self._format_spy_call_raised_with_message( spy.last_call), )) else: self._kgb_assert_fail( 'The last call to %s did not raise an exception.' % self._format_spy_or_call(spy)) def _kgb_assert_fail(self, msg): """Raise an assertion failure. If this class is mixed into a unit test suite, this will call the main :py:meth:`unittest.TestCase.fail` method. Otherwise, it will simply raise an :py:exc:`AssertionError`. Args: msg (unicode): The assertion message. Raises: AssertionError: The assertion error to raise. """ if hasattr(self, 'fail') and hasattr(self, 'failureException'): # This is likely mixed in to a unit test. self.fail(msg) else: raise AssertionError(msg) def _format_spy_or_call(self, spy_or_call): """Format a spy or call for output in an assertion message. Args: spy_or_call (callable or kgb.spies.FunctionSpy or kgb.spies.SpyCall): The spy or call to format. Returns: unicode: The formatted name of the function. """ if isinstance(spy_or_call, FunctionSpy): spy = spy_or_call.orig_func elif isinstance(spy_or_call, SpyCall): spy = spy_or_call.spy.orig_func else: spy = spy_or_call name = spy.__name__ if isinstance(name, bytes): name = name.decode('utf-8') return name def _format_spy_calls(self, spy, formatter): """Format a list of calls for a spy. Args: spy (callable or kgb.spies.FunctionSpy): The spy to format. formatter (callable): A formatting function used for each recorded call. Returns: unicode: The formatted output of the calls. """ return '\n\n'.join( 'Call %d:\n%s' % (i, formatter(call, indent=2)) for i, call in enumerate(spy.calls) ) def _format_spy_call_args(self, call, indent=0): """Format a call's arguments. Args: call (kgb.spies.SpyCall): The call containing arguments to format. indent (int, optional): The indentation level for any output. Returns: unicode: The formatted output of the arguments for the call. """ return '%s\n%s' % ( self._format_spy_lines(call.args, prefix='args=', indent=indent), self._format_spy_lines(call.kwargs, prefix='kwargs=', indent=indent), ) def _format_spy_call_returned(self, call, indent=0): """Format the return value from a call. Args: call (kgb.spies.SpyCall): The call containing a return value to format. indent (int, optional): The indentation level for any output. Returns: unicode: The formatted return value from the call. """ return self._format_spy_lines(call.return_value, indent=indent) def _format_spy_call_raised(self, call, indent=0): """Format the exception type raised by a call. Args: call (kgb.spies.SpyCall): The call that raised an exception to format. indent (int, optional): The indentation level for any output. Returns: unicode: The formatted name of the exception raised by a call. """ return self._format_spy_lines(call.exception.__class__.__name__, indent=indent, format_data=False) def _format_spy_call_raised_with_message(self, call, indent=0): """Format the exception type and message raised by a call. Args: call (kgb.spies.SpyCall): The call that raised an exception to format. indent (int, optional): The indentation level for any output. Returns: unicode: The formatted name of the exception and accompanying message raised by a call. """ return '%s\n%s' % ( self._format_spy_lines(call.exception.__class__.__name__, prefix='exception=', indent=indent, format_data=False), self._format_spy_lines(str(call.exception), prefix='message=', indent=indent), ) def _format_spy_lines(self, data, prefix='', indent=0, format_data=True): """Format a multi-line list of output for an assertion message. Unless otherwise specified, the provided data will be formatted using :py:func:`pprint.pformat`. The first line of data will be prefixed, if a prefix is provided. Subsequent lines be aligned with the contents after the prefix. All line will be indented by the given amount. Args: data (object): The data to format. prefix (unicode, optional): An optional prefix for the first line in the data. indent (int, optional): The indentation level for any output. format_data (bool, optional): Whether to format the provided ``data`` using :py:func:`pprint.pformat`. Returns: unicode: The formatted string for the data. """ indent_str = ' ' * indent if format_data: data = pformat(data) data_lines = data.splitlines() lines = ['%s%s%s' % (indent_str, prefix, data_lines[0])] if len(data_lines) > 1: indent_str = ' ' * (indent + len(prefix)) lines += [ '%s%s' % (indent_str, line) for line in data_lines[1:] ] return '\n'.join(lines) # snake_case versions of the test functions. # # Useful for pytest and other uses. assert_has_spy = assertHasSpy assert_spy_called = assertSpyCalled assert_spy_not_called = assertSpyNotCalled assert_spy_call_count = assertSpyCallCount assert_spy_called_with = assertSpyCalledWith assert_spy_not_called_with = assertSpyNotCalledWith assert_spy_last_called_with = assertSpyLastCalledWith assert_spy_returned = assertSpyReturned assert_spy_last_returned = assertSpyLastReturned assert_spy_raised = assertSpyRaised assert_spy_last_raised = assertSpyLastRaised assert_spy_raised_message = assertSpyRaisedMessage assert_spy_last_raised_message = assertSpyLastRaisedMessage ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1730689021.0 kgb-7.2/kgb/asserts.py0000644000076500000240000000066514712033775014162 0ustar00chipx86staff"""Independent assertion functions. These assertion functions can be used in pytest or in other places where :py:class:`kgb.SpyAgency` can't be mixed. Version Added: 7.0 """ from __future__ import unicode_literals from kgb.agency import SpyAgency _agency = SpyAgency() __all__ = [] for name in vars(SpyAgency): if name.startswith('assert_'): globals()[name] = getattr(_agency, name) __all__.append(name) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1730689021.0 kgb-7.2/kgb/calls.py0000644000076500000240000001032014712033775013561 0ustar00chipx86staff"""Call tracking and checks for spiess.""" from __future__ import unicode_literals from kgb.pycompat import iteritems, text_type from kgb.signature import FunctionSig from kgb.utils import format_spy_kwargs class SpyCall(object): """Records arguments made to a spied function call. SpyCalls are created and stored by a FunctionSpy every time it is called. They're accessible through the FunctionSpy's ``calls`` attribute. """ def __init__(self, spy, args, kwargs): """Initialize the call. Args: spy (kgb.spies.FunctionSpy): The function spy that the call was made on. args (tuple): A tuple of positional arguments from the spy. These correspond to positional arguments in the function's signature. kwargs (dict): A dictionary of keyword arguments from the spy. These correspond to keyword arguments in the function's signature. """ self.spy = spy self.args = args self.kwargs = kwargs self.return_value = None self.exception = None def called_with(self, *args, **kwargs): """Return whether this call was made with the given arguments. Not every argument and keyword argument made in the call must be provided to this method. These can be a subset of the positional and keyword arguments in the call, but cannot contain any arguments not made in the call. Args: *args (tuple): The positional arguments made in the call, or a subset of those arguments (starting with the first argument). **kwargs (dict): The keyword arguments made in the call, or a subset of those arguments. Returns: bool: ``True`` if the call's arguments match the provided arguments. ``False`` if they do not. """ if len(args) > len(self.args): return False if self.args[:len(args)] != args: return False pos_args = self.spy._sig.arg_names if self.spy.func_type in (FunctionSig.TYPE_BOUND_METHOD, FunctionSig.TYPE_UNBOUND_METHOD): pos_args = pos_args[1:] all_args = dict(zip(pos_args, self.args)) all_args.update(self.kwargs) for key, value in iteritems(kwargs): if key not in all_args or all_args[key] != value: return False return True def returned(self, value): """Return whether this call returned the given value. Args: value (object): The expected returned value from the call. Returns: bool: ``True`` if this call returned the given value. ``False`` if it did not. """ return self.return_value == value def raised(self, exception_cls): """Return whether this call raised this exception. Args: exception_cls (type): The expected type of exception raised by the call. Returns: bool: ``True`` if this call raised the given exception type. ``False`` if it did not. """ return ((self.exception is None and exception_cls is None) or type(self.exception) is exception_cls) def raised_with_message(self, exception_cls, message): """Return whether this call raised this exception and message. Args: exception_cls (type): The expected type of exception raised by the call. message (unicode): The expected message from the exception. Returns: bool: ``True`` if this call raised the given exception type and message. ``False`` if it did not. """ return (self.exception is not None and self.raised(exception_cls) and text_type(self.exception) == message) def __repr__(self): return '' % ( self.args, format_spy_kwargs(self.kwargs), self.return_value, self.exception) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1730689021.0 kgb-7.2/kgb/contextmanagers.py0000644000076500000240000000235714712033775015700 0ustar00chipx86staff"""Standalone context managers for working with spies.""" from __future__ import unicode_literals from contextlib import contextmanager from kgb.agency import SpyAgency @contextmanager def spy_on(*args, **kwargs): """Spy on a function. By default, the spy will allow the call to go through to the original function. This can be disabled by passing ``call_original=False`` when initiating the spy. If disabled, the original function will never be called. This can also be passed a ``call_fake`` parameter pointing to another function to call instead of the original. If passed, this will take precedence over ``call_original``. The spy will only remain throughout the duration of the context. See :py:class:`~kgb.spies.FunctionSpy` for more details on arguments. Args: *args (tuple): Positional arguments to pass to :py:class:`~kgb.spies.FunctionSpy`. **kwargs (dict): Keyword arguments to pass to :py:class:`~kgb.spies.FunctionSpy`. Context: kgb.spies.FunctionSpy: The newly-created spy. """ agency = SpyAgency() spy = agency.spy_on(*args, **kwargs) try: yield spy finally: spy.unspy() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1730689021.0 kgb-7.2/kgb/errors.py0000644000076500000240000000524614712033775014012 0ustar00chipx86staff"""Spy-related errors.""" from __future__ import unicode_literals import traceback class InternalKGBError(Exception): """An internal error about the inner workings of KGB.""" def __init__(self, msg): """Initialize the error. Args: msg (unicode): The message to display. A general message about contacting support will be appended to this. """ super(InternalKGBError, self).__init__( '%s\n\n' 'This is an internal error in KGB. Please report it!' % msg) class ExistingSpyError(ValueError): """An error for when an existing spy was found on a function. This will provide a helpful error message explaining what went wrong, showing a backtrace of the original spy's setup in order to help diagnose the problem. """ def __init__(self, func): """Initialize the error. Args: func (callable): The function containing an existing spy. """ super(ExistingSpyError, self).__init__( 'The function %(func)r has already been spied on. Here is where ' 'that spy was set up:\n\n' '%(stacktrace)s\n' 'You may have encountered a crash in that test preventing the ' 'spy from being unregistered. Try running that test manually.' % { 'func': func, 'stacktrace': ''.join(traceback.format_stack( func.spy.init_frame.f_back)[-4:]), }) class IncompatibleFunctionError(ValueError): """An error for when a function signature is incompatible. This is used for the ``call_fake`` function passed in when setting up a spy. """ def __init__(self, func, func_sig, incompatible_func, incompatible_func_sig): """Initialize the error. Args: func (callable): The function containing the original signature. func_sig (kgb.signature.FunctionSig): The signature of ``func``. incompatible_func (callable): The function that was not compatible. incompatible_func_sig (kgb.signature.FunctionSig): The signature of ``incompatible_func``. """ super(IncompatibleFunctionError, self).__init__( 'The function signature of %r (%s) is not compatible with %r (%s).' % (incompatible_func, incompatible_func_sig.format_arg_spec(), func, func_sig.format_arg_spec())) class UnexpectedCallError(AssertionError): """A call was made to a spy that was not expected.""" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1730689021.0 kgb-7.2/kgb/ops.py0000644000076500000240000004765614712033775013312 0ustar00chipx86staff"""Planned operations for spies to perform.""" from __future__ import unicode_literals from kgb.errors import UnexpectedCallError class BaseSpyOperation(object): """Base class for a spy operation. Spy operations can be performed when a spied-on function is called, handling it according to a plan provided by the caller. They're registered by passing ``op=`` when spying on the function. There are a handful of built-in operations in KGB, but projects can subclass this and define their own. """ def handle_call(self, spy_call, *args, **kwargs): """Handle a call to this operation. Args: spy_call (kgb.calls.SpyCall): The call to handle. *args (tuple): Positional arguments passed into the call. This will be normalized to not contain an object instance for bound method or class methods. **kwargs (tuple): Keyword arguments passed into the call. Returns: object: The value to return to the caller of the spied function. Raises: Exception: Any exception to raise to the caller of the spied function. """ raise NotImplementedError def setup(self, spy, force_unbound=False): """Set up the operation. This associates the spy with the operation, and then returns a fake function to set for the spy, which will in turn call the operation's handler. Args: spy (kgb.spies.FunctionSpy): The spy this operation is for. force_unbound (bool, optional): Whether to force building an unbound fake function. This is needed for any functions called by an operation itself (and is thus used for nested operations). Version Added: 6.1 Returns: callable: The fake function to set up with the spy. """ self.spy = spy if spy.func_type == spy.TYPE_BOUND_METHOD and not force_unbound: def fake_func(_self, *args, **kwargs): return self._on_spy_call(*args, **kwargs) else: def fake_func(*args, **kwargs): return self._on_spy_call(*args, **kwargs) return fake_func def _on_spy_call(self, *args, **kwargs): """Internal handler for a call to this operation. This normalizes and sanity-checks the arguments and then calls :py:meth:`handle_call`. Args: *args (tuple): All positional arguments made in the call. This may include the object instance for bound methods or the class for classmethods. **kwargs (dict): All keyword arguments made in the call. Returns: object: The value to return to the caller of the spied function. Raises: Exception: Any exception to raise to the caller of the spied function. """ spy = self.spy spy_call = spy.last_call if spy.func_type == spy.TYPE_UNBOUND_METHOD: assert spy_call.called_with(*args[1:], **kwargs) else: assert spy_call.called_with(*args, **kwargs) return self.handle_call(spy_call, *args, **kwargs) class BaseMatchingSpyOperation(BaseSpyOperation): """Base class for a operation that handles calls based on matched rules. This helps subclasses to call consumer-defined handlers for calls based on some kind of conditions. For instance, based on arguments, or the order in which calls are made. """ def __init__(self, calls): """Initialize the operation. By default, this takes a list of configurations for matching calls, which the subclass will use to validate and handle a call. The calls are a list of dictionaries with the following keys: ``args`` (:py:class:`tuple`, optional): Positional arguments for a match. ``kwargs`` (:py:class:`dict`, optional): Keyword arguments for a match. ``call_fake`` (:py:class:`callable`, optional): A function to call when all arguments have matched. This takes precedence over ``call_original``. ``call_original`` (:py:class:`bool`, optional): Whether to call the original function. This is the default if ``call_fake`` is not provided. ``op`` (:py:class:`BaseSpyOperation`, optional): A spy operation to call instead of ``call_fake`` or ``call_original``. Subclasses may define custom keys. Version Changed: 6.1: Added support for ``op``. Args: calls (list of dict): A list of call match configurations. """ super(BaseMatchingSpyOperation, self).__init__() self._calls = calls def setup(self, spy, **kwargs): """Set up the operation. This invokes the common behavior for setting up a spy operation, and then goes through all registered calls to see if any call-provided operations also need to be set up. Version Added: 6.1 Args: spy (kgb.spies.FunctionSpy): The spy this operation is for. **kwargs (dict): Additional keyword arguments to pass to the parent. Returns: callable: The fake function to set up with the spy. """ result = super(BaseMatchingSpyOperation, self).setup(spy, **kwargs) new_calls = [] # Convert any operations into fake functions. # # Note that we have to force the resulting fake function to be # unbound, since all fake functions provided to an operation are # called without an owner. for call in self._calls: if 'op' in call: call = call.copy() call['call_fake'] = call.pop('op').setup(spy, force_unbound=True) new_calls.append(call) self._calls = new_calls return result def get_call_match_config(self, spy_call): """Return a call match configuration for a call. This will typically be one of the call match configurations provided during initialization. Subclasses must override this to return a dictionary containing information that can be used to assert, track, and handle a call. If they can't find a suitable call, they must raise :py:class:`kgb.errors.UnexpectedCallError`. Args: spy_call (kgb.calls.SpyCall): The call to return a match for. Returns: dict: The call match configuration. Raises: kgb.errors.UnexpectedCallError: A call match configuration could not be found. Details should be in the error message. """ raise NotImplementedError def validate_call(self, call_match_config): """Validate that the last call matches the call configuration. This will assert that the last call matches the ``args`` and ``kwargs`` from the given call match configuration. Subclasses can override this to check other conditions. Args: call_match_config (dict): The call match configuration returned from :py:meth:`get_call_match_config` for the last call. Raises: AssertionError: The call did not match the configuration. """ self.spy.agency.assertSpyCalledWith( self.spy.last_call, *call_match_config.get('args', ()), **call_match_config.get('kwargs', {})) def handle_call(self, spy_call, *args, **kwargs): """Handle a call to this operation. This will find a suitable call match configuration, if one was provided, and then call one of the following, in order of preference: 1. The spy operation (if ``op`` is set) 2. The fake function (if ``call_fake`` was provided) 3. The original function (if ``call_original`` is not set to ``False``) If none of the above are invoked, ``None`` will be returned instead. Version Changed: 6.1: Added support for ``op``. Args: spy_call (kgb.calls.SpyCall): The call to handle. *args (tuple): Positional arguments passed into the call. This will be normalized to not contain an object instance for bound method or class methods. **kwargs (tuple): Keyword arguments passed into the call. Returns: object: The value to return to the caller of the spied function. This may be returned by a fake or original function. Raises: AssertionError: The call did not match the returned configuration. Exception: Any exception to raise to the caller of the spied function. This may be raised by a fake or original function. kgb.errors.UnexpectedCallError: A call match configuration could not be found. Details should be in the error message. """ call_match_config = self.get_call_match_config(spy_call) assert call_match_config is not None self.validate_call(call_match_config) # We'll be respecting these arguments in the order that FunctionSpy # would with its parameters. func = call_match_config.get('call_fake') if func is not None: return func(*args, **kwargs) if call_match_config.get('call_original', True): return self.spy.call_original(*args, **kwargs) return None class SpyOpMatchAny(BaseMatchingSpyOperation): """A operation for handling one or more expected calls in any order. This is used to list the calls (specifying positional and keyword arguments) that are expected to be made, raising an error if any calls are made that weren't expected. Each of those expected sets of arguments can optionally result in a call to a fake function or the original function. This can be specified per set of arguments. Example: spy_on(traps.trigger, op=SpyOpMatchAny([ { 'args': ('hallway_lasers',), 'call_fake': _send_wolves, }, { 'args': ('trap_tile',), 'call_fake': _spill_hot_oil, }, { 'args': ('infrared_camera',), 'kwargs': { 'sector': 'underground_passage', }, 'call_original': False, }, ])) """ def __init__(self, calls): """Initialize the operation. This takes a list of configurations for matching calls, which can be called in any order. those calls are expected. The calls are a list of dictionaries with the following keys: ``args`` (:py:class:`tuple`, optional): Positional arguments for a match. ``kwargs`` (:py:class:`dict`, optional): Keyword arguments for a match. ``call_fake`` (:py:class:`callable`, optional): A function to call when all arguments have matched. This takes precedence over ``call_original``. ``call_original`` (:py:class:`bool`, optional): Whether to call the original function. This is the default if ``call_fake`` is not provided. Args: calls (list of dict): A list of call match configurations. """ super(SpyOpMatchAny, self).__init__(calls) def get_call_match_config(self, spy_call): """Return a call match configuration for a call. This will check if there are any call match configurations provided during initialization that match the call. Args: spy_call (kgb.calls.SpyCall): The call to return a match for. Returns: dict: The call match configuration. Raises: kgb.errors.UnexpectedCallError: A call match configuration could not be found. Details should be in the error message. """ for call_match_config in self._calls: if spy_call.called_with(*call_match_config.get('args', ()), **call_match_config.get('kwargs', {})): return call_match_config raise UnexpectedCallError( '%(spy)s was not called with any expected arguments.' % { 'spy': self.spy.func_name, }) class SpyOpMatchInOrder(BaseMatchingSpyOperation): """A operation for handling expected calls in a given order. This is used to list the calls (specifying positional and keyword arguments) that are expected to be made, in the order they should be made, raising an error if too many calls were made or a call didn't match the expected arguments. Each of those expected sets of arguments can optionally result in a call to a fake function or the original function. This can be specified per set of arguments. Example: spy_on(lockbox.enter_code, op=SpyOpMatchInOrder([ { 'args': (1, 2, 3, 4, 5, 6), 'call_original': False, }, { 'args': (9, 0, 2, 1, 0, 0), 'call_fake': _start_countdown, }, { 'args': (4, 8, 15, 16, 23, 42), 'kwargs': { 'secret_button_pushed': True, }, 'call_original': True, }, { 'args': (4, 8, 15, 16, 23, 42), 'kwargs': { 'secret_button_pushed': True, }, 'op': SpyOpRaise(Exception('Oh no')), }, ])) """ def __init__(self, calls): """Initialize the operation. This takes a list of configurations for matching calls, in the order those calls are expected. The calls are a list of dictionaries with the following keys: ``args`` (:py:class:`tuple`, optional): Positional arguments for a match. ``kwargs`` (:py:class:`dict`, optional): Keyword arguments for a match. ``call_fake`` (:py:class:`callable`, optional): A function to call when all arguments have matched. This takes precedence over ``call_original``. ``call_original`` (:py:class:`bool`, optional): Whether to call the original function. This is the default if ``call_fake`` is not provided. ``op`` (:py:class:`BaseSpyOperation`, optional): A spy operation to call instead of ``call_fake`` or ``call_original``. Args: calls (list of dict): A list of call match configurations. """ super(SpyOpMatchInOrder, self).__init__(calls) self._next = 0 def get_call_match_config(self, spy_call): """Return a call match configuration for a call. This will check if the spy call matches the next call match configuration in the list provided by the consumer. Args: spy_call (kgb.calls.SpyCall): The call to return a match for. Returns: dict: The call match configuration. Raises: kgb.errors.UnexpectedCallError: Too many calls were made to the function. """ i = self._next try: call_match_config = self._calls[i] except IndexError: raise UnexpectedCallError( '%(spy)s was called %(num_calls)s time(s), but only ' '%(expected_calls)s call(s) were expected. Latest call: ' '%(latest_call)s' % { 'expected_calls': len(self._calls), 'latest_call': spy_call, 'num_calls': i + 1, 'spy': self.spy.func_name, }) self._next += 1 return call_match_config class SpyOpRaise(BaseSpyOperation): """An operation for raising an exception. This is used to simulate a failure of some sort in a function or method. Example: spy_on(pen.emit_poison, op=SpyOpRaise(PoisonEmptyError())) """ def __init__(self, exc): """Initialize the operation. Args: exc (Exception): The exception instance to raise when the function is called. """ self.exc = exc def handle_call(self, *args, **kwargs): """Handle a call to this operation. This will raise the exception provided to the operation. Args: *args (tuple, ignored): Positional arguments passed into the call. **kwargs (tuple, ignored): Keyword arguments passed into the call. Raises: Exception: The exception provided to the operation. """ raise self.exc class SpyOpRaiseInOrder(SpyOpMatchInOrder): """An operation for raising exceptions in the order of calls. This is similar to :py:class:`SpyOpRaise`, but will raise a different exception for each call, based on a provided list. Example: spy_on(our_agent.get_identities, op=SpyOpRaiseInOrder([ PoisonEmptyError(), Kaboom(), MissingPenError(), ])) """ def __init__(self, exceptions): """Initialize the operation. Args: exceptions (list of Exception): The list of exceptions, one for each function call. """ super(SpyOpRaiseInOrder, self).__init__([ { 'op': SpyOpRaise(exc), } for exc in exceptions ]) class SpyOpReturn(BaseSpyOperation): """An operation for returning a value. This is used to simulate a simple result from a function call without having to override the method or provide a lambda. Example: spy_on(our_agent.get_identity, op=SpyOpReturn('nobody...')) """ def __init__(self, return_value): """Initialize the operation. Args: return_value (object): The value to return when the function is called. """ self.return_value = return_value def handle_call(self, *args, **kwargs): """Handle a call to this operation. This will return the value provided to the operation. Args: *args (tuple, ignored): Positional arguments passed into the call. **kwargs (tuple, ignored): Keyword arguments passed into the call. Returns: object: The return value provided to the operation. """ return self.return_value class SpyOpReturnInOrder(SpyOpMatchInOrder): """An operation for returning a value. This is similar to :py:class:`SpyOpReturn`, but will return a different value for each call, based on a provided list. Example: spy_on(our_agent.get_identities, op=SpyOpReturnInOrder([ 'nobody...', 'who?', 'never heard of them...', ])) """ def __init__(self, return_values): """Initialize the operation. Args: return_values (list): The list of values, one for each function call. """ super(SpyOpReturnInOrder, self).__init__([ { 'op': SpyOpReturn(value), } for value in return_values ]) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1730689021.0 kgb-7.2/kgb/pycompat.py0000644000076500000240000000063114712033775014323 0ustar00chipx86staff"""Python compatibility functions and types.""" from __future__ import unicode_literals import sys pyver = sys.version_info[:2] if pyver[0] == 2: text_type = unicode def iterkeys(d): return d.iterkeys() def iteritems(d): return d.iteritems() else: text_type = str def iterkeys(d): return iter(d.keys()) def iteritems(d): return iter(d.items()) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1730689021.0 kgb-7.2/kgb/pytest_plugin.py0000644000076500000240000000061014712033775015372 0ustar00chipx86staff"""Pytest plugin for kgb. Version Added: 7.0 """ from __future__ import unicode_literals import pytest from kgb import SpyAgency @pytest.fixture def spy_agency(): """Provide a KGB spy agency to a Pytest unit test. Yields: kgb.SpyAgency: The spy agency. """ agency = SpyAgency() try: yield agency finally: agency.unspy_all() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1730689021.0 kgb-7.2/kgb/signature.py0000644000076500000240000006061114712033775014474 0ustar00chipx86staff"""Function signature introspection and code generation.""" from __future__ import unicode_literals import inspect import logging import sys import types from kgb.errors import InternalKGBError from kgb.utils import get_defined_attr_value logger = logging.getLogger('kgb') class _UnsetArg(object): """Internal class for representation unset arguments on functions.""" def __repr__(self): """Return a string representation of this object. Returns: unicode: ``_UNSET_ARG``. """ return '_UNSET_ARG' _UNSET_ARG = _UnsetArg() class BaseFunctionSig(object): """Base class for a function signature introspector. This is responsible for taking a function (and a user-requested owner) and determining the actual owner, function type, function name, and arguments. It's also responsible for generating code that can be used to help define functions or perform calls, for use in spy code generation. How this is all done depends entirely on the version of Python. Subclasses must implement all this logic. Version Changed: 5.0: Added support for the following attributes: * :py:attr:`defined_func`. * :py:attr:`has_getter`. * :py:attr:`has_setter`. * :py:attr:`is_slippery`. Attributes: defined_func (callable or object): The actual function (or wrapping object) that's defined on somewhere in the owner's class hierarchy (or the function itself if this is a standalone function). This may differ from :py:attr:`func`. has_getter (bool): Whether this signature represents a descriptor with a ``__get__`` method. has_setter (bool): Whether this signature represents a descriptor with a ``__set__`` method. is_slippery (bool): Whether this represents a slippery function. This is a method on a class that returns a different function every time its attribute is accessed on an instance. This occurs when a method decorator is used that wraps a function on access and returns the wrapper function, but does not cache the wrapper function. These are returned as standard functions and not methods. Slippery functions can only be detected when an explicit owner is provided. """ #: The signature represents a standard function. TYPE_FUNCTION = 0 #: The signature represents a bound method. #: #: Bound methods are functions on an instance of a class, or classmethods. TYPE_BOUND_METHOD = 1 #: The signature represents an unbound method. #: #: Unbound methods are standard methods on a class. TYPE_UNBOUND_METHOD = 2 def __init__(self, func, owner=_UNSET_ARG, func_name=None): """Initialize the signature. Subclasses must override this to parse function types/ownership and available arguments. They must call :py:meth:`finalize_state` once they've calculated all signature state. Args: func (callable): The function to use for the signature. owner (type or object, optional): The owning class, as provided when spying on the function. This is not stored directly (as it may be invalid), but can be used for informative purposes for subclasses. func_name (str, optional): An explicit name for the function. This will be used instead of the function's specified name, and is usually a sign of a bad decorator. Version Added: 7.0 """ self.func = func self.func_type = self.TYPE_FUNCTION self.func_name = func_name or getattr(func, self.FUNC_NAME_ATTR) self.owner = None if hasattr(func, '__func__'): # This is an instancemethod on a class. Grab the real function # from it. self.real_func = func.__func__ else: self.real_func = func self.all_arg_names = [] self.arg_names = [] self.kwarg_names = [] self.args_param_name = [] self.kwargs_param_name = [] self.is_slippery = False self.has_getter = False self.has_setter = False def is_compatible_with(self, other_sig): """Return whether two function signatures are compatible. This will check if the signature for a function (the ``call_fake`` passed in, technically) is compatible with another (the spied function), to help ensure that unit tests with incompatible function signatures don't blow up with strange errors later. This will attempt to be somewhat flexible in what it considers compatible. Basically, so long as all the arguments passed in to the source function could be resolved using the argument list in the other function (taking into account things like positional argument names as keyword arguments), they're considered compatible. Args: other_sig (BaseFunctionSig): The other signature to check for compatibility with. Returns: bool: ``True`` if ``other_sig`` is considered compatible with this signature. ``False`` if it is not. """ source_args_name = self.args_param_name compat_args_name = other_sig.args_param_name source_kwargs_name = self.kwargs_param_name compat_kwargs_name = other_sig.kwargs_param_name if compat_args_name and compat_kwargs_name: return True if ((source_args_name and not compat_args_name) or (source_kwargs_name and not compat_kwargs_name)): return False source_args = self.arg_names compat_args = other_sig.arg_names compat_all_args = set(other_sig.all_arg_names) compat_kwargs = set(other_sig.kwarg_names) if self.func_type in (self.TYPE_BOUND_METHOD, self.TYPE_UNBOUND_METHOD): source_args = source_args[1:] compat_args = compat_args[1:] if (len(source_args) != len(compat_args) and ((len(source_args) < len(compat_args) and not source_args_name and not compat_kwargs.issuperset(source_args)) or (len(source_args) > len(compat_args) and not compat_args_name))): return False if (not compat_all_args.issuperset(self.kwarg_names) and not compat_kwargs_name): return False return True def format_forward_call_args(self): """Format arguments to pass in for forwarding a call. This will build a string for use in the forwarding call, which will pass every positional and keyword parameter defined for the function to forwarded function, along with the ``*args`` and ``**kwargs``, if specified. Returns: unicode: A string representing the arguments to pass when forwarding a call. """ _format_arg = self.format_forward_call_arg # Build the list of positional and keyword arguments. result = [ _format_arg(arg_name) for arg_name in self.arg_names ] + [ '%s=%s' % (arg_name, _format_arg(arg_name)) for arg_name in self.kwarg_names ] # Add the variable arguments. if self.args_param_name: result.append('*%s' % _format_arg(self.args_param_name)) if self.kwargs_param_name: result.append('**%s' % _format_arg(self.kwargs_param_name)) return ', '.join(result) def format_forward_call_arg(self, arg_name): """Return a string used to reference an argument in a forwarding call. Subclasses must implement this to return code the spy can use when generating a function to forward arguments in a call. Args: arg_name (unicode): The name of the argument. Returns: unicode: The string used to format the argument call. """ raise NotImplementedError def format_arg_spec(self): """Format the function's arguments for a new function definition. This will build a list of parameters for a function definition based on the argument specification found when introspecting a spied function. This consists of all supported argument types for the version of Python. Returns: unicode: A string representing an argument list for a function definition. """ raise NotImplementedError def finalize_state(self): """Finalize the state for the signature. This will set any remaining values for the signature based on the calculations already performed by the subclasses. This must be called at the end of a subclass's :py:meth:`__init__`. """ if self.owner is None: self.defined_func = self.func else: try: self.defined_func = get_defined_attr_value(self.owner, self.func_name) except AttributeError: # This was a dynamically-injected function. We won't find it # in the class hierarchy. Use the provided function instead. self.defined_func = self.func if not isinstance(self.defined_func, (types.FunctionType, types.MethodType, classmethod, staticmethod)): if hasattr(self.defined_func, '__get__'): self.has_getter = True if hasattr(self.defined_func, '__set__'): self.has_setter = True class FunctionSigPy2(BaseFunctionSig): """Function signature introspector for Python 2. This supports introspecting functions and generating code for use in spies when running on Python 2. """ FUNC_CLOSURE_ATTR = 'func_closure' FUNC_CODE_ATTR = 'func_code' FUNC_DEFAULTS_ATTR = 'func_defaults' FUNC_GLOBALS_ATTR = 'func_globals' FUNC_NAME_ATTR = 'func_name' METHOD_SELF_ATTR = 'im_self' def __init__(self, func, owner=_UNSET_ARG, func_name=None): """Initialize the signature. Subclasses must override this to parse function types/ownership and available arguments. Args: func (callable): The function to use for the signature. owner (type, optional): The owning class, as provided when spying on the function. The value is ignored for methods on which an owner can be calculated, favoring the calculated value instead. func_name (str, optional): An explicit name for the function. This will be used instead of the function's specified name, and is usually a sign of a bad decorator. Version Added: 7.0 """ super(FunctionSigPy2, self).__init__(func=func, owner=owner, func_name=func_name) func_name = self.func_name # Figure out the owner and method type. if inspect.ismethod(func): # This is a bound or unbound method. If it's unbound, and an # owner is not specified, we're going to need to warn the user, # since things are going to break on Python 3. # # Otherwise, we're going to determine the bound vs. unbound type # and use the owner specified by the method. (The provided owner # will be validated in FunctionSpy.) method_owner = func.im_self if method_owner is None: self.func_type = self.TYPE_UNBOUND_METHOD self.owner = func.im_class if owner is _UNSET_ARG: logger.warning('Unbound method owners can easily be ' 'determined on Python 2.x, but not on ' '3.x. Please pass owner= to spy_on() ' 'to set a specific owner for %r.', func) else: self.func_type = self.TYPE_BOUND_METHOD self.owner = method_owner elif owner is not _UNSET_ARG: # This is a standard function, but an owner (as an instance) has # been provided. Find out if the owner has this function (either # the actual instance or one with the same name and bytecode), # and if so, treat this as a bound method. # # This is necessary when trying to spy on a decorated method that # generates functions dynamically (using something like # functools.wraps). We call these "slippery functions." A # real-world example is something like Stripe's # stripe.Customer.delete() method, which is a different function # every time you call it. owner_func = getattr(owner, func_name, None) if (owner_func is not None and (owner_func is func or owner_func.func_code is func.func_code)): if inspect.isclass(owner): self.func_type = self.TYPE_UNBOUND_METHOD else: self.func_type = self.TYPE_BOUND_METHOD self.owner = owner self.is_slippery = owner_func is not func # Load information on the arguments. argspec = inspect.getargspec(func) all_args = argspec.args defaults = argspec.defaults if all_args and defaults: num_defaults = len(defaults) keyword_args = all_args[-num_defaults:] pos_args = all_args[:-num_defaults] else: pos_args = all_args keyword_args = [] self.all_arg_names = argspec.args self.arg_names = pos_args self.kwarg_names = keyword_args self.args_param_name = argspec.varargs self.kwargs_param_name = argspec.keywords self._defaults = argspec.defaults self.finalize_state() def format_forward_call_arg(self, arg_name): """Return a string used to reference an argument in a forwarding call. Args: arg_name (unicode): The name of the argument. Returns: unicode: The string used to format the argument call. """ return arg_name def format_arg_spec(self): """Format the function's arguments for a new function definition. This will build a list of parameters for a function definition based on the argument specification found when introspecting a spied function. This consists of all positional arguments, keyword arguments, and the special ``*args`` and ``**kwargs`` arguments. Returns: unicode: A string representing an argument list for a function definition. """ return inspect.formatargspec( args=self.all_arg_names, varargs=self.args_param_name, varkw=self.kwargs_param_name, defaults=self._defaults, formatvalue=lambda value: '=_UNSET_ARG')[1:-1] class FunctionSigPy3(BaseFunctionSig): """Function signature introspector for Python 3. This supports introspecting functions and generating code for use in spies when running on Python 3. There are some differences in function capabilities between Python 3.x releases (such as the addition of positional-only keyword arguments). This class provides compatibility for all these versions, currently up through Python 3.8. """ FUNC_CLOSURE_ATTR = '__closure__' FUNC_CODE_ATTR = '__code__' FUNC_DEFAULTS_ATTR = '__defaults__' FUNC_GLOBALS_ATTR = '__globals__' FUNC_NAME_ATTR = '__name__' METHOD_SELF_ATTR = '__self__' def __init__(self, func, owner=_UNSET_ARG, func_name=None): """Initialize the signature. Subclasses must override this to parse function types/ownership and available arguments. Args: func (callable): The function to use for the signature. owner (type, optional): The owning class, as provided when spying on the function. This is used only when spying on unbound or slippery methods. func_name (str, optional): An explicit name for the function. This will be used instead of the function's specified name, and is usually a sign of a bad decorator. Version Added: 7.0 """ super(FunctionSigPy3, self).__init__(func=func, owner=owner, func_name=func_name) if not hasattr(inspect, '_signature_from_callable'): raise InternalKGBError( 'Python %s.%s does not have inspect._signature_from_callable, ' 'which is needed in order to generate a Signature from a ' 'function.' % sys.version_info[:2]) func_name = self.func_name # Figure out the owner and method type. # # Python 3 does not officially have unbound methods. Methods on # instances are easily identified as types.MethodType, but # unbound methods are just standard functions without something # like __self__ to point to the parent class. # # However, the owner can generally be inferred (but not always!). # Python 3.3 introduced __qualname__, which is a string # identifying the path to the class within the containing module. # The path is expected to be traversable, unless it contains # "" in it, in which case it's defined somewhere you can't # get to it (like in a function). # # So to determine if it's an unbound method, we check to see what # __qualname__ looks like, and then we try to find it. If we can, # we grab the owner and identify it as an unbound method. If not, # it stays as a standard function. if inspect.ismethod(func): self.func_type = self.TYPE_BOUND_METHOD self.owner = func.__self__ elif '.' in func.__qualname__: if owner is not _UNSET_ARG: self.owner = owner try: self.is_slippery = ( owner is not _UNSET_ARG and getattr(owner, func_name) is not func ) except AttributeError: if '' in func.__qualname__: logger.warning( "%r doesn't have a function named \"%s\". This " "appears to be a decorator that doesn't " "preserve function names. Try passing " "func_name= when setting up the spy.", owner, func_name) else: logger.warning( "%r doesn't have a function named \"%s\". It's " "not clear why this is. Try passing func_name= " "when setting up the spy.", owner, func_name) if owner is _UNSET_ARG or inspect.isclass(owner): self.func_type = self.TYPE_UNBOUND_METHOD else: self.func_type = self.TYPE_BOUND_METHOD elif '' in func.__qualname__: # We can only assume this is a function. It might not be. self.func_type = self.TYPE_FUNCTION else: real_func = self.real_func method_owner = inspect.getmodule(real_func) for part in real_func.__qualname__.split('.')[:-1]: try: method_owner = getattr(method_owner, part) except AttributeError: method_owner = None break if method_owner is not None: self.func_type = self.TYPE_UNBOUND_METHOD self.owner = method_owner logger.warning('Determined the owner of %r to be %r, ' 'but it may be wrong. Please pass ' 'owner= to spy_on() to set a specific ' 'owner.', func, self.owner) # Load information on the arguments. sig = inspect._signature_from_callable( func, follow_wrapper_chains=False, skip_bound_arg=False, sigcls=inspect.Signature) all_args = [] args = [] kwargs = [] for param in sig.parameters.values(): kind = param.kind name = param.name if kind is param.POSITIONAL_OR_KEYWORD: # Standard arguments -- either positional or keyword. all_args.append(name) if param.default is param.empty: args.append(name) else: kwargs.append(name) elif kind is param.POSITIONAL_ONLY: # Positional-only arguments (Python 3.8+). all_args.append(name) args.append(name) elif kind is param.KEYWORD_ONLY: # Keyword-only arguments (Python 3+). kwargs.append(name) elif kind is param.VAR_POSITIONAL: # *args self.args_param_name = name elif kind is param.VAR_KEYWORD: # **kwargs self.kwargs_param_name = name self.all_arg_names = all_args self.arg_names = args self.kwarg_names = kwargs self._sig = sig self.finalize_state() def format_forward_call_arg(self, arg_name): """Return a string used to reference an argument in a forwarding call. Args: arg_name (unicode): The name of the argument. Returns: unicode: The string used to format the argument call. """ # Starting in Python 3, something changed with variables. Due to # the way we generate the hybrid code object, we can't always # reference the local variables directly. Sometimes we can, but # other times we have to get them from locals(). We can't always # get them from there, though, so instead we conditionally check # both. This is wordy, but necessary. return '_kgb_l["%(arg)s"] if "%(arg)s" in _kgb_l else %(arg)s' % { 'arg': arg_name, } def format_arg_spec(self): """Format the function's arguments for a new function definition. This will build a list of parameters for a function definition based on the argument specification found when introspecting a spied function. This consists of all positional arguments, positional-only arguments, keyword arguments, keyword-only arguments, and the special ``*args`` and ``**kwargs`` arguments. Returns: unicode: A string representing an argument list for a function definition. """ parameters = [] # Make a copy of the Signature and its parameters, but leave out # all type annotations. for orig_param in self._sig.parameters.values(): default = orig_param.default if (orig_param.kind is orig_param.POSITIONAL_OR_KEYWORD and default is not orig_param.empty): default = _UNSET_ARG parameters.append(inspect.Parameter( name=orig_param.name, kind=orig_param.kind, default=default)) sig = inspect.Signature(parameters=parameters) return str(sig)[1:-1] if sys.version_info[0] == 2: FunctionSig = FunctionSigPy2 elif sys.version_info[0] == 3: FunctionSig = FunctionSigPy3 else: raise Exception('Unsupported Python version') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1730689021.0 kgb-7.2/kgb/spies.py0000644000076500000240000012261714712033775013623 0ustar00chipx86stafffrom __future__ import absolute_import, unicode_literals import copy import inspect import types from kgb.calls import SpyCall from kgb.errors import (ExistingSpyError, IncompatibleFunctionError, InternalKGBError) from kgb.pycompat import iterkeys, pyver from kgb.signature import FunctionSig, _UNSET_ARG from kgb.utils import is_attr_defined_on_ancestor class FunctionSpy(object): """A spy infiltrating a function. A FunctionSpy takes the place of another function. It will record any calls made to the function for later inspection. By default, a FunctionSpy will allow the call to go through to the original function. This can be disabled by passing call_original=False when initiating the spy. If disabled, the original function will never be called. This can also be passed a call_fake parameter pointing to another function to call instead of the original. If passed, this will take precedence over call_original. """ #: The spy represents a standard function. TYPE_FUNCTION = FunctionSig.TYPE_FUNCTION #: The spy represents a bound method. #: #: Bound methods are functions on an instance of a class, or classmethods. TYPE_BOUND_METHOD = FunctionSig.TYPE_BOUND_METHOD #: The spy represents an unbound method. #: #: Unbound methods are standard methods on a class. TYPE_UNBOUND_METHOD = FunctionSig.TYPE_UNBOUND_METHOD _PROXY_METHODS = [ 'call_original', 'called_with', 'last_called_with', 'raised', 'last_raised', 'returned', 'last_returned', 'raised_with_message', 'last_raised_with_message', 'reset_calls', 'unspy', ] _FUNC_ATTR_DEFAULTS = { 'calls': [], 'called': False, 'last_call': None, } _spy_map = {} def __init__(self, agency, func, call_fake=None, call_original=True, op=None, owner=_UNSET_ARG, func_name=None): """Initialize the spy. This will begin spying on the provided function or method, injecting new code into the function to help record how it was called and what it returned, and adding methods and state onto the function for callers to access in order to get those results. Version Added: 7.0: Added support for specifying an explicit function name using ``func_name=``. Version Added: 5.0: Added support for specifying an instance in ``owner`` when spying on bound methods using decorators that return plain functions. Args: agency (kgb.agency.SpyAgency): The spy agency that manages this spy. func (callable): The function or method to spy on. call_fake (callable, optional): The optional function to call when this function is invoked. This cannot be specified if ``op`` is provided. call_original (bool, optional): Whether to call the original function when the spy is invoked. If ``False``, no function will be called. This is ignored if ``call_fake`` or ``op`` are provided. op (kgb.spies.BaseOperation, optional): An operation to perform. This cannot be specified if ``call_fake`` is provided. owner (type or object, optional): The owner of the function or method. If spying on an unbound method, this **must** be set to the class that owns it. If spying on a bound method that identifies as a plain function (which may happen if the method is decorated and dynamically returns a new function on access), this should be the instance of the object you're spying on. func_name (str, optional): An explicit name for the function. This will be used instead of the function's specified name, and is usually a sign of a bad decorator. Version Added: 7.0 """ # Start off by grabbing the current frame. This will be needed for # some errors. self.init_frame = inspect.currentframe() # Check the parameters passed to make sure that invalid data wasn't # provided. if op is not None and call_fake is not None: raise ValueError('op and call_fake cannot both be provided.') if hasattr(func, 'spy'): raise ExistingSpyError(func) if (not callable(func) or not hasattr(func, FunctionSig.FUNC_NAME_ATTR) or not (hasattr(func, FunctionSig.METHOD_SELF_ATTR) or hasattr(func, FunctionSig.FUNC_GLOBALS_ATTR))): raise ValueError('%r cannot be spied on. It does not appear to ' 'be a valid function or method.' % func) # Construct a signature for the function and begin closely inspecting # the parameters, making sure everything will be compatible so we # don't have unexpected breakages when setting up or calling spies. sig = FunctionSig(func=func, owner=owner, func_name=func_name) self._sig = sig # If the caller passed an explicit owner, check to see if it's at all # valid. Note that it may have been handled above (for unbound # methods). if owner is not _UNSET_ARG and owner is not self.owner: if self.func_type == self.TYPE_FUNCTION: raise ValueError( 'This function has no owner, but an owner was passed ' 'to spy_on().') else: if not hasattr(owner, self.func_name): raise ValueError('The owner passed does not contain the ' 'spied method.') elif (self.func_type == self.TYPE_BOUND_METHOD or (pyver[0] == 2 and self.func_type == self.TYPE_UNBOUND_METHOD)): raise ValueError( 'The owner passed does not match the actual owner of ' 'the bound method.') # We cannot currently spy on unbound methods that result in slippery # functions, so check for that and bail early. if (sig.is_slippery and self.func_type == self.TYPE_UNBOUND_METHOD): raise ValueError('Unable to spy on unbound slippery methods ' '(those that return a new function on each ' 'attribute access). Please spy on an instance ' 'instead.') # If call_fake was provided, check that it's valid and has a # compatible function signature. if op is not None: # We've already checked this above, but check it again. assert call_fake is None call_fake = op.setup(self) assert call_fake is not None if call_fake is not None: if not callable(call_fake): raise ValueError('%r cannot be used for call_fake. It does ' 'not appear to be a valid function or method.' % call_fake) call_fake_sig = FunctionSig(call_fake, func_name=func_name) if not sig.is_compatible_with(call_fake_sig): raise IncompatibleFunctionError( func=func, func_sig=sig, incompatible_func=call_fake, incompatible_func_sig=call_fake_sig) # Now that we're done validating, we can start setting state and # patching things. self.agency = agency self.orig_func = func self._real_func = sig.real_func self._call_orig_func = self._clone_function(self.orig_func) if self._get_owner_needs_patching(): # We need to store the original attribute value for the function, # as defined in the class that owns it. That may be the provided # or calculated owner, or a parent of it. # # This is needed because the function provided may not actually be # what's defined on the class. What's defined might be a decorator # that returns a function, and it might not even be the same # function each time it's accessed. self._owner_func_attr_value = \ self.owner.__dict__.get(self.func_name) # Now we can patch the owner to prevent conflicts between spies. self._patch_owner() else: self._owner_func_attr_value = self.orig_func # Determine what we're going to invoke when the spy is called. if call_fake: self.func = call_fake elif call_original: self.func = self.orig_func else: self.func = None # Build our proxy function. This is the spy itself, the function that # will actually be invoked when the spied-on function is called. self._build_proxy_func(func) # If we're calling the original function above, we need to replace what # we're calling with something that acts like the original function. # Otherwise, we'll just call the forwarding_call above in an infinite # loop. if self.func is self.orig_func: self.func = self._clone_function(self.func, code=self._old_code) @property def func_type(self): """The type of function being spied on. This will be one of :py:attr:`TYPE_FUNCTION`, :py:attr:`TYPE_UNBOUND_METHOD`, or :py:attr:`TYPE_BOUND_METHOD`. Type: int """ return self._sig.func_type @property def func_name(self): """The name of the function being spied on. Type: str """ return self._sig.func_name @property def owner(self): """The owner of the method, if a bound or unbound method. This will be ``None`` if there is no owner. Type: type """ return self._sig.owner @property def called(self): """Whether or not the spy was ever called.""" try: return self._real_func.called except AttributeError: return False @property def calls(self): """The list of calls made to the function. Each is an instance of :py:class:`SpyCall`. """ try: return self._real_func.calls except AttributeError: return [] @property def last_call(self): """The last call made to this function. If a spy hasn't been called yet, this will be ``None``. """ try: return self._real_func.last_call except AttributeError: return None def unspy(self, unregister=True): """Remove the spy from the function, restoring the original. The spy will, by default, be removed from the registry's list of spies. This can be disabled by passing ``unregister=False``, but don't do that. That's for internal use. Args: unregister (bool, optional): Whether to unregister the spy from the associated agency. """ real_func = self._real_func owner = self.owner assert hasattr(real_func, 'spy') del FunctionSpy._spy_map[id(self)] del real_func.spy for attr_name in iterkeys(self._FUNC_ATTR_DEFAULTS): delattr(real_func, attr_name) for func_name in self._PROXY_METHODS: delattr(real_func, func_name) setattr(real_func, FunctionSig.FUNC_CODE_ATTR, self._old_code) if owner is not None: self._set_method(owner, self.func_name, self._owner_func_attr_value) if unregister: self.agency.spies.remove(self) def call_original(self, *args, **kwargs): """Call the original function being spied on. The function will behave as normal, and will not trigger any spied behavior or call tracking. Args: *args (tuple): The positional arguments to pass to the function. **kwargs (dict): The keyword arguments to pass to the function. Returns: object: The return value of the function. Raises: Exception: Any exceptions raised by the function. """ if self.func_type == self.TYPE_BOUND_METHOD: return self._call_orig_func(self.owner, *args, **kwargs) else: if self.func_type == self.TYPE_UNBOUND_METHOD: if not args or not isinstance(args[0], self.owner): raise TypeError( 'The first argument to %s.call_original() must be ' 'an instance of %s.%s, since this is an unbound ' 'method.' % (self._call_orig_func.__name__, self.owner.__module__, self.owner.__name__)) return self._call_orig_func(*args, **kwargs) def called_with(self, *args, **kwargs): """Return whether the spy was ever called with the given arguments. This will check each and every recorded call to see if the arguments and keyword arguments match up. If at least one call does match, this will return ``True``. Not every argument and keyword argument made in the call must be provided to this method. These can be a subset of the positional and keyword arguments in the call, but cannot contain any arguments not made in the call. Args: *args (tuple): The positional arguments made in the call, or a subset of those arguments (starting with the first argument). **kwargs (dict): The keyword arguments made in the call, or a subset of those arguments. Returns: bool: ``True`` if there's at least one call matching these arguments. ``False`` if no call matches. """ return any( call.called_with(*args, **kwargs) for call in self.calls ) def last_called_with(self, *args, **kwargs): """Return whether the spy was last called with the given arguments. Not every argument and keyword argument made in the call must be provided to this method. These can be a subset of the positional and keyword arguments in the call, but cannot contain any arguments not made in the call. Args: *args (tuple): The positional arguments made in the call, or a subset of those arguments (starting with the first argument). **kwargs (dict): The keyword arguments made in the call, or a subset of those arguments. Returns: bool: ``True`` if the last call's arguments match the provided arguments. ``False`` if they do not. """ call = self.last_call return call is not None and call.called_with(*args, **kwargs) def returned(self, value): """Return whether the spy was ever called and returned the given value. This will check each and every recorded call to see if any of them returned the given value. If at least one call did, this will return ``True``. Args: value (object): The expected returned value from the call. Returns: bool: ``True`` if there's at least one call that returned this value. ``False`` if no call returned the value. """ return any( call.returned(value) for call in self.calls ) def last_returned(self, value): """Return whether the spy's last call returned the given value. Args: value (object): The expected returned value from the call. Returns: bool: ``True`` if the last call returned this value. ``False`` if it did not. """ call = self.last_call return call is not None and call.returned(value) def raised(self, exception_cls): """Return whether the spy was ever called and raised this exception. This will check each and every recorded call to see if any of them raised an exception of a given type. If at least one call does match, this will return ``True``. Args: exception_cls (type): The expected type of exception raised by a call. Returns: bool: ``True`` if there's at least one call raising the given exception type. ``False`` if no call matches. """ return any( call.raised(exception_cls) for call in self.calls ) def last_raised(self, exception_cls): """Return whether the spy's last call raised this exception. Args: exception_cls (type): The expected type of exception raised by a call. Returns: bool: ``True`` if the last call raised the given exception type. ``False`` if it did not. """ call = self.last_call return call is not None and call.raised(exception_cls) def raised_with_message(self, exception_cls, message): """Return whether the spy's calls ever raised this exception/message. This will check each and every recorded call to see if any of them raised an exception of a given type with the given message. If at least one call does match, this will return ``True``. Args: exception_cls (type): The expected type of exception raised by a call. message (unicode): The expected message from the exception. Returns: bool: ``True`` if there's at least one call raising the given exception type and message. ``False`` if no call matches. """ return any( call.raised_with_message(exception_cls, message) for call in self.calls ) def last_raised_with_message(self, exception_cls, message): """Return whether the spy's last call raised this exception/message. Args: exception_cls (type): The expected type of exception raised by a call. message (unicode): The expected message from the exception. Returns: bool: ``True`` if the last call raised the given exception type and message. ``False`` if it did not. """ call = self.last_call return (call is not None and call.raised_with_message(exception_cls, message)) def reset_calls(self): """Reset the list of calls recorded by this spy.""" self._real_func.calls = [] self._real_func.called = False self._real_func.last_call = None def __call__(self, *args, **kwargs): """Call the original function or fake function for the spy. This will be called automatically when calling the spied function, recording the call and the results from the call. Args: *args (tuple): Positional arguments passed to the function. **kwargs (dict): All dictionary arguments either passed to the function or default values for unspecified keyword arguments in the function signature. Returns: object: The result of the function call. """ record_args = args if self.func_type in (self.TYPE_BOUND_METHOD, self.TYPE_UNBOUND_METHOD): record_args = record_args[1:] sig = self._sig real_func = self._real_func func = self.func call = SpyCall(self, record_args, kwargs) real_func.calls.append(call) real_func.called = True real_func.last_call = call if func is None: result = None else: try: if sig.has_getter: # This isn't a standard function. It's a descriptor with # a __get__() method. We need to fetch the value it # returns. result = sig.defined_func.__get__(self.owner) if sig.is_slippery: # Since we know this represents a slippery function, # we need to take the function from the descriptor's # result and call it. result = result(*args, **kwargs) else: # This is a typical function/method. We can call it # directly. result = func(*args, **kwargs) except Exception as e: call.exception = e raise call.return_value = result return result def __repr__(self): """Return a string representation of the spy. This is mainly used for debugging information. It will show some details on the spied function and call log. Returns: unicode: The resulting string representation. """ func_type = self.func_type if func_type == self.TYPE_FUNCTION: func_type_str = 'function' qualname = self.func_name else: owner = self.owner if func_type == self.TYPE_BOUND_METHOD: # It's important we use __class__ instead of type(), because # we may be dealing with an old-style class. owner_cls = self.owner.__class__ if owner_cls is type: class_name = owner.__name__ func_type_str = 'classmethod' else: class_name = owner_cls.__name__ func_type_str = 'bound method' elif func_type == self.TYPE_UNBOUND_METHOD: class_name = owner.__name__ func_type_str = 'unbound method' qualname = '%s.%s of %r' % (class_name, self.func_name, owner) call_count = len(self.calls) if call_count == 1: calls_str = 'call' else: calls_str = 'calls' return '' % (func_type_str, qualname, len(self.calls), calls_str) def _get_owner_needs_patching(self): """Return whether the owner (if any) needs to be patched. Owners need patching if they're an instance, if the function is slippery, or if the function is defined on an ancestor of the class and not the class itself. See :py:meth:`_patch_owner` for what patching entails. Returns: bool: ``True`` if the owner needs patching. ``False`` if it does not. """ owner = self.owner return (owner is not None and (not inspect.isclass(owner) or self._sig.is_slippery or is_attr_defined_on_ancestor(owner, self.func_name))) def _patch_owner(self): """Patch the owner. This will create a new method in place of an existing one on the owner, in order to ensure that the owner has its own unique copy for spying purposes. Patching the owner will avoid collisions between spies in the event that the method being spied on is defined by a parent of the owner, rather than the owner itself. See :py:meth:`_get_owner_needs_patching` the conditions under which patching will occur. """ # Construct a replacement function for this method, and # re-assign it to the owner. We do this in order to prevent # two spies on the same method on two separate instances # of the class, or two subclasses of a common class owning the # method from conflicting with each other. real_func = self._clone_function(self._real_func) owner = self.owner if self.func_type == self.TYPE_BOUND_METHOD: method_type_args = [real_func, owner] if pyver[0] >= 3: method_type_args.append(owner) self._set_method(owner, self.func_name, types.MethodType(real_func, self.owner)) else: self._set_method(owner, self.func_name, real_func) self._real_func = real_func def _build_proxy_func(self, func): """Build the proxy function used to forward calls to this spy. This will construct a new function compatible with the signature of the provided function, which will call this spy whenever it's called. The bytecode of the provided function will be set to that of the generated proxy function. See the comment within this function for details on how this works. Args: func (callable): The function to proxy. """ # Prior to kgb 2.0, we attempted to optimistically replace # methods on a class with a FunctionSpy, forwarding on calls to the # fake or original function. This was the design since kgb 1.0, but # wasn't sufficient. We realized in the first release that this # wouldn't work for standard functions, and so we had two designs: # One for methods, one for standard functions. # # In kgb 2.0, in an effort to standardize behavior, we moved fully # to the method originally used for standard functions (largely due # to the fact that in Python 3, unbound methods are just standard # functions). # # Standard functions can't be replaced. Unlike a bound function, # we can't reliably figure out what dictionary it lives in (it # could be a locals() inside another function), and even if we # replace that, we can't replace all the copies that have been # imported up to this point. # # The only option is to change what happens when we call the # function. That's easier said than done. We can't just replace # the __call__ method on it, like you could on a fake method for # a class. # # What we must do is replace the code backing it. This must be # done carefully. The "co_freevars" and "co_cellvars" fields must # remain the same between the old code and the new one. The # actual bytecode and most of the rest of the fields can be taken # from another function (the "forwarding_call" function defined # inline below). # # Unfortunately, we no longer have access to "self" (since we # replaced "co_freevars"). Instead, we store a global mapping # of codes to spies. # # We also must build the function dynamically, using exec(). # The reason is that we want to accurately mimic the function # signature of the original function (in terms of specifying # the correct positional and keyword arguments). The way we format # arguments depends on the version of Python. We maintain # compatibility through the FunctionSig.format_arg_spec() methods # (which has implementations for both Python 2 and 3). # # We do use different values for the default keyword arguments, # which is actually okay. Within the function, these will all be # set to a special value (_UNSET_ARG), which is used later for # determining which keyword arguments were provided and which # were not. Anything attempting to inspect this function with # getargspec(), getfullargspec(), or inspect.Signature will get the # defaults from the original function, by way of the # original func.func_defaults attribute (on Python 2) or # __defaults__ (on Python 3). # # This forwarding function then needs to call the forwarded # function in exactly the same manner as it was called. That is, # if a keyword argument's value was passed in as a positional # argument, or a positional argument was specified as a keyword # argument in the call, then the forwarded function must be # called the same way, for argument tracking and signature # compatibility. # # In order to do this, we have to find out how forwarding_call was # called. This can be done by inspecting the bytecode of the # call in the parent frame and getting the number of positional # and keyword arguments used. From there, we can determine which # argument slots were specified and start looking for any keyword # arguments not set to _UNSET_ARG, passing them through to the # original function in the same order. Doing this requires # another exec() call in order to build out those arguments. # # Within the function, all imports and variables are prefixed to # avoid the possibility of collisions with arguments. # # Since we're only overriding the code, all other attributes (like # func_defaults, __doc__, etc.) will make use of those from # the original function. # # The result is that we've completely hijacked the original # function, making it call our own forwarding function instead. # It's a wonderful bag of tricks that are fully legal, but really # dirty. Somehow, it all really fits in with the idea of spies, # though. sig = self._sig spy_id = id(self) real_func = self._real_func forwarding_call = self._compile_forwarding_call_func( func=func, sig=sig, spy_id=spy_id) old_code, new_code = self._build_spy_code(func, forwarding_call) self._old_code = old_code setattr(real_func, FunctionSig.FUNC_CODE_ATTR, new_code) # Update our spy lookup map so the proxy function can easily find # the spy instance. FunctionSpy._spy_map[spy_id] = self # Update the attributes on the function. we'll be placing all spy # state and some proxy methods pointing to this spy, so that we can # easily access them through the function. real_func.spy = self real_func.__dict__.update(copy.deepcopy(self._FUNC_ATTR_DEFAULTS)) for proxy_func_name in self._PROXY_METHODS: assert not hasattr(real_func, proxy_func_name) setattr(real_func, proxy_func_name, getattr(self, proxy_func_name)) def _compile_forwarding_call_func(self, func, sig, spy_id): """Compile a forwarding call function for the spy. This will build the Python code for a function that approximates the function we're spying on, with the same function definition and closure behavior. Version Added: 7.1 Args: func (callable): The function being spied on. sig (kgb.signature.BaseFunctionSig): The function signature to use for this function. spy_id (int): The ID used for the spy registration. Returns: callable: The resulting forwarding function. """ closure_vars = func.__code__.co_freevars use_closure = bool(closure_vars) # If the function is in a closure, we'll need to mirror the closure # state by using the referenced variables within _kgb_forwarding_call # and by defining those variables within a closure. # # Start by setting up a string that will use each closure. if use_closure: # This is an efficient way of referencing each variable without # side effects (at least in Python 2.7 through 3.11). Tuple # operations are fast and compact, and don't risk any inadvertent # invocation of the variables. use_closure_vars_str = ( ' (%s)\n' % ', '.join(func.__code__.co_freevars) ) else: # No closure, so nothing to set up. use_closure_vars_str = '' # Now define the forwarding call. This will always be nested within # either a closure of an if statement, letting us build a single # version at the right indentation level, keeping this as fast and # portable as possible. forwarding_call_str = ( ' def _kgb_forwarding_call(%(params)s):\n' ' from kgb.spies import FunctionSpy as _kgb_cls\n' '%(use_closure_vars)s' ' _kgb_l = locals()\n' ' return _kgb_cls._spy_map[%(spy_id)s](%(call_args)s)\n' % { 'call_args': sig.format_forward_call_args(), 'params': sig.format_arg_spec(), 'spy_id': spy_id, 'use_closure_vars': use_closure_vars_str, } ) if use_closure: # We now need to put _kgb_forwarding_call in a closure, to mirror # the behavior of the spied function. The closure will provide # the closure variables, and will return the function we can # later use. func_code_str = ( 'def _kgb_forwarding_call_closure(%(params)s):\n' '%(forwarding_call)s' ' return _kgb_forwarding_call\n' % { 'forwarding_call': forwarding_call_str, 'params': ', '.join( '%s=None' % _var for _var in closure_vars ) } ) else: # No closure, so just define the function as-is. We will need to # wrap in an "if 1:" though, just to ensure indentation is fine. func_code_str = ( 'if 1:\n' '%s' % forwarding_call_str ) # We can now build our function. exec_locals = {} try: eval(compile(func_code_str, '', 'exec'), globals(), exec_locals) except Exception as e: raise InternalKGBError( 'Unable to compile a spy function for %(func)r: %(error)s' '\n\n' '%(code)s' % { 'code': func_code_str, 'error': e, 'func': func, }) # Grab the resulting compiled function out of the locals. if use_closure: # It's in our closure, so call that and get the result. forwarding_call = exec_locals['_kgb_forwarding_call_closure']() else: forwarding_call = exec_locals['_kgb_forwarding_call'] assert forwarding_call is not None return forwarding_call def _build_spy_code(self, func, forwarding_call): """Build a CodeType to inject into the spied function. This will create a function bytecode object that contains a mix of attributes from the original function and the forwarding call. The result can be injected directly into the spied function, containing just the right data to impersonate the function and call our own logic. Version Added: 7.1 Args: func (callable): The function being spied on. forwarding_call (callable): The spy forwarding call we built. Returns: tuple: A 2-tuple containing: 1. The spied function's code object (:py:class:`types.CodeType`). 1. The new spy code object (:py:class:`types.CodeType`). """ old_code = getattr(func, FunctionSig.FUNC_CODE_ATTR) temp_code = getattr(forwarding_call, FunctionSig.FUNC_CODE_ATTR) assert old_code != temp_code if hasattr(old_code, 'replace'): # Python >= 3.8 # # It's important we replace the code instead of building a new # one when possible. On Python 3.11, this will ensure that # state needed for exceptions (co_positions()) will be set # correctly. # # NOTE: Prior to kgb 7.2, we had set co_freevars and co_vellvars # here. This caused crashes with Python 3.13 beta 2 (the # latest release as of this writing -- June 19, 2024). We # don't appear to actually need or want to set these on # Python 3, so we removed this. replace_kwargs = { 'co_name': old_code.co_name, } if pyver >= (3, 11): replace_kwargs['co_qualname'] = old_code.co_qualname new_code = temp_code.replace(**replace_kwargs) else: # Python <= 3.7 # # We have to build this manually, using a combination of the # two. We won't bother with anything newer than Python 3.7. code_args = [temp_code.co_argcount] if pyver >= (3, 0): code_args.append(temp_code.co_kwonlyargcount) code_args += [ temp_code.co_nlocals, temp_code.co_stacksize, temp_code.co_flags, temp_code.co_code, temp_code.co_consts, temp_code.co_names, temp_code.co_varnames, temp_code.co_filename, old_code.co_name, temp_code.co_firstlineno, temp_code.co_lnotab, old_code.co_freevars, old_code.co_cellvars, ] new_code = types.CodeType(*code_args) assert new_code != old_code assert new_code != temp_code return old_code, new_code def _clone_function(self, func, code=None): """Clone a function, optionally providing new bytecode. This will create a new function that contains all the state of the original (including annotations and any default argument values). Args: func (types.FunctionType): The function to clone. code (types.CodeType, optional): The new bytecode for the function. If not specified, the original function's bytecode will be used. Returns: types.FunctionType: The new function. """ cloned_func = types.FunctionType( code or getattr(func, FunctionSig.FUNC_CODE_ATTR), getattr(func, FunctionSig.FUNC_GLOBALS_ATTR), getattr(func, FunctionSig.FUNC_NAME_ATTR), getattr(func, FunctionSig.FUNC_DEFAULTS_ATTR), getattr(func, FunctionSig.FUNC_CLOSURE_ATTR)) if pyver[0] >= 3: # Python 3.x doesn't support providing any of the new # metadata introduced in Python 3.x to the constructor of # FunctionType. We have to set those manually. for attr in ('__annotations__', '__kwdefaults__'): setattr(cloned_func, attr, copy.deepcopy(getattr(func, attr))) return cloned_func def _set_method(self, owner, name, method): """Set a new method on an object. This will set the method (or delete the attribute for one if setting ``None``). If setting on a class, this will use a standard :py:func:`setattr`/:py:func:`delattr`. If setting on an instance, this will use a standard :py:meth:`object.__setattr__`/:py:meth:`object.__delattr__` (in order to avoid triggering a subclass-defined version of :py:meth:`~object.__setattr__`/:py:meth:`~object.__delattr__`, which might lose or override our spy). Args: owner (type or object): The class or instance to set the method on. name (unicode): The name of the attribute to set for the method. method (types.MethodType): The method to set (or ``None`` to delete). """ if inspect.isclass(owner): if method is None: delattr(owner, name) else: setattr(owner, name, method) elif method is None: try: object.__delattr__(owner, name) except TypeError as e: if str(e) == "can't apply this __delattr__ to instance object": # This is likely Python 2.6, or early 2.7, where we can't # run object.__delattr__ on old-style classes. We have to # fall back to modifying __dict__. It's not ideal but # doable. del owner.__dict__[name] else: try: object.__setattr__(owner, name, method) except TypeError as e: if str(e) == "can't apply this __setattr__ to instance object": # Similarly as above, we have to default to dict # manipulation on this version of Python. owner.__dict__[name] = method ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1730689026.544373 kgb-7.2/kgb/tests/0000755000076500000240000000000014712034003013240 5ustar00chipx86staff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1730689021.0 kgb-7.2/kgb/tests/__init__.py0000644000076500000240000000000014712033775015356 0ustar00chipx86staff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1730689021.0 kgb-7.2/kgb/tests/base.py0000644000076500000240000000466614712033775014557 0ustar00chipx86stafffrom __future__ import unicode_literals import re import sys import textwrap if sys.version_info[:2] > (2, 7): import unittest else: import unittest2 as unittest from kgb.agency import SpyAgency class MathClass(object): def do_math(self, a=1, b=2, *args, **kwargs): assert isinstance(self, MathClass) return a + b def do_math_pos(self, a, b): assert isinstance(self, MathClass) return a + b def do_math_mixed(self, a, b=2, *args, **kwargs): assert isinstance(self, MathClass) return a + b @classmethod def class_do_math(cls, a=2, b=5, *args, **kwargs): assert issubclass(cls, MathClass) return a * b @classmethod def class_do_math_pos(cls, a, b): assert issubclass(cls, MathClass) return a * b class TestCase(unittest.TestCase): """Base class for test cases for kgb.""" ws_re = re.compile(r'\s+') def setUp(self): self.agency = SpyAgency() self.orig_class_do_math = MathClass.class_do_math def tearDown(self): MathClass.class_do_math = self.orig_class_do_math self.agency.unspy_all() def shortDescription(self): """Return the description of the current test. This changes the default behavior to replace all newlines with spaces, allowing a test description to span lines. It should still be kept short, though. Returns: bytes: The description of the test. """ doc = self._testMethodDoc if doc is not None: doc = doc.split('\n\n', 1)[0] doc = self.ws_re.sub(' ', doc).strip() return doc def make_func(self, code_str, func_name='func'): """Return a new function, created by the supplied Python code. This is used to create functions with signatures that depend on a specific version of Python, and would generate syntax errors on earlier versions. Args: code_str (unicode): The Python code used to create the function. func_name (unicode, optional): The expected name of the function. Returns: callable: The resulting function. Raises: Exception: There was an error with the supplied code. """ scope = {} exec(textwrap.dedent(code_str), scope) return scope[func_name] ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1730689026.5446012 kgb-7.2/kgb/tests/py3/0000755000076500000240000000000014712034003013753 5ustar00chipx86staff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1730689021.0 kgb-7.2/kgb/tests/py3/__init__.py0000644000076500000240000000000014712033775016071 0ustar00chipx86staff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1730689021.0 kgb-7.2/kgb/tests/py3/test_function_spy.py0000644000076500000240000004474614712033775020142 0ustar00chipx86staffimport functools import inspect import logging import sys from unittest import SkipTest from kgb.tests.base import TestCase logger = logging.getLogger('kgb') def require_func_pos_only_args(func): """Require positional-only arguments for a function. If not available, the test will be skippd. Args: func (callable): The unit test function to decorate. """ @functools.wraps(func) def _wrap(*args, **kwargs): if sys.version_info[:2] >= (3, 8): return func(*args, **kwargs) else: raise SkipTest('inspect.getargspec is not available on Python 3.8') return _wrap class FunctionSpyTests(TestCase): """Python 3 unit tests for kgb.spies.FunctionSpy.""" def test_spy_with_function_copies_attribute_state(self): """Testing FunctionSpy with functions copies all attribute states""" def func(a: str, b: int = 1, *, c: int = 3) -> bool: return False self.agency.spy_on(func) spy_func = func.spy.func self.assertIsNot(spy_func, func) self.assertEqual( spy_func.__kwdefaults__, { 'c': 3, }) self.assertEqual( spy_func.__annotations__, { 'a': str, 'b': int, 'c': int, 'return': bool, }) def test_spy_with_bound_methods_copies_attribute_state(self): """Testing FunctionSpy with bound methods copies all attribute states """ class A: def func(a: str, b: int = 1, *, c: int = 3) -> bool: return False a = A() self.agency.spy_on(a.func) spy_func = a.func.spy.func self.assertIsNot(spy_func, a.func) self.assertEqual( spy_func.__kwdefaults__, { 'c': 3, }) self.assertEqual( spy_func.__annotations__, { 'a': str, 'b': int, 'c': int, 'return': bool, }) def test_spy_with_unbound_methods_copies_attribute_state(self): """Testing FunctionSpy with unbound methods copies all attribute states """ class A: def func(a: str, b: int = 1, *, c: int = 3) -> bool: return False self.agency.spy_on(A.func) spy_func = A.func.spy.func self.assertIsNot(spy_func, A.func) self.assertEqual( spy_func.__kwdefaults__, { 'c': 3, }) self.assertEqual( spy_func.__annotations__, { 'a': str, 'b': int, 'c': int, 'return': bool, }) @require_func_pos_only_args def test_call_with_function_and_positional_only_args(self): """Testing FunctionSpy calls with function containing positional-only arguments """ func = self.make_func(""" def func(a, b=1, /): return a * b """) self.agency.spy_on(func) result = func(2, 5) self.assertEqual(result, 10) self.assertEqual(len(func.spy.calls), 1) self.assertEqual(func.spy.calls[0].args, (2, 5)) self.assertEqual(func.spy.calls[0].kwargs, {}) @require_func_pos_only_args def test_call_with_function_and_positional_only_args_no_pos_passed(self): """Testing FunctionSpy calls with function containing positional-only arguments and no positional argument passed """ func = self.make_func(""" def func(a, b=2, /): return a * b """) self.agency.spy_on(func) result = func(2) self.assertEqual(result, 4) self.assertEqual(len(func.spy.calls), 1) self.assertEqual(func.spy.calls[0].args, (2, 2)) self.assertEqual(func.spy.calls[0].kwargs, {}) def test_call_with_function_and_keyword_only_args(self): """Testing FunctionSpy calls with function containing keyword-only arguments """ def func(a, *, b=2): return a * b self.agency.spy_on(func) result = func(2, b=5) self.assertEqual(result, 10) self.assertEqual(len(func.spy.calls), 1) self.assertEqual(func.spy.calls[0].args, (2,)) self.assertEqual(func.spy.calls[0].kwargs, {'b': 5}) def test_call_with_function_and_keyword_only_args_no_kw_passed(self): """Testing FunctionSpy calls with function containing keyword-only arguments and no keyword passed """ def func(a, *, b=2): return a * b self.agency.spy_on(func) result = func(2) self.assertEqual(result, 4) self.assertEqual(len(func.spy.calls), 1) self.assertEqual(func.spy.calls[0].args, (2,)) self.assertEqual(func.spy.calls[0].kwargs, {'b': 2}) def test_init_with_unbound_method_decorator_bad_func_name(self): """Testing FunctionSpy construction with a decorator not preserving an unbound method name """ def bad_deco(func): def _wrapper(*args, **kwargs): return func(*args, **kwargs) return _wrapper class MyObject(object): @bad_deco def my_method(self): pass self.agency.spy_on(logger.warning) self.agency.spy_on(MyObject.my_method, owner=MyObject) self.agency.assertSpyCalledWith( logger.warning, "%r doesn't have a function named \"%s\". This " "appears to be a decorator that doesn't " "preserve function names. Try passing " "func_name= when setting up the spy.", MyObject, '_wrapper') def test_init_with_unbound_method_decorator_corrected_func_name(self): """Testing FunctionSpy construction with a decorator not preserving an unbound method name and explicit func_name= provided """ def bad_deco(func): def _wrapper(*args, **kwargs): return func(*args, **kwargs) return _wrapper class MyObject(object): @bad_deco def my_method(self): pass self.agency.spy_on(logger.warning) self.agency.spy_on(MyObject.my_method, owner=MyObject, func_name='my_method') self.agency.assertSpyNotCalled(logger.warning) def test_getfullargspec_with_function(self): """Testing FunctionSpy in inspect.getfullargspec() with function""" def func(a, b=2): return a * b self.agency.spy_on(func) argspec = inspect.getfullargspec(func) self.assertEqual(argspec.args, ['a', 'b']) self.assertIsNone(argspec.varargs) self.assertIsNone(argspec.varkw) self.assertEqual(argspec.defaults, (2,)) self.assertEqual(argspec.kwonlyargs, []) self.assertIsNone(argspec.kwonlydefaults) self.assertEqual(argspec.annotations, {}) @require_func_pos_only_args def test_getfullargspec_with_function_pos_only(self): """Testing FunctionSpy in inspect.getfullargspec() with function and positional-only arguments """ func = self.make_func(""" def func(a, b=2, /, c=3): return a * b """) self.agency.spy_on(func) argspec = inspect.getfullargspec(func) self.assertEqual(argspec.args, ['a', 'b', 'c']) self.assertIsNone(argspec.varargs) self.assertIsNone(argspec.varkw) self.assertEqual(argspec.defaults, (2, 3)) self.assertEqual(argspec.kwonlyargs, []) self.assertIsNone(argspec.kwonlydefaults) self.assertEqual(argspec.annotations, {}) def test_getfullargspec_with_function_keyword_only(self): """Testing FunctionSpy in inspect.getfullargspec() with function and keyword-only arguments """ def func(*, a, b=2): return a * b self.agency.spy_on(func) argspec = inspect.getfullargspec(func) self.assertEqual(argspec.args, []) self.assertIsNone(argspec.varargs) self.assertIsNone(argspec.varkw) self.assertIsNone(argspec.defaults) self.assertEqual(argspec.kwonlyargs, ['a', 'b']) self.assertEqual(argspec.kwonlydefaults, { 'b': 2, }) self.assertEqual(argspec.annotations, {}) def test_getfullargspec_with_function_annotations(self): """Testing FunctionSpy in inspect.getfullargspec() with function and annotations """ def func(a: int, b: int = 2) -> int: return a * b self.agency.spy_on(func) argspec = inspect.getfullargspec(func) self.assertEqual(argspec.args, ['a', 'b']) self.assertIsNone(argspec.varargs) self.assertIsNone(argspec.varkw) self.assertEqual(argspec.defaults, (2,)) self.assertEqual(argspec.kwonlyargs, []) self.assertIsNone(argspec.kwonlydefaults) self.assertEqual(argspec.annotations, { 'a': int, 'b': int, 'return': int, }) def test_getfullargspec_with_bound_method(self): """Testing FunctionSpy in inspect.getfullargspec() with bound method""" class MyObject: def func(self, a, b=2): return a * b obj = MyObject() self.agency.spy_on(obj.func) argspec = inspect.getfullargspec(obj.func) self.assertEqual(argspec.args, ['self', 'a', 'b']) self.assertIsNone(argspec.varargs) self.assertIsNone(argspec.varkw) self.assertEqual(argspec.defaults, (2,)) self.assertEqual(argspec.kwonlyargs, []) self.assertIsNone(argspec.kwonlydefaults) self.assertEqual(argspec.annotations, {}) @require_func_pos_only_args def test_getfullargspec_with_bound_method_pos_only(self): """Testing FunctionSpy in inspect.getfullargspec() with bound method and positional-only arguments """ MyObject = self.make_func( """ class MyObject: def func(self, a, b=2, /, c=3): return a * b """, func_name='MyObject') obj = MyObject() self.agency.spy_on(obj.func) argspec = inspect.getfullargspec(obj.func) self.assertEqual(argspec.args, ['self', 'a', 'b', 'c']) self.assertIsNone(argspec.varargs) self.assertIsNone(argspec.varkw) self.assertEqual(argspec.defaults, (2, 3)) self.assertEqual(argspec.kwonlyargs, []) self.assertIsNone(argspec.kwonlydefaults) self.assertEqual(argspec.annotations, {}) def test_getfullargspec_with_bound_method_keyword_only(self): """Testing FunctionSpy in inspect.getfullargspec() with bound method and keyword-only arguments """ class MyObject: def func(self, *, a, b=2): return a * b obj = MyObject() self.agency.spy_on(obj.func) argspec = inspect.getfullargspec(obj.func) self.assertEqual(argspec.args, ['self']) self.assertIsNone(argspec.varargs) self.assertIsNone(argspec.varkw) self.assertIsNone(argspec.defaults) self.assertEqual(argspec.kwonlyargs, ['a', 'b']) self.assertEqual(argspec.kwonlydefaults, { 'b': 2, }) self.assertEqual(argspec.annotations, {}) def test_getfullargspec_with_bound_method_annotations(self): """Testing FunctionSpy in inspect.getfullargspec() with bound method and annotations """ class MyObject: def func(self, a: int, b: int = 2) -> int: return a * b obj = MyObject() self.agency.spy_on(obj.func) argspec = inspect.getfullargspec(obj.func) self.assertEqual(argspec.args, ['self', 'a', 'b']) self.assertIsNone(argspec.varargs) self.assertIsNone(argspec.varkw) self.assertEqual(argspec.defaults, (2,)) self.assertEqual(argspec.kwonlyargs, []) self.assertIsNone(argspec.kwonlydefaults) self.assertEqual(argspec.annotations, { 'a': int, 'b': int, 'return': int, }) def test_getfullargspec_with_unbound_method(self): """Testing FunctionSpy in inspect.getfullargspec() with unbound method """ class MyObject: def func(self, a, b=2): return a * b self.agency.spy_on(MyObject.func) argspec = inspect.getfullargspec(MyObject.func) self.assertEqual(argspec.args, ['self', 'a', 'b']) self.assertIsNone(argspec.varargs) self.assertIsNone(argspec.varkw) self.assertEqual(argspec.defaults, (2,)) self.assertEqual(argspec.kwonlyargs, []) self.assertIsNone(argspec.kwonlydefaults) self.assertEqual(argspec.annotations, {}) @require_func_pos_only_args def test_getfullargspec_with_unbound_method_pos_only(self): """Testing FunctionSpy in inspect.getfullargspec() with unbound method and positional-only arguments """ MyObject = self.make_func( """ class MyObject: def func(self, a, b=2, /, c=3): return a * b """, func_name='MyObject') self.agency.spy_on(MyObject.func) argspec = inspect.getfullargspec(MyObject.func) self.assertEqual(argspec.args, ['self', 'a', 'b', 'c']) self.assertIsNone(argspec.varargs) self.assertIsNone(argspec.varkw) self.assertEqual(argspec.defaults, (2, 3)) self.assertEqual(argspec.kwonlyargs, []) self.assertIsNone(argspec.kwonlydefaults) self.assertEqual(argspec.annotations, {}) def test_getfullargspec_with_unbound_method_keyword_only(self): """Testing FunctionSpy in inspect.getfullargspec() with unbound method and keyword-only arguments """ class MyObject: def func(self, *, a, b=2): return a * b self.agency.spy_on(MyObject.func) argspec = inspect.getfullargspec(MyObject.func) self.assertEqual(argspec.args, ['self']) self.assertIsNone(argspec.varargs) self.assertIsNone(argspec.varkw) self.assertIsNone(argspec.defaults) self.assertEqual(argspec.kwonlyargs, ['a', 'b']) self.assertEqual(argspec.kwonlydefaults, { 'b': 2, }) self.assertEqual(argspec.annotations, {}) def test_getfullargspec_with_unbound_method_annotations(self): """Testing FunctionSpy in inspect.getfullargspec() with unbound method and annotations """ class MyObject: def func(self, a: int, b: int = 2) -> int: return a * b self.agency.spy_on(MyObject.func) argspec = inspect.getfullargspec(MyObject.func) self.assertEqual(argspec.args, ['self', 'a', 'b']) self.assertIsNone(argspec.varargs) self.assertIsNone(argspec.varkw) self.assertEqual(argspec.defaults, (2,)) self.assertEqual(argspec.kwonlyargs, []) self.assertIsNone(argspec.kwonlydefaults) self.assertEqual(argspec.annotations, { 'a': int, 'b': int, 'return': int, }) def test_getfullargspec_with_classmethod(self): """Testing FunctionSpy in inspect.getfullargspec() with classmethod """ class MyObject: @classmethod def func(cls, a, b=2): return a * b self.agency.spy_on(MyObject.func) argspec = inspect.getfullargspec(MyObject.func) self.assertEqual(argspec.args, ['cls', 'a', 'b']) self.assertIsNone(argspec.varargs) self.assertIsNone(argspec.varkw) self.assertEqual(argspec.defaults, (2,)) self.assertEqual(argspec.kwonlyargs, []) self.assertIsNone(argspec.kwonlydefaults) self.assertEqual(argspec.annotations, {}) @require_func_pos_only_args def test_getfullargspec_with_classmethod_pos_only(self): """Testing FunctionSpy in inspect.getfullargspec() with classmethod and positional-only arguments """ MyObject = self.make_func( """ class MyObject: @classmethod def func(cls, a, b=2, /, c=3): return a * b """, func_name='MyObject') self.agency.spy_on(MyObject.func) argspec = inspect.getfullargspec(MyObject.func) self.assertEqual(argspec.args, ['cls', 'a', 'b', 'c']) self.assertIsNone(argspec.varargs) self.assertIsNone(argspec.varkw) self.assertEqual(argspec.defaults, (2, 3)) self.assertEqual(argspec.kwonlyargs, []) self.assertIsNone(argspec.kwonlydefaults) self.assertEqual(argspec.annotations, {}) def test_getfullargspec_with_classmethod_keyword_only(self): """Testing FunctionSpy in inspect.getfullargspec() with classmethod and keyword-only arguments """ class MyObject: @classmethod def func(cls, *, a, b=2): return a * b self.agency.spy_on(MyObject.func) argspec = inspect.getfullargspec(MyObject.func) self.assertEqual(argspec.args, ['cls']) self.assertIsNone(argspec.varargs) self.assertIsNone(argspec.varkw) self.assertIsNone(argspec.defaults) self.assertEqual(argspec.kwonlyargs, ['a', 'b']) self.assertEqual(argspec.kwonlydefaults, { 'b': 2, }) self.assertEqual(argspec.annotations, {}) def test_getfullargspec_with_classmethod_annotations(self): """Testing FunctionSpy in inspect.getfullargspec() with classmethod and annotations """ class MyObject: @classmethod def func(cls, a: int, b: int = 2) -> int: return a * b self.agency.spy_on(MyObject.func) argspec = inspect.getfullargspec(MyObject.func) self.assertEqual(argspec.args, ['cls', 'a', 'b']) self.assertIsNone(argspec.varargs) self.assertIsNone(argspec.varkw) self.assertEqual(argspec.defaults, (2,)) self.assertEqual(argspec.kwonlyargs, []) self.assertIsNone(argspec.kwonlydefaults) self.assertEqual(argspec.annotations, { 'a': int, 'b': int, 'return': int, }) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1730689021.0 kgb-7.2/kgb/tests/test_context_managers.py0000644000076500000240000000164514712033775020237 0ustar00chipx86stafffrom __future__ import unicode_literals from kgb.contextmanagers import spy_on from kgb.tests.base import MathClass, TestCase class SpyOnTests(TestCase): """Unit tests for spies.contextmanagers.spy_on.""" def test_spy_on(self): """Testing spy_on context manager""" obj = MathClass() with spy_on(obj.do_math): self.assertTrue(hasattr(obj.do_math, 'spy')) result = obj.do_math() self.assertEqual(result, 3) self.assertFalse(hasattr(obj.do_math, 'spy')) def test_expose_spy(self): """Testing spy_on exposes `spy` via context manager""" obj = MathClass() with spy_on(obj.do_math) as spy: self.assertTrue(hasattr(obj.do_math, 'spy')) self.assertIs(obj.do_math.spy, spy) result = obj.do_math() self.assertEqual(result, 3) self.assertFalse(hasattr(obj.do_math, 'spy')) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1730689021.0 kgb-7.2/kgb/tests/test_function_spy.py0000644000076500000240000020222614712033775017414 0ustar00chipx86stafffrom __future__ import unicode_literals import functools import inspect import re import sys import traceback import types import unittest from contextlib import contextmanager from warnings import catch_warnings from kgb.errors import ExistingSpyError, IncompatibleFunctionError from kgb.pycompat import text_type from kgb.signature import FunctionSig from kgb.tests.base import MathClass, TestCase has_getargspec = hasattr(inspect, 'getargspec') has_getfullargspec = hasattr(inspect, 'getfullargspec') def require_getargspec(func): """Require getargspec for a unit test. If not available, the test will be skippd. Args: func (callable): The unit test function to decorate. """ @functools.wraps(func) def _wrap(*args, **kwargs): if has_getargspec: with catch_warnings(record=True): return func(*args, **kwargs) else: raise unittest.SkipTest( 'inspect.getargspec is not available on Python %s.%s.%s' % sys.version_info[:3]) return _wrap def do_math(a=1, b=2, *args, **kwargs): return a - b def do_math_pos(a, b): return a - b def do_math_mixed(a, b=2, *args, **kwargs): return a - b def fake_do_math(self, a, b, *args, **kwargs): assert isinstance(self, MathClass) return a - b def fake_class_do_math(cls, a, b, *args, **kwargs): assert issubclass(cls, MathClass) return a - b def something_awesome(): return 'Tada!' def fake_something_awesome(): return r'\o/' @contextmanager def do_context(a=1, b=2): yield a + b class AdderObject(object): def func(self): assert isinstance(self, AdderObject) return [self.add_one(i) for i in (1, 2, 3)] def add_one(self, i): assert isinstance(self, AdderObject) return i + 1 @classmethod def class_func(cls): assert cls is AdderObject return [cls.class_add_one(i) for i in (1, 2, 3)] @classmethod def class_add_one(cls, i): assert issubclass(cls, AdderObject) return i + 1 class AdderSubclass(AdderObject): pass class slippery_func(object): count = 0 def __init__(self): pass def __call__(self, method): self.method = method return self def __get__(self, obj, objtype=None): @functools.wraps(self.method) def _wrapper(*args, **kwargs): return self.method(obj, _wrapper.counter) _wrapper.counter = self.count type(self).count += 1 return _wrapper class SlipperyFuncObject(object): @slippery_func() def my_func(self, value): return value class FunctionSpyTests(TestCase): """Test cases for kgb.spies.FunctionSpy.""" def test_construction_with_call_precedence(self): """Testing FunctionSpy construction with call option precedence""" spy = self.agency.spy_on(something_awesome, call_fake=fake_something_awesome, call_original=True) self.assertEqual(spy.func, fake_something_awesome) def test_construction_with_call_fake(self): """Testing FunctionSpy construction with call_fake""" spy = self.agency.spy_on(something_awesome, call_fake=fake_something_awesome) self.assertTrue(hasattr(something_awesome, 'spy')) self.assertEqual(something_awesome.spy, spy) self.assertEqual( getattr(spy.func, FunctionSig.FUNC_NAME_ATTR), getattr(fake_something_awesome, FunctionSig.FUNC_NAME_ATTR)) self.assertEqual(spy.orig_func, something_awesome) self.assertEqual(spy.func_name, 'something_awesome') self.assertEqual(spy.func_type, spy.TYPE_FUNCTION) self.assertIsInstance(something_awesome, types.FunctionType) def test_construction_with_call_fake_and_bound_method(self): """Testing FunctionSpy construction with call_fake and bound method""" obj = MathClass() orig_method = obj.do_math spy = self.agency.spy_on(obj.do_math, call_fake=fake_do_math) self.assertTrue(hasattr(obj.do_math, 'spy')) self.assertIs(obj.do_math.spy, spy) self.assertIsNot(obj.do_math, orig_method) self.assertFalse(hasattr(MathClass.do_math, 'spy')) self.assertEqual(spy.func, fake_do_math) self.assertEqual(spy.func_name, 'do_math') self.assertEqual(spy.func_type, spy.TYPE_BOUND_METHOD) self.assertIsInstance(obj.do_math, types.MethodType) def test_construction_with_call_fake_and_unbound_method(self): """Testing FunctionSpy construction with call_fake and unbound method """ orig_method = MathClass.do_math spy = self.agency.spy_on(MathClass.do_math, call_fake=fake_do_math) self.assertTrue(hasattr(MathClass.do_math, 'spy')) self.assertIs(MathClass.do_math.spy, spy) self.assertEqual(spy.func, fake_do_math) self.assertEqual(spy.func_name, 'do_math') self.assertEqual(spy.func_type, spy.TYPE_UNBOUND_METHOD) self.assertEqual(spy.owner, MathClass) if isinstance(orig_method, types.FunctionType): # Python 3 self.assertIs(MathClass.do_math, orig_method) elif isinstance(orig_method, types.MethodType): # Python 2 self.assertIsNot(MathClass.do_math, orig_method) else: self.fail('Method has an unexpected type %r' % type(orig_method)) obj = MathClass() self.assertTrue(hasattr(obj.do_math, 'spy')) self.assertIs(obj.do_math.spy, MathClass.do_math.spy) self.assertIsInstance(obj.do_math, types.MethodType) def test_construction_with_call_fake_and_classmethod(self): """Testing FunctionSpy construction with call_fake and classmethod""" def fake_class_do_math(cls, *args, **kwargs): return 42 orig_method = MathClass.class_do_math spy = self.agency.spy_on(MathClass.class_do_math, call_fake=fake_class_do_math) self.assertTrue(hasattr(MathClass.class_do_math, 'spy')) self.assertIs(MathClass.class_do_math.spy, spy) self.assertIs(MathClass.class_do_math, orig_method) self.assertEqual(spy.func, fake_class_do_math) self.assertEqual(spy.orig_func, self.orig_class_do_math) self.assertEqual(spy.func_name, 'class_do_math') self.assertIsInstance(MathClass.class_do_math, types.MethodType) def test_construction_with_call_original_false(self): """Testing FunctionSpy construction with call_original=False""" obj = MathClass() spy = self.agency.spy_on(obj.do_math, call_original=False) self.assertTrue(hasattr(obj.do_math, 'spy')) self.assertIs(obj.do_math.spy, spy) self.assertIsNone(spy.func) self.assertEqual(spy.func_name, 'do_math') self.assertIsInstance(obj.do_math, types.MethodType) def test_construction_with_call_original_true(self): """Testing FunctionSpy construction with call_original=True""" spy = self.agency.spy_on(something_awesome, call_original=True) self.assertTrue(hasattr(something_awesome, 'spy')) self.assertEqual(something_awesome.spy, spy) self.assertEqual( getattr(spy.func, FunctionSig.FUNC_NAME_ATTR), getattr(something_awesome, FunctionSig.FUNC_NAME_ATTR)) self.assertEqual(spy.orig_func, something_awesome) self.assertEqual(spy.func_name, 'something_awesome') self.assertIsInstance(something_awesome, types.FunctionType) def test_construction_with_call_original_true_and_bound_method(self): """Testing FunctionSpy construction with call_original=True and bound method """ obj = MathClass() orig_do_math = obj.do_math spy = self.agency.spy_on(obj.do_math, call_original=True) self.assertTrue(hasattr(obj.do_math, 'spy')) self.assertIs(obj.do_math.spy, spy) self.assertFalse(hasattr(MathClass.do_math, 'spy')) self.assertEqual(spy.orig_func, orig_do_math) self.assertEqual(spy.func_name, 'do_math') self.assertEqual(spy.func_type, spy.TYPE_BOUND_METHOD) self.assertIsInstance(obj.do_math, types.MethodType) def test_construction_with_call_original_and_classmethod(self): """Testing FunctionSpy construction with call_original and classmethod """ spy = self.agency.spy_on(MathClass.class_do_math, call_original=True) self.assertTrue(hasattr(MathClass.class_do_math, 'spy')) self.assertIs(MathClass.class_do_math.spy, spy) self.assertEqual(spy.orig_func, self.orig_class_do_math) self.assertEqual(spy.func_name, 'class_do_math') self.assertEqual(spy.func_type, spy.TYPE_BOUND_METHOD) self.assertIsInstance(MathClass.class_do_math, types.MethodType) def test_construction_with_function_and_owner(self): """Testing FunctionSpy constructions with function and owner passed""" with self.assertRaises(ValueError) as cm: self.agency.spy_on(do_math, owner=AdderObject) self.assertEqual(text_type(cm.exception), 'This function has no owner, but an owner was ' 'passed to spy_on().') def test_init_with_slippery_bound_method(self): """Testing FunctionSpy construction with new slippery function generated dynamically from accessing a spied-on bound method from instance """ obj = SlipperyFuncObject() # Verify that we're producing a different function on each access. func1 = obj.my_func func2 = obj.my_func self.assertIsInstance(func1, types.FunctionType) self.assertIsInstance(func2, types.FunctionType) self.assertIsNot(func1, func2) spy = self.agency.spy_on(obj.my_func, owner=obj, call_original=True) self.assertTrue(hasattr(obj.my_func, 'spy')) self.assertIs(obj.my_func.spy, spy) self.assertEqual(spy.func_name, 'my_func') self.assertEqual(spy.func_type, spy.TYPE_BOUND_METHOD) self.assertIsInstance(obj.my_func, types.MethodType) slippery_func.count = 0 func1 = obj.my_func func2 = obj.my_func self.assertIs(func1, func2) # Since the functions are wrapped, we'll be getting a new value each # time we invoke them (and not when accessing the attributes as we # did above). self.assertEqual(func1(), 0) self.assertEqual(func1(), 1) self.assertEqual(func2(), 2) self.assertEqual(len(obj.my_func.calls), 3) def test_init_with_slippery_unbound_method(self): """Testing FunctionSpy construction with new slippery function generated dynamically from accessing a spied-on unbound method from instance """ with self.assertRaises(ValueError) as cm: self.agency.spy_on(SlipperyFuncObject.my_func, owner=SlipperyFuncObject) self.assertEqual( text_type(cm.exception), 'Unable to spy on unbound slippery methods (those that return ' 'a new function on each attribute access). Please spy on an ' 'instance instead.') def test_construction_with_classmethod_on_parent(self): """Testing FunctionSpy construction with classmethod from parent of class """ class MyParent(object): @classmethod def foo(self): pass class MyObject(MyParent): pass obj = MyObject() orig_method = obj.foo spy = self.agency.spy_on(MyObject.foo) self.assertTrue(hasattr(MyObject.foo, 'spy')) self.assertFalse(hasattr(MyParent.foo, 'spy')) self.assertIs(MyObject.foo.spy, spy) self.assertEqual(spy.func_name, 'foo') self.assertEqual(spy.func_type, spy.TYPE_BOUND_METHOD) self.assertEqual(spy.owner, MyObject) if isinstance(orig_method, types.FunctionType): # Python 3 self.assertIs(MyObject.foo, orig_method) elif isinstance(orig_method, types.MethodType): # Python 2 self.assertIsNot(MyObject.foo, orig_method) else: self.fail('Method has an unexpected type %r' % type(orig_method)) obj2 = MyObject() self.assertTrue(hasattr(obj2.foo, 'spy')) self.assertIs(obj2.foo.spy, MyObject.foo.spy) self.assertIsInstance(obj2.foo, types.MethodType) obj3 = MyParent() self.assertFalse(hasattr(obj3.foo, 'spy')) def test_construction_with_unbound_method_on_parent(self): """Testing FunctionSpy construction with unbound method from parent of class """ obj = AdderSubclass() orig_method = obj.func spy = self.agency.spy_on(AdderSubclass.func, owner=AdderSubclass) self.assertTrue(hasattr(AdderSubclass.func, 'spy')) self.assertFalse(hasattr(AdderObject.func, 'spy')) self.assertIs(AdderSubclass.func.spy, spy) self.assertEqual(spy.func_name, 'func') self.assertEqual(spy.func_type, spy.TYPE_UNBOUND_METHOD) self.assertEqual(spy.owner, AdderSubclass) if isinstance(orig_method, types.FunctionType): # Python 3 self.assertIs(AdderSubclass.func, orig_method) elif isinstance(orig_method, types.MethodType): # Python 2 self.assertIsNot(AdderSubclass.func, orig_method) else: self.fail('Method has an unexpected type %r' % type(orig_method)) obj2 = AdderSubclass() self.assertTrue(hasattr(obj2.func, 'spy')) self.assertIs(obj2.func.spy, AdderSubclass.func.spy) self.assertIsInstance(obj2.func, types.MethodType) obj3 = AdderObject() self.assertFalse(hasattr(obj3.func, 'spy')) def test_construction_with_falsy_im_self(self): """Testing FunctionSpy construction with a falsy function.im_self""" class MyObject(dict): def foo(self): pass my_object = MyObject() orig_foo = my_object.foo # Ensure it's falsy. self.assertFalse(my_object) spy = self.agency.spy_on(my_object.foo) self.assertEqual(spy.orig_func, orig_foo) self.assertNotEqual(MyObject.foo, spy) self.assertEqual(spy.func_name, 'foo') self.assertEqual(spy.func_type, spy.TYPE_BOUND_METHOD) self.assertIsInstance(my_object.foo, types.MethodType) def test_construction_with_existing_spy(self): """Testing FunctionSpy constructions with function already spied on""" def setup_spy(): self.agency.spy_on(do_math) setup_spy() with self.assertRaises(ExistingSpyError) as cm: self.agency.spy_on(do_math) self.assertIn(', in setup_spy', text_type(cm.exception)) def test_construction_with_bound_method_and_custom_setattr(self): """Testing FunctionSpy constructions with a bound method on a class containing a custom __setattr__ """ class MyObject(object): def __setattr__(self, key, value): assert False def foo(self): pass obj = MyObject() orig_foo = obj.foo spy = self.agency.spy_on(obj.foo) self.assertEqual(spy.orig_func, orig_foo) self.assertNotEqual(MyObject.foo, spy) self.assertEqual(spy.func_name, 'foo') self.assertEqual(spy.func_type, spy.TYPE_BOUND_METHOD) self.assertIsInstance(obj.foo, types.MethodType) self.assertTrue(hasattr(obj.foo, 'spy')) self.assertTrue(hasattr(obj.foo, 'called_with')) obj2 = MyObject() self.assertFalse(hasattr(obj2.foo, 'spy')) def test_construction_with_bound_method_and_bad_owner(self): """Testing FunctionSpy constructions with a bound method and an explicit owner not matching the class """ class MyObject(object): def foo(self): pass class BadObject(object): def foo(self): pass obj = MyObject() with self.assertRaises(ValueError) as cm: self.agency.spy_on(obj.foo, owner=BadObject) self.assertEqual(text_type(cm.exception), 'The owner passed does not match the actual owner ' 'of the bound method.') def test_construction_with_owner_without_method(self): """Testing FunctionSpy constructions with an owner passed that does not provide the spied method """ class MyObject(object): def foo(self): pass obj = MyObject() with self.assertRaises(ValueError) as cm: self.agency.spy_on(obj.foo, owner=AdderObject) self.assertEqual(text_type(cm.exception), 'The owner passed does not contain the spied method.') def test_construction_with_non_function(self): """Testing FunctionSpy constructions with non-function""" with self.assertRaises(ValueError) as cm: self.agency.spy_on(42) self.assertEqual(text_type(cm.exception), '42 cannot be spied on. It does not appear to be a ' 'valid function or method.') def test_construction_with_call_fake_non_function(self): """Testing FunctionSpy constructions with call_fake as non-function""" with self.assertRaises(ValueError) as cm: self.agency.spy_on(do_math, call_fake=True) self.assertEqual(text_type(cm.exception), 'True cannot be used for call_fake. It does not ' 'appear to be a valid function or method.') def test_construction_with_call_fake_compatibility(self): """Testing FunctionSpy constructions with call_fake with signature compatibility """ def source1(a, b): pass def source2(a, b, *args): pass def source3(c=1, d=2): pass def source4(c=1, d=2, **kwargs): pass with self.assertRaises(IncompatibleFunctionError): self.agency.spy_on( source1, call_fake=lambda a, b, c: None) with self.assertRaises(IncompatibleFunctionError): self.agency.spy_on( source1, call_fake=lambda a: None) with self.assertRaises(IncompatibleFunctionError): self.agency.spy_on( source1, call_fake=lambda **kwargs: None) with self.assertRaises(IncompatibleFunctionError): self.agency.spy_on( source2, call_fake=lambda a, b: None) with self.assertRaises(IncompatibleFunctionError): self.agency.spy_on( source3, call_fake=lambda c=1: None) with self.assertRaises(IncompatibleFunctionError): self.agency.spy_on( source4, call_fake=lambda c=1, d=2, e=3: None) with self.assertRaises(IncompatibleFunctionError): self.agency.spy_on( source4, call_fake=lambda c=1, d=2: None) self.agency.spy_on(source1, call_fake=lambda a, b: None) source1.unspy() self.agency.spy_on(source1, call_fake=lambda *args: None) source1.unspy() self.agency.spy_on(source4, call_fake=lambda c=1, d=2, **kwargs: None) source4.unspy() self.agency.spy_on(source4, call_fake=lambda c=1, **kwargs: None) source4.unspy() self.agency.spy_on(source4, call_fake=lambda c, d=None, **kwargs: None) source4.unspy() self.agency.spy_on(source4, call_fake=lambda c, e, **kwargs: None) source4.unspy() self.agency.spy_on(source4, call_fake=lambda **kwargs: None) source4.unspy() def test_construction_with_old_style_class(self): """Testing FunctionSpy with old-style class""" class MyClass: def test_func(self): return 100 obj = MyClass() self.agency.spy_on(obj.test_func, call_fake=lambda obj: 200) self.assertEqual(obj.test_func(), 200) def test_call_with_fake(self): """Testing FunctionSpy calls with call_fake""" self.agency.spy_on(something_awesome, call_fake=fake_something_awesome) result = something_awesome() self.assertEqual(result, r'\o/') self.assertEqual(len(something_awesome.spy.calls), 1) self.assertEqual(len(something_awesome.spy.calls[0].args), 0) self.assertEqual(len(something_awesome.spy.calls[0].kwargs), 0) def test_call_with_fake_and_bound_method(self): """Testing FunctionSpy calls with call_fake and bound method""" obj = MathClass() self.agency.spy_on(obj.do_math, call_fake=fake_do_math) result = obj.do_math() self.assertEqual(result, -1) self.assertEqual(len(obj.do_math.calls), 1) self.assertTrue(obj.do_math.last_called_with( a=1, b=2)) def test_call_with_fake_and_unbound_method(self): """Testing FunctionSpy calls with call_fake and unbound method""" self.agency.spy_on(MathClass.do_math, call_fake=fake_do_math) obj = MathClass() result = obj.do_math() self.assertEqual(result, -1) self.assertEqual(len(obj.do_math.calls), 1) self.assertTrue(obj.do_math.last_called_with( a=1, b=2)) def test_call_with_fake_and_classmethod(self): """Testing FunctionSpy calls with call_fake and classmethod""" self.agency.spy_on(MathClass.class_do_math, call_fake=fake_class_do_math) result = MathClass.class_do_math() self.assertEqual(result, -3) self.assertEqual(len(MathClass.class_do_math.calls), 1) self.assertTrue(MathClass.class_do_math.last_called_with( a=2, b=5)) def test_call_with_fake_and_contextmanager(self): """Testing FunctionSpy calls with call_fake and context manager""" @contextmanager def fake_do_context(a=1, b=2): yield a * b self.agency.spy_on(do_context, call_fake=fake_do_context) with do_context(a=5) as ctx: result = ctx self.assertEqual(result, 10) self.assertEqual(len(do_context.calls), 1) self.assertEqual(do_context.calls[0].args, ()) self.assertEqual(do_context.calls[0].kwargs, {'a': 5}) def test_call_with_fake_and_contextmanager_func_raises_exception(self): """Testing FunctionSpy calls with call_fake and context manager and function raises exception """ e = Exception('oh no') @contextmanager def fake_do_context(*args, **kwargs): raise e self.agency.spy_on(do_context, call_fake=fake_do_context) with self.assertRaisesRegex(Exception, 'oh no'): with do_context(a=5): pass self.assertEqual(len(do_context.calls), 1) self.assertEqual(do_context.calls[0].args, ()) self.assertEqual(do_context.calls[0].kwargs, {'a': 5}) self.assertEqual(do_context.calls[0].exception, e) def test_call_with_fake_and_contextmanager_body_raises_exception(self): """Testing FunctionSpy calls with call_fake and context manager and context body raises exception """ e = Exception('oh no') @contextmanager def fake_do_context(a=1, b=2): yield a * b self.agency.spy_on(do_context, call_fake=fake_do_context) with self.assertRaisesRegex(Exception, 'oh no'): with do_context(a=5): raise e self.assertEqual(len(do_context.calls), 1) self.assertEqual(do_context.calls[0].args, ()) self.assertEqual(do_context.calls[0].kwargs, {'a': 5}) self.assertIsNone(do_context.calls[0].exception) def test_call_with_exception(self): e = ValueError('oh no') def orig_func(arg1=None, arg2=None): # Create enough of a difference in code positions between this # and the forwarding functions, to ensure the exception's # position count is higher than that of the forwarding function. # # This is important for sanity checks on Python 3.11. try: if 1: if 2: try: a = 1 b = a a = 2 except Exception: raise else: c = [1, 2, 3, 4, 5] a = c except Exception: raise for i in range(10): try: d = [1, 2, 3, 4, 5] a = d b = a d = b except Exception: raise # We should be good. We'll verify counts later. raise e # Verify the above. orig_func_code = orig_func.__code__ supports_co_positions = hasattr(orig_func_code, 'co_positions') if supports_co_positions: orig_positions_count = len(list(orig_func_code.co_positions())) else: orig_positions_count = None # Now spy. self.agency.spy_on(orig_func) if supports_co_positions: spy_positions_count = len(list(orig_func.__code__.co_positions())) # Make sure we had enough padding up above. self.assertGreater(orig_positions_count, spy_positions_count) # Now test. try: orig_func() except Exception as ex: # This should fail if we've built the CodeType wrong and have a # resulting offset issue. The act of pretty-printing the exception # triggers the noticeable co_positions() issue. traceback.print_exception(*sys.exc_info()) self.assertEqual(len(orig_func.calls), 1) self.assertIs(orig_func.calls[0].exception, e) def test_call_with_fake_and_args(self): """Testing FunctionSpy calls with call_fake and arguments""" obj = MathClass() self.agency.spy_on(obj.do_math_pos, call_fake=fake_do_math) result = obj.do_math_pos(10, 20) self.assertEqual(result, -10) self.assertEqual(len(obj.do_math_pos.calls), 1) self.assertEqual(obj.do_math_pos.calls[0].args, (10, 20)) self.assertEqual(obj.do_math_pos.calls[0].kwargs, {}) def test_call_with_fake_and_args_for_kwargs(self): """Testing FunctionSpy calls with call_fake and positional arguments in place of keyword arguments """ obj = MathClass() self.agency.spy_on(obj.do_math, call_fake=fake_do_math) result = obj.do_math(10, 20) self.assertEqual(result, -10) self.assertEqual(len(obj.do_math.calls), 1) self.assertEqual(obj.do_math.calls[0].args, ()) self.assertEqual(obj.do_math.calls[0].kwargs, { 'a': 10, 'b': 20, }) def test_call_with_fake_and_kwargs(self): """Testing FunctionSpy calls with call_fake and keyword arguments""" obj = MathClass() self.agency.spy_on(obj.do_math, call_fake=fake_do_math) result = obj.do_math(a=10, b=20) self.assertEqual(result, -10) self.assertEqual(len(obj.do_math.calls), 1) self.assertEqual(len(obj.do_math.calls[0].args), 0) self.assertEqual(obj.do_math.calls[0].kwargs, { 'a': 10, 'b': 20, }) def test_call_with_original_false(self): """Testing FunctionSpy calls with call_original=False""" obj = MathClass() self.agency.spy_on(obj.do_math, call_original=False) result = obj.do_math() self.assertIsNone(result) self.assertEqual(len(obj.do_math.calls), 1) self.assertTrue(obj.do_math.last_called_with(a=1, b=2)) def test_call_with_all_original_false_and_args(self): """Testing FunctionSpy calls with call_original=False and positional arguments """ obj = MathClass() self.agency.spy_on(obj.do_math_pos, call_original=False) result = obj.do_math_pos(10, 20) self.assertIsNone(result) self.assertEqual(len(obj.do_math_pos.calls), 1) self.assertEqual(obj.do_math_pos.calls[0].args, (10, 20)) self.assertEqual(obj.do_math_pos.calls[0].kwargs, {}) def test_call_with_all_original_false_and_args_for_kwargs(self): """Testing FunctionSpy calls with call_original=False and positional arguments in place of keyword arguments """ obj = MathClass() self.agency.spy_on(obj.do_math, call_original=False) result = obj.do_math(10, 20) self.assertEqual(result, None) self.assertEqual(len(obj.do_math.calls), 1) self.assertEqual(obj.do_math.calls[0].args, ()) self.assertEqual(obj.do_math.calls[0].kwargs, { 'a': 10, 'b': 20, }) def test_call_with_original_false_and_kwargs(self): """Testing FunctionSpy calls with call_original=False and keyword arguments """ obj = MathClass() self.agency.spy_on(obj.do_math, call_original=False) result = obj.do_math(a=10, b=20) self.assertEqual(result, None) self.assertEqual(len(obj.do_math.calls), 1) self.assertEqual(len(obj.do_math.calls[0].args), 0) self.assertEqual(obj.do_math.calls[0].kwargs, { 'a': 10, 'b': 20 }) def test_call_with_original_true_and_function(self): """Testing FunctionSpy calls with call_original=True and function""" self.agency.spy_on(something_awesome, call_original=True) result = something_awesome() self.assertEqual(result, 'Tada!') self.assertTrue(hasattr(something_awesome, 'spy')) self.assertEqual(len(something_awesome.spy.calls), 1) self.assertEqual(len(something_awesome.spy.calls[0].args), 0) self.assertEqual(len(something_awesome.spy.calls[0].kwargs), 0) def test_call_with_original_true_and_function_args(self): """Testing FunctionSpy calls with call_original=True and function with all positional arguments """ self.agency.spy_on(do_math_pos, call_original=True) result = do_math_pos(10, 20) self.assertEqual(result, -10) self.assertEqual(len(do_math_pos.spy.calls), 1) self.assertEqual(do_math_pos.spy.calls[0].args, (10, 20)) self.assertEqual(len(do_math_pos.spy.calls[0].kwargs), 0) def test_call_with_original_true_and_function_args_for_kwargs(self): """Testing FunctionSpy calls with call_original=True and function with all positional arguments in place of keyword arguments """ self.agency.spy_on(do_math, call_original=True) result = do_math(10, 20) self.assertEqual(result, -10) self.assertEqual(len(do_math.spy.calls), 1) self.assertEqual(do_math.spy.calls[0].args, ()) self.assertEqual(do_math.spy.calls[0].kwargs, { 'a': 10, 'b': 20, }) def test_call_with_original_true_and_function_kwargs(self): """Testing FunctionSpy calls with call_original=True and function with all keyword arguments """ self.agency.spy_on(do_math, call_original=True) result = do_math(b=10, a=20) self.assertEqual(result, 10) self.assertEqual(len(do_math.spy.calls), 1) self.assertEqual(len(do_math.spy.calls[0].args), 0) self.assertEqual(do_math.spy.calls[0].kwargs, { 'a': 20, 'b': 10 }) def test_call_with_original_true_and_function_mixed(self): """Testing FunctionSpy calls with call_original=True and function with all mixed argument types """ self.agency.spy_on(do_math, call_original=True) result = do_math(10, b=20, unused=True) self.assertEqual(result, -10) self.assertEqual(len(do_math.spy.calls), 1) self.assertEqual(do_math.spy.calls[0].args, ()) self.assertEqual(do_math.spy.calls[0].kwargs, { 'a': 10, 'b': 20, 'unused': True, }) self.agency.spy_on(do_math_pos, call_original=True) result = do_math_pos(10, b=20) self.assertEqual(result, -10) self.assertEqual(len(do_math_pos.spy.calls), 1) self.assertEqual(do_math_pos.spy.calls[0].args, (10, 20)) self.assertEqual(do_math_pos.spy.calls[0].kwargs, {}) self.agency.spy_on(do_math_mixed, call_original=True) result = do_math_mixed(10, b=20, unused=True) self.assertEqual(result, -10) self.assertEqual(len(do_math_mixed.spy.calls), 1) self.assertEqual(do_math_mixed.spy.calls[0].args, (10,)) self.assertEqual(do_math_mixed.spy.calls[0].kwargs, { 'b': 20, 'unused': True, }) def test_call_with_original_true_and_bound_method(self): """Testing FunctionSpy calls with call_original=True and bound method """ obj = MathClass() self.agency.spy_on(obj.do_math, call_original=True) result = obj.do_math() self.assertEqual(result, 3) self.assertEqual(len(obj.do_math.calls), 1) self.assertTrue(obj.do_math.last_called_with(a=1, b=2)) def test_call_with_original_true_and_bound_method_args(self): """Testing FunctionSpy calls with call_original=True and bound method with all positional arguments """ obj = MathClass() self.agency.spy_on(obj.do_math_pos, call_original=True) result = obj.do_math_pos(10, 20) self.assertEqual(result, 30) self.assertEqual(len(obj.do_math_pos.calls), 1) self.assertEqual(obj.do_math_pos.calls[0].args, (10, 20)) self.assertEqual(len(obj.do_math_pos.calls[0].kwargs), 0) def test_call_with_original_true_and_bound_method_args_for_kwargs(self): """Testing FunctionSpy calls with call_original=True and bound method with all positional arguments in place of keyword arguments """ obj = MathClass() self.agency.spy_on(obj.do_math, call_original=True) result = obj.do_math(10, 20) self.assertEqual(result, 30) self.assertEqual(len(obj.do_math.calls), 1) self.assertEqual(obj.do_math.calls[0].args, ()) self.assertEqual(obj.do_math.calls[0].kwargs, { 'a': 10, 'b': 20, }) def test_call_with_original_true_and_bound_method_kwargs(self): """Testing FunctionSpy calls with call_original=True and bound method with all keyword arguments """ obj = MathClass() self.agency.spy_on(obj.do_math, call_original=True) result = obj.do_math(a=10, b=20) self.assertEqual(result, 30) self.assertEqual(len(obj.do_math.calls), 1) self.assertEqual(len(obj.do_math.calls[0].args), 0) self.assertEqual(obj.do_math.calls[0].kwargs, { 'a': 10, 'b': 20 }) def test_call_with_original_true_and_unbound_method(self): """Testing FunctionSpy calls with call_original=True and unbound method """ self.agency.spy_on(MathClass.do_math, call_original=True) obj = MathClass() result = obj.do_math() self.assertEqual(result, 3) self.assertEqual(len(MathClass.do_math.calls), 1) self.assertTrue(MathClass.do_math.last_called_with(a=1, b=2)) def test_call_with_original_true_and_unbound_method_args(self): """Testing FunctionSpy calls with call_original=True and unbound method with all positional arguments """ self.agency.spy_on(MathClass.do_math_pos, call_original=True) obj = MathClass() result = obj.do_math_pos(10, 20) self.assertEqual(result, 30) self.assertEqual(len(MathClass.do_math_pos.calls), 1) self.assertTrue(MathClass.do_math_pos.last_called_with(10, 20)) def test_call_with_original_true_and_unbound_method_args_for_kwargs(self): """Testing FunctionSpy calls with call_original=True and unbound method with all positional arguments in place of keyword arguments """ self.agency.spy_on(MathClass.do_math, call_original=True) obj = MathClass() result = obj.do_math(10, 20) self.assertEqual(result, 30) self.assertEqual(len(MathClass.do_math.calls), 1) self.assertTrue(MathClass.do_math.last_called_with(a=10, b=20)) def test_call_with_original_true_and_unbound_method_kwargs(self): """Testing FunctionSpy calls with call_original=True and unbound method with all keyword arguments """ self.agency.spy_on(MathClass.do_math, call_original=True) obj = MathClass() result = obj.do_math(a=10, b=20) self.assertEqual(result, 30) self.assertEqual(len(MathClass.do_math.calls), 1) self.assertEqual(len(MathClass.do_math.calls[0].args), 0) self.assertTrue(MathClass.do_math.last_called_with(a=10, b=20)) def test_call_with_original_true_and_classmethod(self): """Testing FunctionSpy calls with call_original=True and classmethod""" self.agency.spy_on(MathClass.class_do_math, call_original=True) result = MathClass.class_do_math() self.assertEqual(result, 10) self.assertEqual(len(MathClass.class_do_math.calls), 1) self.assertTrue(MathClass.class_do_math.last_called_with(a=2, b=5)) def test_call_with_original_true_and_classmethod_args(self): """Testing FunctionSpy calls with call_original=True and classmethod with all positional arguments """ self.agency.spy_on(MathClass.class_do_math_pos, call_original=True) result = MathClass.class_do_math_pos(10, 20) self.assertEqual(result, 200) self.assertEqual(len(MathClass.class_do_math_pos.calls), 1) self.assertEqual(MathClass.class_do_math_pos.calls[0].args, (10, 20)) self.assertEqual(len(MathClass.class_do_math_pos.calls[0].kwargs), 0) def test_call_with_original_true_and_classmethod_args_for_kwargs(self): """Testing FunctionSpy calls with call_original=True and classmethod with all positional arguments in place of keyword arguments """ self.agency.spy_on(MathClass.class_do_math, call_original=True) result = MathClass.class_do_math(10, 20) self.assertEqual(result, 200) self.assertEqual(len(MathClass.class_do_math.calls), 1) self.assertEqual(MathClass.class_do_math.calls[0].args, ()) self.assertEqual(MathClass.class_do_math.calls[0].kwargs, { 'a': 10, 'b': 20, }) def test_call_with_original_true_and_classmethod_kwargs(self): """Testing FunctionSpy calls with call_original=True and classmethod with all keyword arguments """ self.agency.spy_on(MathClass.class_do_math, call_original=True) result = MathClass.class_do_math(a=10, b=20) self.assertEqual(result, 200) self.assertEqual(len(MathClass.class_do_math.calls), 1) self.assertEqual(len(MathClass.class_do_math.calls[0].args), 0) self.assertEqual(MathClass.class_do_math.calls[0].kwargs, { 'a': 10, 'b': 20 }) def test_call_with_inline_function_using_closure_vars(self): """Testing FunctionSpy calls for inline function using a closure's variables """ d = {} def func(): d['called'] = True self.agency.spy_on(func) func() self.assertTrue(func.called) self.assertEqual(d, {'called': True}) def test_call_with_inline_function_using_closure_vars_and_args(self): """Testing FunctionSpy calls for inline function using a closure's variables and function with arguments """ d = {} def func(a, b, c): d['call_result'] = a + b + c self.agency.spy_on(func) func(1, 2, c=3) self.assertTrue(func.called) self.assertEqual(d, {'call_result': 6}) def test_call_with_function_providing_closure_vars(self): """Testing FunctionSpy calls for function providing variables for an inline function """ def func(): d = {} def inline_func(): d['called'] = True inline_func() return d self.agency.spy_on(func) d = func() self.assertTrue(func.called) self.assertEqual(d, {'called': True}) def test_call_with_function_providing_closure_access_args(self): """Testing FunctionSpy calls with an inline function accessing parent's arguments """ # NOTE: This originally caused a crash on Python 3.13 beta 2. # See: https://github.com/beanbaginc/kgb/issues/11 def func(arg): def inline_func(): print(arg) return 123 self.agency.spy_on(func) d = func(42) self.assertEqual(d, 123) self.assertTrue(func.called) def test_call_with_bound_method_with_list_comprehension_and_self(self): """Testing FunctionSpy calls for bound method using a list comprehension referencing 'self' """ obj = AdderObject() self.agency.spy_on(obj.func) result = obj.func() self.assertTrue(obj.func.called) self.assertEqual(result, [2, 3, 4]) def test_call_with_unbound_method_with_list_comprehension_and_self(self): """Testing FunctionSpy calls for unbound method using a list comprehension referencing 'self' """ self.agency.spy_on(AdderObject.func) obj = AdderObject() result = obj.func() self.assertTrue(obj.func.called) self.assertEqual(result, [2, 3, 4]) def test_call_with_classmethod_with_list_comprehension_and_self(self): """Testing FunctionSpy calls for classmethod using a list comprehension referencing 'cls' """ self.agency.spy_on(AdderObject.class_func) result = AdderObject.class_func() self.assertTrue(AdderObject.class_func.called) self.assertEqual(result, [2, 3, 4]) def test_call_original_with_orig_func_and_function(self): """Testing FunctionSpy.call_original with spy on function set up using call_original=True """ self.agency.spy_on(do_math, call_original=True) result = do_math.call_original(10, 20) self.assertEqual(result, -10) self.assertFalse(do_math.called) def test_call_original_with_orig_func_and_bound_method(self): """Testing FunctionSpy.call_original with spy on bound method set up using call_original=True """ obj = AdderObject() self.agency.spy_on(obj.add_one, call_original=True) result = obj.add_one.call_original(5) self.assertEqual(result, 6) self.assertFalse(obj.add_one.called) def test_call_original_with_orig_func_and_unbound_method(self): """Testing FunctionSpy.call_original with spy on unbound method set up using call_original=True """ self.agency.spy_on(AdderObject.add_one, call_original=True) obj = AdderObject() result = obj.add_one.call_original(obj, 5) self.assertEqual(result, 6) self.assertFalse(AdderObject.add_one.called) self.assertFalse(obj.add_one.called) def test_call_original_with_orig_func_and_classmethod(self): """Testing FunctionSpy.call_original with spy on classmethod set up using call_original=True """ self.agency.spy_on(AdderObject.class_add_one, call_original=True) result = AdderObject.class_add_one.call_original(5) self.assertEqual(result, 6) self.assertFalse(AdderObject.class_add_one.called) def test_call_original_with_no_func_and_function(self): """Testing FunctionSpy.call_original with spy on function set up using call_original=False """ self.agency.spy_on(do_math) result = do_math.call_original(10, 20) self.assertEqual(result, -10) self.assertFalse(do_math.called) def test_call_original_with_no_func_and_bound_method(self): """Testing FunctionSpy.call_original with spy on bound method set up using call_original=False """ obj = MathClass() self.agency.spy_on(obj.do_math) result = obj.do_math.call_original(10, 20) self.assertEqual(result, 30) self.assertFalse(obj.do_math.called) def test_call_original_with_no_func_and_unbound_method(self): """Testing FunctionSpy.call_original with spy on unbound method set up using call_original=False """ self.agency.spy_on(MathClass.do_math) obj = MathClass() result = obj.do_math.call_original(obj, 10, 20) self.assertEqual(result, 30) self.assertFalse(MathClass.do_math.called) self.assertFalse(obj.do_math.called) def test_call_original_with_no_func_and_classmethod(self): """Testing FunctionSpy.call_original with spy on classmethod set up using call_original=False """ self.agency.spy_on(MathClass.class_do_math) result = MathClass.class_do_math.call_original(10, 20) self.assertEqual(result, 200) self.assertFalse(MathClass.class_do_math.called) def test_call_original_with_fake_func_and_function(self): """Testing FunctionSpy.call_original with spy on function set up using call_fake= """ self.agency.spy_on(do_math, call_fake=fake_do_math) result = do_math.call_original(10, 20) self.assertEqual(result, -10) self.assertFalse(do_math.called) def test_call_original_with_fake_func_and_bound_method(self): """Testing FunctionSpy.call_original with spy on bound method set up using call_fake= """ obj = MathClass() self.agency.spy_on(obj.do_math, call_fake=fake_do_math) result = obj.do_math.call_original(10, 20) self.assertEqual(result, 30) self.assertFalse(obj.do_math.called) def test_call_original_with_fake_func_and_unbound_method(self): """Testing FunctionSpy.call_original with spy on unbound method set up using call_fake= """ self.agency.spy_on(MathClass.do_math, call_fake=fake_do_math) obj = MathClass() result = obj.do_math.call_original(obj, 10, 20) self.assertEqual(result, 30) self.assertFalse(MathClass.do_math.called) self.assertFalse(obj.do_math.called) def test_call_original_with_fake_func_and_classmethod(self): """Testing FunctionSpy.call_original with spy on classmethod set up using call_fake= """ self.agency.spy_on(MathClass.class_do_math, call_fake=fake_class_do_math) result = MathClass.class_do_math.call_original(10, 20) self.assertEqual(result, 200) self.assertFalse(MathClass.class_do_math.called) def test_call_original_with_unbound_method_no_instance(self): """Testing FunctionSpy.call_original with spy on unbound method set up using call_original=True without passing instance """ self.agency.spy_on(AdderObject.add_one, call_original=True) obj = AdderObject() message = re.escape( 'The first argument to add_one.call_original() must be an ' 'instance of kgb.tests.test_function_spy.AdderObject, since this ' 'is an unbound method.' ) with self.assertRaisesRegex(TypeError, message): obj.add_one.call_original(5) def test_called(self): """Testing FunctionSpy.called""" obj = MathClass() self.agency.spy_on(obj.do_math) self.assertFalse(obj.do_math.called) obj.do_math(10, 20) self.assertTrue(obj.do_math.called) def test_last_call(self): """Testing FunctionSpy.last_call""" obj = MathClass() self.agency.spy_on(obj.do_math) obj.do_math(10, 20) obj.do_math(20, 30) self.assertEqual(len(obj.do_math.calls), 2) last_call = obj.do_math.last_call self.assertNotEqual(last_call, None) self.assertTrue(last_call.called_with(a=20, b=30)) def test_last_call_with_no_calls(self): """Testing FunctionSpy.last_call on uncalled function""" obj = MathClass() self.agency.spy_on(obj.do_math) self.assertEqual(len(obj.do_math.calls), 0) last_call = obj.do_math.last_call self.assertEqual(last_call, None) def test_unspy(self): """Testing FunctionSpy.unspy""" orig_code = getattr(something_awesome, FunctionSig.FUNC_CODE_ATTR) spy = self.agency.spy_on(something_awesome, call_fake=lambda: 'spy!') self.assertTrue(hasattr(something_awesome, 'spy')) self.assertEqual(something_awesome.spy, spy) self.assertEqual(something_awesome(), 'spy!') spy.unspy() self.assertFalse(hasattr(something_awesome, 'spy')) self.assertEqual(getattr(something_awesome, FunctionSig.FUNC_CODE_ATTR), orig_code) self.assertEqual(something_awesome(), 'Tada!') def test_unspy_and_bound_method(self): """Testing FunctionSpy.unspy and bound method""" obj = MathClass() func_dict = obj.do_math.__dict__.copy() spy = self.agency.spy_on(obj.do_math) self.assertNotEqual(obj.do_math.__dict__, func_dict) spy.unspy() self.assertEqual(obj.do_math.__dict__, func_dict) def test_unspy_with_bound_method_and_custom_setattr(self): """Testing FunctionSpy.unspy with a bound method on a class containing a custom __setattr__ """ class MyObject(object): def __setattr__(self, key, value): assert False def foo(self): pass obj = MyObject() func_dict = obj.foo.__dict__.copy() spy = self.agency.spy_on(obj.foo) self.assertNotEqual(obj.foo.__dict__, func_dict) spy.unspy() self.assertEqual(obj.foo.__dict__, func_dict) def test_unspy_and_unbound_method(self): """Testing FunctionSpy.unspy and unbound method""" func_dict = MathClass.do_math.__dict__.copy() spy = self.agency.spy_on(MathClass.do_math) self.assertNotEqual(MathClass.do_math.__dict__, func_dict) spy.unspy() self.assertEqual(MathClass.do_math.__dict__, func_dict) def test_unspy_with_classmethod(self): """Testing FunctionSpy.unspy with classmethod""" func_dict = MathClass.class_do_math.__dict__.copy() spy = self.agency.spy_on(MathClass.class_do_math) self.assertNotEqual(MathClass.class_do_math.__dict__, func_dict) spy.unspy() self.assertEqual(MathClass.class_do_math.__dict__, func_dict) def test_unspy_with_classmethod_on_parent(self): """Testing FunctionSpy.unspy with classmethod on parent class""" class MyParent(object): @classmethod def foo(self): pass class MyObject(MyParent): pass parent_func_dict = MyParent.foo.__dict__.copy() obj_func_dict = MyObject.foo.__dict__.copy() spy = self.agency.spy_on(MyObject.foo) self.assertNotEqual(MyObject.foo.__dict__, obj_func_dict) self.assertEqual(MyParent.foo.__dict__, parent_func_dict) spy.unspy() self.assertEqual(MyObject.foo.__dict__, obj_func_dict) self.assertEqual(MyParent.foo.__dict__, parent_func_dict) def test_unspy_with_unbound_method_on_parent(self): """Testing FunctionSpy.unspy with unbound method on parent class""" class MyParent(object): def foo(self): pass class MyObject(MyParent): pass parent_func_dict = MyParent.foo.__dict__.copy() obj_func_dict = MyObject.foo.__dict__.copy() spy = self.agency.spy_on(MyObject.foo, owner=MyObject) self.assertNotEqual(MyObject.foo.__dict__, obj_func_dict) self.assertEqual(MyParent.foo.__dict__, parent_func_dict) spy.unspy() self.assertEqual(MyObject.foo.__dict__, obj_func_dict) self.assertEqual(MyParent.foo.__dict__, parent_func_dict) def test_unspy_with_slippery_bound_method(self): """Testing FunctionSpy.unspy with slippery function generated by spied-on bound method """ obj = SlipperyFuncObject() spy = self.agency.spy_on(obj.my_func, owner=obj) spy.unspy() self.assertFalse(hasattr(obj.my_func, 'spy')) self.assertNotIn('my_func', obj.__dict__) # Make sure the old behavior has reverted. slippery_func.count = 0 func1 = obj.my_func func2 = obj.my_func self.assertIsNot(func1, func2) # These will have already had their values set, so we should get # stable results again. self.assertEqual(func1(), 0) self.assertEqual(func1(), 0) self.assertEqual(func2(), 1) self.assertEqual(func2(), 1) # These will trigger new counter increments when re-generating the # function in the decorator. self.assertEqual(obj.my_func(), 2) self.assertEqual(obj.my_func(), 3) def test_called_with(self): """Testing FunctionSpy.called_with""" obj = MathClass() self.agency.spy_on(obj.do_math_mixed) obj.do_math_mixed(1, b=2) obj.do_math_mixed(3, b=4) self.assertTrue(obj.do_math_mixed.called_with(1, b=2)) self.assertTrue(obj.do_math_mixed.called_with(3, b=4)) self.assertTrue(obj.do_math_mixed.called_with(a=1, b=2)) self.assertTrue(obj.do_math_mixed.called_with(a=3, b=4)) self.assertFalse(obj.do_math_mixed.called_with(1, 2)) self.assertFalse(obj.do_math_mixed.called_with(3, 4)) self.assertFalse(obj.do_math_mixed.called_with(5, b=6)) def test_called_with_and_keyword_args(self): """Testing FunctionSpy.called_with and keyword arguments""" obj = MathClass() self.agency.spy_on(obj.do_math_mixed) obj.do_math_mixed(a=1, b=2) obj.do_math_mixed(a=3, b=4) self.assertTrue(obj.do_math_mixed.called_with(1, b=2)) self.assertTrue(obj.do_math_mixed.called_with(3, b=4)) self.assertTrue(obj.do_math_mixed.called_with(a=1, b=2)) self.assertTrue(obj.do_math_mixed.called_with(a=3, b=4)) self.assertFalse(obj.do_math_mixed.called_with(5, b=6)) def test_called_with_and_partial_args(self): """Testing FunctionSpy.called_with and partial arguments""" obj = MathClass() self.agency.spy_on(obj.do_math_mixed) obj.do_math_mixed(1, 2) obj.do_math_mixed(3, 4) self.assertTrue(obj.do_math_mixed.called_with(1)) self.assertTrue(obj.do_math_mixed.called_with(3)) self.assertFalse(obj.do_math_mixed.called_with(4)) self.assertFalse(obj.do_math_mixed.called_with(1, 2, 3)) def test_called_with_and_partial_kwargs(self): """Testing FunctionSpy.called_with and partial keyword arguments""" obj = MathClass() self.agency.spy_on(obj.do_math_mixed) obj.do_math_mixed(a=1, b=2) obj.do_math_mixed(a=3, b=4) self.assertTrue(obj.do_math_mixed.called_with(1)) self.assertTrue(obj.do_math_mixed.called_with(b=2)) self.assertTrue(obj.do_math_mixed.called_with(3)) self.assertTrue(obj.do_math_mixed.called_with(b=4)) self.assertTrue(obj.do_math_mixed.called_with(a=1, b=2)) self.assertTrue(obj.do_math_mixed.called_with(a=3, b=4)) self.assertFalse(obj.do_math_mixed.called_with(1, 2)) self.assertFalse(obj.do_math_mixed.called_with(3, 4)) self.assertFalse(obj.do_math_mixed.called_with(a=4)) self.assertFalse(obj.do_math_mixed.called_with(a=1, b=2, c=3)) self.assertFalse(obj.do_math_mixed.called_with(a=1, b=4)) self.assertFalse(obj.do_math_mixed.called_with(a=3, b=2)) def test_last_called_with(self): """Testing FunctionSpy.last_called_with""" obj = MathClass() self.agency.spy_on(obj.do_math_mixed) obj.do_math_mixed(1, 2) obj.do_math_mixed(3, 4) self.assertFalse(obj.do_math_mixed.last_called_with(1, a=2)) self.assertTrue(obj.do_math_mixed.last_called_with(3, b=4)) def test_last_called_with_and_keyword_args(self): """Testing FunctionSpy.last_called_with and keyword arguments""" obj = MathClass() self.agency.spy_on(obj.do_math_mixed) obj.do_math_mixed(a=1, b=2) obj.do_math_mixed(a=3, b=4) self.assertTrue(obj.do_math_mixed.last_called_with(3, b=4)) self.assertFalse(obj.do_math_mixed.last_called_with(1, b=2)) self.assertFalse(obj.do_math_mixed.last_called_with(1, b=2, c=3)) def test_last_called_with_and_partial_args(self): """Testing FunctionSpy.called_with and partial arguments""" obj = MathClass() self.agency.spy_on(obj.do_math_mixed) obj.do_math_mixed(1, 2) obj.do_math_mixed(3, 4) self.assertTrue(obj.do_math_mixed.last_called_with(3)) self.assertTrue(obj.do_math_mixed.last_called_with(3, b=4)) self.assertFalse(obj.do_math_mixed.last_called_with(3, b=4, c=5)) self.assertFalse(obj.do_math_mixed.last_called_with(1, b=2)) def test_last_called_with_and_partial_kwargs(self): """Testing FunctionSpy.called_with and partial keyword arguments""" obj = MathClass() self.agency.spy_on(obj.do_math_mixed) obj.do_math_mixed(a=1, b=2) obj.do_math_mixed(a=3, b=4) self.assertTrue(obj.do_math_mixed.last_called_with(3)) self.assertTrue(obj.do_math_mixed.last_called_with(b=4)) self.assertFalse(obj.do_math_mixed.last_called_with(a=1)) self.assertFalse(obj.do_math_mixed.last_called_with(b=2)) self.assertFalse(obj.do_math_mixed.last_called_with(b=3)) self.assertFalse(obj.do_math_mixed.last_called_with(3, 4)) self.assertFalse(obj.do_math_mixed.last_called_with(a=1, b=2, c=3)) self.assertFalse(obj.do_math_mixed.last_called_with(a=1, c=3)) def test_returned(self): """Testing FunctionSpy.returned""" obj = MathClass() self.agency.spy_on(obj.do_math) obj.do_math(1, 2) obj.do_math(3, 4) self.assertTrue(obj.do_math.returned(3)) self.assertTrue(obj.do_math.returned(7)) self.assertFalse(obj.do_math.returned(10)) self.assertFalse(obj.do_math.returned(None)) def test_last_returned(self): """Testing FunctionSpy.last_returned""" obj = MathClass() self.agency.spy_on(obj.do_math) obj.do_math(1, 2) obj.do_math(3, 4) self.assertFalse(obj.do_math.last_returned(3)) self.assertTrue(obj.do_math.last_returned(7)) self.assertFalse(obj.do_math.last_returned(None)) def test_raised(self): """Testing FunctionSpy.raised""" obj = MathClass() self.agency.spy_on(obj.do_math) with self.assertRaises(TypeError): obj.do_math(1, 'a') self.assertTrue(obj.do_math.raised(TypeError)) self.assertFalse(obj.do_math.raised(ValueError)) self.assertFalse(obj.do_math.raised(None)) def test_last_raised(self): """Testing FunctionSpy.last_raised""" obj = MathClass() self.agency.spy_on(obj.do_math) with self.assertRaises(TypeError): obj.do_math(1, 'a') self.assertTrue(obj.do_math.last_raised(TypeError)) self.assertFalse(obj.do_math.last_raised(None)) obj.do_math(1, 4) self.assertFalse(obj.do_math.last_raised(TypeError)) self.assertTrue(obj.do_math.last_raised(None)) def test_raised_with_message(self): """Testing FunctionSpy.raised_with_message""" obj = MathClass() self.agency.spy_on(obj.do_math) with self.assertRaises(TypeError): obj.do_math(1, 'a') self.assertTrue(obj.do_math.raised_with_message( TypeError, "unsupported operand type(s) for +: 'int' and '%s'" % text_type.__name__)) self.assertFalse(obj.do_math.raised_with_message( ValueError, "unsupported operand type(s) for +: 'int' and '%s'" % text_type.__name__)) self.assertFalse(obj.do_math.raised_with_message(TypeError, None)) def test_last_raised_with_message(self): """Testing FunctionSpy.last_raised_with_message""" obj = MathClass() self.agency.spy_on(obj.do_math) with self.assertRaises(TypeError): obj.do_math(1, 'a') self.assertTrue(obj.do_math.last_raised_with_message( TypeError, "unsupported operand type(s) for +: 'int' and '%s'" % text_type.__name__)) self.assertFalse(obj.do_math.last_raised_with_message(TypeError, None)) def test_reset_calls(self): """Testing FunctionSpy.reset_calls""" obj = MathClass() self.agency.spy_on(obj.do_math) obj.do_math(1, 2) self.assertEqual(len(obj.do_math.calls), 1) self.assertEqual(obj.do_math.last_call, obj.do_math.calls[-1]) self.assertTrue(obj.do_math.called) obj.do_math.reset_calls() self.assertEqual(len(obj.do_math.calls), 0) self.assertIsNone(obj.do_math.last_call) self.assertFalse(obj.do_math.called) def test_repr(self): """Testing FunctionSpy.__repr__""" self.agency.spy_on(something_awesome) self.assertTrue(hasattr(something_awesome, 'spy')) self.assertTrue(repr(something_awesome.spy), '') def test_repr_and_function(self): """Testing FunctionSpy.__repr__ and function""" self.agency.spy_on(do_math) self.assertEqual(repr(do_math.spy), '') def test_repr_and_bound_method(self): """Testing FunctionSpy.__repr__ and bound method""" obj = MathClass() self.agency.spy_on(obj.do_math) obj.do_math() self.assertEqual(repr(obj.do_math.spy), '' % obj) def test_repr_and_unbound_method(self): """Testing FunctionSpy.__repr__ and unbound method""" self.agency.spy_on(MathClass.do_math) self.assertEqual(repr(MathClass.do_math.spy), '' % MathClass) def test_repr_with_classmethod(self): """Testing FunctionSpy.__repr__ with classmethod""" self.agency.spy_on(MathClass.class_do_math) self.assertEqual( repr(MathClass.class_do_math.spy), '' % MathClass) @require_getargspec def test_getargspec_with_function(self): """Testing FunctionSpy in inspect.getargspec() with function""" self.agency.spy_on(do_math) args, varargs, keywords, defaults = inspect.getargspec(do_math) self.assertEqual(args, ['a', 'b']) self.assertEqual(varargs, 'args') self.assertEqual(keywords, 'kwargs') self.assertEqual(defaults, (1, 2)) @require_getargspec def test_getargspec_with_bound_method(self): """Testing FunctionSpy in inspect.getargspec() with bound method""" obj = MathClass() self.agency.spy_on(obj.do_math) args, varargs, keywords, defaults = inspect.getargspec(obj.do_math) self.assertEqual(args, ['self', 'a', 'b']) self.assertEqual(varargs, 'args') self.assertEqual(keywords, 'kwargs') self.assertEqual(defaults, (1, 2)) @require_getargspec def test_getargspec_with_unbound_method(self): """Testing FunctionSpy in inspect.getargspec() with unbound method""" self.agency.spy_on(MathClass.do_math) args, varargs, keywords, defaults = \ inspect.getargspec(MathClass.do_math) self.assertEqual(args, ['self', 'a', 'b']) self.assertEqual(varargs, 'args') self.assertEqual(keywords, 'kwargs') self.assertEqual(defaults, (1, 2)) @require_getargspec def test_getargspec_with_classmethod(self): """Testing FunctionSpy in inspect.getargspec() with classmethod""" obj = MathClass() self.agency.spy_on(obj.class_do_math) args, varargs, keywords, defaults = \ inspect.getargspec(obj.class_do_math) self.assertEqual(args, ['cls', 'a', 'b']) self.assertEqual(varargs, 'args') self.assertEqual(keywords, 'kwargs') self.assertEqual(defaults, (2, 5)) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1730689021.0 kgb-7.2/kgb/tests/test_ops.py0000644000076500000240000005716714712033775015511 0ustar00chipx86staff"""Unit tests for kgb.ops.""" from __future__ import unicode_literals import re from kgb.errors import UnexpectedCallError from kgb.ops import (SpyOpMatchAny, SpyOpMatchInOrder, SpyOpRaise, SpyOpRaiseInOrder, SpyOpReturn, SpyOpReturnInOrder) from kgb.tests.base import MathClass, TestCase class SpyOpMatchAnyTests(TestCase): """Unit tests for kgb.ops.SpyOpMatchAny.""" def test_setup_with_instance(self): """Testing SpyOpMatchAny set up with op=SpyOpMatchAny([...])""" obj = MathClass() self.agency.spy_on( obj.do_math, op=SpyOpMatchAny([ { 'kwargs': { 'a': 1, 'b': 2, }, 'call_fake': lambda a, b: a - b, }, ])) self.assertEqual(obj.do_math(a=1, b=2), -1) def test_setup_with_instance_and_op(self): """Testing SpyOpMatchAny set up with op=SpyOpMatchAny([...]) and op""" obj = MathClass() self.agency.spy_on( obj.do_math, op=SpyOpMatchAny([ { 'kwargs': { 'a': 1, 'b': 2, }, 'op': SpyOpMatchInOrder([ { 'kwargs': { 'a': 1, 'b': 2, 'x': 1, }, 'op': SpyOpReturn(123), }, { 'kwargs': { 'a': 1, 'b': 2, 'x': 2, }, 'op': SpyOpReturn(456), }, ]), }, ])) self.assertEqual(obj.do_math(a=1, b=2, x=1), 123) self.assertEqual(obj.do_math(a=1, b=2, x=2), 456) def test_with_function(self): """Testing SpyOpMatchAny with function""" def do_math(a, b): return a + b self.agency.spy_on( do_math, op=SpyOpMatchAny([ { 'args': [5, 3], 'call_fake': lambda a, b: a - b }, ])) self.assertEqual(do_math(5, 3), 2) def test_with_function_and_op(self): """Testing SpyOpMatchAny with function and op""" def do_math(a, b, x=0): return a + b self.agency.spy_on( do_math, op=SpyOpMatchAny([ { 'args': [5, 3], 'op': SpyOpMatchInOrder([ { 'args': [5, 3], 'kwargs': { 'x': 1, }, 'op': SpyOpReturn(123), }, { 'args': [5, 3], 'kwargs': { 'x': 2, }, 'op': SpyOpReturn(456), }, ]), }, ])) self.assertEqual(do_math(a=5, b=3, x=1), 123) self.assertEqual(do_math(a=5, b=3, x=2), 456) def test_with_classmethod(self): """Testing SpyOpMatchAny with classmethod""" self.agency.spy_on( MathClass.class_do_math, owner=MathClass, op=SpyOpMatchAny([ { 'kwargs': { 'a': 5, 'b': 3, }, 'call_fake': lambda a, b: a - b }, ])) self.assertEqual(MathClass.class_do_math(a=5, b=3), 2) def test_with_classmethod_and_op(self): """Testing SpyOpMatchAny with classmethod and op""" self.agency.spy_on( MathClass.class_do_math, owner=MathClass, op=SpyOpMatchAny([ { 'kwargs': { 'a': 5, 'b': 3, }, 'op': SpyOpMatchInOrder([ { 'kwargs': { 'a': 5, 'b': 3, 'x': 1, }, 'op': SpyOpReturn(123), }, { 'kwargs': { 'a': 5, 'b': 3, 'x': 2, }, 'op': SpyOpReturn(456), }, ]), }, ])) self.assertEqual(MathClass.class_do_math(a=5, b=3, x=1), 123) self.assertEqual(MathClass.class_do_math(a=5, b=3, x=2), 456) def test_with_unbound_method(self): """Testing SpyOpMatchAny with unbound method""" self.agency.spy_on( MathClass.do_math, owner=MathClass, op=SpyOpMatchAny([ { 'kwargs': { 'a': 4, 'b': 3, }, }, ])) obj = MathClass() self.assertEqual(obj.do_math(a=4, b=3), 7) def test_with_unbound_method_and_op(self): """Testing SpyOpMatchAny with unbound method and op""" self.agency.spy_on( MathClass.do_math, owner=MathClass, op=SpyOpMatchAny([ { 'kwargs': { 'a': 4, 'b': 3, }, 'op': SpyOpMatchInOrder([ { 'kwargs': { 'a': 4, 'b': 3, 'x': 1, }, 'op': SpyOpReturn(123), }, { 'kwargs': { 'a': 4, 'b': 3, 'x': 2, }, 'op': SpyOpReturn(456), }, ]), }, ])) obj = MathClass() self.assertEqual(obj.do_math(a=4, b=3, x=1), 123) self.assertEqual(obj.do_math(a=4, b=3, x=2), 456) def test_with_expected_calls(self): """Testing SpyOpMatchAny with all expected calls""" obj = MathClass() self.agency.spy_on( obj.do_math, op=SpyOpMatchAny([ { 'kwargs': { 'a': 4, 'b': 7, }, }, { 'kwargs': { 'a': 2, 'b': 8, }, 'call_original': False, }, { 'kwargs': { 'a': 100, 'b': 200, }, 'op': SpyOpMatchInOrder([ { 'kwargs': { 'a': 100, 'b': 200, 'x': 1, }, 'op': SpyOpReturn(123), }, { 'kwargs': { 'a': 100, 'b': 200, 'x': 2, }, 'op': SpyOpReturn(456), }, ]), }, { 'kwargs': { 'a': 5, 'b': 9, }, 'call_fake': lambda a, b: a + b + 10, }, { 'a': 2, 'call_fake': lambda a, b: 1001, }, ])) values = [ obj.do_math(5, b=9), obj.do_math(a=2, b=8), obj.do_math(a=100, b=200, x=1), obj.do_math(a=100, b=200, x=2), obj.do_math(a=1, b=1), obj.do_math(4, 7), ] self.assertEqual(values, [24, None, 123, 456, 1001, 11]) def test_with_unexpected_call(self): """Testing SpyOpMatchAny with unexpected call""" obj = MathClass() self.agency.spy_on( obj.do_math, op=SpyOpMatchAny([ { 'kwargs': { 'a': 4, 'b': 7, }, }, ])) expected_message = re.escape( 'do_math was not called with any expected arguments.' ) with self.assertRaisesRegex(AssertionError, expected_message): obj.do_math(a=4, b=9) class SpyOpMatchInOrderTests(TestCase): """Unit tests for kgb.ops.SpyOpMatchInOrder.""" def test_setup_with_instance(self): """Testing SpyOpMatchInOrder set up with op=SpyOpMatchInOrder([...])""" obj = MathClass() self.agency.spy_on( obj.do_math, op=SpyOpMatchInOrder([ { 'kwargs': { 'a': 1, 'b': 2, }, }, ])) self.assertEqual(obj.do_math(a=1, b=2), 3) def test_setup_with_instance_and_op(self): """Testing SpyOpMatchInOrder set up with op=SpyOpMatchInOrder([...]) and op """ obj = MathClass() self.agency.spy_on( obj.do_math, op=SpyOpMatchInOrder([ { 'kwargs': { 'a': 1, 'b': 2, }, 'op': SpyOpReturn(123), }, ])) self.assertEqual(obj.do_math(a=1, b=2), 123) def test_with_function(self): """Testing SpyOpMatchInOrder with function""" def do_math(a, b): return a + b self.agency.spy_on( do_math, op=SpyOpMatchInOrder([ { 'args': [5, 3], 'call_fake': lambda a, b: a - b }, ])) self.assertEqual(do_math(5, 3), 2) def test_with_function_and_op(self): """Testing SpyOpMatchInOrder with function and op""" def do_math(a, b): return a + b self.agency.spy_on( do_math, op=SpyOpMatchInOrder([ { 'args': [5, 3], 'op': SpyOpReturn(123), }, ])) self.assertEqual(do_math(5, 3), 123) def test_with_classmethod(self): """Testing SpyOpMatchInOrder with classmethod""" self.agency.spy_on( MathClass.class_do_math, owner=MathClass, op=SpyOpMatchInOrder([ { 'kwargs': { 'a': 5, 'b': 3, }, 'call_fake': lambda a, b: a - b }, ])) self.assertEqual(MathClass.class_do_math(a=5, b=3), 2) def test_with_classmethod_and_op(self): """Testing SpyOpMatchInOrder with classmethod and op""" self.agency.spy_on( MathClass.class_do_math, owner=MathClass, op=SpyOpMatchInOrder([ { 'kwargs': { 'a': 5, 'b': 3, }, 'op': SpyOpReturn(123), }, ])) self.assertEqual(MathClass.class_do_math(a=5, b=3), 123) def test_with_unbound_method(self): """Testing SpyOpMatchInOrder with unbound method""" self.agency.spy_on( MathClass.do_math, owner=MathClass, op=SpyOpMatchInOrder([ { 'kwargs': { 'a': 4, 'b': 3, }, }, ])) obj = MathClass() self.assertEqual(obj.do_math(a=4, b=3), 7) def test_with_unbound_method_and_op(self): """Testing SpyOpMatchInOrder with unbound method and op""" self.agency.spy_on( MathClass.do_math, owner=MathClass, op=SpyOpMatchInOrder([ { 'kwargs': { 'a': 4, 'b': 3, }, 'op': SpyOpReturn(123), }, ])) obj = MathClass() self.assertEqual(obj.do_math(a=4, b=3), 123) def test_with_expected_calls(self): """Testing SpyOpMatchInOrder with all expected calls""" obj = MathClass() self.agency.spy_on( obj.do_math, op=SpyOpMatchInOrder([ { 'kwargs': { 'a': 4, 'b': 7, }, }, { 'kwargs': { 'a': 2, 'b': 8, }, 'call_original': False, }, { 'kwargs': { 'a': 100, 'b': 200, }, 'op': SpyOpReturn(123), }, { 'kwargs': { 'a': 5, 'b': 9, }, 'call_fake': lambda a, b: a + b + 10, }, { 'call_fake': lambda a, b: 1001, }, ])) values = [ obj.do_math(4, 7), obj.do_math(a=2, b=8), obj.do_math(a=100, b=200), obj.do_math(5, b=9), obj.do_math(a=1, b=1), ] self.assertEqual(values, [11, None, 123, 24, 1001]) def test_with_unexpected_call(self): """Testing SpyOpMatchInOrder with unexpected call""" obj = MathClass() self.agency.spy_on( obj.do_math, op=SpyOpMatchInOrder([ { 'kwargs': { 'a': 4, 'b': 7, }, }, ])) expected_message = re.escape( "This call to do_math was not passed args=(), " "kwargs={'a': 4, 'b': 7}.\n" "\n" "It was called with:\n" "\n" "args=()\n" "kwargs={'a': 4, 'b': 9}" ) with self.assertRaisesRegex(AssertionError, expected_message): obj.do_math(a=4, b=9) def test_with_extra_call(self): """Testing SpyOpMatchInOrder with extra unexpected call""" obj = MathClass() self.agency.spy_on( obj.do_math, op=SpyOpMatchInOrder([ { 'kwargs': { 'a': 4, 'b': 7, }, }, ])) self.assertEqual(obj.do_math(a=4, b=7), 11) expected_message = re.escape( "do_math was called 2 time(s), but only 1 call(s) were expected. " "Latest call: " ) with self.assertRaisesRegex(UnexpectedCallError, expected_message): obj.do_math(a=4, b=9) class SpyOpRaiseTests(TestCase): """Unit tests for kgb.ops.SpyOpRaise.""" def test_with_function(self): """Testing SpyOpRaise with function""" def do_math(a, b): return a + b self.agency.spy_on( do_math, op=SpyOpRaise(ValueError('foo'))) with self.assertRaisesRegex(ValueError, 'foo'): do_math(5, 3) def test_with_classmethod(self): """Testing SpyOpRaise with classmethod""" self.agency.spy_on( MathClass.class_do_math, owner=MathClass, op=SpyOpRaise(ValueError('foo'))) with self.assertRaisesRegex(ValueError, 'foo'): MathClass.class_do_math(5, 3) def test_with_unbound_method(self): """Testing SpyOpRaise with unbound method""" self.agency.spy_on( MathClass.do_math, owner=MathClass, op=SpyOpRaise(ValueError('foo'))) obj = MathClass() with self.assertRaisesRegex(ValueError, 'foo'): obj.do_math(a=4, b=3) class SpyOpRaiseInOrderTests(TestCase): """Unit tests for kgb.ops.SpyOpRaiseInOrder.""" def test_with_function(self): """Testing SpyOpRaiseInOrder with function""" def do_math(a, b): return a + b self.agency.spy_on( do_math, op=SpyOpRaiseInOrder([ ValueError('foo'), KeyError('bar'), AttributeError('foobar'), ])) with self.assertRaisesRegex(ValueError, 'foo'): do_math(5, 3) with self.assertRaisesRegex(KeyError, 'bar'): do_math(5, 3) with self.assertRaisesRegex(AttributeError, 'foobar'): do_math(5, 3) message = re.escape( "do_math was called 4 time(s), but only 3 call(s) were expected. " "Latest call: " ) with self.assertRaisesRegex(UnexpectedCallError, message): do_math(5, 3) def test_with_classmethod(self): """Testing SpyOpRaiseInOrder with classmethod""" self.agency.spy_on( MathClass.class_do_math, owner=MathClass, op=SpyOpRaiseInOrder([ ValueError('foo'), KeyError('bar'), AttributeError('foobar'), ])) with self.assertRaisesRegex(ValueError, 'foo'): MathClass.class_do_math(5, 3) with self.assertRaisesRegex(KeyError, 'bar'): MathClass.class_do_math(5, 3) with self.assertRaisesRegex(AttributeError, 'foobar'): MathClass.class_do_math(5, 3) message = re.escape( "class_do_math was called 4 time(s), but only 3 call(s) were " "expected. Latest call: " ) with self.assertRaisesRegex(UnexpectedCallError, message): MathClass.class_do_math(5, 3) def test_with_unbound_method(self): """Testing SpyOpRaiseInOrder with unbound method""" self.agency.spy_on( MathClass.do_math, owner=MathClass, op=SpyOpRaiseInOrder([ ValueError('foo'), KeyError('bar'), AttributeError('foobar'), ])) obj = MathClass() with self.assertRaisesRegex(ValueError, 'foo'): obj.do_math(a=4, b=3) with self.assertRaisesRegex(KeyError, 'bar'): obj.do_math(a=4, b=3) with self.assertRaisesRegex(AttributeError, 'foobar'): obj.do_math(a=4, b=3) message = re.escape( "do_math was called 4 time(s), but only 3 call(s) were expected. " "Latest call: " ) with self.assertRaisesRegex(UnexpectedCallError, message): obj.do_math(5, 3) class SpyOpReturnTests(TestCase): """Unit tests for kgb.ops.SpyOpReturn.""" def test_with_function(self): """Testing SpyOpReturn with function""" def do_math(a, b): return a + b self.agency.spy_on( do_math, op=SpyOpReturn('abc123')) self.assertEqual(do_math(5, 3), 'abc123') def test_with_classmethod(self): """Testing SpyOpReturn with classmethod""" self.agency.spy_on( MathClass.class_do_math, owner=MathClass, op=SpyOpReturn('abc123')) self.assertEqual(MathClass.class_do_math(5, 3), 'abc123') def test_with_unbound_method(self): """Testing SpyOpReturn with unbound method""" self.agency.spy_on( MathClass.do_math, owner=MathClass, op=SpyOpReturn('abc123')) obj = MathClass() self.assertEqual(obj.do_math(a=4, b=3), 'abc123') class SpyOpReturnInOrderTests(TestCase): """Unit tests for kgb.ops.SpyOpReturnInOrder.""" def test_with_function(self): """Testing SpyOpReturnInOrder with function""" def do_math(a, b): return a + b self.agency.spy_on( do_math, op=SpyOpReturnInOrder([ 'abc123', 'def456', 'ghi789', ])) self.assertEqual(do_math(5, 3), 'abc123') self.assertEqual(do_math(5, 3), 'def456') self.assertEqual(do_math(5, 3), 'ghi789') message = re.escape( "do_math was called 4 time(s), but only 3 call(s) were expected. " "Latest call: " ) with self.assertRaisesRegex(UnexpectedCallError, message): do_math(5, 3) def test_with_classmethod(self): """Testing SpyOpReturnInOrder with classmethod""" self.agency.spy_on( MathClass.class_do_math, owner=MathClass, op=SpyOpReturnInOrder([ 'abc123', 'def456', 'ghi789', ])) self.assertEqual(MathClass.class_do_math(5, 3), 'abc123') self.assertEqual(MathClass.class_do_math(5, 3), 'def456') self.assertEqual(MathClass.class_do_math(5, 3), 'ghi789') message = re.escape( "class_do_math was called 4 time(s), but only 3 call(s) were " "expected. Latest call: " ) with self.assertRaisesRegex(UnexpectedCallError, message): MathClass.class_do_math(5, 3) def test_with_unbound_method(self): """Testing SpyOpReturnInOrder with unbound method""" self.agency.spy_on( MathClass.do_math, owner=MathClass, op=SpyOpReturnInOrder([ 'abc123', 'def456', 'ghi789', ])) obj = MathClass() self.assertEqual(obj.do_math(a=4, b=3), 'abc123') self.assertEqual(obj.do_math(a=4, b=3), 'def456') self.assertEqual(obj.do_math(a=4, b=3), 'ghi789') message = re.escape( "do_math was called 4 time(s), but only 3 call(s) were " "expected. Latest call: " ) with self.assertRaisesRegex(UnexpectedCallError, message): obj.do_math(a=4, b=3) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1730689021.0 kgb-7.2/kgb/tests/test_pytest_plugin.py0000644000076500000240000000062614712033775017602 0ustar00chipx86staff"""Unit tests for kgb.pytest_plugin. Version Added: 7.0 """ from __future__ import unicode_literals from kgb.agency import SpyAgency from kgb.tests.base import MathClass def test_pytest_plugin(spy_agency): """Testing pytest spy_agency fixture""" assert isinstance(spy_agency, SpyAgency) obj = MathClass() spy = spy_agency.spy_on(obj.do_math) assert spy_agency.spies == {spy} ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1730689021.0 kgb-7.2/kgb/tests/test_spy_agency.py0000644000076500000240000007156314712033775017045 0ustar00chipx86staff"""Unit tests for kgb.agency.SpyAgency.""" from __future__ import unicode_literals from contextlib import contextmanager import kgb.asserts from kgb.agency import SpyAgency from kgb.signature import FunctionSig from kgb.tests.base import MathClass, TestCase class SpyAgencyTests(TestCase): """Unit tests for kgb.agency.SpyAgency.""" def test_spy_on(self): """Testing SpyAgency.spy_on""" obj = MathClass() spy = self.agency.spy_on(obj.do_math) self.assertEqual(self.agency.spies, set([spy])) def test_spy_for(self): """Testing SpyAgency.spy_for""" obj = MathClass() @self.agency.spy_for(obj.do_math, owner=obj) def my_func(_self, a=None, b=None, *args, **kwargs): """Some docs.""" return 123 self.assertEqual(obj.do_math(), 123) self.assertEqual(my_func(obj), 123) self.assertIs(obj.do_math.spy.func, my_func) # Make sure we decorated correctly. sig = FunctionSig(my_func) self.assertEqual(sig.func_name, 'my_func') self.assertEqual(sig.func_type, FunctionSig.TYPE_FUNCTION) self.assertEqual(my_func.__doc__, 'Some docs.') def test_unspy(self): """Testing SpyAgency.unspy""" obj = MathClass() orig_do_math = obj.do_math spy = self.agency.spy_on(obj.do_math) self.assertEqual(self.agency.spies, set([spy])) self.assertTrue(hasattr(obj.do_math, 'spy')) self.agency.unspy(obj.do_math) self.assertEqual(self.agency.spies, set()) self.assertFalse(hasattr(obj.do_math, 'spy')) self.assertEqual(obj.do_math, orig_do_math) def test_unspy_all(self): """Testing SpyAgency.unspy_all""" obj = MathClass() orig_do_math = obj.do_math spy1 = self.agency.spy_on(obj.do_math) spy2 = self.agency.spy_on(MathClass.class_do_math) self.assertEqual(self.agency.spies, set([spy1, spy2])) self.assertTrue(hasattr(obj.do_math, 'spy')) self.assertTrue(hasattr(MathClass.class_do_math, 'spy')) self.agency.unspy_all() self.assertEqual(self.agency.spies, set()) self.assertEqual(obj.do_math, orig_do_math) self.assertEqual(MathClass.class_do_math, self.orig_class_do_math) self.assertFalse(hasattr(obj.do_math, 'spy')) self.assertFalse(hasattr(MathClass.class_do_math, 'spy')) class TestCaseMixinTests(SpyAgency, TestCase): """Unit tests for SpyAgency as a TestCase mixin.""" def test_spy_on(self): """Testing SpyAgency mixed in with spy_on""" obj = MathClass() self.spy_on(obj.do_math) self.assertTrue(hasattr(obj.do_math, 'spy')) result = obj.do_math() self.assertEqual(result, 3) def test_spy_for(self): """Testing SpyAgency mixed in with spy_for""" obj = MathClass() @self.spy_for(obj.do_math) def my_func(_self, a=None, b=None, *args, **kwargs): """Some docs.""" return 123 self.assertEqual(obj.do_math(), 123) self.assertEqual(my_func(obj), 123) self.assertIs(obj.do_math.spy.func, my_func) # Make sure we decorated correctly. sig = FunctionSig(my_func) self.assertEqual(sig.func_name, 'my_func') self.assertEqual(sig.func_type, FunctionSig.TYPE_FUNCTION) self.assertEqual(my_func.__doc__, 'Some docs.') def test_tear_down(self): """Testing SpyAgency mixed in with tearDown""" obj = MathClass() orig_do_math = obj.do_math func_dict = obj.do_math.__dict__.copy() self.spy_on(obj.do_math) self.assertTrue(hasattr(obj.do_math, 'spy')) self.assertNotEqual(func_dict, obj.do_math.__dict__) self.tearDown() self.assertEqual(obj.do_math, orig_do_math) self.assertFalse(hasattr(obj.do_math, 'spy')) self.assertEqual(func_dict, obj.do_math.__dict__) def test_assertHasSpy_with_spy(self): """Testing SpyAgency.assertHasSpy with spy""" self.spy_on(MathClass.do_math, owner=MathClass) # These should not fail. self.assertHasSpy(MathClass.do_math) self.assertHasSpy(MathClass.do_math.spy) # Check the aliases. self.assert_has_spy(MathClass.do_math) kgb.asserts.assert_has_spy(MathClass.do_math) def test_assertHasSpy_without_spy(self): """Testing SpyAgency.assertHasSpy without spy""" with self._check_assertion('do_math has not been spied on.'): self.assertHasSpy(MathClass.do_math) def test_assertSpyCalled_with_called(self): """Testing SpyAgency.assertSpyCalled with spy called""" obj = MathClass() self.spy_on(obj.do_math) obj.do_math() # These should not fail. self.assertSpyCalled(obj.do_math) self.assertSpyCalled(obj.do_math.spy) # Check the aliases. self.assert_spy_called(obj.do_math) kgb.asserts.assert_spy_called(obj.do_math) def test_assertSpyCalled_without_called(self): """Testing SpyAgency.assertSpyCalled without spy called""" obj = MathClass() self.spy_on(obj.do_math) msg = 'do_math was not called.' with self._check_assertion(msg): self.assertSpyCalled(obj.do_math) with self._check_assertion(msg): self.assertSpyCalled(obj.do_math.spy) def test_assertSpyNotCalled_without_called(self): """Testing SpyAgency.assertSpyNotCalled without spy called""" obj = MathClass() self.spy_on(obj.do_math) # These should not fail. self.assertSpyNotCalled(obj.do_math) self.assertSpyNotCalled(obj.do_math.spy) # Check the aliases. self.assert_spy_not_called(obj.do_math) kgb.asserts.assert_spy_not_called(obj.do_math) def test_assertSpyNotCalled_with_called(self): """Testing SpyAgency.assertSpyNotCalled with spy called""" obj = MathClass() self.spy_on(obj.do_math) obj.do_math(3, b=4) obj.do_math(2, b=9) msg = ( "do_math was called 2 times:\n" "\n" "Call 0:\n" " args=()\n" " kwargs={'a': 3, 'b': 4}\n" "\n" "Call 1:\n" " args=()\n" " kwargs={'a': 2, 'b': 9}" ) with self._check_assertion(msg): self.assertSpyNotCalled(obj.do_math) with self._check_assertion(msg): self.assertSpyNotCalled(obj.do_math.spy) def test_assertSpyCallCount_with_expected_count(self): """Testing SpyAgency.assertSpyCallCount with expected call count""" obj = MathClass() self.spy_on(obj.do_math) obj.do_math() obj.do_math() # These should not fail. self.assertSpyCallCount(obj.do_math, 2) self.assertSpyCallCount(obj.do_math.spy, 2) # Check the aliases. self.assert_spy_call_count(obj.do_math, 2) kgb.asserts.assert_spy_call_count(obj.do_math, 2) def test_assertSpyCallCount_without_expected_count(self): """Testing SpyAgency.assertSpyCallCount without expected call count""" obj = MathClass() self.spy_on(obj.do_math) obj.do_math() with self._check_assertion('do_math was called 1 time, not 2.'): self.assertSpyCallCount(obj.do_math, 2) # Let's bump and test a plural result. obj.do_math() with self._check_assertion('do_math was called 2 times, not 3.'): self.assertSpyCallCount(obj.do_math.spy, 3) def test_assertSpyCalledWith_with_expected_arguments(self): """Testing SpyAgency.assertSpyCalledWith with expected arguments""" obj = MathClass() self.spy_on(obj.do_math) obj.do_math(1, b=4) obj.do_math(2, b=9) # These should not fail. self.assertSpyCalledWith(obj.do_math, a=1, b=4) self.assertSpyCalledWith(obj.do_math.calls[0], a=1, b=4) self.assertSpyCalledWith(obj.do_math.spy, a=2, b=9) self.assertSpyCalledWith(obj.do_math.spy.calls[1], a=2, b=9) # Check the aliases. self.assert_spy_called_with(obj.do_math, a=1, b=4) kgb.asserts.assert_spy_called_with(obj.do_math, a=1, b=4) def test_assertSpyCalledWith_without_expected_arguments(self): """Testing SpyAgency.assertSpyCalledWith without expected arguments""" obj = MathClass() self.spy_on(obj.do_math) obj.do_math(1, b=4) obj.do_math(2, b=9) msg = ( "No call to do_math was passed args=(), kwargs={'x': 4, 'z': 1}.\n" "\n" "The following calls were recorded:\n" "\n" "Call 0:\n" " args=()\n" " kwargs={'a': 1, 'b': 4}\n" "\n" "Call 1:\n" " args=()\n" " kwargs={'a': 2, 'b': 9}" ) with self._check_assertion(msg): self.assertSpyCalledWith(obj.do_math, x=4, z=1) with self._check_assertion(msg): self.assertSpyCalledWith(obj.do_math.spy, x=4, z=1) msg = ( "This call to do_math was not passed args=()," " kwargs={'x': 4, 'z': 1}.\n" "\n" "It was called with:\n" "\n" "args=()\n" "kwargs={'a': 1, 'b': 4}" ) with self._check_assertion(msg): self.assertSpyCalledWith(obj.do_math.spy.calls[0], x=4, z=1) def test_assertSpyNotCalledWith_with_unexpected_arguments(self): """Testing SpyAgency.assertSpyNotCalledWith with unexpected arguments """ obj = MathClass() self.spy_on(obj.do_math) obj.do_math(1, b=4) obj.do_math(2, b=9) # These should not fail. self.assertSpyNotCalledWith(obj.do_math, a=1, b=3) self.assertSpyNotCalledWith(obj.do_math.calls[0], a=1, b=3) self.assertSpyNotCalledWith(obj.do_math.spy, a=1, b=9) self.assertSpyNotCalledWith(obj.do_math.spy.calls[1], a=1, b=9) # Check the aliases. self.assert_spy_not_called_with(obj.do_math, a=1, b=3) kgb.asserts.assert_spy_not_called_with(obj.do_math, a=1, b=3) def test_assertSpyNotCalledWith_without_unexpected_arguments(self): """Testing SpyAgency.assertSpyNotCalledWith without unexpected arguments """ obj = MathClass() self.spy_on(obj.do_math) obj.do_math(1, b=4) obj.do_math(2, b=9) msg = ( "A call to do_math was unexpectedly passed args=(), " "kwargs={'a': 1, 'b': 4}.\n" "\n" "The following calls were recorded:\n" "\n" "Call 0:\n" " args=()\n" " kwargs={'a': 1, 'b': 4}\n" "\n" "Call 1:\n" " args=()\n" " kwargs={'a': 2, 'b': 9}" ) with self._check_assertion(msg): self.assertSpyNotCalledWith(obj.do_math, a=1, b=4) with self._check_assertion(msg): self.assertSpyNotCalledWith(obj.do_math.spy, a=1, b=4) msg = ( "This call to do_math was unexpectedly passed args=()," " kwargs={'a': 2, 'b': 9}." ) with self._check_assertion(msg): self.assertSpyNotCalledWith(obj.do_math.spy.calls[1], a=2, b=9) def test_assertSpyLastCalledWith_with_expected_arguments(self): """Testing SpyAgency.assertSpyLastCalledWith with expected arguments""" obj = MathClass() self.spy_on(obj.do_math) obj.do_math(1, b=4) obj.do_math(2, b=9) # These should not fail. self.assertSpyLastCalledWith(obj.do_math, a=2, b=9) self.assertSpyLastCalledWith(obj.do_math.spy, a=2, b=9) # Check the aliases. self.assert_spy_last_called_with(obj.do_math, a=2, b=9) kgb.asserts.assert_spy_last_called_with(obj.do_math, a=2, b=9) def test_assertSpyLastCalledWith_without_expected_arguments(self): """Testing SpyAgency.assertSpyLastCalledWith without expected arguments """ obj = MathClass() self.spy_on(obj.do_math) obj.do_math(1, b=4) obj.do_math(2, b=9) msg = ( "The last call to do_math was not passed args=()," " kwargs={'a': 1, 'b': 4}.\n" "\n" "It was last called with:\n" "\n" "args=()\n" "kwargs={'a': 2, 'b': 9}" ) with self._check_assertion(msg): self.assertSpyLastCalledWith(obj.do_math, a=1, b=4) with self._check_assertion(msg): self.assertSpyLastCalledWith(obj.do_math.spy, a=1, b=4) def test_assertSpyReturned_with_expected_return(self): """Testing SpyAgency.assertSpyReturned with expected return value""" obj = MathClass() self.spy_on(obj.do_math) obj.do_math(1, b=4) obj.do_math(2, b=9) # These should not fail. self.assertSpyReturned(obj.do_math, 5) self.assertSpyReturned(obj.do_math.calls[0], 5) self.assertSpyReturned(obj.do_math.spy, 11) self.assertSpyReturned(obj.do_math.spy.calls[1], 11) # Check the aliases. self.assert_spy_returned(obj.do_math, 5) kgb.asserts.assert_spy_returned(obj.do_math, 5) def test_assertSpyReturned_without_expected_return(self): """Testing SpyAgency.assertSpyReturned without expected return value""" obj = MathClass() self.spy_on(obj.do_math) obj.do_math(1, b=4) obj.do_math(2, b=9) msg = ( 'No call to do_math returned 100.\n' '\n' 'The following values have been returned:\n' '\n' 'Call 0:\n' ' 5\n' '\n' 'Call 1:\n' ' 11' ) with self._check_assertion(msg): self.assertSpyReturned(obj.do_math, 100) with self._check_assertion(msg): self.assertSpyReturned(obj.do_math.spy, 100) msg = ( 'This call to do_math did not return 100.\n' '\n' 'It returned:\n' '\n' '5' ) with self._check_assertion(msg): self.assertSpyReturned(obj.do_math.calls[0], 100) def test_assertSpyLastReturned_with_expected_return(self): """Testing SpyAgency.assertSpyLastReturned with expected return value """ obj = MathClass() self.spy_on(obj.do_math) obj.do_math(1, b=4) obj.do_math(2, b=9) # These should not fail. self.assertSpyLastReturned(obj.do_math, 11) self.assertSpyLastReturned(obj.do_math.spy, 11) # Check the aliases. self.assert_spy_last_returned(obj.do_math, 11) kgb.asserts.assert_spy_last_returned(obj.do_math, 11) def test_assertSpyLastReturned_without_expected_return(self): """Testing SpyAgency.assertSpyLastReturned without expected return value """ obj = MathClass() self.spy_on(obj.do_math) obj.do_math(1, b=4) obj.do_math(2, b=9) msg = ( 'The last call to do_math did not return 5.\n' '\n' 'It last returned:\n' '\n' '11' ) with self._check_assertion(msg): self.assertSpyLastReturned(obj.do_math, 5) with self._check_assertion(msg): self.assertSpyLastReturned(obj.do_math.spy, 5) def test_assertSpyRaised_with_expected_exception(self): """Testing SpyAgency.assertSpyRaised with expected exception raised""" def _do_math(_self, a, *args, **kwargs): if a == 1: raise KeyError elif a == 2: raise ValueError obj = MathClass() self.spy_on(obj.do_math, call_fake=_do_math) try: obj.do_math(1) except KeyError: pass try: obj.do_math(2) except ValueError: pass # These should not fail. self.assertSpyRaised(obj.do_math, KeyError) self.assertSpyRaised(obj.do_math.calls[0], KeyError) self.assertSpyRaised(obj.do_math.spy, ValueError) self.assertSpyRaised(obj.do_math.spy.calls[1], ValueError) # Check the aliases. self.assert_spy_raised(obj.do_math, KeyError) kgb.asserts.assert_spy_raised(obj.do_math, KeyError) def test_assertSpyRaised_with_expected_no_exception(self): """Testing SpyAgency.assertSpyRaised with expected completions without exceptions """ obj = MathClass() self.spy_on(obj.do_math) obj.do_math(1) obj.do_math(2) # These should not fail. self.assertSpyRaised(obj.do_math, None) self.assertSpyRaised(obj.do_math.calls[0], None) self.assertSpyRaised(obj.do_math.spy, None) self.assertSpyRaised(obj.do_math.spy.calls[1], None) def test_assertSpyRaised_without_expected_exception(self): """Testing SpyAgency.assertSpyRaised without expected exception raised """ def _do_math(_self, a, *args, **kwargs): if a == 1: raise KeyError elif a == 2: raise ValueError obj = MathClass() self.spy_on(obj.do_math, call_fake=_do_math) # First test without any exceptions raised try: obj.do_math(1) except KeyError: pass try: obj.do_math(2) except ValueError: pass msg = ( 'No call to do_math raised AttributeError.\n' '\n' 'The following exceptions have been raised:\n' '\n' 'Call 0:\n' ' KeyError\n' '\n' 'Call 1:\n' ' ValueError' ) with self._check_assertion(msg): self.assertSpyRaised(obj.do_math, AttributeError) with self._check_assertion(msg): self.assertSpyRaised(obj.do_math.spy, AttributeError) msg = ( 'This call to do_math did not raise AttributeError. It raised ' 'KeyError.' ) with self._check_assertion(msg): self.assertSpyRaised(obj.do_math.calls[0], AttributeError) def test_assertSpyRaised_without_raised(self): """Testing SpyAgency.assertSpyRaised without any exceptions raised""" obj = MathClass() self.spy_on(obj.do_math) # First test without any exceptions raised obj.do_math(1) obj.do_math(2) msg = 'No call to do_math raised an exception.' with self._check_assertion(msg): self.assertSpyRaised(obj.do_math, AttributeError) with self._check_assertion(msg): self.assertSpyRaised(obj.do_math.spy, AttributeError) msg = 'This call to do_math did not raise an exception.' with self._check_assertion(msg): self.assertSpyRaised(obj.do_math.spy.calls[0], AttributeError) def test_assertSpyLastRaised_with_expected_exception(self): """Testing SpyAgency.assertSpyLastRaised with expected exception raised """ def _do_math(_self, a, *args, **kwargs): if a == 1: raise KeyError elif a == 2: raise ValueError obj = MathClass() self.spy_on(obj.do_math, call_fake=_do_math) try: obj.do_math(1) except KeyError: pass try: obj.do_math(2) except ValueError: pass # These should not fail. self.assertSpyLastRaised(obj.do_math, ValueError) self.assertSpyLastRaised(obj.do_math.spy, ValueError) # Check the aliases. self.assert_spy_last_raised(obj.do_math, ValueError) kgb.asserts.assert_spy_last_raised(obj.do_math, ValueError) def test_assertSpyLastRaised_with_expected_no_exception(self): """Testing SpyAgency.assertSpyLastRaised with expected completion without raising """ obj = MathClass() self.spy_on(obj.do_math) obj.do_math(1) obj.do_math(2) # These should not fail. self.assertSpyLastRaised(obj.do_math, None) self.assertSpyLastRaised(obj.do_math.spy, None) def test_assertSpyLastRaised_without_expected_exception(self): """Testing SpyAgency.assertSpyLastRaised without expected exception raised """ def _do_math(_self, a, *args, **kwargs): if a == 1: raise KeyError elif a == 2: raise ValueError obj = MathClass() self.spy_on(obj.do_math, call_fake=_do_math) try: obj.do_math(1) except KeyError: pass try: obj.do_math(2) except ValueError: pass msg = ( 'The last call to do_math did not raise KeyError. It last ' 'raised ValueError.' ) with self._check_assertion(msg): self.assertSpyLastRaised(obj.do_math, KeyError) with self._check_assertion(msg): self.assertSpyLastRaised(obj.do_math.spy, KeyError) def test_assertSpyLastRaised_without_raised(self): """Testing SpyAgency.assertSpyLastRaised without exception raised""" obj = MathClass() self.spy_on(obj.do_math) obj.do_math(1) obj.do_math(2) msg = 'The last call to do_math did not raise an exception.' with self._check_assertion(msg): self.assertSpyLastRaised(obj.do_math, KeyError) with self._check_assertion(msg): self.assertSpyLastRaised(obj.do_math.spy, KeyError) def test_assertSpyRaisedMessage_with_expected(self): """Testing SpyAgency.assertSpyRaised with expected exception and message raised """ def _do_math(_self, a, *args, **kwargs): if a == 1: raise AttributeError('Bad key!') elif a == 2: raise ValueError('Bad value!') obj = MathClass() self.spy_on(obj.do_math, call_fake=_do_math) try: obj.do_math(1) except AttributeError: pass try: obj.do_math(2) except ValueError: pass # These should not fail. self.assertSpyRaisedMessage(obj.do_math, AttributeError, 'Bad key!') self.assertSpyRaisedMessage(obj.do_math.calls[0], AttributeError, 'Bad key!') self.assertSpyRaisedMessage(obj.do_math.spy, ValueError, 'Bad value!') self.assertSpyRaisedMessage(obj.do_math.spy.calls[1], ValueError, 'Bad value!') # Check the aliases. self.assert_spy_raised_message(obj.do_math, AttributeError, 'Bad key!') kgb.asserts.assert_spy_raised_message(obj.do_math, AttributeError, 'Bad key!') def test_assertSpyRaisedMessage_without_expected(self): """Testing SpyAgency.assertSpyRaisedMessage without expected exception and message raised """ def _do_math(_self, a, *args, **kwargs): if a == 1: raise AttributeError('Bad key!') elif a == 2: raise ValueError('Bad value!') obj = MathClass() self.spy_on(obj.do_math, call_fake=_do_math) try: obj.do_math(1) except AttributeError: pass try: obj.do_math(2) except ValueError: pass # Note that we may end up with different string types with different # prefixes on different versions of Python, so we need to repr these. msg = ( 'No call to do_math raised AttributeError with message %r.\n' '\n' 'The following exceptions have been raised:\n' '\n' 'Call 0:\n' ' exception=AttributeError\n' ' message=%r\n' '\n' 'Call 1:\n' ' exception=ValueError\n' ' message=%r' % ('Bad key...', str('Bad key!'), str('Bad value!')) ) with self._check_assertion(msg): self.assertSpyRaisedMessage(obj.do_math, AttributeError, 'Bad key...') with self._check_assertion(msg): self.assertSpyRaisedMessage(obj.do_math.spy, AttributeError, 'Bad key...') msg = ( 'This call to do_math did not raise AttributeError with message' ' %r.\n' '\n' 'It raised:\n' '\n' 'exception=AttributeError\n' 'message=%r' % ('Bad key...', str('Bad key!')) ) with self._check_assertion(msg): self.assertSpyRaisedMessage(obj.do_math.calls[0], AttributeError, 'Bad key...') def test_assertSpyRaisedMessage_without_raised(self): """Testing SpyAgency.assertSpyRaisedMessage without exception raised """ obj = MathClass() self.spy_on(obj.do_math) obj.do_math(1) obj.do_math(2) msg = 'No call to do_math raised an exception.' with self._check_assertion(msg): self.assertSpyRaisedMessage(obj.do_math, KeyError, '...') with self._check_assertion(msg): self.assertSpyRaisedMessage(obj.do_math.spy, KeyError, '...') def test_assertSpyLastRaisedMessage_with_expected(self): """Testing SpyAgency.assertSpyLastRaised with expected exception and message raised """ def _do_math(_self, a, *args, **kwargs): if a == 1: raise AttributeError('Bad key!') elif a == 2: raise ValueError('Bad value!') obj = MathClass() self.spy_on(obj.do_math, call_fake=_do_math) try: obj.do_math(1) except AttributeError: pass try: obj.do_math(2) except ValueError: pass # These should not fail. self.assertSpyLastRaisedMessage(obj.do_math, ValueError, 'Bad value!') self.assertSpyLastRaisedMessage(obj.do_math.spy, ValueError, 'Bad value!') # Check the aliases. self.assert_spy_last_raised_message(obj.do_math, ValueError, 'Bad value!') kgb.asserts.assert_spy_last_raised_message(obj.do_math, ValueError, 'Bad value!') def test_assertSpyLastRaisedMessage_without_expected(self): """Testing SpyAgency.assertSpyLastRaisedMessage without expected exception and message raised """ def _do_math(_self, a, *args, **kwargs): if a == 1: raise AttributeError('Bad key!') elif a == 2: raise ValueError('Bad value!') obj = MathClass() self.spy_on(obj.do_math, call_fake=_do_math) try: obj.do_math(1) except AttributeError: pass try: obj.do_math(2) except ValueError: pass # Note that we may end up with different string types with different # prefixes on different versions of Python, so we need to repr these. msg = ( 'The last call to do_math did not raise AttributeError with ' 'message %r.\n' '\n' 'It last raised:\n' '\n' 'exception=ValueError\n' 'message=%r' % ('Bad key!', str('Bad value!')) ) with self._check_assertion(msg): self.assertSpyLastRaisedMessage(obj.do_math, AttributeError, 'Bad key!') with self._check_assertion(msg): self.assertSpyLastRaisedMessage(obj.do_math.spy, AttributeError, 'Bad key!') def test_assertSpyLastRaisedMessage_without_raised(self): """Testing SpyAgency.assertSpyLastRaisedMessage without exception raised """ obj = MathClass() self.spy_on(obj.do_math) obj.do_math(1) obj.do_math(2) msg = 'The last call to do_math did not raise an exception.' with self._check_assertion(msg): self.assertSpyLastRaisedMessage(obj.do_math, KeyError, '...') with self._check_assertion(msg): self.assertSpyLastRaisedMessage(obj.do_math.spy, KeyError, '...') @contextmanager def _check_assertion(self, msg): """Check that the expected assertion and message is raised. Args: msg (unicode): The assertion message. Context: The context used to run an assertion. """ with self.assertRaises(AssertionError) as ctx: yield self.assertEqual(str(ctx.exception), msg) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1730689021.0 kgb-7.2/kgb/tests/test_spy_call.py0000644000076500000240000000711514712033775016502 0ustar00chipx86stafffrom __future__ import unicode_literals from kgb.pycompat import text_type from kgb.tests.base import MathClass, TestCase class SpyCallTests(TestCase): """Test cases for kgb.spies.SpyCall.""" def test_called_with(self): """Testing SpyCall.called_with""" obj = MathClass() self.agency.spy_on(obj.do_math_mixed) obj.do_math_mixed(1, b=2) obj.do_math_mixed(3, b=4) call = obj.do_math_mixed.calls[0] self.assertTrue(call.called_with(1, b=2)) self.assertTrue(call.called_with(a=1, b=2)) self.assertFalse(call.called_with(3, b=4)) self.assertFalse(call.called_with(1, 2)) def test_called_with_and_keyword_args(self): """Testing SpyCall.called_with and keyword arguments""" obj = MathClass() self.agency.spy_on(obj.do_math_mixed) obj.do_math_mixed(a=1, b=2) obj.do_math_mixed(a=3, b=4) call = obj.do_math_mixed.calls[0] self.assertTrue(call.called_with(1, b=2)) self.assertTrue(call.called_with(a=1, b=2)) self.assertFalse(call.called_with(1, 2)) self.assertFalse(call.called_with(3, b=4)) def test_called_with_and_partial_args(self): """Testing SpyCall.called_with and partial arguments""" obj = MathClass() self.agency.spy_on(obj.do_math_mixed) obj.do_math_mixed(1, 2) obj.do_math_mixed(3, 4) call = obj.do_math_mixed.calls[0] self.assertTrue(call.called_with(1)) self.assertFalse(call.called_with(1, 2, 3)) self.assertFalse(call.called_with(3)) def test_called_with_and_partial_kwargs(self): """Testing SpyCall.called_with and partial keyword arguments""" obj = MathClass() self.agency.spy_on(obj.do_math_mixed) obj.do_math_mixed(a=1, b=2) obj.do_math_mixed(a=3, b=4) call = obj.do_math_mixed.calls[0] self.assertTrue(call.called_with(1)) self.assertTrue(call.called_with(b=2)) self.assertTrue(call.called_with(a=1)) self.assertFalse(call.called_with(a=4)) self.assertFalse(call.called_with(a=1, b=2, c=3)) self.assertFalse(call.called_with(a=3, b=2)) def test_returned(self): """Testing SpyCall.returned""" obj = MathClass() self.agency.spy_on(obj.do_math_mixed) obj.do_math_mixed(1, 2) obj.do_math_mixed(3, 4) call = obj.do_math_mixed.calls[0] self.assertTrue(call.returned(3)) self.assertFalse(call.returned(7)) self.assertFalse(call.returned(None)) def test_raised(self): """Testing SpyCall.raised""" obj = MathClass() self.agency.spy_on(obj.do_math) with self.assertRaises(TypeError): obj.do_math(1, 'a') call = obj.do_math.calls[0] self.assertTrue(call.raised(TypeError)) self.assertFalse(call.raised(ValueError)) self.assertFalse(call.raised(None)) def test_raised_with_message(self): """Testing SpyCall.raised_with_message""" obj = MathClass() self.agency.spy_on(obj.do_math) with self.assertRaises(TypeError): obj.do_math(1, 'a') call = obj.do_math.calls[0] self.assertTrue(call.raised_with_message( TypeError, "unsupported operand type(s) for +: 'int' and '%s'" % text_type.__name__)) self.assertFalse(call.raised_with_message( ValueError, "unsupported operand type(s) for +: 'int' and '%s'" % text_type.__name__)) self.assertFalse(call.raised_with_message(TypeError, None)) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1730689021.0 kgb-7.2/kgb/utils.py0000644000076500000240000000512414712033775013631 0ustar00chipx86staff"""Common utility functions used by kgb.""" from __future__ import unicode_literals import inspect from unittest.util import safe_repr from kgb.pycompat import iteritems def get_defined_attr_value(owner, name, ancestors_only=False): """Return a value as defined in a class, instance, or ancestor. This will look for the real definition, and not the definition returned when accessing the attribute or instantiating a class. This will bypass any descriptors and return the actual definition from the instance or class that defines it. Args: owner (type or object): The owner of the attribute. name (unicode): The name of the attribute. ancestors_only (bool, optional): Whether to only look in ancestors of ``owner``, and not in ``owner`` itself. Returns: object: The attribute value. Raises: AttributeError: The attribute could not be found. """ if not ancestors_only: d = owner.__dict__ if name in d: return d[name] if not inspect.isclass(owner): # NOTE: It's important we use __class__ and not type(). They are not # synonymous. The latter will not return the class for an # instance for old-style classes. return get_defined_attr_value(owner.__class__, name) for parent_cls in owner.__bases__: try: return get_defined_attr_value(parent_cls, name) except AttributeError: pass raise AttributeError def is_attr_defined_on_ancestor(cls, name): """Return whether an attribute is defined on an ancestor of a class. Args: name (unicode): The name of the attribute. Returns: bool: ``True`` if an ancestor defined the attribute. ``False`` if it did not. """ try: get_defined_attr_value(cls, name, ancestors_only=True) return True except AttributeError: return False def format_spy_kwargs(kwargs): """Format keyword arguments. This will convert all keys to native strings, to help with showing more reasonable output that's consistent. The keys will also be provided in sorted order. Args: kwargs (dict): The dictionary of keyword arguments. Returns: unicode: The formatted string representation. """ return '{%s}' % ', '.join( '%s: %s' % (safe_repr(str(key)), safe_repr(value)) for key, value in sorted(iteritems(kwargs), key=lambda pair: pair[0]) ) ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1730689026.544933 kgb-7.2/kgb.egg-info/0000755000076500000240000000000014712034003013570 5ustar00chipx86staff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1730689026.0 kgb-7.2/kgb.egg-info/PKG-INFO0000644000076500000240000005106714712034002014675 0ustar00chipx86staffMetadata-Version: 2.1 Name: kgb Version: 7.2 Summary: Utilities for spying on function calls in unit tests. Author-email: "Beanbag, Inc." License: MIT Project-URL: Homepage, https://github.com/beanbaginc/kgb Project-URL: Documentation, https://github.com/beanbaginc/kgb Project-URL: Repository, https://github.com/beanbaginc/kgb Keywords: pytest,unit tests,spies Classifier: Development Status :: 5 - Production/Stable Classifier: Environment :: Other Environment Classifier: Framework :: Pytest Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Programming Language :: Python :: 3.13 Classifier: Topic :: Software Development Classifier: Topic :: Software Development :: Libraries :: Python Modules Classifier: Topic :: Software Development :: Testing Requires-Python: !=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7 Description-Content-Type: text/x-rst License-File: LICENSE License-File: AUTHORS =============================== kgb - Function spies for Python =============================== Ever deal with a large test suite before, monkey patching functions to figure out whether it was called as expected? It's a dirty job. If you're not careful, you can make a mess of things. Leave behind evidence. kgb's spies will take care of that little problem for you. What are spies? =============== Spies intercept and record calls to functions. They can report on how many times a function was called and with what arguments. They can allow the function call to go through as normal, to block it, or to reroute it to another function. Spies are awesome. (If you've used Jasmine_, you know this.) Spies are like mocks, but better. You're not mocking the world. You're replacing very specific function logic, or listening to functions without altering them. (See the FAQ below.) .. _Jasmine: https://jasmine.github.io/ What test platforms are supported? ================================== Anything Python-based: * unittest_ * pytest_ * nose_ * nose2_ You can even use it outside of unit tests as part of your application. If you really want to. (Probably don't do that.) .. _unittest: https://docs.python.org/3/library/unittest.html .. _pytest: https://pytest.org .. _nose: https://nose.readthedocs.io/en/latest/ .. _nose2: https://docs.nose2.io/en/latest/ Where is kgb used? ================== * `liveswot-api `_ -- REST API Backend for liveswot * `phabricator-emails `_ -- Mozilla's utilities for converting Phabricator feeds to e-mails * `projector `_ -- Takes the overhead out of managing repositories and development environments * `ynab-sdk-python `_ -- Python implementation of the YNAB API Plus our own products: * `Django Evolution `_ -- An alternative approach to Django database migrations * `Djblets `_ -- An assortment of utilities and add-ons for managing large Django projects * `Review Board `_ -- Our open source, extensible code review product * `RBCommons `_ -- Our hosted code review service * `RBTools `_ -- Command line tools for Review Board * `Power Pack `_ -- Document review, reports, and enterprise SCM integrations for Review Board * `Review Bot `_ -- Automated code review add-on for Review Board If you use kgb, let us know and we'll add you! Installing kgb ============== Before you can use kgb, you need to install it. You can do this by typing:: $ pip install kgb kgb supports Python 2.7 and 3.6 through 3.11, both CPython and PyPy. Spying for fun and profit ========================= Spying is really easy. There are four main ways to initiate a spy. 1. Creating a SpyAgency ----------------------- A SpyAgency manages all your spies. You can create as many or as few as you want. Generally, you'll create one per unit test run. Then you'll call ``spy_on()``, passing in the function you want. .. code-block:: python from kgb import SpyAgency def test_mind_control_device(): mcd = MindControlDevice() agency = SpyAgency() agency.spy_on(mcd.assassinate, call_fake=give_hugs) 2. Mixing a SpyAgency into your tests ------------------------------------- A ``SpyAgency`` can be mixed into your unittest_-based test suite, making it super easy to spy all over the place, discretely, without resorting to a separate agency. (We call this the "inside job.") .. code-block:: python from kgb import SpyAgency # Using Python's unittest: class TopSecretTests(SpyAgency, unittest.TestCase): def test_weather_control(self): weather = WeatherControlDevice() self.spy_on(weather.start_raining) # Using pytest with the "spy_agency" fixture (kgb 7+): def test_weather_control(spy_agency): weather = WeatherControlDevice() spy_agency.spy_on(weather.start_raining) 3. Using a decorator -------------------- If you're creating a spy that calls a fake function, you can simplify some things by using the ``spy_for`` decorator: .. code-block:: python from kgb import SpyAgency # Using Python's unittest: class TopSecretTests(SpyAgency, unittest.TestCase): def test_doomsday_device(self): dd = DoomsdayDevice() @self.spy_for(dd.kaboom) def _save_world(*args, **kwargs) print('Sprinkles and ponies!') # Give it your best shot, doomsday device. dd.kaboom() # Using pytest: def test_doomsday_device(spy_agency): dd = DoomsdayDevice() @spy_agency.spy_for(dd.kaboom) def _save_world(*args, **kwargs) print('Sprinkles and ponies!') # Give it your best shot, doomsday device. dd.kaboom() 4. Using a context manager -------------------------- If you just want a spy for a quick job, without all that hassle of a full agency, just use the ``spy_on`` context manager, like so: .. code-block:: python from kgb import spy_on def test_the_bomb(self): bomb = Bomb() with spy_on(bomb.explode, call_original=False): # This won't explode. Phew. bomb.explode() A spy's abilities ================= A spy can do many things. The first thing you need to do is figure out how you want to use the spy. Creating a spy that calls the original function ----------------------------------------------- .. code-block:: python spy_agency.spy_on(obj.function) When your spy is called, the original function will be called as well. It won't even know you were there. Creating a spy that blocks the function call -------------------------------------------- .. code-block:: python spy_agency.spy_on(obj.function, call_original=False) Useful if you want to know that a function was called, but don't want the original function to actually get the call. Creating a spy that reroutes to a fake function ----------------------------------------------- .. code-block:: python def _my_fake_function(some_param, *args, **kwargs): ... spy_agency.spy_on(obj.function, call_fake=my_fake_function) # Or, in kgb 6+ @spy_agency.spy_for(obj.function) def _my_fake_function(some_param, *args, **kwargs): ... Fake the return values or operations without anybody knowing. Stopping a spy operation ------------------------ .. code-block:: python obj.function.unspy() Do your job and get out. Check the call history ---------------------- .. code-block:: python for call in obj.function.calls: print(calls.args, calls.kwargs) See how many times your spy's intercepted a function call, and what was passed. Check a specific call --------------------- .. code-block:: python # Check the latest call... print(obj.function.last_call.args) print(obj.function.last_call.kwargs) print(obj.function.last_call.return_value) print(obj.function.last_call.exception) # For an older call... print(obj.function.calls[0].args) print(obj.function.calls[0].kwargs) print(obj.function.calls[0].return_value) print(obj.function.calls[0].exception) Also a good way of knowing whether it's even been called. ``last_call`` will be ``None`` if nobody's called yet. Check if the function was ever called ------------------------------------- Mixing in ``SpyAgency`` into a unittest_-based test suite: .. code-block:: python # Either one of these is fine. self.assertSpyCalled(obj.function) self.assertTrue(obj.function.called) # Or the inverse: self.assertSpyNotCalled(obj.function) self.assertFalse(obj.function.called) Or using the pytest_ ``spy_agency`` fixture on kgb 7+: .. code-block:: python spy_agency.assert_spy_called(obj.function) spy_agency.assert_spy_not_called(obj.function) Or using standalone assertion methods on kgb 7+: .. code-block:: python from kgb.asserts import (assert_spy_called, assert_spy_not_called) assert_spy_called(obj.function) assert_spy_not_called(obj.function) If the function was ever called at all, this will let you know. Check if the function was ever called with certain arguments ------------------------------------------------------------ Mixing in ``SpyAgency`` into a unittest_-based test suite: .. code-block:: python # Check if it was ever called with these arguments... self.assertSpyCalledWith(obj.function, 'foo', bar='baz') self.assertTrue(obj.function.called_with('foo', bar='baz')) # Check a specific call... self.assertSpyCalledWith(obj.function.calls[0], 'foo', bar='baz') self.assertTrue(obj.function.calls[0].called_with('foo', bar='baz')) # Check the last call... self.assertSpyLastCalledWith(obj.function, 'foo', bar='baz') self.assertTrue(obj.function.last_called_with('foo', bar='baz')) # Or the inverse: self.assertSpyNotCalledWith(obj.function, 'foo', bar='baz') self.assertFalse(obj.function.called) Or using the pytest_ ``spy_agency`` fixture on kgb 7+: .. code-block:: python spy_agency.assert_spy_called_with(obj.function, 'foo', bar='baz') spy_agency.assert_spy_last_called_with(obj.function, 'foo', bar='baz') spy_agency.assert_spy_not_called_with(obj.function, 'foo', bar='baz') Or using standalone assertion methods on kgb 7+: .. code-block:: python from kgb.asserts import (assert_spy_called_with, assert_spy_last_called_with, assert_spy_not_called_with) assert_spy_called_with(obj.function, 'foo', bar='baz') assert_spy_last_called_with(obj.function, 'foo', bar='baz') assert_spy_not_called_with(obj.function, 'foo', bar='baz') The whole callkhistory will be searched. You can provide the entirety of the arguments passed to the function, or you can provide a subset. You can pass positional arguments as-is, or pass them by name using keyword arguments. Recorded calls always follow the function's original signature, so even if a keyword argument was passed a positional value, it will be recorded as a keyword argument. Check if the function ever returned a certain value --------------------------------------------------- Mixing in ``SpyAgency`` into a unittest_-based test suite: .. code-block:: python # Check if the function ever returned a certain value... self.assertSpyReturned(obj.function, 42) self.assertTrue(obj.function.returned(42)) # Check a specific call... self.assertSpyReturned(obj.function.calls[0], 42) self.assertTrue(obj.function.calls[0].returned(42)) # Check the last call... self.assertSpyLastReturned(obj.function, 42) self.assertTrue(obj.function.last_returned(42)) Or using the pytest_ ``spy_agency`` fixture on kgb 7+: .. code-block:: python spy_agency.assert_spy_returned(obj.function, 42) spy_agency.assert_spy_returned(obj.function.calls[0], 42) spy_agency.assert_spy_last_returned(obj.function, 42) Or using standalone assertion methods on kgb 7+: .. code-block:: python from kgb.asserts import (assert_spy_last_returned, assert_spy_returned) assert_spy_returned(obj.function, 42) assert_spy_returned(obj.function.calls[0], 42) assert_spy_last_returned(obj.function, 42) Handy for checking if some function ever returned what you expected it to, when you're not calling that function yourself. Check if a function ever raised a certain type of exception ----------------------------------------------------------- Mixing in ``SpyAgency`` into a unittest_-based test suite: .. code-block:: python # Check if the function ever raised a certain exception... self.assertSpyRaised(obj.function, TypeError) self.assertTrue(obj.function.raised(TypeError)) # Check a specific call... self.assertSpyRaised(obj.function.calls[0], TypeError) self.assertTrue(obj.function.calls[0].raised(TypeError)) # Check the last call... self.assertSpyLastRaised(obj.function, TypeError) self.assertTrue(obj.function.last_raised(TypeError)) Or using the pytest_ ``spy_agency`` fixture on kgb 7+: .. code-block:: python spy_agency.assert_spy_raised(obj.function, TypeError) spy_agency.assert_spy_raised(obj.function.calls[0], TypeError) spy_agency.assert_spy_last_raised(obj.function, TypeError) Or using standalone assertion methods on kgb 7+: .. code-block:: python from kgb.asserts import (assert_spy_last_raised, assert_spy_raised) assert_spy_raised(obj.function, TypeError) assert_spy_raised(obj.function.calls[0], TypeError) assert_spy_last_raised(obj.function, TypeError) You can also go a step further by checking the exception's message. .. code-block:: python # Check if the function ever raised an exception with a given message... self.assertSpyRaisedWithMessage( obj.function, TypeError, "'type' object is not iterable") self.assertTrue(obj.function.raised_with_message( TypeError, "'type' object is not iterable")) # Check a specific call... self.assertSpyRaisedWithMessage( obj.function.calls[0], TypeError, "'type' object is not iterable") self.assertTrue(obj.function.calls[0].raised_with_message( TypeError, "'type' object is not iterable")) # Check the last call... self.assertSpyLastRaisedWithMessage( obj.function, TypeError, "'type' object is not iterable") self.assertTrue(obj.function.last_raised_with_message( TypeError, "'type' object is not iterable")) Reset all the calls ------------------- .. code-block:: python obj.function.reset_calls() Wipe away the call history. Nobody will know. Call the original function -------------------------- .. code-block:: python result = obj.function.call_original('foo', bar='baz') Super, super useful if you want to use ``call_fake=`` or ``@spy_agency.spy_for`` to wrap a function and track or influence some part of it, but still want the original function to do its thing. For instance: .. code-block:: python stored_results = [] @spy_agency.spy_for(obj.function) def my_fake_function(*args, **kwargs): kwargs['bar'] = 'baz' result = obj.function.call_original(*args, **kwargs) stored_results.append(result) return result Plan a spy operation ==================== Why start from scratch when setting up a spy? Let's plan an operation. (Spy operations are only available in kgb 6 or higher.) Raise an exception when called ------------------------------ .. code-block:: python spy_on(pen.emit_poison, op=kgb.SpyOpRaise(PoisonEmptyError())) Or go nuts, have a different exception for each call (in kgb 6.1+): .. code-block:: python spy_on(pen.emit_poison, op=kgb.SpyOpRaiseInOrder([ PoisonEmptyError(), Kaboom(), MissingPenError(), ])) Or return a value ----------------- .. code-block:: python spy_on(our_agent.get_identity, op=kgb.SpyOpReturn('nobody...')) Maybe a different value for each call (in kgb 6.1+)? .. code-block:: python spy_on(our_agent.get_identity, op=kgb.SpyOpReturnInOrder([ 'nobody...', 'who?', 'not telling...', ])) Now for something more complicated. Handle a call based on the arguments used ----------------------------------------- If you're dealing with many calls to the same function, you may want to return different values or only call the original function depending on which arguments were passed in the call. That can be done with a ``SpyOpMatchAny`` operation. .. code-block:: python spy_on(traps.trigger, op=kgb.SpyOpMatchAny([ { 'args': ('hallway_lasers',), 'call_fake': _send_wolves, }, { 'args': ('trap_tile',), 'op': SpyOpMatchInOrder([ { 'call_fake': _spill_hot_oil, }, { 'call_fake': _drop_torch, }, ]), }, { 'args': ('infrared_camera',), 'kwargs': { 'sector': 'underground_passage', }, 'call_original': False, }, ])) Any unexpected calls will automatically assert. Or require those calls in a specific order ------------------------------------------ You can combine that with requiring the calls to be in the order you want using ``SpyOpMatchInOrder``. .. code-block:: python spy_on(lockbox.enter_code, op=kgb.SpyOpMatchInOrder([ { 'args': (1, 2, 3, 4, 5, 6), 'call_original': False, }, { 'args': (9, 0, 2, 1, 0, 0), 'call_fake': _start_countdown, }, { 'args': (42, 42, 42, 42, 42, 42), 'op': kgb.SpyOpRaise(Kaboom()), 'call_original': True, }, { 'args': (4, 8, 15, 16, 23, 42), 'kwargs': { 'secret_button_pushed': True, }, 'call_original': True, } ])) FAQ === Doesn't this just do what mock does? ------------------------------------ kgb's spies and mock_'s patching are very different from each other. When patching using mock, you're simply replacing a method on a class with something that looks like a method, and that works great except you're limited to methods on classes. You can't override a top-level function, like ``urllib2.urlopen``. kgb spies leave the function or method where it is. What it *does* do is replace the *bytecode* of the function, intercepting calls on a very low level, recording everything about it, and then passing on the call to the original function or your replacement function. It's pretty powerful, and allows you to listen to or override calls you normally would have no control over. .. _mock: https://pypi.python.org/pypi/mock What?! There's no way that's stable. ------------------------------------ It is! It really is! We've been using it for years across a wide variety of codebases. It's pretty amazing. Python actually allows this. We're not scanning your RAM and doing terrible things with it, or something like that. Every function or method in Python has a ``func_code`` (Python 2) or ``__code__`` (Python 3) attribute, which is mutable. We can go in and replace the bytecode with something compatible with the original function. How we actually do that, well, that's complicated, and you may not want to know. Does this work with PyPy? ------------------------- I'm going to level with you, I was going to say "hell no!", and then decided to give it a try. Hell yes! (But only accidentally. YMMV... We'll try to officially support this later.) What else do you build? ----------------------- Lots of things. Check out some of our other `open source projects`_. .. _open source projects: https://www.beanbaginc.com/opensource/ ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1730689026.0 kgb-7.2/kgb.egg-info/SOURCES.txt0000644000076500000240000000133414712034002015454 0ustar00chipx86staffAUTHORS LICENSE MANIFEST.in NEWS.rst README.rst conftest.py dev-requirements.txt pyproject.toml setup.cfg tox.ini kgb/__init__.py kgb/agency.py kgb/asserts.py kgb/calls.py kgb/contextmanagers.py kgb/errors.py kgb/ops.py kgb/pycompat.py kgb/pytest_plugin.py kgb/signature.py kgb/spies.py kgb/utils.py kgb.egg-info/PKG-INFO kgb.egg-info/SOURCES.txt kgb.egg-info/dependency_links.txt kgb.egg-info/entry_points.txt kgb.egg-info/top_level.txt kgb/tests/__init__.py kgb/tests/base.py kgb/tests/test_context_managers.py kgb/tests/test_function_spy.py kgb/tests/test_ops.py kgb/tests/test_pytest_plugin.py kgb/tests/test_spy_agency.py kgb/tests/test_spy_call.py kgb/tests/py3/__init__.py kgb/tests/py3/test_function_spy.py tests/runtests.py././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1730689026.0 kgb-7.2/kgb.egg-info/dependency_links.txt0000644000076500000240000000000114712034002017635 0ustar00chipx86staff ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1730689026.0 kgb-7.2/kgb.egg-info/entry_points.txt0000644000076500000240000000004314712034002017062 0ustar00chipx86staff[pytest11] kgb = kgb.pytest_plugin ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1730689026.0 kgb-7.2/kgb.egg-info/top_level.txt0000644000076500000240000000000414712034002016313 0ustar00chipx86staffkgb ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1730689021.0 kgb-7.2/pyproject.toml0000644000076500000240000000341014712033775014264 0ustar00chipx86staff[build-system] requires = ['setuptools'] build-backend = 'setuptools.build_meta' [project] name = 'kgb' description = 'Utilities for spying on function calls in unit tests.' authors = [ {name = 'Beanbag, Inc.', email = 'questions@beanbaginc.com'}, ] license = { text = 'MIT' } readme = 'README.rst' requires-python = '>=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*' dynamic = ['version'] keywords = [ 'pytest', 'unit tests', 'spies', ] classifiers = [ 'Development Status :: 5 - Production/Stable', 'Environment :: Other Environment', 'Framework :: Pytest', 'Intended Audience :: Developers', 'License :: OSI Approved :: MIT License', 'Operating System :: OS Independent', 'Programming Language :: Python', 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.12', 'Programming Language :: Python :: 3.13', 'Topic :: Software Development', 'Topic :: Software Development :: Libraries :: Python Modules', 'Topic :: Software Development :: Testing', ] [project.urls] Homepage = 'https://github.com/beanbaginc/kgb' Documentation = 'https://github.com/beanbaginc/kgb' Repository = 'https://github.com/beanbaginc/kgb' [project.entry-points."pytest11"] kgb = 'kgb.pytest_plugin' [tool.setuptools.dynamic] version = { attr = 'kgb.get_package_version' } [tool.setuptools.packages.find] where = ['.'] include = ['kgb*'] namespaces = false ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1730689026.5454812 kgb-7.2/setup.cfg0000644000076500000240000000026714712034003013161 0ustar00chipx86staff[bdist_wheel] universal = 1 [egg_info] tag_build = tag_date = 0 [flake8] ignore = E121,E125,E129,E241,W504 [aliases] release = egg_info -DRb '' [tool:pytest] addopts = --pyargs ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1730689026.5447528 kgb-7.2/tests/0000755000076500000240000000000014712034003012475 5ustar00chipx86staff././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1730689021.0 kgb-7.2/tests/runtests.py0000755000076500000240000000047414712033775014765 0ustar00chipx86staff#!/usr/bin/env python from __future__ import print_function, unicode_literals import os import sys import pytest if __name__ == '__main__': print('This is deprecated! Please run pytest instead.') print() os.chdir(os.path.join(os.path.dirname(__file__), '..')) sys.exit(pytest.main(sys.argv[1:])) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1730689021.0 kgb-7.2/tox.ini0000644000076500000240000000025114712033775012663 0ustar00chipx86staff[tox] envlist = py{27,36,37,38,39,310,311,312,313},pypy{37,38} skipsdist = True [testenv] commands = pytest {posargs} deps = -r dev-requirements.txt usedevelop = True