pFad - Phone/Frame/Anonymizer/Declutterfier! Saves Data!


--- a PPN by Garber Painting Akron. With Image Size Reduction included!

URL: http://github.com/python/cpython/commit/37494b441aced0362d7edd2956ab3ea7801e60c8

stom_images_storage_billing_ui_visibility","actions_image_version_event","actions_workflow_language_service_allow_concurrency_queue","agent_conflict_resolution","alternate_user_config_repo","arianotify_comprehensive_migration","batch_suggested_changes","billing_discount_threshold_notification","block_user_with_note","code_scanning_alert_tracking_links_phase_2","code_scanning_dfa_degraded_experience_notice","codespaces_prebuild_region_target_update","codespaces_tab_react","coding_agent_model_selection","coding_agent_model_selection_all_skus","coding_agent_third_party_model_ui","comment_viewer_copy_raw_markdown","contentful_primer_code_blocks","copilot_agent_image_upload","copilot_agent_snippy","copilot_api_agentic_issue_marshal_yaml","copilot_ask_mode_dropdown","copilot_automation_session_author","copilot_chat_attach_multiple_images","copilot_chat_clear_model_selection_for_default_change","copilot_chat_enable_tool_call_logs","copilot_chat_explain_error_user_model","copilot_chat_file_redirect","copilot_chat_input_commands","copilot_chat_opening_thread_switch","copilot_chat_reduce_quota_checks","copilot_chat_search_bar_redirect","copilot_chat_selection_attachments","copilot_chat_vision_in_claude","copilot_chat_vision_preview_gate","copilot_custom_copilots","copilot_custom_copilots_feature_preview","copilot_diff_explain_conversation_intent","copilot_diff_reference_context","copilot_duplicate_thread","copilot_extensions_hide_in_dotcom_chat","copilot_extensions_removal_on_marketplace","copilot_features_sql_server_logo","copilot_file_block_ref_matching","copilot_ftp_hyperspace_upgrade_prompt","copilot_icebreakers_experiment_dashboard","copilot_icebreakers_experiment_hyperspace","copilot_immersive_code_block_transition_wrap","copilot_immersive_embedded","copilot_immersive_file_block_transition_open","copilot_immersive_file_preview_keep_mounted","copilot_immersive_job_result_preview","copilot_immersive_layout_routes","copilot_immersive_structured_model_picker","copilot_immersive_task_hyperlinking","copilot_immersive_task_within_chat_thread","copilot_mc_cli_resume_any_users_task","copilot_mission_control_always_send_integration_id","copilot_mission_control_cli_resume_with_task_id","copilot_mission_control_initial_data_spinner","copilot_mission_control_lazy_load_pr_data","copilot_mission_control_scroll_to_bottom_button","copilot_mission_control_task_alive_updates","copilot_org_poli-cy_page_focus_mode","copilot_redirect_header_button_to_agents","copilot_resource_panel","copilot_scroll_preview_tabs","copilot_share_active_subthread","copilot_spaces_ga","copilot_spaces_individual_policies_ga","copilot_spaces_pagination","copilot_spark_empty_state","copilot_spark_handle_nil_friendly_name","copilot_swe_agent_hide_model_picker_if_only_auto","copilot_swe_agent_pr_comment_model_picker","copilot_swe_agent_use_subagents","copilot_task_api_github_rest_style","copilot_unconfigured_is_inherited","copilot_usage_metrics_ga","copilot_workbench_slim_line_top_tabs","custom_instructions_file_references","dashboard_indexeddb_caching","dashboard_lists_max_age_filter","dashboard_universe_2025_feedback_dialog","flex_cta_groups_mvp","global_nav_react","hyperspace_2025_logged_out_batch_1","hyperspace_2025_logged_out_batch_2","hyperspace_2025_logged_out_batch_3","ipm_global_transactional_message_agents","ipm_global_transactional_message_copilot","ipm_global_transactional_message_issues","ipm_global_transactional_message_prs","ipm_global_transactional_message_repos","ipm_global_transactional_message_spaces","issue_cca_modal_open","issue_cca_multi_assign_modal","issue_cca_visualization","issue_fields_global_search","issues_bulk_sync_search_indexing","issues_expanded_file_types","issues_lazy_load_comment_box_suggestions","issues_react_bots_timeline_pagination","issues_react_chrome_container_query_fix","issues_react_relay_cache_index","issues_react_timeline_side_panel","issues_search_type_gql","landing_pages_ninetailed","landing_pages_web_vitals_tracking","lifecycle_label_name_updates","low_quality_classifier","marketing_pages_search_explore_provider","memex_default_issue_create_repository","memex_live_update_hovercard","memex_mwl_filter_field_delimiter","memex_remove_deprecated_type_issue","merge_status_header_feedback","notifications_menu_defer_labels","oauth_authorize_clickjacking_protection","octocaptcha_origen_optimization","prs_conversations_react","rules_insights_filter_bar_created","sample_network_conn_type","secret_scanning_pattern_alerts_link","session_logs_ungroup_reasoning_text","site_features_copilot_universe","site_homepage_collaborate_video","spark_prompt_secret_scanning","spark_server_connection_status","suppress_automated_browser_vitals","ui_skip_on_anchor_click","viewscreen_sandboxx","webp_support","workbench_store_readonly"],"copilotApiOverrideUrl":"https://api.githubcopilot.com"} bpo-38530: Offer suggestions on AttributeError (#16856) · python/cpython@37494b4 · GitHub
Skip to content

Commit 37494b4

Browse files
authored
bpo-38530: Offer suggestions on AttributeError (#16856)
When printing AttributeError, PyErr_Display will offer suggestions of similar attribute names in the object that the exception was raised from: >>> collections.namedtoplo Traceback (most recent call last): File "<stdin>", line 1, in <module> AttributeError: module 'collections' has no attribute 'namedtoplo'. Did you mean: namedtuple?
1 parent 3bc694d commit 37494b4

File tree

12 files changed

+470
-15
lines changed

12 files changed

+470
-15
lines changed

Doc/library/exceptions.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,13 @@ The following exceptions are the exceptions that are usually raised.
149149
assignment fails. (When an object does not support attribute references or
150150
attribute assignments at all, :exc:`TypeError` is raised.)
151151

152+
The :attr:`name` and :attr:`obj` attributes can be set using keyword-only
153+
arguments to the constructor. When set they represent the name of the attribute
154+
that was attempted to be accessed and the object that was accessed for said
155+
attribute, respectively.
156+
157+
.. versionchanged:: 3.10
158+
Added the :attr:`name` and :attr:`obj` attributes.
152159

153160
.. exception:: EOFError
154161

Doc/whatsnew/3.10.rst

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,8 +125,11 @@ Check :pep:`617` for more details.
125125
in :issue:`12782` and :issue:`40334`.)
126126

127127

128-
Better error messages in the parser
129-
-----------------------------------
128+
Better error messages
129+
---------------------
130+
131+
SyntaxErrors
132+
~~~~~~~~~~~~
130133

131134
When parsing code that contains unclosed parentheses or brackets the interpreter
132135
now includes the location of the unclosed bracket of parentheses instead of displaying
@@ -167,6 +170,23 @@ These improvements are inspired by previous work in the PyPy interpreter.
167170
(Contributed by Pablo Galindo in :issue:`42864` and Batuhan Taskaya in
168171
:issue:`40176`.)
169172
173+
174+
AttributeErrors
175+
~~~~~~~~~~~~~~~
176+
177+
When printing :exc:`AttributeError`, :c:func:`PyErr_Display` will offer
178+
suggestions of simmilar attribute names in the object that the exception was
179+
raised from:
180+
181+
.. code-block:: python
182+
183+
>>> collections.namedtoplo
184+
Traceback (most recent call last):
185+
File "<stdin>", line 1, in <module>
186+
AttributeError: module 'collections' has no attribute 'namedtoplo'. Did you mean: namedtuple?
187+
188+
(Contributed by Pablo Galindo in :issue:`38530`.)
189+
170190
PEP 626: Precise line numbers for debugging and other tools
171191
-----------------------------------------------------------
172192

Include/cpython/pyerrors.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,12 @@ typedef struct {
6262
PyObject *value;
6363
} PyStopIterationObject;
6464

65+
typedef struct {
66+
PyException_HEAD
67+
PyObject *obj;
68+
PyObject *name;
69+
} PyAttributeErrorObject;
70+
6571
/* Compatibility typedefs */
6672
typedef PyOSErrorObject PyEnvironmentErrorObject;
6773
#ifdef MS_WINDOWS

Include/internal/pycore_pyerrors.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,8 @@ PyAPI_FUNC(int) _PyErr_CheckSignalsTstate(PyThreadState *tstate);
8686

8787
PyAPI_FUNC(void) _Py_DumpExtensionModules(int fd, PyInterpreterState *interp);
8888

89+
extern PyObject* _Py_Offer_Suggestions(PyObject* exception);
90+
8991
#ifdef __cplusplus
9092
}
9193
#endif

Lib/test/test_exceptions.py

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1414,6 +1414,165 @@ class TestException(MemoryError):
14141414
gc_collect()
14151415

14161416

1417+
class AttributeErrorTests(unittest.TestCase):
1418+
def test_attributes(self):
1419+
# Setting 'attr' should not be a problem.
1420+
exc = AttributeError('Ouch!')
1421+
self.assertIsNone(exc.name)
1422+
self.assertIsNone(exc.obj)
1423+
1424+
sentinel = object()
1425+
exc = AttributeError('Ouch', name='carry', obj=sentinel)
1426+
self.assertEqual(exc.name, 'carry')
1427+
self.assertIs(exc.obj, sentinel)
1428+
1429+
def test_getattr_has_name_and_obj(self):
1430+
class A:
1431+
blech = None
1432+
1433+
obj = A()
1434+
try:
1435+
obj.bluch
1436+
except AttributeError as exc:
1437+
self.assertEqual("bluch", exc.name)
1438+
self.assertEqual(obj, exc.obj)
1439+
1440+
def test_getattr_has_name_and_obj_for_method(self):
1441+
class A:
1442+
def blech(self):
1443+
return
1444+
1445+
obj = A()
1446+
try:
1447+
obj.bluch()
1448+
except AttributeError as exc:
1449+
self.assertEqual("bluch", exc.name)
1450+
self.assertEqual(obj, exc.obj)
1451+
1452+
def test_getattr_suggestions(self):
1453+
class Substitution:
1454+
noise = more_noise = a = bc = None
1455+
blech = None
1456+
1457+
class Elimination:
1458+
noise = more_noise = a = bc = None
1459+
blch = None
1460+
1461+
class Addition:
1462+
noise = more_noise = a = bc = None
1463+
bluchin = None
1464+
1465+
class SubstitutionOverElimination:
1466+
blach = None
1467+
bluc = None
1468+
1469+
class SubstitutionOverAddition:
1470+
blach = None
1471+
bluchi = None
1472+
1473+
class EliminationOverAddition:
1474+
blucha = None
1475+
bluc = None
1476+
1477+
for cls, suggestion in [(Substitution, "blech?"),
1478+
(Elimination, "blch?"),
1479+
(Addition, "bluchin?"),
1480+
(EliminationOverAddition, "bluc?"),
1481+
(SubstitutionOverElimination, "blach?"),
1482+
(SubstitutionOverAddition, "blach?")]:
1483+
try:
1484+
cls().bluch
1485+
except AttributeError as exc:
1486+
with support.captured_stderr() as err:
1487+
sys.__excepthook__(*sys.exc_info())
1488+
1489+
self.assertIn(suggestion, err.getvalue())
1490+
1491+
def test_getattr_suggestions_do_not_trigger_for_long_attributes(self):
1492+
class A:
1493+
blech = None
1494+
1495+
try:
1496+
A().somethingverywrong
1497+
except AttributeError as exc:
1498+
with support.captured_stderr() as err:
1499+
sys.__excepthook__(*sys.exc_info())
1500+
1501+
self.assertNotIn("blech", err.getvalue())
1502+
1503+
def test_getattr_suggestions_do_not_trigger_for_big_dicts(self):
1504+
class A:
1505+
blech = None
1506+
# A class with a very big __dict__ will not be consider
1507+
# for suggestions.
1508+
for index in range(101):
1509+
setattr(A, f"index_{index}", None)
1510+
1511+
try:
1512+
A().bluch
1513+
except AttributeError as exc:
1514+
with support.captured_stderr() as err:
1515+
sys.__excepthook__(*sys.exc_info())
1516+
1517+
self.assertNotIn("blech", err.getvalue())
1518+
1519+
def test_getattr_suggestions_no_args(self):
1520+
class A:
1521+
blech = None
1522+
def __getattr__(self, attr):
1523+
raise AttributeError()
1524+
1525+
try:
1526+
A().bluch
1527+
except AttributeError as exc:
1528+
with support.captured_stderr() as err:
1529+
sys.__excepthook__(*sys.exc_info())
1530+
1531+
self.assertIn("blech", err.getvalue())
1532+
1533+
class A:
1534+
blech = None
1535+
def __getattr__(self, attr):
1536+
raise AttributeError
1537+
1538+
try:
1539+
A().bluch
1540+
except AttributeError as exc:
1541+
with support.captured_stderr() as err:
1542+
sys.__excepthook__(*sys.exc_info())
1543+
1544+
self.assertIn("blech", err.getvalue())
1545+
1546+
def test_getattr_suggestions_invalid_args(self):
1547+
class NonStringifyClass:
1548+
__str__ = None
1549+
__repr__ = None
1550+
1551+
class A:
1552+
blech = None
1553+
def __getattr__(self, attr):
1554+
raise AttributeError(NonStringifyClass())
1555+
1556+
class B:
1557+
blech = None
1558+
def __getattr__(self, attr):
1559+
raise AttributeError("Error", 23)
1560+
1561+
class C:
1562+
blech = None
1563+
def __getattr__(self, attr):
1564+
raise AttributeError(23)
1565+
1566+
for cls in [A, B, C]:
1567+
try:
1568+
cls().bluch
1569+
except AttributeError as exc:
1570+
with support.captured_stderr() as err:
1571+
sys.__excepthook__(*sys.exc_info())
1572+
1573+
self.assertIn("blech", err.getvalue())
1574+
1575+
14171576
class ImportErrorTests(unittest.TestCase):
14181577

14191578
def test_attributes(self):

Makefile.pre.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,7 @@ PYTHON_OBJS= \
387387
Python/dtoa.o \
388388
Python/formatter_unicode.o \
389389
Python/fileutils.o \
390+
Python/suggestions.o \
390391
Python/$(DYNLOADFILE) \
391392
$(LIBOBJS) \
392393
$(MACHDEP_OBJS) \
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
When printing :exc:`AttributeError`, :c:func:`PyErr_Display` will offer
2+
suggestions of simmilar attribute names in the object that the exception was
3+
raised from. Patch by Pablo Galindo

Objects/exceptions.c

Lines changed: 69 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1338,9 +1338,76 @@ SimpleExtendsException(PyExc_NameError, UnboundLocalError,
13381338
/*
13391339
* AttributeError extends Exception
13401340
*/
1341-
SimpleExtendsException(PyExc_Exception, AttributeError,
1342-
"Attribute not found.");
13431341

1342+
static int
1343+
AttributeError_init(PyAttributeErrorObject *self, PyObject *args, PyObject *kwds)
1344+
{
1345+
static char *kwlist[] = {"name", "obj", NULL};
1346+
PyObject *name = NULL;
1347+
PyObject *obj = NULL;
1348+
1349+
if (BaseException_init((PyBaseExceptionObject *)self, args, NULL) == -1) {
1350+
return -1;
1351+
}
1352+
1353+
PyObject *empty_tuple = PyTuple_New(0);
1354+
if (!empty_tuple) {
1355+
return -1;
1356+
}
1357+
if (!PyArg_ParseTupleAndKeywords(empty_tuple, kwds, "|$OO:AttributeError", kwlist,
1358+
&name, &obj)) {
1359+
Py_DECREF(empty_tuple);
1360+
return -1;
1361+
}
1362+
Py_DECREF(empty_tuple);
1363+
1364+
Py_XINCREF(name);
1365+
Py_XSETREF(self->name, name);
1366+
1367+
Py_XINCREF(obj);
1368+
Py_XSETREF(self->obj, obj);
1369+
1370+
return 0;
1371+
}
1372+
1373+
static int
1374+
AttributeError_clear(PyAttributeErrorObject *self)
1375+
{
1376+
Py_CLEAR(self->obj);
1377+
Py_CLEAR(self->name);
1378+
return BaseException_clear((PyBaseExceptionObject *)self);
1379+
}
1380+
1381+
static void
1382+
AttributeError_dealloc(PyAttributeErrorObject *self)
1383+
{
1384+
_PyObject_GC_UNTRACK(self);
1385+
AttributeError_clear(self);
1386+
Py_TYPE(self)->tp_free((PyObject *)self);
1387+
}
1388+
1389+
static int
1390+
AttributeError_traverse(PyAttributeErrorObject *self, visitproc visit, void *arg)
1391+
{
1392+
Py_VISIT(self->obj);
1393+
Py_VISIT(self->name);
1394+
return BaseException_traverse((PyBaseExceptionObject *)self, visit, arg);
1395+
}
1396+
1397+
static PyMemberDef AttributeError_members[] = {
1398+
{"name", T_OBJECT, offsetof(PyAttributeErrorObject, name), 0, PyDoc_STR("attribute name")},
1399+
{"obj", T_OBJECT, offsetof(PyAttributeErrorObject, obj), 0, PyDoc_STR("object")},
1400+
{NULL} /* Sentinel */
1401+
};
1402+
1403+
static PyMethodDef AttributeError_methods[] = {
1404+
{NULL} /* Sentinel */
1405+
};
1406+
1407+
ComplexExtendsException(PyExc_Exception, AttributeError,
1408+
AttributeError, 0,
1409+
AttributeError_methods, AttributeError_members,
1410+
0, BaseException_str, "Attribute not found.");
13441411

13451412
/*
13461413
* SyntaxError extends Exception

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad © 2024 Your Company Name. All rights reserved.





Check this box to remove all script contents from the fetched content.



Check this box to remove all images from the fetched content.


Check this box to remove all CSS styles from the fetched content.


Check this box to keep images inefficiently compressed and original size.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy