Use parentheses on method access on float and int literals
diff --git a/CHANGES.md b/CHANGES.md
index 0dc4952f069..6966a91aa11 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -42,6 +42,9 @@
 - Make passing `SRC` or `--code` mandatory and mutually exclusive (#2804)
 - Work around bug that causes unstable formatting in some cases in the presence of the
   magic trailing comma (#2807)
+- Use parentheses for attribute access on decimal float and int literals (#2799)
+- Don't add whitespace for attribute access on hexadecimal, binary, octal, and complex
+  literals (#2799)
 - Deprecate the `black-primer` tool (#2809)
 
 ### Packaging
diff --git a/src/black/linegen.py b/src/black/linegen.py
index ac60ed1986d..b572ed0b52f 100644
--- a/src/black/linegen.py
+++ b/src/black/linegen.py
@@ -197,6 +197,28 @@ def visit_decorators(self, node: Node) -> Iterator[Line]:
             yield from self.line()
             yield from self.visit(child)
 
+    def visit_power(self, node: Node) -> Iterator[Line]:
+        for idx, leaf in enumerate(node.children[:-1]):
+            next_leaf = node.children[idx + 1]
+
+            if not isinstance(leaf, Leaf):
+                continue
+
+            value = leaf.value.lower()
+            if (
+                leaf.type == token.NUMBER
+                and next_leaf.type == syms.trailer
+                # Ensure that we are in an attribute trailer
+                and next_leaf.children[0].type == token.DOT
+                # It shouldn't wrap hexadecimal, binary and octal literals
+                and not value.startswith(("0x", "0b", "0o"))
+                # It shouldn't wrap complex literals
+                and "j" not in value
+            ):
+                wrap_in_parentheses(node, leaf)
+
+        yield from self.visit_default(node)
+
     def visit_SEMI(self, leaf: Leaf) -> Iterator[Line]:
         """Remove a semicolon and put the other statement on a separate line."""
         yield from self.line()
diff --git a/src/black/nodes.py b/src/black/nodes.py
index 74dfa896295..51d4cb8618d 100644
--- a/src/black/nodes.py
+++ b/src/black/nodes.py
@@ -306,12 +306,7 @@ def whitespace(leaf: Leaf, *, complex_subscript: bool) -> str:  # noqa: C901
             return NO
 
         if not prev:
-            if t == token.DOT:
-                prevp = preceding_leaf(p)
-                if not prevp or prevp.type != token.NUMBER:
-                    return NO
-
-            elif t == token.LSQB:
+            if t == token.DOT or t == token.LSQB:
                 return NO
 
         elif prev.type != token.COMMA:
diff --git a/tests/data/attribute_access_on_number_literals.py b/tests/data/attribute_access_on_number_literals.py
new file mode 100644
index 00000000000..7c16bdfb3a5
--- /dev/null
+++ b/tests/data/attribute_access_on_number_literals.py
@@ -0,0 +1,47 @@
+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)
+
+# output
+
+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)
diff --git a/tests/data/expression.diff b/tests/data/expression.diff
index 5f29a18dc7f..2eaaeb479f8 100644
--- a/tests/data/expression.diff
+++ b/tests/data/expression.diff
@@ -11,7 +11,7 @@
  True
  False
  1
-@@ -21,71 +21,104 @@
+@@ -21,99 +21,135 @@
  Name1 or (Name2 and Name3) or Name4
  Name1 or Name2 and Name3 or Name4
  v1 << 2
@@ -144,8 +144,11 @@
  call(**self.screen_kwargs)
  call(b, **self.screen_kwargs)
  lukasz.langa.pl
-@@ -94,26 +127,29 @@
- 1.0 .real
+ call.me(maybe)
+-1 .real
+-1.0 .real
++(1).real
++(1.0).real
  ....__class__
  list[str]
  dict[str, int]
diff --git a/tests/data/expression.py b/tests/data/expression.py
index b056841027d..06096c589f1 100644
--- a/tests/data/expression.py
+++ b/tests/data/expression.py
@@ -382,8 +382,8 @@ async def f():
 call(b, **self.screen_kwargs)
 lukasz.langa.pl
 call.me(maybe)
-1 .real
-1.0 .real
+(1).real
+(1.0).real
 ....__class__
 list[str]
 dict[str, int]
diff --git a/tests/data/expression_skip_magic_trailing_comma.diff b/tests/data/expression_skip_magic_trailing_comma.diff
index 5b722c91352..eba3fd2da7d 100644
--- a/tests/data/expression_skip_magic_trailing_comma.diff
+++ b/tests/data/expression_skip_magic_trailing_comma.diff
@@ -11,7 +11,7 @@
  True
  False
  1
-@@ -21,71 +21,92 @@
+@@ -21,99 +21,118 @@
  Name1 or (Name2 and Name3) or Name4
  Name1 or Name2 and Name3 or Name4
  v1 << 2
@@ -132,8 +132,11 @@
  call(**self.screen_kwargs)
  call(b, **self.screen_kwargs)
  lukasz.langa.pl
-@@ -94,26 +115,24 @@
- 1.0 .real
+ call.me(maybe)
+-1 .real
+-1.0 .real
++(1).real
++(1.0).real
  ....__class__
  list[str]
  dict[str, int]
diff --git a/tests/test_format.py b/tests/test_format.py
index 88f084ea478..aef22545f5b 100644
--- a/tests/test_format.py
+++ b/tests/test_format.py
@@ -15,6 +15,7 @@
 )
 
 SIMPLE_CASES: List[str] = [
+    "attribute_access_on_number_literals",
     "beginning_backslash",
     "bracketmatch",
     "class_blank_parentheses",

Closes #2034 

### Checklist - did you ...

- [X] Add a CHANGELOG entry if necessary?
- [X] Add / update tests if necessary?
- [ ] Add new / update outdated documentation? -> n/a
