Skip to content

Commit

Permalink
Finish 0.2.0
Browse files Browse the repository at this point in the history
  • Loading branch information
Tiendil committed Jan 19, 2019
2 parents a81d0a2 + cc52bba commit cd728d2
Show file tree
Hide file tree
Showing 30 changed files with 1,795 additions and 438 deletions.
27 changes: 27 additions & 0 deletions .travis.yml
@@ -0,0 +1,27 @@
# <https://travis-ci.org/Tiendil/smart-imports>

language: python

#########################
# required by python 3.7
sudo: required
dist: xenial
#########################

os:
- linux

python:
- 3.5
- 3.6
- 3.7

cache: pip

install:
- pip install .

script: python -m unittest discover -t . smart_imports

after_success:
- pip install coveralls && coverage run --source=smart_imports setup.py test && coveralls
23 changes: 23 additions & 0 deletions CHANGELOG.rst
@@ -0,0 +1,23 @@

##########
Change log
##########

This document records all notable changes to `smart_imports`.

-----------
0.2.0 (dev)
-----------

* Add support for Python 3.6 `gh-1 <https://github.com/Tiendil/smart-imports/issues/1>`_
* Add support for Python 3.7 `gh-1 <https://github.com/Tiendil/smart-imports/issues/1>`_
* Implement obtaining modules' source code by standard Python functionality `gh-a142548 <https://github.com/Tiendil/smart-imports/commit/a142548de8dac3c0bedae18dc71d7ad01b2674c2>`_
* Add string numbers to import error messages `gh-6 <https://github.com/Tiendil/smart-imports/issues/6>`_
* Refactoring functional rules into class based rules `gh-7 <https://github.com/Tiendil/smart-imports/issues/7>`_
* Add optional caching `gh-10 <https://github.com/Tiendil/smart-imports/issues/10>`_

-----
0.1.0
-----

* Initial implementation
2 changes: 0 additions & 2 deletions README.md

This file was deleted.

55 changes: 55 additions & 0 deletions README.rst
@@ -0,0 +1,55 @@

#######################
Smart import for Python
#######################

|pypi| |python_versions| |test_coverege_develop|

Automatically discovers & imports entities, used in current module.

No magic or monkey patching. Only standard Python functionality.

+---------------------------------------------+---------------------------------------------+
| Before | After |
+=============================================+=============================================+
|.. code:: python |.. code:: python |
| | |
| import math | import smart_imports |
| from my_project import calc | smart_imports.all() |
| # 100500 other imports | # no any other imports |
| | |
| def my_code(argument, function=calc): | def my_code(argument, function=calc): |
| return math.log(function(argument)) | return math.log(function(argument)) |
| | |
+---------------------------------------------+---------------------------------------------+

-------------
Short summary
-------------

* Get source code of module, from which ``smart_imports`` has called;
* Parse it, find all not initialized variables;
* Search imports, suitable for found variables;
* Import them.

Process only modules, from which ``smart_imports`` called explicitly.

--------
See also
--------

- `Gitter chat room <https://gitter.im/smart-imports/discussion>`_
- `Change log <https://github.com/Tiendil/smart-imports/blob/develop/CHANGELOG.rst>`_


.. |pypi| image:: https://img.shields.io/pypi/v/smart_imports.svg?style=flat-square&label=latest%20stable%20version&reset_github_caches=3
:target: https://pypi.python.org/pypi/smart_imports
:alt: Latest version released on PyPi

.. |python_versions| image:: https://img.shields.io/pypi/pyversions/smart_imports.svg?style=flat-square&reset_github_caches=3
:target: https://pypi.python.org/pypi/smart_imports
:alt: Supported Python versions

.. |test_coverege_develop| image:: https://coveralls.io/repos/github/Tiendil/smart-imports/badge.svg?branch=develop&reset_github_caches=3
:target: https://coveralls.io/github/Tiendil/smart-imports?branch=develop
:alt: Test coverage in develop
10 changes: 6 additions & 4 deletions setup.py
Expand Up @@ -3,7 +3,7 @@

setuptools.setup(
name='smart_imports',
version='0.1.1',
version='0.2.0',
description='automatic importing for Python modules',
url='https://github.com/Tiendil/smart-imports',
author='Aleksey Yeletsky <Tiendil>',
Expand All @@ -16,14 +16,16 @@

'License :: OSI Approved :: BSD License',

'Programming language :: Python :: 3 :: only',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',

'Natural Language :: English',
'Natural Language :: Russian'],
keywords=['python', 'impot'],
keywords=['python', 'import'],
packages=setuptools.find_packages(),
install_requires=[],

include_package_data=True,
test_suite = 'tests' )
include_package_data=True)
45 changes: 28 additions & 17 deletions smart_imports/ast_parser.py
Expand Up @@ -12,11 +12,11 @@ def __init__(self):
super().__init__()
self.scope = scopes_tree.Scope(type=c.SCOPE_TYPE.NORMAL)

def register_variable_get(self, variable):
self.scope.register_variable(variable, c.VARIABLE_STATE.UNINITIALIZED)
def register_variable_get(self, variable, line):
self.scope.register_variable(variable, c.VARIABLE_STATE.UNINITIALIZED, line)

def register_variable_set(self, variable):
self.scope.register_variable(variable, c.VARIABLE_STATE.INITIALIZED)
def register_variable_set(self, variable, line):
self.scope.register_variable(variable, c.VARIABLE_STATE.INITIALIZED, line)

def push_scope(self, type):
scope = scopes_tree.Scope(type)
Expand All @@ -28,12 +28,12 @@ def pop_scope(self):

def visit_Name(self, node):
if not isinstance(node.ctx, ast.Store):
self.register_variable_get(node.id)
self.register_variable_get(node.id, node.lineno)
else:
self.register_variable_set(node.id)
self.register_variable_set(node.id, node.lineno)

def _visit_comprehension(self, node):
self.push_scope(type=type)
self.push_scope(type=c.SCOPE_TYPE.COMPREHENSION)

for generator in node.generators:
self._visit_for_comprehension(generator)
Expand Down Expand Up @@ -70,22 +70,27 @@ def visit_DictComp(self, node):

def visit_Import(self, node):
for alias in node.names:
self.register_variable_set(alias.asname if alias.asname else alias.name)
self.register_variable_set(alias.asname if alias.asname else alias.name,
node.lineno)
self.generic_visit(node)

def visit_ImportFrom(self, node):
for alias in node.names:
self.register_variable_set(alias.asname if alias.asname else alias.name)
self.register_variable_set(alias.asname if alias.asname else alias.name,
node.lineno)
self.generic_visit(node)

def visit_FunctionDef(self, node):
self.register_variable_set(node.name)
self.register_variable_set(node.name, node.lineno)

for decorator in node.decorator_list:
self.visit(decorator)

self.visit_default_arguments(node.args)

if node.returns is not None:
self.visit(node.returns)

self.push_scope(type=c.SCOPE_TYPE.NORMAL)

self.visit_arguments(node.args)
Expand Down Expand Up @@ -122,26 +127,32 @@ def visit_default_arguments(self, node):

self.visit(default)

def process_arg(self, arg):
self.register_variable_set(arg.arg, arg.lineno)

if arg.annotation is not None:
self.visit(arg.annotation)

def visit_arguments(self, node):
for arg in node.args:
self.register_variable_set(arg.arg)
self.process_arg(arg)

for arg in node.kwonlyargs:
self.register_variable_set(arg.arg)
self.process_arg(arg)

if node.vararg:
self.register_variable_set(node.vararg.arg)
self.process_arg(node.vararg)

if node.kwarg:
self.register_variable_set(node.kwarg.arg)
self.process_arg(node.kwarg)

def visit_ClassDef(self, node):
self.register_variable_set(node.name)
self.register_variable_set(node.name, node.lineno)

self.push_scope(type=c.SCOPE_TYPE.CLASS)

for keyword in node.keywords:
self.register_variable_set(keyword)
self.register_variable_set(keyword, node.lineno)

self.generic_visit(node)
self.pop_scope()
Expand All @@ -153,7 +164,7 @@ def visit_ExceptHandler(self, node):
self.push_scope(type=c.SCOPE_TYPE.NORMAL)

if node.name:
self.register_variable_set(node.name)
self.register_variable_set(node.name, node.lineno)

for body_node in node.body:
self.visit(body_node)
Expand Down
100 changes: 100 additions & 0 deletions smart_imports/cache.py
@@ -0,0 +1,100 @@

import os
import pathlib
import hashlib
import warnings
import functools

from . import constants


WARNING_MESSAGE = 'Error while accessing smart imports cache'


def get_checksum(source):
return hashlib.sha256(source.encode('utf-8')).hexdigest()


def get_cache_path(cache_dir, module_name):
return os.path.join(cache_dir, module_name + '.cache')


def ignore_errors(function):

@functools.wraps(function)
def wrapper(*argv, **kwargs):
try:
return function(*argv, **kwargs)
except Exception as e:
warnings.warn('{}: {}'.format(WARNING_MESSAGE, e), UserWarning, stacklevel=2)
return None

return wrapper


@ignore_errors
def get(cache_dir, module_name, checksum):

cache_path = get_cache_path(cache_dir, module_name)

if not os.path.isfile(cache_path):
return None

with open(cache_path) as f:
protocol_version = f.readline().strip()

if protocol_version != constants.CACHE_PROTOCOL_VERSION:
return None

saved_checksum = f.readline().strip()

if saved_checksum != checksum:
return None

variables = [variable.strip() for variable in f.readlines()]

return variables


@ignore_errors
def set(cache_dir, module_name, checksum, variables):
pathlib.Path(cache_dir).mkdir(parents=True, exist_ok=True)

cache_path = get_cache_path(cache_dir, module_name)

with open(cache_path, 'w') as f:
f.write(constants.CACHE_PROTOCOL_VERSION)
f.write('\n')

f.write(checksum)
f.write('\n')

for variable in variables:
f.write(variable)
f.write('\n')


class Cache:
__slots__ = ('cache_dir', 'module_name', 'checksum',)

def __init__(self, cache_dir, module_name, source):
self.cache_dir = cache_dir
self.module_name = module_name
self.checksum = get_checksum(source)

def get(self):
if self.cache_dir is None:
return None

return get(cache_dir=self.cache_dir,
module_name=self.module_name,
checksum=self.checksum)

def set(self, variables):
if self.cache_dir is None:
return None

set(cache_dir=self.cache_dir,
module_name=self.module_name,
checksum=self.checksum,
variables=variables)

0 comments on commit cd728d2

Please sign in to comment.