Skip to content

gh-131927: Prevent emitting optimizer warnings twice in the REPL #131993

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

Merged
merged 12 commits into from
Apr 12, 2025

Conversation

tomasr8
Copy link
Member

@tomasr8 tomasr8 commented Apr 1, 2025

Fix based on the idea from @iritkatriel: #131927 (comment)

Yet another option is to leave ast_opt alone, and handle this in the repl. So the repl would save the warnings from the ast construction and then filter out duplicated warnings from the compilation call.

PEP 765 is new in 3.14 so I think we can skip a news entry?

incomplete_input=False,
)
with warnings.catch_warnings(record=True) as all_warnings:
warnings.simplefilter("always")
Copy link
Member

@iritkatriel iritkatriel Apr 1, 2025

Choose a reason for hiding this comment

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

Can we use "default" instead of "always", and just one context manager that spans both compiler calls? I think that will do the de-duplication, if I understand the doc correctly.

https://docs.python.org/3/library/warnings.html#the-warnings-filter

Copy link
Member Author

Choose a reason for hiding this comment

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

From what I can tell it does not work in this case. The syntax warning uses _PyErr_EmitSyntaxWarning which in turn calls PyErr_WarnExplicitObject and sets the registry (last argument) to NULL. The registry is used to dedup the warnings so when it's not used, all warnings are emitted regardless of the filter setting.

Here's an example that shows that:

import warnings

def _compile():
    compile('1 is 1', 'input', 'eval')

with warnings.catch_warnings():
    warnings.simplefilter("default")
    _compile()
    _compile()

This is probably not something we want to change, so doing a manual deduplication in pyrepl seems like the way to go. What do you think?

Copy link
Member

Choose a reason for hiding this comment

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

Maybe we can pass a registry?

Copy link
Member Author

@tomasr8 tomasr8 Apr 2, 2025

Choose a reason for hiding this comment

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

Maybe, though the registry would have to persist between separate invocations of compile. I'll check if there's a nice way to do that.

Copy link
Member Author

Choose a reason for hiding this comment

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

Ok so I spent some time going over the warning code and this is my understanding so far. The warning registries are dictionaries that are inserted as __warningregistry__ into the globals of a Python frame. Which frame this is, is controlled by the stack level. The registries are created on demand in setup_context:

cpython/Python/_warnings.c

Lines 911 to 916 in 37bc386

static int
setup_context(Py_ssize_t stack_level,
PyTupleObject *skip_file_prefixes,
PyObject **filename, int *lineno,
PyObject **module, PyObject **registry)
{

This function is pretty complex and depends on other pieces in the warnings module so we cannot construct a registry manually in _PyErr_EmitSyntaxWarning (which is the one responsible for emitting warnings from ast_opt and currently sets the registry to NULL):

cpython/Python/errors.c

Lines 1909 to 1910 in 37bc386

if (PyErr_WarnExplicitObject(PyExc_SyntaxWarning, msg, filename,
lineno, NULL, NULL) < 0)

One thing we could do, though I don't know how desirable it is, is to add a new private function akin to PyErr_WarnExplicitObject which sets up the registry for us. Basically something like this:

int
- PyErr_WarnExplicitObject(PyObject *category, PyObject *message,
-                          PyObject *filename, int lineno,
-                          PyObject *module, PyObject *registry)
+ _PyErr_WarnExplicitObjectRegistry(PyObject *category, PyObject *message,
+                                  PyObject *filename, int lineno)
{
    PyObject *res;
    if (category == NULL)
        category = PyExc_RuntimeWarning;
    PyThreadState *tstate = get_current_tstate();
    if (tstate == NULL) {
        return -1;
    }

+    PyObject *unused_filename, *module, *registry;
+    int unused_lineno;
+
+    if (!setup_context(1, NULL,
+                       &unused_filename, &unused_lineno, &module, &registry))
+        return -1;

    warnings_lock(tstate->interp);
    res = warn_explicit(tstate, category, message, filename, lineno,
                        module, registry, NULL, NULL);
    warnings_unlock(tstate->interp);
    if (res == NULL)
        return -1;
    Py_DECREF(res);
    return 0;
}

This seems to eliminate the duplicated syntax warnings in the REPL. What do you think about this approach?

Copy link
Member

Choose a reason for hiding this comment

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

I like this. Can _PyErr_WarnExplicitObjectRegistry just create a registry and call PyErr_WarnExplicitObject with it?

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 think so, basically something like this:

int
_PyErr_WarnExplicitObjectRegistry(PyObject *category, PyObject *message,
                         PyObject *filename, int lineno)
{
    PyObject *unused_filename, *module, *registry;
    int unused_lineno;
    int stack_level = 1;

    if (!setup_context(stack_level, NULL, &unused_filename, &unused_lineno,
                       &module, &registry))
        return -1;

    int rc = PyErr_WarnExplicitObject(category, message, filename, lineno,
                                      module, registry);
    Py_DECREF(module);
    return rc;
}

I'll update the PR to see if everything still works with this change.

@iritkatriel
Copy link
Member

tests are failing.

@tomasr8
Copy link
Member Author

tomasr8 commented Apr 5, 2025

tests are failing.

Yep, I suspect it's because setup_context fails which then causes _PyErr_EmitSyntaxWarning to raise instead of emit a warning (I should probably change that). Though I'm not able to reproduce this locally yet.

@python-cla-bot
Copy link

python-cla-bot bot commented Apr 6, 2025

All commit authors signed the Contributor License Agreement.

CLA signed

@iritkatriel
Copy link
Member

LGTM. @serhiy-storchaka what do you think?

@tomasr8
Copy link
Member Author

tomasr8 commented Apr 7, 2025

Ok it seems some other test modified the warning filters so I just needed to add warnings.simplefilter("default") to the test.

I have another question though, regarding the setup_context function. It can currently fail but I'm not sure what is the best way to handle that? Should we raise a SystemError or maybe just fall back to emitting a warning without constructing the registry (and silently ignore the failure)? Or just do what we do now and let it raise a SyntaxError (instead of emitting a warning)?

warnings.simplefilter("default")
console.runsource(code)

count = sum("'return' in a 'finally' block" in str(w.message)
Copy link
Member

Choose a reason for hiding this comment

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

You can use .count() here.

"'return' in a 'finally' block" may be an error in future versions. Maybe use some more long living warnings, like assert(1, 'message') or 1 is 1? BTW, they are currently emitted 3 times, not 2.

Copy link
Member Author

Choose a reason for hiding this comment

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

You can use .count() here.

Hmm I don't think I can, unless I want to match the warning message exactly?

"'return' in a 'finally' block" may be an error in future versions. Maybe use some more long living warnings, like assert(1, 'message') or 1 is 1?

I can't if I want to test the new repl. The PEP 765 warning is the only one emitted from the ast optimizer which is needed to trigger this behaviour in the repl. However this does change the behaviour of all warnings that use _PyErr_EmitSyntaxWarning so I also added tests to test_compile for it.

""")

with warnings.catch_warnings(record=True) as caught:
warnings.simplefilter("default")
Copy link
Member

Choose a reason for hiding this comment

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

What happens if set it to "always"?

Copy link
Member Author

Choose a reason for hiding this comment

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

It emits twice, I added a test to test_compile for it

Copy link
Member

@serhiy-storchaka serhiy-storchaka left a comment

Choose a reason for hiding this comment

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

LGTM. 👍

@serhiy-storchaka
Copy link
Member

Please add also a compile() test for a warning emitted in the finally block. Currently it is emitted twice because the code in the finally block is complied twice (for normal execution and for exception handling).

@iritkatriel iritkatriel merged commit 3d08c8a into python:main Apr 12, 2025
41 checks passed
@iritkatriel
Copy link
Member

Let's create a new issue for that.

1 similar comment
@iritkatriel
Copy link
Member

Let's create a new issue for that.

@tomasr8 tomasr8 deleted the repl-warnings branch April 12, 2025 11:30
@tomasr8
Copy link
Member Author

tomasr8 commented Apr 12, 2025

Please add also a compile() test for a warning emitted in the finally block. Currently it is emitted twice because the code in the finally block is complied twice (for normal execution and for exception handling).

PR is here: #132436

@tomasr8 tomasr8 added the needs backport to 3.13 bugs and security fixes label Apr 12, 2025
@miss-islington-app
Copy link

Thanks @tomasr8 for the PR, and @iritkatriel for merging it 🌮🎉.. I'm working now to backport this PR to: 3.13.
🐍🍒⛏🤖

@miss-islington-app
Copy link

Sorry, @tomasr8 and @iritkatriel, I could not cleanly backport this to 3.13 due to a conflict.
Please backport using cherry_picker on command line.

cherry_picker 3d08c8ad20dfabd4864be139cd9c2eb5602ccdfe 3.13

@tomasr8
Copy link
Member Author

tomasr8 commented Apr 12, 2025

I'll take care of the backport

tomasr8 added a commit to tomasr8/cpython that referenced this pull request Apr 13, 2025
…the REPL (pythonGH-131993)

(cherry picked from commit 3d08c8a)

Co-authored-by: Tomas R. <tomas.roun8@gmail.com>
tomasr8 added a commit to tomasr8/cpython that referenced this pull request Apr 13, 2025
…the REPL (pythonGH-131993)

(cherry picked from commit 3d08c8a)

Co-authored-by: Tomas R. <tomas.roun8@gmail.com>
tomasr8 added a commit to tomasr8/cpython that referenced this pull request Apr 13, 2025
…the REPL (pythonGH-131993)

(cherry picked from commit 3d08c8a)

Co-authored-by: Tomas R. <tomas.roun8@gmail.com>
@bedevere-app
Copy link

bedevere-app bot commented Apr 13, 2025

GH-132463 is a backport of this pull request to the 3.13 branch.

@bedevere-app bedevere-app bot removed the needs backport to 3.13 bugs and security fixes label Apr 13, 2025
@bedevere-app
Copy link

bedevere-app bot commented Apr 13, 2025

GH-132463 is a backport of this pull request to the 3.13 branch.

1 similar comment
@bedevere-app
Copy link

bedevere-app bot commented Apr 13, 2025

GH-132463 is a backport of this pull request to the 3.13 branch.

serhiy-storchaka pushed a commit that referenced this pull request Apr 13, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants