adding new stuff
This commit is contained in:
parent
f84d7183aa
commit
9ef8a96f9a
1580 changed files with 0 additions and 0 deletions
|
|
@ -0,0 +1,7 @@
|
|||
"""rope IDE tools package
|
||||
|
||||
This package contains modules that can be used in IDEs
|
||||
but do not depend on the UI. So these modules will be used
|
||||
by `rope.ui` modules.
|
||||
|
||||
"""
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1,222 @@
|
|||
import re
|
||||
|
||||
from rope.base import builtins
|
||||
from rope.base import exceptions
|
||||
from rope.base import libutils
|
||||
from rope.base import pynames
|
||||
from rope.base import pyobjects
|
||||
from rope.base import resources
|
||||
from rope.base import resourceobserver
|
||||
from rope.base import taskhandle
|
||||
from rope.refactor import importutils
|
||||
|
||||
|
||||
class AutoImport(object):
|
||||
"""A class for finding the module that provides a name
|
||||
|
||||
This class maintains a cache of global names in python modules.
|
||||
Note that this cache is not accurate and might be out of date.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, project, observe=True, underlined=False):
|
||||
"""Construct an AutoImport object
|
||||
|
||||
If `observe` is `True`, listen for project changes and update
|
||||
the cache.
|
||||
|
||||
If `underlined` is `True`, underlined names are cached, too.
|
||||
"""
|
||||
self.project = project
|
||||
self.underlined = underlined
|
||||
self.names = project.data_files.read_data('globalnames')
|
||||
if self.names is None:
|
||||
self.names = {}
|
||||
project.data_files.add_write_hook(self._write)
|
||||
# XXX: using a filtered observer
|
||||
observer = resourceobserver.ResourceObserver(
|
||||
changed=self._changed, moved=self._moved, removed=self._removed)
|
||||
if observe:
|
||||
project.add_observer(observer)
|
||||
|
||||
def import_assist(self, starting):
|
||||
"""Return a list of ``(name, module)`` tuples
|
||||
|
||||
This function tries to find modules that have a global name
|
||||
that starts with `starting`.
|
||||
"""
|
||||
# XXX: breaking if gave up! use generators
|
||||
result = []
|
||||
for module in self.names:
|
||||
for global_name in self.names[module]:
|
||||
if global_name.startswith(starting):
|
||||
result.append((global_name, module))
|
||||
return result
|
||||
|
||||
def get_modules(self, name):
|
||||
"""Return the list of modules that have global `name`"""
|
||||
result = []
|
||||
for module in self.names:
|
||||
if name in self.names[module]:
|
||||
result.append(module)
|
||||
return result
|
||||
|
||||
def get_all_names(self):
|
||||
"""Return the list of all cached global names"""
|
||||
result = set()
|
||||
for module in self.names:
|
||||
result.update(set(self.names[module]))
|
||||
return result
|
||||
|
||||
def get_name_locations(self, name):
|
||||
"""Return a list of ``(resource, lineno)`` tuples"""
|
||||
result = []
|
||||
for module in self.names:
|
||||
if name in self.names[module]:
|
||||
try:
|
||||
pymodule = self.project.get_module(module)
|
||||
if name in pymodule:
|
||||
pyname = pymodule[name]
|
||||
module, lineno = pyname.get_definition_location()
|
||||
if module is not None:
|
||||
resource = module.get_module().get_resource()
|
||||
if resource is not None and lineno is not None:
|
||||
result.append((resource, lineno))
|
||||
except exceptions.ModuleNotFoundError:
|
||||
pass
|
||||
return result
|
||||
|
||||
def generate_cache(self, resources=None, underlined=None,
|
||||
task_handle=taskhandle.NullTaskHandle()):
|
||||
"""Generate global name cache for project files
|
||||
|
||||
If `resources` is a list of `rope.base.resource.File`\s, only
|
||||
those files are searched; otherwise all python modules in the
|
||||
project are cached.
|
||||
|
||||
"""
|
||||
if resources is None:
|
||||
resources = self.project.get_python_files()
|
||||
job_set = task_handle.create_jobset(
|
||||
'Generatig autoimport cache', len(resources))
|
||||
for file in resources:
|
||||
job_set.started_job('Working on <%s>' % file.path)
|
||||
self.update_resource(file, underlined)
|
||||
job_set.finished_job()
|
||||
|
||||
def generate_modules_cache(self, modules, underlined=None,
|
||||
task_handle=taskhandle.NullTaskHandle()):
|
||||
"""Generate global name cache for modules listed in `modules`"""
|
||||
job_set = task_handle.create_jobset(
|
||||
'Generatig autoimport cache for modules', len(modules))
|
||||
for modname in modules:
|
||||
job_set.started_job('Working on <%s>' % modname)
|
||||
if modname.endswith('.*'):
|
||||
mod = self.project.find_module(modname[:-2])
|
||||
if mod:
|
||||
for sub in submodules(mod):
|
||||
self.update_resource(sub, underlined)
|
||||
else:
|
||||
self.update_module(modname, underlined)
|
||||
job_set.finished_job()
|
||||
|
||||
def clear_cache(self):
|
||||
"""Clear all entries in global-name cache
|
||||
|
||||
It might be a good idea to use this function before
|
||||
regenerating global names.
|
||||
|
||||
"""
|
||||
self.names.clear()
|
||||
|
||||
def find_insertion_line(self, code):
|
||||
"""Guess at what line the new import should be inserted"""
|
||||
match = re.search(r'^(def|class)\s+', code)
|
||||
if match is not None:
|
||||
code = code[:match.start()]
|
||||
try:
|
||||
pymodule = libutils.get_string_module(self.project, code)
|
||||
except exceptions.ModuleSyntaxError:
|
||||
return 1
|
||||
testmodname = '__rope_testmodule_rope'
|
||||
importinfo = importutils.NormalImport(((testmodname, None),))
|
||||
module_imports = importutils.get_module_imports(self.project,
|
||||
pymodule)
|
||||
module_imports.add_import(importinfo)
|
||||
code = module_imports.get_changed_source()
|
||||
offset = code.index(testmodname)
|
||||
lineno = code.count('\n', 0, offset) + 1
|
||||
return lineno
|
||||
|
||||
def update_resource(self, resource, underlined=None):
|
||||
"""Update the cache for global names in `resource`"""
|
||||
try:
|
||||
pymodule = self.project.get_pymodule(resource)
|
||||
modname = self._module_name(resource)
|
||||
self._add_names(pymodule, modname, underlined)
|
||||
except exceptions.ModuleSyntaxError:
|
||||
pass
|
||||
|
||||
def update_module(self, modname, underlined=None):
|
||||
"""Update the cache for global names in `modname` module
|
||||
|
||||
`modname` is the name of a module.
|
||||
"""
|
||||
try:
|
||||
pymodule = self.project.get_module(modname)
|
||||
self._add_names(pymodule, modname, underlined)
|
||||
except exceptions.ModuleNotFoundError:
|
||||
pass
|
||||
|
||||
def _module_name(self, resource):
|
||||
return libutils.modname(resource)
|
||||
|
||||
def _add_names(self, pymodule, modname, underlined):
|
||||
if underlined is None:
|
||||
underlined = self.underlined
|
||||
globals = []
|
||||
if isinstance(pymodule, pyobjects.PyDefinedObject):
|
||||
attributes = pymodule._get_structural_attributes()
|
||||
else:
|
||||
attributes = pymodule.get_attributes()
|
||||
for name, pyname in attributes.items():
|
||||
if not underlined and name.startswith('_'):
|
||||
continue
|
||||
if isinstance(pyname, (pynames.AssignedName, pynames.DefinedName)):
|
||||
globals.append(name)
|
||||
if isinstance(pymodule, builtins.BuiltinModule):
|
||||
globals.append(name)
|
||||
self.names[modname] = globals
|
||||
|
||||
def _write(self):
|
||||
self.project.data_files.write_data('globalnames', self.names)
|
||||
|
||||
def _changed(self, resource):
|
||||
if not resource.is_folder():
|
||||
self.update_resource(resource)
|
||||
|
||||
def _moved(self, resource, newresource):
|
||||
if not resource.is_folder():
|
||||
modname = self._module_name(resource)
|
||||
if modname in self.names:
|
||||
del self.names[modname]
|
||||
self.update_resource(newresource)
|
||||
|
||||
def _removed(self, resource):
|
||||
if not resource.is_folder():
|
||||
modname = self._module_name(resource)
|
||||
if modname in self.names:
|
||||
del self.names[modname]
|
||||
|
||||
|
||||
def submodules(mod):
|
||||
if isinstance(mod, resources.File):
|
||||
if mod.name.endswith('.py') and mod.name != '__init__.py':
|
||||
return set([mod])
|
||||
return set()
|
||||
if not mod.has_child('__init__.py'):
|
||||
return set()
|
||||
result = set([mod])
|
||||
for child in mod.get_children():
|
||||
result |= submodules(child)
|
||||
return result
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
"""For performing many refactorings as a single command
|
||||
|
||||
`changestack` module can be used to perform many refactorings on top
|
||||
of each other as one bigger command. It can be used like::
|
||||
|
||||
stack = ChangeStack(project, 'my big command')
|
||||
|
||||
#..
|
||||
stack.push(refactoring1.get_changes())
|
||||
#..
|
||||
stack.push(refactoring2.get_changes())
|
||||
#..
|
||||
stack.push(refactoringX.get_changes())
|
||||
|
||||
stack.pop_all()
|
||||
changes = stack.merged()
|
||||
|
||||
Now `changes` can be previewed or performed as before.
|
||||
"""
|
||||
|
||||
from rope.base import change
|
||||
|
||||
|
||||
class ChangeStack(object):
|
||||
|
||||
def __init__(self, project, description='merged changes'):
|
||||
self.project = project
|
||||
self.description = description
|
||||
self.stack = []
|
||||
|
||||
def push(self, changes):
|
||||
self.stack.append(changes)
|
||||
self.project.do(changes)
|
||||
|
||||
def pop_all(self):
|
||||
for i in range(len(self.stack)):
|
||||
self.project.history.undo(drop=True)
|
||||
|
||||
def merged(self):
|
||||
result = change.ChangeSet(self.description)
|
||||
for changes in self.stack:
|
||||
for c in self._basic_changes(changes):
|
||||
result.add_change(c)
|
||||
return result
|
||||
|
||||
def _basic_changes(self, changes):
|
||||
if isinstance(changes, change.ChangeSet):
|
||||
for child in changes.changes:
|
||||
for atom in self._basic_changes(child):
|
||||
yield atom
|
||||
else:
|
||||
yield changes
|
||||
|
|
@ -0,0 +1,695 @@
|
|||
import keyword
|
||||
import sys
|
||||
import warnings
|
||||
|
||||
import rope.base.codeanalyze
|
||||
import rope.base.evaluate
|
||||
from rope.base import builtins
|
||||
from rope.base import exceptions
|
||||
from rope.base import libutils
|
||||
from rope.base import pynames
|
||||
from rope.base import pynamesdef
|
||||
from rope.base import pyobjects
|
||||
from rope.base import pyobjectsdef
|
||||
from rope.base import pyscopes
|
||||
from rope.base import worder
|
||||
from rope.contrib import fixsyntax
|
||||
from rope.refactor import functionutils
|
||||
|
||||
|
||||
def code_assist(project, source_code, offset, resource=None,
|
||||
templates=None, maxfixes=1, later_locals=True):
|
||||
"""Return python code completions as a list of `CodeAssistProposal`\s
|
||||
|
||||
`resource` is a `rope.base.resources.Resource` object. If
|
||||
provided, relative imports are handled.
|
||||
|
||||
`maxfixes` is the maximum number of errors to fix if the code has
|
||||
errors in it.
|
||||
|
||||
If `later_locals` is `False` names defined in this scope and after
|
||||
this line is ignored.
|
||||
|
||||
"""
|
||||
if templates is not None:
|
||||
warnings.warn('Codeassist no longer supports templates',
|
||||
DeprecationWarning, stacklevel=2)
|
||||
assist = _PythonCodeAssist(
|
||||
project, source_code, offset, resource=resource,
|
||||
maxfixes=maxfixes, later_locals=later_locals)
|
||||
return assist()
|
||||
|
||||
|
||||
def starting_offset(source_code, offset):
|
||||
"""Return the offset in which the completion should be inserted
|
||||
|
||||
Usually code assist proposals should be inserted like::
|
||||
|
||||
completion = proposal.name
|
||||
result = (source_code[:starting_offset] +
|
||||
completion + source_code[offset:])
|
||||
|
||||
Where starting_offset is the offset returned by this function.
|
||||
|
||||
"""
|
||||
word_finder = worder.Worder(source_code, True)
|
||||
expression, starting, starting_offset = \
|
||||
word_finder.get_splitted_primary_before(offset)
|
||||
return starting_offset
|
||||
|
||||
|
||||
def get_doc(project, source_code, offset, resource=None, maxfixes=1):
|
||||
"""Get the pydoc"""
|
||||
fixer = fixsyntax.FixSyntax(project, source_code, resource, maxfixes)
|
||||
pyname = fixer.pyname_at(offset)
|
||||
if pyname is None:
|
||||
return None
|
||||
pyobject = pyname.get_object()
|
||||
return PyDocExtractor().get_doc(pyobject)
|
||||
|
||||
|
||||
def get_calltip(project, source_code, offset, resource=None,
|
||||
maxfixes=1, ignore_unknown=False, remove_self=False):
|
||||
"""Get the calltip of a function
|
||||
|
||||
The format of the returned string is
|
||||
``module_name.holding_scope_names.function_name(arguments)``. For
|
||||
classes `__init__()` and for normal objects `__call__()` function
|
||||
is used.
|
||||
|
||||
Note that the offset is on the function itself *not* after the its
|
||||
open parenthesis. (Actually it used to be the other way but it
|
||||
was easily confused when string literals were involved. So I
|
||||
decided it is better for it not to try to be too clever when it
|
||||
cannot be clever enough). You can use a simple search like::
|
||||
|
||||
offset = source_code.rindex('(', 0, offset) - 1
|
||||
|
||||
to handle simple situations.
|
||||
|
||||
If `ignore_unknown` is `True`, `None` is returned for functions
|
||||
without source-code like builtins and extensions.
|
||||
|
||||
If `remove_self` is `True`, the first parameter whose name is self
|
||||
will be removed for methods.
|
||||
"""
|
||||
fixer = fixsyntax.FixSyntax(project, source_code, resource, maxfixes)
|
||||
pyname = fixer.pyname_at(offset)
|
||||
if pyname is None:
|
||||
return None
|
||||
pyobject = pyname.get_object()
|
||||
return PyDocExtractor().get_calltip(pyobject, ignore_unknown, remove_self)
|
||||
|
||||
|
||||
def get_definition_location(project, source_code, offset,
|
||||
resource=None, maxfixes=1):
|
||||
"""Return the definition location of the python name at `offset`
|
||||
|
||||
Return a (`rope.base.resources.Resource`, lineno) tuple. If no
|
||||
`resource` is given and the definition is inside the same module,
|
||||
the first element of the returned tuple would be `None`. If the
|
||||
location cannot be determined ``(None, None)`` is returned.
|
||||
|
||||
"""
|
||||
fixer = fixsyntax.FixSyntax(project, source_code, resource, maxfixes)
|
||||
pyname = fixer.pyname_at(offset)
|
||||
if pyname is not None:
|
||||
module, lineno = pyname.get_definition_location()
|
||||
if module is not None:
|
||||
return module.get_module().get_resource(), lineno
|
||||
return (None, None)
|
||||
|
||||
|
||||
def find_occurrences(*args, **kwds):
|
||||
import rope.contrib.findit
|
||||
warnings.warn('Use `rope.contrib.findit.find_occurrences()` instead',
|
||||
DeprecationWarning, stacklevel=2)
|
||||
return rope.contrib.findit.find_occurrences(*args, **kwds)
|
||||
|
||||
|
||||
def get_canonical_path(project, resource, offset):
|
||||
"""Get the canonical path to an object.
|
||||
|
||||
Given the offset of the object, this returns a list of
|
||||
(name, name_type) tuples representing the canonical path to the
|
||||
object. For example, the 'x' in the following code:
|
||||
|
||||
class Foo(object):
|
||||
def bar(self):
|
||||
class Qux(object):
|
||||
def mux(self, x):
|
||||
pass
|
||||
|
||||
we will return:
|
||||
|
||||
[('Foo', 'CLASS'), ('bar', 'FUNCTION'), ('Qux', 'CLASS'),
|
||||
('mux', 'FUNCTION'), ('x', 'PARAMETER')]
|
||||
|
||||
`resource` is a `rope.base.resources.Resource` object.
|
||||
|
||||
`offset` is the offset of the pyname you want the path to.
|
||||
|
||||
"""
|
||||
# Retrieve the PyName.
|
||||
pymod = project.get_pymodule(resource)
|
||||
pyname = rope.base.evaluate.eval_location(pymod, offset)
|
||||
|
||||
# Now get the location of the definition and its containing scope.
|
||||
defmod, lineno = pyname.get_definition_location()
|
||||
if not defmod:
|
||||
return None
|
||||
scope = defmod.get_scope().get_inner_scope_for_line(lineno)
|
||||
|
||||
# Start with the name of the object we're interested in.
|
||||
names = []
|
||||
if isinstance(pyname, pynamesdef.ParameterName):
|
||||
names = [(worder.get_name_at(pymod.get_resource(), offset),
|
||||
'PARAMETER') ]
|
||||
elif isinstance(pyname, pynamesdef.AssignedName):
|
||||
names = [(worder.get_name_at(pymod.get_resource(), offset),
|
||||
'VARIABLE')]
|
||||
|
||||
# Collect scope names.
|
||||
while scope.parent:
|
||||
if isinstance(scope, pyscopes.FunctionScope):
|
||||
scope_type = 'FUNCTION'
|
||||
elif isinstance(scope, pyscopes.ClassScope):
|
||||
scope_type = 'CLASS'
|
||||
else:
|
||||
scope_type = None
|
||||
names.append((scope.pyobject.get_name(), scope_type))
|
||||
scope = scope.parent
|
||||
|
||||
names.append((defmod.get_resource().real_path, 'MODULE'))
|
||||
names.reverse()
|
||||
return names
|
||||
|
||||
|
||||
class CompletionProposal(object):
|
||||
"""A completion proposal
|
||||
|
||||
The `scope` instance variable shows where proposed name came from
|
||||
and can be 'global', 'local', 'builtin', 'attribute', 'keyword',
|
||||
'imported', 'parameter_keyword'.
|
||||
|
||||
The `type` instance variable shows the approximate type of the
|
||||
proposed object and can be 'instance', 'class', 'function', 'module',
|
||||
and `None`.
|
||||
|
||||
All possible relations between proposal's `scope` and `type` are shown
|
||||
in the table below (different scopes in rows and types in columns):
|
||||
|
||||
| instance | class | function | module | None
|
||||
local | + | + | + | + |
|
||||
global | + | + | + | + |
|
||||
builtin | + | + | + | |
|
||||
attribute | + | + | + | + |
|
||||
imported | + | + | + | + |
|
||||
keyword | | | | | +
|
||||
parameter_keyword | | | | | +
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, name, scope, pyname=None):
|
||||
self.name = name
|
||||
self.pyname = pyname
|
||||
self.scope = self._get_scope(scope)
|
||||
|
||||
def __str__(self):
|
||||
return '%s (%s, %s)' % (self.name, self.scope, self.type)
|
||||
|
||||
def __repr__(self):
|
||||
return str(self)
|
||||
|
||||
@property
|
||||
def parameters(self):
|
||||
"""The names of the parameters the function takes.
|
||||
|
||||
Returns None if this completion is not a function.
|
||||
"""
|
||||
pyname = self.pyname
|
||||
if isinstance(pyname, pynames.ImportedName):
|
||||
pyname = pyname._get_imported_pyname()
|
||||
if isinstance(pyname, pynames.DefinedName):
|
||||
pyobject = pyname.get_object()
|
||||
if isinstance(pyobject, pyobjects.AbstractFunction):
|
||||
return pyobject.get_param_names()
|
||||
|
||||
@property
|
||||
def type(self):
|
||||
pyname = self.pyname
|
||||
if isinstance(pyname, builtins.BuiltinName):
|
||||
pyobject = pyname.get_object()
|
||||
if isinstance(pyobject, builtins.BuiltinFunction):
|
||||
return 'function'
|
||||
elif isinstance(pyobject, builtins.BuiltinClass):
|
||||
return 'class'
|
||||
elif isinstance(pyobject, builtins.BuiltinObject) or \
|
||||
isinstance(pyobject, builtins.BuiltinName):
|
||||
return 'instance'
|
||||
elif isinstance(pyname, pynames.ImportedModule):
|
||||
return 'module'
|
||||
elif isinstance(pyname, pynames.ImportedName) or \
|
||||
isinstance(pyname, pynames.DefinedName):
|
||||
pyobject = pyname.get_object()
|
||||
if isinstance(pyobject, pyobjects.AbstractFunction):
|
||||
return 'function'
|
||||
if isinstance(pyobject, pyobjects.AbstractClass):
|
||||
return 'class'
|
||||
return 'instance'
|
||||
|
||||
def _get_scope(self, scope):
|
||||
if isinstance(self.pyname, builtins.BuiltinName):
|
||||
return 'builtin'
|
||||
if isinstance(self.pyname, pynames.ImportedModule) or \
|
||||
isinstance(self.pyname, pynames.ImportedName):
|
||||
return 'imported'
|
||||
return scope
|
||||
|
||||
def get_doc(self):
|
||||
"""Get the proposed object's docstring.
|
||||
|
||||
Returns None if it can not be get.
|
||||
"""
|
||||
if not self.pyname:
|
||||
return None
|
||||
pyobject = self.pyname.get_object()
|
||||
if not hasattr(pyobject, 'get_doc'):
|
||||
return None
|
||||
return self.pyname.get_object().get_doc()
|
||||
|
||||
@property
|
||||
def kind(self):
|
||||
warnings.warn("the proposal's `kind` property is deprecated, "
|
||||
"use `scope` instead")
|
||||
return self.scope
|
||||
|
||||
|
||||
# leaved for backward compatibility
|
||||
CodeAssistProposal = CompletionProposal
|
||||
|
||||
|
||||
class NamedParamProposal(CompletionProposal):
|
||||
"""A parameter keyword completion proposal
|
||||
|
||||
Holds reference to ``_function`` -- the function which
|
||||
parameter ``name`` belongs to. This allows to determine
|
||||
default value for this parameter.
|
||||
"""
|
||||
def __init__(self, name, function):
|
||||
self.argname = name
|
||||
name = '%s=' % name
|
||||
super(NamedParamProposal, self).__init__(name, 'parameter_keyword')
|
||||
self._function = function
|
||||
|
||||
def get_default(self):
|
||||
"""Get a string representation of a param's default value.
|
||||
|
||||
Returns None if there is no default value for this param.
|
||||
"""
|
||||
definfo = functionutils.DefinitionInfo.read(self._function)
|
||||
for arg, default in definfo.args_with_defaults:
|
||||
if self.argname == arg:
|
||||
return default
|
||||
return None
|
||||
|
||||
|
||||
def sorted_proposals(proposals, scopepref=None, typepref=None):
|
||||
"""Sort a list of proposals
|
||||
|
||||
Return a sorted list of the given `CodeAssistProposal`\s.
|
||||
|
||||
`scopepref` can be a list of proposal scopes. Defaults to
|
||||
``['parameter_keyword', 'local', 'global', 'imported',
|
||||
'attribute', 'builtin', 'keyword']``.
|
||||
|
||||
`typepref` can be a list of proposal types. Defaults to
|
||||
``['class', 'function', 'instance', 'module', None]``.
|
||||
(`None` stands for completions with no type like keywords.)
|
||||
"""
|
||||
sorter = _ProposalSorter(proposals, scopepref, typepref)
|
||||
return sorter.get_sorted_proposal_list()
|
||||
|
||||
|
||||
def starting_expression(source_code, offset):
|
||||
"""Return the expression to complete"""
|
||||
word_finder = worder.Worder(source_code, True)
|
||||
expression, starting, starting_offset = \
|
||||
word_finder.get_splitted_primary_before(offset)
|
||||
if expression:
|
||||
return expression + '.' + starting
|
||||
return starting
|
||||
|
||||
|
||||
def default_templates():
|
||||
warnings.warn('default_templates() is deprecated.',
|
||||
DeprecationWarning, stacklevel=2)
|
||||
return {}
|
||||
|
||||
|
||||
class _PythonCodeAssist(object):
|
||||
|
||||
def __init__(self, project, source_code, offset, resource=None,
|
||||
maxfixes=1, later_locals=True):
|
||||
self.project = project
|
||||
self.code = source_code
|
||||
self.resource = resource
|
||||
self.maxfixes = maxfixes
|
||||
self.later_locals = later_locals
|
||||
self.word_finder = worder.Worder(source_code, True)
|
||||
self.expression, self.starting, self.offset = \
|
||||
self.word_finder.get_splitted_primary_before(offset)
|
||||
|
||||
keywords = keyword.kwlist
|
||||
|
||||
def _find_starting_offset(self, source_code, offset):
|
||||
current_offset = offset - 1
|
||||
while current_offset >= 0 and (source_code[current_offset].isalnum() or
|
||||
source_code[current_offset] in '_'):
|
||||
current_offset -= 1
|
||||
return current_offset + 1
|
||||
|
||||
def _matching_keywords(self, starting):
|
||||
result = []
|
||||
for kw in self.keywords:
|
||||
if kw.startswith(starting):
|
||||
result.append(CompletionProposal(kw, 'keyword'))
|
||||
return result
|
||||
|
||||
def __call__(self):
|
||||
if self.offset > len(self.code):
|
||||
return []
|
||||
completions = list(self._code_completions().values())
|
||||
if self.expression.strip() == '' and self.starting.strip() != '':
|
||||
completions.extend(self._matching_keywords(self.starting))
|
||||
return completions
|
||||
|
||||
def _dotted_completions(self, module_scope, holding_scope):
|
||||
result = {}
|
||||
found_pyname = rope.base.evaluate.eval_str(holding_scope,
|
||||
self.expression)
|
||||
if found_pyname is not None:
|
||||
element = found_pyname.get_object()
|
||||
compl_scope = 'attribute'
|
||||
if isinstance(element, (pyobjectsdef.PyModule,
|
||||
pyobjectsdef.PyPackage)):
|
||||
compl_scope = 'imported'
|
||||
for name, pyname in element.get_attributes().items():
|
||||
if name.startswith(self.starting):
|
||||
result[name] = CompletionProposal(name, compl_scope,
|
||||
pyname)
|
||||
return result
|
||||
|
||||
def _undotted_completions(self, scope, result, lineno=None):
|
||||
if scope.parent is not None:
|
||||
self._undotted_completions(scope.parent, result)
|
||||
if lineno is None:
|
||||
names = scope.get_propagated_names()
|
||||
else:
|
||||
names = scope.get_names()
|
||||
for name, pyname in names.items():
|
||||
if name.startswith(self.starting):
|
||||
compl_scope = 'local'
|
||||
if scope.get_kind() == 'Module':
|
||||
compl_scope = 'global'
|
||||
if lineno is None or self.later_locals or \
|
||||
not self._is_defined_after(scope, pyname, lineno):
|
||||
result[name] = CompletionProposal(name, compl_scope,
|
||||
pyname)
|
||||
|
||||
def _from_import_completions(self, pymodule):
|
||||
module_name = self.word_finder.get_from_module(self.offset)
|
||||
if module_name is None:
|
||||
return {}
|
||||
pymodule = self._find_module(pymodule, module_name)
|
||||
result = {}
|
||||
for name in pymodule:
|
||||
if name.startswith(self.starting):
|
||||
result[name] = CompletionProposal(name, scope='global',
|
||||
pyname=pymodule[name])
|
||||
return result
|
||||
|
||||
def _find_module(self, pymodule, module_name):
|
||||
dots = 0
|
||||
while module_name[dots] == '.':
|
||||
dots += 1
|
||||
pyname = pynames.ImportedModule(pymodule,
|
||||
module_name[dots:], dots)
|
||||
return pyname.get_object()
|
||||
|
||||
def _is_defined_after(self, scope, pyname, lineno):
|
||||
location = pyname.get_definition_location()
|
||||
if location is not None and location[1] is not None:
|
||||
if location[0] == scope.pyobject.get_module() and \
|
||||
lineno <= location[1] <= scope.get_end():
|
||||
return True
|
||||
|
||||
def _code_completions(self):
|
||||
lineno = self.code.count('\n', 0, self.offset) + 1
|
||||
fixer = fixsyntax.FixSyntax(self.project, self.code,
|
||||
self.resource, self.maxfixes)
|
||||
pymodule = fixer.get_pymodule()
|
||||
module_scope = pymodule.get_scope()
|
||||
code = pymodule.source_code
|
||||
lines = code.split('\n')
|
||||
result = {}
|
||||
start = fixsyntax._logical_start(lines, lineno)
|
||||
indents = fixsyntax._get_line_indents(lines[start - 1])
|
||||
inner_scope = module_scope.get_inner_scope_for_line(start, indents)
|
||||
if self.word_finder.is_a_name_after_from_import(self.offset):
|
||||
return self._from_import_completions(pymodule)
|
||||
if self.expression.strip() != '':
|
||||
result.update(self._dotted_completions(module_scope, inner_scope))
|
||||
else:
|
||||
result.update(self._keyword_parameters(module_scope.pyobject,
|
||||
inner_scope))
|
||||
self._undotted_completions(inner_scope, result, lineno=lineno)
|
||||
return result
|
||||
|
||||
def _keyword_parameters(self, pymodule, scope):
|
||||
offset = self.offset
|
||||
if offset == 0:
|
||||
return {}
|
||||
word_finder = worder.Worder(self.code, True)
|
||||
if word_finder.is_on_function_call_keyword(offset - 1):
|
||||
function_parens = word_finder.\
|
||||
find_parens_start_from_inside(offset - 1)
|
||||
primary = word_finder.get_primary_at(function_parens - 1)
|
||||
try:
|
||||
function_pyname = rope.base.evaluate.\
|
||||
eval_str(scope, primary)
|
||||
except exceptions.BadIdentifierError:
|
||||
return {}
|
||||
if function_pyname is not None:
|
||||
pyobject = function_pyname.get_object()
|
||||
if isinstance(pyobject, pyobjects.AbstractFunction):
|
||||
pass
|
||||
elif isinstance(pyobject, pyobjects.AbstractClass) and \
|
||||
'__init__' in pyobject:
|
||||
pyobject = pyobject['__init__'].get_object()
|
||||
elif '__call__' in pyobject:
|
||||
pyobject = pyobject['__call__'].get_object()
|
||||
if isinstance(pyobject, pyobjects.AbstractFunction):
|
||||
param_names = []
|
||||
param_names.extend(
|
||||
pyobject.get_param_names(special_args=False))
|
||||
result = {}
|
||||
for name in param_names:
|
||||
if name.startswith(self.starting):
|
||||
result[name + '='] = NamedParamProposal(
|
||||
name, pyobject
|
||||
)
|
||||
return result
|
||||
return {}
|
||||
|
||||
|
||||
class _ProposalSorter(object):
|
||||
"""Sort a list of code assist proposals"""
|
||||
|
||||
def __init__(self, code_assist_proposals, scopepref=None, typepref=None):
|
||||
self.proposals = code_assist_proposals
|
||||
if scopepref is None:
|
||||
scopepref = ['parameter_keyword', 'local', 'global', 'imported',
|
||||
'attribute', 'builtin', 'keyword']
|
||||
self.scopepref = scopepref
|
||||
if typepref is None:
|
||||
typepref = ['class', 'function', 'instance', 'module', None]
|
||||
self.typerank = dict((type, index)
|
||||
for index, type in enumerate(typepref))
|
||||
|
||||
def get_sorted_proposal_list(self):
|
||||
"""Return a list of `CodeAssistProposal`"""
|
||||
proposals = {}
|
||||
for proposal in self.proposals:
|
||||
proposals.setdefault(proposal.scope, []).append(proposal)
|
||||
result = []
|
||||
for scope in self.scopepref:
|
||||
scope_proposals = proposals.get(scope, [])
|
||||
scope_proposals = [proposal for proposal in scope_proposals
|
||||
if proposal.type in self.typerank]
|
||||
scope_proposals.sort(key=self._proposal_key)
|
||||
result.extend(scope_proposals)
|
||||
return result
|
||||
|
||||
def _proposal_key(self, proposal1):
|
||||
def _underline_count(name):
|
||||
return sum(1 for c in name if c == "_")
|
||||
return (self.typerank.get(proposal1.type, 100),
|
||||
_underline_count(proposal1.name),
|
||||
proposal1.name)
|
||||
#if proposal1.type != proposal2.type:
|
||||
# return cmp(self.typerank.get(proposal1.type, 100),
|
||||
# self.typerank.get(proposal2.type, 100))
|
||||
#return self._compare_underlined_names(proposal1.name,
|
||||
# proposal2.name)
|
||||
|
||||
|
||||
class PyDocExtractor(object):
|
||||
|
||||
def get_doc(self, pyobject):
|
||||
if isinstance(pyobject, pyobjects.AbstractFunction):
|
||||
return self._get_function_docstring(pyobject)
|
||||
elif isinstance(pyobject, pyobjects.AbstractClass):
|
||||
return self._get_class_docstring(pyobject)
|
||||
elif isinstance(pyobject, pyobjects.AbstractModule):
|
||||
return self._trim_docstring(pyobject.get_doc())
|
||||
return None
|
||||
|
||||
def get_calltip(self, pyobject, ignore_unknown=False, remove_self=False):
|
||||
try:
|
||||
if isinstance(pyobject, pyobjects.AbstractClass):
|
||||
pyobject = pyobject['__init__'].get_object()
|
||||
if not isinstance(pyobject, pyobjects.AbstractFunction):
|
||||
pyobject = pyobject['__call__'].get_object()
|
||||
except exceptions.AttributeNotFoundError:
|
||||
return None
|
||||
if ignore_unknown and not isinstance(pyobject, pyobjects.PyFunction):
|
||||
return
|
||||
if isinstance(pyobject, pyobjects.AbstractFunction):
|
||||
result = self._get_function_signature(pyobject, add_module=True)
|
||||
if remove_self and self._is_method(pyobject):
|
||||
return result.replace('(self)', '()').replace('(self, ', '(')
|
||||
return result
|
||||
|
||||
def _get_class_docstring(self, pyclass):
|
||||
contents = self._trim_docstring(pyclass.get_doc(), 2)
|
||||
supers = [super.get_name() for super in pyclass.get_superclasses()]
|
||||
doc = 'class %s(%s):\n\n' % (pyclass.get_name(), ', '.join(supers)) \
|
||||
+ contents
|
||||
|
||||
if '__init__' in pyclass:
|
||||
init = pyclass['__init__'].get_object()
|
||||
if isinstance(init, pyobjects.AbstractFunction):
|
||||
doc += '\n\n' + self._get_single_function_docstring(init)
|
||||
return doc
|
||||
|
||||
def _get_function_docstring(self, pyfunction):
|
||||
functions = [pyfunction]
|
||||
if self._is_method(pyfunction):
|
||||
functions.extend(self._get_super_methods(pyfunction.parent,
|
||||
pyfunction.get_name()))
|
||||
return '\n\n'.join([self._get_single_function_docstring(function)
|
||||
for function in functions])
|
||||
|
||||
def _is_method(self, pyfunction):
|
||||
return isinstance(pyfunction, pyobjects.PyFunction) and \
|
||||
isinstance(pyfunction.parent, pyobjects.PyClass)
|
||||
|
||||
def _get_single_function_docstring(self, pyfunction):
|
||||
signature = self._get_function_signature(pyfunction)
|
||||
docs = self._trim_docstring(pyfunction.get_doc(), indents=2)
|
||||
return signature + ':\n\n' + docs
|
||||
|
||||
def _get_super_methods(self, pyclass, name):
|
||||
result = []
|
||||
for super_class in pyclass.get_superclasses():
|
||||
if name in super_class:
|
||||
function = super_class[name].get_object()
|
||||
if isinstance(function, pyobjects.AbstractFunction):
|
||||
result.append(function)
|
||||
result.extend(self._get_super_methods(super_class, name))
|
||||
return result
|
||||
|
||||
def _get_function_signature(self, pyfunction, add_module=False):
|
||||
location = self._location(pyfunction, add_module)
|
||||
if isinstance(pyfunction, pyobjects.PyFunction):
|
||||
info = functionutils.DefinitionInfo.read(pyfunction)
|
||||
return location + info.to_string()
|
||||
else:
|
||||
return '%s(%s)' % (location + pyfunction.get_name(),
|
||||
', '.join(pyfunction.get_param_names()))
|
||||
|
||||
def _location(self, pyobject, add_module=False):
|
||||
location = []
|
||||
parent = pyobject.parent
|
||||
while parent and not isinstance(parent, pyobjects.AbstractModule):
|
||||
location.append(parent.get_name())
|
||||
location.append('.')
|
||||
parent = parent.parent
|
||||
if add_module:
|
||||
if isinstance(pyobject, pyobjects.PyFunction):
|
||||
location.insert(0, self._get_module(pyobject))
|
||||
if isinstance(parent, builtins.BuiltinModule):
|
||||
location.insert(0, parent.get_name() + '.')
|
||||
return ''.join(location)
|
||||
|
||||
def _get_module(self, pyfunction):
|
||||
module = pyfunction.get_module()
|
||||
if module is not None:
|
||||
resource = module.get_resource()
|
||||
if resource is not None:
|
||||
return libutils.modname(resource) + '.'
|
||||
return ''
|
||||
|
||||
def _trim_docstring(self, docstring, indents=0):
|
||||
"""The sample code from :PEP:`257`"""
|
||||
if not docstring:
|
||||
return ''
|
||||
# Convert tabs to spaces (following normal Python rules)
|
||||
# and split into a list of lines:
|
||||
lines = docstring.expandtabs().splitlines()
|
||||
# Determine minimum indentation (first line doesn't count):
|
||||
indent = sys.maxsize
|
||||
for line in lines[1:]:
|
||||
stripped = line.lstrip()
|
||||
if stripped:
|
||||
indent = min(indent, len(line) - len(stripped))
|
||||
# Remove indentation (first line is special):
|
||||
trimmed = [lines[0].strip()]
|
||||
if indent < sys.maxsize:
|
||||
for line in lines[1:]:
|
||||
trimmed.append(line[indent:].rstrip())
|
||||
# Strip off trailing and leading blank lines:
|
||||
while trimmed and not trimmed[-1]:
|
||||
trimmed.pop()
|
||||
while trimmed and not trimmed[0]:
|
||||
trimmed.pop(0)
|
||||
# Return a single string:
|
||||
return '\n'.join((' ' * indents + line for line in trimmed))
|
||||
|
||||
|
||||
# Deprecated classes
|
||||
|
||||
class TemplateProposal(CodeAssistProposal):
|
||||
def __init__(self, name, template):
|
||||
warnings.warn('TemplateProposal is deprecated.',
|
||||
DeprecationWarning, stacklevel=2)
|
||||
super(TemplateProposal, self).__init__(name, 'template')
|
||||
self.template = template
|
||||
|
||||
|
||||
class Template(object):
|
||||
|
||||
def __init__(self, template):
|
||||
self.template = template
|
||||
warnings.warn('Template is deprecated.',
|
||||
DeprecationWarning, stacklevel=2)
|
||||
|
||||
def variables(self):
|
||||
return []
|
||||
|
||||
def substitute(self, mapping):
|
||||
return self.template
|
||||
|
||||
def get_cursor_location(self, mapping):
|
||||
return len(self.template)
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
"""Finding bad name and attribute accesses
|
||||
|
||||
`find_errors` function can be used to find possible bad name and
|
||||
attribute accesses. As an example::
|
||||
|
||||
errors = find_errors(project, project.get_resource('mod.py'))
|
||||
for error in errors:
|
||||
print('%s: %s' % (error.lineno, error.error))
|
||||
|
||||
prints possible errors for ``mod.py`` file.
|
||||
|
||||
TODO:
|
||||
|
||||
* use task handles
|
||||
* reporting names at most once
|
||||
* attributes of extension modules that don't appear in
|
||||
extension_modules project config can be ignored
|
||||
* not calling `PyScope.get_inner_scope_for_line()` if it is a
|
||||
bottleneck; needs profiling
|
||||
* not reporting occurrences where rope cannot infer the object
|
||||
* rope saves multiple objects for some of the names in its objectdb
|
||||
use all of them not to give false positives
|
||||
* ... ;-)
|
||||
|
||||
"""
|
||||
from rope.base import ast, evaluate, pyobjects
|
||||
|
||||
|
||||
def find_errors(project, resource):
|
||||
"""Find possible bad name and attribute accesses
|
||||
|
||||
It returns a list of `Error`\s.
|
||||
"""
|
||||
pymodule = project.get_pymodule(resource)
|
||||
finder = _BadAccessFinder(pymodule)
|
||||
ast.walk(pymodule.get_ast(), finder)
|
||||
return finder.errors
|
||||
|
||||
|
||||
class _BadAccessFinder(object):
|
||||
|
||||
def __init__(self, pymodule):
|
||||
self.pymodule = pymodule
|
||||
self.scope = pymodule.get_scope()
|
||||
self.errors = []
|
||||
|
||||
def _Name(self, node):
|
||||
if isinstance(node.ctx, (ast.Store, ast.Param)):
|
||||
return
|
||||
scope = self.scope.get_inner_scope_for_line(node.lineno)
|
||||
pyname = scope.lookup(node.id)
|
||||
if pyname is None:
|
||||
self._add_error(node, 'Unresolved variable')
|
||||
elif self._is_defined_after(scope, pyname, node.lineno):
|
||||
self._add_error(node, 'Defined later')
|
||||
|
||||
def _Attribute(self, node):
|
||||
if not isinstance(node.ctx, ast.Store):
|
||||
scope = self.scope.get_inner_scope_for_line(node.lineno)
|
||||
pyname = evaluate.eval_node(scope, node.value)
|
||||
if pyname is not None and \
|
||||
pyname.get_object() != pyobjects.get_unknown():
|
||||
if node.attr not in pyname.get_object():
|
||||
self._add_error(node, 'Unresolved attribute')
|
||||
ast.walk(node.value, self)
|
||||
|
||||
def _add_error(self, node, msg):
|
||||
if isinstance(node, ast.Attribute):
|
||||
name = node.attr
|
||||
else:
|
||||
name = node.id
|
||||
if name != 'None':
|
||||
error = Error(node.lineno, msg + ' ' + name)
|
||||
self.errors.append(error)
|
||||
|
||||
def _is_defined_after(self, scope, pyname, lineno):
|
||||
location = pyname.get_definition_location()
|
||||
if location is not None and location[1] is not None:
|
||||
if location[0] == self.pymodule and \
|
||||
lineno <= location[1] <= scope.get_end():
|
||||
return True
|
||||
|
||||
|
||||
class Error(object):
|
||||
|
||||
def __init__(self, lineno, error):
|
||||
self.lineno = lineno
|
||||
self.error = error
|
||||
|
||||
def __str__(self):
|
||||
return '%s: %s' % (self.lineno, self.error)
|
||||
114
plugins/bundle/python-mode/pymode/libs/rope/contrib/findit.py
Normal file
114
plugins/bundle/python-mode/pymode/libs/rope/contrib/findit.py
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
import rope.base.codeanalyze
|
||||
import rope.base.evaluate
|
||||
import rope.base.pyobjects
|
||||
from rope.base import taskhandle, exceptions, worder
|
||||
from rope.contrib import fixsyntax
|
||||
from rope.refactor import occurrences
|
||||
|
||||
|
||||
def find_occurrences(project, resource, offset, unsure=False, resources=None,
|
||||
in_hierarchy=False,
|
||||
task_handle=taskhandle.NullTaskHandle()):
|
||||
"""Return a list of `Location`\s
|
||||
|
||||
If `unsure` is `True`, possible matches are returned, too. You
|
||||
can use `Location.unsure` to see which are unsure occurrences.
|
||||
`resources` can be a list of `rope.base.resource.File`\s that
|
||||
should be searched for occurrences; if `None` all python files
|
||||
in the project are searched.
|
||||
|
||||
"""
|
||||
name = worder.get_name_at(resource, offset)
|
||||
this_pymodule = project.get_pymodule(resource)
|
||||
primary, pyname = rope.base.evaluate.eval_location2(
|
||||
this_pymodule, offset)
|
||||
|
||||
def is_match(occurrence):
|
||||
return unsure
|
||||
finder = occurrences.create_finder(
|
||||
project, name, pyname, unsure=is_match,
|
||||
in_hierarchy=in_hierarchy, instance=primary)
|
||||
if resources is None:
|
||||
resources = project.get_python_files()
|
||||
job_set = task_handle.create_jobset('Finding Occurrences',
|
||||
count=len(resources))
|
||||
return _find_locations(finder, resources, job_set)
|
||||
|
||||
|
||||
def find_implementations(project, resource, offset, resources=None,
|
||||
task_handle=taskhandle.NullTaskHandle()):
|
||||
"""Find the places a given method is overridden.
|
||||
|
||||
Finds the places a method is implemented. Returns a list of
|
||||
`Location`\s.
|
||||
"""
|
||||
name = worder.get_name_at(resource, offset)
|
||||
this_pymodule = project.get_pymodule(resource)
|
||||
pyname = rope.base.evaluate.eval_location(this_pymodule, offset)
|
||||
if pyname is not None:
|
||||
pyobject = pyname.get_object()
|
||||
if not isinstance(pyobject, rope.base.pyobjects.PyFunction) or \
|
||||
pyobject.get_kind() != 'method':
|
||||
raise exceptions.BadIdentifierError('Not a method!')
|
||||
else:
|
||||
raise exceptions.BadIdentifierError('Cannot resolve the identifier!')
|
||||
|
||||
def is_defined(occurrence):
|
||||
if not occurrence.is_defined():
|
||||
return False
|
||||
|
||||
def not_self(occurrence):
|
||||
if occurrence.get_pyname().get_object() == pyname.get_object():
|
||||
return False
|
||||
filters = [is_defined, not_self,
|
||||
occurrences.InHierarchyFilter(pyname, True)]
|
||||
finder = occurrences.Finder(project, name, filters=filters)
|
||||
if resources is None:
|
||||
resources = project.get_python_files()
|
||||
job_set = task_handle.create_jobset('Finding Implementations',
|
||||
count=len(resources))
|
||||
return _find_locations(finder, resources, job_set)
|
||||
|
||||
|
||||
def find_definition(project, code, offset, resource=None, maxfixes=1):
|
||||
"""Return the definition location of the python name at `offset`
|
||||
|
||||
A `Location` object is returned if the definition location can be
|
||||
determined, otherwise ``None`` is returned.
|
||||
"""
|
||||
fixer = fixsyntax.FixSyntax(project, code, resource, maxfixes)
|
||||
pyname = fixer.pyname_at(offset)
|
||||
if pyname is not None:
|
||||
module, lineno = pyname.get_definition_location()
|
||||
name = rope.base.worder.Worder(code).get_word_at(offset)
|
||||
if lineno is not None:
|
||||
start = module.lines.get_line_start(lineno)
|
||||
|
||||
def check_offset(occurrence):
|
||||
if occurrence.offset < start:
|
||||
return False
|
||||
pyname_filter = occurrences.PyNameFilter(pyname)
|
||||
finder = occurrences.Finder(project, name,
|
||||
[check_offset, pyname_filter])
|
||||
for occurrence in finder.find_occurrences(pymodule=module):
|
||||
return Location(occurrence)
|
||||
|
||||
|
||||
class Location(object):
|
||||
|
||||
def __init__(self, occurrence):
|
||||
self.resource = occurrence.resource
|
||||
self.region = occurrence.get_word_range()
|
||||
self.offset = self.region[0]
|
||||
self.unsure = occurrence.is_unsure()
|
||||
self.lineno = occurrence.lineno
|
||||
|
||||
|
||||
def _find_locations(finder, resources, job_set):
|
||||
result = []
|
||||
for resource in resources:
|
||||
job_set.started_job(resource.path)
|
||||
for occurrence in finder.find_occurrences(resource):
|
||||
result.append(Location(occurrence))
|
||||
job_set.finished_job()
|
||||
return result
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
"""Fix the name of modules
|
||||
|
||||
This module is useful when you want to rename many of the modules in
|
||||
your project. That can happen specially when you want to change their
|
||||
naming style.
|
||||
|
||||
For instance::
|
||||
|
||||
fixer = FixModuleNames(project)
|
||||
changes = fixer.get_changes(fixer=str.lower)
|
||||
project.do(changes)
|
||||
|
||||
Here it renames all modules and packages to use lower-cased chars.
|
||||
You can tell it to use any other style by using the ``fixer``
|
||||
argument.
|
||||
|
||||
"""
|
||||
from rope.base import taskhandle
|
||||
from rope.contrib import changestack
|
||||
from rope.refactor import rename
|
||||
|
||||
|
||||
class FixModuleNames(object):
|
||||
|
||||
def __init__(self, project):
|
||||
self.project = project
|
||||
|
||||
def get_changes(self, fixer=str.lower,
|
||||
task_handle=taskhandle.NullTaskHandle()):
|
||||
"""Fix module names
|
||||
|
||||
`fixer` is a function that takes and returns a `str`. Given
|
||||
the name of a module, it should return the fixed name.
|
||||
|
||||
"""
|
||||
stack = changestack.ChangeStack(self.project, 'Fixing module names')
|
||||
jobset = task_handle.create_jobset('Fixing module names',
|
||||
self._count_fixes(fixer) + 1)
|
||||
try:
|
||||
while True:
|
||||
for resource in self._tobe_fixed(fixer):
|
||||
jobset.started_job(resource.path)
|
||||
renamer = rename.Rename(self.project, resource)
|
||||
changes = renamer.get_changes(fixer(self._name(resource)))
|
||||
stack.push(changes)
|
||||
jobset.finished_job()
|
||||
break
|
||||
else:
|
||||
break
|
||||
finally:
|
||||
jobset.started_job('Reverting to original state')
|
||||
stack.pop_all()
|
||||
jobset.finished_job()
|
||||
return stack.merged()
|
||||
|
||||
def _count_fixes(self, fixer):
|
||||
return len(list(self._tobe_fixed(fixer)))
|
||||
|
||||
def _tobe_fixed(self, fixer):
|
||||
for resource in self.project.get_python_files():
|
||||
modname = self._name(resource)
|
||||
if modname != fixer(modname):
|
||||
yield resource
|
||||
|
||||
def _name(self, resource):
|
||||
modname = resource.name.rsplit('.', 1)[0]
|
||||
if modname == '__init__':
|
||||
modname = resource.parent.name
|
||||
return modname
|
||||
181
plugins/bundle/python-mode/pymode/libs/rope/contrib/fixsyntax.py
Normal file
181
plugins/bundle/python-mode/pymode/libs/rope/contrib/fixsyntax.py
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
import rope.base.codeanalyze
|
||||
import rope.base.evaluate
|
||||
from rope.base import exceptions
|
||||
from rope.base import libutils
|
||||
from rope.base import utils
|
||||
from rope.base import worder
|
||||
from rope.base.codeanalyze import ArrayLinesAdapter, LogicalLineFinder
|
||||
|
||||
|
||||
class FixSyntax(object):
|
||||
|
||||
def __init__(self, project, code, resource, maxfixes=1):
|
||||
self.project = project
|
||||
self.code = code
|
||||
self.resource = resource
|
||||
self.maxfixes = maxfixes
|
||||
|
||||
@utils.saveit
|
||||
def get_pymodule(self):
|
||||
"""Get a `PyModule`"""
|
||||
msg = None
|
||||
code = self.code
|
||||
tries = 0
|
||||
while True:
|
||||
try:
|
||||
if tries == 0 and self.resource is not None and \
|
||||
self.resource.read() == code:
|
||||
return self.project.get_pymodule(self.resource,
|
||||
force_errors=True)
|
||||
return libutils.get_string_module(
|
||||
self.project, code, resource=self.resource,
|
||||
force_errors=True)
|
||||
except exceptions.ModuleSyntaxError as e:
|
||||
if msg is None:
|
||||
msg = '%s:%s %s' % (e.filename, e.lineno, e.message_)
|
||||
if tries < self.maxfixes:
|
||||
tries += 1
|
||||
self.commenter.comment(e.lineno)
|
||||
code = '\n'.join(self.commenter.lines)
|
||||
else:
|
||||
raise exceptions.ModuleSyntaxError(
|
||||
e.filename, e.lineno,
|
||||
'Failed to fix error: {0}'.format(msg))
|
||||
|
||||
@property
|
||||
@utils.saveit
|
||||
def commenter(self):
|
||||
return _Commenter(self.code)
|
||||
|
||||
def pyname_at(self, offset):
|
||||
pymodule = self.get_pymodule()
|
||||
|
||||
def old_pyname():
|
||||
word_finder = worder.Worder(self.code, True)
|
||||
expression = word_finder.get_primary_at(offset)
|
||||
expression = expression.replace('\\\n', ' ').replace('\n', ' ')
|
||||
lineno = self.code.count('\n', 0, offset)
|
||||
scope = pymodule.get_scope().get_inner_scope_for_line(lineno)
|
||||
return rope.base.evaluate.eval_str(scope, expression)
|
||||
new_code = pymodule.source_code
|
||||
|
||||
def new_pyname():
|
||||
newoffset = self.commenter.transfered_offset(offset)
|
||||
return rope.base.evaluate.eval_location(pymodule, newoffset)
|
||||
if new_code.startswith(self.code[:offset + 1]):
|
||||
return new_pyname()
|
||||
result = old_pyname()
|
||||
if result is None:
|
||||
return new_pyname()
|
||||
return result
|
||||
|
||||
|
||||
class _Commenter(object):
|
||||
|
||||
def __init__(self, code):
|
||||
self.code = code
|
||||
self.lines = self.code.split('\n')
|
||||
self.lines.append('\n')
|
||||
self.origs = list(range(len(self.lines) + 1))
|
||||
self.diffs = [0] * (len(self.lines) + 1)
|
||||
|
||||
def comment(self, lineno):
|
||||
start = _logical_start(self.lines, lineno, check_prev=True) - 1
|
||||
# using self._get_stmt_end() instead of self._get_block_end()
|
||||
# to lower commented lines
|
||||
end = self._get_stmt_end(start)
|
||||
indents = _get_line_indents(self.lines[start])
|
||||
if 0 < start:
|
||||
last_lineno = self._last_non_blank(start - 1)
|
||||
last_line = self.lines[last_lineno]
|
||||
if last_line.rstrip().endswith(':'):
|
||||
indents = _get_line_indents(last_line) + 4
|
||||
self._set(start, ' ' * indents + 'pass')
|
||||
for line in range(start + 1, end + 1):
|
||||
self._set(line, self.lines[start])
|
||||
self._fix_incomplete_try_blocks(lineno, indents)
|
||||
|
||||
def transfered_offset(self, offset):
|
||||
lineno = self.code.count('\n', 0, offset)
|
||||
diff = sum(self.diffs[:lineno])
|
||||
return offset + diff
|
||||
|
||||
def _last_non_blank(self, start):
|
||||
while start > 0 and self.lines[start].strip() == '':
|
||||
start -= 1
|
||||
return start
|
||||
|
||||
def _get_block_end(self, lineno):
|
||||
end_line = lineno
|
||||
base_indents = _get_line_indents(self.lines[lineno])
|
||||
for i in range(lineno + 1, len(self.lines)):
|
||||
if _get_line_indents(self.lines[i]) >= base_indents:
|
||||
end_line = i
|
||||
else:
|
||||
break
|
||||
return end_line
|
||||
|
||||
def _get_stmt_end(self, lineno):
|
||||
base_indents = _get_line_indents(self.lines[lineno])
|
||||
for i in range(lineno + 1, len(self.lines)):
|
||||
if _get_line_indents(self.lines[i]) <= base_indents:
|
||||
return i - 1
|
||||
return lineno
|
||||
|
||||
def _fix_incomplete_try_blocks(self, lineno, indents):
|
||||
block_start = lineno
|
||||
last_indents = indents
|
||||
while block_start > 0:
|
||||
block_start = rope.base.codeanalyze.get_block_start(
|
||||
ArrayLinesAdapter(self.lines), block_start) - 1
|
||||
if self.lines[block_start].strip().startswith('try:'):
|
||||
indents = _get_line_indents(self.lines[block_start])
|
||||
if indents > last_indents:
|
||||
continue
|
||||
last_indents = indents
|
||||
block_end = self._find_matching_deindent(block_start)
|
||||
line = self.lines[block_end].strip()
|
||||
if not (line.startswith('finally:') or
|
||||
line.startswith('except ') or
|
||||
line.startswith('except:')):
|
||||
self._insert(block_end, ' ' * indents + 'finally:')
|
||||
self._insert(block_end + 1, ' ' * indents + ' pass')
|
||||
|
||||
def _find_matching_deindent(self, line_number):
|
||||
indents = _get_line_indents(self.lines[line_number])
|
||||
current_line = line_number + 1
|
||||
while current_line < len(self.lines):
|
||||
line = self.lines[current_line]
|
||||
if not line.strip().startswith('#') and not line.strip() == '':
|
||||
# HACK: We should have used logical lines here
|
||||
if _get_line_indents(self.lines[current_line]) <= indents:
|
||||
return current_line
|
||||
current_line += 1
|
||||
return len(self.lines) - 1
|
||||
|
||||
def _set(self, lineno, line):
|
||||
self.diffs[self.origs[lineno]] += len(line) - len(self.lines[lineno])
|
||||
self.lines[lineno] = line
|
||||
|
||||
def _insert(self, lineno, line):
|
||||
self.diffs[self.origs[lineno]] += len(line) + 1
|
||||
self.origs.insert(lineno, self.origs[lineno])
|
||||
self.lines.insert(lineno, line)
|
||||
|
||||
|
||||
def _logical_start(lines, lineno, check_prev=False):
|
||||
logical_finder = LogicalLineFinder(ArrayLinesAdapter(lines))
|
||||
if check_prev:
|
||||
prev = lineno - 1
|
||||
while prev > 0:
|
||||
start, end = logical_finder.logical_line_in(prev)
|
||||
if end is None or start <= lineno < end:
|
||||
return start
|
||||
if start <= prev:
|
||||
break
|
||||
prev -= 1
|
||||
return logical_finder.logical_line_in(lineno)[0]
|
||||
|
||||
|
||||
def _get_line_indents(line):
|
||||
return rope.base.codeanalyze.count_line_indents(line)
|
||||
362
plugins/bundle/python-mode/pymode/libs/rope/contrib/generate.py
Normal file
362
plugins/bundle/python-mode/pymode/libs/rope/contrib/generate.py
Normal file
|
|
@ -0,0 +1,362 @@
|
|||
import rope.base.evaluate
|
||||
from rope.base import libutils
|
||||
from rope.base import (change, pyobjects, exceptions, pynames, worder,
|
||||
codeanalyze)
|
||||
from rope.refactor import sourceutils, importutils, functionutils, suites
|
||||
|
||||
|
||||
def create_generate(kind, project, resource, offset):
|
||||
"""A factory for creating `Generate` objects
|
||||
|
||||
`kind` can be 'variable', 'function', 'class', 'module' or
|
||||
'package'.
|
||||
|
||||
"""
|
||||
generate = eval('Generate' + kind.title())
|
||||
return generate(project, resource, offset)
|
||||
|
||||
|
||||
def create_module(project, name, sourcefolder=None):
|
||||
"""Creates a module and returns a `rope.base.resources.File`"""
|
||||
if sourcefolder is None:
|
||||
sourcefolder = project.root
|
||||
packages = name.split('.')
|
||||
parent = sourcefolder
|
||||
for package in packages[:-1]:
|
||||
parent = parent.get_child(package)
|
||||
return parent.create_file(packages[-1] + '.py')
|
||||
|
||||
|
||||
def create_package(project, name, sourcefolder=None):
|
||||
"""Creates a package and returns a `rope.base.resources.Folder`"""
|
||||
if sourcefolder is None:
|
||||
sourcefolder = project.root
|
||||
packages = name.split('.')
|
||||
parent = sourcefolder
|
||||
for package in packages[:-1]:
|
||||
parent = parent.get_child(package)
|
||||
made_packages = parent.create_folder(packages[-1])
|
||||
made_packages.create_file('__init__.py')
|
||||
return made_packages
|
||||
|
||||
|
||||
class _Generate(object):
|
||||
|
||||
def __init__(self, project, resource, offset):
|
||||
self.project = project
|
||||
self.resource = resource
|
||||
self.info = self._generate_info(project, resource, offset)
|
||||
self.name = self.info.get_name()
|
||||
self._check_exceptional_conditions()
|
||||
|
||||
def _generate_info(self, project, resource, offset):
|
||||
return _GenerationInfo(project.pycore, resource, offset)
|
||||
|
||||
def _check_exceptional_conditions(self):
|
||||
if self.info.element_already_exists():
|
||||
raise exceptions.RefactoringError(
|
||||
'Element <%s> already exists.' % self.name)
|
||||
if not self.info.primary_is_found():
|
||||
raise exceptions.RefactoringError(
|
||||
'Cannot determine the scope <%s> should be defined in.' %
|
||||
self.name)
|
||||
|
||||
def get_changes(self):
|
||||
changes = change.ChangeSet('Generate %s <%s>' %
|
||||
(self._get_element_kind(), self.name))
|
||||
indents = self.info.get_scope_indents()
|
||||
blanks = self.info.get_blank_lines()
|
||||
base_definition = sourceutils.fix_indentation(self._get_element(),
|
||||
indents)
|
||||
definition = '\n' * blanks[0] + base_definition + '\n' * blanks[1]
|
||||
|
||||
resource = self.info.get_insertion_resource()
|
||||
start, end = self.info.get_insertion_offsets()
|
||||
|
||||
collector = codeanalyze.ChangeCollector(resource.read())
|
||||
collector.add_change(start, end, definition)
|
||||
changes.add_change(change.ChangeContents(
|
||||
resource, collector.get_changed()))
|
||||
return changes
|
||||
|
||||
def get_location(self):
|
||||
return (self.info.get_insertion_resource(),
|
||||
self.info.get_insertion_lineno())
|
||||
|
||||
def _get_element_kind(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
def _get_element(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class GenerateFunction(_Generate):
|
||||
|
||||
def _generate_info(self, project, resource, offset):
|
||||
return _FunctionGenerationInfo(project.pycore, resource, offset)
|
||||
|
||||
def _get_element(self):
|
||||
decorator = ''
|
||||
args = []
|
||||
if self.info.is_static_method():
|
||||
decorator = '@staticmethod\n'
|
||||
if self.info.is_method() or self.info.is_constructor() or \
|
||||
self.info.is_instance():
|
||||
args.append('self')
|
||||
args.extend(self.info.get_passed_args())
|
||||
definition = '%sdef %s(%s):\n pass\n' % (decorator, self.name,
|
||||
', '.join(args))
|
||||
return definition
|
||||
|
||||
def _get_element_kind(self):
|
||||
return 'Function'
|
||||
|
||||
|
||||
class GenerateVariable(_Generate):
|
||||
|
||||
def _get_element(self):
|
||||
return '%s = None\n' % self.name
|
||||
|
||||
def _get_element_kind(self):
|
||||
return 'Variable'
|
||||
|
||||
|
||||
class GenerateClass(_Generate):
|
||||
|
||||
def _get_element(self):
|
||||
return 'class %s(object):\n pass\n' % self.name
|
||||
|
||||
def _get_element_kind(self):
|
||||
return 'Class'
|
||||
|
||||
|
||||
class GenerateModule(_Generate):
|
||||
|
||||
def get_changes(self):
|
||||
package = self.info.get_package()
|
||||
changes = change.ChangeSet('Generate Module <%s>' % self.name)
|
||||
new_resource = self.project.get_file('%s/%s.py' %
|
||||
(package.path, self.name))
|
||||
if new_resource.exists():
|
||||
raise exceptions.RefactoringError(
|
||||
'Module <%s> already exists' % new_resource.path)
|
||||
changes.add_change(change.CreateResource(new_resource))
|
||||
changes.add_change(_add_import_to_module(
|
||||
self.project, self.resource, new_resource))
|
||||
return changes
|
||||
|
||||
def get_location(self):
|
||||
package = self.info.get_package()
|
||||
return (package.get_child('%s.py' % self.name), 1)
|
||||
|
||||
|
||||
class GeneratePackage(_Generate):
|
||||
|
||||
def get_changes(self):
|
||||
package = self.info.get_package()
|
||||
changes = change.ChangeSet('Generate Package <%s>' % self.name)
|
||||
new_resource = self.project.get_folder('%s/%s' %
|
||||
(package.path, self.name))
|
||||
if new_resource.exists():
|
||||
raise exceptions.RefactoringError(
|
||||
'Package <%s> already exists' % new_resource.path)
|
||||
changes.add_change(change.CreateResource(new_resource))
|
||||
changes.add_change(_add_import_to_module(
|
||||
self.project, self.resource, new_resource))
|
||||
child = self.project.get_folder(package.path + '/' + self.name)
|
||||
changes.add_change(change.CreateFile(child, '__init__.py'))
|
||||
return changes
|
||||
|
||||
def get_location(self):
|
||||
package = self.info.get_package()
|
||||
child = package.get_child(self.name)
|
||||
return (child.get_child('__init__.py'), 1)
|
||||
|
||||
|
||||
def _add_import_to_module(project, resource, imported):
|
||||
pymodule = project.get_pymodule(resource)
|
||||
import_tools = importutils.ImportTools(project)
|
||||
module_imports = import_tools.module_imports(pymodule)
|
||||
module_name = libutils.modname(imported)
|
||||
new_import = importutils.NormalImport(((module_name, None), ))
|
||||
module_imports.add_import(new_import)
|
||||
return change.ChangeContents(resource, module_imports.get_changed_source())
|
||||
|
||||
|
||||
class _GenerationInfo(object):
|
||||
|
||||
def __init__(self, pycore, resource, offset):
|
||||
self.pycore = pycore
|
||||
self.resource = resource
|
||||
self.offset = offset
|
||||
self.source_pymodule = self.pycore.project.get_pymodule(resource)
|
||||
finder = rope.base.evaluate.ScopeNameFinder(self.source_pymodule)
|
||||
self.primary, self.pyname = finder.get_primary_and_pyname_at(offset)
|
||||
self._init_fields()
|
||||
|
||||
def _init_fields(self):
|
||||
self.source_scope = self._get_source_scope()
|
||||
self.goal_scope = self._get_goal_scope()
|
||||
self.goal_pymodule = self._get_goal_module(self.goal_scope)
|
||||
|
||||
def _get_goal_scope(self):
|
||||
if self.primary is None:
|
||||
return self._get_source_scope()
|
||||
pyobject = self.primary.get_object()
|
||||
if isinstance(pyobject, pyobjects.PyDefinedObject):
|
||||
return pyobject.get_scope()
|
||||
elif isinstance(pyobject.get_type(), pyobjects.PyClass):
|
||||
return pyobject.get_type().get_scope()
|
||||
|
||||
def _get_goal_module(self, scope):
|
||||
if scope is None:
|
||||
return
|
||||
while scope.parent is not None:
|
||||
scope = scope.parent
|
||||
return scope.pyobject
|
||||
|
||||
def _get_source_scope(self):
|
||||
module_scope = self.source_pymodule.get_scope()
|
||||
lineno = self.source_pymodule.lines.get_line_number(self.offset)
|
||||
return module_scope.get_inner_scope_for_line(lineno)
|
||||
|
||||
def get_insertion_lineno(self):
|
||||
lines = self.goal_pymodule.lines
|
||||
if self.goal_scope == self.source_scope:
|
||||
line_finder = self.goal_pymodule.logical_lines
|
||||
lineno = lines.get_line_number(self.offset)
|
||||
lineno = line_finder.logical_line_in(lineno)[0]
|
||||
root = suites.ast_suite_tree(self.goal_scope.pyobject.get_ast())
|
||||
suite = root.find_suite(lineno)
|
||||
indents = sourceutils.get_indents(lines, lineno)
|
||||
while self.get_scope_indents() < indents:
|
||||
lineno = suite.get_start()
|
||||
indents = sourceutils.get_indents(lines, lineno)
|
||||
suite = suite.parent
|
||||
return lineno
|
||||
else:
|
||||
return min(self.goal_scope.get_end() + 1, lines.length())
|
||||
|
||||
def get_insertion_resource(self):
|
||||
return self.goal_pymodule.get_resource()
|
||||
|
||||
def get_insertion_offsets(self):
|
||||
if self.goal_scope.get_kind() == 'Class':
|
||||
start, end = sourceutils.get_body_region(self.goal_scope.pyobject)
|
||||
if self.goal_pymodule.source_code[start:end].strip() == 'pass':
|
||||
return start, end
|
||||
lines = self.goal_pymodule.lines
|
||||
start = lines.get_line_start(self.get_insertion_lineno())
|
||||
return (start, start)
|
||||
|
||||
def get_scope_indents(self):
|
||||
if self.goal_scope.get_kind() == 'Module':
|
||||
return 0
|
||||
return sourceutils.get_indents(self.goal_pymodule.lines,
|
||||
self.goal_scope.get_start()) + 4
|
||||
|
||||
def get_blank_lines(self):
|
||||
if self.goal_scope.get_kind() == 'Module':
|
||||
base_blanks = 2
|
||||
if self.goal_pymodule.source_code.strip() == '':
|
||||
base_blanks = 0
|
||||
if self.goal_scope.get_kind() == 'Class':
|
||||
base_blanks = 1
|
||||
if self.goal_scope.get_kind() == 'Function':
|
||||
base_blanks = 0
|
||||
if self.goal_scope == self.source_scope:
|
||||
return (0, base_blanks)
|
||||
return (base_blanks, 0)
|
||||
|
||||
def get_package(self):
|
||||
primary = self.primary
|
||||
if self.primary is None:
|
||||
return self.pycore.project.get_source_folders()[0]
|
||||
if isinstance(primary.get_object(), pyobjects.PyPackage):
|
||||
return primary.get_object().get_resource()
|
||||
raise exceptions.RefactoringError(
|
||||
'A module/package can be only created in a package.')
|
||||
|
||||
def primary_is_found(self):
|
||||
return self.goal_scope is not None
|
||||
|
||||
def element_already_exists(self):
|
||||
if self.pyname is None or isinstance(self.pyname, pynames.UnboundName):
|
||||
return False
|
||||
return self.get_name() in self.goal_scope.get_defined_names()
|
||||
|
||||
def get_name(self):
|
||||
return worder.get_name_at(self.resource, self.offset)
|
||||
|
||||
|
||||
class _FunctionGenerationInfo(_GenerationInfo):
|
||||
|
||||
def _get_goal_scope(self):
|
||||
if self.is_constructor():
|
||||
return self.pyname.get_object().get_scope()
|
||||
if self.is_instance():
|
||||
return self.pyname.get_object().get_type().get_scope()
|
||||
if self.primary is None:
|
||||
return self._get_source_scope()
|
||||
pyobject = self.primary.get_object()
|
||||
if isinstance(pyobject, pyobjects.PyDefinedObject):
|
||||
return pyobject.get_scope()
|
||||
elif isinstance(pyobject.get_type(), pyobjects.PyClass):
|
||||
return pyobject.get_type().get_scope()
|
||||
|
||||
def element_already_exists(self):
|
||||
if self.pyname is None or isinstance(self.pyname, pynames.UnboundName):
|
||||
return False
|
||||
return self.get_name() in self.goal_scope.get_defined_names()
|
||||
|
||||
def is_static_method(self):
|
||||
return self.primary is not None and \
|
||||
isinstance(self.primary.get_object(), pyobjects.PyClass)
|
||||
|
||||
def is_method(self):
|
||||
return self.primary is not None and \
|
||||
isinstance(self.primary.get_object().get_type(), pyobjects.PyClass)
|
||||
|
||||
def is_constructor(self):
|
||||
return self.pyname is not None and \
|
||||
isinstance(self.pyname.get_object(), pyobjects.PyClass)
|
||||
|
||||
def is_instance(self):
|
||||
if self.pyname is None:
|
||||
return False
|
||||
pyobject = self.pyname.get_object()
|
||||
return isinstance(pyobject.get_type(), pyobjects.PyClass)
|
||||
|
||||
def get_name(self):
|
||||
if self.is_constructor():
|
||||
return '__init__'
|
||||
if self.is_instance():
|
||||
return '__call__'
|
||||
return worder.get_name_at(self.resource, self.offset)
|
||||
|
||||
def get_passed_args(self):
|
||||
result = []
|
||||
source = self.source_pymodule.source_code
|
||||
finder = worder.Worder(source)
|
||||
if finder.is_a_function_being_called(self.offset):
|
||||
start, end = finder.get_primary_range(self.offset)
|
||||
parens_start, parens_end = finder.get_word_parens_range(end - 1)
|
||||
call = source[start:parens_end]
|
||||
parser = functionutils._FunctionParser(call, False)
|
||||
args, keywords = parser.get_parameters()
|
||||
for arg in args:
|
||||
if self._is_id(arg):
|
||||
result.append(arg)
|
||||
else:
|
||||
result.append('arg%d' % len(result))
|
||||
for name, value in keywords:
|
||||
result.append(name)
|
||||
return result
|
||||
|
||||
def _is_id(self, arg):
|
||||
def id_or_underline(c):
|
||||
return c.isalpha() or c == '_'
|
||||
for c in arg:
|
||||
if not id_or_underline(c) and not c.isdigit():
|
||||
return False
|
||||
return id_or_underline(arg[0])
|
||||
Loading…
Add table
Add a link
Reference in a new issue