Skip to content

Commit

Permalink
Merge pull request #788 from python-rope/lieryan-preferred-import-style
Browse files Browse the repository at this point in the history
Implement preferred_import_style
  • Loading branch information
lieryan authored Nov 17, 2024
2 parents 33d596a + 3460498 commit cd3ab6a
Show file tree
Hide file tree
Showing 7 changed files with 187 additions and 10 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 4 additions & 0 deletions docs/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,10 @@ autoimport.* Options

.. autopytoolconfigtable:: rope.base.prefs.AutoimportPrefs

imports.* Options
----------------

.. autopytoolconfigtable:: rope.base.prefs.ImportPrefs

Old Configuration File
----------------------
Expand Down
9 changes: 6 additions & 3 deletions docs/default_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,10 +107,13 @@ def set_prefs(prefs):
#
# prefs["ignore_bad_imports"] = False

# If `True`, rope will insert new module imports as
# `from <package> import <module>` by default.
# Controls how rope inserts new import statements. Must be one of:
#
# - "normal-import" will insert `import <package>`
# - "from-module" will insert `from <package> import <module>`
# - "from-global" insert insert `from <package>.<module> import <object>`
#
# 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
Expand Down
49 changes: 43 additions & 6 deletions rope/base/prefs.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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 <package>``; if
set to ``"from-module"`` will insert ``from <package> import
<module>``; if set to ``"from-global"`` rope will insert ``from
<package>.<module> import <object>``.
"""),
)


@dataclass
class Prefs:
"""Class to store rope preferences."""
Expand Down Expand Up @@ -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.
"""),
)
Expand All @@ -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
<package> import <module>`` by default.
**Deprecated**. ``imports.preferred_import_style`` takes
precedence over ``prefer_module_from_imports``.
"""),
)

Expand Down Expand Up @@ -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`."""
Expand Down Expand Up @@ -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
7 changes: 6 additions & 1 deletion rope/refactor/importutils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -299,20 +301,23 @@ 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 = []
selected_import = 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:
Expand Down
58 changes: 58 additions & 0 deletions ropetest/refactor/importutilstest.py
Original file line number Diff line number Diff line change
@@ -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()
Expand Down
69 changes: 69 additions & 0 deletions ropetest/refactor/movetest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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("""\
Expand Down

0 comments on commit cd3ab6a

Please sign in to comment.