From b96663a61893f22e41f19a07bc412ee028f1654d Mon Sep 17 00:00:00 2001 From: Lie Ryan Date: Fri, 5 Apr 2024 03:18:54 +1100 Subject: [PATCH 1/6] Remove unnecessary global mypy ignore in rope/base/prefs.py --- rope/base/prefs.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/rope/base/prefs.py b/rope/base/prefs.py index cac0b7f1..83f72e73 100644 --- a/rope/base/prefs.py +++ b/rope/base/prefs.py @@ -1,5 +1,3 @@ -# mypy reports many problems. -# type: ignore """Rope preferences.""" from dataclasses import asdict, dataclass from textwrap import dedent From 9d7f2efc188bb8b725f9361d09e8a3a86c846df8 Mon Sep 17 00:00:00 2001 From: Lie Ryan Date: Fri, 5 Apr 2024 04:00:32 +1100 Subject: [PATCH 2/6] Add autopytoolconfigtable for ImportPrefs --- docs/configuration.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/configuration.rst b/docs/configuration.rst index a044a55d..449687a7 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -69,6 +69,10 @@ autoimport.* Options .. autopytoolconfigtable:: rope.base.prefs.AutoimportPrefs +import.* Options +---------------- + +.. autopytoolconfigtable:: rope.base.prefs.ImportPrefs Old Configuration File ---------------------- From 4bba655689b0efaafdee5e5ed537b4ccb9b53c0e Mon Sep 17 00:00:00 2001 From: Lie Ryan Date: Fri, 5 Apr 2024 03:47:08 +1100 Subject: [PATCH 3/6] Implement config setting imports.preferred_import_style --- docs/default_config.py | 9 +++-- rope/base/prefs.py | 47 ++++++++++++++++++++-- ropetest/refactor/importutilstest.py | 58 ++++++++++++++++++++++++++++ 3 files changed, 107 insertions(+), 7 deletions(-) diff --git a/docs/default_config.py b/docs/default_config.py index 3543b07f..59e34126 100644 --- a/docs/default_config.py +++ b/docs/default_config.py @@ -107,10 +107,13 @@ def set_prefs(prefs): # # prefs["ignore_bad_imports"] = False - # If `True`, rope will insert new module imports as - # `from import ` by default. + # Controls how rope inserts new import statements. Must be one of: + # + # - "normal-import" will insert `import ` + # - "from-module" will insert `from import ` + # - "from-global" insert insert `from . import ` # - # prefs["prefer_module_from_imports"] = False + # prefs.imports.preferred_import_style = "normal-import" # If `True`, rope will transform a comma list of imports into # multiple separate import statements when organizing diff --git a/rope/base/prefs.py b/rope/base/prefs.py index 83f72e73..b28610ce 100644 --- a/rope/base/prefs.py +++ b/rope/base/prefs.py @@ -1,4 +1,5 @@ """Rope preferences.""" +from enum import Enum from dataclasses import asdict, dataclass from textwrap import dedent from typing import Any, Callable, Dict, List, Optional, Tuple @@ -34,6 +35,20 @@ class AutoimportPrefs: ) +@dataclass +class ImportPrefs: + preferred_import_style: str = field( + default="default", + description=dedent(""" + Controls how rope inserts new import statements. If set to + ``"normal-import"`` (default) will insert ``import ``; if + set to ``"from-module"`` will insert ``from import + ``; if set to ``"from-global"`` rope will insert ``from + . import ``. + """), + ) + + @dataclass class Prefs: """Class to store rope preferences.""" @@ -149,7 +164,7 @@ class Prefs: default=False, description=dedent(""" If ``True`` modules with syntax errors are considered to be empty. - The default value is ``False``; When ``False`` syntax errors raise + The default value is ``False``; when ``False`` syntax errors raise ``rope.base.exceptions.ModuleSyntaxError`` exception. """), ) @@ -164,8 +179,8 @@ class Prefs: prefer_module_from_imports: bool = field( default=False, description=dedent(""" - If ``True``, rope will insert new module imports as ``from - import `` by default. + **Deprecated**. ``imports.preferred_import_style`` takes + precedence over ``prefer_module_from_imports``. """), ) @@ -232,7 +247,13 @@ class Prefs: """), ) autoimport: AutoimportPrefs = field( - default_factory=AutoimportPrefs, description="Preferences for Autoimport") + default_factory=AutoimportPrefs, + description="Preferences for Autoimport", + ) + imports: ImportPrefs = field( + default_factory=ImportPrefs, + description="Preferences for Import Organiser", + ) def set(self, key: str, value: Any): """Set the value of `key` preference to `value`.""" @@ -320,3 +341,21 @@ def get_config(root: Folder, ropefolder: Folder) -> PyToolConfig: global_config=True, ) return config + + +class ImportStyle(Enum): # FIXME: Use StrEnum once we're on minimum Python 3.11 + normal_import = "normal-import" + from_module = "from-module" + from_global = "from-global" + + +DEFAULT_IMPORT_STYLE = ImportStyle.normal_import + + +def get_preferred_import_style(prefs: Prefs) -> ImportStyle: + try: + return ImportStyle(prefs.imports.preferred_import_style) + except ValueError: + if prefs.imports.preferred_import_style == "default" and prefs.prefer_module_from_imports: + return ImportStyle.from_module + return DEFAULT_IMPORT_STYLE diff --git a/ropetest/refactor/importutilstest.py b/ropetest/refactor/importutilstest.py index 732f1c9e..845bebd5 100644 --- a/ropetest/refactor/importutilstest.py +++ b/ropetest/refactor/importutilstest.py @@ -1,10 +1,68 @@ import unittest from textwrap import dedent +from rope.base.prefs import get_preferred_import_style, ImportStyle, Prefs, ImportPrefs +from rope.base.prefs import DEFAULT_IMPORT_STYLE from rope.refactor.importutils import ImportTools, add_import, importinfo from ropetest import testutils +class TestImportPrefs: + def test_preferred_import_style_is_normal_import(self, project): + pref = Prefs(imports=ImportPrefs(preferred_import_style="normal-import")) + assert pref.imports.preferred_import_style == "normal-import" + assert get_preferred_import_style(pref) == ImportStyle.normal_import + + def test_preferred_import_style_is_from_module(self, project): + pref = Prefs(imports=ImportPrefs(preferred_import_style="from-module")) + assert pref.imports.preferred_import_style == "from-module" + assert get_preferred_import_style(pref) == ImportStyle.from_module + + def test_preferred_import_style_is_from_global(self, project): + pref = Prefs(imports=ImportPrefs(preferred_import_style="from-global")) + assert pref.imports.preferred_import_style == "from-global" + assert get_preferred_import_style(pref) == ImportStyle.from_global + + def test_invalid_preferred_import_style_is_default(self, project): + pref = Prefs(imports=ImportPrefs(preferred_import_style="invalid-value")) + assert pref.imports.preferred_import_style == "invalid-value" + assert get_preferred_import_style(pref) == DEFAULT_IMPORT_STYLE + assert get_preferred_import_style(pref) == ImportStyle.normal_import + + def test_default_preferred_import_style_default_is_normal_imports(self, project): + pref = Prefs() + assert pref.imports.preferred_import_style == "default" + assert get_preferred_import_style(pref) == ImportStyle.normal_import + + def test_default_preferred_import_style_default_and_prefer_module_from_imports(self, project): + pref = Prefs( + prefer_module_from_imports=True, + imports=ImportPrefs(preferred_import_style="default"), + ) + assert get_preferred_import_style(pref) == ImportStyle.from_module + + def test_preferred_import_style_is_normal_import_takes_precedence_over_prefer_module_from_imports(self, project): + pref = Prefs( + prefer_module_from_imports=True, + imports=ImportPrefs(preferred_import_style="normal_import"), + ) + assert get_preferred_import_style(pref) == ImportStyle.normal_import + + def test_preferred_import_style_is_from_module_takes_precedence_over_prefer_module_from_imports(self, project): + pref = Prefs( + prefer_module_from_imports=True, + imports=ImportPrefs(preferred_import_style="from-module"), + ) + assert get_preferred_import_style(pref) == ImportStyle.from_module + + def test_preferred_import_style_is_from_global_takes_precedence_over_prefer_module_from_imports(self, project): + pref = Prefs( + prefer_module_from_imports=True, + imports=ImportPrefs(preferred_import_style="from-global"), + ) + assert get_preferred_import_style(pref) == ImportStyle.from_global + + class ImportUtilsTest(unittest.TestCase): def setUp(self): super().setUp() From a4bbb9e77d468551701193218842c74c8fea0cf6 Mon Sep 17 00:00:00 2001 From: Lie Ryan Date: Fri, 5 Apr 2024 04:28:03 +1100 Subject: [PATCH 4/6] Implement preferred_import_style This is a configuration option to select the import style that rope will use when adding new imports. Co-authored-by: Nicolas Zermati --- rope/refactor/importutils/__init__.py | 7 ++- ropetest/refactor/movetest.py | 69 +++++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 1 deletion(-) diff --git a/rope/refactor/importutils/__init__.py b/rope/refactor/importutils/__init__.py index ef0f1bac..1e1810e5 100644 --- a/rope/refactor/importutils/__init__.py +++ b/rope/refactor/importutils/__init__.py @@ -8,6 +8,8 @@ import rope.base.codeanalyze import rope.base.evaluate from rope.base import libutils +from rope.base.prefs import get_preferred_import_style +from rope.base.prefs import ImportStyle from rope.base.change import ChangeContents, ChangeSet from rope.refactor import occurrences, rename from rope.refactor.importutils import actions, module_imports @@ -299,6 +301,7 @@ def get_module_imports(project, pymodule): def add_import(project, pymodule, module_name, name=None): + preferred_import_style = get_preferred_import_style(project.prefs) imports = get_module_imports(project, pymodule) candidates = [] names = [] @@ -306,13 +309,15 @@ def add_import(project, pymodule, module_name, name=None): # from mod import name if name is not None: from_import = FromImport(module_name, 0, [(name, None)]) + if preferred_import_style == ImportStyle.from_global: + selected_import = from_import names.append(name) candidates.append(from_import) # from pkg import mod if "." in module_name: pkg, mod = module_name.rsplit(".", 1) from_import = FromImport(pkg, 0, [(mod, None)]) - if project.prefs.get("prefer_module_from_imports"): + if preferred_import_style == ImportStyle.from_module: selected_import = from_import candidates.append(from_import) if name: diff --git a/ropetest/refactor/movetest.py b/ropetest/refactor/movetest.py index 804fa688..1bf07655 100644 --- a/ropetest/refactor/movetest.py +++ b/ropetest/refactor/movetest.py @@ -254,6 +254,75 @@ def a_function(): self.mod3.read(), ) + def test_adding_imports_preferred_import_style_is_normal_import(self) -> None: + self.project.prefs.imports.preferred_import_style = "normal-import" + self.origin_module.write(dedent("""\ + class AClass(object): + pass + def a_function(): + pass + """)) + self.mod3.write(dedent("""\ + import origin_module + a_var = origin_module.AClass() + origin_module.a_function()""")) + # Move to destination_module_in_pkg which is in a different package + self._move(self.origin_module, self.origin_module.read().index("AClass") + 1, self.destination_module_in_pkg) + self.assertEqual( + dedent("""\ + import origin_module + import pkg.destination_module_in_pkg + a_var = pkg.destination_module_in_pkg.AClass() + origin_module.a_function()"""), + self.mod3.read(), + ) + + def test_adding_imports_preferred_import_style_is_from_module(self) -> None: + self.project.prefs.imports.preferred_import_style = "from-module" + self.origin_module.write(dedent("""\ + class AClass(object): + pass + def a_function(): + pass + """)) + self.mod3.write(dedent("""\ + import origin_module + a_var = origin_module.AClass() + origin_module.a_function()""")) + # Move to destination_module_in_pkg which is in a different package + self._move(self.origin_module, self.origin_module.read().index("AClass") + 1, self.destination_module_in_pkg) + self.assertEqual( + dedent("""\ + import origin_module + from pkg import destination_module_in_pkg + a_var = destination_module_in_pkg.AClass() + origin_module.a_function()"""), + self.mod3.read(), + ) + + def test_adding_imports_preferred_import_style_is_from_global(self) -> None: + self.project.prefs.imports.preferred_import_style = "from-global" + self.origin_module.write(dedent("""\ + class AClass(object): + pass + def a_function(): + pass + """)) + self.mod3.write(dedent("""\ + import origin_module + a_var = origin_module.AClass() + origin_module.a_function()""")) + # Move to destination_module_in_pkg which is in a different package + self._move(self.origin_module, self.origin_module.read().index("AClass") + 1, self.destination_module_in_pkg) + self.assertEqual( + dedent("""\ + import origin_module + from pkg.destination_module_in_pkg import AClass + a_var = AClass() + origin_module.a_function()"""), + self.mod3.read(), + ) + def test_adding_imports_noprefer_from_module(self) -> None: self.project.prefs["prefer_module_from_imports"] = False self.origin_module.write(dedent("""\ From c4469afbdd37e34cf340591e635c075528826c2e Mon Sep 17 00:00:00 2001 From: Lie Ryan Date: Fri, 5 Apr 2024 04:33:49 +1100 Subject: [PATCH 5/6] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 20b1e72a..c6c6aa2f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ - #787 Add type hints to importinfo.py and add repr to ImportInfo (@lieryan) - #786 Upgrade Actions used in Github Workflows (@lieryan) - #785 Refactoring movetest.py (@lieryan) +- #788 Introduce the `preferred_import_style` configuration (@nicoolas25, @lieryan) # Release 1.13.0 From 0789b8181e6f489015cdb6f2d41ce2a8daee4f21 Mon Sep 17 00:00:00 2001 From: Lie Ryan Date: Tue, 9 Apr 2024 09:36:20 +1000 Subject: [PATCH 6/6] Fix config section name --- docs/configuration.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/configuration.rst b/docs/configuration.rst index 449687a7..7b336208 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -69,7 +69,7 @@ autoimport.* Options .. autopytoolconfigtable:: rope.base.prefs.AutoimportPrefs -import.* Options +imports.* Options ---------------- .. autopytoolconfigtable:: rope.base.prefs.ImportPrefs