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 diff --git a/docs/configuration.rst b/docs/configuration.rst index a044a55d..7b336208 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -69,6 +69,10 @@ autoimport.* Options .. autopytoolconfigtable:: rope.base.prefs.AutoimportPrefs +imports.* Options +---------------- + +.. autopytoolconfigtable:: rope.base.prefs.ImportPrefs Old Configuration File ---------------------- 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 cac0b7f1..b28610ce 100644 --- a/rope/base/prefs.py +++ b/rope/base/prefs.py @@ -1,6 +1,5 @@ -# mypy reports many problems. -# type: ignore """Rope preferences.""" +from enum import Enum from dataclasses import asdict, dataclass from textwrap import dedent from typing import Any, Callable, Dict, List, Optional, Tuple @@ -36,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.""" @@ -151,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. """), ) @@ -166,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``. """), ) @@ -234,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`.""" @@ -322,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/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/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() 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("""\