Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

PEP 553 built-in debug() function (bpo-31353) #3355

Merged
merged 45 commits into from Oct 5, 2017
Merged

Conversation

warsaw
Copy link
Member

@warsaw warsaw commented Sep 5, 2017

@warsaw warsaw changed the title PEP 553 built-in debug() function PEP 553 built-in debug() function (bpo-31353) Sep 5, 2017
@warsaw
Copy link
Member Author

warsaw commented Sep 5, 2017

@@ -1951,6 +1978,9 @@ _PySys_BeginInit(void)
PyDict_GetItemString(sysdict, "displayhook"));
SET_SYS_FROM_STRING_BORROW("__excepthook__",
PyDict_GetItemString(sysdict, "excepthook"));
SET_SYS_FROM_STRING_BORROW(
Copy link
Member

Choose a reason for hiding this comment

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

Your indentation here is different from what's already present here. While I personally prefer how you've done it, local inconsistency is ugly.

Copy link
Member Author

Choose a reason for hiding this comment

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

True, but unfortunately that's the only way to not break 80 columns (or maybe "the least horrible way").

Copy link
Member

Choose a reason for hiding this comment

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

Changing the indentation of the other entries should not be discarded as an option if that allows PEP 7 conformance.

Not ideal, perhaps, but allows local consistency.

Copy link
Member Author

Choose a reason for hiding this comment

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

I'm not sure I want to touch lines outside those related to this PEP.

Copy link
Member

Choose a reason for hiding this comment

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

In the interest of local consistency, that could be done after PEP code is landed (assuming acceptance, which sounds likely), as a separate commit. Otherwise, some way to conform would be valuable.

Copy link
Member

@vstinner vstinner left a comment

Choose a reason for hiding this comment

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

You requested my review. Here you have, enjoy! :-)

My main question is if PYTHONBREAKPOINT should be evaluated at each sys.breakpoint() call, or if it could be read only once at Python startup?

The PEP explicitly says "PYTHONBREAKPOINT is re-interpreted every time sys.breakpointhook() is reached.". That's surprising to me, but I don't really care and the PEP is already accepted. So please ignore my comments related to this ;-)

resulting module must have a callable named ``function()``. This is run,
passing in ``*args`` and ``**kws``, and whatever ``function()`` returns,
``sys.breakpointhook()`` returns to the built-in :func:`breakpoint`
function.
Copy link
Member

Choose a reason for hiding this comment

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

PYTHONBREAKPOINT should be written:

:envvar:`PYTHONBREAKPOINT`

(as you did in the What's New in Python 3.7) and you should document the variable in Doc/using/cmdline.rst.

Copy link
Member Author

Choose a reason for hiding this comment

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

Thanks, fixed!


class TestBreakpoint(unittest.TestCase):
def test_breakpoint(self):
with patch('pdb.set_trace') as mock:
Copy link
Member

Choose a reason for hiding this comment

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

Hum, would it be possible to modify the unit test so it doesn't rely on breakpoint() calling pdb.set_trace()? The test may be run with PYTHONBREAKPOINT set to something else.. to debug the test (or another test while running the test suite). Or skip the test if sys.breakpointhook is no sys.breakpointhook?

Copy link
Member Author

Choose a reason for hiding this comment

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

See above about the clean slate, but in this case we're testing the default behavior, which is exactly that the clean slate calls pdb.set_trace().

call_status = 'Not called'
def my_breakpointhook():
nonlocal call_status
call_status = 'Called'
Copy link
Member

Choose a reason for hiding this comment

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

You might use a mock to get arguments: assert_called_once_with(...).

Copy link
Member Author

Choose a reason for hiding this comment

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

I'm not quite sure what you're suggesting here. There are other tests to ensure that the right positional and keyword arguments are passed through. This test just ensures that a custom sys.breakpointhook gets called at all. (I like focused unit tests.)

Copy link
Member

@merwok merwok Oct 4, 2017

Choose a reason for hiding this comment

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

I think haypo is suggesting code like:

myhook = mock.MagicMock()
sys.breakpointhook = myhook

breakpoint('hello')

myhook.assert_called_once_with('hello')

instead of manual tracking of the call status.

Copy link
Member Author

Choose a reason for hiding this comment

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

@merwok ah, thanks! I like that.

sys.breakpointhook = my_breakpointhook
breakpoint()
finally:
sys.breakpointhook = sys.__breakpointhook__
Copy link
Member

Choose a reason for hiding this comment

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

I suggest to use support.swap_attr(sys, "breakpointhook", ...): it's shorter and restore the variable to its previous value. Again, the test runner might have replaced sys.breakpointhook for good reasons. Don't reset it to sys.breakpointhook.

Copy link
Member Author

Choose a reason for hiding this comment

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

Excellent suggestion. I'm going to use this as mentioned above.

self.assertEqual(call_status, 'Called')
with patch('pdb.set_trace') as mock:
breakpoint()
mock.assert_called_once()
Copy link
Member

Choose a reason for hiding this comment

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

Would it be possible to restore sys.breakpointhook to its original value at the end of the test?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yep, see above.

@@ -96,6 +96,80 @@ PySys_SetObject(const char *name, PyObject *v)
return PyDict_SetItemString(sd, name, v);
}

static PyObject *
sys_breakpointhook(PyObject *self, PyObject *args, PyObject *keywords)
{
Copy link
Member

Choose a reason for hiding this comment

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

Add "assert(!PyErr_Occurred());" at the entry point since this function is called when something goes wrong, and the implementation don't expect that an exception is currently set.

... Maybe even raise an exception at runtime if it's called with an exception set? See _Py_CheckFunctionResult() which implements a similar check at exit point.

Copy link
Member Author

Choose a reason for hiding this comment

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

Is that true, i.e. that it's possible to call this with an exception set? I don't mind adding an assertion for that, but I'm not sure I see how it's possible. builtin_breakpoint() won't call it with an exception set AFAICT. What scenario are you thinking about?

Copy link
Member

Choose a reason for hiding this comment

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

Is that true, i.e. that it's possible to call this with an exception set?

In my experience, it's very common that a function returning a result and set an exception is not catched immediately, but somewhere "later". I had such issues when I ran my pyfailmalloc tool which injects MemoryError anywhere.

But I made many changes in CPython internals to add assert(!PyErr_Occurred()) at many places. The main change is that all functions calling functions should now check that no exception is raised if the called function returned a result. This is _Py_CheckFunctionResult().

To come back to your breakpoint() implementation, "assert(!PyErr_Occurred());" should be enough.

Copy link
Member Author

Choose a reason for hiding this comment

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

Just to be clear, you're suggesting assert(!PyErr_Occurred()); at the beginning of sys_breakpointhook() right? I'm cool with that.

Copy link
Member

Choose a reason for hiding this comment

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

Yes.

PyErr_Clear();
int status = PyErr_WarnFormat(
PyExc_RuntimeWarning, 0,
"Ignoring unimportable $PYTHONBREAKPOINT: \"%s\"", envar);
Copy link
Member

Choose a reason for hiding this comment

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

I suggest to not dump the variable value to avoid encoding/escaping issues (%s doesn't escape quotes). It's up to you.

Copy link
Member Author

Choose a reason for hiding this comment

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

For now I think it's useful to include the value. Let's see if it's a problem in practice.

int status = PyErr_WarnFormat(
PyExc_RuntimeWarning, 0,
"Ignoring unimportable $PYTHONBREAKPOINT: \"%s\"", envar);
if (status == 0) {
Copy link
Member

Choose a reason for hiding this comment

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

I would prefer to handle the error first "if (status < 0) { return NULL; }", and use "Py_RETURN_NONE" for the regular case. (Except true and false branches.)

}

PyDoc_STRVAR(breakpointhook_doc,
"breakpointhook()\n"
Copy link
Member

Choose a reason for hiding this comment

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

It's unclear to me if the prototype is breakpointhook() or breakpointhook(*args, **kw)?

PyDoc_STRVAR(breakpointhook_doc,
"breakpointhook()\n"
"\n"
"Called when the built-in breakpoint() function is called.\n"
Copy link
Member

Choose a reason for hiding this comment

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

I suggest to repeat the first sentence of its doc:

This hook function is called by built-in breakpoint().

@bedevere-bot
Copy link

A Python core developer has requested some changes be made to your pull request before we can consider merging it. If you could please address their requests along with any other requests in other reviews from core developers that would be appreciated.

Once you have made the requested changes, please leave a comment on this pull request containing the phrase I didn't expect the Spanish Inquisition!. I will then notify any core developers who have left a review that you're ready for them to take another look at this pull request.

The biggest change is to use a setUp() that ensures a clean slate for the
tests, regardless of whether sys.breakpointhook or PYTHONBREAKPOINT is set.
* Improve the readability of the docstring.
* Add an assertion.
* Use Py_GETENV to obey -E
@warsaw
Copy link
Member Author

warsaw commented Oct 5, 2017

@Haypo @merwok Thanks for the great comments. I think all issues are address, save the behavior of PYTHONBREAKPOINT under -E. Let's see how the python-dev discussion shakes out.

@vstinner
Copy link
Member

vstinner commented Oct 5, 2017

There is still a bug somewhere in tests:

haypo@selma$ PYTHONBREAKPOINT=0 ./python -E -m test test_builtin
Run tests sequentially
0:00:00 load avg: 1.46 [1/1] test_builtin
> /home/haypo/prog/python/master/Lib/test/test_builtin.py(1581)test_envar_good_path_other()
-> mock.assert_called_once_with()
(Pdb) 

@vstinner
Copy link
Member

vstinner commented Oct 5, 2017

I ran "./python -m test -R 3:3 test_builtins" and there is no reference leak.

Note: I had to disable two unrelated unit tests because of https://bugs.python.org/issue31703

Copy link
Member

@vstinner vstinner left a comment

Choose a reason for hiding this comment

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

LGTM, but there is a minor issue in tests (see my previous comment) and a missing "verisonadded" in the doc. Moreover, tests are failing, so I cannot approve the change yet.

called by built-in :func:`breakpoint`. If not set, or set to the empty
string, it is equivalent to the value "pdb.set_trace". Setting this to the
string "0" causes the default implementation of :func:`sys.breakpointhook`
to do nothing but return immediately.
Copy link
Member

Choose a reason for hiding this comment

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

".. versionadded:: 3.7" is missing here.

Copy link
Member Author

Choose a reason for hiding this comment

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

Fixed

@warsaw
Copy link
Member Author

warsaw commented Oct 5, 2017

@Haypo Yeah, a bunch of tests have to be skipped when -E is given! I don't think there's any way to run them in that case, since Py_IgnoreEnvironmentFlag is not settable or mockable from Python.

@ncoghlan
Copy link
Contributor

ncoghlan commented Oct 5, 2017

@warsaw There are some other tests that are run in subprocesses specifically to cope with the main test runner being executed with -E set. It's less convenient for coverage collection, but avoids the need to skip the tests when -E is set.

@warsaw
Copy link
Member Author

warsaw commented Oct 5, 2017

I ran "./python -m test -R 3:3 test_builtins" and there is no reference leak.

I couldn't get that to do anything other than just (seemingly) hang on macOS. :(

% ./python.exe -m test -R 3:3 test_builtin
./python.exe -m test -R 3:3 test_builtin
Run tests sequentially
0:00:00 load avg: 2.25 [1/1] test_builtin
beginning 6 repetitions
123456
  C-c C-c^C
Test suite interrupted by signal SIGINT.
1 test omitted:
    test_builtin

Total duration: 49 min 14 sec
Tests result: INTERRUPTED

Maybe that's caused by the bug you referenced?

@warsaw
Copy link
Member Author

warsaw commented Oct 5, 2017

@ncoghlan

There are some other tests that are run in subprocesses specifically to cope with the main test runner being executed with -E set. It's less convenient for coverage collection, but avoids the need to skip the tests when -E is set.

I think you're talking about test_cmd_line.py's use of interpreter_requires_environment()? I'm not sure this is a big enough win for the complexity. I'm fine skipping the tests since they really make no sense under -E.

@warsaw
Copy link
Member Author

warsaw commented Oct 5, 2017

@Haypo I think we're good to go. Do you agree?

@vstinner
Copy link
Member

vstinner commented Oct 5, 2017

Yeah, LGTM. We can still enhance tests later if needed (run tests which depends on -E in subprocesses).

@warsaw warsaw merged commit 36c1d1f into python:master Oct 5, 2017
@warsaw warsaw deleted the debughook branch October 5, 2017 16:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

7 participants