Repository: asottile/pyupgrade

Issue:
py39+: abspath(__file__) -> __file__
since `__file__` is now an absolute path in python3.9+

Discussion:

Patch:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index c4d0384..49f61a6 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,6 +1,6 @@
 repos:
 -   repo: https://github.com/pre-commit/pre-commit-hooks
-    rev: v4.1.0
+    rev: v4.0.1
     hooks:
     -   id: check-docstring-first
     -   id: check-yaml
@@ -39,6 +39,6 @@ repos:
     -   id: pyupgrade
         args: [--py36-plus]
 -   repo: https://github.com/pre-commit/mirrors-mypy
-    rev: v0.930
+    rev: v0.910-1
     hooks:
     -   id: mypy
diff --git a/README.md b/README.md
index 5fa9cbf..9352083 100644
--- a/README.md
+++ b/README.md
@@ -270,7 +270,7 @@ Availability:
 
 ```diff
 -str()
-+''
++"''"
 -str("foo")
 +"foo"
 ```
@@ -756,6 +756,19 @@ Availability:
 ```
 
 
+### remove unnecessary abspath
+
+Availability:
+- `--py39-plus` is passed on the commandline.
+
+```diff
+ from os.path import abspath
+
+-abspath(__file__)
++__file__
+```
+
+
 ### pep 604 typing rewrites
 
 Availability:
diff --git a/pyupgrade/_data.py b/pyupgrade/_data.py
index 5d7aaf7..1581a55 100644
--- a/pyupgrade/_data.py
+++ b/pyupgrade/_data.py
@@ -44,6 +44,7 @@ ASTFunc = Callable[[State, AST_T, ast.AST], Iterable[Tuple[Offset, TokenFunc]]]
 
 RECORD_FROM_IMPORTS = frozenset((
     '__future__',
+    'os.path',
     'functools',
     'mmap',
     'select',
@@ -116,7 +117,8 @@ def visit(
 
 
 def _import_plugins() -> None:
-    plugins_path = _plugins.__path__
+    # https://github.com/python/mypy/issues/1422
+    plugins_path: str = _plugins.__path__  # type: ignore
     mod_infos = pkgutil.walk_packages(plugins_path, f'{_plugins.__name__}.')
     for _, name, _ in mod_infos:
         __import__(name, fromlist=['_trash'])
diff --git a/pyupgrade/_plugins/abspath_file.py b/pyupgrade/_plugins/abspath_file.py
new file mode 100644
index 0000000..b70b2db
--- /dev/null
+++ b/pyupgrade/_plugins/abspath_file.py
@@ -0,0 +1,53 @@
+import ast
+from typing import Iterable
+from typing import List
+from typing import Tuple
+
+from tokenize_rt import Offset
+from tokenize_rt import Token
+
+from pyupgrade._ast_helpers import ast_to_offset
+from pyupgrade._data import register
+from pyupgrade._data import State
+from pyupgrade._data import TokenFunc
+from pyupgrade._token_helpers import find_closing_bracket
+from pyupgrade._token_helpers import find_open_paren
+
+
+def _remove_abspath(i: int, tokens: List[Token]) -> None:
+    paren_start = find_open_paren(tokens, i + 1)
+    paren_end = find_closing_bracket(tokens, paren_start)
+    while i <= paren_start:
+        tokens[i] = Token('PLACEHOLDER', '')
+        i += 1
+    tokens[paren_end] = Token('PLACEHOLDER', '')
+
+
+@register(ast.Call)
+def visit_Call(
+        state: State,
+        node: ast.Call,
+        parent: ast.AST,
+) -> Iterable[Tuple[Offset, TokenFunc]]:
+    if (
+            state.settings.min_version >= (3, 9) and
+            (
+                (
+                    isinstance(node.func, ast.Name) and
+                    node.func.id == 'abspath' and
+                    node.func.id in state.from_imports['os.path']
+                ) or
+                (
+                    isinstance(node.func, ast.Attribute) and
+                    isinstance(node.func.value, ast.Attribute) and
+                    isinstance(node.func.value.value, ast.Name) and
+                    node.func.value.value.id == 'os' and
+                    node.func.value.attr == 'path' and
+                    node.func.attr == 'abspath'
+                )
+            ) and
+            len(node.args) == 1 and
+            isinstance(node.args[0], ast.Name) and
+            node.args[0].id == '__file__'
+    ):
+        yield ast_to_offset(node), _remove_abspath


Test patch:
diff --git a/tests/features/abspath_file_test.py b/tests/features/abspath_file_test.py
new file mode 100644
index 0000000..dabbaa2
--- /dev/null
+++ b/tests/features/abspath_file_test.py
@@ -0,0 +1,52 @@
+import pytest
+
+from pyupgrade._data import Settings
+from pyupgrade._main import _fix_plugins
+
+
+@pytest.mark.parametrize(
+    ('s', 'expected'),
+    (
+        pytest.param(
+            'from os.path import abspath\n'
+            'abspath(__file__)',
+            'from os.path import abspath\n'
+            '__file__',
+            id='abspath',
+        ),
+        pytest.param(
+            'import os\n'
+            'os.path.abspath(__file__)',
+            'import os\n'
+            '__file__',
+            id='os.path.abspath',
+        ),
+    ),
+)
+def test_fix_abspath_file(s, expected):
+    ret = _fix_plugins(s, settings=Settings(min_version=(3, 9)))
+    assert ret == expected
+
+
+@pytest.mark.parametrize(
+    's, min_version',
+    (
+        pytest.param(
+            'abspath(__file__)',
+            (3, 8),
+            id='Not Python3.9+',
+        ),
+        pytest.param(
+            'os.path.abspath(file)',
+            (3, 9),
+            id='Abspath of not-__file__',
+        ),
+        pytest.param(
+            'os.path.abspath(file, foo)',
+            (3, 9),
+            id='Garbage (don\'t rewrite)',
+        ),
+    ),
+)
+def test_fix_abspath_file_noop(s, min_version):
+    assert _fix_plugins(s, settings=Settings(min_version=min_version)) == s