17 Commits

Author SHA1 Message Date
Dave Halter
fc6202ffb3 Rename _get_definition to get_definition. 2017-09-03 00:42:06 +02:00
Dave Halter
0e201810fa Remove the old get_definition function. 2017-09-03 00:41:39 +02:00
Dave Halter
7a9739c5d6 Add an option to the new get_definition. 2017-09-02 23:45:50 +02:00
Dave Halter
1bf9ca94bb A small docstring. 2017-09-02 22:41:15 +02:00
Dave Halter
a4d28d2eda Rework is_definition. 2017-09-02 17:26:29 +02:00
Dave Halter
36ddbaddf4 Remove a note about not parsing f-strings. 2017-09-02 14:13:47 +02:00
Dave Halter
b944fb9145 Fix a test around rb/br string literals. 2017-09-02 14:09:15 +02:00
Dave Halter
7a85409da7 Get rid of one more isinstance. 2017-09-02 13:59:21 +02:00
Dave Halter
e79c0755eb iter_return_stmts should also return return statements without a value. 2017-09-02 12:32:32 +02:00
Dave Halter
9ab5937a3c Fix yield scanning. 2017-09-01 18:03:47 +02:00
Dave Halter
d3e58955a9 iter_yield_expr should not work correctly. 2017-09-01 09:34:01 +02:00
Dave Halter
a21ec2c0ad Better yield/yield from support. 2017-09-01 01:06:07 +02:00
Dave Halter
910a660c6f Fix some todos. 2017-09-01 00:35:22 +02:00
Dave Halter
68fa70b959 future_import_names is not public, at the moment. 2017-09-01 00:25:32 +02:00
Dave Halter
fa0bf4951c Fix string prefixes for Python2. 2017-09-01 00:20:24 +02:00
Dave Halter
ba2c0ad41a Bump version. 2017-08-31 22:44:15 +02:00
Dave Halter
4b32408001 Finally get a release out with this script. 2017-08-31 22:41:28 +02:00
10 changed files with 190 additions and 78 deletions

View File

@@ -41,7 +41,6 @@ Future
Known Issues Known Issues
============ ============
- Python3.6's f-strings are not parsed, yet. This means no errors are found in them.
- `async`/`await` are already used as keywords in Python3.6. - `async`/`await` are already used as keywords in Python3.6.
- `from __future__ import print_function` is not supported, - `from __future__ import print_function` is not supported,

View File

@@ -5,45 +5,48 @@
set -eu -o pipefail set -eu -o pipefail
BASE_DIR=$(dirname $(readlink -f "$0")) BASE_DIR=$(dirname $(readlink -f "$0"))
cd $(BASE_DIR) cd $BASE_DIR
git fetch --tags
PROJECT_NAME=parso PROJECT_NAME=parso
BRANCH=master BRANCH=master
BUILD_FOLDER=build BUILD_FOLDER=build
FOLDER=$BUILD_FOLDER/$PROJECT_NAME
# Test first.
#tox
[ -d $BUILD_FOLDER ] || mkdir $BUILD_FOLDER [ -d $BUILD_FOLDER ] || mkdir $BUILD_FOLDER
# Remove the previous deployment first. # Remove the previous deployment first.
rm -rf $FOLDER
# Checkout the right branch # Checkout the right branch
cd $BUILD_FOLDER cd $BUILD_FOLDER
rm -rf $PROJECT_NAME
git clone .. $PROJECT_NAME git clone .. $PROJECT_NAME
cd $PROJECT_NAME
git checkout $BRANCH git checkout $BRANCH
# Test first.
tox
# Create tag # Create tag
tag=v$(python -c "import $PROJECT_NAME; print($PROJECT_NAME.__version__)") tag=v$(python -c "import $PROJECT_NAME; print($PROJECT_NAME.__version__)")
master_ref=$(git show-ref -s $BRANCH) master_ref=$(git show-ref -s heads/$BRANCH)
tag_ref=$(git show-ref -s $tag | true) tag_ref=$(git show-ref -s $tag || true)
if [ $tag_ref ]; then if [[ $tag_ref ]]; then
if [ $tag_ref != $master_ref ]; then if [[ $tag_ref != $master_ref ]]; then
echo 'Cannot tag something that has already been tagged with another commit.' echo 'Cannot tag something that has already been tagged with another commit.'
exit 1 exit 1
fi fi
else else
git tag $BRANCH git tag $tag
git push --tags git push --tags
fi fi
# Package and upload to PyPI # Package and upload to PyPI
rm -rf dist/ #rm -rf dist/ - Not needed anymore, because the folder is never reused.
echo `pwd`
python setup.py sdist bdist_wheel python setup.py sdist bdist_wheel
# Maybe do a pip install twine before. # Maybe do a pip install twine before.
twine upload dist/* twine upload dist/*
cd $(BASE_DIR) cd $BASE_DIR
# Back in the development directory fetch tags. # Back in the development directory fetch tags.
git fetch --tags git fetch --tags

View File

@@ -23,7 +23,7 @@ from parso.parser import ParserSyntaxError
from parso.grammar import Grammar, load_grammar from parso.grammar import Grammar, load_grammar
__version__ = '0.0.3' __version__ = '0.0.4'
def parse(code=None, **kwargs): def parse(code=None, **kwargs):

View File

@@ -62,17 +62,18 @@ def maybe(*choices):
# Return the empty string, plus all of the valid string prefixes. # Return the empty string, plus all of the valid string prefixes.
def _all_string_prefixes(version_info): def _all_string_prefixes(version_info):
def different_case_versions(prefix):
for s in _itertools.product(*[(c, c.upper()) for c in prefix]):
yield ''.join(s)
# The valid string prefixes. Only contain the lower case versions, # The valid string prefixes. Only contain the lower case versions,
# and don't contain any permuations (include 'fr', but not # and don't contain any permuations (include 'fr', but not
# 'rf'). The various permutations will be generated. # 'rf'). The various permutations will be generated.
_valid_string_prefixes = ['b', 'r', 'u', 'br'] _valid_string_prefixes = ['b', 'r', 'u']
if version_info >= (3, 0):
_valid_string_prefixes.append('br')
if version_info >= (3, 6): if version_info >= (3, 6):
_valid_string_prefixes += ['f', 'fr'] _valid_string_prefixes += ['f', 'fr']
if version_info <= (2, 7):
# TODO this is actually not 100% valid. ur is valid in Python 2.7,
# while ru is not.
# TODO rb is also not valid.
_valid_string_prefixes.append('ur')
# if we add binary f-strings, add: ['fb', 'fbr'] # if we add binary f-strings, add: ['fb', 'fbr']
result = set(['']) result = set([''])
@@ -80,8 +81,11 @@ def _all_string_prefixes(version_info):
for t in _itertools.permutations(prefix): for t in _itertools.permutations(prefix):
# create a list with upper and lower versions of each # create a list with upper and lower versions of each
# character # character
for s in _itertools.product(*[(c, c.upper()) for c in t]): result.update(different_case_versions(t))
result.add(''.join(s)) if version_info <= (2, 7):
# In Python 2 the order cannot just be random.
result.update(different_case_versions('ur'))
result.update(different_case_versions('br'))
return result return result

View File

@@ -32,6 +32,17 @@ from parso.tree import Node, BaseNode, Leaf, ErrorNode, ErrorLeaf, \
search_ancestor search_ancestor
from parso.python.prefix import split_prefix from parso.python.prefix import split_prefix
_FLOW_CONTAINERS = set(['if_stmt', 'while_stmt', 'for_stmt', 'try_stmt',
'with_stmt', 'async_stmt', 'suite'])
_RETURN_STMT_CONTAINERS = set(['suite', 'simple_stmt']) | _FLOW_CONTAINERS
_FUNC_CONTAINERS = set(['suite', 'simple_stmt', 'decorated']) | _FLOW_CONTAINERS
_GET_DEFINITION_TYPES = set([
'expr_stmt', 'comp_for', 'with_stmt', 'for_stmt', 'import_name',
'import_from', 'param'
])
_IMPORTS = set(['import_name', 'import_from'])
class DocstringMixin(object): class DocstringMixin(object):
__slots__ = () __slots__ = ()
@@ -42,7 +53,7 @@ class DocstringMixin(object):
""" """
if self.type == 'file_input': if self.type == 'file_input':
node = self.children[0] node = self.children[0]
elif isinstance(self, ClassOrFunc): elif self.type in ('funcdef', 'classdef'):
node = self.children[self.children.index(':') + 1] node = self.children[self.children.index(':') + 1]
if node.type == 'suite': # Normally a suite if node.type == 'suite': # Normally a suite
node = node.children[1] # -> NEWLINE stmt node = node.children[1] # -> NEWLINE stmt
@@ -67,24 +78,6 @@ class PythonMixin(object):
""" """
__slots__ = () __slots__ = ()
def get_definition(self):
if self.type in ('newline', 'endmarker'):
raise ValueError('Cannot get the indentation of whitespace or indentation.')
scope = self
while scope.parent is not None:
parent = scope.parent
if isinstance(scope, (PythonNode, PythonLeaf)) and parent.type not in ('simple_stmt', 'file_input'):
if scope.type == 'testlist_comp':
try:
if scope.children[1].type == 'comp_for':
return scope.children[1]
except IndexError:
pass
scope = parent
else:
break
return scope
def get_name_of_position(self, position): def get_name_of_position(self, position):
for c in self.children: for c in self.children:
if isinstance(c, Leaf): if isinstance(c, Leaf):
@@ -173,21 +166,55 @@ class Name(_LeafWithoutNewlines):
self.line, self.column) self.line, self.column)
def is_definition(self): def is_definition(self):
if self.parent.type in ('power', 'atom_expr'): """
# In `self.x = 3` self is not a definition, but x is. Returns True if the name is being defined.
return False """
return self.get_definition() is not None
def get_definition(self, import_name_always=False):
"""
Returns None if there's on definition for a name.
:param import_name_alway: Specifies if an import name is always a
definition. Normally foo in `from foo import bar` is not a
definition.
"""
node = self.parent
type_ = node.type
if type_ in ('power', 'atom_expr'):
# In `self.x = 3` self is not a definition, but x is.
return None
if type_ in ('funcdef', 'classdef'):
if self == node.name:
return node
return None
if type_ in ():
if self in node.get_defined_names():
return node
return None
if type_ == 'except_clause':
# TODO in Python 2 this doesn't work correctly. See grammar file.
# I think we'll just let it be. Python 2 will be gone in a few
# years.
if self.get_previous_sibling() == 'as':
return node.parent # The try_stmt.
return None
while node is not None:
if node.type == 'suite':
return None
if node.type in _GET_DEFINITION_TYPES:
if self in node.get_defined_names():
return node
if import_name_always and node.type in _IMPORTS:
return node
return None
node = node.parent
return None
stmt = self.get_definition()
if stmt.type in ('funcdef', 'classdef', 'param'):
return self == stmt.name
elif stmt.type == 'for_stmt':
return self.start_pos < stmt.children[2].start_pos
elif stmt.type == 'try_stmt':
return self.get_previous_sibling() == 'as'
else:
return stmt.type in ('expr_stmt', 'import_name', 'import_from',
'comp_for', 'with_stmt') \
and self in stmt.get_defined_names()
class Literal(PythonLeaf): class Literal(PythonLeaf):
@@ -279,8 +306,7 @@ class Scope(PythonBaseNode, DocstringMixin):
for element in children: for element in children:
if element.type in names: if element.type in names:
yield element yield element
if element.type in ('suite', 'simple_stmt', 'decorated') \ if element.type in _FUNC_CONTAINERS:
or isinstance(element, Flow):
for e in scan(element.children): for e in scan(element.children):
yield e yield e
@@ -315,14 +341,14 @@ class Module(Scope):
super(Module, self).__init__(children) super(Module, self).__init__(children)
self._used_names = None self._used_names = None
def iter_future_import_names(self): def _iter_future_import_names(self):
""" """
:return list of str: A list of future import names. :return list of str: A list of future import names.
""" """
# TODO this is a strange scan and not fully correct. I think Python's # In Python it's not allowed to use future imports after the first
# parser does it in a different way and scans for the first # actual (non-future) statement. However this is not a linter here,
# statement/import with a tokenizer (to check for syntax changes like # just return all future imports. If people want to scan for issues
# the future print statement). # they should use the API.
for imp in self.iter_imports(): for imp in self.iter_imports():
if imp.type == 'import_from' and imp.level == 0: if imp.type == 'import_from' and imp.level == 0:
for path in imp.get_paths(): for path in imp.get_paths():
@@ -330,13 +356,14 @@ class Module(Scope):
if len(names) == 2 and names[0] == '__future__': if len(names) == 2 and names[0] == '__future__':
yield names[1] yield names[1]
def has_explicit_absolute_import(self): def _has_explicit_absolute_import(self):
""" """
Checks if imports in this module are explicitly absolute, i.e. there Checks if imports in this module are explicitly absolute, i.e. there
is a ``__future__`` import. is a ``__future__`` import.
Currently not public, might be in the future.
:return bool: :return bool:
""" """
for name in self.iter_future_import_names(): for name in self._iter_future_import_names():
if name == 'absolute_import': if name == 'absolute_import':
return True return True
return False return False
@@ -518,14 +545,39 @@ class Function(ClassOrFunc):
""" """
Returns a generator of `yield_expr`. Returns a generator of `yield_expr`.
""" """
# TODO This is incorrect, yields are also possible in a statement. def scan(children):
return self._search_in_scope('yield_expr') for element in children:
if element.type in ('classdef', 'funcdef', 'lambdef'):
continue
try:
nested_children = element.children
except AttributeError:
if element.value == 'yield':
if element.parent.type == 'yield_expr':
yield element.parent
else:
yield element
else:
for result in scan(nested_children):
yield result
return scan(self.children)
def iter_return_stmts(self): def iter_return_stmts(self):
""" """
Returns a generator of `return_stmt`. Returns a generator of `return_stmt`.
""" """
return self._search_in_scope('return_stmt') def scan(children):
for element in children:
if element.type == 'return_stmt' \
or element.type == 'keyword' and element.value == 'return':
yield element
if element.type in _RETURN_STMT_CONTAINERS:
for e in scan(element.children):
yield e
return scan(self.children)
def is_generator(self): def is_generator(self):
""" """
@@ -651,6 +703,9 @@ class ForStmt(Flow):
""" """
return self.children[3] return self.children[3]
def get_defined_names(self):
return _defined_names(self.children[1])
class TryStmt(Flow): class TryStmt(Flow):
type = 'try_stmt' type = 'try_stmt'
@@ -662,7 +717,6 @@ class TryStmt(Flow):
Returns ``[None]`` for except clauses without an exception given. Returns ``[None]`` for except clauses without an exception given.
""" """
for node in self.children: for node in self.children:
# TODO this is not correct. We're not returning an except clause.
if node.type == 'except_clause': if node.type == 'except_clause':
yield node.children[1] yield node.children[1]
elif node == 'except': elif node == 'except':
@@ -685,8 +739,7 @@ class WithStmt(Flow):
names += _defined_names(with_item.children[2]) names += _defined_names(with_item.children[2])
return names return names
def get_context_manager_from_name(self, name): def get_test_node_from_name(self, name):
# TODO Replace context_manager with test?
node = name.parent node = name.parent
if node.type != 'with_item': if node.type != 'with_item':
raise ValueError('The name is not actually part of a with statement.') raise ValueError('The name is not actually part of a with statement.')
@@ -1025,6 +1078,9 @@ class Param(PythonBaseNode):
else: else:
return self._tfpdef() return self._tfpdef()
def get_defined_names(self):
return [self.name]
@property @property
def position_index(self): def position_index(self):
""" """

View File

@@ -2,7 +2,7 @@
addopts = --doctest-modules addopts = --doctest-modules
# Ignore broken files inblackbox test directories # Ignore broken files inblackbox test directories
norecursedirs = .* docs scripts normalizer_issue_files norecursedirs = .* docs scripts normalizer_issue_files build
# Activate `clean_jedi_cache` fixture for all tests. This should be # Activate `clean_jedi_cache` fixture for all tests. This should be
# fine as long as we are using `clean_jedi_cache` as a session scoped # fine as long as we are using `clean_jedi_cache` as a session scoped

View File

@@ -10,14 +10,14 @@ def test_explicit_absolute_imports():
Detect modules with ``from __future__ import absolute_import``. Detect modules with ``from __future__ import absolute_import``.
""" """
module = parse("from __future__ import absolute_import") module = parse("from __future__ import absolute_import")
assert module.has_explicit_absolute_import() assert module._has_explicit_absolute_import()
def test_no_explicit_absolute_imports(): def test_no_explicit_absolute_imports():
""" """
Detect modules without ``from __future__ import absolute_import``. Detect modules without ``from __future__ import absolute_import``.
""" """
assert not parse("1").has_explicit_absolute_import() assert not parse("1")._has_explicit_absolute_import()
def test_dont_break_imports_without_namespaces(): def test_dont_break_imports_without_namespaces():
@@ -26,4 +26,4 @@ def test_dont_break_imports_without_namespaces():
assume that all imports have non-``None`` namespaces. assume that all imports have non-``None`` namespaces.
""" """
src = "from __future__ import absolute_import\nimport xyzzy" src = "from __future__ import absolute_import\nimport xyzzy"
assert parse(src).has_explicit_absolute_import() assert parse(src)._has_explicit_absolute_import()

View File

@@ -115,3 +115,37 @@ def test_ellipsis_py2(each_py2_version):
subscript = trailer.children[1] subscript = trailer.children[1]
assert subscript.type == 'subscript' assert subscript.type == 'subscript'
assert [leaf.value for leaf in subscript.children] == ['.', '.', '.'] assert [leaf.value for leaf in subscript.children] == ['.', '.', '.']
def get_yield_exprs(code, version):
return list(parse(code, version=version).children[0].iter_yield_exprs())
def get_return_stmts(code):
return list(parse(code).children[0].iter_return_stmts())
def test_yields(each_version):
y, = get_yield_exprs('def x(): yield', each_version)
assert y.value == 'yield'
assert y.type == 'keyword'
y, = get_yield_exprs('def x(): (yield 1)', each_version)
assert y.type == 'yield_expr'
y, = get_yield_exprs('def x(): [1, (yield)]', each_version)
assert y.type == 'keyword'
def test_yield_from():
y, = get_yield_exprs('def x(): (yield from 1)', '3.3')
assert y.type == 'yield_expr'
def test_returns():
r, = get_return_stmts('def x(): return')
assert r.value == 'return'
assert r.type == 'keyword'
r, = get_return_stmts('def x(): return 1')
assert r.type == 'return_stmt'

View File

@@ -254,3 +254,19 @@ def test_multiline_str_literals(each_version):
def test_py2_backticks(works_in_py2): def test_py2_backticks(works_in_py2):
works_in_py2.parse("`1`") works_in_py2.parse("`1`")
def test_py2_string_prefixes(works_in_py2):
works_in_py2.parse("ur'1'")
works_in_py2.parse("Ur'1'")
works_in_py2.parse("UR'1'")
_invalid_syntax("ru'1'", works_in_py2.version)
def py_br(each_version):
_parse('br""', each_version)
def test_py3_rb(works_ge_py3):
works_ge_py3.parse("rb'1'")
works_ge_py3.parse("RB'1'")

View File

@@ -172,9 +172,9 @@ def test_ur_literals():
check('Ur""', is_literal=not py_version >= 30) check('Ur""', is_literal=not py_version >= 30)
check('UR""', is_literal=not py_version >= 30) check('UR""', is_literal=not py_version >= 30)
check('bR""') check('bR""')
# Starting with Python 3.3 this ordering is also possible, but we just # Starting with Python 3.3 this ordering is also possible.
# enable it for all versions. It doesn't hurt. if py_version >= 33:
check('Rb""') check('Rb""')
# Starting with Python 3.6 format strings where introduced. # Starting with Python 3.6 format strings where introduced.
check('fr""', is_literal=py_version >= 36) check('fr""', is_literal=py_version >= 36)
check('rF""', is_literal=py_version >= 36) check('rF""', is_literal=py_version >= 36)