Repository: wemake-services/wemake-python-styleguide

Issue:
Forbid `from-import` for meaningless names
# Rule request

<!-- Thanks for coming up with a proposal! -->

## Thesis

<!-- What exactly do you propose? Please, provide code examples. -->

Some packages designed to be imported only as module, and functions inside have name that completes module name.

Bad:

```python
from attr import s
from json import load, dumps
from tomlkit import parse
from schemathesis import from_file
```

## Reasoning

When you see `datetime(...)` in the code, you know that it's from `datetime`. When you see `BaseView` in a Django project, you know that it's from Django framework. When you see `loads`, it can be anything: yaml, toml, json, custom config.

## Proposed solution

Consider the next blacklist:

+ `load`, `loads`
+ `dump`, `dumps`
+ `parse`
+ PyYAML specific: `safe_load`, `safe_dump`, `load_all`, `dump_all`, `safe_load_all`, `safe_dump_all`
+ one-letter names
+ Anything that starts from `from_` or `to_`



Discussion:
Awesome!
Can I take it?
Sure! Thanks a lot!
I assume this new violation goes under "Best practices"?
I will say that it is a consistency rule.

Resolving patch:
diff --git a/wemake_python_styleguide/constants.py b/wemake_python_styleguide/constants.py
--- a/wemake_python_styleguide/constants.py
+++ b/wemake_python_styleguide/constants.py
@@ -328,3 +328,20 @@
 # Used to specify a pattern which checks variables and modules for underscored
 # numbers in their names:
 UNDERSCORED_NUMBER_PATTERN: Final = re.compile(r'.+\D\_\d+(\D|$)')
+
+# List of vague method names that may cause confusion if imported as is:
+VAGUE_IMPORTS_BLACKLIST = frozenset([
+    'read',
+    'write',
+    'load',
+    'loads',
+    'dump',
+    'dumps',
+    'parse',
+    'safe_load',
+    'safe_dump',
+    'load_all',
+    'dump_all',
+    'safe_load_all',
+    'safe_dump_all',
+])
diff --git a/wemake_python_styleguide/violations/consistency.py b/wemake_python_styleguide/violations/consistency.py
--- a/wemake_python_styleguide/violations/consistency.py
+++ b/wemake_python_styleguide/violations/consistency.py
@@ -72,6 +72,7 @@
    ZeroDivisionViolation
    MeaninglessNumberOperationViolation
    OperationSignNegationViolation
+   VagueImportViolation
 
 Consistency checks
 ------------------
@@ -123,6 +124,7 @@
 .. autoclass:: ZeroDivisionViolation
 .. autoclass:: MeaninglessNumberOperationViolation
 .. autoclass:: OperationSignNegationViolation
+.. autoclass:: VagueImportViolation
 
 """
 
@@ -1748,3 +1750,37 @@ class OperationSignNegationViolation(ASTViolation):
 
     error_template = 'Found wrong operation sign'
     code = 346
+
+
+@final
+class VagueImportViolation(ASTViolation):
+    """
+    Forbids imports that outside of the module may cause confusion.
+
+    Reasoning:
+        See ``datetime.*`` in code? You know that it's from datetime.
+        See ``BaseView`` in a Django project? You know where it is from.
+        See ``loads``? It can be anything: ``yaml``, ``toml``, ``json`` ...
+
+    Example::
+
+        # Correct:
+        import json
+
+        ...
+
+        json.loads(content)
+
+        # Wrong:
+        from json import loads
+
+        ...
+
+        loads(content)
+
+    .. versionadded:: 0.13.0
+
+    """
+
+    error_template = 'Found vague import that may cause confusion: {0}'
+    code = 347
diff --git a/wemake_python_styleguide/visitors/ast/imports.py b/wemake_python_styleguide/visitors/ast/imports.py
--- a/wemake_python_styleguide/visitors/ast/imports.py
+++ b/wemake_python_styleguide/visitors/ast/imports.py
@@ -6,6 +6,7 @@
 
 from typing_extensions import final
 
+from wemake_python_styleguide import constants
 from wemake_python_styleguide.constants import FUTURE_IMPORTS_WHITELIST
 from wemake_python_styleguide.logic import imports, nodes
 from wemake_python_styleguide.logic.naming import access
@@ -19,6 +20,7 @@
 from wemake_python_styleguide.violations.consistency import (
     DottedRawImportViolation,
     LocalFolderImportViolation,
+    VagueImportViolation,
 )
 from wemake_python_styleguide.violations.naming import SameAliasImportViolation
 from wemake_python_styleguide.visitors.base import BaseNodeVisitor
@@ -64,6 +66,19 @@ def check_alias(self, node: AnyImport) -> None:
                     SameAliasImportViolation(node, text=alias.name),
                 )
 
+            for name in (alias.name, alias.asname):
+                if name is None:
+                    continue
+
+                blacklisted = name in constants.VAGUE_IMPORTS_BLACKLIST
+                with_from = name.startswith('from_')
+                with_to = name.startswith('to_')
+
+                if blacklisted or with_from or with_to or len(name) == 1:
+                    self._error_callback(
+                        VagueImportViolation(node, text=alias.name),
+                    )
+
     def check_protected_import(self, node: AnyImport) -> None:
         import_names = [alias.name for alias in node.names]
         for name in chain(imports.get_import_parts(node), import_names):


Test patch:
diff --git a/tests/fixtures/noqa.py b/tests/fixtures/noqa.py
--- a/tests/fixtures/noqa.py
+++ b/tests/fixtures/noqa.py
@@ -565,5 +565,11 @@ async def async_gen(self):
 def literal_none_func(arg: Literal[None]):  # noqa: WPS701
     '''Literal[None]'''
 
+
 def literal_none_return_func() -> Literal[None]:  # noqa: WPS701
     '''Literal[None]'''
+
+
+from json import loads  # noqa: WPS347
+from some_module import a  # noqa: WPS347
+from text import from_file  # noqa: WPS347
diff --git a/tests/test_checker/test_noqa.py b/tests/test_checker/test_noqa.py
--- a/tests/test_checker/test_noqa.py
+++ b/tests/test_checker/test_noqa.py
@@ -120,6 +120,7 @@
     'WPS344': 1,
     'WPS345': 1,
     'WPS346': 1,
+    'WPS347': 3,
 
     'WPS400': 0,
     'WPS401': 0,
diff --git a/tests/test_visitors/test_ast/test_imports/test_vague_imports.py b/tests/test_visitors/test_ast/test_imports/test_vague_imports.py
new file mode 100644
--- /dev/null
+++ b/tests/test_visitors/test_ast/test_imports/test_vague_imports.py
@@ -0,0 +1,51 @@
+# -*- coding: utf-8 -*-
+
+import pytest
+
+from wemake_python_styleguide import constants
+from wemake_python_styleguide.violations.consistency import (
+    VagueImportViolation,
+)
+from wemake_python_styleguide.visitors.ast.imports import WrongImportVisitor
+
+import_template = 'from mod import {0}'
+
+alias_import_template = 'from mod import something as {0}'
+
+
+@pytest.mark.parametrize(
+    'method_name',
+    list(constants.VAGUE_IMPORTS_BLACKLIST) + ['a', 'from_thing', 'to_thing'],
+)
+def test_vague_method_name_import(
+    assert_errors,
+    parse_ast_tree,
+    method_name,
+    default_options,
+):
+    """Testing that imports with the same aliases are restricted."""
+    tree = parse_ast_tree(import_template.format(method_name))
+
+    visitor = WrongImportVisitor(default_options, tree=tree)
+    visitor.run()
+
+    assert_errors(visitor, [VagueImportViolation])
+
+
+@pytest.mark.parametrize(
+    'alias',
+    list(constants.VAGUE_IMPORTS_BLACKLIST) + ['a', 'from_a', 'to_a'],
+)
+def test_vague_alias_import(
+    assert_errors,
+    parse_ast_tree,
+    alias,
+    default_options,
+):
+    """Testing that imports with the same aliases are restricted."""
+    tree = parse_ast_tree(alias_import_template.format(alias))
+
+    visitor = WrongImportVisitor(default_options, tree=tree)
+    visitor.run()
+
+    assert_errors(visitor, [VagueImportViolation])