diff --git a/conftest.py b/conftest.py index 02182648..ae888af1 100644 --- a/conftest.py +++ b/conftest.py @@ -14,6 +14,7 @@ collect_ignore = [ '__main__.py', 'jedi/evaluate/compiled/subprocess/__main__.py', 'build/', + 'test/examples', ] diff --git a/jedi/_compatibility.py b/jedi/_compatibility.py index e3e50e5f..6c5e531a 100644 --- a/jedi/_compatibility.py +++ b/jedi/_compatibility.py @@ -264,6 +264,11 @@ try: except NameError: FileNotFoundError = IOError +try: + NotADirectoryError = NotADirectoryError +except NameError: + NotADirectoryError = IOError + def no_unicode_pprint(dct): """ diff --git a/jedi/api/__init__.py b/jedi/api/__init__.py index 49d0859b..6fc1fa2f 100644 --- a/jedi/api/__init__.py +++ b/jedi/api/__init__.py @@ -101,7 +101,7 @@ class Script(object): sys_path = list(map(force_unicode, sys_path)) # Load the Python grammar of the current interpreter. - project = get_default_project() + project = get_default_project(path or os.getcwd()) # TODO deprecate and remove sys_path from the Script API. if sys_path is not None: project._sys_path = sys_path diff --git a/jedi/api/project.py b/jedi/api/project.py index fcef1b9f..d7a5fb67 100644 --- a/jedi/api/project.py +++ b/jedi/api/project.py @@ -1,13 +1,14 @@ import os import json -from jedi._compatibility import FileNotFoundError +from jedi._compatibility import FileNotFoundError, NotADirectoryError from jedi.api.environment import DefaultEnvironment, \ get_default_environment, from_executable from jedi.api.exceptions import WrongVersion from jedi._compatibility import force_unicode from jedi.evaluate.sys_path import detect_additional_paths from jedi.evaluate.cache import evaluator_as_method_param_cache +from jedi.evaluate.sys_path import traverse_parents _CONFIG_FOLDER = '.jedi' _CONTAINS_POTENTIAL_PROJECT = 'setup.py', '.git', '.hg', 'MANIFEST.in' @@ -54,7 +55,8 @@ class Project(object): rely on your packages being properly configured on the ``sys.path``. """ - def py2_comp(path, environment=None, sys_path=None, smart_sys_path=True): + def py2_comp(path, environment=None, sys_path=None, + smart_sys_path=True, _django=False): self._path = path if isinstance(environment, DefaultEnvironment): self._environment = environment @@ -62,6 +64,7 @@ class Project(object): self._sys_path = sys_path self._smart_sys_path = smart_sys_path + self._django = _django py2_comp(path, **kwargs) @@ -90,11 +93,15 @@ class Project(object): if evaluator.script_path is None or not self._smart_sys_path: return sys_path + prefixed = [] + if self._smart_sys_path: + if self._django: + prefixed.append(self._path) added_paths = map( force_unicode, detect_additional_paths(evaluator, evaluator.script_path) ) - return sys_path + list(added_paths) + return prefixed + sys_path + list(added_paths) def save(self): data = dict(self.__dict__) @@ -121,26 +128,38 @@ def _is_potential_project(path): return False +def _is_django_path(directory): + """ Detects the path of the very well known Django library (if used) """ + try: + with open(os.path.join(directory, 'manage.py'), 'br') as f: + return b"DJANGO_SETTINGS_MODULE" in f.read() + except (FileNotFoundError, NotADirectoryError): + return False + + return False + + def get_default_project(path=None): if path is None: path = os.getcwd() - previous = None - curdir = dir = os.path.realpath(path) + check = os.path.realpath(path) probable_path = None - while dir != previous: + for dir in traverse_parents(check, include_current=True): try: return Project.load(dir) - except FileNotFoundError: + except (FileNotFoundError, NotADirectoryError): pass + if _is_django_path(dir): + return Project(dir, _django=True) + if probable_path is None and _is_potential_project(dir): probable_path = dir - previous = dir - dir = os.path.dirname(dir) - else: - if probable_path is not None: - # TODO search for setup.py etc - return Project(probable_path) - return Project(curdir) + if probable_path is not None: + # TODO search for setup.py etc + return Project(probable_path) + + curdir = path if os.path.isdir(path) else os.path.dirname(path) + return Project(curdir) diff --git a/jedi/evaluate/sys_path.py b/jedi/evaluate/sys_path.py index 7dd42938..aa07d924 100644 --- a/jedi/evaluate/sys_path.py +++ b/jedi/evaluate/sys_path.py @@ -134,14 +134,13 @@ def sys_path_with_modifications(evaluator, module_context): def detect_additional_paths(evaluator, script_path): - django_paths = _detect_django_path(script_path) buildout_script_paths = set() for buildout_script_path in _get_buildout_script_paths(script_path): for path in _get_paths_from_buildout_script(evaluator, buildout_script_path): buildout_script_paths.add(path) - return django_paths + list(buildout_script_paths) + return list(buildout_script_paths) def _get_paths_from_buildout_script(evaluator, buildout_script_path): @@ -161,13 +160,15 @@ def _get_paths_from_buildout_script(evaluator, buildout_script_path): yield path -def traverse_parents(path): - while True: - new = os.path.dirname(path) - if new == path: - return - path = new +def traverse_parents(path, include_current=False): + if not include_current: + path = os.path.dirname(path) + + previous = None + while previous != path: yield path + previous = path + path = os.path.dirname(path) def _get_parent_dir_with_file(path, filename): @@ -177,18 +178,6 @@ def _get_parent_dir_with_file(path, filename): return None -def _detect_django_path(module_path): - """ Detects the path of the very well known Django library (if used) """ - result = [] - - for parent in traverse_parents(module_path): - with ignored(IOError): - with open(parent + os.path.sep + 'manage.py'): - debug.dbg('Found django path: %s', module_path) - result.append(parent) - return result - - def _get_buildout_script_paths(module_path): """ if there is a 'buildout.cfg' file in one of the parent directories of the diff --git a/test/helpers.py b/test/helpers.py index d1ee7828..54d4a2e0 100644 --- a/test/helpers.py +++ b/test/helpers.py @@ -13,7 +13,7 @@ else: TestCase = unittest.TestCase import os -from os.path import abspath, dirname +from os.path import abspath, dirname, join import functools test_dir = dirname(abspath(__file__)) @@ -21,6 +21,11 @@ root_dir = dirname(test_dir) sample_int = 1 # This is used in completion/imports.py + +def get_example_dir(name): + return join(test_dir, 'examples', name) + + def cwd_at(path): """ Decorator to run function at `path`. diff --git a/test/test_api/test_project.py b/test/test_api/test_project.py new file mode 100644 index 00000000..a0f4e4d5 --- /dev/null +++ b/test/test_api/test_project.py @@ -0,0 +1,15 @@ +import os + +from ..helpers import get_example_dir + + +def test_django_default_project(Script): + dir = get_example_dir('django') + + script = Script( + "from app import models\nmodels.SomeMo", + path=os.path.join(dir, 'models/x.py') + ) + c, = script.completions() + assert c.name == "SomeModel" + assert script._project._django is True