diff --git a/crates/ruff_python_formatter/src/expression/expr_attribute.rs b/crates/ruff_python_formatter/src/expression/expr_attribute.rs index 16d3956b26..5535cebe88 100644 --- a/crates/ruff_python_formatter/src/expression/expr_attribute.rs +++ b/crates/ruff_python_formatter/src/expression/expr_attribute.rs @@ -38,14 +38,13 @@ impl FormatNodeRule for FormatExprAttribute { let format_inner = format_with(|f: &mut PyFormatter| { let parenthesize_value = - // If the value is an integer, we need to parenthesize it to avoid a syntax error. - matches!( - value.as_ref(), - Expr::Constant(ExprConstant { - value: Constant::Int(_) | Constant::Float(_), - .. - }) - ) || is_expression_parenthesized(value.into(), f.context().comments().ranges(), f.context().source()); + is_base_ten_number_literal(value.as_ref(), f.context().source()) || { + is_expression_parenthesized( + value.into(), + f.context().comments().ranges(), + f.context().source(), + ) + }; if call_chain_layout == CallChainLayout::Fluent { if parenthesize_value { @@ -164,3 +163,23 @@ impl NeedsParentheses for ExprAttribute { } } } + +// Non Hex, octal or binary number literals need parentheses to disambiguate the attribute `.` from +// a decimal point. Floating point numbers don't strictly need parentheses but it reads better (rather than 0.0.test()). +fn is_base_ten_number_literal(expr: &Expr, source: &str) -> bool { + if let Some(ExprConstant { value, range }) = expr.as_constant_expr() { + match value { + Constant::Float(_) => true, + Constant::Int(_) => { + let text = &source[*range]; + !matches!( + text.as_bytes().get(0..2), + Some([b'0', b'x' | b'X' | b'o' | b'O' | b'b' | b'B']) + ) + } + _ => false, + } + } else { + false + } +} diff --git a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__attribute_access_on_number_literals.py.snap b/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__attribute_access_on_number_literals.py.snap deleted file mode 100644 index f1c8df13c2..0000000000 --- a/crates/ruff_python_formatter/tests/snapshots/black_compatibility@simple_cases__attribute_access_on_number_literals.py.snap +++ /dev/null @@ -1,108 +0,0 @@ ---- -source: crates/ruff_python_formatter/tests/fixtures.rs -input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/attribute_access_on_number_literals.py ---- -## Input - -```py -x = 123456789 .bit_count() -x = (123456).__abs__() -x = .1.is_integer() -x = 1. .imag -x = 1E+1.imag -x = 1E-1.real -x = 123456789.123456789.hex() -x = 123456789.123456789E123456789 .real -x = 123456789E123456789 .conjugate() -x = 123456789J.real -x = 123456789.123456789J.__add__(0b1011.bit_length()) -x = 0XB1ACC.conjugate() -x = 0B1011 .conjugate() -x = 0O777 .real -x = 0.000000006 .hex() -x = -100.0000J - -if 10 .real: - ... - -y = 100[no] -y = 100(no) -``` - -## Black Differences - -```diff ---- Black -+++ Ruff -@@ -8,10 +8,10 @@ - x = (123456789.123456789e123456789).real - x = (123456789e123456789).conjugate() - x = 123456789j.real --x = 123456789.123456789j.__add__(0b1011.bit_length()) --x = 0xB1ACC.conjugate() --x = 0b1011.conjugate() --x = 0o777.real -+x = 123456789.123456789j.__add__((0b1011).bit_length()) -+x = (0xB1ACC).conjugate() -+x = (0b1011).conjugate() -+x = (0o777).real - x = (0.000000006).hex() - x = -100.0000j - -``` - -## Ruff Output - -```py -x = (123456789).bit_count() -x = (123456).__abs__() -x = (0.1).is_integer() -x = (1.0).imag -x = (1e1).imag -x = (1e-1).real -x = (123456789.123456789).hex() -x = (123456789.123456789e123456789).real -x = (123456789e123456789).conjugate() -x = 123456789j.real -x = 123456789.123456789j.__add__((0b1011).bit_length()) -x = (0xB1ACC).conjugate() -x = (0b1011).conjugate() -x = (0o777).real -x = (0.000000006).hex() -x = -100.0000j - -if (10).real: - ... - -y = 100[no] -y = 100(no) -``` - -## Black Output - -```py -x = (123456789).bit_count() -x = (123456).__abs__() -x = (0.1).is_integer() -x = (1.0).imag -x = (1e1).imag -x = (1e-1).real -x = (123456789.123456789).hex() -x = (123456789.123456789e123456789).real -x = (123456789e123456789).conjugate() -x = 123456789j.real -x = 123456789.123456789j.__add__(0b1011.bit_length()) -x = 0xB1ACC.conjugate() -x = 0b1011.conjugate() -x = 0o777.real -x = (0.000000006).hex() -x = -100.0000j - -if (10).real: - ... - -y = 100[no] -y = 100(no) -``` - -