├── smart_imports ├── plugins │ ├── __init__.pt │ ├── __init__.py │ └── mypy.py ├── tests │ ├── __init__.py │ ├── fixtures │ │ ├── python_3_5 │ │ │ └── full_parser_test.py │ │ ├── python_3_6 │ │ │ └── full_parser_test.py │ │ ├── python_3_7 │ │ │ └── full_parser_test.py │ │ ├── python_3_8 │ │ │ └── full_parser_test.py │ │ ├── python_3_9 │ │ │ └── full_parser_test.py │ │ └── python_3_10 │ │ │ └── full_parser_test.py │ ├── test_discovering.py │ ├── test_cache.py │ ├── test_config.py │ ├── test_importer.py │ ├── test_scopes_tree.py │ ├── test_rules.py │ └── test_ast_parser.py ├── __init__.py ├── constants.py ├── helpers.py ├── exceptions.py ├── discovering.py ├── cache.py ├── importer.py ├── config.py ├── scopes_tree.py ├── fixtures │ ├── python_3_5_packages.txt │ ├── python_3_6_packages.txt │ ├── python_3_7_packages.txt │ ├── python_3_8_packages.txt │ ├── python_3_9_packages.txt │ └── python_3_10_packages.txt ├── ast_parser.py └── rules.py ├── poetry.lock ├── .github └── workflows │ └── run_tests.yml ├── pyproject.toml ├── CHANGELOG.rst ├── LICENSE ├── .gitignore └── README.rst /smart_imports/plugins/__init__.pt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /smart_imports/plugins/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /smart_imports/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /smart_imports/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | from .importer import all 3 | 4 | 5 | __all__ = (all,) 6 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | package = [] 2 | 3 | [metadata] 4 | lock-version = "1.1" 5 | python-versions = ">=3.5" 6 | content-hash = "af1206dca1852e24b3c8de4095f9611669624b80e84a346fd47d58cba3f80d67" 7 | 8 | [metadata.files] 9 | -------------------------------------------------------------------------------- /smart_imports/constants.py: -------------------------------------------------------------------------------- 1 | 2 | import enum 3 | 4 | 5 | class SCOPE_TYPE(enum.Enum): 6 | NORMAL = 0 7 | CLASS = 1 8 | COMPREHENSION = 2 9 | 10 | 11 | class VARIABLE_STATE(enum.Enum): 12 | INITIALIZED = 0 13 | UNINITIALIZED = 1 14 | 15 | 16 | class VARIABLE_USAGE_TYPE(enum.Enum): 17 | FULLY_DEFINED = 0 18 | PARTIALY_DEFINED = 1 19 | FULLY_UNDEFINED = 2 20 | 21 | 22 | CONFIG_FILE_NAME = 'smart_imports.json' 23 | 24 | 25 | CACHE_PROTOCOL_VERSION = '1' 26 | -------------------------------------------------------------------------------- /smart_imports/helpers.py: -------------------------------------------------------------------------------- 1 | 2 | import sys 3 | import types 4 | import tempfile 5 | import contextlib 6 | 7 | from . import discovering 8 | 9 | 10 | def unload_test_packages(): 11 | for package_name in ['a']: 12 | for key, value in list(sys.modules.items()): 13 | if (key == package_name or key.startswith(package_name+'.')) and isinstance(value, types.ModuleType): 14 | del sys.modules[key] 15 | 16 | discovering.SPEC_CACHE.clear() 17 | 18 | 19 | @contextlib.contextmanager 20 | def test_directory(): 21 | unload_test_packages() 22 | 23 | with tempfile.TemporaryDirectory() as temp_directory: 24 | sys.path.append(temp_directory) 25 | yield temp_directory 26 | sys.path.pop() 27 | -------------------------------------------------------------------------------- /.github/workflows/run_tests.yml: -------------------------------------------------------------------------------- 1 | name: Run tests 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | 7 | build: 8 | 9 | runs-on: ubuntu-latest 10 | strategy: 11 | matrix: 12 | python-version: ['3.5', '3.6', '3.7', '3.8', '3.9', '3.10'] 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Set up Python ${{ matrix.python-version }} 17 | uses: actions/setup-python@v2 18 | with: 19 | python-version: ${{ matrix.python-version }} 20 | - name: Install dependencies 21 | run: | 22 | python -m pip install --upgrade pip 23 | pip install poetry 24 | poetry install 25 | - name: Run Tests 26 | run: poetry run python -m unittest discover -t . smart_imports 27 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "smart_imports" 3 | version = "0.2.7" 4 | description = "Automatic importing for Python modules." 5 | readme = "README.rst" 6 | repository = "https://github.com/Tiendil/smart-imports" 7 | authors = ["Aliaksei Yaletski (Tiendil) "] 8 | license = "BSD-3" 9 | include = ["*.txt", "*.json"] 10 | keywords = ["python", "import"] 11 | classifiers = [ 12 | "Development Status :: 5 - Production/Stable", 13 | 14 | "Intended Audience :: Developers", 15 | 16 | "License :: OSI Approved :: BSD License", 17 | 18 | "Programming Language :: Python :: 3", 19 | 20 | 'Programming Language :: Python :: 3', 21 | 'Programming Language :: Python :: 3.5', 22 | 'Programming Language :: Python :: 3.6', 23 | 'Programming Language :: Python :: 3.7', 24 | 'Programming Language :: Python :: 3.8', 25 | 'Programming Language :: Python :: 3.9', 26 | 'Programming Language :: Python :: 3.10', 27 | 28 | "Natural Language :: English", 29 | "Natural Language :: Russian"] 30 | 31 | [tool.poetry.dependencies] 32 | python = ">=3.5" 33 | -------------------------------------------------------------------------------- /smart_imports/exceptions.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | class SmartImportsError(Exception): 4 | MESSAGE = None 5 | 6 | def __init__(self, **kwargs): 7 | super(SmartImportsError, self).__init__(self.MESSAGE.format(**kwargs)) 8 | self.arguments = kwargs 9 | 10 | 11 | class ConfigError(SmartImportsError): 12 | MESSAGE = None 13 | 14 | 15 | class ConfigNotFound(ConfigError): 16 | MESSAGE = 'config "{path}" does not exists' 17 | 18 | 19 | class ConfigHasWrongFormat(ConfigError): 20 | MESSAGE = 'config "{path}" has wrong format: {message}' 21 | 22 | 23 | class ImporterError(SmartImportsError): 24 | MESSAGE = None 25 | 26 | 27 | class NoImportFound(ImporterError): 28 | MESSAGE = 'can not find import rule for variable "{variable}"\n\n' \ 29 | 'module: "{module}"\n' \ 30 | 'file: {path}\n' \ 31 | 'lines: {lines}' 32 | 33 | 34 | class RulesError(ImporterError): 35 | MESSAGE = None 36 | 37 | 38 | class RuleAlreadyRegistered(RulesError): 39 | MESSAGE = 'rule "{rule}" has been registered already' 40 | 41 | 42 | class RuleNotRegistered(RulesError): 43 | MESSAGE = 'rule "{rule}" has not registered' 44 | -------------------------------------------------------------------------------- /smart_imports/discovering.py: -------------------------------------------------------------------------------- 1 | 2 | import sys 3 | import inspect 4 | import importlib 5 | 6 | 7 | def find_target_module(): 8 | # can not use inspect.stack() here becouse of bug, look: 9 | # - https://github.com/ipython/ipython/issues/1456/ 10 | # - https://github.com/ipython/ipython/commit/298fdab5025745cd25f7f48147d8bc4c65be9d4a#diff-fd943bf091e5f13c5ef9b58043fa5129R209 11 | # - https://mail.python.org/pipermail/python-list/2010-September/587974.html 12 | # 13 | # instead emulate simplier behaviour (and, probably, faster) 14 | 15 | frame = sys._getframe(1) 16 | 17 | while frame: 18 | if frame.f_code.co_name == '': 19 | # faster than inspect.getmodule(frame) 20 | for module in sys.modules.values(): 21 | if getattr(module, '__file__', None) == frame.f_code.co_filename: 22 | return module 23 | 24 | return sys.modules[inspect.getmodulename(frame.f_code.co_filename)] 25 | 26 | frame = frame.f_back 27 | 28 | 29 | SPEC_CACHE = {} 30 | 31 | 32 | def find_spec(module_name): 33 | if module_name not in SPEC_CACHE: 34 | spec = importlib.util.find_spec(module_name) 35 | 36 | # prevent python from determining empty directories ('fixtures' directory, 'jinja2' templates for django) as namespace packages 37 | if spec is not None and spec.origin is None: 38 | spec = None 39 | 40 | SPEC_CACHE[module_name] = spec 41 | 42 | return SPEC_CACHE[module_name] 43 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | 2 | ########## 3 | Change log 4 | ########## 5 | 6 | This document records all notable changes to `smart_imports`. 7 | 8 | ----- 9 | 0.2.7 10 | ----- 11 | 12 | * Add support for Python 3.10 `gh-16 `_ 13 | 14 | ----- 15 | 0.2.6 16 | ----- 17 | 18 | * Add support for Python 3.9 `gh-16 `_ 19 | 20 | ----- 21 | 0.2.5 22 | ----- 23 | 24 | * Add support for MyPy `gh-15 `_ 25 | 26 | ----- 27 | 0.2.4 28 | ----- 29 | 30 | * Add support for Python 3.8 `gh-12 `_ 31 | * Prevent python from determining empty directories as namespace packages `gh-13 `_ 32 | 33 | ----- 34 | 0.2.3 35 | ----- 36 | 37 | * Add support for Python 3.6 `gh-1 `_ 38 | * Add support for Python 3.7 `gh-1 `_ 39 | * Implement obtaining modules' source code by standard Python functionality `gh-a142548 `_ 40 | * Add string numbers to import error messages `gh-6 `_ 41 | * Refactoring functional rules into class based rules `gh-7 `_ 42 | * Add optional caching `gh-10 `_ 43 | 44 | ----- 45 | 0.1.0 46 | ----- 47 | 48 | * Initial implementation 49 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2018, Aliaksei Yaletski 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | 106 | *~ -------------------------------------------------------------------------------- /smart_imports/tests/fixtures/python_3_5/full_parser_test.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | var_1 = True 4 | 5 | var_2 = var_3 # var_3 undefined 6 | 7 | 8 | def function_1(var_4: annotation_1, *var_5, **var_6) -> annotation_2: 9 | return var_4 + var_5[var_7] + var_6.get(var_8) # var_7 & var_8 undefined 10 | 11 | 12 | def function_2(var_9): 13 | 14 | def closure(var_10): 15 | return var_1 + var_9 + var_10 + var_11() # var 11 undefined 16 | 17 | return closure 18 | 19 | 20 | class Class(var_12): # var_12 undefined 21 | 22 | var_13 = var_2 23 | 24 | var_14 = var_1 + abs(var_9) # var_9 undefined 25 | 26 | def __init__(self): 27 | super().__init__(var_15+var_13) # var_13 & var_15 undefined 28 | 29 | 30 | var_16 = [var_17 for var_17 in range(10)] 31 | var_18 = {var_19 for var_19 in range(10)} 32 | var_20 = {var_21:var_22 for var_21 in zip(range(10), range(10, 20))} 33 | var_23 = (var_24 for var_23 in range(10)) 34 | 35 | 36 | for var_25 in range(10): 37 | pass 38 | 39 | var_26 = var_27 40 | 41 | 42 | var_28 = {var_29:[var_20[var_31] for var_31 in range(10)] 43 | for var_29 in range(10)} 44 | 45 | 46 | class OtherClass: 47 | 48 | var_32 = 1 49 | 50 | def var_33(self, func): 51 | pass 52 | 53 | @var_33 54 | def method_1(self, var_34=var_32): 55 | pass 56 | 57 | 58 | var_35 = lambda var_36, var_37=var_38: var_36+var_37+var_39 59 | 60 | 61 | var_40 = [var_41 + var_42 + var_43 62 | for var_41, var_43 in var_35 63 | if var_43 and var_44] 64 | 65 | 66 | {(var_45, var_46) 67 | for var_45 in {(var_46, var_47) 68 | for var_46 in var_48}} 69 | 70 | 71 | try: 72 | var_49 = var_50 73 | except var_51 as var_52: 74 | var_53(var_50, var_52, var_51, var_49) 75 | raise var_55 76 | 77 | 78 | async def function_3(): 79 | var_56 = 1 80 | 81 | def function_4(): 82 | print(var_56) # defined 83 | 84 | await var_49 85 | 86 | await var_57 87 | 88 | return function_4 89 | 90 | 91 | # unicode 92 | 93 | переменная_1 = перменная_2 + var_1 94 | -------------------------------------------------------------------------------- /smart_imports/plugins/mypy.py: -------------------------------------------------------------------------------- 1 | 2 | import importlib 3 | 4 | from mypy.plugin import Plugin 5 | from mypy import nodes 6 | 7 | from smart_imports import config as sm_config 8 | from smart_imports import importer as sm_importer 9 | 10 | 11 | class SmartImportsPlugin(Plugin): 12 | 13 | # search for imports and add them to AST of files, processed by mypy 14 | def get_additional_deps(self, file_node): 15 | 16 | smart_import_import = None 17 | 18 | for imported_module in file_node.imports: 19 | 20 | if not isinstance(imported_module, nodes.Import): 21 | # TODO: check all nodes 22 | continue 23 | 24 | for fullname, name in imported_module.ids: 25 | if fullname == 'smart_imports': 26 | smart_import_import = imported_module 27 | break 28 | 29 | if smart_import_import is None: 30 | continue 31 | 32 | if smart_import_import is None: 33 | return [] 34 | 35 | import_index = file_node.defs.index(smart_import_import) + 1 36 | 37 | module_config = sm_config.get(file_node.path) 38 | 39 | target_module = importlib.import_module(file_node.fullname) 40 | 41 | commands = sm_importer.process_module(module_config=module_config, 42 | module=target_module, 43 | variables_processor=sm_importer.variables_processor) 44 | 45 | dependencies = [] 46 | 47 | for command in commands: 48 | dependencies.append((0, command.source_module, -1)) 49 | 50 | if command.source_attribute is None: 51 | new_import = nodes.Import(ids=[(command.source_module, command.target_attribute)]) 52 | else: 53 | names = [(command.source_attribute, 54 | command.target_attribute if command.source_attribute != command.target_attribute else None)] 55 | 56 | new_import = nodes.ImportFrom(id=command.source_module, 57 | names=names, 58 | relative=0) 59 | 60 | new_import.line = smart_import_import.line 61 | new_import.column = smart_import_import.column 62 | new_import.end_line = smart_import_import.end_line 63 | new_import.is_top_level = True 64 | new_import.is_unreachable = False 65 | 66 | file_node.defs.insert(import_index, new_import) 67 | file_node.imports.append(new_import) 68 | 69 | return dependencies 70 | 71 | 72 | def plugin(version: str): 73 | return SmartImportsPlugin 74 | -------------------------------------------------------------------------------- /smart_imports/tests/test_discovering.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | import unittest 4 | 5 | from .. import helpers 6 | from .. import discovering 7 | 8 | 9 | class TestFindSpec(unittest.TestCase): 10 | 11 | def prepair_modules(self, base_directory): 12 | os.makedirs(os.path.join(base_directory, 'a', 'b', 'c')) 13 | os.makedirs(os.path.join(base_directory, 'a', 'b', 'd')) 14 | 15 | with open(os.path.join(base_directory, 'a', '__init__.py'), 'w') as f: 16 | f.write(' ') 17 | 18 | with open(os.path.join(base_directory, 'a', 'x.py'), 'w') as f: 19 | f.write(' ') 20 | 21 | with open(os.path.join(base_directory, 'a', 'b', '__init__.py'), 'w') as f: 22 | f.write(' ') 23 | 24 | with open(os.path.join(base_directory, 'a', 'b', 'y.py'), 'w') as f: 25 | f.write(' ') 26 | 27 | def test_no_spec(self): 28 | with helpers.test_directory() as temp_directory: 29 | self.prepair_modules(temp_directory) 30 | 31 | spec = discovering.find_spec('a.c') 32 | 33 | self.assertEqual(spec, None) 34 | self.assertEqual(discovering.SPEC_CACHE, {'a.c': None}) 35 | 36 | def test_spec_found(self): 37 | with helpers.test_directory() as temp_directory: 38 | self.prepair_modules(temp_directory) 39 | 40 | spec = discovering.find_spec('a.b') 41 | 42 | self.assertEqual(spec.name, 'a.b') 43 | self.assertEqual(discovering.SPEC_CACHE, {'a.b': spec}) 44 | 45 | def test_spec_from_cache(self): 46 | with helpers.test_directory() as temp_directory: 47 | self.prepair_modules(temp_directory) 48 | 49 | spec_1 = discovering.find_spec('a.b') 50 | spec_2 = discovering.find_spec('a.b') 51 | 52 | self.assertEqual(spec_1.name, 'a.b') 53 | self.assertEqual(discovering.SPEC_CACHE, {'a.b': spec_1}) 54 | 55 | self.assertTrue(spec_1 is spec_2) 56 | 57 | def test_multiple_modules(self): 58 | with helpers.test_directory() as temp_directory: 59 | self.prepair_modules(temp_directory) 60 | 61 | spec_1 = discovering.find_spec('a.b') 62 | spec_2 = discovering.find_spec('a.x') 63 | 64 | self.assertEqual(spec_1.name, 'a.b') 65 | self.assertEqual(spec_2.name, 'a.x') 66 | self.assertEqual(discovering.SPEC_CACHE, {'a.b': spec_1, 67 | 'a.x': spec_2}) 68 | 69 | def test_fake_namespace_package(self): 70 | with helpers.test_directory() as temp_directory: 71 | self.prepair_modules(temp_directory) 72 | 73 | self.assertEqual(discovering.find_spec('a.d'), None) 74 | -------------------------------------------------------------------------------- /smart_imports/tests/fixtures/python_3_6/full_parser_test.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | var_1 = True 4 | 5 | var_2 = var_3 # var_3 undefined 6 | 7 | 8 | def function_1(var_4: annotation_1, *var_5, **var_6) -> annotation_2: 9 | return var_4 + var_5[var_7] + var_6.get(var_8) # var_7 & var_8 undefined 10 | 11 | 12 | def function_2(var_9): 13 | 14 | def closure(var_10): 15 | return var_1 + var_9 + var_10 + var_11() # var 11 undefined 16 | 17 | return closure 18 | 19 | 20 | class Class(var_12): # var_12 undefined 21 | 22 | var_13 = var_2 23 | 24 | var_14 = var_1 + abs(var_9) # var_9 undefined 25 | 26 | def __init__(self): 27 | super().__init__(var_15+var_13) # var_13 & var_15 undefined 28 | 29 | 30 | var_16 = [var_17 for var_17 in range(10)] 31 | var_18 = {var_19 for var_19 in range(10)} 32 | var_20 = {var_21:var_22 for var_21 in zip(range(10), range(10, 20))} 33 | var_23 = (var_24 for var_23 in range(10)) 34 | 35 | 36 | for var_25 in range(10): 37 | pass 38 | 39 | var_26 = var_27 40 | 41 | 42 | var_28 = {var_29:[var_20[var_31] for var_31 in range(10)] 43 | for var_29 in range(10)} 44 | 45 | 46 | class OtherClass: 47 | 48 | var_32 = 1 49 | 50 | def var_33(self, func): 51 | pass 52 | 53 | @var_33 54 | def method_1(self, var_34=var_32): 55 | pass 56 | 57 | 58 | var_35 = lambda var_36, var_37=var_38: var_36+var_37+var_39 59 | 60 | 61 | var_40 = [var_41 + var_42 + var_43 62 | for var_41, var_43 in var_35 63 | if var_43 and var_44] 64 | 65 | 66 | {(var_45, var_46) 67 | for var_45 in {(var_46, var_47) 68 | for var_46 in var_48}} 69 | 70 | 71 | try: 72 | var_49 = var_50 73 | except var_51 as var_52: 74 | var_53(var_50, var_52, var_51, var_49) 75 | raise var_55 76 | 77 | 78 | async def function_3(): 79 | var_56 = 1 80 | 81 | def function_4(): 82 | print(var_56) # defined 83 | 84 | await var_49 85 | 86 | await var_57 87 | 88 | return function_4 89 | 90 | 91 | # formatted string literals 92 | 93 | var_58 = "Fred" 94 | 95 | f"He said his name is {var_58} and {var_59}." 96 | 97 | var_60 = 10 98 | 99 | f"result: {var_58:{var_60}.{var_61}}" 100 | 101 | 102 | # variable annotations 103 | 104 | var_62: var_63[var_58] = var_65 105 | 106 | var_66: var_67 # Note: no initial value! 107 | 108 | 109 | class var_68: 110 | var_69: var_70(var_66, var_72) = {} 111 | 112 | 113 | # asynchronous comprehensions 114 | 115 | async def fuction_4(): 116 | var_66 = [(var_73 + var_74+var_81) async for var_73, var_74 in var_75() if (var_76 + var_74) % 2] 117 | 118 | var_66 = [await var_77() for var_77, var_78 in var_79 if await var_80(var_78)] 119 | 120 | 121 | # unicode 122 | 123 | переменная_1 = перменная_2 + var_1 124 | -------------------------------------------------------------------------------- /smart_imports/tests/fixtures/python_3_7/full_parser_test.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | var_1 = True 4 | 5 | var_2 = var_3 # var_3 undefined 6 | 7 | 8 | def function_1(var_4: annotation_1, *var_5, **var_6) -> annotation_2: 9 | return var_4 + var_5[var_7] + var_6.get(var_8) # var_7 & var_8 undefined 10 | 11 | 12 | def function_2(var_9): 13 | 14 | def closure(var_10): 15 | return var_1 + var_9 + var_10 + var_11() # var 11 undefined 16 | 17 | return closure 18 | 19 | 20 | class Class(var_12): # var_12 undefined 21 | 22 | var_13 = var_2 23 | 24 | var_14 = var_1 + abs(var_9) # var_9 undefined 25 | 26 | def __init__(self): 27 | super().__init__(var_15+var_13) # var_13 & var_15 undefined 28 | 29 | 30 | var_16 = [var_17 for var_17 in range(10)] 31 | var_18 = {var_19 for var_19 in range(10)} 32 | var_20 = {var_21:var_22 for var_21 in zip(range(10), range(10, 20))} 33 | var_23 = (var_24 for var_23 in range(10)) 34 | 35 | 36 | for var_25 in range(10): 37 | pass 38 | 39 | var_26 = var_27 40 | 41 | 42 | var_28 = {var_29:[var_20[var_31] for var_31 in range(10)] 43 | for var_29 in range(10)} 44 | 45 | 46 | class OtherClass: 47 | 48 | var_32 = 1 49 | 50 | def var_33(self, func): 51 | pass 52 | 53 | @var_33 54 | def method_1(self, var_34=var_32): 55 | pass 56 | 57 | 58 | var_35 = lambda var_36, var_37=var_38: var_36+var_37+var_39 59 | 60 | 61 | var_40 = [var_41 + var_42 + var_43 62 | for var_41, var_43 in var_35 63 | if var_43 and var_44] 64 | 65 | 66 | {(var_45, var_46) 67 | for var_45 in {(var_46, var_47) 68 | for var_46 in var_48}} 69 | 70 | 71 | try: 72 | var_49 = var_50 73 | except var_51 as var_52: 74 | var_53(var_50, var_52, var_51, var_49) 75 | raise var_55 76 | 77 | 78 | async def function_3(): 79 | var_56 = 1 80 | 81 | def function_4(): 82 | print(var_56) # defined 83 | 84 | await var_49 85 | 86 | await var_57 87 | 88 | return function_4 89 | 90 | 91 | # formatted string literals 92 | 93 | var_58 = "Fred" 94 | 95 | f"He said his name is {var_58} and {var_59}." 96 | 97 | var_60 = 10 98 | 99 | f"result: {var_58:{var_60}.{var_61}}" 100 | 101 | 102 | # variable annotations 103 | 104 | var_62: var_63[var_58] = var_65 105 | 106 | var_66: var_67 # Note: no initial value! 107 | 108 | 109 | class var_68: 110 | var_69: var_70(var_66, var_72) = {} 111 | 112 | 113 | # asynchronous comprehensions 114 | 115 | async def fuction_4(): 116 | var_66 = [(var_73 + var_74+var_81) async for var_73, var_74 in var_75() if (var_76 + var_74) % 2] 117 | 118 | var_66 = [await var_77() for var_77, var_78 in var_79 if await var_80(var_78)] 119 | 120 | 121 | # unicode 122 | 123 | переменная_1 = перменная_2 + var_1 124 | -------------------------------------------------------------------------------- /smart_imports/cache.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | import pathlib 4 | import hashlib 5 | import warnings 6 | import functools 7 | 8 | from . import constants 9 | 10 | 11 | WARNING_MESSAGE = 'Error while accessing smart imports cache' 12 | 13 | 14 | def get_checksum(source): 15 | return hashlib.sha256(source.encode('utf-8')).hexdigest() 16 | 17 | 18 | def get_cache_path(cache_dir, module_name): 19 | return os.path.join(cache_dir, module_name + '.cache') 20 | 21 | 22 | def ignore_errors(function): 23 | 24 | @functools.wraps(function) 25 | def wrapper(*argv, **kwargs): 26 | try: 27 | return function(*argv, **kwargs) 28 | except Exception as e: 29 | warnings.warn('{}: {}'.format(WARNING_MESSAGE, e), UserWarning, stacklevel=2) 30 | return None 31 | 32 | return wrapper 33 | 34 | 35 | @ignore_errors 36 | def get(cache_dir, module_name, checksum): 37 | 38 | cache_path = get_cache_path(cache_dir, module_name) 39 | 40 | if not os.path.isfile(cache_path): 41 | return None 42 | 43 | with open(cache_path) as f: 44 | protocol_version = f.readline().strip() 45 | 46 | if protocol_version != constants.CACHE_PROTOCOL_VERSION: 47 | return None 48 | 49 | saved_checksum = f.readline().strip() 50 | 51 | if saved_checksum != checksum: 52 | return None 53 | 54 | variables = [variable.strip() for variable in f.readlines()] 55 | 56 | return variables 57 | 58 | 59 | @ignore_errors 60 | def set(cache_dir, module_name, checksum, variables): 61 | pathlib.Path(cache_dir).mkdir(parents=True, exist_ok=True) 62 | 63 | cache_path = get_cache_path(cache_dir, module_name) 64 | 65 | with open(cache_path, 'w') as f: 66 | f.write(constants.CACHE_PROTOCOL_VERSION) 67 | f.write('\n') 68 | 69 | f.write(checksum) 70 | f.write('\n') 71 | 72 | for variable in variables: 73 | f.write(variable) 74 | f.write('\n') 75 | 76 | 77 | class Cache: 78 | __slots__ = ('cache_dir', 'module_name', 'checksum',) 79 | 80 | def __init__(self, cache_dir, module_name, source): 81 | self.cache_dir = cache_dir 82 | self.module_name = module_name 83 | self.checksum = get_checksum(source) 84 | 85 | def get(self): 86 | if self.cache_dir is None: 87 | return None 88 | 89 | return get(cache_dir=self.cache_dir, 90 | module_name=self.module_name, 91 | checksum=self.checksum) 92 | 93 | def set(self, variables): 94 | if self.cache_dir is None: 95 | return None 96 | 97 | set(cache_dir=self.cache_dir, 98 | module_name=self.module_name, 99 | checksum=self.checksum, 100 | variables=variables) 101 | -------------------------------------------------------------------------------- /smart_imports/tests/fixtures/python_3_8/full_parser_test.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | var_1 = True 4 | 5 | var_2 = var_3 # var_3 undefined 6 | 7 | 8 | def function_1(var_4: annotation_1, *var_5, **var_6) -> annotation_2: 9 | return var_4 + var_5[var_7] + var_6.get(var_8) # var_7 & var_8 undefined 10 | 11 | 12 | def function_2(var_9): 13 | 14 | def closure(var_10): 15 | return var_1 + var_9 + var_10 + var_11() # var 11 undefined 16 | 17 | return closure 18 | 19 | 20 | class Class(var_12): # var_12 undefined 21 | 22 | var_13 = var_2 23 | 24 | var_14 = var_1 + abs(var_9) # var_9 undefined 25 | 26 | def __init__(self): 27 | super().__init__(var_15+var_13) # var_13 & var_15 undefined 28 | 29 | 30 | var_16 = [var_17 for var_17 in range(10)] 31 | var_18 = {var_19 for var_19 in range(10)} 32 | var_20 = {var_21:var_22 for var_21 in zip(range(10), range(10, 20))} 33 | var_23 = (var_24 for var_23 in range(10)) 34 | 35 | 36 | for var_25 in range(10): 37 | pass 38 | 39 | var_26 = var_27 40 | 41 | 42 | var_28 = {var_29:[var_20[var_31] for var_31 in range(10)] 43 | for var_29 in range(10)} 44 | 45 | 46 | class OtherClass: 47 | 48 | var_32 = 1 49 | 50 | def var_33(self, func): 51 | pass 52 | 53 | @var_33 54 | def method_1(self, var_34=var_32): 55 | pass 56 | 57 | 58 | var_35 = lambda var_36, var_37=var_38: var_36+var_37+var_39 59 | 60 | 61 | var_40 = [var_41 + var_42 + var_43 62 | for var_41, var_43 in var_35 63 | if var_43 and var_44] 64 | 65 | 66 | {(var_45, var_46) 67 | for var_45 in {(var_46, var_47) 68 | for var_46 in var_48}} 69 | 70 | 71 | try: 72 | var_49 = var_50 73 | except var_51 as var_52: 74 | var_53(var_50, var_52, var_51, var_49) 75 | raise var_55 76 | 77 | 78 | async def function_3(): 79 | var_56 = 1 80 | 81 | def function_4(): 82 | print(var_56) # defined 83 | 84 | await var_49 85 | 86 | await var_57 87 | 88 | return function_4 89 | 90 | 91 | # formatted string literals 92 | 93 | var_58 = 'Fred' 94 | 95 | f'He said his name is {var_58} and {var_59}.' 96 | 97 | var_60 = 10 98 | 99 | f'result: {var_58:{var_60}.{var_61}}' 100 | 101 | 102 | # variable annotations 103 | 104 | var_62: var_63[var_58] = var_65 105 | 106 | var_66: var_67 # Note: no initial value! 107 | 108 | 109 | class var_68: 110 | var_69: var_70(var_66, var_72) = {} 111 | 112 | 113 | # asynchronous comprehensions 114 | 115 | async def fuction_4(): 116 | var_66 = [(var_73 + var_74+var_81) async for var_73, var_74 in var_75() if (var_76 + var_74) % 2] 117 | 118 | var_66 = [await var_77() for var_77, var_78 in var_79 if await var_80(var_78)] 119 | 120 | 121 | # unicode 122 | 123 | переменная_1 = перменная_2 + var_1 124 | 125 | 126 | # assigment expressions 127 | 128 | if (var_82 := var_83 + var_66) > var_84 + var_82: 129 | pass 130 | 131 | 132 | # position only parameters 133 | 134 | def var_85(var_86, var_87, /, var_89): 135 | return var_86 + var_87 + var_89 136 | 137 | 138 | # f-string self-documenting expressions 139 | 140 | f'{var_82=}, {var_90=}' 141 | -------------------------------------------------------------------------------- /smart_imports/tests/fixtures/python_3_9/full_parser_test.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | var_1 = True 4 | 5 | var_2 = var_3 # var_3 undefined 6 | 7 | 8 | def function_1(var_4: annotation_1, *var_5, **var_6) -> annotation_2: 9 | return var_4 + var_5[var_7] + var_6.get(var_8) # var_7 & var_8 undefined 10 | 11 | 12 | def function_2(var_9): 13 | 14 | def closure(var_10): 15 | return var_1 + var_9 + var_10 + var_11() # var 11 undefined 16 | 17 | return closure 18 | 19 | 20 | class Class(var_12): # var_12 undefined 21 | 22 | var_13 = var_2 23 | 24 | var_14 = var_1 + abs(var_9) # var_9 undefined 25 | 26 | def __init__(self): 27 | super().__init__(var_15+var_13) # var_13 & var_15 undefined 28 | 29 | 30 | var_16 = [var_17 for var_17 in range(10)] 31 | var_18 = {var_19 for var_19 in range(10)} 32 | var_20 = {var_21:var_22 for var_21 in zip(range(10), range(10, 20))} 33 | var_23 = (var_24 for var_23 in range(10)) 34 | 35 | 36 | for var_25 in range(10): 37 | pass 38 | 39 | var_26 = var_27 40 | 41 | 42 | var_28 = {var_29:[var_20[var_31] for var_31 in range(10)] 43 | for var_29 in range(10)} 44 | 45 | 46 | class OtherClass: 47 | 48 | var_32 = 1 49 | 50 | def var_33(self, func): 51 | pass 52 | 53 | @var_33 54 | def method_1(self, var_34=var_32): 55 | pass 56 | 57 | 58 | var_35 = lambda var_36, var_37=var_38: var_36+var_37+var_39 59 | 60 | 61 | var_40 = [var_41 + var_42 + var_43 62 | for var_41, var_43 in var_35 63 | if var_43 and var_44] 64 | 65 | 66 | {(var_45, var_46) 67 | for var_45 in {(var_46, var_47) 68 | for var_46 in var_48}} 69 | 70 | 71 | try: 72 | var_49 = var_50 73 | except var_51 as var_52: 74 | var_53(var_50, var_52, var_51, var_49) 75 | raise var_55 76 | 77 | 78 | async def function_3(): 79 | var_56 = 1 80 | 81 | def function_4(): 82 | print(var_56) # defined 83 | 84 | await var_49 85 | 86 | await var_57 87 | 88 | return function_4 89 | 90 | 91 | # formatted string literals 92 | 93 | var_58 = 'Fred' 94 | 95 | f'He said his name is {var_58} and {var_59}.' 96 | 97 | var_60 = 10 98 | 99 | f'result: {var_58:{var_60}.{var_61}}' 100 | 101 | 102 | # variable annotations 103 | 104 | var_62: var_63[var_58] = var_65 105 | 106 | var_66: var_67 # Note: no initial value! 107 | 108 | 109 | class var_68: 110 | var_69: var_70(var_66, var_72) = {} 111 | 112 | 113 | # asynchronous comprehensions 114 | 115 | async def fuction_4(): 116 | var_66 = [(var_73 + var_74+var_81) async for var_73, var_74 in var_75() if (var_76 + var_74) % 2] 117 | 118 | var_66 = [await var_77() for var_77, var_78 in var_79 if await var_80(var_78)] 119 | 120 | 121 | # unicode 122 | 123 | переменная_1 = перменная_2 + var_1 124 | 125 | 126 | # assigment expressions 127 | 128 | if (var_82 := var_83 + var_66) > var_84 + var_82: 129 | pass 130 | 131 | 132 | # position only parameters 133 | 134 | def var_85(var_86, var_87, /, var_89): 135 | return var_86 + var_87 + var_89 136 | 137 | 138 | # f-string self-documenting expressions 139 | 140 | f'{var_82=}, {var_90=}' 141 | 142 | 143 | # extended decorator syntax 144 | @var_91[var_62].value 145 | def var_92(): 146 | pass 147 | 148 | 149 | # extended decorator syntax 150 | @function_2[var_93].value 151 | def var_94(): 152 | pass 153 | -------------------------------------------------------------------------------- /smart_imports/importer.py: -------------------------------------------------------------------------------- 1 | 2 | import ast 3 | 4 | from . import cache 5 | from . import rules 6 | from . import config 7 | from . import ast_parser 8 | from . import exceptions 9 | from . import scopes_tree 10 | from . import discovering 11 | 12 | 13 | def apply_rules(module_config, module, variable): 14 | 15 | for rule in rules.get_for_config(module_config): 16 | command = rule.apply(module, variable) 17 | 18 | if command: 19 | return command 20 | 21 | return None 22 | 23 | 24 | def get_module_scopes_tree(source): 25 | tree = ast.parse(source) 26 | 27 | analyzer = ast_parser.Analyzer() 28 | 29 | analyzer.visit(tree) 30 | 31 | return analyzer.scope 32 | 33 | 34 | def variables_processor(variables): 35 | return variables 36 | 37 | 38 | def extract_variables(source): 39 | 40 | root_scope = get_module_scopes_tree(source) 41 | 42 | variables = scopes_tree.search_candidates_to_import(root_scope) 43 | 44 | fully_undefined_variables, partialy_undefined_variables, variables_scopes = variables 45 | 46 | variables = list(fully_undefined_variables) 47 | variables.extend(partialy_undefined_variables) 48 | 49 | return variables, variables_scopes 50 | 51 | 52 | def process_module(module_config, module, variables_processor=variables_processor): 53 | 54 | source = module.__loader__.get_source(module.__name__) 55 | 56 | parser_cache = cache.Cache(cache_dir=module_config.cache_dir, 57 | module_name=module.__name__, 58 | source=source) 59 | 60 | variables = parser_cache.get() 61 | 62 | variables_scopes = None 63 | 64 | if variables is None: 65 | variables, variables_scopes = extract_variables(source=source) 66 | 67 | parser_cache.set(variables) 68 | 69 | # sort variables to fixate import order 70 | variables.sort() 71 | 72 | variables = variables_processor(variables) 73 | 74 | commands = [] 75 | 76 | for variable in variables: 77 | command = apply_rules(module_config=module_config, 78 | module=module, 79 | variable=variable) 80 | 81 | if isinstance(command, rules.NoImportCommand): 82 | continue 83 | 84 | if command is not None: 85 | commands.append(command) 86 | continue 87 | 88 | # process import error 89 | if variables_scopes is None: 90 | _, variables_scopes = extract_variables(source=source) 91 | 92 | undefined_lines = scopes_tree.search_undefined_variable_lines(variable, variables_scopes[variable]) 93 | 94 | raise exceptions.NoImportFound(variable=variable, 95 | module=module.__name__, 96 | path=module.__file__, 97 | lines=undefined_lines) 98 | 99 | return commands 100 | 101 | 102 | def all(target_module=None, variables_processor=variables_processor): 103 | 104 | if target_module is None: 105 | target_module = discovering.find_target_module() 106 | 107 | module_config = config.get(target_module.__file__) 108 | 109 | commands = process_module(module_config=module_config, 110 | module=target_module, 111 | variables_processor=variables_processor) 112 | 113 | for command in commands: 114 | command() 115 | -------------------------------------------------------------------------------- /smart_imports/config.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | import json 4 | import copy 5 | import pathlib 6 | 7 | from . import constants 8 | from . import exceptions 9 | 10 | 11 | CONFIGS_CACHE = {} 12 | 13 | 14 | def expand_cache_dir_path(config_path, cache_dir): 15 | 16 | if cache_dir is None: 17 | return cache_dir 18 | 19 | path = pathlib.Path(cache_dir) 20 | 21 | if path.is_absolute(): 22 | return cache_dir 23 | 24 | if cache_dir[0] == '~': 25 | return str(path.expanduser()) 26 | 27 | return str(pathlib.Path(config_path).parent / cache_dir) 28 | 29 | 30 | class Config: 31 | __slots__ = ('path', 'cache_dir', 'rules') 32 | 33 | def __init__(self): 34 | self.path = None 35 | self.cache_dir = None 36 | self.rules = [] 37 | 38 | @property 39 | def uid(self): 40 | return self.path 41 | 42 | def initialize(self, path, data): 43 | self.path = path 44 | 45 | self.cache_dir = expand_cache_dir_path(config_path=path, 46 | cache_dir=data.get('cache_dir', self.cache_dir)) 47 | 48 | if 'rules' not in data: 49 | raise exceptions.ConfigHasWrongFormat(path=path, message='"rules" MUST be defined') 50 | 51 | self.rules = data['rules'] 52 | 53 | for rule_config in self.rules: 54 | if 'type' not in rule_config: 55 | raise exceptions.ConfigHasWrongFormat(path=path, message='rule type does not specified for every rule') 56 | 57 | def serialize(self): 58 | return {'path': self.path, 59 | 'cache_dir': self.cache_dir, 60 | 'rules': self.rules} 61 | 62 | def clone(self, **kwargs): 63 | clone = copy.deepcopy(self) 64 | 65 | for field, value in kwargs.items(): 66 | setattr(clone, field, value) 67 | 68 | return clone 69 | 70 | def __eq__(self, other): 71 | return (self.__class__ == other.__class__ and 72 | all(getattr(self, field) == getattr(other, field) for field in self.__slots__)) 73 | 74 | def __ne__(self, other): 75 | return not self.__eq__(other) 76 | 77 | 78 | DEFAULT_CONFIG = Config() 79 | 80 | DEFAULT_CONFIG.initialize(path='#default_config', 81 | data={'rules': [{'type': 'rule_local_modules'}, 82 | {'type': 'rule_stdlib'}, 83 | {'type': 'rule_predefined_names'}, 84 | {'type': 'rule_global_modules'}]}) 85 | 86 | 87 | def get(path, config_name=constants.CONFIG_FILE_NAME): 88 | 89 | if not os.path.isdir(path): 90 | path = os.path.dirname(path) 91 | 92 | config = None 93 | checked_paths = [] 94 | 95 | while path not in ('', '/'): 96 | 97 | if path in CONFIGS_CACHE: 98 | config = CONFIGS_CACHE[path] 99 | break 100 | 101 | checked_paths.append(path) 102 | 103 | config_path = os.path.join(path, config_name) 104 | 105 | if os.path.isfile(config_path): 106 | config = load(config_path) 107 | break 108 | 109 | path = os.path.dirname(path) 110 | 111 | if config is None: 112 | config = DEFAULT_CONFIG.clone(path=path) 113 | 114 | for path in checked_paths: 115 | CONFIGS_CACHE[path] = config 116 | 117 | return config 118 | 119 | 120 | def load(path): 121 | 122 | if not os.path.isfile(path): 123 | raise exceptions.ConfigNotFound(path=path) 124 | 125 | with open(path) as f: 126 | try: 127 | data = json.load(f) 128 | except ValueError as e: 129 | raise exceptions.ConfigHasWrongFormat(path=path, message=str(e)) 130 | 131 | config = Config() 132 | 133 | config.initialize(path, data) 134 | 135 | return config 136 | -------------------------------------------------------------------------------- /smart_imports/scopes_tree.py: -------------------------------------------------------------------------------- 1 | 2 | import collections 3 | 4 | from . import constants as c 5 | 6 | 7 | class VariableInfo: 8 | __slots__ = ('state', 'line') 9 | 10 | def __init__(self, state, line): 11 | self.state = state 12 | self.line = line 13 | 14 | def __eq__(self, other): 15 | return (self.__class__ == other.__class__ and 16 | self.state == other.state and 17 | self.line == other.line) 18 | 19 | def __ne__(self, other): 20 | return not self.__ne__(other) 21 | 22 | def __repr__(self): 23 | return 'VariableInfo({}, {})'.format(repr(self.state), self.line) 24 | 25 | 26 | class Scope: 27 | __slots__ = ('variables', 'variables_lines', 'type', 'children', 'parent') 28 | 29 | def __init__(self, type): 30 | self.type = type 31 | self.variables = {} 32 | self.children = [] 33 | self.parent = None 34 | 35 | def register_variable(self, variable, state, line): 36 | if variable in self.variables: 37 | return 38 | 39 | self.variables[variable] = VariableInfo(state, line) 40 | 41 | def add_child(self, child): 42 | self.children.append(child) 43 | child.parent = self 44 | 45 | def level(self): 46 | if self.parent is None: 47 | return 0 48 | 49 | return self.parent.level() + 1 50 | 51 | 52 | def find_root(scope): 53 | while scope.parent: 54 | scope = scope.parent 55 | 56 | return scope 57 | 58 | 59 | def reversed_branch(scope): 60 | 61 | while scope: 62 | yield scope 63 | scope = scope.parent 64 | 65 | 66 | def get_variables_scopes(root_scope): 67 | 68 | variables = {} 69 | 70 | queue = collections.deque() 71 | 72 | queue.append(root_scope) 73 | 74 | while queue: 75 | scope = queue.popleft() 76 | 77 | queue.extend(scope.children) 78 | 79 | for variable in scope.variables: 80 | if variable not in variables: 81 | variables[variable] = [] 82 | 83 | variables[variable].append(scope) 84 | 85 | return variables 86 | 87 | 88 | def is_variable_defined(variable, scope): 89 | 90 | if variable not in scope.variables: 91 | return False 92 | 93 | if scope.variables[variable].state == c.VARIABLE_STATE.INITIALIZED: 94 | return True 95 | 96 | for scope in reversed_branch(scope): 97 | if scope.type == c.SCOPE_TYPE.CLASS: 98 | continue 99 | 100 | variable_info = scope.variables.get(variable) 101 | 102 | if variable_info and variable_info.state == c.VARIABLE_STATE.INITIALIZED: 103 | return True 104 | 105 | return False 106 | 107 | 108 | def determine_variable_usage(variable, scopes, usage_checker): 109 | 110 | if not scopes: 111 | return c.VARIABLE_USAGE_TYPE.FULLY_UNDEFINED 112 | 113 | counter = collections.Counter(1 if usage_checker(variable, scope) else 0 114 | for scope in scopes) 115 | 116 | if counter.get(1) == len(scopes): 117 | return c.VARIABLE_USAGE_TYPE.FULLY_DEFINED 118 | 119 | if counter.get(0) == len(scopes): 120 | return c.VARIABLE_USAGE_TYPE.FULLY_UNDEFINED 121 | 122 | return c.VARIABLE_USAGE_TYPE.PARTIALY_DEFINED 123 | 124 | 125 | def search_undefined_variable_lines(variable, scopes, usage_checker=is_variable_defined): 126 | 127 | if not scopes: 128 | return [] 129 | 130 | lines = [] 131 | 132 | for scope in scopes: 133 | if usage_checker(variable, scope): 134 | continue 135 | 136 | lines.append(scope.variables[variable].line) 137 | 138 | lines.sort() 139 | 140 | return lines 141 | 142 | 143 | def search_candidates_to_import(root_scope): 144 | variables_scopes = get_variables_scopes(root_scope) 145 | 146 | fully_undefined_variables = set() 147 | partialy_undefined_variables = set() 148 | 149 | for variable, scopes in variables_scopes.items(): 150 | variable_usage = determine_variable_usage(variable=variable, 151 | scopes=scopes, 152 | usage_checker=is_variable_defined) 153 | 154 | if variable_usage == c.VARIABLE_USAGE_TYPE.FULLY_DEFINED: 155 | continue 156 | 157 | if variable_usage == c.VARIABLE_USAGE_TYPE.FULLY_UNDEFINED: 158 | fully_undefined_variables.add(variable) 159 | continue 160 | 161 | partialy_undefined_variables.add(variable) 162 | 163 | return fully_undefined_variables, partialy_undefined_variables, variables_scopes 164 | -------------------------------------------------------------------------------- /smart_imports/tests/fixtures/python_3_10/full_parser_test.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | var_1 = True 4 | 5 | var_2 = var_3 # var_3 undefined 6 | 7 | 8 | def function_1(var_4: annotation_1, *var_5, **var_6) -> annotation_2: 9 | return var_4 + var_5[var_7] + var_6.get(var_8) # var_7 & var_8 undefined 10 | 11 | 12 | def function_2(var_9): 13 | 14 | def closure(var_10): 15 | return var_1 + var_9 + var_10 + var_11() # var 11 undefined 16 | 17 | return closure 18 | 19 | 20 | class Class(var_12): # var_12 undefined 21 | 22 | var_13 = var_2 23 | 24 | var_14 = var_1 + abs(var_9) # var_9 undefined 25 | 26 | def __init__(self): 27 | super().__init__(var_15+var_13) # var_13 & var_15 undefined 28 | 29 | 30 | var_16 = [var_17 for var_17 in range(10)] 31 | var_18 = {var_19 for var_19 in range(10)} 32 | var_20 = {var_21:var_22 for var_21 in zip(range(10), range(10, 20))} 33 | var_23 = (var_24 for var_23 in range(10)) 34 | 35 | 36 | for var_25 in range(10): 37 | pass 38 | 39 | var_26 = var_27 40 | 41 | 42 | var_28 = {var_29:[var_20[var_31] for var_31 in range(10)] 43 | for var_29 in range(10)} 44 | 45 | 46 | class OtherClass: 47 | 48 | var_32 = 1 49 | 50 | def var_33(self, func): 51 | pass 52 | 53 | @var_33 54 | def method_1(self, var_34=var_32): 55 | pass 56 | 57 | 58 | var_35 = lambda var_36, var_37=var_38: var_36+var_37+var_39 59 | 60 | 61 | var_40 = [var_41 + var_42 + var_43 62 | for var_41, var_43 in var_35 63 | if var_43 and var_44] 64 | 65 | 66 | {(var_45, var_46) 67 | for var_45 in {(var_46, var_47) 68 | for var_46 in var_48}} 69 | 70 | 71 | try: 72 | var_49 = var_50 73 | except var_51 as var_52: 74 | var_53(var_50, var_52, var_51, var_49) 75 | raise var_55 76 | 77 | 78 | async def function_3(): 79 | var_56 = 1 80 | 81 | def function_4(): 82 | print(var_56) # defined 83 | 84 | await var_49 85 | 86 | await var_57 87 | 88 | return function_4 89 | 90 | 91 | # formatted string literals 92 | 93 | var_58 = 'Fred' 94 | 95 | f'He said his name is {var_58} and {var_59}.' 96 | 97 | var_60 = 10 98 | 99 | f'result: {var_58:{var_60}.{var_61}}' 100 | 101 | 102 | # variable annotations 103 | 104 | var_62: var_63[var_58] = var_65 105 | 106 | var_66: var_67 # Note: no initial value! 107 | 108 | 109 | class var_68: 110 | var_69: var_70(var_66, var_72) = {} 111 | 112 | 113 | # asynchronous comprehensions 114 | 115 | async def fuction_4(): 116 | var_66 = [(var_73 + var_74+var_81) async for var_73, var_74 in var_75() if (var_76 + var_74) % 2] 117 | 118 | var_66 = [await var_77() for var_77, var_78 in var_79 if await var_80(var_78)] 119 | 120 | 121 | # unicode 122 | 123 | переменная_1 = перменная_2 + var_1 124 | 125 | 126 | # assigment expressions 127 | 128 | if (var_82 := var_83 + var_66) > var_84 + var_82: 129 | pass 130 | 131 | 132 | # position only parameters 133 | 134 | def var_85(var_86, var_87, /, var_89): 135 | return var_86 + var_87 + var_89 136 | 137 | 138 | # f-string self-documenting expressions 139 | 140 | f'{var_82=}, {var_90=}' 141 | 142 | 143 | # extended decorator syntax 144 | @var_91[var_62].value 145 | def var_92(): 146 | pass 147 | 148 | 149 | # extended decorator syntax 150 | @function_2[var_93].value 151 | def var_94(): 152 | pass 153 | 154 | 155 | # context managers in parents 156 | with (var_82.manager(), 157 | var_93.manager() as var_95, 158 | var_96(), 159 | var_97() as var_98): 160 | pass 161 | 162 | 163 | # match expression 164 | match var_99: 165 | case (0, 0): 166 | var_100 = var_99 + var_101 167 | case (0, var_102): 168 | var_100 = 0 169 | case (var_103, 0): 170 | var_100 = var_103 + 1 171 | case var_104: 172 | var_100 = var_104 + 1 173 | case (var_105, var_106): 174 | var_100 = var_105 + var_106 175 | case var_108(var_109=var_110, var_111=0): 176 | var_100 = var_110 + 1 + var_111 177 | case (var_112, var_113) if var_112 == var_113 and var_112 != var_114: 178 | var_100 = var_112 + var_113 179 | case (1, 2, *var_115): 180 | var_100 = var_115 + 0 181 | case {"a": var_116, 'b': var_117, 'c': var_94}: 182 | var_100 = var_116 + var_117 + var_94 183 | case {"a": var_116, **var_118}: 184 | var_100 = var_116 + var_118 185 | case (var_108(var_109=var_110), var_119(var_120=var_121) as var_122): 186 | var_100 = var_108 + var_110 + var_119 + var_121 + var_122 187 | case var_123.var_124: 188 | var_100 = var_123 189 | case {var_125.var_126: var_127}: 190 | var_100 = var_125 + var_127 191 | case _: 192 | var_100 = var_107 193 | -------------------------------------------------------------------------------- /smart_imports/fixtures/python_3_5_packages.txt: -------------------------------------------------------------------------------- 1 | __future__ 2 | __main__ 3 | _dummy_thread 4 | _thread 5 | abc 6 | aifc 7 | argparse 8 | array 9 | ast 10 | asynchat 11 | asyncio 12 | asyncore 13 | atexit 14 | audioop 15 | base64 16 | bdb 17 | binascii 18 | binhex 19 | bisect 20 | builtins 21 | bz2 22 | cProfile 23 | calendar 24 | cgi 25 | cgitb 26 | chunk 27 | cmath 28 | cmd 29 | code 30 | codecs 31 | codeop 32 | collections 33 | collections.abc 34 | colorsys 35 | compileall 36 | concurrent.futures 37 | configparser 38 | contextlib 39 | copy 40 | copyreg 41 | crypt 42 | csv 43 | ctypes 44 | curses 45 | curses.ascii 46 | curses.panel 47 | curses.textpad 48 | datetime 49 | dbm 50 | dbm.dumb 51 | dbm.gnu 52 | dbm.ndbm 53 | decimal 54 | difflib 55 | dis 56 | distutils 57 | distutils.archive_util 58 | distutils.bcppcompiler 59 | distutils.ccompiler 60 | distutils.cmd 61 | distutils.command 62 | distutils.command.bdist 63 | distutils.command.bdist_dumb 64 | distutils.command.bdist_msi 65 | distutils.command.bdist_packager 66 | distutils.command.bdist_rpm 67 | distutils.command.bdist_wininst 68 | distutils.command.build 69 | distutils.command.build_clib 70 | distutils.command.build_ext 71 | distutils.command.build_py 72 | distutils.command.build_scripts 73 | distutils.command.check 74 | distutils.command.clean 75 | distutils.command.config 76 | distutils.command.install 77 | distutils.command.install_data 78 | distutils.command.install_headers 79 | distutils.command.install_lib 80 | distutils.command.install_scripts 81 | distutils.command.register 82 | distutils.command.sdist 83 | distutils.core 84 | distutils.cygwinccompiler 85 | distutils.debug 86 | distutils.dep_util 87 | distutils.dir_util 88 | distutils.dist 89 | distutils.errors 90 | distutils.extension 91 | distutils.fancy_getopt 92 | distutils.file_util 93 | distutils.filelist 94 | distutils.log 95 | distutils.msvccompiler 96 | distutils.spawn 97 | distutils.sysconfig 98 | distutils.text_file 99 | distutils.unixccompiler 100 | distutils.util 101 | distutils.version 102 | doctest 103 | dummy_threading 104 | email 105 | email.charset 106 | email.contentmanager 107 | email.encoders 108 | email.errors 109 | email.generator 110 | email.header 111 | email.headerregistry 112 | email.iterators 113 | email.message 114 | email.mime 115 | email.parser 116 | email.policy 117 | email.utils 118 | encodings.idna 119 | encodings.mbcs 120 | encodings.utf_8_sig 121 | ensurepip 122 | enum 123 | errno 124 | faulthandler 125 | fcntl 126 | filecmp 127 | fileinput 128 | fnmatch 129 | formatter 130 | fpectl 131 | fractions 132 | ftplib 133 | functools 134 | gc 135 | getopt 136 | getpass 137 | gettext 138 | glob 139 | grp 140 | gzip 141 | hashlib 142 | heapq 143 | hmac 144 | html 145 | html.entities 146 | html.parser 147 | http 148 | http.client 149 | http.cookiejar 150 | http.cookies 151 | http.server 152 | imaplib 153 | imghdr 154 | imp 155 | importlib 156 | importlib.abc 157 | importlib.machinery 158 | importlib.util 159 | inspect 160 | io 161 | ipaddress 162 | itertools 163 | json 164 | json.tool 165 | keyword 166 | lib2to3 167 | linecache 168 | locale 169 | logging 170 | logging.config 171 | logging.handlers 172 | lzma 173 | macpath 174 | mailbox 175 | mailcap 176 | marshal 177 | math 178 | mimetypes 179 | mmap 180 | modulefinder 181 | msilib 182 | msvcrt 183 | multiprocessing 184 | multiprocessing.connection 185 | multiprocessing.dummy 186 | multiprocessing.managers 187 | multiprocessing.pool 188 | multiprocessing.sharedctypes 189 | netrc 190 | nis 191 | nntplib 192 | numbers 193 | operator 194 | optparse 195 | os 196 | os.path 197 | ossaudiodev 198 | parser 199 | pathlib 200 | pdb 201 | pickle 202 | pickletools 203 | pipes 204 | pkgutil 205 | platform 206 | plistlib 207 | poplib 208 | posix 209 | pprint 210 | profile 211 | pstats 212 | pty 213 | pwd 214 | py_compile 215 | pyclbr 216 | pydoc 217 | queue 218 | quopri 219 | random 220 | re 221 | readline 222 | reprlib 223 | resource 224 | rlcompleter 225 | runpy 226 | sched 227 | select 228 | selectors 229 | shelve 230 | shlex 231 | shutil 232 | signal 233 | site 234 | smtpd 235 | smtplib 236 | sndhdr 237 | socket 238 | socketserver 239 | spwd 240 | sqlite3 241 | ssl 242 | stat 243 | statistics 244 | string 245 | stringprep 246 | struct 247 | subprocess 248 | sunau 249 | symbol 250 | symtable 251 | sys 252 | sysconfig 253 | syslog 254 | tabnanny 255 | tarfile 256 | telnetlib 257 | tempfile 258 | termios 259 | test 260 | test.support 261 | textwrap 262 | threading 263 | time 264 | timeit 265 | tkinter 266 | tkinter.scrolledtext 267 | tkinter.tix 268 | tkinter.ttk 269 | token 270 | tokenize 271 | trace 272 | traceback 273 | tracemalloc 274 | tty 275 | turtle 276 | turtledemo 277 | types 278 | typing 279 | unicodedata 280 | unittest 281 | unittest.mock 282 | urllib 283 | urllib.error 284 | urllib.parse 285 | urllib.request 286 | urllib.response 287 | urllib.robotparser 288 | uu 289 | uuid 290 | venv 291 | warnings 292 | wave 293 | weakref 294 | webbrowser 295 | winreg 296 | winsound 297 | wsgiref 298 | wsgiref.handlers 299 | wsgiref.headers 300 | wsgiref.simple_server 301 | wsgiref.util 302 | wsgiref.validate 303 | xdrlib 304 | xml 305 | xml.dom 306 | xml.dom.minidom 307 | xml.dom.pulldom 308 | xml.etree.ElementTree 309 | xml.parsers.expat 310 | xml.parsers.expat.errors 311 | xml.parsers.expat.model 312 | xml.sax 313 | xml.sax.handler 314 | xml.sax.saxutils 315 | xml.sax.xmlreader 316 | xmlrpc.client 317 | xmlrpc.server 318 | zipapp 319 | zipfile 320 | zipimport 321 | zlib 322 | -------------------------------------------------------------------------------- /smart_imports/fixtures/python_3_6_packages.txt: -------------------------------------------------------------------------------- 1 | __future__ 2 | __main__ 3 | _dummy_thread 4 | _thread 5 | abc 6 | aifc 7 | argparse 8 | array 9 | ast 10 | asynchat 11 | asyncio 12 | asyncore 13 | atexit 14 | audioop 15 | base64 16 | bdb 17 | binascii 18 | binhex 19 | bisect 20 | builtins 21 | bz2 22 | cProfile 23 | calendar 24 | cgi 25 | cgitb 26 | chunk 27 | cmath 28 | cmd 29 | code 30 | codecs 31 | codeop 32 | collections 33 | collections.abc 34 | colorsys 35 | compileall 36 | concurrent.futures 37 | configparser 38 | contextlib 39 | copy 40 | copyreg 41 | crypt 42 | csv 43 | ctypes 44 | curses 45 | curses.ascii 46 | curses.panel 47 | curses.textpad 48 | datetime 49 | dbm 50 | dbm.dumb 51 | dbm.gnu 52 | dbm.ndbm 53 | decimal 54 | difflib 55 | dis 56 | distutils 57 | distutils.archive_util 58 | distutils.bcppcompiler 59 | distutils.ccompiler 60 | distutils.cmd 61 | distutils.command 62 | distutils.command.bdist 63 | distutils.command.bdist_dumb 64 | distutils.command.bdist_msi 65 | distutils.command.bdist_packager 66 | distutils.command.bdist_rpm 67 | distutils.command.bdist_wininst 68 | distutils.command.build 69 | distutils.command.build_clib 70 | distutils.command.build_ext 71 | distutils.command.build_py 72 | distutils.command.build_scripts 73 | distutils.command.check 74 | distutils.command.clean 75 | distutils.command.config 76 | distutils.command.install 77 | distutils.command.install_data 78 | distutils.command.install_headers 79 | distutils.command.install_lib 80 | distutils.command.install_scripts 81 | distutils.command.register 82 | distutils.command.sdist 83 | distutils.core 84 | distutils.cygwinccompiler 85 | distutils.debug 86 | distutils.dep_util 87 | distutils.dir_util 88 | distutils.dist 89 | distutils.errors 90 | distutils.extension 91 | distutils.fancy_getopt 92 | distutils.file_util 93 | distutils.filelist 94 | distutils.log 95 | distutils.msvccompiler 96 | distutils.spawn 97 | distutils.sysconfig 98 | distutils.text_file 99 | distutils.unixccompiler 100 | distutils.util 101 | distutils.version 102 | doctest 103 | dummy_threading 104 | email 105 | email.charset 106 | email.contentmanager 107 | email.encoders 108 | email.errors 109 | email.generator 110 | email.header 111 | email.headerregistry 112 | email.iterators 113 | email.message 114 | email.mime 115 | email.parser 116 | email.policy 117 | email.utils 118 | encodings.idna 119 | encodings.mbcs 120 | encodings.utf_8_sig 121 | ensurepip 122 | enum 123 | errno 124 | faulthandler 125 | fcntl 126 | filecmp 127 | fileinput 128 | fnmatch 129 | formatter 130 | fpectl 131 | fractions 132 | ftplib 133 | functools 134 | gc 135 | getopt 136 | getpass 137 | gettext 138 | glob 139 | grp 140 | gzip 141 | hashlib 142 | heapq 143 | hmac 144 | html 145 | html.entities 146 | html.parser 147 | http 148 | http.client 149 | http.cookiejar 150 | http.cookies 151 | http.server 152 | imaplib 153 | imghdr 154 | imp 155 | importlib 156 | importlib.abc 157 | importlib.machinery 158 | importlib.util 159 | inspect 160 | io 161 | ipaddress 162 | itertools 163 | json 164 | json.tool 165 | keyword 166 | lib2to3 167 | linecache 168 | locale 169 | logging 170 | logging.config 171 | logging.handlers 172 | lzma 173 | macpath 174 | mailbox 175 | mailcap 176 | marshal 177 | math 178 | mimetypes 179 | mmap 180 | modulefinder 181 | msilib 182 | msvcrt 183 | multiprocessing 184 | multiprocessing.connection 185 | multiprocessing.dummy 186 | multiprocessing.managers 187 | multiprocessing.pool 188 | multiprocessing.sharedctypes 189 | netrc 190 | nis 191 | nntplib 192 | numbers 193 | operator 194 | optparse 195 | os 196 | os.path 197 | ossaudiodev 198 | parser 199 | pathlib 200 | pdb 201 | pickle 202 | pickletools 203 | pipes 204 | pkgutil 205 | platform 206 | plistlib 207 | poplib 208 | posix 209 | pprint 210 | profile 211 | pstats 212 | pty 213 | pwd 214 | py_compile 215 | pyclbr 216 | pydoc 217 | queue 218 | quopri 219 | random 220 | re 221 | readline 222 | reprlib 223 | resource 224 | rlcompleter 225 | runpy 226 | sched 227 | secrets 228 | select 229 | selectors 230 | shelve 231 | shlex 232 | shutil 233 | signal 234 | site 235 | smtpd 236 | smtplib 237 | sndhdr 238 | socket 239 | socketserver 240 | spwd 241 | sqlite3 242 | ssl 243 | stat 244 | statistics 245 | string 246 | stringprep 247 | struct 248 | subprocess 249 | sunau 250 | symbol 251 | symtable 252 | sys 253 | sysconfig 254 | syslog 255 | tabnanny 256 | tarfile 257 | telnetlib 258 | tempfile 259 | termios 260 | test 261 | test.support 262 | textwrap 263 | threading 264 | time 265 | timeit 266 | tkinter 267 | tkinter.scrolledtext 268 | tkinter.tix 269 | tkinter.ttk 270 | token 271 | tokenize 272 | trace 273 | traceback 274 | tracemalloc 275 | tty 276 | turtle 277 | turtledemo 278 | types 279 | typing 280 | unicodedata 281 | unittest 282 | unittest.mock 283 | urllib 284 | urllib.error 285 | urllib.parse 286 | urllib.request 287 | urllib.response 288 | urllib.robotparser 289 | uu 290 | uuid 291 | venv 292 | warnings 293 | wave 294 | weakref 295 | webbrowser 296 | winreg 297 | winsound 298 | wsgiref 299 | wsgiref.handlers 300 | wsgiref.headers 301 | wsgiref.simple_server 302 | wsgiref.util 303 | wsgiref.validate 304 | xdrlib 305 | xml 306 | xml.dom 307 | xml.dom.minidom 308 | xml.dom.pulldom 309 | xml.etree.ElementTree 310 | xml.parsers.expat 311 | xml.parsers.expat.errors 312 | xml.parsers.expat.model 313 | xml.sax 314 | xml.sax.handler 315 | xml.sax.saxutils 316 | xml.sax.xmlreader 317 | xmlrpc.client 318 | xmlrpc.server 319 | zipapp 320 | zipfile 321 | zipimport 322 | zlib 323 | -------------------------------------------------------------------------------- /smart_imports/fixtures/python_3_7_packages.txt: -------------------------------------------------------------------------------- 1 | __future__ 2 | __main__ 3 | _dummy_thread 4 | _thread 5 | abc 6 | aifc 7 | argparse 8 | array 9 | ast 10 | asynchat 11 | asyncio 12 | asyncore 13 | atexit 14 | audioop 15 | base64 16 | bdb 17 | binascii 18 | binhex 19 | bisect 20 | builtins 21 | bz2 22 | cProfile 23 | calendar 24 | cgi 25 | cgitb 26 | chunk 27 | cmath 28 | cmd 29 | code 30 | codecs 31 | codeop 32 | collections 33 | collections.abc 34 | colorsys 35 | compileall 36 | concurrent.futures 37 | configparser 38 | contextlib 39 | contextvars 40 | copy 41 | copyreg 42 | crypt 43 | csv 44 | ctypes 45 | curses 46 | curses.ascii 47 | curses.panel 48 | curses.textpad 49 | dataclasses 50 | datetime 51 | dbm 52 | dbm.dumb 53 | dbm.gnu 54 | dbm.ndbm 55 | decimal 56 | difflib 57 | dis 58 | distutils 59 | distutils.archive_util 60 | distutils.bcppcompiler 61 | distutils.ccompiler 62 | distutils.cmd 63 | distutils.command 64 | distutils.command.bdist 65 | distutils.command.bdist_dumb 66 | distutils.command.bdist_msi 67 | distutils.command.bdist_packager 68 | distutils.command.bdist_rpm 69 | distutils.command.bdist_wininst 70 | distutils.command.build 71 | distutils.command.build_clib 72 | distutils.command.build_ext 73 | distutils.command.build_py 74 | distutils.command.build_scripts 75 | distutils.command.check 76 | distutils.command.clean 77 | distutils.command.config 78 | distutils.command.install 79 | distutils.command.install_data 80 | distutils.command.install_headers 81 | distutils.command.install_lib 82 | distutils.command.install_scripts 83 | distutils.command.register 84 | distutils.command.sdist 85 | distutils.core 86 | distutils.cygwinccompiler 87 | distutils.debug 88 | distutils.dep_util 89 | distutils.dir_util 90 | distutils.dist 91 | distutils.errors 92 | distutils.extension 93 | distutils.fancy_getopt 94 | distutils.file_util 95 | distutils.filelist 96 | distutils.log 97 | distutils.msvccompiler 98 | distutils.spawn 99 | distutils.sysconfig 100 | distutils.text_file 101 | distutils.unixccompiler 102 | distutils.util 103 | distutils.version 104 | doctest 105 | dummy_threading 106 | email 107 | email.charset 108 | email.contentmanager 109 | email.encoders 110 | email.errors 111 | email.generator 112 | email.header 113 | email.headerregistry 114 | email.iterators 115 | email.message 116 | email.mime 117 | email.parser 118 | email.policy 119 | email.utils 120 | encodings.idna 121 | encodings.mbcs 122 | encodings.utf_8_sig 123 | ensurepip 124 | enum 125 | errno 126 | faulthandler 127 | fcntl 128 | filecmp 129 | fileinput 130 | fnmatch 131 | formatter 132 | fractions 133 | ftplib 134 | functools 135 | gc 136 | getopt 137 | getpass 138 | gettext 139 | glob 140 | grp 141 | gzip 142 | hashlib 143 | heapq 144 | hmac 145 | html 146 | html.entities 147 | html.parser 148 | http 149 | http.client 150 | http.cookiejar 151 | http.cookies 152 | http.server 153 | imaplib 154 | imghdr 155 | imp 156 | importlib 157 | importlib.abc 158 | importlib.machinery 159 | importlib.resources 160 | importlib.util 161 | inspect 162 | io 163 | ipaddress 164 | itertools 165 | json 166 | json.tool 167 | keyword 168 | lib2to3 169 | linecache 170 | locale 171 | logging 172 | logging.config 173 | logging.handlers 174 | lzma 175 | macpath 176 | mailbox 177 | mailcap 178 | marshal 179 | math 180 | mimetypes 181 | mmap 182 | modulefinder 183 | msilib 184 | msvcrt 185 | multiprocessing 186 | multiprocessing.connection 187 | multiprocessing.dummy 188 | multiprocessing.managers 189 | multiprocessing.pool 190 | multiprocessing.sharedctypes 191 | netrc 192 | nis 193 | nntplib 194 | numbers 195 | operator 196 | optparse 197 | os 198 | os.path 199 | ossaudiodev 200 | parser 201 | pathlib 202 | pdb 203 | pickle 204 | pickletools 205 | pipes 206 | pkgutil 207 | platform 208 | plistlib 209 | poplib 210 | posix 211 | pprint 212 | profile 213 | pstats 214 | pty 215 | pwd 216 | py_compile 217 | pyclbr 218 | pydoc 219 | queue 220 | quopri 221 | random 222 | re 223 | readline 224 | reprlib 225 | resource 226 | rlcompleter 227 | runpy 228 | sched 229 | secrets 230 | select 231 | selectors 232 | shelve 233 | shlex 234 | shutil 235 | signal 236 | site 237 | smtpd 238 | smtplib 239 | sndhdr 240 | socket 241 | socketserver 242 | spwd 243 | sqlite3 244 | ssl 245 | stat 246 | statistics 247 | string 248 | stringprep 249 | struct 250 | subprocess 251 | sunau 252 | symbol 253 | symtable 254 | sys 255 | sysconfig 256 | syslog 257 | tabnanny 258 | tarfile 259 | telnetlib 260 | tempfile 261 | termios 262 | test 263 | test.support 264 | test.support.script_helper 265 | textwrap 266 | threading 267 | time 268 | timeit 269 | tkinter 270 | tkinter.scrolledtext 271 | tkinter.tix 272 | tkinter.ttk 273 | token 274 | tokenize 275 | trace 276 | traceback 277 | tracemalloc 278 | tty 279 | turtle 280 | turtledemo 281 | types 282 | typing 283 | unicodedata 284 | unittest 285 | unittest.mock 286 | urllib 287 | urllib.error 288 | urllib.parse 289 | urllib.request 290 | urllib.response 291 | urllib.robotparser 292 | uu 293 | uuid 294 | venv 295 | warnings 296 | wave 297 | weakref 298 | webbrowser 299 | winreg 300 | winsound 301 | wsgiref 302 | wsgiref.handlers 303 | wsgiref.headers 304 | wsgiref.simple_server 305 | wsgiref.util 306 | wsgiref.validate 307 | xdrlib 308 | xml 309 | xml.dom 310 | xml.dom.minidom 311 | xml.dom.pulldom 312 | xml.etree.ElementTree 313 | xml.parsers.expat 314 | xml.parsers.expat.errors 315 | xml.parsers.expat.model 316 | xml.sax 317 | xml.sax.handler 318 | xml.sax.saxutils 319 | xml.sax.xmlreader 320 | xmlrpc.client 321 | xmlrpc.server 322 | zipapp 323 | zipfile 324 | zipimport 325 | zlib 326 | -------------------------------------------------------------------------------- /smart_imports/fixtures/python_3_8_packages.txt: -------------------------------------------------------------------------------- 1 | __future__ 2 | __main__ 3 | _dummy_thread 4 | _thread 5 | abc 6 | aifc 7 | argparse 8 | array 9 | ast 10 | asynchat 11 | asyncio 12 | asyncore 13 | atexit 14 | audioop 15 | base64 16 | bdb 17 | binascii 18 | binhex 19 | bisect 20 | builtins 21 | bz2 22 | cProfile 23 | calendar 24 | cgi 25 | cgitb 26 | chunk 27 | cmath 28 | cmd 29 | code 30 | codecs 31 | codeop 32 | collections 33 | collections.abc 34 | colorsys 35 | compileall 36 | concurrent.futures 37 | configparser 38 | contextlib 39 | contextvars 40 | copy 41 | copyreg 42 | crypt 43 | csv 44 | ctypes 45 | curses 46 | curses.ascii 47 | curses.panel 48 | curses.textpad 49 | dataclasses 50 | datetime 51 | dbm 52 | dbm.dumb 53 | dbm.gnu 54 | dbm.ndbm 55 | decimal 56 | difflib 57 | dis 58 | distutils 59 | distutils.archive_util 60 | distutils.bcppcompiler 61 | distutils.ccompiler 62 | distutils.cmd 63 | distutils.command 64 | distutils.command.bdist 65 | distutils.command.bdist_dumb 66 | distutils.command.bdist_msi 67 | distutils.command.bdist_packager 68 | distutils.command.bdist_rpm 69 | distutils.command.bdist_wininst 70 | distutils.command.build 71 | distutils.command.build_clib 72 | distutils.command.build_ext 73 | distutils.command.build_py 74 | distutils.command.build_scripts 75 | distutils.command.check 76 | distutils.command.clean 77 | distutils.command.config 78 | distutils.command.install 79 | distutils.command.install_data 80 | distutils.command.install_headers 81 | distutils.command.install_lib 82 | distutils.command.install_scripts 83 | distutils.command.register 84 | distutils.command.sdist 85 | distutils.core 86 | distutils.cygwinccompiler 87 | distutils.debug 88 | distutils.dep_util 89 | distutils.dir_util 90 | distutils.dist 91 | distutils.errors 92 | distutils.extension 93 | distutils.fancy_getopt 94 | distutils.file_util 95 | distutils.filelist 96 | distutils.log 97 | distutils.msvccompiler 98 | distutils.spawn 99 | distutils.sysconfig 100 | distutils.text_file 101 | distutils.unixccompiler 102 | distutils.util 103 | distutils.version 104 | doctest 105 | dummy_threading 106 | email 107 | email.charset 108 | email.contentmanager 109 | email.encoders 110 | email.errors 111 | email.generator 112 | email.header 113 | email.headerregistry 114 | email.iterators 115 | email.message 116 | email.mime 117 | email.parser 118 | email.policy 119 | email.utils 120 | encodings.idna 121 | encodings.mbcs 122 | encodings.utf_8_sig 123 | ensurepip 124 | enum 125 | errno 126 | faulthandler 127 | fcntl 128 | filecmp 129 | fileinput 130 | fnmatch 131 | formatter 132 | fractions 133 | ftplib 134 | functools 135 | gc 136 | getopt 137 | getpass 138 | gettext 139 | glob 140 | grp 141 | gzip 142 | hashlib 143 | heapq 144 | hmac 145 | html 146 | html.entities 147 | html.parser 148 | http 149 | http.client 150 | http.cookiejar 151 | http.cookies 152 | http.server 153 | imaplib 154 | imghdr 155 | imp 156 | importlib 157 | importlib.abc 158 | importlib.machinery 159 | importlib.resources 160 | importlib.util 161 | inspect 162 | io 163 | ipaddress 164 | itertools 165 | json 166 | json.tool 167 | keyword 168 | lib2to3 169 | linecache 170 | locale 171 | logging 172 | logging.config 173 | logging.handlers 174 | lzma 175 | mailbox 176 | mailcap 177 | marshal 178 | math 179 | mimetypes 180 | mmap 181 | modulefinder 182 | msilib 183 | msvcrt 184 | multiprocessing 185 | multiprocessing.connection 186 | multiprocessing.dummy 187 | multiprocessing.managers 188 | multiprocessing.pool 189 | multiprocessing.shared_memory 190 | multiprocessing.sharedctypes 191 | netrc 192 | nis 193 | nntplib 194 | numbers 195 | operator 196 | optparse 197 | os 198 | os.path 199 | ossaudiodev 200 | parser 201 | pathlib 202 | pdb 203 | pickle 204 | pickletools 205 | pipes 206 | pkgutil 207 | platform 208 | plistlib 209 | poplib 210 | posix 211 | pprint 212 | profile 213 | pstats 214 | pty 215 | pwd 216 | py_compile 217 | pyclbr 218 | pydoc 219 | queue 220 | quopri 221 | random 222 | re 223 | readline 224 | reprlib 225 | resource 226 | rlcompleter 227 | runpy 228 | sched 229 | secrets 230 | select 231 | selectors 232 | shelve 233 | shlex 234 | shutil 235 | signal 236 | site 237 | smtpd 238 | smtplib 239 | sndhdr 240 | socket 241 | socketserver 242 | spwd 243 | sqlite3 244 | ssl 245 | stat 246 | statistics 247 | string 248 | stringprep 249 | struct 250 | subprocess 251 | sunau 252 | symbol 253 | symtable 254 | sys 255 | sysconfig 256 | syslog 257 | tabnanny 258 | tarfile 259 | telnetlib 260 | tempfile 261 | termios 262 | test 263 | test.support 264 | test.support.script_helper 265 | textwrap 266 | threading 267 | time 268 | timeit 269 | tkinter 270 | tkinter.scrolledtext 271 | tkinter.tix 272 | tkinter.ttk 273 | token 274 | tokenize 275 | trace 276 | traceback 277 | tracemalloc 278 | tty 279 | turtle 280 | turtledemo 281 | types 282 | typing 283 | unicodedata 284 | unittest 285 | unittest.mock 286 | urllib 287 | urllib.error 288 | urllib.parse 289 | urllib.request 290 | urllib.response 291 | urllib.robotparser 292 | uu 293 | uuid 294 | venv 295 | warnings 296 | wave 297 | weakref 298 | webbrowser 299 | winreg 300 | winsound 301 | wsgiref 302 | wsgiref.handlers 303 | wsgiref.headers 304 | wsgiref.simple_server 305 | wsgiref.util 306 | wsgiref.validate 307 | xdrlib 308 | xml 309 | xml.dom 310 | xml.dom.minidom 311 | xml.dom.pulldom 312 | xml.etree.ElementTree 313 | xml.parsers.expat 314 | xml.parsers.expat.errors 315 | xml.parsers.expat.model 316 | xml.sax 317 | xml.sax.handler 318 | xml.sax.saxutils 319 | xml.sax.xmlreader 320 | xmlrpc.client 321 | xmlrpc.server 322 | zipapp 323 | zipfile 324 | zipimport 325 | zlib 326 | -------------------------------------------------------------------------------- /smart_imports/fixtures/python_3_9_packages.txt: -------------------------------------------------------------------------------- 1 | __future__ 2 | __main__ 3 | _thread 4 | abc 5 | aifc 6 | argparse 7 | array 8 | ast 9 | asynchat 10 | asyncio 11 | asyncore 12 | atexit 13 | audioop 14 | base64 15 | bdb 16 | binascii 17 | binhex 18 | bisect 19 | builtins 20 | bz2 21 | cProfile 22 | calendar 23 | cgi 24 | cgitb 25 | chunk 26 | cmath 27 | cmd 28 | code 29 | codecs 30 | codeop 31 | collections 32 | collections.abc 33 | colorsys 34 | compileall 35 | concurrent.futures 36 | configparser 37 | contextlib 38 | contextvars 39 | copy 40 | copyreg 41 | crypt 42 | csv 43 | ctypes 44 | curses 45 | curses.ascii 46 | curses.panel 47 | curses.textpad 48 | dataclasses 49 | datetime 50 | dbm 51 | dbm.dumb 52 | dbm.gnu 53 | dbm.ndbm 54 | decimal 55 | difflib 56 | dis 57 | distutils 58 | distutils.archive_util 59 | distutils.bcppcompiler 60 | distutils.ccompiler 61 | distutils.cmd 62 | distutils.command 63 | distutils.command.bdist 64 | distutils.command.bdist_dumb 65 | distutils.command.bdist_msi 66 | distutils.command.bdist_packager 67 | distutils.command.bdist_rpm 68 | distutils.command.bdist_wininst 69 | distutils.command.build 70 | distutils.command.build_clib 71 | distutils.command.build_ext 72 | distutils.command.build_py 73 | distutils.command.build_scripts 74 | distutils.command.check 75 | distutils.command.clean 76 | distutils.command.config 77 | distutils.command.install 78 | distutils.command.install_data 79 | distutils.command.install_headers 80 | distutils.command.install_lib 81 | distutils.command.install_scripts 82 | distutils.command.register 83 | distutils.command.sdist 84 | distutils.core 85 | distutils.cygwinccompiler 86 | distutils.debug 87 | distutils.dep_util 88 | distutils.dir_util 89 | distutils.dist 90 | distutils.errors 91 | distutils.extension 92 | distutils.fancy_getopt 93 | distutils.file_util 94 | distutils.filelist 95 | distutils.log 96 | distutils.msvccompiler 97 | distutils.spawn 98 | distutils.sysconfig 99 | distutils.text_file 100 | distutils.unixccompiler 101 | distutils.util 102 | distutils.version 103 | doctest 104 | email 105 | email.charset 106 | email.contentmanager 107 | email.encoders 108 | email.errors 109 | email.generator 110 | email.header 111 | email.headerregistry 112 | email.iterators 113 | email.message 114 | email.mime 115 | email.parser 116 | email.policy 117 | email.utils 118 | encodings.idna 119 | encodings.mbcs 120 | encodings.utf_8_sig 121 | ensurepip 122 | enum 123 | errno 124 | faulthandler 125 | fcntl 126 | filecmp 127 | fileinput 128 | fnmatch 129 | formatter 130 | fractions 131 | ftplib 132 | functools 133 | gc 134 | getopt 135 | getpass 136 | gettext 137 | glob 138 | graphlib 139 | grp 140 | gzip 141 | hashlib 142 | heapq 143 | hmac 144 | html 145 | html.entities 146 | html.parser 147 | http 148 | http.client 149 | http.cookiejar 150 | http.cookies 151 | http.server 152 | imaplib 153 | imghdr 154 | imp 155 | importlib 156 | importlib.abc 157 | importlib.machinery 158 | importlib.resources 159 | importlib.util 160 | inspect 161 | io 162 | ipaddress 163 | itertools 164 | json 165 | json.tool 166 | keyword 167 | lib2to3 168 | linecache 169 | locale 170 | logging 171 | logging.config 172 | logging.handlers 173 | lzma 174 | mailbox 175 | mailcap 176 | marshal 177 | math 178 | mimetypes 179 | mmap 180 | modulefinder 181 | msilib 182 | msvcrt 183 | multiprocessing 184 | multiprocessing.connection 185 | multiprocessing.dummy 186 | multiprocessing.managers 187 | multiprocessing.pool 188 | multiprocessing.shared_memory 189 | multiprocessing.sharedctypes 190 | netrc 191 | nis 192 | nntplib 193 | numbers 194 | operator 195 | optparse 196 | os 197 | os.path 198 | ossaudiodev 199 | parser 200 | pathlib 201 | pdb 202 | pickle 203 | pickletools 204 | pipes 205 | pkgutil 206 | platform 207 | plistlib 208 | poplib 209 | posix 210 | pprint 211 | profile 212 | pstats 213 | pty 214 | pwd 215 | py_compile 216 | pyclbr 217 | pydoc 218 | queue 219 | quopri 220 | random 221 | re 222 | readline 223 | reprlib 224 | resource 225 | rlcompleter 226 | runpy 227 | sched 228 | secrets 229 | select 230 | selectors 231 | shelve 232 | shlex 233 | shutil 234 | signal 235 | site 236 | smtpd 237 | smtplib 238 | sndhdr 239 | socket 240 | socketserver 241 | spwd 242 | sqlite3 243 | ssl 244 | stat 245 | statistics 246 | string 247 | stringprep 248 | struct 249 | subprocess 250 | sunau 251 | symbol 252 | symtable 253 | sys 254 | sysconfig 255 | syslog 256 | tabnanny 257 | tarfile 258 | telnetlib 259 | tempfile 260 | termios 261 | test 262 | test.support 263 | test.support.bytecode_helper 264 | test.support.script_helper 265 | test.support.socket_helper 266 | textwrap 267 | threading 268 | time 269 | timeit 270 | tkinter 271 | tkinter.colorchooser 272 | tkinter.commondialog 273 | tkinter.dnd 274 | tkinter.filedialog 275 | tkinter.font 276 | tkinter.messagebox 277 | tkinter.scrolledtext 278 | tkinter.simpledialog 279 | tkinter.tix 280 | tkinter.ttk 281 | token 282 | tokenize 283 | trace 284 | traceback 285 | tracemalloc 286 | tty 287 | turtle 288 | turtledemo 289 | types 290 | typing 291 | unicodedata 292 | unittest 293 | unittest.mock 294 | urllib 295 | urllib.error 296 | urllib.parse 297 | urllib.request 298 | urllib.response 299 | urllib.robotparser 300 | uu 301 | uuid 302 | venv 303 | warnings 304 | wave 305 | weakref 306 | webbrowser 307 | winreg 308 | winsound 309 | wsgiref 310 | wsgiref.handlers 311 | wsgiref.headers 312 | wsgiref.simple_server 313 | wsgiref.util 314 | wsgiref.validate 315 | xdrlib 316 | xml 317 | xml.dom 318 | xml.dom.minidom 319 | xml.dom.pulldom 320 | xml.etree.ElementTree 321 | xml.parsers.expat 322 | xml.parsers.expat.errors 323 | xml.parsers.expat.model 324 | xml.sax 325 | xml.sax.handler 326 | xml.sax.saxutils 327 | xml.sax.xmlreader 328 | xmlrpc.client 329 | xmlrpc.server 330 | zipapp 331 | zipfile 332 | zipimport 333 | zlib 334 | zoneinfo 335 | -------------------------------------------------------------------------------- /smart_imports/fixtures/python_3_10_packages.txt: -------------------------------------------------------------------------------- 1 | __future__ 2 | __main__ 3 | _thread 4 | abc 5 | aifc 6 | argparse 7 | array 8 | ast 9 | asynchat 10 | asyncio 11 | asyncore 12 | atexit 13 | audioop 14 | base64 15 | bdb 16 | binascii 17 | binhex 18 | bisect 19 | builtins 20 | bz2 21 | cProfile 22 | calendar 23 | cgi 24 | cgitb 25 | chunk 26 | cmath 27 | cmd 28 | code 29 | codecs 30 | codeop 31 | collections 32 | collections.abc 33 | colorsys 34 | compileall 35 | concurrent.futures 36 | configparser 37 | contextlib 38 | contextvars 39 | copy 40 | copyreg 41 | crypt 42 | csv 43 | ctypes 44 | curses 45 | curses.ascii 46 | curses.panel 47 | curses.textpad 48 | dataclasses 49 | datetime 50 | dbm 51 | dbm.dumb 52 | dbm.gnu 53 | dbm.ndbm 54 | decimal 55 | difflib 56 | dis 57 | distutils 58 | distutils.archive_util 59 | distutils.bcppcompiler 60 | distutils.ccompiler 61 | distutils.cmd 62 | distutils.command 63 | distutils.command.bdist 64 | distutils.command.bdist_dumb 65 | distutils.command.bdist_msi 66 | distutils.command.bdist_packager 67 | distutils.command.bdist_rpm 68 | distutils.command.build 69 | distutils.command.build_clib 70 | distutils.command.build_ext 71 | distutils.command.build_py 72 | distutils.command.build_scripts 73 | distutils.command.check 74 | distutils.command.clean 75 | distutils.command.config 76 | distutils.command.install 77 | distutils.command.install_data 78 | distutils.command.install_headers 79 | distutils.command.install_lib 80 | distutils.command.install_scripts 81 | distutils.command.register 82 | distutils.command.sdist 83 | distutils.core 84 | distutils.cygwinccompiler 85 | distutils.debug 86 | distutils.dep_util 87 | distutils.dir_util 88 | distutils.dist 89 | distutils.errors 90 | distutils.extension 91 | distutils.fancy_getopt 92 | distutils.file_util 93 | distutils.filelist 94 | distutils.log 95 | distutils.msvccompiler 96 | distutils.spawn 97 | distutils.sysconfig 98 | distutils.text_file 99 | distutils.unixccompiler 100 | distutils.util 101 | distutils.version 102 | doctest 103 | email 104 | email.charset 105 | email.contentmanager 106 | email.encoders 107 | email.errors 108 | email.generator 109 | email.header 110 | email.headerregistry 111 | email.iterators 112 | email.message 113 | email.mime 114 | email.parser 115 | email.policy 116 | email.utils 117 | encodings.idna 118 | encodings.mbcs 119 | encodings.utf_8_sig 120 | ensurepip 121 | enum 122 | errno 123 | faulthandler 124 | fcntl 125 | filecmp 126 | fileinput 127 | fnmatch 128 | fractions 129 | ftplib 130 | functools 131 | gc 132 | getopt 133 | getpass 134 | gettext 135 | glob 136 | graphlib 137 | grp 138 | gzip 139 | hashlib 140 | heapq 141 | hmac 142 | html 143 | html.entities 144 | html.parser 145 | http 146 | http.client 147 | http.cookiejar 148 | http.cookies 149 | http.server 150 | imaplib 151 | imghdr 152 | imp 153 | importlib 154 | importlib.abc 155 | importlib.machinery 156 | importlib.metadata 157 | importlib.resources 158 | importlib.util 159 | inspect 160 | io 161 | ipaddress 162 | itertools 163 | json 164 | json.tool 165 | keyword 166 | lib2to3 167 | linecache 168 | locale 169 | logging 170 | logging.config 171 | logging.handlers 172 | lzma 173 | mailbox 174 | mailcap 175 | marshal 176 | math 177 | mimetypes 178 | mmap 179 | modulefinder 180 | msilib 181 | msvcrt 182 | multiprocessing 183 | multiprocessing.connection 184 | multiprocessing.dummy 185 | multiprocessing.managers 186 | multiprocessing.pool 187 | multiprocessing.shared_memory 188 | multiprocessing.sharedctypes 189 | netrc 190 | nis 191 | nntplib 192 | numbers 193 | operator 194 | optparse 195 | os 196 | os.path 197 | ossaudiodev 198 | pathlib 199 | pdb 200 | pickle 201 | pickletools 202 | pipes 203 | pkgutil 204 | platform 205 | plistlib 206 | poplib 207 | posix 208 | pprint 209 | profile 210 | pstats 211 | pty 212 | pwd 213 | py_compile 214 | pyclbr 215 | pydoc 216 | queue 217 | quopri 218 | random 219 | re 220 | readline 221 | reprlib 222 | resource 223 | rlcompleter 224 | runpy 225 | sched 226 | secrets 227 | select 228 | selectors 229 | shelve 230 | shlex 231 | shutil 232 | signal 233 | site 234 | smtpd 235 | smtplib 236 | sndhdr 237 | socket 238 | socketserver 239 | spwd 240 | sqlite3 241 | ssl 242 | stat 243 | statistics 244 | string 245 | stringprep 246 | struct 247 | subprocess 248 | sunau 249 | symtable 250 | sys 251 | sysconfig 252 | syslog 253 | tabnanny 254 | tarfile 255 | telnetlib 256 | tempfile 257 | termios 258 | test 259 | test.support 260 | test.support.bytecode_helper 261 | test.support.import_helper 262 | test.support.os_helper 263 | test.support.script_helper 264 | test.support.socket_helper 265 | test.support.threading_helper 266 | test.support.warnings_helper 267 | textwrap 268 | threading 269 | time 270 | timeit 271 | tkinter 272 | tkinter.colorchooser 273 | tkinter.commondialog 274 | tkinter.dnd 275 | tkinter.filedialog 276 | tkinter.font 277 | tkinter.messagebox 278 | tkinter.scrolledtext 279 | tkinter.simpledialog 280 | tkinter.tix 281 | tkinter.ttk 282 | token 283 | tokenize 284 | trace 285 | traceback 286 | tracemalloc 287 | tty 288 | turtle 289 | turtledemo 290 | types 291 | typing 292 | unicodedata 293 | unittest 294 | unittest.mock 295 | urllib 296 | urllib.error 297 | urllib.parse 298 | urllib.request 299 | urllib.response 300 | urllib.robotparser 301 | uu 302 | uuid 303 | venv 304 | warnings 305 | wave 306 | weakref 307 | webbrowser 308 | winreg 309 | winsound 310 | wsgiref 311 | wsgiref.handlers 312 | wsgiref.headers 313 | wsgiref.simple_server 314 | wsgiref.util 315 | wsgiref.validate 316 | xdrlib 317 | xml 318 | xml.dom 319 | xml.dom.minidom 320 | xml.dom.pulldom 321 | xml.etree.ElementTree 322 | xml.parsers.expat 323 | xml.parsers.expat.errors 324 | xml.parsers.expat.model 325 | xml.sax 326 | xml.sax.handler 327 | xml.sax.saxutils 328 | xml.sax.xmlreader 329 | xmlrpc.client 330 | xmlrpc.server 331 | zipapp 332 | zipfile 333 | zipimport 334 | zlib 335 | zoneinfo 336 | -------------------------------------------------------------------------------- /smart_imports/ast_parser.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | import ast 4 | 5 | from . import constants as c 6 | from . import scopes_tree 7 | 8 | 9 | class Analyzer(ast.NodeVisitor): 10 | 11 | def __init__(self): 12 | super().__init__() 13 | self.scope = scopes_tree.Scope(type=c.SCOPE_TYPE.NORMAL) 14 | 15 | def register_variable_get(self, variable, line): 16 | self.scope.register_variable(variable, c.VARIABLE_STATE.UNINITIALIZED, line) 17 | 18 | def register_variable_set(self, variable, line): 19 | self.scope.register_variable(variable, c.VARIABLE_STATE.INITIALIZED, line) 20 | 21 | def push_scope(self, type): 22 | scope = scopes_tree.Scope(type) 23 | self.scope.add_child(scope) 24 | self.scope = scope 25 | 26 | def pop_scope(self): 27 | self.scope = self.scope.parent 28 | 29 | def visit_Name(self, node): 30 | if not isinstance(node.ctx, ast.Store): 31 | self.register_variable_get(node.id, node.lineno) 32 | else: 33 | self.register_variable_set(node.id, node.lineno) 34 | 35 | def _visit_comprehension(self, node): 36 | self.push_scope(type=c.SCOPE_TYPE.COMPREHENSION) 37 | 38 | for generator in node.generators: 39 | self._visit_for_comprehension(generator) 40 | 41 | if hasattr(node, 'elt'): 42 | self.visit(node.elt) 43 | 44 | if hasattr(node, 'key'): 45 | self.visit(node.key) 46 | 47 | if hasattr(node, 'value'): 48 | self.visit(node.value) 49 | 50 | self.pop_scope() 51 | 52 | def _visit_for_comprehension(self, comprehension): 53 | self.visit(comprehension.iter) 54 | self.visit(comprehension.target) 55 | 56 | for if_expression in comprehension.ifs: 57 | self.visit(if_expression) 58 | 59 | def visit_ListComp(self, node): 60 | self._visit_comprehension(node) 61 | 62 | def visit_SetComp(self, node): 63 | self._visit_comprehension(node) 64 | 65 | def visit_GeneratorExp(self, node): 66 | self._visit_comprehension(node) 67 | 68 | def visit_DictComp(self, node): 69 | self._visit_comprehension(node) 70 | 71 | def visit_Import(self, node): 72 | for alias in node.names: 73 | self.register_variable_set(alias.asname if alias.asname else alias.name, 74 | node.lineno) 75 | self.generic_visit(node) 76 | 77 | def visit_ImportFrom(self, node): 78 | for alias in node.names: 79 | self.register_variable_set(alias.asname if alias.asname else alias.name, 80 | node.lineno) 81 | self.generic_visit(node) 82 | 83 | def visit_FunctionDef(self, node): 84 | self.register_variable_set(node.name, node.lineno) 85 | 86 | for decorator in node.decorator_list: 87 | self.visit(decorator) 88 | 89 | self.visit_default_arguments(node.args) 90 | 91 | if node.returns is not None: 92 | self.visit(node.returns) 93 | 94 | self.push_scope(type=c.SCOPE_TYPE.NORMAL) 95 | 96 | self.visit_arguments(node.args) 97 | 98 | for body_node in node.body: 99 | self.visit(body_node) 100 | 101 | self.pop_scope() 102 | 103 | def visit_AsyncFunctionDef(self, node): 104 | self.visit_FunctionDef(node) 105 | 106 | def visit_Lambda(self, node): 107 | self.visit_default_arguments(node.args) 108 | 109 | self.push_scope(type=c.SCOPE_TYPE.NORMAL) 110 | 111 | self.visit_arguments(node.args) 112 | 113 | self.visit(node.body) 114 | 115 | self.pop_scope() 116 | 117 | def visit_default_arguments(self, node): 118 | for default in node.defaults: 119 | if default is None: 120 | continue 121 | 122 | self.visit(default) 123 | 124 | for default in node.kw_defaults: 125 | if default is None: 126 | continue 127 | 128 | self.visit(default) 129 | 130 | def process_arg(self, arg): 131 | self.register_variable_set(arg.arg, arg.lineno) 132 | 133 | if arg.annotation is not None: 134 | self.visit(arg.annotation) 135 | 136 | def visit_arguments(self, node): 137 | if hasattr(node, 'posonlyargs'): 138 | for arg in node.posonlyargs : 139 | self.process_arg(arg) 140 | 141 | for arg in node.args: 142 | self.process_arg(arg) 143 | 144 | for arg in node.kwonlyargs: 145 | self.process_arg(arg) 146 | 147 | if node.vararg: 148 | self.process_arg(node.vararg) 149 | 150 | if node.kwarg: 151 | self.process_arg(node.kwarg) 152 | 153 | def visit_ClassDef(self, node): 154 | self.register_variable_set(node.name, node.lineno) 155 | 156 | self.push_scope(type=c.SCOPE_TYPE.CLASS) 157 | 158 | for keyword in node.keywords: 159 | self.register_variable_set(keyword, node.lineno) 160 | 161 | self.generic_visit(node) 162 | self.pop_scope() 163 | 164 | def visit_ExceptHandler(self, node): 165 | if node.type: 166 | self.visit(node.type) 167 | 168 | self.push_scope(type=c.SCOPE_TYPE.NORMAL) 169 | 170 | if node.name: 171 | self.register_variable_set(node.name, node.lineno) 172 | 173 | for body_node in node.body: 174 | self.visit(body_node) 175 | 176 | self.pop_scope() 177 | 178 | def visit_match_case(self, node): 179 | 180 | self.visit(node.pattern) 181 | 182 | if node.guard is not None: 183 | self.visit(node.guard) 184 | 185 | for body_node in node.body: 186 | self.visit(body_node) 187 | 188 | def visit_MatchAs(self, node): 189 | self.register_variable_set(node.name, node.lineno) 190 | 191 | def visit_MatchStar(self, node): 192 | self.register_variable_set(node.name, node.lineno) 193 | 194 | def visit_MatchMapping(self, node): 195 | for key in node.keys: 196 | self.visit(key) 197 | 198 | for pattern in node.patterns: 199 | self.visit(pattern) 200 | 201 | if node.rest is not None: 202 | self.register_variable_set(node.rest, node.lineno) 203 | -------------------------------------------------------------------------------- /smart_imports/tests/test_cache.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | import uuid 4 | import unittest 5 | import tempfile 6 | import warnings 7 | 8 | from unittest import mock 9 | 10 | from .. import cache 11 | 12 | 13 | class TestGetChecksum(unittest.TestCase): 14 | 15 | def test_not_intersect(self): 16 | sums = set() 17 | 18 | for i in range(1000): 19 | sums.add(cache.get_checksum(uuid.uuid4().hex)) 20 | 21 | self.assertEqual(len(sums), 1000) 22 | 23 | 24 | class TestGetCachePath(unittest.TestCase): 25 | 26 | def test(self): 27 | self.assertEqual(cache.get_cache_path(cache_dir='/tmp/cache_dir', 28 | module_name='my.super.module'), 29 | '/tmp/cache_dir/my.super.module.cache') 30 | 31 | 32 | class TestGetSet(unittest.TestCase): 33 | 34 | def test_not_cached(self): 35 | with tempfile.TemporaryDirectory() as temp_directory: 36 | variables = cache.get(cache_dir=temp_directory, 37 | module_name='x.y', 38 | checksum=cache.get_checksum('abc')) 39 | self.assertEqual(variables, None) 40 | 41 | def test_set_get(self): 42 | variables = ['a', 'x', 'zzz', 'long_long_long'] 43 | 44 | with tempfile.TemporaryDirectory() as temp_directory: 45 | cache.set(cache_dir=temp_directory, 46 | module_name='x.y', 47 | checksum=cache.get_checksum('abc'), 48 | variables=variables) 49 | 50 | self.assertTrue(os.path.isfile(os.path.join(temp_directory, 'x.y.cache'))) 51 | 52 | loaded_variables = cache.get(cache_dir=temp_directory, 53 | module_name='x.y', 54 | checksum=cache.get_checksum('abc')) 55 | 56 | self.assertEqual(variables, loaded_variables) 57 | 58 | def test_set_get__create_directories(self): 59 | variables = ['a', 'x', 'zzz', 'long_long_long'] 60 | 61 | with tempfile.TemporaryDirectory() as temp_directory: 62 | cache_dir = os.path.join(temp_directory, 'unexisted_1', 'unexisted_2') 63 | 64 | cache.set(cache_dir=cache_dir, 65 | module_name='x.y', 66 | checksum=cache.get_checksum('abc'), 67 | variables=variables) 68 | 69 | self.assertTrue(os.path.isdir(cache_dir)) 70 | 71 | self.assertTrue(os.path.isfile(os.path.join(cache_dir, 'x.y.cache'))) 72 | 73 | loaded_variables = cache.get(cache_dir=cache_dir, 74 | module_name='x.y', 75 | checksum=cache.get_checksum('abc')) 76 | 77 | self.assertEqual(variables, loaded_variables) 78 | 79 | def test_set_get__ignore_errors(self): 80 | variables = ['a', 'x', 'zzz', 'long_long_long'] 81 | 82 | with warnings.catch_warnings(record=True) as w: 83 | warnings.simplefilter("always") 84 | 85 | cache.set(cache_dir=None, 86 | module_name='x.y', 87 | checksum=cache.get_checksum('abc'), 88 | variables=variables) 89 | 90 | assert len(w) == 1 91 | assert issubclass(w[-1].category, UserWarning) 92 | assert cache.WARNING_MESSAGE in str(w[-1].message) 93 | 94 | with warnings.catch_warnings(record=True) as w: 95 | warnings.simplefilter("always") 96 | 97 | loaded_variables = cache.get(cache_dir=None, 98 | module_name='x.y', 99 | checksum=cache.get_checksum('abc')) 100 | 101 | assert len(w) == 1 102 | assert issubclass(w[-1].category, UserWarning) 103 | assert cache.WARNING_MESSAGE in str(w[-1].message) 104 | 105 | self.assertEqual(loaded_variables, None) 106 | 107 | 108 | def test_wrong_checksum(self): 109 | with tempfile.TemporaryDirectory() as temp_directory: 110 | cache.set(cache_dir=temp_directory, 111 | module_name='x.y', 112 | checksum=cache.get_checksum('abc'), 113 | variables=['x']) 114 | 115 | loaded_variables = cache.get(cache_dir=temp_directory, 116 | module_name='x.y', 117 | checksum=cache.get_checksum('abcd')) 118 | 119 | self.assertEqual(loaded_variables, None) 120 | 121 | def test_wrong_protocol_version(self): 122 | with tempfile.TemporaryDirectory() as temp_directory: 123 | cache.set(cache_dir=temp_directory, 124 | module_name='x.y', 125 | checksum=cache.get_checksum('abc'), 126 | variables=['x']) 127 | 128 | loaded_variables = cache.get(cache_dir=temp_directory, 129 | module_name='x.y', 130 | checksum=cache.get_checksum('abc')) 131 | 132 | self.assertEqual(loaded_variables, ['x']) 133 | 134 | with mock.patch('smart_imports.constants.CACHE_PROTOCOL_VERSION', uuid.uuid4().hex): 135 | loaded_variables = cache.get(cache_dir=temp_directory, 136 | module_name='x.y', 137 | checksum=cache.get_checksum('abc')) 138 | 139 | self.assertEqual(loaded_variables, None) 140 | 141 | def test_no_variables(self): 142 | with tempfile.TemporaryDirectory() as temp_directory: 143 | cache.set(cache_dir=temp_directory, 144 | module_name='x.y', 145 | checksum=cache.get_checksum('abc'), 146 | variables=[]) 147 | 148 | self.assertTrue(os.path.isfile(os.path.join(temp_directory, 'x.y.cache'))) 149 | 150 | loaded_variables = cache.get(cache_dir=temp_directory, 151 | module_name='x.y', 152 | checksum=cache.get_checksum('abc')) 153 | 154 | self.assertEqual(loaded_variables, []) 155 | 156 | 157 | class TestCache(unittest.TestCase): 158 | 159 | def test_no_cache_dir(self): 160 | module_cache = cache.Cache(cache_dir=None, 161 | module_name='x.y', 162 | source='abc') 163 | 164 | variables = module_cache.get() 165 | 166 | self.assertEqual(variables, None) 167 | 168 | def test_has_cache_dir(self): 169 | variables = ['x', 'long_long'] 170 | 171 | with tempfile.TemporaryDirectory() as temp_directory: 172 | module_cache = cache.Cache(cache_dir=temp_directory, 173 | module_name='x.y', 174 | source='abc') 175 | 176 | module_cache.set(variables=variables) 177 | 178 | self.assertTrue(os.path.isfile(os.path.join(temp_directory, 'x.y.cache'))) 179 | 180 | loaded_variables = module_cache.get() 181 | 182 | self.assertTrue(loaded_variables, variables) 183 | -------------------------------------------------------------------------------- /smart_imports/tests/test_config.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | import json 4 | import uuid 5 | import pathlib 6 | import tempfile 7 | import unittest 8 | 9 | from .. import config 10 | from .. import constants 11 | from .. import exceptions 12 | 13 | 14 | class TestGet(unittest.TestCase): 15 | 16 | def setUp(self): 17 | super().setUp() 18 | config.CONFIGS_CACHE.clear() 19 | 20 | def tearDown(self): 21 | super().tearDown() 22 | config.CONFIGS_CACHE.clear() 23 | 24 | def prepair_data(self, temp_directory, 25 | parent_config=config.DEFAULT_CONFIG, 26 | child_config=config.DEFAULT_CONFIG): 27 | path = os.path.join(temp_directory, 28 | 'dir_with_not_reached_config', 29 | 'dir_with_config', 30 | 'empty_dir', 31 | 'leaf_dir') 32 | os.makedirs(path) 33 | 34 | path_1 = os.path.join(temp_directory, 35 | 'dir_with_not_reached_config', 36 | constants.CONFIG_FILE_NAME) 37 | 38 | with open(path_1, 'w') as f: 39 | data = parent_config.clone(path=path_1) 40 | f.write(json.dumps(data.serialize())) 41 | 42 | path_2 = os.path.join(temp_directory, 43 | 'dir_with_not_reached_config', 44 | 'dir_with_config', 45 | constants.CONFIG_FILE_NAME) 46 | 47 | with open(path_2, 'w') as f: 48 | data = child_config.clone(path='#config.2') 49 | f.write(json.dumps(data.serialize())) 50 | 51 | return path 52 | 53 | def test_get(self): 54 | with tempfile.TemporaryDirectory() as temp_directory: 55 | leaf_path = self.prepair_data(temp_directory) 56 | 57 | loaded_config = config.get(leaf_path) 58 | 59 | config_path = os.path.join(os.path.dirname(os.path.dirname(leaf_path)), constants.CONFIG_FILE_NAME) 60 | 61 | expected_config = config.DEFAULT_CONFIG.clone(path=config_path) 62 | 63 | self.assertEqual(expected_config, loaded_config) 64 | 65 | # test cache here, file already removed, but cached data still exists 66 | loaded_config = config.get(leaf_path) 67 | 68 | self.assertEqual(expected_config, loaded_config) 69 | 70 | self.assertEqual(config.CONFIGS_CACHE, 71 | {leaf_path: expected_config, 72 | os.path.dirname(leaf_path): expected_config, 73 | os.path.dirname(os.path.dirname(leaf_path)): expected_config}) 74 | 75 | def test_get__fill_missed_arguments(self): 76 | with tempfile.TemporaryDirectory() as temp_directory: 77 | 78 | child_config = config.DEFAULT_CONFIG.clone(rules=[]) 79 | 80 | leaf_path = self.prepair_data(temp_directory, child_config=child_config) 81 | 82 | loaded_config = config.get(leaf_path) 83 | 84 | self.assertEqual(loaded_config.uid, loaded_config.path) 85 | self.assertEqual(loaded_config.path, loaded_config.path) 86 | 87 | self.assertTrue(os.path.isfile(loaded_config.path)) 88 | 89 | def test_two_configs(self): 90 | with tempfile.TemporaryDirectory() as temp_directory: 91 | leaf_path_1 = self.prepair_data(temp_directory) 92 | 93 | leaf_path_2 = leaf_path_1 + '2' 94 | os.makedirs(leaf_path_2) 95 | 96 | config_2_path = os.path.join(leaf_path_2, constants.CONFIG_FILE_NAME) 97 | 98 | with open(config_2_path, 'w') as f: 99 | expected_config_2 = config.DEFAULT_CONFIG.clone(path=config_2_path) 100 | f.write(json.dumps(expected_config_2.serialize())) 101 | 102 | loaded_config_1 = config.get(leaf_path_1) 103 | loaded_config_2 = config.get(leaf_path_2) 104 | 105 | config_1_path = os.path.join(os.path.dirname(os.path.dirname(leaf_path_1)), constants.CONFIG_FILE_NAME) 106 | 107 | expected_config_1 = config.DEFAULT_CONFIG.clone(path=config_1_path) 108 | 109 | expected_config_1.path = config_1_path 110 | expected_config_2.path = config_2_path 111 | 112 | self.assertEqual(expected_config_1, loaded_config_1) 113 | self.assertEqual(expected_config_2, loaded_config_2) 114 | 115 | self.assertEqual(config.CONFIGS_CACHE, 116 | {leaf_path_2: expected_config_2, 117 | leaf_path_1: expected_config_1, 118 | os.path.dirname(leaf_path_1): expected_config_1, 119 | os.path.dirname(os.path.dirname(leaf_path_1)): expected_config_1}) 120 | 121 | def test_not_found_find(self): 122 | with tempfile.TemporaryDirectory() as temp_directory: 123 | leaf_path = self.prepair_data(temp_directory) 124 | 125 | loaded_config = config.get(leaf_path, config_name='not_found.json') 126 | 127 | self.assertEqual(loaded_config, config.DEFAULT_CONFIG.clone(path=loaded_config.path)) 128 | 129 | 130 | class TestLoad(unittest.TestCase): 131 | 132 | def test_not_exists(self): 133 | with self.assertRaises(exceptions.ConfigNotFound): 134 | config.load('/tmp/{}'.format(uuid.uuid4().hex)) 135 | 136 | def test_wrong_format(self): 137 | with self.assertRaises(exceptions.ConfigHasWrongFormat): 138 | with tempfile.NamedTemporaryFile(delete=False) as f: 139 | f.write('broken json'.encode('utf-8')) 140 | f.close() 141 | 142 | config.load(f.name) 143 | 144 | def test_success(self): 145 | with tempfile.NamedTemporaryFile(delete=False) as f: 146 | f.write(json.dumps(config.DEFAULT_CONFIG.serialize()).encode('utf-8')) 147 | f.close() 148 | 149 | self.assertEqual(config.load(f.name), config.DEFAULT_CONFIG.clone(path=f.name)) 150 | 151 | def test_check_on_load(self): 152 | with self.assertRaises(exceptions.ConfigError): 153 | with tempfile.NamedTemporaryFile(delete=False) as f: 154 | f.write(b'{}') 155 | f.close() 156 | 157 | config.load(f.name) 158 | 159 | 160 | class TestCheck(unittest.TestCase): 161 | 162 | def check_load(self, data): 163 | with tempfile.NamedTemporaryFile(delete=False) as f: 164 | f.write(json.dumps(data).encode('utf-8')) 165 | f.close() 166 | 167 | config.load(f.name) 168 | 169 | def test_no_rules(self): 170 | data = config.DEFAULT_CONFIG.serialize() 171 | del data['rules'] 172 | 173 | with self.assertRaises(exceptions.ConfigHasWrongFormat): 174 | self.check_load(data) 175 | 176 | def test_success(self): 177 | self.check_load(config.DEFAULT_CONFIG.serialize()) 178 | 179 | 180 | class TestExpandCacheDirPath(unittest.TestCase): 181 | 182 | def test_none(self): 183 | with tempfile.NamedTemporaryFile(delete=False) as f: 184 | path = config.expand_cache_dir_path(config_path=f.name, cache_dir=None) 185 | self.assertEqual(path, None) 186 | 187 | def test_homedir(self): 188 | with tempfile.NamedTemporaryFile(delete=False) as f: 189 | path = config.expand_cache_dir_path(config_path=f.name, cache_dir='~/1/2/3') 190 | self.assertEqual(path, str(pathlib.Path.home() / '1/2/3')) 191 | 192 | def test_absolute(self): 193 | with tempfile.NamedTemporaryFile(delete=False) as f: 194 | path = config.expand_cache_dir_path(config_path=f.name, cache_dir='/1/2/3') 195 | self.assertEqual(path, '/1/2/3') 196 | 197 | def test_relative(self): 198 | with tempfile.NamedTemporaryFile(delete=False) as f: 199 | path = config.expand_cache_dir_path(config_path=f.name, cache_dir='./1/2/3') 200 | self.assertEqual(path, str(pathlib.Path(f.name).parent / './1/2/3')) 201 | 202 | path = config.expand_cache_dir_path(config_path=f.name, cache_dir='1/2/3') 203 | self.assertEqual(path, str(pathlib.Path(f.name).parent / '1/2/3')) 204 | -------------------------------------------------------------------------------- /smart_imports/rules.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | import sys 4 | import pkgutil 5 | import importlib 6 | import importlib.util 7 | 8 | from . import exceptions 9 | from . import discovering 10 | 11 | 12 | _FABRICS = {} 13 | _RULES = {} 14 | 15 | 16 | def register(name, rule): 17 | if name in _FABRICS: 18 | raise exceptions.RuleAlreadyRegistered(rule=name) 19 | 20 | _FABRICS[name] = rule 21 | 22 | 23 | def remove(name): 24 | if name in _FABRICS: 25 | del _FABRICS[name] 26 | 27 | 28 | def get_for_config(config): 29 | uid = config.uid 30 | 31 | if uid not in _RULES: 32 | rules = [] 33 | 34 | for rule_config in config.rules: 35 | fabric_type = rule_config['type'] 36 | 37 | if fabric_type not in _FABRICS: 38 | raise exceptions.RuleNotRegistered(rule=fabric_type) 39 | 40 | rule = _FABRICS[fabric_type](config=rule_config) 41 | 42 | if not rule.verify_config(): 43 | raise exceptions.ConfigHasWrongFormat(path=config.path, 44 | message='wrong format of rule {}'.format(fabric_type)) 45 | 46 | rules.append(rule) 47 | 48 | _RULES[uid] = rules 49 | 50 | return _RULES[uid] 51 | 52 | 53 | def reset_rules_cache(): 54 | _RULES.clear() 55 | 56 | 57 | class ImportCommand: 58 | __slots__ = ('target_module', 'target_attribute', 'source_module', 'source_attribute') 59 | 60 | def __init__(self, target_module, target_attribute, source_module, source_attribute): 61 | self.target_module = target_module 62 | self.target_attribute = target_attribute 63 | self.source_module = source_module 64 | self.source_attribute = source_attribute 65 | 66 | def __call__(self): 67 | imported_module = importlib.import_module(self.source_module) 68 | 69 | if self.source_attribute is None: 70 | value = imported_module 71 | else: 72 | value = getattr(imported_module, self.source_attribute) 73 | 74 | setattr(self.target_module, self.target_attribute, value) 75 | 76 | def __str__(self): 77 | return 'ImportCommand({}, {}, {}, {})'.format(self.target_module, 78 | self.target_attribute, 79 | self.source_module, 80 | self.source_attribute) 81 | 82 | def __eq__(self, other): 83 | return (self.__class__ == other.__class__ and 84 | self.target_module == other.target_module and 85 | self.target_attribute == other.target_attribute and 86 | self.source_module == other.source_module and 87 | self.source_attribute == other.source_attribute) 88 | 89 | def __ne__(self, other): 90 | return not self.__eq__(other) 91 | 92 | 93 | class NoImportCommand(ImportCommand): 94 | __slots__ = () 95 | 96 | def __init__(self): 97 | super().__init__(target_module=None, 98 | target_attribute=None, 99 | source_module=None, 100 | source_attribute=None) 101 | 102 | def __call__(self): 103 | pass 104 | 105 | 106 | class BaseRule: 107 | __slots__ = ('config',) 108 | 109 | def __init__(self, config): 110 | self.config = config 111 | 112 | def verify_config(self): 113 | return True 114 | 115 | def apply(self, module, variable): 116 | raise NotImplementedError 117 | 118 | 119 | class CustomRule(BaseRule): 120 | __slots__ = () 121 | 122 | def verify_config(self): 123 | if 'variables' not in self.config: 124 | return False 125 | 126 | return super().verify_config() 127 | 128 | def apply(self, module, variable): 129 | 130 | if variable not in self.config['variables']: 131 | return None 132 | 133 | module_name = self.config['variables'][variable]['module'] 134 | attribute = self.config['variables'][variable].get('attribute') 135 | return ImportCommand(module, variable, module_name, attribute) 136 | 137 | 138 | class LocalModulesRule(BaseRule): 139 | __slots__ = () 140 | 141 | _LOCAL_MODULES_CACHE = {} 142 | 143 | def verify_config(self): 144 | return super().verify_config() 145 | 146 | def apply(self, module, variable): 147 | 148 | package_name = getattr(module, '__package__', None) 149 | 150 | if not package_name: 151 | return None 152 | 153 | if package_name not in self._LOCAL_MODULES_CACHE: 154 | 155 | parent = sys.modules[package_name] 156 | 157 | local_modules = set() 158 | 159 | for module_finder, name, ispkg in pkgutil.iter_modules(path=parent.__path__): 160 | local_modules.add(name) 161 | 162 | self._LOCAL_MODULES_CACHE[package_name] = frozenset(local_modules) 163 | 164 | if variable not in self._LOCAL_MODULES_CACHE[package_name]: 165 | return None 166 | 167 | return ImportCommand(target_module=module, 168 | target_attribute=variable, 169 | source_module='{}.{}'.format(package_name, variable), 170 | source_attribute=None) 171 | 172 | 173 | class GlobalModulesRule(BaseRule): 174 | __slots__ = () 175 | 176 | def verify_config(self): 177 | return super().verify_config() 178 | 179 | def apply(self, module, variable): 180 | 181 | spec = discovering.find_spec(variable) 182 | 183 | if spec is None: 184 | return None 185 | 186 | return ImportCommand(target_module=module, 187 | target_attribute=variable, 188 | source_module=variable, 189 | source_attribute=None) 190 | 191 | 192 | # packages lists for every python version can be found here: 193 | # https://github.com/jackmaney/python-stdlib-list 194 | def _collect_stdlib_modules(): 195 | variables = {} 196 | 197 | for compiled_module_name in sys.builtin_module_names: 198 | variables[compiled_module_name] = {'module': compiled_module_name} 199 | 200 | with open(os.path.join(os.path.dirname(__file__), 201 | 'fixtures', 202 | 'python_{}_{}_packages.txt'.format(sys.version_info.major, sys.version_info.minor))) as f: 203 | for line in f.readlines(): 204 | names = line.strip().split('.') 205 | for i in range(len(names)): 206 | variables['_'.join(names[:i+1])] = {'module': '.'.join(names[:i+1])} 207 | 208 | return variables 209 | 210 | 211 | class StdLibRule(BaseRule): 212 | __slots__ = ('_stdlib_modules',) 213 | 214 | _STDLIB_MODULES = _collect_stdlib_modules() 215 | 216 | def verify_config(self): 217 | return super().verify_config() 218 | 219 | def apply(self, module, variable): 220 | 221 | if variable not in self._STDLIB_MODULES: 222 | return None 223 | 224 | module_name = self._STDLIB_MODULES[variable]['module'] 225 | attribute = self._STDLIB_MODULES[variable].get('attribute') 226 | 227 | return ImportCommand(module, variable, module_name, attribute) 228 | 229 | 230 | class PredefinedNamesRule(BaseRule): 231 | __slots__ = () 232 | 233 | PREDEFINED_NAMES = frozenset({'__file__', '__annotations__'}) 234 | 235 | def verify_config(self): 236 | return super().verify_config() 237 | 238 | def apply(self, module, variable): 239 | 240 | if variable in self.PREDEFINED_NAMES: 241 | return NoImportCommand() 242 | 243 | if variable in __builtins__: 244 | return NoImportCommand() 245 | 246 | return None 247 | 248 | 249 | class PrefixRule(BaseRule): 250 | __slots__ = () 251 | 252 | def verify_config(self): 253 | if 'prefixes' not in self.config: 254 | return False 255 | 256 | for rule in self.config['prefixes']: 257 | if 'prefix' not in rule: 258 | return False 259 | 260 | return super().verify_config() 261 | 262 | def apply(self, module, variable): 263 | 264 | for rule in self.config['prefixes']: 265 | prefix = rule['prefix'] 266 | 267 | if not variable.startswith(prefix): 268 | continue 269 | 270 | return ImportCommand(module, variable, '{}.{}'.format(rule['module'], variable[len(prefix):]), None) 271 | 272 | return None 273 | 274 | 275 | class LocalModulesFromParentRule(BaseRule): 276 | __slots__ = () 277 | 278 | def verify_config(self): 279 | if 'suffixes' not in self.config: 280 | return False 281 | 282 | return super().verify_config() 283 | 284 | def apply(self, module, variable): 285 | 286 | package_name = module.__package__ 287 | 288 | for suffix in self.config['suffixes']: 289 | 290 | if not package_name.endswith(suffix): 291 | continue 292 | 293 | base_package_name = package_name[:-len(suffix)] 294 | 295 | source_module = '{}.{}'.format(base_package_name, variable) 296 | 297 | if discovering.find_spec(source_module) is None: 298 | continue 299 | 300 | return ImportCommand(target_module=module, 301 | target_attribute=variable, 302 | source_module=source_module, 303 | source_attribute=None) 304 | 305 | 306 | class LocalModulesFromNamespaceRule(BaseRule): 307 | __slots__ = () 308 | 309 | def verify_config(self): 310 | return super().verify_config() 311 | 312 | def apply(self, module, variable): 313 | 314 | if 'map' not in self.config: 315 | return False 316 | 317 | package_name = module.__package__ 318 | 319 | for target, namespaces in self.config['map'].items(): 320 | for namespace in namespaces: 321 | 322 | if package_name != target: 323 | continue 324 | 325 | spec = discovering.find_spec(namespace) 326 | 327 | if spec is None: 328 | continue 329 | 330 | namespace_package = spec.parent 331 | 332 | source_module = '{}.{}'.format(namespace_package, variable) 333 | 334 | if discovering.find_spec(source_module) is None: 335 | continue 336 | 337 | return ImportCommand(target_module=module, 338 | target_attribute=variable, 339 | source_module=source_module, 340 | source_attribute=None) 341 | 342 | 343 | register('rule_predefined_names', PredefinedNamesRule) 344 | register('rule_local_modules', LocalModulesRule) 345 | register('rule_global_modules', GlobalModulesRule) 346 | register('rule_custom', CustomRule) 347 | register('rule_stdlib', StdLibRule) 348 | register('rule_prefix', PrefixRule) 349 | register('rule_local_modules_from_parent', LocalModulesFromParentRule) 350 | register('rule_local_modules_from_namespace', LocalModulesFromNamespaceRule) 351 | -------------------------------------------------------------------------------- /smart_imports/tests/test_importer.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | import math 4 | import json 5 | import uuid 6 | import unittest 7 | import importlib 8 | import subprocess 9 | 10 | from unittest import mock 11 | 12 | from .. import rules 13 | from .. import config 14 | from .. import helpers 15 | from .. import importer 16 | from .. import constants 17 | from .. import exceptions 18 | from .. import scopes_tree 19 | 20 | 21 | TEST_FIXTURES_DIR = os.path.join(os.path.dirname(__file__), 'fixtures') 22 | 23 | 24 | class TestApplyRules(unittest.TestCase): 25 | 26 | def setUp(self): 27 | self.source_module = 'smart_imports.tests.fake_package.config_variables' 28 | 29 | self.config = config.DEFAULT_CONFIG.clone(path='#config.1', 30 | rules=[{'type': 'rule_custom', 31 | 'variables': {'config_variable': {'module': self.source_module}}}, 32 | {'type': 'rule_local_modules'}, 33 | {'type': 'rule_stdlib'}, 34 | {'type': 'rule_predefined_names'}]) 35 | 36 | self.module = type(os) 37 | 38 | def test_command_not_found(self): 39 | result = importer.apply_rules(module_config=self.config, 40 | module=self.module, 41 | variable='x') 42 | self.assertEqual(result, None) 43 | 44 | def test_command_found(self): 45 | command = importer.apply_rules(module_config=self.config, 46 | module=self.module, 47 | variable='config_variable') 48 | 49 | self.assertEqual(command, rules.ImportCommand(target_module=self.module, 50 | target_attribute='config_variable', 51 | source_module=self.source_module, 52 | source_attribute=None)) 53 | 54 | def test_rules_priority(self): 55 | test_config = config.DEFAULT_CONFIG.clone(path='#config.2', 56 | rules=[{'type': 'rule_custom', 57 | 'variables': {'var_1': {'module': 'math'}}}, 58 | {'type': 'rule_custom', 59 | 'variables': {'var_1': {'module': 'json'}}}]) 60 | command = importer.apply_rules(module_config=test_config, 61 | module=self.module, 62 | variable='var_1') 63 | 64 | self.assertEqual(command, rules.ImportCommand(target_module=self.module, 65 | target_attribute='var_1', 66 | source_module='math', 67 | source_attribute=None)) 68 | 69 | 70 | class TestGetModuleScopesTree(unittest.TestCase): 71 | 72 | def test(self): 73 | source = ''' 74 | x = 1 75 | 76 | def y(q): 77 | return q + z 78 | ''' 79 | scope = importer.get_module_scopes_tree(source) 80 | 81 | self.assertEqual(scope.variables, {'x': scopes_tree.VariableInfo(constants.VARIABLE_STATE.INITIALIZED, 2), 82 | 'y': scopes_tree.VariableInfo(constants.VARIABLE_STATE.INITIALIZED, 4)}) 83 | 84 | self.assertEqual(scope.children[0].variables, 85 | {'q': scopes_tree.VariableInfo(constants.VARIABLE_STATE.INITIALIZED, 4), 86 | 'z': scopes_tree.VariableInfo(constants.VARIABLE_STATE.UNINITIALIZED, 5)}) 87 | 88 | 89 | class TestExtractVariables(unittest.TestCase): 90 | 91 | def test_empty_source(self): 92 | self.assertEqual(importer.extract_variables(''), ([], {})) 93 | 94 | def test_has_source(self): 95 | source = ''' 96 | x = 1 + y 97 | 98 | def y(): 99 | return x + z 100 | ''' 101 | self.assertEqual(set(importer.extract_variables(source)[0]), 102 | {'z', 'y'}) 103 | 104 | 105 | class TestProcessModule(unittest.TestCase): 106 | 107 | SIMPLE_SOURCE = ''' 108 | x = 'X' 109 | 110 | def y(z): 111 | return z + math.log(1) 112 | ''' 113 | 114 | def apply_commands(self, commands): 115 | for command in commands: 116 | command() 117 | 118 | def test_process_simple(self): 119 | module_name = 'process_simple_' + uuid.uuid4().hex 120 | 121 | with helpers.test_directory() as temp_directory: 122 | with open(os.path.join(temp_directory, module_name + '.py'), 'w') as f: 123 | f.write(self.SIMPLE_SOURCE) 124 | 125 | module = importlib.import_module(module_name) 126 | 127 | self.assertEqual(getattr(module, 'math', None), None) 128 | 129 | commands = importer.process_module(module_config=config.DEFAULT_CONFIG, 130 | module=module) 131 | 132 | self.assertEqual(commands, 133 | [rules.ImportCommand(target_module=module, 134 | target_attribute='math', 135 | source_module='math', 136 | source_attribute=None)]) 137 | 138 | self.apply_commands(commands) 139 | 140 | self.assertEqual(getattr(module, 'math'), math) 141 | 142 | def test_process_simple__cached(self): 143 | module_name = 'process_simple_' + uuid.uuid4().hex 144 | 145 | with helpers.test_directory() as temp_directory: 146 | with open(os.path.join(temp_directory, module_name + '.py'), 'w') as f: 147 | f.write(self.SIMPLE_SOURCE) 148 | 149 | module = importlib.import_module(module_name) 150 | 151 | self.assertEqual(getattr(module, 'math', None), None) 152 | 153 | # not required to create other temp directory, since filenames do not intersect 154 | test_config = config.DEFAULT_CONFIG.clone(cache_dir=temp_directory) 155 | 156 | commands = importer.process_module(module_config=test_config, 157 | module=module) 158 | 159 | self.apply_commands(commands) 160 | 161 | self.assertEqual(getattr(module, 'math'), math) 162 | 163 | self.assertTrue(os.path.isfile(os.path.join(temp_directory, module_name + '.cache'))) 164 | 165 | with mock.patch('smart_imports.importer.extract_variables') as extract_variables: 166 | importer.process_module(module_config=test_config, 167 | module=module) 168 | 169 | extract_variables.assert_not_called() 170 | 171 | def prepair_data(self, temp_directory): 172 | modules_names = [] 173 | 174 | for i in range(1, 5): 175 | modules_names.append('process_module_circular_{}_{}'.format(i, uuid.uuid4().hex)) 176 | 177 | source_1 = ''' 178 | def import_hook(): 179 | from smart_imports import config 180 | from smart_imports import importer 181 | from smart_imports import discovering 182 | 183 | target_module = discovering.find_target_module() 184 | 185 | commands = importer.process_module(module_config=config.DEFAULT_CONFIG, 186 | module=target_module) 187 | 188 | for command in commands: 189 | command() 190 | 191 | 192 | import_hook() 193 | 194 | 195 | x = 1 196 | 197 | 198 | def y(): 199 | return {module_2_name}.z() 200 | '''.format(module_2_name=modules_names[1]) 201 | 202 | source_2 = ''' 203 | def import_hook(): 204 | from smart_imports import config 205 | from smart_imports import importer 206 | from smart_imports import discovering 207 | 208 | target_module = discovering.find_target_module() 209 | 210 | commands = importer.process_module(module_config=config.DEFAULT_CONFIG, 211 | module=target_module) 212 | 213 | 214 | for command in commands: 215 | command() 216 | 217 | 218 | import_hook() 219 | 220 | 221 | def z(): 222 | return {module_1_name}.x 223 | '''.format(module_1_name=modules_names[0]) 224 | 225 | source_3 = ''' 226 | def import_hook(): 227 | from smart_imports import config 228 | from smart_imports import importer 229 | from smart_imports import discovering 230 | 231 | target_module = discovering.find_target_module() 232 | 233 | commands = importer.process_module(module_config=config.DEFAULT_CONFIG, 234 | module=target_module) 235 | 236 | for command in commands: 237 | command() 238 | 239 | 240 | import_hook() 241 | 242 | x = 1 243 | 244 | y = 10 + {module_4_name}.z 245 | 246 | '''.format(module_4_name=modules_names[3]) 247 | 248 | source_4 = ''' 249 | 250 | def import_hook(): 251 | from smart_imports import config 252 | from smart_imports import importer 253 | from smart_imports import discovering 254 | 255 | target_module = discovering.find_target_module() 256 | 257 | commands = importer.process_module(module_config=config.DEFAULT_CONFIG, 258 | module=target_module) 259 | 260 | 261 | for command in commands: 262 | command() 263 | 264 | 265 | import_hook() 266 | 267 | 268 | z = 100 + {module_1_name}.x 269 | '''.format(module_1_name=modules_names[0]) 270 | 271 | sources = [source_1, source_2, source_3, source_4] 272 | 273 | for name, source in zip(modules_names, sources): 274 | with open(os.path.join(temp_directory, name + '.py'), 'w') as f: 275 | f.write(source) 276 | 277 | return modules_names 278 | 279 | def test_process_circular__local_namespace(self): 280 | 281 | with helpers.test_directory() as temp_directory: 282 | 283 | modules_names = self.prepair_data(temp_directory) 284 | 285 | module = importlib.import_module(modules_names[0]) 286 | 287 | self.assertTrue(hasattr(module, modules_names[1])) 288 | 289 | self.assertEqual(module.y(), 1) 290 | 291 | def test_process_circular__global_namespace(self): 292 | with helpers.test_directory() as temp_directory: 293 | modules_names = self.prepair_data(temp_directory) 294 | 295 | module = importlib.import_module(modules_names[2]) 296 | 297 | self.assertTrue(hasattr(module, modules_names[3])) 298 | 299 | self.assertEqual(module.y, 111) 300 | 301 | def test_no_import_found(self): 302 | module_name = 'process_module_no_imports_{}'.format(uuid.uuid4().hex) 303 | 304 | source = ''' 305 | def y(): 306 | print(x) 307 | 308 | def z(): 309 | print(x) 310 | ''' 311 | with helpers.test_directory() as temp_directory: 312 | with open(os.path.join(temp_directory, module_name + '.py'), 'w') as f: 313 | f.write(source) 314 | 315 | module = importlib.import_module(module_name) 316 | 317 | with self.assertRaises(exceptions.NoImportFound) as error: 318 | importer.process_module(module_config=config.DEFAULT_CONFIG, 319 | module=module) 320 | 321 | self.assertEqual(set(error.exception.arguments['lines']), {3, 6}) 322 | 323 | def test_no_import_found__cached_module(self): 324 | module_name = 'process_module_no_imports_{}'.format(uuid.uuid4().hex) 325 | 326 | source = ''' 327 | def y(): 328 | print(x) 329 | 330 | def z(): 331 | print(x) 332 | ''' 333 | with helpers.test_directory() as temp_directory: 334 | with open(os.path.join(temp_directory, module_name + '.py'), 'w') as f: 335 | f.write(source) 336 | 337 | module = importlib.import_module(module_name) 338 | 339 | # not required to create other temp directory, since filenames do not intersect 340 | test_config = config.DEFAULT_CONFIG.clone(cache_dir=temp_directory) 341 | 342 | # test repeated calls 343 | for i in range(3): 344 | with self.assertRaises(exceptions.NoImportFound) as error: 345 | importer.process_module(module_config=test_config, 346 | module=module) 347 | 348 | self.assertEqual(set(error.exception.arguments['lines']), {3, 6}) 349 | 350 | self.assertTrue(os.path.isfile(os.path.join(temp_directory, module_name + '.cache'))) 351 | 352 | 353 | class TestAll(unittest.TestCase): 354 | 355 | def test(self): 356 | self.assertNotIn('string', globals()) 357 | 358 | importer.all(importlib.import_module('smart_imports.tests.test_importer')) 359 | 360 | self.assertIn('string', globals()) 361 | 362 | self.assertEqual(string.digits, '0123456789') 363 | 364 | 365 | class TestSimpleScript(unittest.TestCase): 366 | 367 | def prepair_modules(self, base_directory): 368 | os.makedirs(os.path.join(base_directory, 'a', 'b', 'c')) 369 | 370 | script = ''' 371 | import smart_imports 372 | 373 | smart_imports.all() 374 | 375 | myprint((__name__, datetime.datetime.now())) 376 | ''' 377 | 378 | with open(os.path.join(base_directory, 'a.py'), 'w') as f: 379 | f.write(script) 380 | 381 | config = {'rules': [{'type': 'rule_predefined_names'}, 382 | {'type': 'rule_stdlib'}, 383 | {'type': 'rule_custom', 384 | 'variables': {'myprint': {'module': 'pprint', 'attribute': 'pprint'}}}]} 385 | 386 | with open(os.path.join(base_directory, 'smart_imports.json'), 'w') as f: 387 | f.write(json.dumps(config)) 388 | 389 | def test(self): 390 | with helpers.test_directory() as temp_directory: 391 | self.prepair_modules(temp_directory) 392 | 393 | output = subprocess.check_output(['python', os.path.join(temp_directory, 'a.py')]) 394 | 395 | self.assertIn(b"'__main__'", output) 396 | self.assertIn(b"datetime.datetime", output) 397 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | 2 | ========================= 3 | Smart import for Python 4 | ========================= 5 | 6 | |pypi| |python_versions| 7 | 8 | - `Changelog `_ 9 | 10 | Automatically discovers & imports entities, used in the current module. 11 | 12 | No magic or monkey patching. Only standard Python functionality. 13 | 14 | +---------------------------------------------+---------------------------------------------+ 15 | | Before | After | 16 | +=============================================+=============================================+ 17 | |.. code:: python |.. code:: python | 18 | | | | 19 | | import math | import smart_imports | 20 | | from my_project import calc | smart_imports.all() | 21 | | # 100500 other imports | # no any other imports | 22 | | | | 23 | | def my_code(argument, function=calc): | def my_code(argument, function=calc): | 24 | | return math.log(function(argument)) | return math.log(function(argument)) | 25 | | | | 26 | +---------------------------------------------+---------------------------------------------+ 27 | 28 | `MyPy`_ supported. 29 | 30 | Summary 31 | ======= 32 | 33 | * Get source code of the module, from which ``smart_imports.all()`` has called. 34 | * Parse it, find all not initialized variables. 35 | * Search imports, suitable for found variables. 36 | * Import them. 37 | 38 | Library process only modules, from which ``smart_imports`` called explicitly. 39 | 40 | Main idea 41 | ========= 42 | 43 | With time every complex project develops own naming convention. If we translate that convention into more formal rules, we will be able to make automatic imports of every entity, knowing only its name. 44 | 45 | For example, we will not need to write ``import math`` to call ``math.pi``, since our system will understand that ``math`` is the module of the standard library. 46 | 47 | How it works 48 | ============ 49 | 50 | Code from the header works in such way: 51 | 52 | - ``smart_imports.all()`` builds `AST `_ of the module from which it has called. 53 | - Library analyses AST and searches for not initialized variables. 54 | - Name of every found variable processed thought chain of rules to determine the correct module (or its attribute) to import. If the rule finds the target module, chain breaks and the next rules will not be processed. 55 | - Library load found modules and add imported entities into the global namespace. 56 | 57 | ``Smart Imports`` searches not initialized variables in every part of code (including new Python syntax). 58 | 59 | Automatic importing turns on only for modules, that do explicit call of ``smart_imports.all()``. 60 | 61 | Moreover, you can use normal imports with ``Smart Imports`` at the same time. That helps to integrate ``Smart Imports`` step by step. 62 | 63 | You can notice, that AST of module builts two times: 64 | 65 | - when CPython imports module; 66 | - when ``Smart Imports`` process call of ``smart_imports.all()``. 67 | 68 | We can build AST once (for that we can add hook into the process of importing modules with help of `PEP-0302 `_), but it will make import event slower. I think that it is because at import time CPython builds AST in terms of its internal structures (probably implemented in C). Conversion from them to Python AST cost more than building new AST from scratch. 69 | 70 | ``Smart Imports`` build AST only once for every module. 71 | 72 | Default import rules 73 | ==================== 74 | 75 | ``Smart Imports`` can be used without configuration. By default it uses such rules: 76 | 77 | #. By exact match looks for the module with the required name in the folder of the current module. 78 | #. Checks if the standard library has a module with the required name. 79 | 80 | #. By exact match with top-level packages (for example, ``math`` ). 81 | #. For sub-packages and modules checks complex names with dots replaced by underscores (for example, ``os.path`` will be imported for name ``os_path``). 82 | 83 | #. By exact match looks for installed packages with the required name (for example, ``requests`` ). 84 | 85 | Performance 86 | =========== 87 | 88 | ``Smart Imports`` does not slow down runtime but increases startup time. 89 | 90 | Because of building AST, startup time increased in 1.5-2 times. For small projects it is inconsequential. At the same time, the startup time of large projects depends mostly on architecture and dependencies between modules, than from the time of modules import. 91 | 92 | In the future, part of ``Smart Imports`` can be rewritten in C — it should eliminate startup delays. 93 | 94 | To speed up startup time, results of AST processing can be cached on the file system. That behavior can be turned on in the config. ``SmartImports`` invalidates cache when module source code changes. 95 | 96 | Also, ``Smart Imports``' work time highly depends on rules and their sequence. You can reduce these costs by modifying configs. For example, you can specify an explicit import path for a name with `Rule 4: custom names`_. 97 | 98 | Configuration 99 | ============= 100 | 101 | The logic of default configuration was already described. It should be enough to work with the standard library. 102 | 103 | Default config: 104 | 105 | .. code-block:: javascript 106 | 107 | { 108 | "cache_dir": null, 109 | "rules": [{"type": "rule_local_modules"}, 110 | {"type": "rule_stdlib"}, 111 | {"type": "rule_predefined_names"}, 112 | {"type": "rule_global_modules"}] 113 | } 114 | 115 | 116 | If necessary, a more complex config can be put on a file system. 117 | 118 | `Example of complex config `_ (from my pet project). 119 | 120 | At the time of call ``smart_import.all()`` library detects a location of config file by searching file ``smart_imports.json`` from the current folder up to root. If a file will be found, it will become config for the current module. 121 | 122 | You can use multiple config files (place them in different folders). 123 | 124 | There are few config parameters now: 125 | 126 | .. code-block:: javascript 127 | 128 | { 129 | // folder to store cached AST 130 | // if not specified or null, cache will not be used 131 | "cache_dir": null|"string", 132 | 133 | // list of import rules (see further) 134 | "rules": [] 135 | } 136 | 137 | Import rules 138 | ============ 139 | 140 | A sequence of rules in configs determines the order of their application. The first success rule stops processing and makes import. 141 | 142 | `Rule 1: predefined names`_ will be often used in the examples below. It required for the correct processing of default names like ``print``. 143 | 144 | Rule 1: predefined names 145 | ------------------------ 146 | 147 | Rule silences import search for predefined names like ``__file__`` and builtins like ``print``. 148 | 149 | .. code-block:: python 150 | 151 | # config: 152 | # { 153 | # "rules": [{"type": "rule_predefined_names"}] 154 | # } 155 | 156 | import smart_imports 157 | 158 | smart_imports.all() 159 | 160 | # Smart Imports will not search for module with name __file__ 161 | # event if variable is not initialized explicity in code 162 | print(__file__) 163 | 164 | 165 | Rule 2: local modules 166 | --------------------- 167 | 168 | Rule checks if a module with the required name exists in the folder of the current module. If the module found, it will be imported. 169 | 170 | .. code-block:: python 171 | 172 | # config: 173 | # { 174 | # "rules": [{"type": "rule_predefined_names"}, 175 | # {"type": "rule_local_modules"}] 176 | # } 177 | # 178 | # project on file sytem: 179 | # 180 | # my_package 181 | # |-- __init__.py 182 | # |-- a.py 183 | # |-- b.py 184 | 185 | # b.py 186 | import smart_imports 187 | 188 | smart_imports.all() 189 | 190 | # module "a" will be found and imported 191 | print(a) 192 | 193 | 194 | Rule 3: global modules 195 | ---------------------- 196 | 197 | Rule tries to import the module by name. 198 | 199 | .. code-block:: python 200 | 201 | # config: 202 | # { 203 | # "rules": [{"type": "rule_predefined_names"}, 204 | # {"type": "rule_global_modules"}] 205 | # } 206 | # 207 | # install external package 208 | # 209 | # pip install requests 210 | 211 | import smart_imports 212 | 213 | smart_imports.all() 214 | 215 | # module "requests" will be found and imported 216 | print(requests.get('http://example.com')) 217 | 218 | 219 | Rule 4: custom names 220 | -------------------- 221 | 222 | Rule links a name to the specified module and its attribute (optionally). 223 | 224 | .. code-block:: python 225 | 226 | # config: 227 | # { 228 | # "rules": [{"type": "rule_predefined_names"}, 229 | # {"type": "rule_custom", 230 | # "variables": {"my_import_module": {"module": "os.path"}, 231 | # "my_import_attribute": {"module": "random", "attribute": "seed"}}}] 232 | # } 233 | 234 | import smart_imports 235 | 236 | smart_imports.all() 237 | 238 | # we use modules of the standard library in that example 239 | # but any module can be used 240 | print(my_import_module) 241 | print(my_import_attribute) 242 | 243 | 244 | Rule 5: standard library 245 | ------------------------ 246 | 247 | Rule checks if the standard library has a module with the required name. For example ``math`` or ``os.path`` (which will be imported for the name ``os_path``). 248 | 249 | That rule works faster than `Rule 3: global modules`_, since it searches module by predefined list. Lists of modules for every Python version was collected with help of `stdlib-list `_. 250 | 251 | .. code-block:: python 252 | 253 | # config: 254 | # { 255 | # "rules": [{"type": "rule_predefined_names"}, 256 | # {"type": "rule_stdlib"}] 257 | # } 258 | 259 | import smart_imports 260 | 261 | smart_imports.all() 262 | 263 | print(math.pi) 264 | 265 | 266 | Rule 6: import by prefix 267 | ------------------------ 268 | 269 | Rule imports module by name from the package, which associated with name prefix. It can be helpful when you have a package used in the whole project. For example, you can access modules from package ``utils`` with prefix ``utils_``. 270 | 271 | .. code-block:: python 272 | 273 | # config: 274 | # { 275 | # "rules": [{"type": "rule_predefined_names"}, 276 | # {"type": "rule_prefix", 277 | # "prefixes": [{"prefix": "utils_", "module": "my_package.utils"}]}] 278 | # } 279 | # 280 | # project on filesystem 281 | # 282 | # my_package 283 | # |-- __init__.py 284 | # |-- utils 285 | # |-- |-- __init__.py 286 | # |-- |-- a.py 287 | # |-- |-- b.py 288 | # |-- subpackage 289 | # |-- |-- __init__.py 290 | # |-- |-- c.py 291 | 292 | # c.py 293 | 294 | import smart_imports 295 | 296 | smart_imports.all() 297 | 298 | print(utils_a) 299 | print(utils_b) 300 | 301 | 302 | Rule 7: modules from parent package 303 | ----------------------------------- 304 | 305 | If you have sub-packages with the same name in different parts of your project (for example, ``tests`` or ``migrations``), you can allow for them to search modules by name in parent packages. 306 | 307 | .. code-block:: python 308 | 309 | # config: 310 | # { 311 | # "rules": [{"type": "rule_predefined_names"}, 312 | # {"type": "rule_local_modules_from_parent", 313 | # "suffixes": [".tests"]}] 314 | # } 315 | # 316 | # project on file system: 317 | # 318 | # my_package 319 | # |-- __init__.py 320 | # |-- a.py 321 | # |-- tests 322 | # |-- |-- __init__.py 323 | # |-- |-- b.py 324 | 325 | # b.py 326 | 327 | import smart_imports 328 | 329 | smart_imports.all() 330 | 331 | print(a) 332 | 333 | 334 | Rule 8: modules from namespace 335 | ------------------------------ 336 | 337 | The rule allows for modules from a specified package to import by name modules from another package. 338 | 339 | .. code-block:: python 340 | 341 | # config: 342 | # { 343 | # "rules": [{"type": "rule_predefined_names"}, 344 | # {"type": "rule_local_modules_from_namespace", 345 | # "map": {"my_package.subpackage_1": ["my_package.subpackage_2"]}}] 346 | # } 347 | # 348 | # project on filesystem: 349 | # 350 | # my_package 351 | # |-- __init__.py 352 | # |-- subpackage_1 353 | # |-- |-- __init__.py 354 | # |-- |-- a.py 355 | # |-- subpackage_2 356 | # |-- |-- __init__.py 357 | # |-- |-- b.py 358 | 359 | # a.py 360 | 361 | import smart_imports 362 | 363 | smart_imports.all() 364 | 365 | print(b) 366 | 367 | How to add custom rule? 368 | ----------------------- 369 | 370 | #. Subclass ``smart_imports.rules.BaseRule``. 371 | #. Implement required logic. 372 | #. Register rule with method ``smart_imports.rules.register``. 373 | #. Add rule to config. 374 | #. ??? 375 | #. Profit. 376 | 377 | Look into the implementation of current rules, if you need an example. 378 | 379 | Similar projects 380 | ================ 381 | 382 | There are a couple of projects with a similar approach: 383 | 384 | - `autoimport `_ — automatically fixes wrong import statements. 385 | 386 | If you think that some projects should be listed here, feel free to make a pull request. 387 | 388 | 389 | MyPY 390 | ==== 391 | 392 | Plugin for integration with MyPy implemented. 393 | 394 | MyPy config (mypy.ini) example: 395 | 396 | .. code-block:: ini 397 | 398 | [mypy] 399 | plugins = smart_imports.plugins.mypy 400 | 401 | 402 | Plans 403 | ===== 404 | 405 | I love the idea of determining code properties by used names. So, I will try to develop it in the borders of ``Smart Imports`` and other projects. 406 | 407 | What I planning for ``Smart Imports``: 408 | 409 | - Continue support and patch it for new versions of Python. 410 | - Research usage of type annotations to import automatization. 411 | - Try to implement lazy imports. 412 | - Implement utilities for automatic config generation and code refactoring. 413 | - Rewrite part of code in C, to speedup AST construction. 414 | - Implement integrations with popular IDEs. 415 | 416 | I open to your suggestions. Feel free to contact me in any way. 417 | 418 | 419 | .. |pypi| image:: https://img.shields.io/pypi/v/smart_imports.svg?style=flat-square&label=latest%20stable%20version&reset_github_caches=10 420 | :target: https://pypi.python.org/pypi/smart_imports 421 | :alt: Latest version released on PyPi 422 | 423 | .. |python_versions| image:: https://img.shields.io/pypi/pyversions/smart_imports.svg?style=flat-square&reset_github_caches=10 424 | :target: https://pypi.python.org/pypi/smart_imports 425 | :alt: Supported Python versions 426 | -------------------------------------------------------------------------------- /smart_imports/tests/test_scopes_tree.py: -------------------------------------------------------------------------------- 1 | 2 | import unittest 3 | 4 | from .. import scopes_tree 5 | from .. import constants as c 6 | 7 | 8 | class TestScope(unittest.TestCase): 9 | 10 | def test_initialization(self): 11 | scope = scopes_tree.Scope(type=c.SCOPE_TYPE.NORMAL) 12 | 13 | self.assertEqual(scope.type, c.SCOPE_TYPE.NORMAL) 14 | self.assertEqual(scope.variables, {}) 15 | self.assertEqual(scope.children, []) 16 | self.assertEqual(scope.parent, None) 17 | 18 | def test_register_variable(self): 19 | scope = scopes_tree.Scope(type=c.SCOPE_TYPE.NORMAL) 20 | 21 | scope.register_variable('variable_name', 'state', line=7) 22 | 23 | self.assertEqual(scope.variables, {'variable_name': scopes_tree.VariableInfo('state', 7)}) 24 | 25 | def test_register_variable__duplicate_registration(self): 26 | scope = scopes_tree.Scope(type=c.SCOPE_TYPE.NORMAL) 27 | 28 | scope.register_variable('variable_name', 'state.1', 7) 29 | scope.register_variable('variable_name', 'state.2', 8) 30 | 31 | self.assertEqual(scope.variables, {'variable_name': scopes_tree.VariableInfo('state.1', 7)}) 32 | 33 | def test_add_child(self): 34 | scope_1 = scopes_tree.Scope(type=c.SCOPE_TYPE.NORMAL) 35 | scope_2 = scopes_tree.Scope(type=c.SCOPE_TYPE.NORMAL) 36 | scope_1.add_child(scope_2) 37 | 38 | self.assertEqual(scope_1.parent, None) 39 | self.assertEqual(scope_1.children, [scope_2]) 40 | 41 | self.assertEqual(scope_2.parent, scope_1) 42 | self.assertEqual(scope_2.children, []) 43 | 44 | 45 | class TestFindRoot(unittest.TestCase): 46 | 47 | def test_already_root(self): 48 | scope = scopes_tree.Scope(type=c.SCOPE_TYPE.NORMAL) 49 | self.assertEqual(scopes_tree.find_root(scope), scope) 50 | 51 | def test_not_root(self): 52 | scope_root = scopes_tree.Scope(type=c.SCOPE_TYPE.NORMAL) 53 | scope_median = scopes_tree.Scope(type=c.SCOPE_TYPE.NORMAL) 54 | scope_leaf = scopes_tree.Scope(type=c.SCOPE_TYPE.NORMAL) 55 | 56 | scope_root.add_child(scope_median) 57 | scope_median.add_child(scope_leaf) 58 | 59 | self.assertEqual(scopes_tree.find_root(scope_leaf), scope_root) 60 | self.assertEqual(scopes_tree.find_root(scope_median), scope_root) 61 | 62 | 63 | class TestReversedBranch(unittest.TestCase): 64 | 65 | def test_already_root(self): 66 | scope = scopes_tree.Scope(type=c.SCOPE_TYPE.NORMAL) 67 | self.assertEqual(list(scopes_tree.reversed_branch(scope)), [scope]) 68 | 69 | def test_not_root(self): 70 | scope_root = scopes_tree.Scope(type=c.SCOPE_TYPE.NORMAL) 71 | scope_median = scopes_tree.Scope(type=c.SCOPE_TYPE.NORMAL) 72 | scope_leaf = scopes_tree.Scope(type=c.SCOPE_TYPE.NORMAL) 73 | 74 | scope_root.add_child(scope_median) 75 | scope_median.add_child(scope_leaf) 76 | 77 | self.assertEqual(list(scopes_tree.reversed_branch(scope_leaf)), 78 | [scope_leaf, scope_median, scope_root]) 79 | 80 | self.assertEqual(list(scopes_tree.reversed_branch(scope_median)), 81 | [scope_median, scope_root]) 82 | 83 | 84 | class TestGetVariableScopes(unittest.TestCase): 85 | 86 | def test_no_variables(self): 87 | scope = scopes_tree.Scope(type=c.SCOPE_TYPE.NORMAL) 88 | variables = scopes_tree.get_variables_scopes(scope) 89 | self.assertEqual(variables, {}) 90 | 91 | def test_single_node(self): 92 | scope = scopes_tree.Scope(type=c.SCOPE_TYPE.NORMAL) 93 | scope.variables['var_1'] = 'state' 94 | scope.variables['var_2'] = 'state' 95 | 96 | variables = scopes_tree.get_variables_scopes(scope) 97 | 98 | self.assertEqual(variables, {'var_1': [scope], 99 | 'var_2': [scope]}) 100 | 101 | def test_single_branch(self): 102 | scope_root = scopes_tree.Scope(type=c.SCOPE_TYPE.NORMAL) 103 | scope_root.variables['var_1'] = 'state' 104 | scope_root.variables['var_2'] = 'state' 105 | 106 | scope_median = scopes_tree.Scope(type=c.SCOPE_TYPE.NORMAL) 107 | scope_median.variables['var_2'] = 'state' 108 | scope_median.variables['var_3'] = 'state' 109 | 110 | scope_leaf = scopes_tree.Scope(type=c.SCOPE_TYPE.NORMAL) 111 | scope_leaf.variables['var_3'] = 'state' 112 | scope_leaf.variables['var_4'] = 'state' 113 | 114 | scope_root.add_child(scope_median) 115 | scope_median.add_child(scope_leaf) 116 | 117 | variables = scopes_tree.get_variables_scopes(scope_root) 118 | 119 | self.assertEqual(variables.keys(), {'var_1', 'var_2', 'var_3', 'var_4'}) 120 | 121 | self.assertCountEqual(variables['var_1'], [scope_root]) 122 | self.assertCountEqual(variables['var_2'], [scope_root, scope_median]) 123 | self.assertCountEqual(variables['var_3'], [scope_median, scope_leaf]) 124 | self.assertCountEqual(variables['var_4'], [scope_leaf]) 125 | 126 | def test_tree(self): 127 | scope_root = scopes_tree.Scope(type=c.SCOPE_TYPE.NORMAL) 128 | scope_root.variables['var_1'] = 'state' 129 | scope_root.variables['var_2'] = 'state' 130 | 131 | scope_median_1 = scopes_tree.Scope(type=c.SCOPE_TYPE.NORMAL) 132 | scope_median_1.variables['var_2'] = 'state' 133 | scope_median_1.variables['var_3'] = 'state' 134 | 135 | scope_leaf_1 = scopes_tree.Scope(type=c.SCOPE_TYPE.NORMAL) 136 | scope_leaf_1.variables['var_3'] = 'state' 137 | scope_leaf_1.variables['var_4'] = 'state' 138 | 139 | scope_median_2 = scopes_tree.Scope(type=c.SCOPE_TYPE.NORMAL) 140 | scope_median_2.variables['var_4'] = 'state' 141 | scope_median_2.variables['var_5'] = 'state' 142 | 143 | scope_leaf_2 = scopes_tree.Scope(type=c.SCOPE_TYPE.NORMAL) 144 | scope_leaf_2.variables['var_4'] = 'state' 145 | scope_leaf_2.variables['var_6'] = 'state' 146 | 147 | scope_leaf_3 = scopes_tree.Scope(type=c.SCOPE_TYPE.NORMAL) 148 | scope_leaf_3.variables['var_5'] = 'state' 149 | scope_leaf_3.variables['var_6'] = 'state' 150 | 151 | scope_root.add_child(scope_median_1) 152 | scope_root.add_child(scope_median_2) 153 | 154 | scope_median_1.add_child(scope_leaf_1) 155 | 156 | scope_median_2.add_child(scope_leaf_2) 157 | scope_median_2.add_child(scope_leaf_3) 158 | 159 | variables = scopes_tree.get_variables_scopes(scope_root) 160 | 161 | self.assertEqual(variables.keys(), {'var_1', 'var_2', 'var_3', 'var_4', 'var_5', 'var_6'}) 162 | 163 | self.assertCountEqual(variables['var_1'], [scope_root]) 164 | self.assertCountEqual(variables['var_2'], [scope_root, scope_median_1]) 165 | self.assertCountEqual(variables['var_3'], [scope_median_1, scope_leaf_1]) 166 | self.assertCountEqual(variables['var_4'], [scope_leaf_1, scope_median_2, scope_leaf_2]) 167 | self.assertCountEqual(variables['var_5'], [scope_median_2, scope_leaf_3]) 168 | self.assertCountEqual(variables['var_6'], [scope_leaf_2, scope_leaf_3]) 169 | 170 | 171 | class TestIsVariableDefined(unittest.TestCase): 172 | 173 | def test_variable_not_in_scope(self): 174 | scope = scopes_tree.Scope(type=c.SCOPE_TYPE.NORMAL) 175 | self.assertFalse(scopes_tree.is_variable_defined('var_1', scope)) 176 | 177 | def test_variable_initialized(self): 178 | scope = scopes_tree.Scope(type=c.SCOPE_TYPE.NORMAL) 179 | scope.register_variable('var_1', c.VARIABLE_STATE.INITIALIZED, 7) 180 | self.assertTrue(scopes_tree.is_variable_defined('var_1', scope)) 181 | 182 | def test_branch_processing(self): 183 | scope_root = scopes_tree.Scope(type=c.SCOPE_TYPE.NORMAL) 184 | scope_root.register_variable('var_1', c.VARIABLE_STATE.UNINITIALIZED, 7) 185 | scope_root.register_variable('var_2', c.VARIABLE_STATE.INITIALIZED, 8) 186 | 187 | scope_median = scopes_tree.Scope(type=c.SCOPE_TYPE.NORMAL) 188 | scope_median.register_variable('var_2', c.VARIABLE_STATE.UNINITIALIZED, 9) 189 | scope_median.register_variable('var_3', c.VARIABLE_STATE.INITIALIZED, 10) 190 | 191 | scope_leaf = scopes_tree.Scope(type=c.SCOPE_TYPE.NORMAL) 192 | scope_leaf.register_variable('var_1', c.VARIABLE_STATE.UNINITIALIZED, 11) 193 | scope_leaf.register_variable('var_2', c.VARIABLE_STATE.UNINITIALIZED, 12) 194 | scope_leaf.register_variable('var_3', c.VARIABLE_STATE.UNINITIALIZED, 13) 195 | 196 | scope_root.add_child(scope_median) 197 | scope_median.add_child(scope_leaf) 198 | 199 | self.assertFalse(scopes_tree.is_variable_defined('var_1', scope_leaf)) 200 | self.assertTrue(scopes_tree.is_variable_defined('var_2', scope_leaf)) 201 | self.assertTrue(scopes_tree.is_variable_defined('var_3', scope_leaf)) 202 | 203 | def test_ignore_middle_class_scope(self): 204 | scope_root = scopes_tree.Scope(type=c.SCOPE_TYPE.NORMAL) 205 | scope_root.register_variable('var_1', c.VARIABLE_STATE.UNINITIALIZED, 1) 206 | scope_root.register_variable('var_2', c.VARIABLE_STATE.INITIALIZED, 2) 207 | 208 | scope_median = scopes_tree.Scope(type=c.SCOPE_TYPE.CLASS) 209 | scope_median.register_variable('var_2', c.VARIABLE_STATE.UNINITIALIZED, 3) 210 | scope_median.register_variable('var_3', c.VARIABLE_STATE.INITIALIZED, 4) 211 | 212 | scope_leaf = scopes_tree.Scope(type=c.SCOPE_TYPE.NORMAL) 213 | scope_leaf.register_variable('var_1', c.VARIABLE_STATE.UNINITIALIZED, 5) 214 | scope_leaf.register_variable('var_2', c.VARIABLE_STATE.UNINITIALIZED, 6) 215 | scope_leaf.register_variable('var_3', c.VARIABLE_STATE.UNINITIALIZED, 7) 216 | 217 | scope_root.add_child(scope_median) 218 | scope_median.add_child(scope_leaf) 219 | 220 | self.assertFalse(scopes_tree.is_variable_defined('var_1', scope_leaf)) 221 | self.assertTrue(scopes_tree.is_variable_defined('var_2', scope_leaf)) 222 | self.assertFalse(scopes_tree.is_variable_defined('var_3', scope_leaf)) 223 | 224 | def test_not_ignore_original_class_scope(self): 225 | scope_root = scopes_tree.Scope(type=c.SCOPE_TYPE.NORMAL) 226 | scope_root.register_variable('var_1', c.VARIABLE_STATE.UNINITIALIZED, 1) 227 | scope_root.register_variable('var_2', c.VARIABLE_STATE.INITIALIZED, 2) 228 | 229 | scope_median = scopes_tree.Scope(type=c.SCOPE_TYPE.NORMAL) 230 | scope_median.register_variable('var_2', c.VARIABLE_STATE.UNINITIALIZED, 3) 231 | scope_median.register_variable('var_3', c.VARIABLE_STATE.INITIALIZED, 4) 232 | 233 | scope_leaf = scopes_tree.Scope(type=c.SCOPE_TYPE.CLASS) 234 | scope_leaf.register_variable('var_1', c.VARIABLE_STATE.UNINITIALIZED, 5) 235 | scope_leaf.register_variable('var_2', c.VARIABLE_STATE.UNINITIALIZED, 6) 236 | scope_leaf.register_variable('var_3', c.VARIABLE_STATE.UNINITIALIZED, 7) 237 | 238 | scope_root.add_child(scope_median) 239 | scope_median.add_child(scope_leaf) 240 | 241 | self.assertFalse(scopes_tree.is_variable_defined('var_1', scope_leaf)) 242 | self.assertTrue(scopes_tree.is_variable_defined('var_2', scope_leaf)) 243 | self.assertTrue(scopes_tree.is_variable_defined('var_3', scope_leaf)) 244 | 245 | 246 | class FakeUsageChecker: 247 | 248 | def __init__(self, results): 249 | self.results = results 250 | 251 | def __call__(self, variable, scope): 252 | return self.results.pop(0) 253 | 254 | 255 | class TestDetermineVariableUsage(unittest.TestCase): 256 | 257 | def check(self, results, expected_state): 258 | scopes = [scopes_tree.Scope(type=c.SCOPE_TYPE.NORMAL) 259 | for i in range(len(results))] 260 | real_state = scopes_tree.determine_variable_usage(variable='var_1', 261 | scopes=scopes, 262 | usage_checker=FakeUsageChecker(results)) 263 | self.assertEqual(real_state, expected_state) 264 | 265 | def test(self): 266 | self.check([], c.VARIABLE_USAGE_TYPE.FULLY_UNDEFINED) 267 | self.check([True], c.VARIABLE_USAGE_TYPE.FULLY_DEFINED) 268 | self.check([False], c.VARIABLE_USAGE_TYPE.FULLY_UNDEFINED) 269 | self.check([True, True], c.VARIABLE_USAGE_TYPE.FULLY_DEFINED) 270 | self.check([False, True], c.VARIABLE_USAGE_TYPE.PARTIALY_DEFINED) 271 | self.check([False, False], c.VARIABLE_USAGE_TYPE.FULLY_UNDEFINED) 272 | 273 | 274 | class TestSearchUndefinedVariableLines(unittest.TestCase): 275 | 276 | def test(self): 277 | scope_root = scopes_tree.Scope(type=c.SCOPE_TYPE.NORMAL) 278 | scope_root.register_variable('var_1', c.VARIABLE_STATE.UNINITIALIZED, 1) 279 | scope_root.register_variable('var_2', c.VARIABLE_STATE.INITIALIZED, 2) 280 | 281 | scope_median = scopes_tree.Scope(type=c.SCOPE_TYPE.NORMAL) 282 | scope_median.register_variable('var_2', c.VARIABLE_STATE.UNINITIALIZED, 3) 283 | 284 | scope_leaf = scopes_tree.Scope(type=c.SCOPE_TYPE.CLASS) 285 | scope_leaf.register_variable('var_1', c.VARIABLE_STATE.UNINITIALIZED, 4) 286 | scope_leaf.register_variable('var_2', c.VARIABLE_STATE.UNINITIALIZED, 5) 287 | scope_leaf.register_variable('var_3', c.VARIABLE_STATE.UNINITIALIZED, 6) 288 | 289 | scope_root.add_child(scope_median) 290 | scope_median.add_child(scope_leaf) 291 | 292 | self.assertEqual(scopes_tree.search_undefined_variable_lines('var_1', 293 | [scope_root, scope_leaf], 294 | scopes_tree.is_variable_defined), 295 | [1, 4]) 296 | 297 | self.assertEqual(scopes_tree.search_undefined_variable_lines('var_2', 298 | [scope_root, scope_median, scope_leaf], 299 | scopes_tree.is_variable_defined), 300 | []) 301 | 302 | self.assertEqual(scopes_tree.search_undefined_variable_lines('var_3', 303 | [scope_leaf], 304 | scopes_tree.is_variable_defined), 305 | [6]) 306 | 307 | 308 | class TestSearchCandidatesToImport(unittest.TestCase): 309 | 310 | def test(self): 311 | scope_root = scopes_tree.Scope(type=c.SCOPE_TYPE.NORMAL) 312 | scope_root.register_variable('var_1', c.VARIABLE_STATE.UNINITIALIZED, 1) 313 | scope_root.register_variable('var_2', c.VARIABLE_STATE.INITIALIZED, 2) 314 | 315 | scope_median_1 = scopes_tree.Scope(type=c.SCOPE_TYPE.NORMAL) 316 | scope_median_1.register_variable('var_2', c.VARIABLE_STATE.UNINITIALIZED, 3) 317 | scope_median_1.register_variable('var_3', c.VARIABLE_STATE.INITIALIZED, 4) 318 | 319 | scope_leaf = scopes_tree.Scope(type=c.SCOPE_TYPE.CLASS) 320 | scope_leaf.register_variable('var_1', c.VARIABLE_STATE.UNINITIALIZED, 5) 321 | scope_leaf.register_variable('var_2', c.VARIABLE_STATE.UNINITIALIZED, 6) 322 | scope_leaf.register_variable('var_3', c.VARIABLE_STATE.UNINITIALIZED, 7) 323 | 324 | scope_median_2 = scopes_tree.Scope(type=c.SCOPE_TYPE.NORMAL) 325 | scope_median_2.register_variable('var_2', c.VARIABLE_STATE.UNINITIALIZED, 8) 326 | scope_median_2.register_variable('var_3', c.VARIABLE_STATE.UNINITIALIZED, 9) 327 | 328 | scope_root.add_child(scope_median_1) 329 | scope_root.add_child(scope_median_2) 330 | scope_median_1.add_child(scope_leaf) 331 | 332 | variables = scopes_tree.search_candidates_to_import(scope_root) 333 | 334 | fully_undefined_variables, partialy_undefined_variables, variables_scopes = variables 335 | 336 | self.assertEqual(fully_undefined_variables, {'var_1'}) 337 | self.assertEqual(partialy_undefined_variables, {'var_3'}) 338 | self.assertEqual(variables_scopes, {'var_1': [scope_root, scope_leaf], 339 | 'var_2': [scope_root, scope_median_1, scope_median_2, scope_leaf], 340 | 'var_3': [scope_median_1, scope_median_2, scope_leaf]}) 341 | -------------------------------------------------------------------------------- /smart_imports/tests/test_rules.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | import sys 4 | import uuid 5 | import unittest 6 | import importlib 7 | 8 | from unittest import mock 9 | 10 | from .. import rules 11 | from .. import config 12 | from .. import helpers 13 | from .. import exceptions 14 | 15 | 16 | class TestCustomRule(unittest.TestCase): 17 | 18 | def setUp(self): 19 | super().setUp() 20 | self.config = {'variables': {'y': {'module': 'z'}, 21 | 'p': {'module': 'q', 'attribute': 'w'}}} 22 | self.rule = rules.CustomRule(config=self.config) 23 | 24 | def test_no_variables(self): 25 | command = rules.CustomRule(config={'variables': {}}).apply(module='module', 26 | variable='x') 27 | self.assertEqual(command, None) 28 | 29 | def test_no_variable(self): 30 | command = self.rule.apply(module='module', 31 | variable='x') 32 | self.assertEqual(command, None) 33 | 34 | def test_only_module(self): 35 | command = self.rule.apply(module='module', 36 | variable='y') 37 | self.assertEqual(command, rules.ImportCommand(target_module='module', 38 | target_attribute='y', 39 | source_module='z', 40 | source_attribute=None)) 41 | 42 | def test_module_attribute(self): 43 | command = self.rule.apply(module='module', 44 | variable='p') 45 | self.assertEqual(command, rules.ImportCommand(target_module='module', 46 | target_attribute='p', 47 | source_module='q', 48 | source_attribute='w')) 49 | 50 | 51 | class TestLocalModulesRule(unittest.TestCase): 52 | 53 | def setUp(self): 54 | super().setUp() 55 | self.rule = rules.LocalModulesRule(config={}) 56 | 57 | def prepair_modules(self, base_directory): 58 | os.makedirs(os.path.join(base_directory, 'a', 'b', 'c')) 59 | 60 | with open(os.path.join(base_directory, 'a', '__init__.py'), 'w') as f: 61 | f.write(' ') 62 | 63 | with open(os.path.join(base_directory, 'a', 'x.py'), 'w') as f: 64 | f.write(' ') 65 | 66 | with open(os.path.join(base_directory, 'a', 'b', '__init__.py'), 'w') as f: 67 | f.write(' ') 68 | 69 | with open(os.path.join(base_directory, 'a', 'b', 'y.py'), 'w') as f: 70 | f.write(' ') 71 | 72 | def test_wrong_package(self): 73 | module = type(os)('some_module') 74 | 75 | self.assertEqual(module.__package__, None) 76 | 77 | command = self.rule.apply(module=module, 78 | variable='y') 79 | 80 | self.assertEqual(command, None) 81 | 82 | command = self.rule.apply(module=mock.Mock(), 83 | variable='y') 84 | 85 | self.assertEqual(command, None) 86 | 87 | 88 | def test_module_found(self): 89 | with helpers.test_directory() as temp_directory: 90 | self.prepair_modules(temp_directory) 91 | 92 | module = importlib.import_module('a.b') 93 | 94 | command = self.rule.apply(module=module, 95 | variable='y') 96 | 97 | self.assertEqual(command, rules.ImportCommand(target_module=module, 98 | target_attribute='y', 99 | source_module='a.b.y', 100 | source_attribute=None)) 101 | 102 | def test_package_path_not_found(self): 103 | with helpers.test_directory() as temp_directory: 104 | self.prepair_modules(temp_directory) 105 | 106 | module = importlib.import_module('a.b') 107 | 108 | command = self.rule.apply(module=module, 109 | variable='x') 110 | 111 | self.assertEqual(command, None) 112 | 113 | 114 | class TestGlobalModulesRule(unittest.TestCase): 115 | 116 | def setUp(self): 117 | super().setUp() 118 | self.rule = rules.GlobalModulesRule(config={}) 119 | 120 | def test_no_global_module(self): 121 | module_name = 'global_module_{}'.format(uuid.uuid4().hex) 122 | 123 | with helpers.test_directory() as temp_directory: 124 | with open(os.path.join(temp_directory, '{}.py'.format(module_name)), 'w') as f: 125 | f.write(' ') 126 | 127 | module = type(os)('some_module') 128 | 129 | command = self.rule.apply(module=module, 130 | variable='y') 131 | 132 | self.assertEqual(command, None) 133 | 134 | def test_has_global_module(self): 135 | module_name = 'global_module_{}'.format(uuid.uuid4().hex) 136 | 137 | with helpers.test_directory() as temp_directory: 138 | with open(os.path.join(temp_directory, '{}.py'.format(module_name)), 'w') as f: 139 | f.write(' ') 140 | 141 | module = type(os)('some_module') 142 | 143 | command = self.rule.apply(module=module, 144 | variable=module_name) 145 | 146 | self.assertEqual(command, rules.ImportCommand(target_module=module, 147 | target_attribute=module_name, 148 | source_module=module_name, 149 | source_attribute=None)) 150 | 151 | 152 | class TestStdLibRule(unittest.TestCase): 153 | 154 | def setUp(self): 155 | super().setUp() 156 | self.rule = rules.StdLibRule(config={}) 157 | 158 | def test_system_modules(self): 159 | self.assertEqual(self.rule._STDLIB_MODULES['os'], {'module': 'os'}) 160 | self.assertEqual(self.rule._STDLIB_MODULES['os_path'], {'module': 'os.path'}) 161 | 162 | def test_builting_modules(self): 163 | self.assertTrue(set(sys.builtin_module_names).issubset(set(self.rule._STDLIB_MODULES.keys()))) 164 | 165 | def test_not_system_module(self): 166 | command = self.rule.apply('module', 'bla_bla') 167 | self.assertEqual(command, None) 168 | 169 | def test_system_module(self): 170 | command = self.rule.apply('module', 'os_path') 171 | self.assertEqual(command, rules.ImportCommand(target_module='module', 172 | target_attribute='os_path', 173 | source_module='os.path', 174 | source_attribute=None)) 175 | 176 | def test_builtin_module(self): 177 | command = self.rule.apply('module', 'math') 178 | self.assertEqual(command, rules.ImportCommand(target_module='module', 179 | target_attribute='math', 180 | source_module='math', 181 | source_attribute=None)) 182 | 183 | 184 | class TestPredifinedNamesRule(unittest.TestCase): 185 | 186 | def setUp(self): 187 | super().setUp() 188 | self.rule = rules.PredefinedNamesRule(config={}) 189 | 190 | def test_common_name(self): 191 | command = self.rule.apply('module', 'bla_bla') 192 | self.assertEqual(command, None) 193 | 194 | def test_predefined_names(self): 195 | for name in {'__name__', '__file__', '__doc__', '__annotations__'}: 196 | command = self.rule.apply('module', name) 197 | self.assertEqual(command, rules.NoImportCommand()) 198 | 199 | 200 | class TestPrefixRule(unittest.TestCase): 201 | 202 | def setUp(self): 203 | self.config = {'prefixes': [{"prefix": "other_", "module": "xxx.yyy"}, 204 | {"prefix": "some_xxx_", "module": "aaa.bbb.qqq"}, 205 | {"prefix": "some_", "module": "aaa.bbb"}]} 206 | self.rule = rules.PrefixRule(config=self.config) 207 | 208 | def test_wrong_prefix(self): 209 | command = self.rule.apply(module='module', 210 | variable='pqr_variable') 211 | self.assertEqual(command, None) 212 | 213 | def test_prefix_found(self): 214 | command = self.rule.apply(module='module', 215 | variable='some_variable') 216 | 217 | self.assertEqual(command, rules.ImportCommand(target_module='module', 218 | target_attribute='some_variable', 219 | source_module='aaa.bbb.variable', 220 | source_attribute=None)) 221 | 222 | def test_prefix_order(self): 223 | command = self.rule.apply(module='module', 224 | variable='some_xxx_variable') 225 | 226 | self.assertEqual(command, rules.ImportCommand(target_module='module', 227 | target_attribute='some_xxx_variable', 228 | source_module='aaa.bbb.qqq.variable', 229 | source_attribute=None)) 230 | 231 | 232 | class TestLocalModulesFromParentRule(unittest.TestCase): 233 | 234 | def setUp(self): 235 | self.config = {"suffixes": [".c", 236 | ".b.c"]} 237 | self.rule = rules.LocalModulesFromParentRule(config=self.config) 238 | 239 | def prepair_modules(self, base_directory): 240 | os.makedirs(os.path.join(base_directory, 'a', 'b', 'c')) 241 | 242 | with open(os.path.join(base_directory, 'a', '__init__.py'), 'w') as f: 243 | f.write(' ') 244 | 245 | with open(os.path.join(base_directory, 'a', 'x.py'), 'w') as f: 246 | f.write(' ') 247 | 248 | with open(os.path.join(base_directory, 'a', 'b', '__init__.py'), 'w') as f: 249 | f.write(' ') 250 | 251 | with open(os.path.join(base_directory, 'a', 'b', 'y.py'), 'w') as f: 252 | f.write(' ') 253 | 254 | with open(os.path.join(base_directory, 'a', 'b', 'c', '__init__.py'), 'w') as f: 255 | f.write(' ') 256 | 257 | with open(os.path.join(base_directory, 'a', 'b', 'c', 'z.py'), 'w') as f: 258 | f.write(' ') 259 | 260 | def test_no_parents_found(self): 261 | with helpers.test_directory() as temp_directory: 262 | self.prepair_modules(temp_directory) 263 | 264 | module = importlib.import_module('a.b.y') 265 | 266 | command = self.rule.apply(module=module, 267 | variable='xxx') 268 | 269 | self.assertEqual(command, None) 270 | 271 | def test_parents_found(self): 272 | with helpers.test_directory() as temp_directory: 273 | self.prepair_modules(temp_directory) 274 | 275 | module = importlib.import_module('a.b.c.z') 276 | 277 | command = self.rule.apply(module=module, 278 | variable='y') 279 | 280 | self.assertEqual(command, rules.ImportCommand(target_module=module, 281 | target_attribute='y', 282 | source_module='a.b.y', 283 | source_attribute=None)) 284 | 285 | def test_parents_found__complex(self): 286 | with helpers.test_directory() as temp_directory: 287 | self.prepair_modules(temp_directory) 288 | 289 | module = importlib.import_module('a.b.c.z') 290 | 291 | command = self.rule.apply(module=module, 292 | variable='x') 293 | 294 | self.assertEqual(command, rules.ImportCommand(target_module=module, 295 | target_attribute='x', 296 | source_module='a.x', 297 | source_attribute=None)) 298 | 299 | 300 | class TestLocalModulesFromNamespaceRule(unittest.TestCase): 301 | 302 | def setUp(self): 303 | self.config = {'map': {'a.b': ['a.c'], 304 | 'a.c': ['a.b', 'a']}} 305 | self.rule = rules.LocalModulesFromNamespaceRule(config=self.config) 306 | 307 | def prepair_modules(self, base_directory): 308 | os.makedirs(os.path.join(base_directory, 'a', 'b')) 309 | os.makedirs(os.path.join(base_directory, 'a', 'c')) 310 | 311 | with open(os.path.join(base_directory, 'a', '__init__.py'), 'w') as f: 312 | f.write(' ') 313 | 314 | with open(os.path.join(base_directory, 'a', 'x.py'), 'w') as f: 315 | f.write(' ') 316 | 317 | with open(os.path.join(base_directory, 'a', 'b', '__init__.py'), 'w') as f: 318 | f.write(' ') 319 | 320 | with open(os.path.join(base_directory, 'a', 'b', 'y.py'), 'w') as f: 321 | f.write(' ') 322 | 323 | with open(os.path.join(base_directory, 'a', 'c', '__init__.py'), 'w') as f: 324 | f.write(' ') 325 | 326 | with open(os.path.join(base_directory, 'a', 'c', 'z.py'), 'w') as f: 327 | f.write(' ') 328 | 329 | def test_no_module_found(self): 330 | with helpers.test_directory() as temp_directory: 331 | self.prepair_modules(temp_directory) 332 | 333 | module = importlib.import_module('a.x') 334 | 335 | command = self.rule.apply(module=module, 336 | variable='z') 337 | 338 | self.assertEqual(command, None) 339 | 340 | def test_no_relations_found(self): 341 | with helpers.test_directory() as temp_directory: 342 | self.prepair_modules(temp_directory) 343 | 344 | module = importlib.import_module('a.b.y') 345 | 346 | command = self.rule.apply(module=module, 347 | variable='q') 348 | 349 | self.assertEqual(command, None) 350 | 351 | def test_relation_found(self): 352 | with helpers.test_directory() as temp_directory: 353 | self.prepair_modules(temp_directory) 354 | 355 | module = importlib.import_module('a.b.y') 356 | 357 | command = self.rule.apply(module=module, 358 | variable='z') 359 | 360 | self.assertEqual(command, rules.ImportCommand(target_module=module, 361 | target_attribute='z', 362 | source_module='a.c.z', 363 | source_attribute=None)) 364 | 365 | def test_relation_found__second_relation(self): 366 | with helpers.test_directory() as temp_directory: 367 | self.prepair_modules(temp_directory) 368 | 369 | module = importlib.import_module('a.c.z') 370 | 371 | command = self.rule.apply(module=module, 372 | variable='x') 373 | 374 | self.assertEqual(command, rules.ImportCommand(target_module=module, 375 | target_attribute='x', 376 | source_module='a.x', 377 | source_attribute=None)) 378 | 379 | 380 | class TestDefaultRules(unittest.TestCase): 381 | 382 | def test(self): 383 | self.assertCountEqual(rules._FABRICS.keys(), {'rule_local_modules_from_parent', 384 | 'rule_predefined_names', 385 | 'rule_stdlib', 386 | 'rule_prefix', 387 | 'rule_local_modules_from_namespace', 388 | 'rule_local_modules', 389 | 'rule_global_modules', 390 | 'rule_custom'}) 391 | 392 | 393 | class TestRegister(unittest.TestCase): 394 | 395 | def setUp(self): 396 | super().setUp() 397 | rules.remove('xxx') 398 | 399 | def tearDown(self): 400 | super().tearDown() 401 | rules.remove('xxx') 402 | 403 | def test_success(self): 404 | rules.register('xxx', 'my.rule') 405 | self.assertEqual(rules._FABRICS['xxx'], 'my.rule') 406 | 407 | def test_already_registered(self): 408 | rules.register('xxx', 'my.rule') 409 | 410 | with self.assertRaises(exceptions.RuleAlreadyRegistered): 411 | rules.register('xxx', 'my.rule') 412 | 413 | 414 | class TestGetForConfig(unittest.TestCase): 415 | 416 | def setUp(self): 417 | super().setUp() 418 | rules.remove('xxx') 419 | rules.reset_rules_cache() 420 | 421 | def tearDown(self): 422 | super().tearDown() 423 | rules.remove('xxx') 424 | rules.reset_rules_cache() 425 | 426 | def test_no_rule(self): 427 | with self.assertRaises(exceptions.RuleNotRegistered): 428 | test_config = config.DEFAULT_CONFIG.clone(rules=[{'type': 'xxx'}]) 429 | rules.get_for_config(test_config) 430 | 431 | def test_success(self): 432 | test_config = config.DEFAULT_CONFIG.clone(rules=[{"type": "rule_local_modules"}, 433 | {"type": "rule_stdlib"}]) 434 | 435 | found_rules_1 = rules.get_for_config(test_config) 436 | found_rules_2 = rules.get_for_config(test_config) 437 | 438 | for rule_1, rule_2 in zip(found_rules_1, found_rules_2): 439 | self.assertIs(rule_1, rule_2) 440 | -------------------------------------------------------------------------------- /smart_imports/tests/test_ast_parser.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | import sys 4 | import ast 5 | import unittest 6 | 7 | from .. import ast_parser 8 | from .. import scopes_tree 9 | 10 | 11 | TEST_FIXTURES_DIR = os.path.join(os.path.dirname(__file__), 'fixtures') 12 | 13 | 14 | class TestAnalyzer(unittest.TestCase): 15 | 16 | @unittest.skipUnless(sys.version.startswith('3.5'), 'test only for python 3.5') 17 | def test_python_3_5(self): 18 | 19 | with open(os.path.join(TEST_FIXTURES_DIR, 'python_3_5', 'full_parser_test.py')) as f: 20 | code = f.read() 21 | 22 | tree = ast.parse(code) 23 | 24 | analyzer = ast_parser.Analyzer() 25 | 26 | analyzer.visit(tree) 27 | 28 | variables = scopes_tree.search_candidates_to_import(analyzer.scope) 29 | 30 | fully_undefined_variables, partialy_undefined_variables, variables_scopes = variables 31 | 32 | self.assertEqual(fully_undefined_variables, 33 | {'перменная_2', 34 | 'annotation_1', 35 | 'annotation_2', 36 | 'var_3', 37 | 'var_7', 38 | 'var_8', 39 | 'var_11', 40 | 'var_12', 41 | 'var_15', 42 | 'var_22', 43 | 'var_24', 44 | 'var_27', 45 | 'var_38', 46 | 'var_39', 47 | 'var_42', 48 | 'var_44', 49 | 'var_47', 50 | 'var_48', 51 | 'var_50', 52 | 'var_51', 53 | 'var_53', 54 | 'var_55', 55 | 'var_57', 56 | 'super', 57 | 'abs', 58 | 'range', 59 | 'zip', 60 | 'print'}) 61 | 62 | self.assertEqual(partialy_undefined_variables, 63 | {'var_9', 64 | 'var_13', 65 | 'var_46'}) 66 | 67 | self.assertEqual(variables_scopes['zip'][0].variables['zip'].line, 32) 68 | self.assertEqual(variables_scopes['var_33'][0].variables['var_33'].line, 50) 69 | 70 | self.assertEqual({scope.variables['var_46'].line for scope in variables_scopes['var_46']}, 71 | {66, 68}) # skip line 66 since it is the same scope with line 67 and loop assigment has priority 72 | 73 | self.assertEqual({scope.variables['var_56'].line for scope in variables_scopes['var_56']}, 74 | {79, 82}) 75 | 76 | @unittest.skipUnless(sys.version.startswith('3.6'), 'test only for python 3.6') 77 | def test_python_3_6(self): 78 | 79 | with open(os.path.join(TEST_FIXTURES_DIR, 'python_3_6', 'full_parser_test.py')) as f: 80 | code = f.read() 81 | 82 | tree = ast.parse(code) 83 | 84 | analyzer = ast_parser.Analyzer() 85 | 86 | analyzer.visit(tree) 87 | 88 | variables = scopes_tree.search_candidates_to_import(analyzer.scope) 89 | 90 | fully_undefined_variables, partialy_undefined_variables, variables_scopes = variables 91 | 92 | self.assertEqual(fully_undefined_variables, 93 | {'перменная_2', 94 | 'annotation_1', 95 | 'annotation_2', 96 | 'var_3', 97 | 'var_7', 98 | 'var_8', 99 | 'var_11', 100 | 'var_12', 101 | 'var_15', 102 | 'var_22', 103 | 'var_24', 104 | 'var_27', 105 | 'var_38', 106 | 'var_39', 107 | 'var_42', 108 | 'var_44', 109 | 'var_47', 110 | 'var_48', 111 | 'var_50', 112 | 'var_51', 113 | 'var_53', 114 | 'var_55', 115 | 'var_57', 116 | 'var_59', 117 | 'var_61', 118 | 'var_63', 119 | 'var_65', 120 | 'var_67', 121 | 'var_70', 122 | 'var_72', 123 | 'var_75', 124 | 'var_76', 125 | 'var_79', 126 | 'var_80', 127 | 'var_81', 128 | 'super', 129 | 'abs', 130 | 'range', 131 | 'zip', 132 | 'print'}) 133 | 134 | self.assertEqual(partialy_undefined_variables, 135 | {'var_9', 136 | 'var_13', 137 | 'var_46'}) 138 | 139 | self.assertEqual(variables_scopes['zip'][0].variables['zip'].line, 32) 140 | self.assertEqual(variables_scopes['var_33'][0].variables['var_33'].line, 50) 141 | 142 | self.assertEqual({scope.variables['var_46'].line for scope in variables_scopes['var_46']}, 143 | {66, 68}) # skip line 66 since it is the same scope with line 67 and loop assigment has priority 144 | 145 | self.assertEqual({scope.variables['var_56'].line for scope in variables_scopes['var_56']}, 146 | {79, 82}) 147 | 148 | @unittest.skipUnless(sys.version.startswith('3.7'), 'test only for python 3.7') 149 | def test_python_3_7(self): 150 | 151 | with open(os.path.join(TEST_FIXTURES_DIR, 'python_3_7', 'full_parser_test.py')) as f: 152 | code = f.read() 153 | 154 | tree = ast.parse(code) 155 | 156 | analyzer = ast_parser.Analyzer() 157 | 158 | analyzer.visit(tree) 159 | 160 | variables = scopes_tree.search_candidates_to_import(analyzer.scope) 161 | 162 | fully_undefined_variables, partialy_undefined_variables, variables_scopes = variables 163 | 164 | self.assertEqual(fully_undefined_variables, 165 | {'перменная_2', 166 | 'annotation_1', 167 | 'annotation_2', 168 | 'var_3', 169 | 'var_7', 170 | 'var_8', 171 | 'var_11', 172 | 'var_12', 173 | 'var_15', 174 | 'var_22', 175 | 'var_24', 176 | 'var_27', 177 | 'var_38', 178 | 'var_39', 179 | 'var_42', 180 | 'var_44', 181 | 'var_47', 182 | 'var_48', 183 | 'var_50', 184 | 'var_51', 185 | 'var_53', 186 | 'var_55', 187 | 'var_57', 188 | 'var_59', 189 | 'var_61', 190 | 'var_63', 191 | 'var_65', 192 | 'var_67', 193 | 'var_70', 194 | 'var_72', 195 | 'var_75', 196 | 'var_76', 197 | 'var_79', 198 | 'var_80', 199 | 'var_81', 200 | 'super', 201 | 'abs', 202 | 'range', 203 | 'zip', 204 | 'print'}) 205 | 206 | self.assertEqual(partialy_undefined_variables, 207 | {'var_9', 208 | 'var_13', 209 | 'var_46'}) 210 | 211 | self.assertEqual(variables_scopes['zip'][0].variables['zip'].line, 32) 212 | self.assertEqual(variables_scopes['var_33'][0].variables['var_33'].line, 50) 213 | 214 | self.assertEqual({scope.variables['var_46'].line for scope in variables_scopes['var_46']}, 215 | {66, 68}) # skip line 66 since it is the same scope with line 67 and loop assigment has priority 216 | 217 | self.assertEqual({scope.variables['var_56'].line for scope in variables_scopes['var_56']}, 218 | {79, 82}) 219 | 220 | @unittest.skipUnless(sys.version.startswith('3.8'), 'test only for python 3.8') 221 | def test_python_3_8(self): 222 | 223 | with open(os.path.join(TEST_FIXTURES_DIR, 'python_3_8', 'full_parser_test.py')) as f: 224 | code = f.read() 225 | 226 | tree = ast.parse(code) 227 | 228 | analyzer = ast_parser.Analyzer() 229 | 230 | analyzer.visit(tree) 231 | 232 | variables = scopes_tree.search_candidates_to_import(analyzer.scope) 233 | 234 | fully_undefined_variables, partialy_undefined_variables, variables_scopes = variables 235 | 236 | self.assertEqual(fully_undefined_variables, 237 | {'перменная_2', 238 | 'annotation_1', 239 | 'annotation_2', 240 | 'var_3', 241 | 'var_7', 242 | 'var_8', 243 | 'var_11', 244 | 'var_12', 245 | 'var_15', 246 | 'var_22', 247 | 'var_24', 248 | 'var_27', 249 | 'var_38', 250 | 'var_39', 251 | 'var_42', 252 | 'var_44', 253 | 'var_47', 254 | 'var_48', 255 | 'var_50', 256 | 'var_51', 257 | 'var_53', 258 | 'var_55', 259 | 'var_57', 260 | 'var_59', 261 | 'var_61', 262 | 'var_63', 263 | 'var_65', 264 | 'var_67', 265 | 'var_70', 266 | 'var_72', 267 | 'var_75', 268 | 'var_76', 269 | 'var_79', 270 | 'var_80', 271 | 'var_81', 272 | 'var_83', 273 | 'var_84', 274 | 'var_90', 275 | 'super', 276 | 'abs', 277 | 'range', 278 | 'zip', 279 | 'print'}) 280 | 281 | self.assertEqual(partialy_undefined_variables, 282 | {'var_9', 283 | 'var_13', 284 | 'var_46'}) 285 | 286 | self.assertEqual(variables_scopes['zip'][0].variables['zip'].line, 32) 287 | self.assertEqual(variables_scopes['var_33'][0].variables['var_33'].line, 50) 288 | 289 | self.assertEqual({scope.variables['var_46'].line for scope in variables_scopes['var_46']}, 290 | {66, 68}) # skip line 66 since it is the same scope with line 67 and loop assigment has priority 291 | 292 | self.assertEqual({scope.variables['var_56'].line for scope in variables_scopes['var_56']}, 293 | {79, 82}) 294 | 295 | @unittest.skipUnless(sys.version.startswith('3.9'), 'test only for python 3.9') 296 | def test_python_3_9(self): 297 | 298 | with open(os.path.join(TEST_FIXTURES_DIR, 'python_3_9', 'full_parser_test.py')) as f: 299 | code = f.read() 300 | 301 | tree = ast.parse(code) 302 | 303 | analyzer = ast_parser.Analyzer() 304 | 305 | analyzer.visit(tree) 306 | 307 | variables = scopes_tree.search_candidates_to_import(analyzer.scope) 308 | 309 | fully_undefined_variables, partialy_undefined_variables, variables_scopes = variables 310 | 311 | self.assertEqual(fully_undefined_variables, 312 | {'перменная_2', 313 | 'annotation_1', 314 | 'annotation_2', 315 | 'var_3', 316 | 'var_7', 317 | 'var_8', 318 | 'var_11', 319 | 'var_12', 320 | 'var_15', 321 | 'var_22', 322 | 'var_24', 323 | 'var_27', 324 | 'var_38', 325 | 'var_39', 326 | 'var_42', 327 | 'var_44', 328 | 'var_47', 329 | 'var_48', 330 | 'var_50', 331 | 'var_51', 332 | 'var_53', 333 | 'var_55', 334 | 'var_57', 335 | 'var_59', 336 | 'var_61', 337 | 'var_63', 338 | 'var_65', 339 | 'var_67', 340 | 'var_70', 341 | 'var_72', 342 | 'var_75', 343 | 'var_76', 344 | 'var_79', 345 | 'var_80', 346 | 'var_81', 347 | 'var_83', 348 | 'var_84', 349 | 'var_90', 350 | 'var_91', 351 | 'var_93', 352 | 'super', 353 | 'abs', 354 | 'range', 355 | 'zip', 356 | 'print'}) 357 | 358 | self.assertEqual(partialy_undefined_variables, 359 | {'var_9', 360 | 'var_13', 361 | 'var_46'}) 362 | 363 | self.assertEqual(variables_scopes['zip'][0].variables['zip'].line, 32) 364 | self.assertEqual(variables_scopes['var_33'][0].variables['var_33'].line, 50) 365 | 366 | self.assertEqual({scope.variables['var_46'].line for scope in variables_scopes['var_46']}, 367 | {66, 68}) # skip line 66 since it is the same scope with line 67 and loop assigment has priority 368 | 369 | self.assertEqual({scope.variables['var_56'].line for scope in variables_scopes['var_56']}, 370 | {79, 82}) 371 | 372 | @unittest.skipUnless(sys.version.startswith('3.10'), 'test only for python 3.10') 373 | def test_python_3_10(self): 374 | 375 | with open(os.path.join(TEST_FIXTURES_DIR, 'python_3_10', 'full_parser_test.py')) as f: 376 | code = f.read() 377 | 378 | tree = ast.parse(code) 379 | 380 | analyzer = ast_parser.Analyzer() 381 | 382 | analyzer.visit(tree) 383 | 384 | variables = scopes_tree.search_candidates_to_import(analyzer.scope) 385 | 386 | fully_undefined_variables, partialy_undefined_variables, variables_scopes = variables 387 | 388 | self.assertEqual(fully_undefined_variables, 389 | {'перменная_2', 390 | 'annotation_1', 391 | 'annotation_2', 392 | 'var_3', 393 | 'var_7', 394 | 'var_8', 395 | 'var_11', 396 | 'var_12', 397 | 'var_15', 398 | 'var_22', 399 | 'var_24', 400 | 'var_27', 401 | 'var_38', 402 | 'var_39', 403 | 'var_42', 404 | 'var_44', 405 | 'var_47', 406 | 'var_48', 407 | 'var_50', 408 | 'var_51', 409 | 'var_53', 410 | 'var_55', 411 | 'var_57', 412 | 'var_59', 413 | 'var_61', 414 | 'var_63', 415 | 'var_65', 416 | 'var_67', 417 | 'var_70', 418 | 'var_72', 419 | 'var_75', 420 | 'var_76', 421 | 'var_79', 422 | 'var_80', 423 | 'var_81', 424 | 'var_83', 425 | 'var_84', 426 | 'var_90', 427 | 'var_91', 428 | 'var_93', 429 | 'var_96', 430 | 'var_97', 431 | 'var_99', 432 | 'var_101', 433 | 'var_107', 434 | 'var_108', 435 | 'var_111', 436 | 'var_114', 437 | 'var_119', 438 | 'var_121', 439 | 'var_123', 440 | 'var_125', 441 | 'super', 442 | 'abs', 443 | 'range', 444 | 'zip', 445 | 'print'}) 446 | 447 | self.assertEqual(partialy_undefined_variables, 448 | {'var_9', 449 | 'var_13', 450 | 'var_46'}) 451 | 452 | self.assertEqual(variables_scopes['zip'][0].variables['zip'].line, 32) 453 | self.assertEqual(variables_scopes['var_33'][0].variables['var_33'].line, 50) 454 | 455 | self.assertEqual({scope.variables['var_46'].line for scope in variables_scopes['var_46']}, 456 | {66, 68}) # skip line 66 since it is the same scope with line 67 and loop assigment has priority 457 | 458 | self.assertEqual({scope.variables['var_56'].line for scope in variables_scopes['var_56']}, 459 | {79, 82}) 460 | --------------------------------------------------------------------------------