Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Doc/library/token-list.inc

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Grammar/Tokens
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ RARROW '->'
ELLIPSIS '...'
COLONEQUAL ':='
EXCLAMATION '!'
QUESTIONMARKDOT '?.'

OP
TYPE_IGNORE
Expand Down
5 changes: 5 additions & 0 deletions Grammar/python.gram
Original file line number Diff line number Diff line change
Expand Up @@ -545,6 +545,10 @@ attr[expr_ty]:
| value=name_or_attr '.' attr=NAME {
_PyAST_Attribute(value, attr->v.Name.id, Load, EXTRA) }

safe_attr[expr_ty]:
| value=name_or_attr '.' attr=NAME {
_PyAST_SafeAttribute(value, attr->v.Name.id, Load, EXTRA) }

name_or_attr[expr_ty]:
| attr
| NAME
Expand Down Expand Up @@ -822,6 +826,7 @@ await_primary[expr_ty] (memo):

primary[expr_ty]:
| a=primary '.' b=NAME { _PyAST_Attribute(a, b->v.Name.id, Load, EXTRA) }
| a=primary '?.' b=NAME { _PyAST_SafeAttribute(a, b->v.Name.id, Load, EXTRA) }
| a=primary b=genexp { _PyAST_Call(a, CHECK(asdl_expr_seq*, (asdl_expr_seq*)_PyPegen_singleton_seq(p, b)), NULL, EXTRA) }
| a=primary '(' b=[arguments] ')' {
_PyAST_Call(a,
Expand Down
14 changes: 12 additions & 2 deletions Include/internal/pycore_ast.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Include/internal/pycore_ast_state.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

23 changes: 12 additions & 11 deletions Include/internal/pycore_token.h
Original file line number Diff line number Diff line change
Expand Up @@ -68,17 +68,18 @@ extern "C" {
#define ELLIPSIS 52
#define COLONEQUAL 53
#define EXCLAMATION 54
#define OP 55
#define TYPE_IGNORE 56
#define TYPE_COMMENT 57
#define SOFT_KEYWORD 58
#define FSTRING_START 59
#define FSTRING_MIDDLE 60
#define FSTRING_END 61
#define COMMENT 62
#define NL 63
#define ERRORTOKEN 64
#define N_TOKENS 66
#define QUESTIONMARKDOT 55
#define OP 56
#define TYPE_IGNORE 57
#define TYPE_COMMENT 58
#define SOFT_KEYWORD 59
#define FSTRING_START 60
#define FSTRING_MIDDLE 61
#define FSTRING_END 62
#define COMMENT 63
#define NL 64
#define ERRORTOKEN 65
#define N_TOKENS 67
#define NT_OFFSET 256

/* Special definitions for cooperation with parser */
Expand Down
46 changes: 46 additions & 0 deletions Lib/test/test_safeattr.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import unittest
from Lib.test.test_syntax import SyntaxTestCase


class A:
def __init__(self) -> None:
self.b = 0


class SafeAttrTest(SyntaxTestCase):
def test_safe_attr_is_safe(self):
a = None
with self.assertRaises(AttributeError):
a.b
assert a?.b is None

def test_safe_attr_nonnull_case(self):
a = A()
assert a.b == a?.b == 0

def test_cannot_delete_with_safe_attr(self):
self._check_error("""
class A:
def __init__(self) -> None:
self.b = 0
a = A()

del a.b

del a?.b
""", "syntax")


def test_cannot_store_with_safe_attr(self):
self._check_error("""
class A:
def __init__(self) -> None:
self.b = 0
a = A()
a.b = 4
a?.b = 3
""", "cannot assign to safe attribute here")


if __name__ == '__main__':
unittest.main()
2 changes: 0 additions & 2 deletions Lib/test/test_until.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,3 @@ def test_until(self):

if __name__ == '__main__':
unittest.main()


26 changes: 14 additions & 12 deletions Lib/token.py

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Parser/Python.asdl
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ module Python

-- the following expression can appear in assignment context
| Attribute(expr value, identifier attr, expr_context ctx)
| SafeAttribute(expr value, identifier attr, expr_context ctx)
| Subscript(expr value, expr slice, expr_context ctx)
| Starred(expr value, expr_context ctx)
| Name(identifier id, expr_context ctx)
Expand Down
15 changes: 15 additions & 0 deletions Parser/action_helpers.c
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,15 @@ _set_attribute_context(Parser *p, expr_ty e, expr_context_ty ctx)
ctx, EXTRA_EXPR(e, e));
}


static expr_ty
_set_safe_attribute_context(Parser *p, expr_ty e, expr_context_ty ctx)
{
return _PyAST_SafeAttribute(e->v.SafeAttribute.value, e->v.SafeAttribute.attr,
ctx, EXTRA_EXPR(e, e));
}


static expr_ty
_set_starred_context(Parser *p, expr_ty e, expr_context_ty ctx)
{
Expand Down Expand Up @@ -353,6 +362,9 @@ _PyPegen_set_expr_context(Parser *p, expr_ty expr, expr_context_ty ctx)
case Attribute_kind:
new = _set_attribute_context(p, expr, ctx);
break;
case SafeAttribute_kind:
new = _set_safe_attribute_context(p, expr, ctx);
break;
case Starred_kind:
new = _set_starred_context(p, expr, ctx);
break;
Expand Down Expand Up @@ -1042,6 +1054,8 @@ _PyPegen_get_expr_name(expr_ty e)
switch (e->kind) {
case Attribute_kind:
return "attribute";
case SafeAttribute_kind:
return "safe attribute";
case Subscript_kind:
return "subscript";
case Starred_kind:
Expand Down Expand Up @@ -1204,6 +1218,7 @@ _PyPegen_get_invalid_target(expr_ty e, TARGETS_TYPE targets_type)
case Name_kind:
case Subscript_kind:
case Attribute_kind:
case SafeAttribute_kind:
return NULL;
default:
return e;
Expand Down
Loading