Skip to content

os-chmod (PTH101) fix incorrect with indirect file descriptors #24895

Description

@gucci-on-fleek

Summary

If a file descriptor (int) is used as the first argument of os.chmod, and the file descriptor isn't explicitly/locally annotated as an int, then the PTH101 fix incorrectly replaces os.chmod with Path.chmod, which only works with file paths (str) and not file descriptors (int), which converts working code into code that raises a TypeError at runtime.

$ ruff --version
ruff 0.15.12

$ cat <<EOF > ./original.py
import os
from tempfile import NamedTemporaryFile

file = NamedTemporaryFile()


class TestName:
    def __init__(self, name: str):
        self.name = name
        os.chmod(name, 0o644)  # Case 1: Fix applied, correct results

    def test_name(self):
        os.chmod(self.name, 0o644)  # Case 2: Fix applied, correct results


TestName(file.name).test_name()


class TestFd:
    def __init__(self, fd: int):
        self.fd = fd
        os.chmod(fd, 0o644)  # Case 3: Fix not applied, correct results

    def test_fd(self):
        os.chmod(self.fd, 0o644)  # Case 4: Fix applied, incorrect results!


TestFd(file.fileno()).test_fd()

print("Done")
EOF

$ python3 ./original.py
Done

$ ruff --isolated --config='lint.select=["PTH101"]' check --preview --no-fix original.py
error[PTH101][*]: `os.chmod()` should be replaced by `Path.chmod()`
  --> original.py:10:9
   |
 8 |     def __init__(self, name: str):
 9 |         self.name = name
10 |         os.chmod(name, 0o644)  # Case 1: Fix applied, correct results
   |         ^^^^^^^^
11 |
12 |     def test_name(self):
   |
help: Replace with `Path(...).chmod(...)`
1  | import os
2  | from tempfile import NamedTemporaryFile
3  + import pathlib
4  |
5  | file = NamedTemporaryFile()
6  |
--------------------------------------------------------------------------------
8  | class TestName:
9  |     def __init__(self, name: str):
10 |         self.name = name
   -         os.chmod(name, 0o644)  # Case 1: Fix applied, correct results
11 +         pathlib.Path(name).chmod(0o644)  # Case 1: Fix applied, correct results
12 |
13 |     def test_name(self):
14 |         os.chmod(self.name, 0o644)  # Case 2: Fix applied, correct results

error[PTH101][*]: `os.chmod()` should be replaced by `Path.chmod()`
  --> original.py:13:9
   |
12 |     def test_name(self):
13 |         os.chmod(self.name, 0o644)  # Case 2: Fix applied, correct results
   |         ^^^^^^^^
   |
help: Replace with `Path(...).chmod(...)`
1  | import os
2  | from tempfile import NamedTemporaryFile
3  + import pathlib
4  |
5  | file = NamedTemporaryFile()
6  |
--------------------------------------------------------------------------------
11 |         os.chmod(name, 0o644)  # Case 1: Fix applied, correct results
12 |
13 |     def test_name(self):
   -         os.chmod(self.name, 0o644)  # Case 2: Fix applied, correct results
14 +         pathlib.Path(self.name).chmod(0o644)  # Case 2: Fix applied, correct results
15 |
16 |
17 | TestName(file.name).test_name()

error[PTH101][*]: `os.chmod()` should be replaced by `Path.chmod()`
  --> original.py:25:9
   |
24 |     def test_fd(self):
25 |         os.chmod(self.fd, 0o644)  # Case 4: Fix applied, incorrect results!
   |         ^^^^^^^^
   |
help: Replace with `Path(...).chmod(...)`
1  | import os
2  | from tempfile import NamedTemporaryFile
3  + import pathlib
4  |
5  | file = NamedTemporaryFile()
6  |
--------------------------------------------------------------------------------
23 |         os.chmod(fd, 0o644)  # Case 3: Fix not applied, correct results
24 |
25 |     def test_fd(self):
   -         os.chmod(self.fd, 0o644)  # Case 4: Fix applied, incorrect results!
26 +         pathlib.Path(self.fd).chmod(0o644)  # Case 4: Fix applied, incorrect results!
27 |
28 |
29 | TestFd(file.fileno()).test_fd()

Found 3 errors.
[*] 3 fixable with the `--fix` option.

$ cp ./original.py ./fixed.py

$ ruff --isolated --config='lint.select=["PTH101"]' check --preview --fix fixed.py
Found 3 errors (3 fixed, 0 remaining).

$ python3 ./fixed.py
Traceback (most recent call last):
  File "./fixed.py", line 29, in <module>
    TestFd(file.fileno()).test_fd()
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^
  File "./fixed.py", line 26, in test_fd
    pathlib.Path(self.fd).chmod(0o644)  # Case 4: Fix applied, incorrect results!
    ~~~~~~~~~~~~^^^^^^^^^
  File "/usr/lib64/python3.14/pathlib/__init__.py", line 150, in __init__
    raise TypeError(
    ...<2 lines>...
        f"not {type(path).__name__!r}")
TypeError: argument should be a str or an os.PathLike object where __fspath__ returns a str, not 'int'

$ cat fixed.py
# Setup code
import os
from tempfile import NamedTemporaryFile
import pathlib

file = NamedTemporaryFile()


class TestName:
    def __init__(self, name: str):
        self.name = name
        pathlib.Path(name).chmod(0o644)  # Case 1: Fix applied, correct results

    def test_name(self):
        pathlib.Path(self.name).chmod(0o644)  # Case 2: Fix applied, correct results


TestName(file.name).test_name()


class TestFd:
    def __init__(self, fd: int):
        self.fd = fd
        os.chmod(fd, 0o644)  # Case 3: Fix not applied, correct results

    def test_fd(self):
        pathlib.Path(self.fd).chmod(0o644)  # Case 4: Fix applied, incorrect results!


TestFd(file.fileno()).test_fd()

print("Done")

Playground

Version

ruff 0.15.12

Metadata

Metadata

Assignees

No one assigned

    Labels

    fixesRelated to suggested fixes for violations

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions