diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index a22acc8..e47b69c 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -10,6 +10,10 @@ this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ## [Unreleased] +### Fixed + +- [Running Pycln in a virtual env against a folder (that has no `__init__.py` file) contains sub-packages causes ReadPermissionError by @hadialqattan](https://github.com/hadialqattan/pycln/pull/215) + ## [2.2.0] - 2023-07-31 ### Changed diff --git a/pycln/utils/_exceptions.py b/pycln/utils/_exceptions.py index 373a83a..0a64c69 100644 --- a/pycln/utils/_exceptions.py +++ b/pycln/utils/_exceptions.py @@ -24,6 +24,12 @@ class WritePermissionError(BaseOSError): """Raises when the file does not have write permission.""" +class InitFileDoesNotExistError(BaseOSError): + + """Raises when an `__init__.py` file path encountered of a non-existing + path.""" + + class UnexpandableImportStar(Exception): """Raises when the import `*` statement unexpandable.""" diff --git a/pycln/utils/iou.py b/pycln/utils/iou.py index a53b82f..858413b 100644 --- a/pycln/utils/iou.py +++ b/pycln/utils/iou.py @@ -6,7 +6,12 @@ from pathlib import Path from typing import List, Tuple -from ._exceptions import ReadPermissionError, UnparsableFile, WritePermissionError +from ._exceptions import ( + InitFileDoesNotExistError, + ReadPermissionError, + UnparsableFile, + WritePermissionError, +) # Constants. STDIN_FILE = Path("STDIN") @@ -14,6 +19,7 @@ FORM_FEED_CHAR = "\x0c" CRLF = "\r\n" LF = "\n" +__INIT__ = "__init__.py" # Types FileContent = str @@ -61,7 +67,13 @@ def safe_read( and the source does not have write permission. :raises UnparsableFile: If both a BOM and a cookie are present, but disagree. or some rare characters presented. + :raises InitFileDoesNotExistError: when `path` is a path to a non-existing + `__init__.py` file. """ + # Check for a non-existing `__init__.py` file case. + if str(path).endswith(__INIT__) and not path.exists(): + raise InitFileDoesNotExistError(2, "`__init__.py` file does not exist", path) + # Check these permissions before openinig the file. for permission in permissions: if not os.access(path, permission): diff --git a/pycln/utils/refactor.py b/pycln/utils/refactor.py index e41cd21..c42ef53 100644 --- a/pycln/utils/refactor.py +++ b/pycln/utils/refactor.py @@ -8,6 +8,7 @@ from .. import ISWIN from . import iou, pathu, regexu, scan from ._exceptions import ( + InitFileDoesNotExistError, ReadPermissionError, UnexpandableImportStar, UnparsableFile, @@ -175,6 +176,8 @@ def session(self, path: Path) -> None: UnparsableFile, ) as err: self.reporter.failure(str(err)) + except InitFileDoesNotExistError: + pass finally: self._reset() diff --git a/tests/test_iou.py b/tests/test_iou.py index ba65d1c..5492f94 100644 --- a/tests/test_iou.py +++ b/tests/test_iou.py @@ -1,6 +1,7 @@ """pycln/utils/iou.py tests.""" # pylint: disable=R0201,W0613 import os +from pathlib import Path from typing import List from unittest import mock @@ -10,6 +11,7 @@ from pycln import ISWIN from pycln.utils import iou from pycln.utils._exceptions import ( + InitFileDoesNotExistError, ReadPermissionError, UnparsableFile, WritePermissionError, @@ -76,9 +78,10 @@ def test_read_stdin( raise sysu.Pass() @pytest.mark.parametrize( - "content, expec_code, expec_newline, expec_err, chmod", + "file_path, content, expec_code, expec_newline, expec_err, chmod", [ pytest.param( + None, # None means using the generated temp file path. "print('Hello')", "print('Hello')", iou.LF, @@ -87,6 +90,16 @@ def test_read_stdin( id="bast case", ), pytest.param( + "DoesNotExistInit/__init__.py", + None, + None, + None, + InitFileDoesNotExistError, + 0o0644, + id="Init file does not exist", + ), + pytest.param( + None, "code...", None, None, @@ -98,6 +111,7 @@ def test_read_stdin( ), ), pytest.param( + None, "code...", None, None, @@ -109,6 +123,7 @@ def test_read_stdin( ), ), pytest.param( + None, #: Make conflict between BOM and encoding Cookie. #: For more information: https://bit.ly/32o3eVl "\ufeff\n# -*- coding: utf-32 -*-\nbad encoding", @@ -119,6 +134,7 @@ def test_read_stdin( id="bad encoding", ), pytest.param( + None, "try: pass\x0c;\nfinally: pass", None, None, @@ -127,6 +143,7 @@ def test_read_stdin( id="form feed char", ), pytest.param( + None, "print('Hello')\r\n", "print('Hello')\n\n", iou.CRLF, @@ -135,6 +152,7 @@ def test_read_stdin( id="detect CRLF", ), pytest.param( + None, "print('Hello')\n", "print('Hello')\n", iou.LF, @@ -148,9 +166,17 @@ def test_read_stdin( ], ) def test_safe_read( - self, content: str, expec_code: str, expec_newline: str, expec_err, chmod: int + self, + file_path: str, + content: str, + expec_code: str, + expec_newline: str, + expec_err, + chmod: int, ): with pytest.raises(expec_err): + if file_path: + iou.safe_read(Path(file_path)) if expec_newline: content = content.replace(os.linesep, expec_newline) with sysu.reopenable_temp_file(content) as tmp_path: diff --git a/tests/test_refactor.py b/tests/test_refactor.py index 22865e4..2bd8fe1 100644 --- a/tests/test_refactor.py +++ b/tests/test_refactor.py @@ -9,6 +9,7 @@ from pycln.utils import config, iou, refactor, report from pycln.utils._exceptions import ( + InitFileDoesNotExistError, ReadPermissionError, UnexpandableImportStar, UnparsableFile, @@ -302,6 +303,13 @@ def test_remove_useless_passes(self, source_lines, expec_lines): UnparsableFile(Path(""), SyntaxError("")), id="UnparsableFile[code-session]", ), + pytest.param( + Path("DoesNotExistInitFile/__init__.py"), + None, + InitFileDoesNotExistError(2, "", Path("")), + None, + id="InitFileDoesNotExistError[file]", + ), ], ) @mock.patch(MOCK % "Refactor._output")