diff --git a/jedi/api.py b/jedi/api.py index e2631507..9f1fdeea 100644 --- a/jedi/api.py +++ b/jedi/api.py @@ -468,6 +468,25 @@ class Script(object): api_classes._clear_caches() +def defined_names(source, source_path=None, source_encoding='utf-8'): + """ + Get all definitions in `source` sorted by its position. + + This functions can be used for listing functions, classes and + data defined in a file. This can be useful if you want to list + them in "sidebar". Each element in the returned list also has + `defined_names` method which can be used to get sub-definitions + (e.g., methods in class). + + :rtype: list of api_classes.Definition + """ + parser = parsing.Parser( + modules.source_to_unicode(source, source_encoding), + module_path=source_path, + ) + return api_classes.defined_names(parser.scope) + + def set_debug_function(func_cb=debug.print_to_stdout, warnings=True, notices=True, speed=True): """ diff --git a/jedi/api_classes.py b/jedi/api_classes.py index 3a966609..a84c7fe8 100644 --- a/jedi/api_classes.py +++ b/jedi/api_classes.py @@ -8,7 +8,7 @@ import re import os import warnings -from _compatibility import unicode +from _compatibility import unicode, next import cache import dynamic import recursion @@ -58,6 +58,9 @@ class BaseDefinition(object): def __init__(self, definition, start_pos): self.start_pos = start_pos self.definition = definition + """ + An instance of :class:`jedi.parsing_representation.Base` subclass. + """ self.is_keyword = isinstance(definition, keywords.Keyword) # generate a path to the definition @@ -271,6 +274,40 @@ class Definition(BaseDefinition): def __init__(self, definition): super(Definition, self).__init__(definition, definition.start_pos) + @property + def name(self): + """ + Name of variable/function/class/module. + + For example, for ``x = None`` it returns ``'x'``. + + :rtype: str or None + """ + d = self.definition + if isinstance(d, er.InstanceElement): + d = d.var + + if isinstance(d, pr.Name): + return d.names[-1] if d.names else None + elif isinstance(d, er.Array): + return unicode(d.type) + elif isinstance(d, (pr.Class, er.Class, er.Instance, + er.Function, pr.Function)): + return unicode(d.name) + elif isinstance(d, pr.Module): + return self.module_name + elif isinstance(d, pr.Import): + try: + return d.get_defined_names()[0].names[-1] + except (AttributeError, IndexError): + return None + elif isinstance(d, pr.Statement): + try: + return d.assignment_details[0][1].values[0][0].name.names[-1] + except IndexError: + return None + return None + @property def description(self): """ @@ -317,6 +354,32 @@ class Definition(BaseDefinition): position = '' return "%s:%s%s" % (self.module_name, self.description, position) + def defined_names(self): + """ + List sub-definitions (e.g., methods in class). + + :rtype: list of Definition + """ + d = self.definition + if isinstance(d, er.InstanceElement): + d = d.var + if isinstance(d, pr.Name): + d = d.parent + return defined_names(d) + + +def defined_names(scope): + """ + List sub-definitions (e.g., methods in class). + + :type scope: Scope + :rtype: list of Definition + """ + pair = next(evaluate.get_names_of_scope( + scope, star_search=False, include_builtin=False), None) + names = pair[1] if pair else [] + return [Definition(d) for d in sorted(names, key=lambda s: s.start_pos)] + class RelatedName(BaseDefinition): """TODO: document this""" diff --git a/jedi/evaluate.py b/jedi/evaluate.py index 884a2ea6..4dd1dd88 100644 --- a/jedi/evaluate.py +++ b/jedi/evaluate.py @@ -153,6 +153,8 @@ def get_names_of_scope(scope, position=None, star_search=True, >>> pairs[2] #doctest: +ELLIPSIS (, [, ...]) + :rtype: [(pr.Scope, [pr.Name])] + :return: Return an generator that yields a pair of scope and names. """ in_func_scope = scope non_flow = scope.get_parent_until(pr.Flow, reverse=True) diff --git a/test/regression.py b/test/regression.py index 8f01ea14..70d25d70 100755 --- a/test/regression.py +++ b/test/regression.py @@ -391,6 +391,60 @@ class TestFeature(TestBase): self.assertEqual(quick_values, real_values) +class TestGetDefinitions(TestBase): + + def test_get_definitions_flat(self): + definitions = api.defined_names(""" + import module + class Class: + pass + def func(): + pass + data = None + """) + self.assertEqual([d.name for d in definitions], + ['module', 'Class', 'func', 'data']) + + def test_dotted_assignment(self): + definitions = api.defined_names(""" + x = Class() + x.y.z = None + """) + self.assertEqual([d.name for d in definitions], + ['x']) + + def test_multiple_assignment(self): + definitions = api.defined_names(""" + x = y = None + """) + self.assertEqual([d.name for d in definitions], + ['x', 'y']) + + def test_multiple_imports(self): + definitions = api.defined_names(""" + from module import a, b + from another_module import * + """) + self.assertEqual([d.name for d in definitions], + ['a', 'b']) + + def test_nested_definitions(self): + definitions = api.defined_names(""" + class Class: + def f(): + pass + def g(): + pass + """) + self.assertEqual([d.name for d in definitions], + ['Class']) + subdefinitions = definitions[0].defined_names() + self.assertEqual([d.name for d in subdefinitions], + ['f', 'g']) + self.assertEqual([d.full_name for d in subdefinitions], + ['Class.f', 'Class.g']) + + class TestSpeed(TestBase): def _check_speed(time_per_run, number=4, run_warm=True): """ Speed checks should typically be very tolerant. Some machines are