From df9fedc5faa85e99c528fbe694c5db32fe70cbee Mon Sep 17 00:00:00 2001 From: Lie Ryan Date: Tue, 10 Nov 2026 22:26:31 +1100 Subject: [PATCH 01/17] Prefix exception names in projecttest.py For consistency and to avoid shadowing the builtin ModuleNotFoundError. --- ropetest/projecttest.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/ropetest/projecttest.py b/ropetest/projecttest.py index 1f1b37d83..9b948026c 100644 --- a/ropetest/projecttest.py +++ b/ropetest/projecttest.py @@ -5,7 +5,7 @@ from textwrap import dedent from unittest.mock import patch -from rope.base.exceptions import ResourceNotFoundError, RopeError +from rope.base import exceptions from rope.base.fscommands import FileSystemCommands from rope.base.libutils import path_to_resource from rope.base.project import NoProject, Project, _realpath @@ -51,7 +51,7 @@ def test_project_file_reading(self): self.assertEqual("sample text\n", projectFile.read()) def test_getting_not_existing_project_file(self): - with self.assertRaises(ResourceNotFoundError): + with self.assertRaises(exceptions.ResourceNotFoundError): self.project.get_resource("DoesNotExistFile.txt") def test_writing_in_project_files(self): @@ -66,7 +66,7 @@ def test_creating_files(self): self.assertTrue(newFile is not None) def test_creating_files_that_already_exist(self): - with self.assertRaises(RopeError): + with self.assertRaises(exceptions.RopeError): self.project.root.create_file(self.sample_file) def test_making_root_folder_if_it_does_not_exist(self): @@ -85,7 +85,7 @@ def test_failure_when_project_root_exists_and_is_a_file(self): project_root.touch() assert project_root.exists() and project_root.is_file() - with self.assertRaises(RopeError): + with self.assertRaises(exceptions.RopeError): Project(project_root) def test_creating_folders(self): @@ -96,13 +96,13 @@ def test_creating_folders(self): def test_making_folder_that_already_exists(self): folderName = "SampleFolder" - with self.assertRaises(RopeError): + with self.assertRaises(exceptions.RopeError): self.project.root.create_folder(folderName) self.project.root.create_folder(folderName) def test_failing_if_creating_folder_while_file_already_exists(self): folderName = "SampleFolder" - with self.assertRaises(RopeError): + with self.assertRaises(exceptions.RopeError): self.project.root.create_file(folderName) self.project.root.create_folder(folderName) @@ -120,7 +120,7 @@ def test_creating_file_inside_folder(self): ) def test_failing_when_creating_file_inside_non_existent_folder(self): - with self.assertRaises(ResourceNotFoundError): + with self.assertRaises(exceptions.ResourceNotFoundError): self.project.root.create_file("NonexistentFolder/SomeFile.txt") def test_nested_directories(self): @@ -151,7 +151,7 @@ def test_removing_directories(self): ) def test_removing_non_existent_files(self): - with self.assertRaises(ResourceNotFoundError): + with self.assertRaises(exceptions.ResourceNotFoundError): self.project.get_resource("NonExistentFile.txt").remove() def test_removing_nested_files(self): @@ -533,13 +533,13 @@ def test_using_folder_create(self): self.assertTrue(myfolder.is_folder()) def test_exception_when_creating_twice(self): - with self.assertRaises(RopeError): + with self.assertRaises(exceptions.RopeError): myfile = self.project.get_file("myfile.txt") myfile.create() myfile.create() def test_exception_when_parent_does_not_exist(self): - with self.assertRaises(ResourceNotFoundError): + with self.assertRaises(exceptions.ResourceNotFoundError): myfile = self.project.get_file("myfolder/myfile.txt") myfile.create() From 5abc03ef31070fcb1841bdbd007390fdb0884fb9 Mon Sep 17 00:00:00 2001 From: Lie Ryan Date: Tue, 10 Nov 2026 22:30:51 +1100 Subject: [PATCH 02/17] Move test_non_existent_module to projecttest.py get_module() now is part of Project not PyCore. --- ropetest/projecttest.py | 43 +++++++++++++++++++++++++++++++++++++++++ ropetest/pycoretest.py | 4 ---- 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/ropetest/projecttest.py b/ropetest/projecttest.py index 9b948026c..79cba131c 100644 --- a/ropetest/projecttest.py +++ b/ropetest/projecttest.py @@ -631,6 +631,49 @@ def test_file_is_pathlike(self): self.assertIsInstance(os.fspath(resource), str) +class ProjectGetModuleResourceTest(unittest.TestCase): + """ + Test cases for Project methods related to getting files, resources, or + modules: + + - Project.get_resource + - Project.get_file + - Project.get_folder + - Project.get_module + - Project.find_module + - Project.get_relative_module + - Project.find_relative_module + + """ + def setUp(self): + unittest.TestCase.setUp(self) + self.project = testutils.sample_project( + foldername="sampleproject", ropefolder=None + ) + self.project_root = self.project.address + self._make_sample_project() + self.no_project = NoProject() + + def _make_sample_project(self): + self.sample_file = "sample_file.txt" + self.sample_path = os.path.join(self.project_root, "sample_file.txt") + if not os.path.exists(self.project_root): + os.mkdir(self.project_root) + self.sample_folder = "sample_folder" + os.mkdir(os.path.join(self.project_root, self.sample_folder)) + sample = open(self.sample_path, "w") + sample.write("sample text\n") + sample.close() + + def tearDown(self): + testutils.remove_project(self.project) + unittest.TestCase.tearDown(self) + + def test_non_existent_module(self): + with self.assertRaises(exceptions.ModuleNotFoundError): + self.project.get_module("doesnotexistmodule") + + class ResourceObserverTest(unittest.TestCase): def setUp(self): super().setUp() diff --git a/ropetest/pycoretest.py b/ropetest/pycoretest.py index 5ccd80d2a..15c9aa137 100644 --- a/ropetest/pycoretest.py +++ b/ropetest/pycoretest.py @@ -140,10 +140,6 @@ class InnerClass(object): var = sample_class["InnerClass"].get_object() self.assertEqual(get_base_type("Type"), var.get_type()) - def test_non_existent_module(self): - with self.assertRaises(exceptions.ModuleNotFoundError): - self.project.get_module("doesnotexistmodule") - def test_imported_names(self): testutils.create_module(self.project, "mod1") mod = testutils.create_module(self.project, "mod2") From 8cd38b35e2e905352114bd7b4f4ca4e75eb84022 Mon Sep 17 00:00:00 2001 From: Lie Ryan Date: Tue, 10 Nov 2026 22:38:29 +1100 Subject: [PATCH 03/17] Move test_getting_not_existing_project_file() to ProjectGetModuleResourceTest --- ropetest/projecttest.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ropetest/projecttest.py b/ropetest/projecttest.py index 79cba131c..58c85c4fd 100644 --- a/ropetest/projecttest.py +++ b/ropetest/projecttest.py @@ -50,10 +50,6 @@ def test_project_file_reading(self): projectFile = self.project.get_resource(self.sample_file) self.assertEqual("sample text\n", projectFile.read()) - def test_getting_not_existing_project_file(self): - with self.assertRaises(exceptions.ResourceNotFoundError): - self.project.get_resource("DoesNotExistFile.txt") - def test_writing_in_project_files(self): project_file = self.project.get_resource(self.sample_file) project_file.write("another text\n") @@ -673,6 +669,10 @@ def test_non_existent_module(self): with self.assertRaises(exceptions.ModuleNotFoundError): self.project.get_module("doesnotexistmodule") + def test_getting_not_existing_project_file(self): + with self.assertRaises(exceptions.ResourceNotFoundError): + self.project.get_resource("DoesNotExistFile.txt") + class ResourceObserverTest(unittest.TestCase): def setUp(self): From 5bf00291cba5ba21663f2ad74c4753e7f8bb7849 Mon Sep 17 00:00:00 2001 From: Lie Ryan Date: Tue, 10 Nov 2026 22:45:40 +1100 Subject: [PATCH 04/17] Convert ProjectGetModuleResourceTest into pytest-based TestProjectGetModuleResource --- ropetest/projecttest.py | 38 +++++++++----------------------------- 1 file changed, 9 insertions(+), 29 deletions(-) diff --git a/ropetest/projecttest.py b/ropetest/projecttest.py index 58c85c4fd..f939db0c8 100644 --- a/ropetest/projecttest.py +++ b/ropetest/projecttest.py @@ -5,6 +5,8 @@ from textwrap import dedent from unittest.mock import patch +import pytest + from rope.base import exceptions from rope.base.fscommands import FileSystemCommands from rope.base.libutils import path_to_resource @@ -627,7 +629,7 @@ def test_file_is_pathlike(self): self.assertIsInstance(os.fspath(resource), str) -class ProjectGetModuleResourceTest(unittest.TestCase): +class TestProjectGetModuleResource: """ Test cases for Project methods related to getting files, resources, or modules: @@ -641,37 +643,15 @@ class ProjectGetModuleResourceTest(unittest.TestCase): - Project.find_relative_module """ - def setUp(self): - unittest.TestCase.setUp(self) - self.project = testutils.sample_project( - foldername="sampleproject", ropefolder=None - ) - self.project_root = self.project.address - self._make_sample_project() - self.no_project = NoProject() - - def _make_sample_project(self): - self.sample_file = "sample_file.txt" - self.sample_path = os.path.join(self.project_root, "sample_file.txt") - if not os.path.exists(self.project_root): - os.mkdir(self.project_root) - self.sample_folder = "sample_folder" - os.mkdir(os.path.join(self.project_root, self.sample_folder)) - sample = open(self.sample_path, "w") - sample.write("sample text\n") - sample.close() - def tearDown(self): - testutils.remove_project(self.project) - unittest.TestCase.tearDown(self) + def test_get_module_non_existent_module(self, project): + with pytest.raises(exceptions.ModuleNotFoundError): + project.get_module("doesnotexistmodule") - def test_non_existent_module(self): - with self.assertRaises(exceptions.ModuleNotFoundError): - self.project.get_module("doesnotexistmodule") + def test_get_resource_getting_not_existing_project_file(self, project): + with pytest.raises(exceptions.ResourceNotFoundError): + project.get_resource("DoesNotExistFile.txt") - def test_getting_not_existing_project_file(self): - with self.assertRaises(exceptions.ResourceNotFoundError): - self.project.get_resource("DoesNotExistFile.txt") class ResourceObserverTest(unittest.TestCase): From 09de14f3fb3f307fb3923e38eb427fd45240acda Mon Sep 17 00:00:00 2001 From: Lie Ryan Date: Tue, 10 Nov 2026 23:44:25 +1100 Subject: [PATCH 05/17] Add test coverages for getting non-existent resources/modules Also make sure that there are tests for the exception messages. --- rope/base/project.py | 8 ++++---- ropetest/conftest.py | 6 ++++++ ropetest/projecttest.py | 43 +++++++++++++++++++++++++++++++++++++++-- 3 files changed, 51 insertions(+), 6 deletions(-) diff --git a/rope/base/project.py b/rope/base/project.py index c7f6a2f8e..319dbb118 100644 --- a/rope/base/project.py +++ b/rope/base/project.py @@ -55,14 +55,14 @@ def get_resource(self, resource_name: str) -> Resource: path = self._get_resource_path(resource_name) if not os.path.exists(path): raise exceptions.ResourceNotFoundError( - "Resource <%s> does not exist" % resource_name + f"Resource <{resource_name}> does not exist" ) elif os.path.isfile(path): return File(self, resource_name) elif os.path.isdir(path): return Folder(self, resource_name) else: - raise exceptions.ResourceNotFoundError("Unknown resource " + resource_name) + raise exceptions.ResourceNotFoundError(f"Unknown resource {resource_name}") def get_python_path_folders(self) -> list[Folder]: result = [] @@ -132,7 +132,7 @@ def get_relative_module( ) -> pyobjectsdef.PyPackage | pyobjectsdef.PyModule: module = self.find_relative_module(name, folder, level) if module is None: - raise ModuleNotFoundError("Module %s not found" % name) + raise ModuleNotFoundError(f"Module {name} not found") return self.pycore.resource_to_pyobject(module) def find_relative_module( @@ -284,7 +284,7 @@ def get_module( return pymod module = self.find_module(name, folder) if module is None: - raise ModuleNotFoundError("Module %s not found" % name) + raise ModuleNotFoundError(f"Module {name} not found") return self.pycore.resource_to_pyobject(module) def get_files(self) -> list[Resource]: diff --git a/ropetest/conftest.py b/ropetest/conftest.py index 99ec1b76b..9033a519f 100644 --- a/ropetest/conftest.py +++ b/ropetest/conftest.py @@ -5,6 +5,7 @@ import pytest +import rope.base.project from rope.base import resources from ropetest import testutils @@ -47,6 +48,11 @@ def session_venv_python_executable(session_venv): yield python_executable +@pytest.fixture +def no_project(): + return rope.base.project.get_no_project() + + @pytest.fixture def project(session_venv, session_venv_site_packages): project = testutils.sample_project( diff --git a/ropetest/projecttest.py b/ropetest/projecttest.py index f939db0c8..77d3992db 100644 --- a/ropetest/projecttest.py +++ b/ropetest/projecttest.py @@ -644,14 +644,53 @@ class TestProjectGetModuleResource: """ + @pytest.fixture(params=["devnull", "mkfifo"]) + def special_file(self, request, project) -> pathlib.Path: + match request.param: + case "devnull": + return pathlib.Path(os.devnull) + case "mkfifo": + fifo_path = project.address + "/fifo" + os.mkfifo(fifo_path) + return pathlib.Path(fifo_path) + def test_get_module_non_existent_module(self, project): - with pytest.raises(exceptions.ModuleNotFoundError): + with pytest.raises( + exceptions.ModuleNotFoundError, + match="Module doesnotexistmodule not found", + ): project.get_module("doesnotexistmodule") def test_get_resource_getting_not_existing_project_file(self, project): - with pytest.raises(exceptions.ResourceNotFoundError): + with pytest.raises( + exceptions.ResourceNotFoundError, + match="Resource does not exist", + ): project.get_resource("DoesNotExistFile.txt") + def test_get_relative_module_non_existent_module(self, project): + src = project.root.create_folder("src") + with pytest.raises( + exceptions.ModuleNotFoundError, + match="Module ..doesnotexistmodule not found", + ): + project.get_relative_module("..doesnotexistmodule", folder=src, level=2) + + def test_no_project_get_resource_special_file(self, no_project, special_file): + with pytest.raises( + exceptions.ResourceNotFoundError, + match=f"Unknown resource {special_file}", + ): + no_project.get_resource(str(special_file)) + + def test_project_get_resource_special_file(self, project, special_file): + with pytest.raises( + exceptions.ResourceNotFoundError, + match=f"Resource <{special_file}> does not exist", + ): + project.get_resource(str(special_file)) + + class ResourceObserverTest(unittest.TestCase): From 48b7078d304626829bcd47ba92489a2eaaada3ad Mon Sep 17 00:00:00 2001 From: Lie Ryan Date: Tue, 10 Nov 2026 23:49:10 +1100 Subject: [PATCH 06/17] Fix non-standard test name --- ropetest/projecttest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ropetest/projecttest.py b/ropetest/projecttest.py index 77d3992db..c82a3909b 100644 --- a/ropetest/projecttest.py +++ b/ropetest/projecttest.py @@ -201,7 +201,7 @@ def test_is_dir(self): with patch('rope.base.resources.File.is_folder') as is_folder2: assert self.project.get_resource(self.sample_file).is_dir() == is_folder2.return_value - def testget_children(self): + def test_get_children(self): children = self.project.get_resource(self.sample_folder).get_children() self.assertEqual([], children) From 5d5fd209a84bfa5f72bd70e1f0618917e3d74e38 Mon Sep 17 00:00:00 2001 From: Lie Ryan Date: Wed, 11 Nov 2026 00:59:12 +1100 Subject: [PATCH 07/17] Add basic tests for Project.get_resource() Also move and improve ProjectTest.test_getting_project_file() -> TestProjectGetModuleResource.test_get_resource_getting_existing_project_file() --- ropetest/projecttest.py | 40 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/ropetest/projecttest.py b/ropetest/projecttest.py index c82a3909b..f8533c5be 100644 --- a/ropetest/projecttest.py +++ b/ropetest/projecttest.py @@ -27,6 +27,11 @@ def setUp(self): self.no_project = NoProject() def _make_sample_project(self): + """ + Sample project structure + /sample_file.txt -- "sample text\n" + /sample_folder + """ self.sample_file = "sample_file.txt" self.sample_path = os.path.join(self.project_root, "sample_file.txt") if not os.path.exists(self.project_root): @@ -44,10 +49,6 @@ def tearDown(self): def test_project_creation(self): self.assertEqual(_realpath(self.project_root), self.project.address) - def test_getting_project_file(self): - project_file = self.project.get_resource(self.sample_file) - self.assertTrue(project_file is not None) - def test_project_file_reading(self): projectFile = self.project.get_resource(self.sample_file) self.assertEqual("sample text\n", projectFile.read()) @@ -644,6 +645,23 @@ class TestProjectGetModuleResource: """ + @pytest.fixture + def sample_project(self, project): + """ + Sample project structure + /sample_file.txt -- "sample text\n" + /sample_folder + """ + project_root = project.address + sample_file = "sample_file.txt" + sample_path = os.path.join(project_root, "sample_file.txt") + if not os.path.exists(project_root): + os.mkdir(project_root) + sample_folder = "sample_folder" + os.mkdir(os.path.join(project_root, sample_folder)) + pathlib.Path(sample_path).write_text("sample text\n") + return project + @pytest.fixture(params=["devnull", "mkfifo"]) def special_file(self, request, project) -> pathlib.Path: match request.param: @@ -690,6 +708,20 @@ def test_project_get_resource_special_file(self, project, special_file): ): project.get_resource(str(special_file)) + def test_get_resource_getting_existing_project_file(self, sample_project): + project_file = sample_project.get_resource("sample_file.txt") + assert isinstance(project_file, File) + + def test_get_resource_getting_existing_project_folder(self, sample_project): + project_file = sample_project.get_resource("sample_folder") + assert isinstance(project_file, Folder) + + def test_get_resource_getting_existing_resource_with_slash(self, sample_project): + # for project files, slashes are interpreted as relative to project directory + project_file = sample_project.get_resource("/sample_file.txt") + assert isinstance(project_file, File) + assert project_file.pathlib.is_relative_to(sample_project.address) + From c011e1ece615522fff0320e502ed9becec271874 Mon Sep 17 00:00:00 2001 From: Lie Ryan Date: Wed, 11 Nov 2026 04:24:41 +1100 Subject: [PATCH 08/17] Add more tests defining `Project.get_resource()` behavior --- ropetest/projecttest.py | 49 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 46 insertions(+), 3 deletions(-) diff --git a/ropetest/projecttest.py b/ropetest/projecttest.py index f8533c5be..d2817f907 100644 --- a/ropetest/projecttest.py +++ b/ropetest/projecttest.py @@ -671,6 +671,15 @@ def special_file(self, request, project) -> pathlib.Path: fifo_path = project.address + "/fifo" os.mkfifo(fifo_path) return pathlib.Path(fifo_path) + case _: + assert False + + @pytest.fixture + def file_outside_project(self, project): + path = "../sample_file.txt" + file = pathlib.Path(project.address) / path + file.touch() + return path def test_get_module_non_existent_module(self, project): with pytest.raises( @@ -716,13 +725,47 @@ def test_get_resource_getting_existing_project_folder(self, sample_project): project_file = sample_project.get_resource("sample_folder") assert isinstance(project_file, Folder) - def test_get_resource_getting_existing_resource_with_slash(self, sample_project): - # for project files, slashes are interpreted as relative to project directory + def test_project_get_resource_getting_existing_resource_with_slash( + self, sample_project + ): + """ + for Project.get_resource(), starting slashes are interpreted as + relative to project directory + """ project_file = sample_project.get_resource("/sample_file.txt") assert isinstance(project_file, File) assert project_file.pathlib.is_relative_to(sample_project.address) - + with tempfile.NamedTemporaryFile() as tmp: + assert tmp.name.startswith("/") + with pytest.raises( + exceptions.ResourceNotFoundError, + match="Resource <.*> does not exist", + ): + project_file = sample_project.get_resource(tmp.name) + + def test_no_project_get_resource_getting_existing_resource_with_slash( + self, no_project, file_outside_project + ): + """ + for NoProject.get_resource(), starting slashes are interpreted as + absolute path relative to project root. NoProject should be used to get + resources for files/folders that's not part of a project. + """ + with tempfile.NamedTemporaryFile() as tmp: + assert tmp.name.startswith("/") + project_file = no_project.get_resource(tmp.name) + assert isinstance(project_file, File) + assert project_file.exists() + + def test_get_resource_getting_existing_resource_with_double_dot_disable_projectroot_check( + self, sample_project, file_outside_project + ): + assert file_outside_project == "../sample_file.txt" + resource = sample_project.get_resource(file_outside_project) + assert isinstance(resource, File) + assert resource.path == "../sample_file.txt" + assert resource.real_path.endswith("sample_project/../sample_file.txt") class ResourceObserverTest(unittest.TestCase): From cc9f508921dee603a75fb29aec2359ff2607be25 Mon Sep 17 00:00:00 2001 From: Lie Ryan Date: Wed, 11 Nov 2026 06:49:59 +1100 Subject: [PATCH 09/17] Split TestProjectGetResource and TestProjectGetModule --- ropetest/projecttest.py | 52 +++++++++++++++++++++++------------------ 1 file changed, 29 insertions(+), 23 deletions(-) diff --git a/ropetest/projecttest.py b/ropetest/projecttest.py index d2817f907..3fce403fe 100644 --- a/ropetest/projecttest.py +++ b/ropetest/projecttest.py @@ -630,19 +630,14 @@ def test_file_is_pathlike(self): self.assertIsInstance(os.fspath(resource), str) -class TestProjectGetModuleResource: +class TestProjectGetResource: """ - Test cases for Project methods related to getting files, resources, or - modules: + Test cases for Project methods related to getting files/folders: - Project.get_resource - Project.get_file - Project.get_folder - - Project.get_module - - Project.find_module - - Project.get_relative_module - - Project.find_relative_module - + - libutils.path_to_resource """ @pytest.fixture @@ -681,13 +676,6 @@ def file_outside_project(self, project): file.touch() return path - def test_get_module_non_existent_module(self, project): - with pytest.raises( - exceptions.ModuleNotFoundError, - match="Module doesnotexistmodule not found", - ): - project.get_module("doesnotexistmodule") - def test_get_resource_getting_not_existing_project_file(self, project): with pytest.raises( exceptions.ResourceNotFoundError, @@ -695,14 +683,6 @@ def test_get_resource_getting_not_existing_project_file(self, project): ): project.get_resource("DoesNotExistFile.txt") - def test_get_relative_module_non_existent_module(self, project): - src = project.root.create_folder("src") - with pytest.raises( - exceptions.ModuleNotFoundError, - match="Module ..doesnotexistmodule not found", - ): - project.get_relative_module("..doesnotexistmodule", folder=src, level=2) - def test_no_project_get_resource_special_file(self, no_project, special_file): with pytest.raises( exceptions.ResourceNotFoundError, @@ -768,6 +748,32 @@ def test_get_resource_getting_existing_resource_with_double_dot_disable_projectr assert resource.real_path.endswith("sample_project/../sample_file.txt") +class TestProjectGetModule: + """ + Test cases for Project methods related to getting modules: + + - Project.get_module + - Project.find_module + - Project.get_relative_module + - Project.find_relative_module + """ + + def test_get_module_non_existent_module(self, project): + with pytest.raises( + exceptions.ModuleNotFoundError, + match="Module doesnotexistmodule not found", + ): + project.get_module("doesnotexistmodule") + + def test_get_relative_module_non_existent_module(self, project): + src = project.root.create_folder("src") + with pytest.raises( + exceptions.ModuleNotFoundError, + match="Module ..doesnotexistmodule not found", + ): + project.get_relative_module("..doesnotexistmodule", folder=src, level=2) + + class ResourceObserverTest(unittest.TestCase): def setUp(self): super().setUp() From 1c76c7ae8036e032df4b784c274ae812351b065f Mon Sep 17 00:00:00 2001 From: Lie Ryan Date: Wed, 11 Nov 2026 07:13:25 +1100 Subject: [PATCH 10/17] Remove unused no_project in ProjectTest --- ropetest/projecttest.py | 1 - 1 file changed, 1 deletion(-) diff --git a/ropetest/projecttest.py b/ropetest/projecttest.py index 3fce403fe..563974074 100644 --- a/ropetest/projecttest.py +++ b/ropetest/projecttest.py @@ -24,7 +24,6 @@ def setUp(self): ) self.project_root = self.project.address self._make_sample_project() - self.no_project = NoProject() def _make_sample_project(self): """ From e1d78a192ef99604c354128ec29f3c3b2ece6f25 Mon Sep 17 00:00:00 2001 From: Lie Ryan Date: Wed, 11 Nov 2026 07:11:59 +1100 Subject: [PATCH 11/17] Update ProjectTest to pytest-style TestCase --- ropetest/projecttest.py | 65 +++++++++++++++++------------------------ 1 file changed, 27 insertions(+), 38 deletions(-) diff --git a/ropetest/projecttest.py b/ropetest/projecttest.py index 563974074..0b2e9748a 100644 --- a/ropetest/projecttest.py +++ b/ropetest/projecttest.py @@ -16,34 +16,40 @@ from ropetest import testutils +@pytest.fixture +def sample_project(project): + """ + Sample project structure + /sample_file.txt -- "sample text\n" + /sample_folder + """ + project_root = project.address + sample_file = "sample_file.txt" + sample_path = os.path.join(project_root, "sample_file.txt") + if not os.path.exists(project_root): + os.mkdir(project_root) + sample_folder = "sample_folder" + os.mkdir(os.path.join(project_root, sample_folder)) + pathlib.Path(sample_path).write_text("sample text\n") + return project + + class ProjectTest(unittest.TestCase): - def setUp(self): - unittest.TestCase.setUp(self) - self.project = testutils.sample_project( - foldername="sampleproject", ropefolder=None - ) + @pytest.fixture(autouse=True) + def setup(self, sample_project): + self.project = sample_project self.project_root = self.project.address - self._make_sample_project() - - def _make_sample_project(self): - """ - Sample project structure - /sample_file.txt -- "sample text\n" - /sample_folder - """ self.sample_file = "sample_file.txt" self.sample_path = os.path.join(self.project_root, "sample_file.txt") - if not os.path.exists(self.project_root): - os.mkdir(self.project_root) self.sample_folder = "sample_folder" - os.mkdir(os.path.join(self.project_root, self.sample_folder)) - sample = open(self.sample_path, "w") - sample.write("sample text\n") - sample.close() - def tearDown(self): + @pytest.fixture + def project(self): + project = testutils.sample_project( + foldername="sampleproject", ropefolder=None + ) + yield project testutils.remove_project(self.project) - unittest.TestCase.tearDown(self) def test_project_creation(self): self.assertEqual(_realpath(self.project_root), self.project.address) @@ -639,23 +645,6 @@ class TestProjectGetResource: - libutils.path_to_resource """ - @pytest.fixture - def sample_project(self, project): - """ - Sample project structure - /sample_file.txt -- "sample text\n" - /sample_folder - """ - project_root = project.address - sample_file = "sample_file.txt" - sample_path = os.path.join(project_root, "sample_file.txt") - if not os.path.exists(project_root): - os.mkdir(project_root) - sample_folder = "sample_folder" - os.mkdir(os.path.join(project_root, sample_folder)) - pathlib.Path(sample_path).write_text("sample text\n") - return project - @pytest.fixture(params=["devnull", "mkfifo"]) def special_file(self, request, project) -> pathlib.Path: match request.param: From 02b073810f32bad03afe923cc47f5a59f03bc34b Mon Sep 17 00:00:00 2001 From: Lie Ryan Date: Wed, 11 Nov 2026 07:42:01 +1100 Subject: [PATCH 12/17] Change default project fixture to not create .ropefolder --- ropetest/conftest.py | 1 + ropetest/contrib/autoimport/conftest.py | 9 +++++++++ 2 files changed, 10 insertions(+) diff --git a/ropetest/conftest.py b/ropetest/conftest.py index 9033a519f..7ad7e4caa 100644 --- a/ropetest/conftest.py +++ b/ropetest/conftest.py @@ -57,6 +57,7 @@ def no_project(): def project(session_venv, session_venv_site_packages): project = testutils.sample_project( python_path=[str(session_venv_site_packages)], + ropefolder=None, ) yield project testutils.remove_project(project) diff --git a/ropetest/contrib/autoimport/conftest.py b/ropetest/contrib/autoimport/conftest.py index 9dca8ccf5..7a667d068 100644 --- a/ropetest/contrib/autoimport/conftest.py +++ b/ropetest/contrib/autoimport/conftest.py @@ -5,6 +5,15 @@ from ropetest import testutils +@pytest.fixture +def project(session_venv, session_venv_site_packages): + project = testutils.sample_project( + python_path=[str(session_venv_site_packages)], + ) + yield project + testutils.remove_project(project) + + @pytest.fixture def mod1(project): mod1 = testutils.create_module(project, "mod1") From 088d86a88a0aa73dce20c724fff547d2e382cf79 Mon Sep 17 00:00:00 2001 From: Lie Ryan Date: Wed, 11 Nov 2026 08:46:41 +1100 Subject: [PATCH 13/17] Move test_simple_path_to_resource to TestProjectGetResource --- ropetest/projecttest.py | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/ropetest/projecttest.py b/ropetest/projecttest.py index 0b2e9748a..3fa65685d 100644 --- a/ropetest/projecttest.py +++ b/ropetest/projecttest.py @@ -547,18 +547,6 @@ def test_exception_when_parent_does_not_exist(self): myfile = self.project.get_file("myfolder/myfile.txt") myfile.create() - def test_simple_path_to_resource(self): - myfile = self.project.root.create_file("myfile.txt") - self.assertEqual(myfile, path_to_resource(self.project, myfile.real_path)) - self.assertEqual( - myfile, path_to_resource(self.project, myfile.real_path, type="file") - ) - myfolder = self.project.root.create_folder("myfolder") - self.assertEqual(myfolder, path_to_resource(self.project, myfolder.real_path)) - self.assertEqual( - myfolder, path_to_resource(self.project, myfolder.real_path, type="folder") - ) - @testutils.skipNotPOSIX() def test_ignoring_symlinks_inside_project(self): project2 = testutils.sample_project(folder_name="sampleproject2") @@ -735,6 +723,16 @@ def test_get_resource_getting_existing_resource_with_double_dot_disable_projectr assert resource.path == "../sample_file.txt" assert resource.real_path.endswith("sample_project/../sample_file.txt") + def test_simple_path_to_resource(self, sample_project): + myfile = sample_project.root.create_file("myfile.txt") + assert myfile == path_to_resource(sample_project, myfile.real_path) + assert myfile == path_to_resource(sample_project, myfile.real_path, type="file") + + myfolder = sample_project.root.create_folder("myfolder") + assert myfolder == path_to_resource(sample_project, myfolder.real_path) + assert myfolder == path_to_resource(sample_project, myfolder.real_path, type="folder") + + class TestProjectGetModule: """ From 2392165590b914ba1cc6c58c27cbb4a933236fb0 Mon Sep 17 00:00:00 2001 From: Lie Ryan Date: Wed, 11 Nov 2026 09:32:01 +1100 Subject: [PATCH 14/17] Split ProjectTest into logical chunks --- ropetest/projecttest.py | 871 +++++++++++++++++++++++----------------- 1 file changed, 513 insertions(+), 358 deletions(-) diff --git a/ropetest/projecttest.py b/ropetest/projecttest.py index 3fa65685d..6429f3cca 100644 --- a/ropetest/projecttest.py +++ b/ropetest/projecttest.py @@ -34,7 +34,7 @@ def sample_project(project): return project -class ProjectTest(unittest.TestCase): +class SetupSampleProject: @pytest.fixture(autouse=True) def setup(self, sample_project): self.project = sample_project @@ -51,28 +51,11 @@ def project(self): yield project testutils.remove_project(self.project) + +class ProjectTest(SetupSampleProject, unittest.TestCase): def test_project_creation(self): self.assertEqual(_realpath(self.project_root), self.project.address) - def test_project_file_reading(self): - projectFile = self.project.get_resource(self.sample_file) - self.assertEqual("sample text\n", projectFile.read()) - - def test_writing_in_project_files(self): - project_file = self.project.get_resource(self.sample_file) - project_file.write("another text\n") - self.assertEqual("another text\n", project_file.read()) - - def test_creating_files(self): - project_file = "newfile.txt" - self.project.root.create_file(project_file) - newFile = self.project.get_resource(project_file) - self.assertTrue(newFile is not None) - - def test_creating_files_that_already_exist(self): - with self.assertRaises(exceptions.RopeError): - self.project.root.create_file(self.sample_file) - def test_making_root_folder_if_it_does_not_exist(self): with tempfile.TemporaryDirectory(dir=testutils.RUN_TMP_DIR) as tmpdir: project_root = pathlib.Path(tmpdir) / "sampleproject2" @@ -92,9 +75,242 @@ def test_failure_when_project_root_exists_and_is_a_file(self): with self.assertRaises(exceptions.RopeError): Project(project_root) + def test_project_root_is_root_folder(self): + self.assertEqual("", self.project.root.path) + + ################################ + # Project.get_source_folders() # + ################################ + + def test_getting_empty_source_folders(self): + self.assertEqual([], self.project.get_source_folders()) + + def test_root_source_folder(self): + self.project.root.create_file("sample.py") + source_folders = self.project.get_source_folders() + self.assertEqual(1, len(source_folders)) + self.assertTrue(self.project.root in source_folders) + + def test_root_source_folder2(self): + self.project.root.create_file("mod1.py") + self.project.root.create_file("mod2.py") + source_folders = self.project.get_source_folders() + self.assertEqual(1, len(source_folders)) + self.assertTrue(self.project.root in source_folders) + + def test_src_source_folder(self): + src = self.project.root.create_folder("src") + src.create_file("sample.py") + source_folders = self.project.get_source_folders() + self.assertEqual(1, len(source_folders)) + self.assertTrue(self.project.get_resource("src") in source_folders) + + def test_packages(self): + src = self.project.root.create_folder("src") + pkg = src.create_folder("package") + pkg.create_file("__init__.py") + source_folders = self.project.get_source_folders() + self.assertEqual(1, len(source_folders)) + self.assertTrue(src in source_folders) + + def test_multi_source_folders(self): + src = self.project.root.create_folder("src") + package = src.create_folder("package") + package.create_file("__init__.py") + test = self.project.root.create_folder("test") + test.create_file("alltests.py") + source_folders = self.project.get_source_folders() + self.assertEqual(2, len(source_folders)) + self.assertTrue(src in source_folders) + self.assertTrue(test in source_folders) + + def test_multi_source_folders2(self): + testutils.create_module(self.project, "mod1") + src = self.project.root.create_folder("src") + package = testutils.create_package(self.project, "package", src) + testutils.create_module(self.project, "mod2", package) + source_folders = self.project.get_source_folders() + self.assertEqual(2, len(source_folders)) + self.assertTrue(self.project.root in source_folders and src in source_folders) + + +class FileTest(SetupSampleProject, unittest.TestCase): + """ + Test cases for basic file content operations: + + - File.read + - File.read_bytes + - File.write + """ + + def test_project_file_reading(self): + projectFile = self.project.get_resource(self.sample_file) + self.assertEqual("sample text\n", projectFile.read()) + + def test_writing_in_project_files(self): + project_file = self.project.get_resource(self.sample_file) + project_file.write("another text\n") + self.assertEqual("another text\n", project_file.read()) + + def test_file_encoding_reading(self): + sample_file = self.project.root.create_file("my_file.txt") + contents = ( + b"# -*- coding: utf-8 -*-\n" + + br"#\N{LATIN SMALL LETTER I WITH DIAERESIS}\n" + ).decode("utf8") + file = open(sample_file.real_path, "wb") + file.write(contents.encode("utf-8")) + file.close() + self.assertEqual(contents, sample_file.read()) + + def test_file_encoding_writing(self): + sample_file = self.project.root.create_file("my_file.txt") + contents = ( + b"# -*- coding: utf-8 -*-\n" + br"\N{LATIN SMALL LETTER I WITH DIAERESIS}\n" + ).decode("utf8") + sample_file.write(contents) + self.assertEqual(contents, sample_file.read()) + + def test_using_utf8_when_writing_in_case_of_errors(self): + sample_file = self.project.root.create_file("my_file.txt") + contents = br"\n\N{LATIN SMALL LETTER I WITH DIAERESIS}\n".decode("utf8") + sample_file.write(contents) + self.assertEqual(contents, sample_file.read()) + + def test_encoding_declaration_in_the_second_line(self): + sample_file = self.project.root.create_file("my_file.txt") + contents = b"\n# -*- coding: latin-1 -*-\n\xa9\n" + file = open(sample_file.real_path, "wb") + file.write(contents) + file.close() + self.assertEqual(contents, sample_file.read().encode("latin-1")) + + def test_not_an_encoding_declaration(self): + sample_file = self.project.root.create_file("my_file.txt") + contents = b"def my_method(self, encoding='latin-1'):\n var = {}\n\xc2\xa9\n" + file = open(sample_file.real_path, "wb") + file.write(contents) + file.close() + self.assertEqual(contents, sample_file.read().encode("utf-8")) + self.assertNotEqual(contents, sample_file.read().encode("latin-1")) + + def test_read_bytes(self): + sample_file = self.project.root.create_file("my_file.txt") + contents = b"\n# -*- coding: latin-1 -*-\n\xa9\n" + file = open(sample_file.real_path, "wb") + file.write(contents) + file.close() + self.assertEqual(contents, sample_file.read_bytes()) + + def test_file_with_unix_line_ending(self): + sample_file = self.project.root.create_file("my_file.txt") + contents = b"1\n" + file = open(sample_file.real_path, "wb") + file.write(contents) + file.close() + self.assertIsNone(sample_file.newlines) + self.assertEqual("1\n", sample_file.read()) + self.assertEqual("\n", sample_file.newlines) + + sample_file.write("1\n") + self.assertEqual(b"1\n", sample_file.read_bytes()) + + def test_file_with_dos_line_ending(self): + sample_file = self.project.root.create_file("my_file.txt") + contents = b"1\r\n" + file = open(sample_file.real_path, "wb") + file.write(contents) + file.close() + self.assertIsNone(sample_file.newlines) + self.assertEqual("1\n", sample_file.read()) + self.assertEqual("\r\n", sample_file.newlines) + + sample_file.write("1\n") + self.assertEqual(b"1\r\n", sample_file.read_bytes()) + + def test_file_with_mac_line_ending(self): + sample_file = self.project.root.create_file("my_file.txt") + contents = b"1\r" + file = open(sample_file.real_path, "wb") + file.write(contents) + file.close() + self.assertIsNone(sample_file.newlines) + self.assertEqual("1\n", sample_file.read()) + self.assertEqual("\r", sample_file.newlines) + + sample_file.write("1\n") + self.assertEqual(b"1\r", sample_file.read_bytes()) + + def test_file_binary(self): + sample_file = self.project.root.create_file("my_file.txt") + contents = b"1\r\n" + file = open(sample_file.real_path, "wb") + file.write(contents) + file.close() + self.assertIsNone(sample_file.newlines) + self.assertEqual(b"1\r\n", sample_file.read_bytes()) + self.assertIsNone(sample_file.newlines) + + sample_file.write(b"1\nx\r") + self.assertEqual((b"1\nx\r"), sample_file.read_bytes()) + + # TODO: Detecting utf-16 encoding + def xxx_test_using_utf16(self): + sample_file = self.project.root.create_file("my_file.txt") + contents = b"# -*- coding: utf-16 -*-\n# This is a sample file ...\n" + file = open(sample_file.real_path, "w") + file.write(contents.encode("utf-16")) + file.close() + sample_file.write(contents) + self.assertEqual(contents, sample_file.read()) + + # XXX: supporting utf_8_sig + def xxx_test_file_encoding_reading_for_notepad_styles(self): + sample_file = self.project.root.create_file("my_file.txt") + contents = "#\N{LATIN SMALL LETTER I WITH DIAERESIS}\n" + file = open(sample_file.real_path, "w") + # file.write('\xef\xbb\xbf') + file.write(contents.encode("utf-8-sig")) + file.close() + self.assertEqual(contents, sample_file.read()) + + +class FolderTest(SetupSampleProject, unittest.TestCase): + """ + Test cases for Folder operations: + + - Folder.create_file/create_folder + - File/Folder.create + - File/Folder.remove + - File/Folder.move + - File/Folder metadata (name, path) + - File/Folder introspection methods (is_folder, is_dir) + - Folder.__fspath__ + - Folder.get_children/get_files/get_folders + - Folder.get_child + """ + + #################################### + # Folder.create_file/create_folder # + #################################### + + def test_creating_files(self): + project_file = "newfile.txt" + + self.project.root.create_file(project_file) + + newFile = self.project.get_resource(project_file) + self.assertTrue(newFile is not None) + + def test_creating_files_that_already_exist(self): + with self.assertRaises(exceptions.RopeError): + self.project.root.create_file(self.sample_file) + def test_creating_folders(self): folderName = "SampleFolder" + self.project.root.create_folder(folderName) + folderPath = os.path.join(self.project.address, folderName) self.assertTrue(os.path.exists(folderPath) and os.path.isdir(folderPath)) @@ -114,8 +330,10 @@ def test_creating_file_inside_folder(self): folder_name = "sampleFolder" file_name = "sample2.txt" file_path = folder_name + "/" + file_name + parent_folder = self.project.root.create_folder(folder_name) parent_folder.create_file(file_name) + file = self.project.get_resource(file_path) file.write("sample notes") self.assertEqual(file_path, file.path) @@ -123,33 +341,114 @@ def test_creating_file_inside_folder(self): "sample notes", open(os.path.join(self.project.address, file_path)).read() ) - def test_failing_when_creating_file_inside_non_existent_folder(self): + def test_failing_when_creating_file_inside_non_existent_folder(self): + with self.assertRaises(exceptions.ResourceNotFoundError): + self.project.root.create_file("NonexistentFolder/SomeFile.txt") + + def test_nested_directories(self): + folder_name = "SampleFolder" + + parent = self.project.root.create_folder(folder_name) + parent.create_folder(folder_name) + + folder_path = os.path.join(self.project.address, folder_name, folder_name) + self.assertTrue(os.path.exists(folder_path) and os.path.isdir(folder_path)) + + def test_folder_creating_files(self): + projectFile = "NewFile.txt" + + self.project.root.create_file(projectFile) + + new_file = self.project.get_resource(projectFile) + self.assertTrue(new_file is not None and not new_file.is_folder()) + + def test_folder_creating_nested_files(self): + project_file = "NewFile.txt" + parent_folder = self.project.get_resource(self.sample_folder) + + parent_folder.create_file(project_file) + + new_file = self.project.get_resource(self.sample_folder + "/" + project_file) + self.assertTrue(new_file is not None and not new_file.is_folder()) + + def test_folder_creating_files2(self): + projectFile = "newfolder" + + self.project.root.create_folder(projectFile) + + new_folder = self.project.get_resource(projectFile) + self.assertTrue(new_folder is not None and new_folder.is_folder()) + + def test_folder_creating_nested_files2(self): + project_file = "newfolder" + parent_folder = self.project.get_resource(self.sample_folder) + + parent_folder.create_folder(project_file) + + new_folder = self.project.get_resource(self.sample_folder + "/" + project_file) + self.assertTrue(new_folder is not None and new_folder.is_folder()) + + + ###################### + # File/Folder.create # + ###################### + + def test_using_file_create(self): + myfile = self.project.get_file("myfile.txt") + self.assertFalse(myfile.exists()) + + myfile.create() + + self.assertTrue(myfile.exists()) + self.assertFalse(myfile.is_folder()) + + def test_using_folder_create(self): + myfolder = self.project.get_folder("myfolder") + self.assertFalse(myfolder.exists()) + + myfolder.create() + + self.assertTrue(myfolder.exists()) + self.assertTrue(myfolder.is_folder()) + + def test_exception_when_creating_twice(self): + with self.assertRaises(exceptions.RopeError): + myfile = self.project.get_file("myfile.txt") + myfile.create() + myfile.create() + + def test_exception_when_parent_does_not_exist(self): with self.assertRaises(exceptions.ResourceNotFoundError): - self.project.root.create_file("NonexistentFolder/SomeFile.txt") + myfile = self.project.get_file("myfolder/myfile.txt") + myfile.create() - def test_nested_directories(self): - folder_name = "SampleFolder" - parent = self.project.root.create_folder(folder_name) - parent.create_folder(folder_name) - folder_path = os.path.join(self.project.address, folder_name, folder_name) - self.assertTrue(os.path.exists(folder_path) and os.path.isdir(folder_path)) + + ###################### + # File/Folder.remove # + ###################### def test_removing_files(self): self.assertTrue(os.path.exists(self.sample_path)) + self.project.get_resource(self.sample_file).remove() + self.assertFalse(os.path.exists(self.sample_path)) def test_removing_files_invalidating_in_project_resource_pool(self): root_folder = self.project.root my_file = root_folder.create_file("my_file.txt") + my_file.remove() + self.assertFalse(root_folder.has_child("my_file.txt")) def test_removing_directories(self): self.assertTrue( os.path.exists(os.path.join(self.project.address, self.sample_folder)) ) + self.project.get_resource(self.sample_folder).remove() + self.assertFalse( os.path.exists(os.path.join(self.project.address, self.sample_folder)) ) @@ -161,7 +460,9 @@ def test_removing_non_existent_files(self): def test_removing_nested_files(self): file_name = self.sample_folder + "/sample_file.txt" self.project.root.create_file(file_name) + self.project.get_resource(file_name).remove() + self.assertTrue( os.path.exists(os.path.join(self.project.address, self.sample_folder)) ) @@ -169,6 +470,56 @@ def test_removing_nested_files(self): not os.path.exists(os.path.join(self.project.address, file_name)) ) + + #################### + # File/Folder.move # + #################### + + def test_moving_files(self): + root_folder = self.project.root + my_file = root_folder.create_file("my_file.txt") + + my_file.move("my_other_file.txt") + + self.assertFalse(my_file.exists()) + root_folder.get_child("my_other_file.txt") + + def test_moving_folders(self): + root_folder = self.project.root + my_folder = root_folder.create_folder("my_folder") + my_file = my_folder.create_file("my_file.txt") + + my_folder.move("new_folder") + + self.assertFalse(root_folder.has_child("my_folder")) + self.assertFalse(my_file.exists()) + self.assertTrue(root_folder.get_child("new_folder") is not None) + + def test_moving_destination_folders(self): + root_folder = self.project.root + my_folder = root_folder.create_folder("my_folder") + my_file = root_folder.create_file("my_file.txt") + + my_file.move("my_folder") + + self.assertFalse(root_folder.has_child("my_file.txt")) + self.assertFalse(my_file.exists()) + my_folder.get_child("my_file.txt") + + def test_moving_files_and_resource_objects(self): + root_folder = self.project.root + my_file = root_folder.create_file("my_file.txt") + old_hash = hash(my_file) + + my_file.move("my_other_file.txt") + + self.assertEqual(old_hash, hash(my_file)) + + + ######################## + # File/Folder metadata # + ######################## + def test_file_get_name(self): file = self.project.get_resource(self.sample_file) self.assertEqual(self.sample_file, file.name) @@ -177,10 +528,12 @@ def test_file_get_name(self): filePath = self.sample_folder + "/" + file_name parent.create_file(file_name) nestedFile = self.project.get_resource(filePath) + self.assertEqual(file_name, nestedFile.name) def test_folder_get_name(self): folder = self.project.get_resource(self.sample_folder) + self.assertEqual(self.sample_folder, folder.name) def test_file_get_path(self): @@ -191,12 +544,19 @@ def test_file_get_path(self): filePath = self.sample_folder + "/" + fileName parent.create_file(fileName) nestedFile = self.project.get_resource(filePath) + self.assertEqual(filePath, nestedFile.path) def test_folder_get_path(self): folder = self.project.get_resource(self.sample_folder) + self.assertEqual(self.sample_folder, folder.path) + + ##################################### + # File/Folder introspection methods # + ##################################### + def test_is_folder(self): self.assertTrue(self.project.get_resource(self.sample_folder).is_folder()) self.assertTrue(not self.project.get_resource(self.sample_file).is_folder()) @@ -207,8 +567,31 @@ def test_is_dir(self): with patch('rope.base.resources.File.is_folder') as is_folder2: assert self.project.get_resource(self.sample_file).is_dir() == is_folder2.return_value + + ########################## + # File/Folder.__fspath__ # + ########################## + + def test_folder_is_pathlike(self): + resource = self.project.root.create_folder("src") + self.assertIsInstance(resource, Folder) + + self.assertIsInstance(os.fspath(resource), str) + + def test_file_is_pathlike(self): + resource = self.project.root.create_file("mod.py") + self.assertIsInstance(resource, File) + + self.assertIsInstance(os.fspath(resource), str) + + + ############################################# + # Folder.get_children/get_files/get_folders # + ############################################# + def test_get_children(self): children = self.project.get_resource(self.sample_folder).get_children() + self.assertEqual([], children) def test_nonempty_get_children(self): @@ -216,7 +599,9 @@ def test_nonempty_get_children(self): filePath = self.sample_folder + "/" + file_name parent = self.project.get_resource(self.sample_folder) parent.create_file(file_name) + children = parent.get_children() + self.assertEqual(1, len(children)) self.assertEqual(filePath, children[0].path) @@ -228,7 +613,9 @@ def test_nonempty_get_children2(self): parent = self.project.get_resource(self.sample_folder) parent.create_file(file_name) parent.create_folder(folder_name) + children = parent.get_children() + self.assertEqual(2, len(children)) self.assertTrue(filePath == children[0].path or filePath == children[1].path) self.assertTrue( @@ -248,304 +635,82 @@ def test_does_not_fail_for_permission_denied(self): def test_getting_files(self): files = self.project.root.get_files() + self.assertEqual(1, len(files)) self.assertTrue(self.project.get_resource(self.sample_file) in files) def test_getting_folders(self): folders = self.project.root.get_folders() - self.assertEqual(1, len(folders)) - self.assertTrue(self.project.get_resource(self.sample_folder) in folders) - - def test_nested_folder_get_files(self): - parent = self.project.root.create_folder("top") - parent.create_file("file1.txt") - parent.create_file("file2.txt") - files = parent.get_files() - self.assertEqual(2, len(files)) - self.assertTrue(self.project.get_resource("top/file2.txt") in files) - self.assertEqual(0, len(parent.get_folders())) - - def test_nested_folder_get_folders(self): - parent = self.project.root.create_folder("top") - parent.create_folder("dir1") - parent.create_folder("dir2") - folders = parent.get_folders() - self.assertEqual(2, len(folders)) - self.assertTrue(self.project.get_resource("top/dir1") in folders) - self.assertEqual(0, len(parent.get_files())) - - def test_root_folder(self): - root_folder = self.project.root - self.assertEqual(2, len(root_folder.get_children())) - self.assertEqual("", root_folder.path) - self.assertEqual("", root_folder.name) - - def test_get_all_files(self): - files = tuple(self.project.get_files()) - self.assertEqual(1, len(files)) - self.assertEqual(self.sample_file, files[0].name) - - def test_get_all_files_after_changing(self): - self.assertEqual(1, len(self.project.get_files())) - myfile = self.project.root.create_file("myfile.txt") - self.assertEqual(2, len(self.project.get_files())) - myfile.move("newfile.txt") - self.assertEqual(2, len(self.project.get_files())) - self.project.get_file("newfile.txt").remove() - self.assertEqual(1, len(self.project.get_files())) - - def test_multifile_get_all_files(self): - fileName = "nestedFile.txt" - parent = self.project.get_resource(self.sample_folder) - parent.create_file(fileName) - files = list(self.project.get_files()) - self.assertEqual(2, len(files)) - self.assertTrue(fileName == files[0].name or fileName == files[1].name) - - def test_ignoring_dot_pyc_files_in_get_files(self): - root = self.project.address - src_folder = os.path.join(root, "src") - os.mkdir(src_folder) - test_pyc = os.path.join(src_folder, "test.pyc") - open(test_pyc, "w").close() - for x in self.project.get_files(): - self.assertNotEqual("src/test.pyc", x.path) - - def test_folder_creating_files(self): - projectFile = "NewFile.txt" - self.project.root.create_file(projectFile) - new_file = self.project.get_resource(projectFile) - self.assertTrue(new_file is not None and not new_file.is_folder()) - - def test_folder_creating_nested_files(self): - project_file = "NewFile.txt" - parent_folder = self.project.get_resource(self.sample_folder) - parent_folder.create_file(project_file) - new_file = self.project.get_resource(self.sample_folder + "/" + project_file) - self.assertTrue(new_file is not None and not new_file.is_folder()) - - def test_folder_creating_files2(self): - projectFile = "newfolder" - self.project.root.create_folder(projectFile) - new_folder = self.project.get_resource(projectFile) - self.assertTrue(new_folder is not None and new_folder.is_folder()) - - def test_folder_creating_nested_files2(self): - project_file = "newfolder" - parent_folder = self.project.get_resource(self.sample_folder) - parent_folder.create_folder(project_file) - new_folder = self.project.get_resource(self.sample_folder + "/" + project_file) - self.assertTrue(new_folder is not None and new_folder.is_folder()) - - def test_folder_get_child(self): - folder = self.project.root - folder.create_file("myfile.txt") - folder.create_folder("myfolder") - self.assertEqual( - self.project.get_resource("myfile.txt"), folder.get_child("myfile.txt") - ) - self.assertEqual( - self.project.get_resource("myfolder"), folder.get_child("myfolder") - ) - - def test_folder_get_child_nested(self): - root = self.project.root - folder = root.create_folder("myfolder") - folder.create_file("myfile.txt") - folder.create_folder("myfolder") - self.assertEqual( - self.project.get_resource("myfolder/myfile.txt"), - folder.get_child("myfile.txt"), - ) - self.assertEqual( - self.project.get_resource("myfolder/myfolder"), folder.get_child("myfolder") - ) - - def test_project_root_is_root_folder(self): - self.assertEqual("", self.project.root.path) - - def test_moving_files(self): - root_folder = self.project.root - my_file = root_folder.create_file("my_file.txt") - my_file.move("my_other_file.txt") - self.assertFalse(my_file.exists()) - root_folder.get_child("my_other_file.txt") - - def test_moving_folders(self): - root_folder = self.project.root - my_folder = root_folder.create_folder("my_folder") - my_file = my_folder.create_file("my_file.txt") - my_folder.move("new_folder") - self.assertFalse(root_folder.has_child("my_folder")) - self.assertFalse(my_file.exists()) - self.assertTrue(root_folder.get_child("new_folder") is not None) - - def test_moving_destination_folders(self): - root_folder = self.project.root - my_folder = root_folder.create_folder("my_folder") - my_file = root_folder.create_file("my_file.txt") - my_file.move("my_folder") - self.assertFalse(root_folder.has_child("my_file.txt")) - self.assertFalse(my_file.exists()) - my_folder.get_child("my_file.txt") - - def test_moving_files_and_resource_objects(self): - root_folder = self.project.root - my_file = root_folder.create_file("my_file.txt") - old_hash = hash(my_file) - my_file.move("my_other_file.txt") - self.assertEqual(old_hash, hash(my_file)) - - def test_file_encoding_reading(self): - sample_file = self.project.root.create_file("my_file.txt") - contents = ( - b"# -*- coding: utf-8 -*-\n" - + br"#\N{LATIN SMALL LETTER I WITH DIAERESIS}\n" - ).decode("utf8") - file = open(sample_file.real_path, "wb") - file.write(contents.encode("utf-8")) - file.close() - self.assertEqual(contents, sample_file.read()) - def test_file_encoding_writing(self): - sample_file = self.project.root.create_file("my_file.txt") - contents = ( - b"# -*- coding: utf-8 -*-\n" + br"\N{LATIN SMALL LETTER I WITH DIAERESIS}\n" - ).decode("utf8") - sample_file.write(contents) - self.assertEqual(contents, sample_file.read()) + self.assertEqual(1, len(folders)) + self.assertTrue(self.project.get_resource(self.sample_folder) in folders) - def test_using_utf8_when_writing_in_case_of_errors(self): - sample_file = self.project.root.create_file("my_file.txt") - contents = br"\n\N{LATIN SMALL LETTER I WITH DIAERESIS}\n".decode("utf8") - sample_file.write(contents) - self.assertEqual(contents, sample_file.read()) + def test_nested_folder_get_files(self): + parent = self.project.root.create_folder("top") + parent.create_file("file1.txt") + parent.create_file("file2.txt") - def test_encoding_declaration_in_the_second_line(self): - sample_file = self.project.root.create_file("my_file.txt") - contents = b"\n# -*- coding: latin-1 -*-\n\xa9\n" - file = open(sample_file.real_path, "wb") - file.write(contents) - file.close() - self.assertEqual(contents, sample_file.read().encode("latin-1")) + files = parent.get_files() - def test_not_an_encoding_declaration(self): - sample_file = self.project.root.create_file("my_file.txt") - contents = b"def my_method(self, encoding='latin-1'):\n var = {}\n\xc2\xa9\n" - file = open(sample_file.real_path, "wb") - file.write(contents) - file.close() - self.assertEqual(contents, sample_file.read().encode("utf-8")) - self.assertNotEqual(contents, sample_file.read().encode("latin-1")) + self.assertEqual(2, len(files)) + self.assertTrue(self.project.get_resource("top/file2.txt") in files) + self.assertEqual(0, len(parent.get_folders())) - def test_read_bytes(self): - sample_file = self.project.root.create_file("my_file.txt") - contents = b"\n# -*- coding: latin-1 -*-\n\xa9\n" - file = open(sample_file.real_path, "wb") - file.write(contents) - file.close() - self.assertEqual(contents, sample_file.read_bytes()) + def test_nested_folder_get_folders(self): + parent = self.project.root.create_folder("top") + parent.create_folder("dir1") + parent.create_folder("dir2") - def test_file_with_unix_line_ending(self): - sample_file = self.project.root.create_file("my_file.txt") - contents = b"1\n" - file = open(sample_file.real_path, "wb") - file.write(contents) - file.close() - self.assertIsNone(sample_file.newlines) - self.assertEqual("1\n", sample_file.read()) - self.assertEqual("\n", sample_file.newlines) + folders = parent.get_folders() - sample_file.write("1\n") - self.assertEqual(b"1\n", sample_file.read_bytes()) + self.assertEqual(2, len(folders)) + self.assertTrue(self.project.get_resource("top/dir1") in folders) + self.assertEqual(0, len(parent.get_files())) - def test_file_with_dos_line_ending(self): - sample_file = self.project.root.create_file("my_file.txt") - contents = b"1\r\n" - file = open(sample_file.real_path, "wb") - file.write(contents) - file.close() - self.assertIsNone(sample_file.newlines) - self.assertEqual("1\n", sample_file.read()) - self.assertEqual("\r\n", sample_file.newlines) + def test_root_folder(self): + root_folder = self.project.root - sample_file.write("1\n") - self.assertEqual(b"1\r\n", sample_file.read_bytes()) + self.assertEqual(2, len(root_folder.get_children())) - def test_file_with_mac_line_ending(self): - sample_file = self.project.root.create_file("my_file.txt") - contents = b"1\r" - file = open(sample_file.real_path, "wb") - file.write(contents) - file.close() - self.assertIsNone(sample_file.newlines) - self.assertEqual("1\n", sample_file.read()) - self.assertEqual("\r", sample_file.newlines) + self.assertEqual("", root_folder.path) + self.assertEqual("", root_folder.name) - sample_file.write("1\n") - self.assertEqual(b"1\r", sample_file.read_bytes()) + def test_get_all_files(self): + files = tuple(self.project.get_files()) - def test_file_binary(self): - sample_file = self.project.root.create_file("my_file.txt") - contents = b"1\r\n" - file = open(sample_file.real_path, "wb") - file.write(contents) - file.close() - self.assertIsNone(sample_file.newlines) - self.assertEqual(b"1\r\n", sample_file.read_bytes()) - self.assertIsNone(sample_file.newlines) + self.assertEqual(1, len(files)) + self.assertEqual(self.sample_file, files[0].name) - sample_file.write(b"1\nx\r") - self.assertEqual((b"1\nx\r"), sample_file.read_bytes()) + def test_get_all_files_after_changing(self): + self.assertEqual(1, len(self.project.get_files())) + myfile = self.project.root.create_file("myfile.txt") + self.assertEqual(2, len(self.project.get_files())) - # TODO: Detecting utf-16 encoding - def xxx_test_using_utf16(self): - sample_file = self.project.root.create_file("my_file.txt") - contents = b"# -*- coding: utf-16 -*-\n# This is a sample file ...\n" - file = open(sample_file.real_path, "w") - file.write(contents.encode("utf-16")) - file.close() - sample_file.write(contents) - self.assertEqual(contents, sample_file.read()) + myfile.move("newfile.txt") - # XXX: supporting utf_8_sig - def xxx_test_file_encoding_reading_for_notepad_styles(self): - sample_file = self.project.root.create_file("my_file.txt") - contents = "#\N{LATIN SMALL LETTER I WITH DIAERESIS}\n" - file = open(sample_file.real_path, "w") - # file.write('\xef\xbb\xbf') - file.write(contents.encode("utf-8-sig")) - file.close() - self.assertEqual(contents, sample_file.read()) + self.assertEqual(2, len(self.project.get_files())) + self.project.get_file("newfile.txt").remove() + self.assertEqual(1, len(self.project.get_files())) - def test_using_project_get_file(self): - myfile = self.project.get_file(self.sample_file) - self.assertTrue(myfile.exists()) + def test_multifile_get_all_files(self): + fileName = "nestedFile.txt" + parent = self.project.get_resource(self.sample_folder) + parent.create_file(fileName) - def test_using_file_create(self): - myfile = self.project.get_file("myfile.txt") - self.assertFalse(myfile.exists()) - myfile.create() - self.assertTrue(myfile.exists()) - self.assertFalse(myfile.is_folder()) + files = list(self.project.get_files()) - def test_using_folder_create(self): - myfolder = self.project.get_folder("myfolder") - self.assertFalse(myfolder.exists()) - myfolder.create() - self.assertTrue(myfolder.exists()) - self.assertTrue(myfolder.is_folder()) + self.assertEqual(2, len(files)) + self.assertTrue(fileName == files[0].name or fileName == files[1].name) - def test_exception_when_creating_twice(self): - with self.assertRaises(exceptions.RopeError): - myfile = self.project.get_file("myfile.txt") - myfile.create() - myfile.create() + def test_ignoring_dot_pyc_files_in_get_files(self): + root = self.project.address + src_folder = os.path.join(root, "src") + os.mkdir(src_folder) + test_pyc = os.path.join(src_folder, "test.pyc") + open(test_pyc, "w").close() - def test_exception_when_parent_does_not_exist(self): - with self.assertRaises(exceptions.ResourceNotFoundError): - myfile = self.project.get_file("myfolder/myfile.txt") - myfile.create() + for x in self.project.get_files(): + self.assertNotEqual("src/test.pyc", x.path) @testutils.skipNotPOSIX() def test_ignoring_symlinks_inside_project(self): @@ -554,73 +719,43 @@ def test_ignoring_symlinks_inside_project(self): try: path = os.path.join(self.project.address, "linkedfile.txt") os.symlink(mod.real_path, path) + files = self.project.root.get_files() + self.assertEqual(1, len(files)) finally: testutils.remove_project(project2) - def test_getting_empty_source_folders(self): - self.assertEqual([], self.project.get_source_folders()) - - def test_root_source_folder(self): - self.project.root.create_file("sample.py") - source_folders = self.project.get_source_folders() - self.assertEqual(1, len(source_folders)) - self.assertTrue(self.project.root in source_folders) - - def test_root_source_folder2(self): - self.project.root.create_file("mod1.py") - self.project.root.create_file("mod2.py") - source_folders = self.project.get_source_folders() - self.assertEqual(1, len(source_folders)) - self.assertTrue(self.project.root in source_folders) - - def test_src_source_folder(self): - src = self.project.root.create_folder("src") - src.create_file("sample.py") - source_folders = self.project.get_source_folders() - self.assertEqual(1, len(source_folders)) - self.assertTrue(self.project.get_resource("src") in source_folders) - - def test_packages(self): - src = self.project.root.create_folder("src") - pkg = src.create_folder("package") - pkg.create_file("__init__.py") - source_folders = self.project.get_source_folders() - self.assertEqual(1, len(source_folders)) - self.assertTrue(src in source_folders) - - def test_multi_source_folders(self): - src = self.project.root.create_folder("src") - package = src.create_folder("package") - package.create_file("__init__.py") - test = self.project.root.create_folder("test") - test.create_file("alltests.py") - source_folders = self.project.get_source_folders() - self.assertEqual(2, len(source_folders)) - self.assertTrue(src in source_folders) - self.assertTrue(test in source_folders) - def test_multi_source_folders2(self): - testutils.create_module(self.project, "mod1") - src = self.project.root.create_folder("src") - package = testutils.create_package(self.project, "package", src) - testutils.create_module(self.project, "mod2", package) - source_folders = self.project.get_source_folders() - self.assertEqual(2, len(source_folders)) - self.assertTrue(self.project.root in source_folders and src in source_folders) + #################### + # Folder.get_child # + #################### - def test_folder_is_pathlike(self): - resource = self.project.root.create_folder("src") - self.assertIsInstance(resource, Folder) + def test_folder_get_child(self): + folder = self.project.root + folder.create_file("myfile.txt") + folder.create_folder("myfolder") - self.assertIsInstance(os.fspath(resource), str) + self.assertEqual( + self.project.get_resource("myfile.txt"), folder.get_child("myfile.txt") + ) + self.assertEqual( + self.project.get_resource("myfolder"), folder.get_child("myfolder") + ) - def test_file_is_pathlike(self): - resource = self.project.root.create_file("mod.py") - self.assertIsInstance(resource, File) + def test_folder_get_child_nested(self): + root = self.project.root + folder = root.create_folder("myfolder") + folder.create_file("myfile.txt") + folder.create_folder("myfolder") - self.assertIsInstance(os.fspath(resource), str) + self.assertEqual( + self.project.get_resource("myfolder/myfile.txt"), + folder.get_child("myfile.txt"), + ) + self.assertEqual( + self.project.get_resource("myfolder/myfolder"), folder.get_child("myfolder") + ) class TestProjectGetResource: @@ -652,6 +787,11 @@ def file_outside_project(self, project): file.touch() return path + + ######################## + # Project.get_resource # + ######################## + def test_get_resource_getting_not_existing_project_file(self, project): with pytest.raises( exceptions.ResourceNotFoundError, @@ -723,6 +863,10 @@ def test_get_resource_getting_existing_resource_with_double_dot_disable_projectr assert resource.path == "../sample_file.txt" assert resource.real_path.endswith("sample_project/../sample_file.txt") + ############################# + # libutils.path_to_resource # + ############################# + def test_simple_path_to_resource(self, sample_project): myfile = sample_project.root.create_file("myfile.txt") assert myfile == path_to_resource(sample_project, myfile.real_path) @@ -733,6 +877,15 @@ def test_simple_path_to_resource(self, sample_project): assert myfolder == path_to_resource(sample_project, myfolder.real_path, type="folder") + #################### + # Project.get_file # + #################### + + def test_using_project_get_file(self, sample_project): + myfile = sample_project.get_file("sample_file.txt") + + assert myfile.exists() + class TestProjectGetModule: """ @@ -742,6 +895,8 @@ class TestProjectGetModule: - Project.find_module - Project.get_relative_module - Project.find_relative_module + + FIXME: most of the test for these methods are in pycoretest.py """ def test_get_module_non_existent_module(self, project): From ead06fae3dfe5f4180d4dff007a97840c035b7b3 Mon Sep 17 00:00:00 2001 From: Lie Ryan Date: Wed, 11 Nov 2026 10:33:05 +1100 Subject: [PATCH 15/17] Skip os.mkfifo test on Windows --- ropetest/projecttest.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ropetest/projecttest.py b/ropetest/projecttest.py index 6429f3cca..beae0688f 100644 --- a/ropetest/projecttest.py +++ b/ropetest/projecttest.py @@ -774,6 +774,8 @@ def special_file(self, request, project) -> pathlib.Path: case "devnull": return pathlib.Path(os.devnull) case "mkfifo": + if not hasattr(os, "mkfifo"): + pytest.skip() fifo_path = project.address + "/fifo" os.mkfifo(fifo_path) return pathlib.Path(fifo_path) From 0d9d2b69fe85ebb6b259236dfb9dad87559e1f88 Mon Sep 17 00:00:00 2001 From: Lie Ryan Date: Wed, 11 Nov 2026 11:08:10 +1100 Subject: [PATCH 16/17] Fix tests on Windows --- ropetest/projecttest.py | 46 +++++++++++++++++++++++++++-------------- 1 file changed, 31 insertions(+), 15 deletions(-) diff --git a/ropetest/projecttest.py b/ropetest/projecttest.py index beae0688f..6b3b4a695 100644 --- a/ropetest/projecttest.py +++ b/ropetest/projecttest.py @@ -769,16 +769,16 @@ class TestProjectGetResource: """ @pytest.fixture(params=["devnull", "mkfifo"]) - def special_file(self, request, project) -> pathlib.Path: + def special_file(self, request, project) -> str: match request.param: case "devnull": - return pathlib.Path(os.devnull) + return os.devnull case "mkfifo": if not hasattr(os, "mkfifo"): pytest.skip() fifo_path = project.address + "/fifo" os.mkfifo(fifo_path) - return pathlib.Path(fifo_path) + return fifo_path case _: assert False @@ -802,18 +802,30 @@ def test_get_resource_getting_not_existing_project_file(self, project): project.get_resource("DoesNotExistFile.txt") def test_no_project_get_resource_special_file(self, no_project, special_file): + match os.name, special_file: + case "nt", os.devnull: + expected_message = f"Unknown resource //./nul" + case _: + expected_message = f"Unknown resource {special_file}" + with pytest.raises( exceptions.ResourceNotFoundError, - match=f"Unknown resource {special_file}", + match=expected_message, ): - no_project.get_resource(str(special_file)) + no_project.get_resource(special_file) def test_project_get_resource_special_file(self, project, special_file): + match os.name, special_file: + case "nt", os.devnull: + expected_message = f"Unknown resource nul" + case _: + expected_message = f"Resource <{special_file}> does not exist" + with pytest.raises( exceptions.ResourceNotFoundError, - match=f"Resource <{special_file}> does not exist", + match=expected_message, ): - project.get_resource(str(special_file)) + project.get_resource(special_file) def test_get_resource_getting_existing_project_file(self, sample_project): project_file = sample_project.get_resource("sample_file.txt") @@ -827,32 +839,36 @@ def test_project_get_resource_getting_existing_resource_with_slash( self, sample_project ): """ - for Project.get_resource(), starting slashes are interpreted as - relative to project directory + For Project.get_resource(), starting slashes are interpreted as + relative to project directory. """ project_file = sample_project.get_resource("/sample_file.txt") assert isinstance(project_file, File) assert project_file.pathlib.is_relative_to(sample_project.address) with tempfile.NamedTemporaryFile() as tmp: - assert tmp.name.startswith("/") + abspath = str(pathlib.Path(tmp.name).absolute()) + _, slashpath = os.path.splitdrive(abspath) + assert slashpath.startswith("/") with pytest.raises( exceptions.ResourceNotFoundError, match="Resource <.*> does not exist", ): - project_file = sample_project.get_resource(tmp.name) + project_file = sample_project.get_resource(slashpath) def test_no_project_get_resource_getting_existing_resource_with_slash( self, no_project, file_outside_project ): """ - for NoProject.get_resource(), starting slashes are interpreted as + For NoProject.get_resource(), starting slashes are interpreted as absolute path relative to project root. NoProject should be used to get resources for files/folders that's not part of a project. """ with tempfile.NamedTemporaryFile() as tmp: - assert tmp.name.startswith("/") - project_file = no_project.get_resource(tmp.name) + abspath = str(pathlib.Path(tmp.name).absolute()) + _, slashpath = os.path.splitdrive(abspath) + assert slashpath.startswith("/") + project_file = no_project.get_resource(slashpath) assert isinstance(project_file, File) assert project_file.exists() @@ -863,7 +879,7 @@ def test_get_resource_getting_existing_resource_with_double_dot_disable_projectr resource = sample_project.get_resource(file_outside_project) assert isinstance(resource, File) assert resource.path == "../sample_file.txt" - assert resource.real_path.endswith("sample_project/../sample_file.txt") + assert resource.real_path.endswith("sample_project/../sample_file.txt".replace("/", os.path.sep)) ############################# # libutils.path_to_resource # From 652c9a9607118f49c1e1694ddd4482acb2d85163 Mon Sep 17 00:00:00 2001 From: Lie Ryan Date: Wed, 11 Nov 2026 10:34:48 +1100 Subject: [PATCH 17/17] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ba328bd9a..82bcf5fa9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - #119, #928 Fix IndexError in rename and find occurrences when a `from` import is the last thing in a module without a trailing newline (@mcepl) - #921 Patch the else suite of try statements that also have a finally (@marlon-costa-dc) - #937 Preserve non-ASCII argument expressions and default values when changing function signatures (@Jinyong-Li) +- #950 Reorganise projecttest.py and improve test coverages (@lieryan) # Release 1.15.1