69 Commits

Author SHA1 Message Date
Dave Halter
a9d0cc1179 Release parso 0.8.1 2020-12-10 16:09:05 +01:00
Dave Halter
f45ffa1948 Merge pull request #162 from pybpc/walrus-set-and-index
Allow unparenthesized walrus in set literals, set comprehensions and indexes
2020-12-10 15:27:56 +01:00
gousaiyang
b287476366 Allow unparenthesized walrus in set literals, set comprehensions and indexes 2020-11-27 14:46:54 -08:00
Tim Hatch
d39aadc4cc Support named unicode characters in f-strings (#160)
* Support named unicode characters in f-strings

Fixes #154

The previous behavior misinterpreted the curly braces as enclosing an
expression.  This change does some cursory validation so we can still
get parse errors in the most egregious cases, but does not validate that
the names are actually valid, only that they are name-shaped and have a
chance of being valid.

The character names appear to obey a few rules:
* Case insensitive
* Name characters are `[A-Z0-9 \-]`
* Whitespace before or after is not allowed
* Whitespace in the middle may only be a single space between words
* Dashes may occur at the start or middle of a word

```py
f"\N{A B}"           # might be legal
f"\N{a b}"           # equivalent to above
f"\N{A     B}"       # no way
f"\N{    A B     }"  # no way
f"""\N{A
B}"""                # no way
```

For confirming this regex matches all (current) unicode character names:

```py
import re
import sys
import unicodedata

R = re.compile(r"[A-Za-z0-9\-]+(?: [A-Za-z0-9\-]+)*")

for i in range(sys.maxunicode):
    try:
        name = unicodedata.name(chr(i))
    except ValueError:
        # Some small values like 0 and 1 have no name, /shrug
        continue
    m = R.fullmatch(name)
    if m is None:
        print("FAIL", repr(name))
```

* Improve tests for named unicode escapes
2020-11-22 15:37:04 +03:00
Saiyang Gou
b08b61b578 Allow some unparenthesized syntactic structures in f-string expression part (#159)
Resolves #157, #158
2020-11-19 16:32:59 +03:00
Saiyang Gou
034a9e8944 Properly check for invalid conversion character with f-string debugging syntax (#156) 2020-11-18 12:56:04 +03:00
Dave Halter
634df56d90 Merge pull request #152 from isidentical/issue-151
Retrieve all kinds of assignment targets from with test
2020-09-29 00:14:19 +02:00
Batuhan Taskaya
52cfa5a8ac satisfy flake8 2020-09-24 10:48:22 +03:00
Batuhan Taskaya
606c528803 Retrieve all kinds of assignment targets from with test 2020-09-24 10:42:56 +03:00
Dave Halter
6ae0efa415 Prepare the 0.8.0 release 2020-08-05 00:51:16 +02:00
Dave Halter
1714c1d0de Make namedexpr_test a proper NamedExpr class 2020-08-05 00:26:16 +02:00
Dave Halter
6405a1227f Merge pull request #146 from davidhalter/python3
Dropping Python <3.6
2020-07-26 13:30:32 +02:00
Dave Halter
cb7d15b332 Prepare the CHANGELOG 2020-07-26 13:19:41 +02:00
Dave Halter
0dec1a4003 Another review suggestion 2020-07-26 13:16:41 +02:00
Dave Halter
020d7a9acb Use a better intersphinx mapping 2020-07-26 13:16:41 +02:00
Dave Halter
f79432ecab Remove support for multiple languages, which was never used 2020-07-26 13:16:41 +02:00
Dave Halter
b0de7e363a Don't use print if not necessary 2020-07-26 13:16:41 +02:00
Dave Halter
f6859538b0 Simplify a cache path
Co-authored-by: Batuhan Taskaya <batuhanosmantaskaya@gmail.com>
2020-07-26 13:03:58 +02:00
Dave Halter
ea6b01b968 Use pathlib.Path instead of strings 2020-07-26 01:19:41 +02:00
Dave Halter
97c10facf7 Remove super arguments 2020-07-25 23:54:21 +02:00
Dave Halter
dcc756a373 Remove object inheritance 2020-07-25 18:20:56 +02:00
Dave Halter
3c3c0b54dc Fix some remaining flake8 issues 2020-07-25 18:17:11 +02:00
Dave Halter
70ec8eecd1 Fix some last mypy issues 2020-07-25 18:16:01 +02:00
Dave Halter
d3c274afa0 Fix an issue with encoding detection 2020-07-25 18:11:49 +02:00
Dave Halter
6a4bb35d80 Removed stubs from generator and grammar_parser and put the annotations into the corresponding files 2020-07-25 18:09:12 +02:00
Dave Halter
ce0fac7630 Move grammar stubs into grammar.py 2020-07-25 17:48:01 +02:00
Dave Halter
395af26fa8 Removed parso/__init__.pyi
It is intentional that the parse function is not typed. It's just a
helper function. For a typed version of it, please have a look at
Grammar.parse.
2020-07-25 16:22:35 +02:00
Dave Halter
e65fb2464e Remove utils.pyi in favor of inline stubs 2020-07-25 16:20:26 +02:00
Dave Halter
4d86f0fdcc Remove the pgen2 stub, didn't really contain anything 2020-07-25 16:12:41 +02:00
Dave Halter
b816c00e77 Remove the token stub in favor of inline annotations 2020-07-25 16:12:04 +02:00
Dave Halter
2197e4c9e8 Fix a linter issue 2020-07-25 15:59:33 +02:00
Dave Halter
b176ed6eee Run mypy and flake8 in CI 2020-07-25 15:54:55 +02:00
Dave Halter
67904f4d24 Make mypy happy 2020-07-25 15:43:28 +02:00
Dave Halter
8a34245239 Get rid of mypy issues with tokenize.py 2020-07-25 15:34:29 +02:00
Dave Halter
a474895764 Start working with mypy 2020-07-25 15:05:42 +02:00
Dave Halter
34152d29b2 Initializing a Grammar now uses keyword only arguments 2020-07-25 14:33:42 +02:00
Dave Halter
d9f60b3473 Remove compatibility in parser for Python 2 2020-07-25 02:38:46 +02:00
Dave Halter
75b467e681 Some more small Python 3 changes 2020-07-25 02:33:24 +02:00
Dave Halter
02eb9b9507 Use keyword only arguments in grammar.py 2020-07-25 02:21:51 +02:00
Dave Halter
f17f94e120 Get rid of the old star checking logic 2020-07-25 02:16:02 +02:00
Dave Halter
902885656d Remove some Python 3.6 references 2020-07-25 02:10:10 +02:00
Dave Halter
4f9f193747 Remove some Python 3.5/3.4 references 2020-07-25 02:04:58 +02:00
Dave Halter
86d53add2d Remove sys.version_info usages that are no longer necessary 2020-07-25 01:53:51 +02:00
Dave Halter
22fb62336e Remove failing examples that are just Python 2 examples 2020-07-25 01:49:44 +02:00
Dave Halter
6eb6ac0bb2 Ignore Python 2 specific code in tests 2020-07-25 01:41:33 +02:00
Dave Halter
7c68ba4c45 Remove absolute import future import checking 2020-07-25 01:33:11 +02:00
Dave Halter
d7ab138864 Remove more python 2 specific code 2020-07-25 01:31:19 +02:00
Dave Halter
4c09583072 Remove Python 2 stuff from errors.py 2020-07-25 01:25:38 +02:00
Dave Halter
19f4550ced Use enum instead of our own logic 2020-07-24 17:39:49 +02:00
Dave Halter
a0662b3b3b flake8 changes 2020-07-24 16:11:06 +02:00
Dave Halter
2962517be0 Get rid of the xfails 2020-07-24 15:43:41 +02:00
Dave Halter
62b4589293 Remove tokenizer support for Python 2 2020-07-24 15:39:18 +02:00
Dave Halter
93e74efc01 Some whitespace changes 2020-07-24 14:50:01 +02:00
Dave Halter
b5e2e67a4d Remove support for parsing Python 2 2020-07-24 14:48:02 +02:00
Dave Halter
5ac4bac368 Pin the precise 3.8 version 2020-07-24 02:29:18 +02:00
Dave Halter
5dd4301235 Remove tox 2020-07-24 02:25:11 +02:00
Dave Halter
1a99fdd333 Don't run older Python versions on travis 2020-07-24 02:15:44 +02:00
Dave Halter
9c5fb1ac94 Fix the tokenizer 2020-07-24 02:14:52 +02:00
Dave Halter
7780cc1c1b Get rid of some Python 2 idiosyncrasies 2020-07-24 02:09:04 +02:00
Dave Halter
561f434f39 Use yield from where possible 2020-07-24 02:01:48 +02:00
Dave Halter
a1829ecc7f Remove the unicode compatibility function 2020-07-24 01:51:36 +02:00
Dave Halter
21f782dc34 Fix the tests 2020-07-24 01:45:31 +02:00
Dave Halter
164489cf97 Remove the u function and u literals 2020-07-24 01:39:03 +02:00
Dave Halter
020b2861df Remove some weird code 2020-07-24 01:33:34 +02:00
Dave Halter
44c0395113 Remove use_metaclass, it's no longer used 2020-07-24 01:31:52 +02:00
Dave Halter
a2fc850dc9 Remove scandir compatibility 2020-07-24 01:28:40 +02:00
Dave Halter
be5429c02c Remove utf8_repr from _compatibility.py 2020-07-24 01:26:36 +02:00
Dave Halter
736f616787 Remove FileNotFoundError and PermissionError from _compatibility.py 2020-07-24 01:24:59 +02:00
Dave Halter
b601ade90b Drop Python 2.7, 3.4 and 3.5 2020-07-24 01:21:44 +02:00
75 changed files with 1044 additions and 2057 deletions

1
.gitignore vendored
View File

@@ -1,7 +1,6 @@
*~ *~
*.sw? *.sw?
*.pyc *.pyc
.tox
.coveralls.yml .coveralls.yml
.coverage .coverage
/build/ /build/

View File

@@ -1,28 +1,31 @@
dist: xenial dist: xenial
language: python language: python
python: python:
- 2.7
- 3.4
- 3.5
- 3.6 - 3.6
- 3.7 - 3.7
- 3.8.2 - 3.8.2
- nightly - nightly
- pypy2.7-6.0
- pypy3.5-6.0
matrix: matrix:
allow_failures: allow_failures:
- python: nightly - python: nightly
include: include:
- python: 3.5 - python: 3.8
env: TOXENV=py35-coverage install:
- 'pip install .[qa]'
script:
# Ignore F401, which are unused imports. flake8 is a primitive tool and is sometimes wrong.
- 'flake8 --extend-ignore F401 parso test/*.py setup.py scripts/'
- mypy parso
- python: 3.8.2
script:
- 'pip install coverage'
- 'coverage run -m pytest'
- 'coverage report'
after_script:
- |
pip install --quiet coveralls
coveralls
install: install:
- pip install --quiet tox-travis - pip install .[testing]
script: script:
- tox - pytest
after_script:
- |
if [ "${TOXENV%-coverage}" == "$TOXENV" ]; then
pip install --quiet coveralls;
coveralls;
fi

View File

@@ -3,6 +3,23 @@
Changelog Changelog
--------- ---------
Unreleased
++++++++++
0.8.1 (2020-12-10)
++++++++++++++++++
- Various small bugfixes
0.8.0 (2020-08-05)
++++++++++++++++++
- Dropped Support for Python 2.7, 3.4, 3.5
- It's possible to use ``pathlib.Path`` objects now in the API
- The stubs are gone, we are now using annotations
- ``namedexpr_test`` nodes are now a proper class called ``NamedExpr``
- A lot of smaller refactorings
0.7.1 (2020-07-24) 0.7.1 (2020-07-24)
++++++++++++++++++ ++++++++++++++++++

View File

@@ -5,7 +5,6 @@ include AUTHORS.txt
include .coveragerc include .coveragerc
include conftest.py include conftest.py
include pytest.ini include pytest.ini
include tox.ini
include parso/python/grammar*.txt include parso/python/grammar*.txt
recursive-include test * recursive-include test *
recursive-include docs * recursive-include docs *

View File

@@ -31,7 +31,7 @@ A simple example:
.. code-block:: python .. code-block:: python
>>> import parso >>> import parso
>>> module = parso.parse('hello + 1', version="3.6") >>> module = parso.parse('hello + 1', version="3.9")
>>> expr = module.children[0] >>> expr = module.children[0]
>>> expr >>> expr
PythonNode(arith_expr, [<Name: hello@1,0>, <Operator: +>, <Number: 1>]) PythonNode(arith_expr, [<Name: hello@1,0>, <Operator: +>, <Number: 1>])

View File

@@ -2,8 +2,8 @@ import re
import tempfile import tempfile
import shutil import shutil
import logging import logging
import sys
import os import os
from pathlib import Path
import pytest import pytest
@@ -13,8 +13,7 @@ from parso.utils import parse_version_string
collect_ignore = ["setup.py"] collect_ignore = ["setup.py"]
VERSIONS_2 = '2.7', _SUPPORTED_VERSIONS = '3.6', '3.7', '3.8', '3.9', '3.10'
VERSIONS_3 = '3.4', '3.5', '3.6', '3.7', '3.8'
@pytest.fixture(scope='session') @pytest.fixture(scope='session')
@@ -30,7 +29,7 @@ def clean_parso_cache():
""" """
old = cache._default_cache_path old = cache._default_cache_path
tmp = tempfile.mkdtemp(prefix='parso-test-') tmp = tempfile.mkdtemp(prefix='parso-test-')
cache._default_cache_path = tmp cache._default_cache_path = Path(tmp)
yield yield
cache._default_cache_path = old cache._default_cache_path = old
shutil.rmtree(tmp) shutil.rmtree(tmp)
@@ -52,18 +51,13 @@ def pytest_generate_tests(metafunc):
ids=[c.name for c in cases] ids=[c.name for c in cases]
) )
elif 'each_version' in metafunc.fixturenames: elif 'each_version' in metafunc.fixturenames:
metafunc.parametrize('each_version', VERSIONS_2 + VERSIONS_3) metafunc.parametrize('each_version', _SUPPORTED_VERSIONS)
elif 'each_py2_version' in metafunc.fixturenames:
metafunc.parametrize('each_py2_version', VERSIONS_2)
elif 'each_py3_version' in metafunc.fixturenames:
metafunc.parametrize('each_py3_version', VERSIONS_3)
elif 'version_ge_py36' in metafunc.fixturenames:
metafunc.parametrize('version_ge_py36', ['3.6', '3.7', '3.8'])
elif 'version_ge_py38' in metafunc.fixturenames: elif 'version_ge_py38' in metafunc.fixturenames:
metafunc.parametrize('version_ge_py38', ['3.8']) ge38 = set(_SUPPORTED_VERSIONS) - {'3.6', '3.7'}
metafunc.parametrize('version_ge_py38', sorted(ge38))
class NormalizerIssueCase(object): class NormalizerIssueCase:
""" """
Static Analysis cases lie in the static_analysis folder. Static Analysis cases lie in the static_analysis folder.
The tests also start with `#!`, like the goto_definition tests. The tests also start with `#!`, like the goto_definition tests.
@@ -95,7 +89,7 @@ def pytest_configure(config):
#root.addHandler(ch) #root.addHandler(ch)
class Checker(): class Checker:
def __init__(self, version, is_passing): def __init__(self, version, is_passing):
self.version = version self.version = version
self._is_passing = is_passing self._is_passing = is_passing
@@ -137,37 +131,16 @@ def works_not_in_py(each_version):
@pytest.fixture @pytest.fixture
def works_in_py2(each_version): def works_in_py(each_version):
return Checker(each_version, each_version.startswith('2')) return Checker(each_version, True)
@pytest.fixture
def works_ge_py27(each_version):
version_info = parse_version_string(each_version)
return Checker(each_version, version_info >= (2, 7))
@pytest.fixture
def works_ge_py3(each_version):
version_info = parse_version_string(each_version)
return Checker(each_version, version_info >= (3, 0))
@pytest.fixture
def works_ge_py35(each_version):
version_info = parse_version_string(each_version)
return Checker(each_version, version_info >= (3, 5))
@pytest.fixture
def works_ge_py36(each_version):
version_info = parse_version_string(each_version)
return Checker(each_version, version_info >= (3, 6))
@pytest.fixture @pytest.fixture
def works_ge_py38(each_version): def works_ge_py38(each_version):
version_info = parse_version_string(each_version) version_info = parse_version_string(each_version)
return Checker(each_version, version_info >= (3, 8)) return Checker(each_version, version_info >= (3, 8))
@pytest.fixture @pytest.fixture
def works_ge_py39(each_version): def works_ge_py39(each_version):
version_info = parse_version_string(each_version) version_info = parse_version_string(each_version)

View File

@@ -23,7 +23,7 @@ cd $PROJECT_NAME
git checkout $BRANCH git checkout $BRANCH
# Test first. # Test first.
tox pytest
# Create tag # Create tag
tag=v$(python3 -c "import $PROJECT_NAME; print($PROJECT_NAME.__version__)") tag=v$(python3 -c "import $PROJECT_NAME; print($PROJECT_NAME.__version__)")

View File

@@ -43,8 +43,8 @@ source_encoding = 'utf-8'
master_doc = 'index' master_doc = 'index'
# General information about the project. # General information about the project.
project = u'parso' project = 'parso'
copyright = u'parso contributors' copyright = 'parso contributors'
import parso import parso
from parso.utils import version_info from parso.utils import version_info
@@ -200,8 +200,8 @@ latex_elements = {
# Grouping the document tree into LaTeX files. List of tuples # Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass [howto/manual]). # (source start file, target name, title, author, documentclass [howto/manual]).
latex_documents = [ latex_documents = [
('index', 'parso.tex', u'parso documentation', ('index', 'parso.tex', 'parso documentation',
u'parso contributors', 'manual'), 'parso contributors', 'manual'),
] ]
# The name of an image file (relative to this directory) to place at the top of # The name of an image file (relative to this directory) to place at the top of
@@ -230,8 +230,8 @@ latex_documents = [
# One entry per manual page. List of tuples # One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section). # (source start file, name, description, authors, manual section).
man_pages = [ man_pages = [
('index', 'parso', u'parso Documentation', ('index', 'parso', 'parso Documentation',
[u'parso contributors'], 1) ['parso contributors'], 1)
] ]
# If true, show URL addresses after external links. # If true, show URL addresses after external links.
@@ -244,8 +244,8 @@ man_pages = [
# (source start file, target name, title, author, # (source start file, target name, title, author,
# dir menu entry, description, category) # dir menu entry, description, category)
texinfo_documents = [ texinfo_documents = [
('index', 'parso', u'parso documentation', ('index', 'parso', 'parso documentation',
u'parso contributors', 'parso', 'Awesome Python autocompletion library.', 'parso contributors', 'parso', 'Awesome Python autocompletion library.',
'Miscellaneous'), 'Miscellaneous'),
] ]
@@ -273,7 +273,7 @@ autodoc_default_flags = []
# -- Options for intersphinx module -------------------------------------------- # -- Options for intersphinx module --------------------------------------------
intersphinx_mapping = { intersphinx_mapping = {
'http://docs.python.org/': ('https://docs.python.org/3.6', None), 'http://docs.python.org/': ('https://docs.python.org/3', None),
} }

View File

@@ -21,18 +21,18 @@ The deprecation process is as follows:
Testing Testing
------- -------
The test suite depends on ``tox`` and ``pytest``:: The test suite depends on ``pytest``::
pip install tox pytest pip install pytest
To run the tests for all supported Python versions:: To run the tests use the following::
tox pytest
If you want to test only a specific Python version (e.g. Python 2.7), it's as If you want to test only a specific Python version (e.g. Python 3.9), it's as
easy as:: easy as::
tox -e py27 python3.9 -m pytest
Tests are also run automatically on `Travis CI Tests are also run automatically on `Travis CI
<https://travis-ci.org/davidhalter/parso/>`_. <https://travis-ci.org/davidhalter/parso/>`_.

View File

@@ -13,7 +13,7 @@ Parso consists of a small API to parse Python and analyse the syntax tree.
A simple example: A simple example:
>>> import parso >>> import parso
>>> module = parso.parse('hello + 1', version="3.6") >>> module = parso.parse('hello + 1', version="3.9")
>>> expr = module.children[0] >>> expr = module.children[0]
>>> expr >>> expr
PythonNode(arith_expr, [<Name: hello@1,0>, <Operator: +>, <Number: 1>]) PythonNode(arith_expr, [<Name: hello@1,0>, <Operator: +>, <Number: 1>])
@@ -43,7 +43,7 @@ from parso.grammar import Grammar, load_grammar
from parso.utils import split_lines, python_bytes_to_unicode from parso.utils import split_lines, python_bytes_to_unicode
__version__ = '0.7.1' __version__ = '0.8.1'
def parse(code=None, **kwargs): def parse(code=None, **kwargs):

View File

@@ -1,19 +0,0 @@
from typing import Any, Optional, Union
from parso.grammar import Grammar as Grammar, load_grammar as load_grammar
from parso.parser import ParserSyntaxError as ParserSyntaxError
from parso.utils import python_bytes_to_unicode as python_bytes_to_unicode, split_lines as split_lines
__version__: str = ...
def parse(
code: Optional[Union[str, bytes]],
*,
version: Optional[str] = None,
error_recovery: bool = True,
path: Optional[str] = None,
start_symbol: Optional[str] = None,
cache: bool = False,
diff_cache: bool = False,
cache_path: Optional[str] = None,
) -> Any: ...

View File

@@ -1,101 +1,3 @@
"""
To ensure compatibility from Python ``2.7`` - ``3.3``, a module has been
created. Clearly there is huge need to use conforming syntax.
"""
import os
import sys
import platform import platform
# unicode function
try:
unicode = unicode
except NameError:
unicode = str
is_pypy = platform.python_implementation() == 'PyPy' is_pypy = platform.python_implementation() == 'PyPy'
def use_metaclass(meta, *bases):
""" Create a class with a metaclass. """
if not bases:
bases = (object,)
return meta("HackClass", bases, {})
try:
encoding = sys.stdout.encoding
if encoding is None:
encoding = 'utf-8'
except AttributeError:
encoding = 'ascii'
def u(string):
"""Cast to unicode DAMMIT!
Written because Python2 repr always implicitly casts to a string, so we
have to cast back to a unicode (and we know that we always deal with valid
unicode, because we check that in the beginning).
"""
if sys.version_info.major >= 3:
return str(string)
if not isinstance(string, unicode):
return unicode(str(string), 'UTF-8')
return string
try:
# Python 3.3+
FileNotFoundError = FileNotFoundError
except NameError:
# Python 2.7 (both IOError + OSError)
FileNotFoundError = EnvironmentError
try:
# Python 3.3+
PermissionError = PermissionError
except NameError:
# Python 2.7 (both IOError + OSError)
PermissionError = EnvironmentError
def utf8_repr(func):
"""
``__repr__`` methods in Python 2 don't allow unicode objects to be
returned. Therefore cast them to utf-8 bytes in this decorator.
"""
def wrapper(self):
result = func(self)
if isinstance(result, unicode):
return result.encode('utf-8')
else:
return result
if sys.version_info.major >= 3:
return func
else:
return wrapper
if sys.version_info < (3, 5):
"""
A super-minimal shim around listdir that behave like
scandir for the information we need.
"""
class _DirEntry:
def __init__(self, name, basepath):
self.name = name
self.basepath = basepath
@property
def path(self):
return os.path.join(self.basepath, self.name)
def stat(self):
# won't follow symlinks
return os.lstat(os.path.join(self.basepath, self.name))
def scandir(dir):
return [_DirEntry(name, dir) for name in os.listdir(dir)]
else:
from os import scandir

View File

@@ -5,17 +5,11 @@ import hashlib
import gc import gc
import shutil import shutil
import platform import platform
import errno
import logging import logging
import warnings import warnings
import pickle
try: from pathlib import Path
import cPickle as pickle from typing import Dict, Any
except:
import pickle
from parso._compatibility import FileNotFoundError, PermissionError, scandir
from parso.file_io import FileIO
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
@@ -64,21 +58,19 @@ _VERSION_TAG = '%s-%s%s-%s' % (
""" """
Short name for distinguish Python implementations and versions. Short name for distinguish Python implementations and versions.
It's like `sys.implementation.cache_tag` but for Python2 It's a bit similar to `sys.implementation.cache_tag`.
we generate something similar. See: See: http://docs.python.org/3/library/sys.html#sys.implementation
http://docs.python.org/3/library/sys.html#sys.implementation
""" """
def _get_default_cache_path(): def _get_default_cache_path():
if platform.system().lower() == 'windows': if platform.system().lower() == 'windows':
dir_ = os.path.join(os.getenv('LOCALAPPDATA') dir_ = Path(os.getenv('LOCALAPPDATA') or '~', 'Parso', 'Parso')
or os.path.expanduser('~'), 'Parso', 'Parso')
elif platform.system().lower() == 'darwin': elif platform.system().lower() == 'darwin':
dir_ = os.path.join('~', 'Library', 'Caches', 'Parso') dir_ = Path('~', 'Library', 'Caches', 'Parso')
else: else:
dir_ = os.path.join(os.getenv('XDG_CACHE_HOME') or '~/.cache', 'parso') dir_ = Path(os.getenv('XDG_CACHE_HOME') or '~/.cache', 'parso')
return os.path.expanduser(dir_) return dir_.expanduser()
_default_cache_path = _get_default_cache_path() _default_cache_path = _get_default_cache_path()
@@ -93,21 +85,22 @@ On Linux, if environment variable ``$XDG_CACHE_HOME`` is set,
_CACHE_CLEAR_THRESHOLD = 60 * 60 * 24 _CACHE_CLEAR_THRESHOLD = 60 * 60 * 24
def _get_cache_clear_lock(cache_path = None):
def _get_cache_clear_lock_path(cache_path=None):
""" """
The path where the cache lock is stored. The path where the cache lock is stored.
Cache lock will prevent continous cache clearing and only allow garbage Cache lock will prevent continous cache clearing and only allow garbage
collection once a day (can be configured in _CACHE_CLEAR_THRESHOLD). collection once a day (can be configured in _CACHE_CLEAR_THRESHOLD).
""" """
cache_path = cache_path or _get_default_cache_path() cache_path = cache_path or _default_cache_path
return FileIO(os.path.join(cache_path, "PARSO-CACHE-LOCK")) return cache_path.joinpath("PARSO-CACHE-LOCK")
parser_cache = {} parser_cache: Dict[str, Any] = {}
class _NodeCacheItem(object): class _NodeCacheItem:
def __init__(self, node, lines, change_time=None): def __init__(self, node, lines, change_time=None):
self.node = node self.node = node
self.lines = lines self.lines = lines
@@ -142,16 +135,9 @@ def load_module(hashed_grammar, file_io, cache_path=None):
def _load_from_file_system(hashed_grammar, path, p_time, cache_path=None): def _load_from_file_system(hashed_grammar, path, p_time, cache_path=None):
cache_path = _get_hashed_path(hashed_grammar, path, cache_path=cache_path) cache_path = _get_hashed_path(hashed_grammar, path, cache_path=cache_path)
try: try:
try: if p_time > os.path.getmtime(cache_path):
if p_time > os.path.getmtime(cache_path): # Cache is outdated
# Cache is outdated return None
return None
except OSError as e:
if e.errno == errno.ENOENT:
# In Python 2 instead of an IOError here we get an OSError.
raise FileNotFoundError
else:
raise
with open(cache_path, 'rb') as f: with open(cache_path, 'rb') as f:
gc.disable() gc.disable()
@@ -225,52 +211,65 @@ def clear_inactive_cache(
inactivity_threshold=_CACHED_FILE_MAXIMUM_SURVIVAL, inactivity_threshold=_CACHED_FILE_MAXIMUM_SURVIVAL,
): ):
if cache_path is None: if cache_path is None:
cache_path = _get_default_cache_path() cache_path = _default_cache_path
if not os.path.exists(cache_path): if not cache_path.exists():
return False return False
for version_path in os.listdir(cache_path): for dirname in os.listdir(cache_path):
version_path = os.path.join(cache_path, version_path) version_path = cache_path.joinpath(dirname)
if not os.path.isdir(version_path): if not version_path.is_dir():
continue continue
for file in scandir(version_path): for file in os.scandir(version_path):
if ( if file.stat().st_atime + _CACHED_FILE_MAXIMUM_SURVIVAL <= time.time():
file.stat().st_atime + _CACHED_FILE_MAXIMUM_SURVIVAL
<= time.time()
):
try: try:
os.remove(file.path) os.remove(file.path)
except OSError: # silently ignore all failures except OSError: # silently ignore all failures
continue continue
else: else:
return True return True
def _remove_cache_and_update_lock(cache_path = None): def _touch(path):
lock = _get_cache_clear_lock(cache_path=cache_path) try:
clear_lock_time = lock.get_last_modified() os.utime(path, None)
except FileNotFoundError:
try:
file = open(path, 'a')
file.close()
except (OSError, IOError): # TODO Maybe log this?
return False
return True
def _remove_cache_and_update_lock(cache_path=None):
lock_path = _get_cache_clear_lock_path(cache_path=cache_path)
try:
clear_lock_time = os.path.getmtime(lock_path)
except FileNotFoundError:
clear_lock_time = None
if ( if (
clear_lock_time is None # first time clear_lock_time is None # first time
or clear_lock_time + _CACHE_CLEAR_THRESHOLD <= time.time() or clear_lock_time + _CACHE_CLEAR_THRESHOLD <= time.time()
): ):
if not lock._touch(): if not _touch(lock_path):
# First make sure that as few as possible other cleanup jobs also # First make sure that as few as possible other cleanup jobs also
# get started. There is still a race condition but it's probably # get started. There is still a race condition but it's probably
# not a big problem. # not a big problem.
return False return False
clear_inactive_cache(cache_path = cache_path) clear_inactive_cache(cache_path=cache_path)
def _get_hashed_path(hashed_grammar, path, cache_path=None): def _get_hashed_path(hashed_grammar, path, cache_path=None):
directory = _get_cache_directory_path(cache_path=cache_path) directory = _get_cache_directory_path(cache_path=cache_path)
file_hash = hashlib.sha256(path.encode("utf-8")).hexdigest() file_hash = hashlib.sha256(str(path).encode("utf-8")).hexdigest()
return os.path.join(directory, '%s-%s.pkl' % (hashed_grammar, file_hash)) return os.path.join(directory, '%s-%s.pkl' % (hashed_grammar, file_hash))
def _get_cache_directory_path(cache_path=None): def _get_cache_directory_path(cache_path=None):
if cache_path is None: if cache_path is None:
cache_path = _default_cache_path cache_path = _default_cache_path
directory = os.path.join(cache_path, _VERSION_TAG) directory = cache_path.joinpath(_VERSION_TAG)
if not os.path.exists(directory): if not directory.exists():
os.makedirs(directory) os.makedirs(directory)
return directory return directory

View File

@@ -1,9 +1,12 @@
import os import os
from parso._compatibility import FileNotFoundError from pathlib import Path
from typing import Union
class FileIO(object): class FileIO:
def __init__(self, path): def __init__(self, path: Union[os.PathLike, str]):
if isinstance(path, str):
path = Path(path)
self.path = path self.path = path
def read(self): # Returns bytes/str def read(self): # Returns bytes/str
@@ -19,20 +22,8 @@ class FileIO(object):
""" """
try: try:
return os.path.getmtime(self.path) return os.path.getmtime(self.path)
except OSError:
# Might raise FileNotFoundError, OSError for Python 2
return None
def _touch(self):
try:
os.utime(self.path, None)
except FileNotFoundError: except FileNotFoundError:
try: return None
file = open(self.path, 'a')
file.close()
except (OSError, IOError): # TODO Maybe log this?
return False
return True
def __repr__(self): def __repr__(self):
return '%s(%s)' % (self.__class__.__name__, self.path) return '%s(%s)' % (self.__class__.__name__, self.path)
@@ -40,7 +31,7 @@ class FileIO(object):
class KnownContentFileIO(FileIO): class KnownContentFileIO(FileIO):
def __init__(self, path, content): def __init__(self, path, content):
super(KnownContentFileIO, self).__init__(path) super().__init__(path)
self._content = content self._content = content
def read(self): def read(self):

View File

@@ -1,9 +1,12 @@
import hashlib import hashlib
import os import os
from typing import Generic, TypeVar, Union, Dict, Optional, Any
from pathlib import Path
from parso._compatibility import FileNotFoundError, is_pypy from parso._compatibility import is_pypy
from parso.pgen2 import generate_grammar from parso.pgen2 import generate_grammar
from parso.utils import split_lines, python_bytes_to_unicode, parse_version_string from parso.utils import split_lines, python_bytes_to_unicode, \
PythonVersionInfo, parse_version_string
from parso.python.diff import DiffParser from parso.python.diff import DiffParser
from parso.python.tokenize import tokenize_lines, tokenize from parso.python.tokenize import tokenize_lines, tokenize
from parso.python.token import PythonTokenTypes from parso.python.token import PythonTokenTypes
@@ -13,23 +16,27 @@ from parso.python.parser import Parser as PythonParser
from parso.python.errors import ErrorFinderConfig from parso.python.errors import ErrorFinderConfig
from parso.python import pep8 from parso.python import pep8
from parso.file_io import FileIO, KnownContentFileIO from parso.file_io import FileIO, KnownContentFileIO
from parso.normalizer import RefactoringNormalizer from parso.normalizer import RefactoringNormalizer, NormalizerConfig
_loaded_grammars = {} _loaded_grammars: Dict[str, 'Grammar'] = {}
_NodeT = TypeVar("_NodeT")
class Grammar(object): class Grammar(Generic[_NodeT]):
""" """
:py:func:`parso.load_grammar` returns instances of this class. :py:func:`parso.load_grammar` returns instances of this class.
Creating custom none-python grammars by calling this is not supported, yet. Creating custom none-python grammars by calling this is not supported, yet.
"""
#:param text: A BNF representation of your grammar.
_error_normalizer_config = None
_token_namespace = None
_default_normalizer_config = pep8.PEP8NormalizerConfig()
def __init__(self, text, tokenizer, parser=BaseParser, diff_parser=None): :param text: A BNF representation of your grammar.
"""
_start_nonterminal: str
_error_normalizer_config: Optional[ErrorFinderConfig] = None
_token_namespace: Any = None
_default_normalizer_config: NormalizerConfig = pep8.PEP8NormalizerConfig()
def __init__(self, text: str, *, tokenizer, parser=BaseParser, diff_parser=None):
self._pgen_grammar = generate_grammar( self._pgen_grammar = generate_grammar(
text, text,
token_namespace=self._get_token_namespace() token_namespace=self._get_token_namespace()
@@ -39,7 +46,16 @@ class Grammar(object):
self._diff_parser = diff_parser self._diff_parser = diff_parser
self._hashed = hashlib.sha256(text.encode("utf-8")).hexdigest() self._hashed = hashlib.sha256(text.encode("utf-8")).hexdigest()
def parse(self, code=None, **kwargs): def parse(self,
code: Union[str, bytes] = None,
*,
error_recovery=True,
path: Union[os.PathLike, str] = None,
start_symbol: str = None,
cache=False,
diff_cache=False,
cache_path: Union[os.PathLike, str] = None,
file_io: FileIO = None) -> _NodeT:
""" """
If you want to parse a Python file you want to start here, most likely. If you want to parse a Python file you want to start here, most likely.
@@ -74,22 +90,14 @@ class Grammar(object):
:return: A subclass of :py:class:`parso.tree.NodeOrLeaf`. Typically a :return: A subclass of :py:class:`parso.tree.NodeOrLeaf`. Typically a
:py:class:`parso.python.tree.Module`. :py:class:`parso.python.tree.Module`.
""" """
if 'start_pos' in kwargs:
raise TypeError("parse() got an unexpected keyword argument.")
return self._parse(code=code, **kwargs)
def _parse(self, code=None, error_recovery=True, path=None,
start_symbol=None, cache=False, diff_cache=False,
cache_path=None, file_io=None, start_pos=(1, 0)):
"""
Wanted python3.5 * operator and keyword only arguments. Therefore just
wrap it all.
start_pos here is just a parameter internally used. Might be public
sometime in the future.
"""
if code is None and path is None and file_io is None: if code is None and path is None and file_io is None:
raise TypeError("Please provide either code or a path.") raise TypeError("Please provide either code or a path.")
if isinstance(path, str):
path = Path(path)
if isinstance(cache_path, str):
cache_path = Path(cache_path)
if start_symbol is None: if start_symbol is None:
start_symbol = self._start_nonterminal start_symbol = self._start_nonterminal
@@ -98,14 +106,14 @@ class Grammar(object):
if file_io is None: if file_io is None:
if code is None: if code is None:
file_io = FileIO(path) file_io = FileIO(path) # type: ignore
else: else:
file_io = KnownContentFileIO(path, code) file_io = KnownContentFileIO(path, code)
if cache and file_io.path is not None: if cache and file_io.path is not None:
module_node = load_module(self._hashed, file_io, cache_path=cache_path) module_node = load_module(self._hashed, file_io, cache_path=cache_path)
if module_node is not None: if module_node is not None:
return module_node return module_node # type: ignore
if code is None: if code is None:
code = file_io.read() code = file_io.read()
@@ -124,7 +132,7 @@ class Grammar(object):
module_node = module_cache_item.node module_node = module_cache_item.node
old_lines = module_cache_item.lines old_lines = module_cache_item.lines
if old_lines == lines: if old_lines == lines:
return module_node return module_node # type: ignore
new_node = self._diff_parser( new_node = self._diff_parser(
self._pgen_grammar, self._tokenizer, module_node self._pgen_grammar, self._tokenizer, module_node
@@ -133,12 +141,12 @@ class Grammar(object):
new_lines=lines new_lines=lines
) )
try_to_save_module(self._hashed, file_io, new_node, lines, try_to_save_module(self._hashed, file_io, new_node, lines,
# Never pickle in pypy, it's slow as hell. # Never pickle in pypy, it's slow as hell.
pickling=cache and not is_pypy, pickling=cache and not is_pypy,
cache_path=cache_path) cache_path=cache_path)
return new_node return new_node # type: ignore
tokens = self._tokenizer(lines, start_pos=start_pos) tokens = self._tokenizer(lines)
p = self._parser( p = self._parser(
self._pgen_grammar, self._pgen_grammar,
@@ -149,10 +157,10 @@ class Grammar(object):
if cache or diff_cache: if cache or diff_cache:
try_to_save_module(self._hashed, file_io, root_node, lines, try_to_save_module(self._hashed, file_io, root_node, lines,
# Never pickle in pypy, it's slow as hell. # Never pickle in pypy, it's slow as hell.
pickling=cache and not is_pypy, pickling=cache and not is_pypy,
cache_path=cache_path) cache_path=cache_path)
return root_node return root_node # type: ignore
def _get_token_namespace(self): def _get_token_namespace(self):
ns = self._token_namespace ns = self._token_namespace
@@ -206,8 +214,8 @@ class PythonGrammar(Grammar):
_token_namespace = PythonTokenTypes _token_namespace = PythonTokenTypes
_start_nonterminal = 'file_input' _start_nonterminal = 'file_input'
def __init__(self, version_info, bnf_text): def __init__(self, version_info: PythonVersionInfo, bnf_text: str):
super(PythonGrammar, self).__init__( super().__init__(
bnf_text, bnf_text,
tokenizer=self._tokenize_lines, tokenizer=self._tokenize_lines,
parser=PythonParser, parser=PythonParser,
@@ -216,14 +224,14 @@ class PythonGrammar(Grammar):
self.version_info = version_info self.version_info = version_info
def _tokenize_lines(self, lines, **kwargs): def _tokenize_lines(self, lines, **kwargs):
return tokenize_lines(lines, self.version_info, **kwargs) return tokenize_lines(lines, version_info=self.version_info, **kwargs)
def _tokenize(self, code): def _tokenize(self, code):
# Used by Jedi. # Used by Jedi.
return tokenize(code, self.version_info) return tokenize(code, version_info=self.version_info)
def load_grammar(**kwargs): def load_grammar(*, version: str = None, path: str = None):
""" """
Loads a :py:class:`parso.Grammar`. The default version is the current Python Loads a :py:class:`parso.Grammar`. The default version is the current Python
version. version.
@@ -231,30 +239,26 @@ def load_grammar(**kwargs):
:param str version: A python version string, e.g. ``version='3.8'``. :param str version: A python version string, e.g. ``version='3.8'``.
:param str path: A path to a grammar file :param str path: A path to a grammar file
""" """
def load_grammar(language='python', version=None, path=None): version_info = parse_version_string(version)
if language == 'python':
version_info = parse_version_string(version)
file = path or os.path.join( file = path or os.path.join(
'python', 'python',
'grammar%s%s.txt' % (version_info.major, version_info.minor) 'grammar%s%s.txt' % (version_info.major, version_info.minor)
)
global _loaded_grammars
path = os.path.join(os.path.dirname(__file__), file)
try:
return _loaded_grammars[path]
except KeyError:
try:
with open(path) as f:
bnf_text = f.read()
grammar = PythonGrammar(version_info, bnf_text)
return _loaded_grammars.setdefault(path, grammar)
except FileNotFoundError:
message = "Python version %s.%s is currently not supported." % (
version_info.major, version_info.minor
) )
raise NotImplementedError(message)
global _loaded_grammars
path = os.path.join(os.path.dirname(__file__), file)
try:
return _loaded_grammars[path]
except KeyError:
try:
with open(path) as f:
bnf_text = f.read()
grammar = PythonGrammar(version_info, bnf_text)
return _loaded_grammars.setdefault(path, grammar)
except FileNotFoundError:
message = "Python version %s.%s is currently not supported." % (version_info.major, version_info.minor)
raise NotImplementedError(message)
else:
raise NotImplementedError("No support for language %s." % language)
return load_grammar(**kwargs)

View File

@@ -1,38 +0,0 @@
from typing import Any, Callable, Generic, Optional, Sequence, TypeVar, Union
from typing_extensions import Literal
from parso.utils import PythonVersionInfo
_Token = Any
_NodeT = TypeVar("_NodeT")
class Grammar(Generic[_NodeT]):
_default_normalizer_config: Optional[Any] = ...
_error_normalizer_config: Optional[Any] = None
_start_nonterminal: str = ...
_token_namespace: Optional[str] = None
def __init__(
self,
text: str,
tokenizer: Callable[[Sequence[str], int], Sequence[_Token]],
parser: Any = ...,
diff_parser: Any = ...,
) -> None: ...
def parse(
self,
code: Union[str, bytes] = ...,
error_recovery: bool = ...,
path: Optional[str] = ...,
start_symbol: Optional[str] = ...,
cache: bool = ...,
diff_cache: bool = ...,
cache_path: Optional[str] = ...,
) -> _NodeT: ...
class PythonGrammar(Grammar):
version_info: PythonVersionInfo
def __init__(self, version_info: PythonVersionInfo, bnf_text: str) -> None: ...
def load_grammar(
language: Literal["python"] = "python", version: Optional[str] = ..., path: str = ...
) -> Grammar: ...

View File

@@ -1,6 +1,5 @@
from contextlib import contextmanager from contextlib import contextmanager
from typing import Dict, List
from parso._compatibility import use_metaclass
class _NormalizerMeta(type): class _NormalizerMeta(type):
@@ -11,9 +10,9 @@ class _NormalizerMeta(type):
return new_cls return new_cls
class Normalizer(use_metaclass(_NormalizerMeta)): class Normalizer(metaclass=_NormalizerMeta):
_rule_type_instances = {} _rule_type_instances: Dict[str, List[type]] = {}
_rule_value_instances = {} _rule_value_instances: Dict[str, List[type]] = {}
def __init__(self, grammar, config): def __init__(self, grammar, config):
self.grammar = grammar self.grammar = grammar
@@ -77,7 +76,7 @@ class Normalizer(use_metaclass(_NormalizerMeta)):
return True return True
@classmethod @classmethod
def register_rule(cls, **kwargs): def register_rule(cls, *, value=None, values=(), type=None, types=()):
""" """
Use it as a class decorator:: Use it as a class decorator::
@@ -86,10 +85,6 @@ class Normalizer(use_metaclass(_NormalizerMeta)):
class MyRule(Rule): class MyRule(Rule):
error_code = 42 error_code = 42
""" """
return cls._register_rule(**kwargs)
@classmethod
def _register_rule(cls, value=None, values=(), type=None, types=()):
values = list(values) values = list(values)
types = list(types) types = list(types)
if value is not None: if value is not None:
@@ -110,7 +105,7 @@ class Normalizer(use_metaclass(_NormalizerMeta)):
return decorator return decorator
class NormalizerConfig(object): class NormalizerConfig:
normalizer_class = Normalizer normalizer_class = Normalizer
def create_normalizer(self, grammar): def create_normalizer(self, grammar):
@@ -120,7 +115,7 @@ class NormalizerConfig(object):
return self.normalizer_class(grammar, self) return self.normalizer_class(grammar, self)
class Issue(object): class Issue:
def __init__(self, node, code, message): def __init__(self, node, code, message):
self.code = code self.code = code
""" """
@@ -150,9 +145,9 @@ class Issue(object):
return '<%s: %s>' % (self.__class__.__name__, self.code) return '<%s: %s>' % (self.__class__.__name__, self.code)
class Rule(object): class Rule:
code = None code: int
message = None message: str
def __init__(self, normalizer): def __init__(self, normalizer):
self._normalizer = normalizer self._normalizer = normalizer
@@ -194,10 +189,10 @@ class RefactoringNormalizer(Normalizer):
try: try:
return self._node_to_str_map[node] return self._node_to_str_map[node]
except KeyError: except KeyError:
return super(RefactoringNormalizer, self).visit(node) return super().visit(node)
def visit_leaf(self, leaf): def visit_leaf(self, leaf):
try: try:
return self._node_to_str_map[leaf] return self._node_to_str_map[leaf]
except KeyError: except KeyError:
return super(RefactoringNormalizer, self).visit_leaf(leaf) return super().visit_leaf(leaf)

View File

@@ -23,6 +23,8 @@ within the statement. This lowers memory usage and cpu time and reduces the
complexity of the ``Parser`` (there's another parser sitting inside complexity of the ``Parser`` (there's another parser sitting inside
``Statement``, which produces ``Array`` and ``Call``). ``Statement``, which produces ``Array`` and ``Call``).
""" """
from typing import Dict
from parso import tree from parso import tree
from parso.pgen2.generator import ReservedString from parso.pgen2.generator import ReservedString
@@ -71,7 +73,7 @@ class Stack(list):
return list(iterate()) return list(iterate())
class StackNode(object): class StackNode:
def __init__(self, dfa): def __init__(self, dfa):
self.dfa = dfa self.dfa = dfa
self.nodes = [] self.nodes = []
@@ -86,7 +88,7 @@ class StackNode(object):
def _token_to_transition(grammar, type_, value): def _token_to_transition(grammar, type_, value):
# Map from token to label # Map from token to label
if type_.contains_syntax: if type_.value.contains_syntax:
# Check for reserved words (keywords) # Check for reserved words (keywords)
try: try:
return grammar.reserved_syntax_strings[value] return grammar.reserved_syntax_strings[value]
@@ -96,7 +98,7 @@ def _token_to_transition(grammar, type_, value):
return type_ return type_
class BaseParser(object): class BaseParser:
"""Parser engine. """Parser engine.
A Parser instance contains state pertaining to the current token A Parser instance contains state pertaining to the current token
@@ -108,11 +110,10 @@ class BaseParser(object):
When a syntax error occurs, error_recovery() is called. When a syntax error occurs, error_recovery() is called.
""" """
node_map = {} node_map: Dict[str, type] = {}
default_node = tree.Node default_node = tree.Node
leaf_map = { leaf_map: Dict[str, type] = {}
}
default_leaf = tree.Leaf default_leaf = tree.Leaf
def __init__(self, pgen_grammar, start_nonterminal='file_input', error_recovery=False): def __init__(self, pgen_grammar, start_nonterminal='file_input', error_recovery=False):

View File

@@ -1 +0,0 @@
from parso.pgen2.generator import generate_grammar as generate_grammar

View File

@@ -27,11 +27,14 @@ because we made some optimizations.
""" """
from ast import literal_eval from ast import literal_eval
from typing import TypeVar, Generic, Mapping, Sequence, Set, Union
from parso.pgen2.grammar_parser import GrammarParser, NFAState from parso.pgen2.grammar_parser import GrammarParser, NFAState
_TokenTypeT = TypeVar("_TokenTypeT")
class Grammar(object):
class Grammar(Generic[_TokenTypeT]):
""" """
Once initialized, this class supplies the grammar tables for the Once initialized, this class supplies the grammar tables for the
parsing engine implemented by parse.py. The parsing engine parsing engine implemented by parse.py. The parsing engine
@@ -41,18 +44,21 @@ class Grammar(object):
dfas. dfas.
""" """
def __init__(self, start_nonterminal, rule_to_dfas, reserved_syntax_strings): def __init__(self,
self.nonterminal_to_dfas = rule_to_dfas # Dict[str, List[DFAState]] start_nonterminal: str,
rule_to_dfas: Mapping[str, Sequence['DFAState[_TokenTypeT]']],
reserved_syntax_strings: Mapping[str, 'ReservedString']):
self.nonterminal_to_dfas = rule_to_dfas
self.reserved_syntax_strings = reserved_syntax_strings self.reserved_syntax_strings = reserved_syntax_strings
self.start_nonterminal = start_nonterminal self.start_nonterminal = start_nonterminal
class DFAPlan(object): class DFAPlan:
""" """
Plans are used for the parser to create stack nodes and do the proper Plans are used for the parser to create stack nodes and do the proper
DFA state transitions. DFA state transitions.
""" """
def __init__(self, next_dfa, dfa_pushes=[]): def __init__(self, next_dfa: 'DFAState', dfa_pushes: Sequence['DFAState'] = []):
self.next_dfa = next_dfa self.next_dfa = next_dfa
self.dfa_pushes = dfa_pushes self.dfa_pushes = dfa_pushes
@@ -60,7 +66,7 @@ class DFAPlan(object):
return '%s(%s, %s)' % (self.__class__.__name__, self.next_dfa, self.dfa_pushes) return '%s(%s, %s)' % (self.__class__.__name__, self.next_dfa, self.dfa_pushes)
class DFAState(object): class DFAState(Generic[_TokenTypeT]):
""" """
The DFAState object is the core class for pretty much anything. DFAState The DFAState object is the core class for pretty much anything. DFAState
are the vertices of an ordered graph while arcs and transitions are the are the vertices of an ordered graph while arcs and transitions are the
@@ -70,20 +76,21 @@ class DFAState(object):
transitions are then calculated to connect the DFA state machines that have transitions are then calculated to connect the DFA state machines that have
different nonterminals. different nonterminals.
""" """
def __init__(self, from_rule, nfa_set, final): def __init__(self, from_rule: str, nfa_set: Set[NFAState], final: NFAState):
assert isinstance(nfa_set, set) assert isinstance(nfa_set, set)
assert isinstance(next(iter(nfa_set)), NFAState) assert isinstance(next(iter(nfa_set)), NFAState)
assert isinstance(final, NFAState) assert isinstance(final, NFAState)
self.from_rule = from_rule self.from_rule = from_rule
self.nfa_set = nfa_set self.nfa_set = nfa_set
self.arcs = {} # map from terminals/nonterminals to DFAState # map from terminals/nonterminals to DFAState
self.arcs: Mapping[str, DFAState] = {}
# In an intermediary step we set these nonterminal arcs (which has the # In an intermediary step we set these nonterminal arcs (which has the
# same structure as arcs). These don't contain terminals anymore. # same structure as arcs). These don't contain terminals anymore.
self.nonterminal_arcs = {} self.nonterminal_arcs: Mapping[str, DFAState] = {}
# Transitions are basically the only thing that the parser is using # Transitions are basically the only thing that the parser is using
# with is_final. Everyting else is purely here to create a parser. # with is_final. Everyting else is purely here to create a parser.
self.transitions = {} #: Dict[Union[TokenType, ReservedString], DFAPlan] self.transitions: Mapping[Union[_TokenTypeT, ReservedString], DFAPlan] = {}
self.is_final = final in nfa_set self.is_final = final in nfa_set
def add_arc(self, next_, label): def add_arc(self, next_, label):
@@ -111,22 +118,20 @@ class DFAState(object):
return False return False
return True return True
__hash__ = None # For Py3 compatibility.
def __repr__(self): def __repr__(self):
return '<%s: %s is_final=%s>' % ( return '<%s: %s is_final=%s>' % (
self.__class__.__name__, self.from_rule, self.is_final self.__class__.__name__, self.from_rule, self.is_final
) )
class ReservedString(object): class ReservedString:
""" """
Most grammars will have certain keywords and operators that are mentioned Most grammars will have certain keywords and operators that are mentioned
in the grammar as strings (e.g. "if") and not token types (e.g. NUMBER). in the grammar as strings (e.g. "if") and not token types (e.g. NUMBER).
This class basically is the former. This class basically is the former.
""" """
def __init__(self, value): def __init__(self, value: str):
self.value = value self.value = value
def __repr__(self): def __repr__(self):
@@ -149,7 +154,6 @@ def _simplify_dfas(dfas):
for j in range(i + 1, len(dfas)): for j in range(i + 1, len(dfas)):
state_j = dfas[j] state_j = dfas[j]
if state_i == state_j: if state_i == state_j:
#print " unify", i, j
del dfas[j] del dfas[j]
for state in dfas: for state in dfas:
state.unifystate(state_j, state_i) state.unifystate(state_j, state_i)
@@ -233,7 +237,7 @@ def _dump_dfas(dfas):
print(" %s -> %d" % (nonterminal, dfas.index(next_))) print(" %s -> %d" % (nonterminal, dfas.index(next_)))
def generate_grammar(bnf_grammar, token_namespace): def generate_grammar(bnf_grammar: str, token_namespace) -> Grammar:
""" """
``bnf_text`` is a grammar in extended BNF (using * for repetition, + for ``bnf_text`` is a grammar in extended BNF (using * for repetition, + for
at-least-once repetition, [] for optional parts, | for alternatives and () at-least-once repetition, [] for optional parts, | for alternatives and ()
@@ -245,19 +249,19 @@ def generate_grammar(bnf_grammar, token_namespace):
rule_to_dfas = {} rule_to_dfas = {}
start_nonterminal = None start_nonterminal = None
for nfa_a, nfa_z in GrammarParser(bnf_grammar).parse(): for nfa_a, nfa_z in GrammarParser(bnf_grammar).parse():
#_dump_nfa(nfa_a, nfa_z) # _dump_nfa(nfa_a, nfa_z)
dfas = _make_dfas(nfa_a, nfa_z) dfas = _make_dfas(nfa_a, nfa_z)
#_dump_dfas(dfas) # _dump_dfas(dfas)
# oldlen = len(dfas) # oldlen = len(dfas)
_simplify_dfas(dfas) _simplify_dfas(dfas)
# newlen = len(dfas) # newlen = len(dfas)
rule_to_dfas[nfa_a.from_rule] = dfas rule_to_dfas[nfa_a.from_rule] = dfas
#print(nfa_a.from_rule, oldlen, newlen) # print(nfa_a.from_rule, oldlen, newlen)
if start_nonterminal is None: if start_nonterminal is None:
start_nonterminal = nfa_a.from_rule start_nonterminal = nfa_a.from_rule
reserved_strings = {} reserved_strings: Mapping[str, ReservedString] = {}
for nonterminal, dfas in rule_to_dfas.items(): for nonterminal, dfas in rule_to_dfas.items():
for dfa_state in dfas: for dfa_state in dfas:
for terminal_or_nonterminal, next_dfa in dfa_state.arcs.items(): for terminal_or_nonterminal, next_dfa in dfa_state.arcs.items():
@@ -272,7 +276,7 @@ def generate_grammar(bnf_grammar, token_namespace):
dfa_state.transitions[transition] = DFAPlan(next_dfa) dfa_state.transitions[transition] = DFAPlan(next_dfa)
_calculate_tree_traversal(rule_to_dfas) _calculate_tree_traversal(rule_to_dfas)
return Grammar(start_nonterminal, rule_to_dfas, reserved_strings) return Grammar(start_nonterminal, rule_to_dfas, reserved_strings) # type: ignore
def _make_transition(token_namespace, reserved_syntax_strings, label): def _make_transition(token_namespace, reserved_syntax_strings, label):

View File

@@ -1,38 +0,0 @@
from typing import Any, Generic, Mapping, Sequence, Set, TypeVar, Union
from parso.pgen2.grammar_parser import NFAState
_TokenTypeT = TypeVar("_TokenTypeT")
class Grammar(Generic[_TokenTypeT]):
nonterminal_to_dfas: Mapping[str, Sequence[DFAState[_TokenTypeT]]]
reserved_syntax_strings: Mapping[str, ReservedString]
start_nonterminal: str
def __init__(
self,
start_nonterminal: str,
rule_to_dfas: Mapping[str, Sequence[DFAState]],
reserved_syntax_strings: Mapping[str, ReservedString],
) -> None: ...
class DFAPlan:
next_dfa: DFAState
dfa_pushes: Sequence[DFAState]
class DFAState(Generic[_TokenTypeT]):
from_rule: str
nfa_set: Set[NFAState]
is_final: bool
arcs: Mapping[str, DFAState] # map from all terminals/nonterminals to DFAState
nonterminal_arcs: Mapping[str, DFAState]
transitions: Mapping[Union[_TokenTypeT, ReservedString], DFAPlan]
def __init__(
self, from_rule: str, nfa_set: Set[NFAState], final: NFAState
) -> None: ...
class ReservedString:
value: str
def __init__(self, value: str) -> None: ...
def __repr__(self) -> str: ...
def generate_grammar(bnf_grammar: str, token_namespace: Any) -> Grammar[Any]: ...

View File

@@ -4,25 +4,49 @@
# Modifications: # Modifications:
# Copyright David Halter and Contributors # Copyright David Halter and Contributors
# Modifications are dual-licensed: MIT and PSF. # Modifications are dual-licensed: MIT and PSF.
from typing import Optional, Iterator, Tuple, List
from parso.python.tokenize import tokenize from parso.python.tokenize import tokenize
from parso.utils import parse_version_string from parso.utils import parse_version_string
from parso.python.token import PythonTokenTypes from parso.python.token import PythonTokenTypes
class GrammarParser(): class NFAArc:
def __init__(self, next_: 'NFAState', nonterminal_or_string: Optional[str]):
self.next: NFAState = next_
self.nonterminal_or_string: Optional[str] = nonterminal_or_string
def __repr__(self):
return '<%s: %s>' % (self.__class__.__name__, self.nonterminal_or_string)
class NFAState:
def __init__(self, from_rule: str):
self.from_rule: str = from_rule
self.arcs: List[NFAArc] = []
def add_arc(self, next_, nonterminal_or_string=None):
assert nonterminal_or_string is None or isinstance(nonterminal_or_string, str)
assert isinstance(next_, NFAState)
self.arcs.append(NFAArc(next_, nonterminal_or_string))
def __repr__(self):
return '<%s: from %s>' % (self.__class__.__name__, self.from_rule)
class GrammarParser:
""" """
The parser for Python grammar files. The parser for Python grammar files.
""" """
def __init__(self, bnf_grammar): def __init__(self, bnf_grammar: str):
self._bnf_grammar = bnf_grammar self._bnf_grammar = bnf_grammar
self.generator = tokenize( self.generator = tokenize(
bnf_grammar, bnf_grammar,
version_info=parse_version_string('3.6') version_info=parse_version_string('3.9')
) )
self._gettoken() # Initialize lookahead self._gettoken() # Initialize lookahead
def parse(self): def parse(self) -> Iterator[Tuple[NFAState, NFAState]]:
# grammar: (NEWLINE | rule)* ENDMARKER # grammar: (NEWLINE | rule)* ENDMARKER
while self.type != PythonTokenTypes.ENDMARKER: while self.type != PythonTokenTypes.ENDMARKER:
while self.type == PythonTokenTypes.NEWLINE: while self.type == PythonTokenTypes.NEWLINE:
@@ -134,26 +158,3 @@ class GrammarParser():
line = self._bnf_grammar.splitlines()[self.begin[0] - 1] line = self._bnf_grammar.splitlines()[self.begin[0] - 1]
raise SyntaxError(msg, ('<grammar>', self.begin[0], raise SyntaxError(msg, ('<grammar>', self.begin[0],
self.begin[1], line)) self.begin[1], line))
class NFAArc(object):
def __init__(self, next_, nonterminal_or_string):
self.next = next_
self.nonterminal_or_string = nonterminal_or_string
def __repr__(self):
return '<%s: %s>' % (self.__class__.__name__, self.nonterminal_or_string)
class NFAState(object):
def __init__(self, from_rule):
self.from_rule = from_rule
self.arcs = [] # List[nonterminal (str), NFAState]
def add_arc(self, next_, nonterminal_or_string=None):
assert nonterminal_or_string is None or isinstance(nonterminal_or_string, str)
assert isinstance(next_, NFAState)
self.arcs.append(NFAArc(next_, nonterminal_or_string))
def __repr__(self):
return '<%s: from %s>' % (self.__class__.__name__, self.from_rule)

View File

@@ -1,20 +0,0 @@
from typing import Generator, List, Optional, Tuple
from parso.python.token import TokenType
class GrammarParser:
generator: Generator[TokenType, None, None]
def __init__(self, bnf_grammar: str) -> None: ...
def parse(self) -> Generator[Tuple[NFAState, NFAState], None, None]: ...
class NFAArc:
next: NFAState
nonterminal_or_string: Optional[str]
def __init__(
self, next_: NFAState, nonterminal_or_string: Optional[str]
) -> None: ...
class NFAState:
from_rule: str
arcs: List[NFAArc]
def __init__(self, from_rule: str) -> None: ...

View File

@@ -247,7 +247,7 @@ def _update_positions(nodes, line_offset, last_leaf):
_update_positions(children, line_offset, last_leaf) _update_positions(children, line_offset, last_leaf)
class DiffParser(object): class DiffParser:
""" """
An advanced form of parsing a file faster. Unfortunately comes with huge An advanced form of parsing a file faster. Unfortunately comes with huge
side effects. It changes the given module. side effects. It changes the given module.
@@ -514,7 +514,7 @@ class DiffParser(object):
yield token yield token
class _NodesTreeNode(object): class _NodesTreeNode:
_ChildrenGroup = namedtuple( _ChildrenGroup = namedtuple(
'_ChildrenGroup', '_ChildrenGroup',
'prefix children line_offset last_line_offset_leaf') 'prefix children line_offset last_line_offset_leaf')
@@ -589,7 +589,7 @@ class _NodesTreeNode(object):
return '<%s: %s>' % (self.__class__.__name__, self.tree_node) return '<%s: %s>' % (self.__class__.__name__, self.tree_node)
class _NodesTree(object): class _NodesTree:
def __init__(self, module): def __init__(self, module):
self._base_node = _NodesTreeNode(module) self._base_node = _NodesTreeNode(module)
self._working_stack = [self._base_node] self._working_stack = [self._base_node]

View File

@@ -15,10 +15,11 @@ _MAX_BLOCK_SIZE = 20
_MAX_INDENT_COUNT = 100 _MAX_INDENT_COUNT = 100
ALLOWED_FUTURES = ( ALLOWED_FUTURES = (
'nested_scopes', 'generators', 'division', 'absolute_import', 'nested_scopes', 'generators', 'division', 'absolute_import',
'with_statement', 'print_function', 'unicode_literals', 'with_statement', 'print_function', 'unicode_literals', 'generator_stop',
) )
_COMP_FOR_TYPES = ('comp_for', 'sync_comp_for') _COMP_FOR_TYPES = ('comp_for', 'sync_comp_for')
def _get_rhs_name(node, version): def _get_rhs_name(node, version):
type_ = node.type type_ = node.type
if type_ == "lambdef": if type_ == "lambdef":
@@ -39,7 +40,7 @@ def _get_rhs_name(node, version):
elif ( elif (
first == "(" first == "("
and (second == ")" and (second == ")"
or (len(node.children) == 3 and node.children[1].type == "testlist_comp")) or (len(node.children) == 3 and node.children[1].type == "testlist_comp"))
): ):
return "tuple" return "tuple"
elif first == "(": elif first == "(":
@@ -79,8 +80,7 @@ def _get_rhs_name(node, version):
elif trailer.children[0] == ".": elif trailer.children[0] == ".":
return "attribute" return "attribute"
elif ( elif (
("expr" in type_ ("expr" in type_ and "star_expr" not in type_) # is a substring
and "star_expr" not in type_) # is a substring
or "_test" in type_ or "_test" in type_
or type_ in ("term", "factor") or type_ in ("term", "factor")
): ):
@@ -91,7 +91,8 @@ def _get_rhs_name(node, version):
return "tuple" return "tuple"
elif type_ == "fstring": elif type_ == "fstring":
return "f-string expression" return "f-string expression"
return type_ # shouldn't reach here return type_ # shouldn't reach here
def _iter_stmts(scope): def _iter_stmts(scope):
""" """
@@ -173,13 +174,11 @@ def _iter_definition_exprs_from_lists(exprlist):
if child.children[0] == '(': if child.children[0] == '(':
testlist_comp = child.children[1] testlist_comp = child.children[1]
if testlist_comp.type == 'testlist_comp': if testlist_comp.type == 'testlist_comp':
for expr in _iter_definition_exprs_from_lists(testlist_comp): yield from _iter_definition_exprs_from_lists(testlist_comp)
yield expr
return return
else: else:
# It's a paren that doesn't do anything, like 1 + (1) # It's a paren that doesn't do anything, like 1 + (1)
for c in check_expr(testlist_comp): yield from check_expr(testlist_comp)
yield c
return return
elif child.children[0] == '[': elif child.children[0] == '[':
yield testlist_comp yield testlist_comp
@@ -188,11 +187,9 @@ def _iter_definition_exprs_from_lists(exprlist):
if exprlist.type in _STAR_EXPR_PARENTS: if exprlist.type in _STAR_EXPR_PARENTS:
for child in exprlist.children[::2]: for child in exprlist.children[::2]:
for c in check_expr(child): # Python 2 sucks yield from check_expr(child)
yield c
else: else:
for c in check_expr(exprlist): # Python 2 sucks yield from check_expr(exprlist)
yield c
def _get_expr_stmt_definition_exprs(expr_stmt): def _get_expr_stmt_definition_exprs(expr_stmt):
@@ -225,7 +222,7 @@ def _any_fstring_error(version, node):
return search_ancestor(node, "fstring") return search_ancestor(node, "fstring")
class _Context(object): class _Context:
def __init__(self, node, add_syntax_error, parent_context=None): def __init__(self, node, add_syntax_error, parent_context=None):
self.node = node self.node = node
self.blocks = [] self.blocks = []
@@ -353,7 +350,7 @@ class ErrorFinder(Normalizer):
Searches for errors in the syntax tree. Searches for errors in the syntax tree.
""" """
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(ErrorFinder, self).__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self._error_dict = {} self._error_dict = {}
self.version = self.grammar.version_info self.version = self.grammar.version_info
@@ -377,7 +374,7 @@ class ErrorFinder(Normalizer):
# might find errors in there that should be ignored, because # might find errors in there that should be ignored, because
# the error node itself already shows that there's an issue. # the error node itself already shows that there's an issue.
return '' return ''
return super(ErrorFinder, self).visit(node) return super().visit(node)
@contextmanager @contextmanager
def visit_node(self, node): def visit_node(self, node):
@@ -424,7 +421,9 @@ class ErrorFinder(Normalizer):
message = 'invalid syntax' message = 'invalid syntax'
if ( if (
self.version >= (3, 9) self.version >= (3, 9)
and leaf.value in _get_token_collection(self.version).always_break_tokens and leaf.value in _get_token_collection(
self.version
).always_break_tokens
): ):
message = "f-string: " + message message = "f-string: " + message
else: else:
@@ -440,7 +439,7 @@ class ErrorFinder(Normalizer):
self.context = self.context.add_context(parent) self.context = self.context.add_context(parent)
# The rest is rule based. # The rest is rule based.
return super(ErrorFinder, self).visit_leaf(leaf) return super().visit_leaf(leaf)
def _add_indentation_error(self, spacing, message): def _add_indentation_error(self, spacing, message):
self.add_issue(spacing, 903, "IndentationError: " + message) self.add_issue(spacing, 903, "IndentationError: " + message)
@@ -466,7 +465,7 @@ class IndentationRule(Rule):
code = 903 code = 903
def _get_message(self, message, node): def _get_message(self, message, node):
message = super(IndentationRule, self)._get_message(message, node) message = super()._get_message(message, node)
return "IndentationError: " + message return "IndentationError: " + message
@@ -491,7 +490,7 @@ class SyntaxRule(Rule):
code = 901 code = 901
def _get_message(self, message, node): def _get_message(self, message, node):
message = super(SyntaxRule, self)._get_message(message, node) message = super()._get_message(message, node)
if ( if (
"f-string" not in message "f-string" not in message
and _any_fstring_error(self._normalizer.version, node) and _any_fstring_error(self._normalizer.version, node)
@@ -589,9 +588,6 @@ class _NameChecks(SyntaxRule):
if leaf.value == '__debug__' and leaf.is_definition(): if leaf.value == '__debug__' and leaf.is_definition():
return True return True
if leaf.value == 'None' and self._normalizer.version < (3, 0) \
and leaf.is_definition():
self.add_issue(leaf, message=self.message_none)
@ErrorFinder.register_rule(type='string') @ErrorFinder.register_rule(type='string')
@@ -601,7 +597,6 @@ class _StringChecks(SyntaxRule):
def is_issue(self, leaf): def is_issue(self, leaf):
string_prefix = leaf.string_prefix.lower() string_prefix = leaf.string_prefix.lower()
if 'b' in string_prefix \ if 'b' in string_prefix \
and self._normalizer.version >= (3, 0) \
and any(c for c in leaf.value if ord(c) > 127): and any(c for c in leaf.value if ord(c) > 127):
# b'ä' # b'ä'
return True return True
@@ -609,14 +604,9 @@ class _StringChecks(SyntaxRule):
if 'r' not in string_prefix: if 'r' not in string_prefix:
# Raw strings don't need to be checked if they have proper # Raw strings don't need to be checked if they have proper
# escaping. # escaping.
is_bytes = self._normalizer.version < (3, 0)
if 'b' in string_prefix:
is_bytes = True
if 'u' in string_prefix:
is_bytes = False
payload = leaf._get_payload() payload = leaf._get_payload()
if is_bytes: if 'b' in string_prefix:
payload = payload.encode('utf-8') payload = payload.encode('utf-8')
func = codecs.escape_decode func = codecs.escape_decode
else: else:
@@ -675,10 +665,6 @@ class _ReturnAndYieldChecks(SyntaxRule):
and any(self._normalizer.context.node.iter_yield_exprs()): and any(self._normalizer.context.node.iter_yield_exprs()):
if leaf.value == 'return' and leaf.parent.type == 'return_stmt': if leaf.value == 'return' and leaf.parent.type == 'return_stmt':
return True return True
elif leaf.value == 'yield' \
and leaf.get_next_leaf() != 'from' \
and self._normalizer.version == (3, 5):
self.add_issue(self.get_node(leaf), message=self.message_async_yield)
@ErrorFinder.register_rule(type='strings') @ErrorFinder.register_rule(type='strings')
@@ -693,12 +679,10 @@ class _BytesAndStringMix(SyntaxRule):
def is_issue(self, node): def is_issue(self, node):
first = node.children[0] first = node.children[0]
# In Python 2 it's allowed to mix bytes and unicode. first_is_bytes = self._is_bytes_literal(first)
if self._normalizer.version >= (3, 0): for string in node.children[1:]:
first_is_bytes = self._is_bytes_literal(first) if first_is_bytes != self._is_bytes_literal(string):
for string in node.children[1:]: return True
if first_is_bytes != self._is_bytes_literal(string):
return True
@ErrorFinder.register_rule(type='import_as_names') @ErrorFinder.register_rule(type='import_as_names')
@@ -731,8 +715,6 @@ class _FutureImportRule(SyntaxRule):
for from_name, future_name in node.get_paths(): for from_name, future_name in node.get_paths():
name = future_name.value name = future_name.value
allowed_futures = list(ALLOWED_FUTURES) allowed_futures = list(ALLOWED_FUTURES)
if self._normalizer.version >= (3, 5):
allowed_futures.append('generator_stop')
if self._normalizer.version >= (3, 7): if self._normalizer.version >= (3, 7):
allowed_futures.append('annotations') allowed_futures.append('annotations')
if name == 'braces': if name == 'braces':
@@ -755,19 +737,6 @@ class _StarExprRule(SyntaxRule):
# [*[] for a in [1]] # [*[] for a in [1]]
if node.parent.children[1].type in _COMP_FOR_TYPES: if node.parent.children[1].type in _COMP_FOR_TYPES:
self.add_issue(node, message=self.message_iterable_unpacking) self.add_issue(node, message=self.message_iterable_unpacking)
if self._normalizer.version <= (3, 4):
n = search_ancestor(node, 'for_stmt', 'expr_stmt')
found_definition = False
if n is not None:
if n.type == 'expr_stmt':
exprs = _get_expr_stmt_definition_exprs(n)
else:
exprs = _get_for_stmt_definition_exprs(n)
if node in exprs:
found_definition = True
if not found_definition:
self.add_issue(node, message=self.message_assignment)
@ErrorFinder.register_rule(types=_STAR_EXPR_PARENTS) @ErrorFinder.register_rule(types=_STAR_EXPR_PARENTS)
@@ -892,22 +861,10 @@ class _ArglistRule(SyntaxRule):
arg_set = set() arg_set = set()
kw_only = False kw_only = False
kw_unpacking_only = False kw_unpacking_only = False
is_old_starred = False
# In python 3 this would be a bit easier (stars are part of
# argument), but we have to understand both.
for argument in node.children: for argument in node.children:
if argument == ',': if argument == ',':
continue continue
if argument in ('*', '**'):
# Python < 3.5 has the order engraved in the grammar
# file. No need to do anything here.
is_old_starred = True
continue
if is_old_starred:
is_old_starred = False
continue
if argument.type == 'argument': if argument.type == 'argument':
first = argument.children[0] first = argument.children[0]
if _is_argument_comprehension(argument) and len(node.children) >= 2: if _is_argument_comprehension(argument) and len(node.children) >= 2:
@@ -1000,7 +957,11 @@ class _FStringRule(SyntaxRule):
if '\\' in expr.get_code(): if '\\' in expr.get_code():
self.add_issue(expr, message=self.message_expr) self.add_issue(expr, message=self.message_expr)
conversion = fstring_expr.children[2] children_2 = fstring_expr.children[2]
if children_2.type == 'operator' and children_2.value == '=':
conversion = fstring_expr.children[3]
else:
conversion = children_2
if conversion.type == 'fstring_conversion': if conversion.type == 'fstring_conversion':
name = conversion.children[1] name = conversion.children[1]
if name.value not in ('s', 'r', 'a'): if name.value not in ('s', 'r', 'a'):
@@ -1149,6 +1110,7 @@ class _CompForRule(_CheckAssignmentRule):
class _ExprStmtRule(_CheckAssignmentRule): class _ExprStmtRule(_CheckAssignmentRule):
message = "illegal expression for augmented assignment" message = "illegal expression for augmented assignment"
extended_message = "'{target}' is an " + message extended_message = "'{target}' is an " + message
def is_issue(self, node): def is_issue(self, node):
augassign = node.children[1] augassign = node.children[1]
is_aug_assign = augassign != '=' and augassign.type != 'annassign' is_aug_assign = augassign != '=' and augassign.type != 'annassign'
@@ -1178,6 +1140,7 @@ class _ExprStmtRule(_CheckAssignmentRule):
), ),
) )
@ErrorFinder.register_rule(type='with_item') @ErrorFinder.register_rule(type='with_item')
class _WithItemRule(_CheckAssignmentRule): class _WithItemRule(_CheckAssignmentRule):
def is_issue(self, with_item): def is_issue(self, with_item):

View File

@@ -1,143 +0,0 @@
# Grammar for Python
# Note: Changing the grammar specified in this file will most likely
# require corresponding changes in the parser module
# (../Modules/parsermodule.c). If you can't make the changes to
# that module yourself, please co-ordinate the required changes
# with someone who can; ask around on python-dev for help. Fred
# Drake <fdrake@acm.org> will probably be listening there.
# NOTE WELL: You should also follow all the steps listed in PEP 306,
# "How to Change Python's Grammar"
# Start symbols for the grammar:
# single_input is a single interactive statement;
# file_input is a module or sequence of commands read from an input file;
# eval_input is the input for the eval() and input() functions.
# NB: compound_stmt in single_input is followed by extra NEWLINE!
single_input: NEWLINE | simple_stmt | compound_stmt NEWLINE
file_input: stmt* ENDMARKER
eval_input: testlist NEWLINE* ENDMARKER
decorator: '@' dotted_name [ '(' [arglist] ')' ] NEWLINE
decorators: decorator+
decorated: decorators (classdef | funcdef)
funcdef: 'def' NAME parameters ':' suite
parameters: '(' [varargslist] ')'
varargslist: ((fpdef ['=' test] ',')*
('*' NAME [',' '**' NAME] | '**' NAME) |
fpdef ['=' test] (',' fpdef ['=' test])* [','])
fpdef: NAME | '(' fplist ')'
fplist: fpdef (',' fpdef)* [',']
stmt: simple_stmt | compound_stmt | NEWLINE
simple_stmt: small_stmt (';' small_stmt)* [';'] NEWLINE
small_stmt: (expr_stmt | print_stmt | del_stmt | pass_stmt | flow_stmt |
import_stmt | global_stmt | exec_stmt | assert_stmt)
expr_stmt: testlist (augassign (yield_expr|testlist) |
('=' (yield_expr|testlist))*)
augassign: ('+=' | '-=' | '*=' | '/=' | '%=' | '&=' | '|=' | '^=' |
'<<=' | '>>=' | '**=' | '//=')
# For normal assignments, additional restrictions enforced by the interpreter
print_stmt: 'print' ( [ test (',' test)* [','] ] |
'>>' test [ (',' test)+ [','] ] )
del_stmt: 'del' exprlist
pass_stmt: 'pass'
flow_stmt: break_stmt | continue_stmt | return_stmt | raise_stmt | yield_stmt
break_stmt: 'break'
continue_stmt: 'continue'
return_stmt: 'return' [testlist]
yield_stmt: yield_expr
raise_stmt: 'raise' [test [',' test [',' test]]]
import_stmt: import_name | import_from
import_name: 'import' dotted_as_names
import_from: ('from' ('.'* dotted_name | '.'+)
'import' ('*' | '(' import_as_names ')' | import_as_names))
import_as_name: NAME ['as' NAME]
dotted_as_name: dotted_name ['as' NAME]
import_as_names: import_as_name (',' import_as_name)* [',']
dotted_as_names: dotted_as_name (',' dotted_as_name)*
dotted_name: NAME ('.' NAME)*
global_stmt: 'global' NAME (',' NAME)*
exec_stmt: 'exec' expr ['in' test [',' test]]
assert_stmt: 'assert' test [',' test]
compound_stmt: if_stmt | while_stmt | for_stmt | try_stmt | with_stmt | funcdef | classdef | decorated
if_stmt: 'if' test ':' suite ('elif' test ':' suite)* ['else' ':' suite]
while_stmt: 'while' test ':' suite ['else' ':' suite]
for_stmt: 'for' exprlist 'in' testlist ':' suite ['else' ':' suite]
try_stmt: ('try' ':' suite
((except_clause ':' suite)+
['else' ':' suite]
['finally' ':' suite] |
'finally' ':' suite))
with_stmt: 'with' with_item (',' with_item)* ':' suite
with_item: test ['as' expr]
# NB compile.c makes sure that the default except clause is last
except_clause: 'except' [test [('as' | ',') test]]
suite: simple_stmt | NEWLINE INDENT stmt+ DEDENT
# Backward compatibility cruft to support:
# [ x for x in lambda: True, lambda: False if x() ]
# even while also allowing:
# lambda x: 5 if x else 2
# (But not a mix of the two)
testlist_safe: old_test [(',' old_test)+ [',']]
old_test: or_test | old_lambdef
old_lambdef: 'lambda' [varargslist] ':' old_test
test: or_test ['if' or_test 'else' test] | lambdef
or_test: and_test ('or' and_test)*
and_test: not_test ('and' not_test)*
not_test: 'not' not_test | comparison
comparison: expr (comp_op expr)*
comp_op: '<'|'>'|'=='|'>='|'<='|'<>'|'!='|'in'|'not' 'in'|'is'|'is' 'not'
expr: xor_expr ('|' xor_expr)*
xor_expr: and_expr ('^' and_expr)*
and_expr: shift_expr ('&' shift_expr)*
shift_expr: arith_expr (('<<'|'>>') arith_expr)*
arith_expr: term (('+'|'-') term)*
term: factor (('*'|'/'|'%'|'//') factor)*
factor: ('+'|'-'|'~') factor | power
power: atom trailer* ['**' factor]
atom: ('(' [yield_expr|testlist_comp] ')' |
'[' [listmaker] ']' |
'{' [dictorsetmaker] '}' |
'`' testlist1 '`' |
NAME | NUMBER | strings)
strings: STRING+
listmaker: test ( list_for | (',' test)* [','] )
testlist_comp: test ( sync_comp_for | (',' test)* [','] )
lambdef: 'lambda' [varargslist] ':' test
trailer: '(' [arglist] ')' | '[' subscriptlist ']' | '.' NAME
subscriptlist: subscript (',' subscript)* [',']
subscript: '.' '.' '.' | test | [test] ':' [test] [sliceop]
sliceop: ':' [test]
exprlist: expr (',' expr)* [',']
testlist: test (',' test)* [',']
dictorsetmaker: ( (test ':' test (sync_comp_for | (',' test ':' test)* [','])) |
(test (sync_comp_for | (',' test)* [','])) )
classdef: 'class' NAME ['(' [testlist] ')'] ':' suite
arglist: (argument ',')* (argument [',']
|'*' test (',' argument)* [',' '**' test]
|'**' test)
# The reason that keywords are test nodes instead of NAME is that using NAME
# results in an ambiguity. ast.c makes sure it's a NAME.
argument: test [sync_comp_for] | test '=' test
list_iter: list_for | list_if
list_for: 'for' exprlist 'in' testlist_safe [list_iter]
list_if: 'if' old_test [list_iter]
comp_iter: sync_comp_for | comp_if
sync_comp_for: 'for' exprlist 'in' or_test [comp_iter]
comp_if: 'if' old_test [comp_iter]
testlist1: test (',' test)*
# not used in grammar, but may appear in "node" passed from Parser to Compiler
encoding_decl: NAME
yield_expr: 'yield' [testlist]

View File

@@ -124,14 +124,14 @@ atom: ('(' [yield_expr|testlist_comp] ')' |
testlist_comp: (namedexpr_test|star_expr) ( comp_for | (',' (namedexpr_test|star_expr))* [','] ) testlist_comp: (namedexpr_test|star_expr) ( comp_for | (',' (namedexpr_test|star_expr))* [','] )
trailer: '(' [arglist] ')' | '[' subscriptlist ']' | '.' NAME trailer: '(' [arglist] ')' | '[' subscriptlist ']' | '.' NAME
subscriptlist: subscript (',' subscript)* [','] subscriptlist: subscript (',' subscript)* [',']
subscript: test | [test] ':' [test] [sliceop] subscript: test [':=' test] | [test] ':' [test] [sliceop]
sliceop: ':' [test] sliceop: ':' [test]
exprlist: (expr|star_expr) (',' (expr|star_expr))* [','] exprlist: (expr|star_expr) (',' (expr|star_expr))* [',']
testlist: test (',' test)* [','] testlist: test (',' test)* [',']
dictorsetmaker: ( ((test ':' test | '**' expr) dictorsetmaker: ( ((test ':' test | '**' expr)
(comp_for | (',' (test ':' test | '**' expr))* [','])) | (comp_for | (',' (test ':' test | '**' expr))* [','])) |
((test | star_expr) ((test [':=' test] | star_expr)
(comp_for | (',' (test | star_expr))* [','])) ) (comp_for | (',' (test [':=' test] | star_expr))* [','])) )
classdef: 'class' NAME ['(' [arglist] ')'] ':' suite classdef: 'class' NAME ['(' [arglist] ')'] ':' suite
@@ -167,5 +167,5 @@ strings: (STRING | fstring)+
fstring: FSTRING_START fstring_content* FSTRING_END fstring: FSTRING_START fstring_content* FSTRING_END
fstring_content: FSTRING_STRING | fstring_expr fstring_content: FSTRING_STRING | fstring_expr
fstring_conversion: '!' NAME fstring_conversion: '!' NAME
fstring_expr: '{' testlist ['='] [ fstring_conversion ] [ fstring_format_spec ] '}' fstring_expr: '{' (testlist_comp | yield_expr) ['='] [ fstring_conversion ] [ fstring_format_spec ] '}'
fstring_format_spec: ':' fstring_content* fstring_format_spec: ':' fstring_content*

View File

@@ -1,134 +0,0 @@
# Grammar for Python
# Note: Changing the grammar specified in this file will most likely
# require corresponding changes in the parser module
# (../Modules/parsermodule.c). If you can't make the changes to
# that module yourself, please co-ordinate the required changes
# with someone who can; ask around on python-dev for help. Fred
# Drake <fdrake@acm.org> will probably be listening there.
# NOTE WELL: You should also follow all the steps listed in PEP 306,
# "How to Change Python's Grammar"
# Start symbols for the grammar:
# single_input is a single interactive statement;
# file_input is a module or sequence of commands read from an input file;
# eval_input is the input for the eval() functions.
# NB: compound_stmt in single_input is followed by extra NEWLINE!
single_input: NEWLINE | simple_stmt | compound_stmt NEWLINE
file_input: stmt* ENDMARKER
eval_input: testlist NEWLINE* ENDMARKER
decorator: '@' dotted_name [ '(' [arglist] ')' ] NEWLINE
decorators: decorator+
decorated: decorators (classdef | funcdef)
funcdef: 'def' NAME parameters ['->' test] ':' suite
parameters: '(' [typedargslist] ')'
typedargslist: (tfpdef ['=' test] (',' tfpdef ['=' test])* [','
['*' [tfpdef] (',' tfpdef ['=' test])* [',' '**' tfpdef] | '**' tfpdef]]
| '*' [tfpdef] (',' tfpdef ['=' test])* [',' '**' tfpdef] | '**' tfpdef)
tfpdef: NAME [':' test]
varargslist: (vfpdef ['=' test] (',' vfpdef ['=' test])* [','
['*' [vfpdef] (',' vfpdef ['=' test])* [',' '**' vfpdef] | '**' vfpdef]]
| '*' [vfpdef] (',' vfpdef ['=' test])* [',' '**' vfpdef] | '**' vfpdef)
vfpdef: NAME
stmt: simple_stmt | compound_stmt | NEWLINE
simple_stmt: small_stmt (';' small_stmt)* [';'] NEWLINE
small_stmt: (expr_stmt | del_stmt | pass_stmt | flow_stmt |
import_stmt | global_stmt | nonlocal_stmt | assert_stmt)
expr_stmt: testlist_star_expr (augassign (yield_expr|testlist) |
('=' (yield_expr|testlist_star_expr))*)
testlist_star_expr: (test|star_expr) (',' (test|star_expr))* [',']
augassign: ('+=' | '-=' | '*=' | '/=' | '%=' | '&=' | '|=' | '^=' |
'<<=' | '>>=' | '**=' | '//=')
# For normal assignments, additional restrictions enforced by the interpreter
del_stmt: 'del' exprlist
pass_stmt: 'pass'
flow_stmt: break_stmt | continue_stmt | return_stmt | raise_stmt | yield_stmt
break_stmt: 'break'
continue_stmt: 'continue'
return_stmt: 'return' [testlist]
yield_stmt: yield_expr
raise_stmt: 'raise' [test ['from' test]]
import_stmt: import_name | import_from
import_name: 'import' dotted_as_names
# note below: the ('.' | '...') is necessary because '...' is tokenized as ELLIPSIS
import_from: ('from' (('.' | '...')* dotted_name | ('.' | '...')+)
'import' ('*' | '(' import_as_names ')' | import_as_names))
import_as_name: NAME ['as' NAME]
dotted_as_name: dotted_name ['as' NAME]
import_as_names: import_as_name (',' import_as_name)* [',']
dotted_as_names: dotted_as_name (',' dotted_as_name)*
dotted_name: NAME ('.' NAME)*
global_stmt: 'global' NAME (',' NAME)*
nonlocal_stmt: 'nonlocal' NAME (',' NAME)*
assert_stmt: 'assert' test [',' test]
compound_stmt: if_stmt | while_stmt | for_stmt | try_stmt | with_stmt | funcdef | classdef | decorated
if_stmt: 'if' test ':' suite ('elif' test ':' suite)* ['else' ':' suite]
while_stmt: 'while' test ':' suite ['else' ':' suite]
for_stmt: 'for' exprlist 'in' testlist ':' suite ['else' ':' suite]
try_stmt: ('try' ':' suite
((except_clause ':' suite)+
['else' ':' suite]
['finally' ':' suite] |
'finally' ':' suite))
with_stmt: 'with' with_item (',' with_item)* ':' suite
with_item: test ['as' expr]
# NB compile.c makes sure that the default except clause is last
except_clause: 'except' [test ['as' NAME]]
suite: simple_stmt | NEWLINE INDENT stmt+ DEDENT
test: or_test ['if' or_test 'else' test] | lambdef
test_nocond: or_test | lambdef_nocond
lambdef: 'lambda' [varargslist] ':' test
lambdef_nocond: 'lambda' [varargslist] ':' test_nocond
or_test: and_test ('or' and_test)*
and_test: not_test ('and' not_test)*
not_test: 'not' not_test | comparison
comparison: expr (comp_op expr)*
# <> isn't actually a valid comparison operator in Python. It's here for the
# sake of a __future__ import described in PEP 401
comp_op: '<'|'>'|'=='|'>='|'<='|'<>'|'!='|'in'|'not' 'in'|'is'|'is' 'not'
star_expr: '*' expr
expr: xor_expr ('|' xor_expr)*
xor_expr: and_expr ('^' and_expr)*
and_expr: shift_expr ('&' shift_expr)*
shift_expr: arith_expr (('<<'|'>>') arith_expr)*
arith_expr: term (('+'|'-') term)*
term: factor (('*'|'/'|'%'|'//') factor)*
factor: ('+'|'-'|'~') factor | power
power: atom trailer* ['**' factor]
atom: ('(' [yield_expr|testlist_comp] ')' |
'[' [testlist_comp] ']' |
'{' [dictorsetmaker] '}' |
NAME | NUMBER | strings | '...' | 'None' | 'True' | 'False')
strings: STRING+
testlist_comp: (test|star_expr) ( sync_comp_for | (',' (test|star_expr))* [','] )
trailer: '(' [arglist] ')' | '[' subscriptlist ']' | '.' NAME
subscriptlist: subscript (',' subscript)* [',']
subscript: test | [test] ':' [test] [sliceop]
sliceop: ':' [test]
exprlist: (expr|star_expr) (',' (expr|star_expr))* [',']
testlist: test (',' test)* [',']
dictorsetmaker: ( (test ':' test (sync_comp_for | (',' test ':' test)* [','])) |
(test (sync_comp_for | (',' test)* [','])) )
classdef: 'class' NAME ['(' [arglist] ')'] ':' suite
arglist: (argument ',')* (argument [',']
|'*' test (',' argument)* [',' '**' test]
|'**' test)
# The reason that keywords are test nodes instead of NAME is that using NAME
# results in an ambiguity. ast.c makes sure it's a NAME.
argument: test [sync_comp_for] | test '=' test # Really [keyword '='] test
comp_iter: sync_comp_for | comp_if
sync_comp_for: 'for' exprlist 'in' or_test [comp_iter]
comp_if: 'if' test_nocond [comp_iter]
# not used in grammar, but may appear in "node" passed from Parser to Compiler
encoding_decl: NAME
yield_expr: 'yield' [yield_arg]
yield_arg: 'from' test | testlist

View File

@@ -1,134 +0,0 @@
# Grammar for Python
# Note: Changing the grammar specified in this file will most likely
# require corresponding changes in the parser module
# (../Modules/parsermodule.c). If you can't make the changes to
# that module yourself, please co-ordinate the required changes
# with someone who can; ask around on python-dev for help. Fred
# Drake <fdrake@acm.org> will probably be listening there.
# NOTE WELL: You should also follow all the steps listed at
# https://docs.python.org/devguide/grammar.html
# Start symbols for the grammar:
# single_input is a single interactive statement;
# file_input is a module or sequence of commands read from an input file;
# eval_input is the input for the eval() functions.
# NB: compound_stmt in single_input is followed by extra NEWLINE!
single_input: NEWLINE | simple_stmt | compound_stmt NEWLINE
file_input: stmt* ENDMARKER
eval_input: testlist NEWLINE* ENDMARKER
decorator: '@' dotted_name [ '(' [arglist] ')' ] NEWLINE
decorators: decorator+
decorated: decorators (classdef | funcdef)
funcdef: 'def' NAME parameters ['->' test] ':' suite
parameters: '(' [typedargslist] ')'
typedargslist: (tfpdef ['=' test] (',' tfpdef ['=' test])* [','
['*' [tfpdef] (',' tfpdef ['=' test])* [',' '**' tfpdef] | '**' tfpdef]]
| '*' [tfpdef] (',' tfpdef ['=' test])* [',' '**' tfpdef] | '**' tfpdef)
tfpdef: NAME [':' test]
varargslist: (vfpdef ['=' test] (',' vfpdef ['=' test])* [','
['*' [vfpdef] (',' vfpdef ['=' test])* [',' '**' vfpdef] | '**' vfpdef]]
| '*' [vfpdef] (',' vfpdef ['=' test])* [',' '**' vfpdef] | '**' vfpdef)
vfpdef: NAME
stmt: simple_stmt | compound_stmt | NEWLINE
simple_stmt: small_stmt (';' small_stmt)* [';'] NEWLINE
small_stmt: (expr_stmt | del_stmt | pass_stmt | flow_stmt |
import_stmt | global_stmt | nonlocal_stmt | assert_stmt)
expr_stmt: testlist_star_expr (augassign (yield_expr|testlist) |
('=' (yield_expr|testlist_star_expr))*)
testlist_star_expr: (test|star_expr) (',' (test|star_expr))* [',']
augassign: ('+=' | '-=' | '*=' | '/=' | '%=' | '&=' | '|=' | '^=' |
'<<=' | '>>=' | '**=' | '//=')
# For normal assignments, additional restrictions enforced by the interpreter
del_stmt: 'del' exprlist
pass_stmt: 'pass'
flow_stmt: break_stmt | continue_stmt | return_stmt | raise_stmt | yield_stmt
break_stmt: 'break'
continue_stmt: 'continue'
return_stmt: 'return' [testlist]
yield_stmt: yield_expr
raise_stmt: 'raise' [test ['from' test]]
import_stmt: import_name | import_from
import_name: 'import' dotted_as_names
# note below: the ('.' | '...') is necessary because '...' is tokenized as ELLIPSIS
import_from: ('from' (('.' | '...')* dotted_name | ('.' | '...')+)
'import' ('*' | '(' import_as_names ')' | import_as_names))
import_as_name: NAME ['as' NAME]
dotted_as_name: dotted_name ['as' NAME]
import_as_names: import_as_name (',' import_as_name)* [',']
dotted_as_names: dotted_as_name (',' dotted_as_name)*
dotted_name: NAME ('.' NAME)*
global_stmt: 'global' NAME (',' NAME)*
nonlocal_stmt: 'nonlocal' NAME (',' NAME)*
assert_stmt: 'assert' test [',' test]
compound_stmt: if_stmt | while_stmt | for_stmt | try_stmt | with_stmt | funcdef | classdef | decorated
if_stmt: 'if' test ':' suite ('elif' test ':' suite)* ['else' ':' suite]
while_stmt: 'while' test ':' suite ['else' ':' suite]
for_stmt: 'for' exprlist 'in' testlist ':' suite ['else' ':' suite]
try_stmt: ('try' ':' suite
((except_clause ':' suite)+
['else' ':' suite]
['finally' ':' suite] |
'finally' ':' suite))
with_stmt: 'with' with_item (',' with_item)* ':' suite
with_item: test ['as' expr]
# NB compile.c makes sure that the default except clause is last
except_clause: 'except' [test ['as' NAME]]
suite: simple_stmt | NEWLINE INDENT stmt+ DEDENT
test: or_test ['if' or_test 'else' test] | lambdef
test_nocond: or_test | lambdef_nocond
lambdef: 'lambda' [varargslist] ':' test
lambdef_nocond: 'lambda' [varargslist] ':' test_nocond
or_test: and_test ('or' and_test)*
and_test: not_test ('and' not_test)*
not_test: 'not' not_test | comparison
comparison: expr (comp_op expr)*
# <> isn't actually a valid comparison operator in Python. It's here for the
# sake of a __future__ import described in PEP 401
comp_op: '<'|'>'|'=='|'>='|'<='|'<>'|'!='|'in'|'not' 'in'|'is'|'is' 'not'
star_expr: '*' expr
expr: xor_expr ('|' xor_expr)*
xor_expr: and_expr ('^' and_expr)*
and_expr: shift_expr ('&' shift_expr)*
shift_expr: arith_expr (('<<'|'>>') arith_expr)*
arith_expr: term (('+'|'-') term)*
term: factor (('*'|'/'|'%'|'//') factor)*
factor: ('+'|'-'|'~') factor | power
power: atom trailer* ['**' factor]
atom: ('(' [yield_expr|testlist_comp] ')' |
'[' [testlist_comp] ']' |
'{' [dictorsetmaker] '}' |
NAME | NUMBER | strings | '...' | 'None' | 'True' | 'False')
strings: STRING+
testlist_comp: (test|star_expr) ( sync_comp_for | (',' (test|star_expr))* [','] )
trailer: '(' [arglist] ')' | '[' subscriptlist ']' | '.' NAME
subscriptlist: subscript (',' subscript)* [',']
subscript: test | [test] ':' [test] [sliceop]
sliceop: ':' [test]
exprlist: (expr|star_expr) (',' (expr|star_expr))* [',']
testlist: test (',' test)* [',']
dictorsetmaker: ( (test ':' test (sync_comp_for | (',' test ':' test)* [','])) |
(test (sync_comp_for | (',' test)* [','])) )
classdef: 'class' NAME ['(' [arglist] ')'] ':' suite
arglist: (argument ',')* (argument [',']
|'*' test (',' argument)* [',' '**' test]
|'**' test)
# The reason that keywords are test nodes instead of NAME is that using NAME
# results in an ambiguity. ast.c makes sure it's a NAME.
argument: test [sync_comp_for] | test '=' test # Really [keyword '='] test
comp_iter: sync_comp_for | comp_if
sync_comp_for: 'for' exprlist 'in' or_test [comp_iter]
comp_if: 'if' test_nocond [comp_iter]
# not used in grammar, but may appear in "node" passed from Parser to Compiler
encoding_decl: NAME
yield_expr: 'yield' [yield_arg]
yield_arg: 'from' test | testlist

View File

@@ -1,153 +0,0 @@
# Grammar for Python
# Note: Changing the grammar specified in this file will most likely
# require corresponding changes in the parser module
# (../Modules/parsermodule.c). If you can't make the changes to
# that module yourself, please co-ordinate the required changes
# with someone who can; ask around on python-dev for help. Fred
# Drake <fdrake@acm.org> will probably be listening there.
# NOTE WELL: You should also follow all the steps listed at
# https://docs.python.org/devguide/grammar.html
# Start symbols for the grammar:
# single_input is a single interactive statement;
# file_input is a module or sequence of commands read from an input file;
# eval_input is the input for the eval() functions.
# NB: compound_stmt in single_input is followed by extra NEWLINE!
single_input: NEWLINE | simple_stmt | compound_stmt NEWLINE
file_input: stmt* ENDMARKER
eval_input: testlist NEWLINE* ENDMARKER
decorator: '@' dotted_name [ '(' [arglist] ')' ] NEWLINE
decorators: decorator+
decorated: decorators (classdef | funcdef | async_funcdef)
# NOTE: Reinoud Elhorst, using ASYNC/AWAIT keywords instead of tokens
# skipping python3.5 compatibility, in favour of 3.7 solution
async_funcdef: 'async' funcdef
funcdef: 'def' NAME parameters ['->' test] ':' suite
parameters: '(' [typedargslist] ')'
typedargslist: (tfpdef ['=' test] (',' tfpdef ['=' test])* [','
['*' [tfpdef] (',' tfpdef ['=' test])* [',' '**' tfpdef] | '**' tfpdef]]
| '*' [tfpdef] (',' tfpdef ['=' test])* [',' '**' tfpdef] | '**' tfpdef)
tfpdef: NAME [':' test]
varargslist: (vfpdef ['=' test] (',' vfpdef ['=' test])* [','
['*' [vfpdef] (',' vfpdef ['=' test])* [',' '**' vfpdef] | '**' vfpdef]]
| '*' [vfpdef] (',' vfpdef ['=' test])* [',' '**' vfpdef] | '**' vfpdef)
vfpdef: NAME
stmt: simple_stmt | compound_stmt | NEWLINE
simple_stmt: small_stmt (';' small_stmt)* [';'] NEWLINE
small_stmt: (expr_stmt | del_stmt | pass_stmt | flow_stmt |
import_stmt | global_stmt | nonlocal_stmt | assert_stmt)
expr_stmt: testlist_star_expr (augassign (yield_expr|testlist) |
('=' (yield_expr|testlist_star_expr))*)
testlist_star_expr: (test|star_expr) (',' (test|star_expr))* [',']
augassign: ('+=' | '-=' | '*=' | '@=' | '/=' | '%=' | '&=' | '|=' | '^=' |
'<<=' | '>>=' | '**=' | '//=')
# For normal assignments, additional restrictions enforced by the interpreter
del_stmt: 'del' exprlist
pass_stmt: 'pass'
flow_stmt: break_stmt | continue_stmt | return_stmt | raise_stmt | yield_stmt
break_stmt: 'break'
continue_stmt: 'continue'
return_stmt: 'return' [testlist]
yield_stmt: yield_expr
raise_stmt: 'raise' [test ['from' test]]
import_stmt: import_name | import_from
import_name: 'import' dotted_as_names
# note below: the ('.' | '...') is necessary because '...' is tokenized as ELLIPSIS
import_from: ('from' (('.' | '...')* dotted_name | ('.' | '...')+)
'import' ('*' | '(' import_as_names ')' | import_as_names))
import_as_name: NAME ['as' NAME]
dotted_as_name: dotted_name ['as' NAME]
import_as_names: import_as_name (',' import_as_name)* [',']
dotted_as_names: dotted_as_name (',' dotted_as_name)*
dotted_name: NAME ('.' NAME)*
global_stmt: 'global' NAME (',' NAME)*
nonlocal_stmt: 'nonlocal' NAME (',' NAME)*
assert_stmt: 'assert' test [',' test]
compound_stmt: if_stmt | while_stmt | for_stmt | try_stmt | with_stmt | funcdef | classdef | decorated | async_stmt
async_stmt: 'async' (funcdef | with_stmt | for_stmt)
if_stmt: 'if' test ':' suite ('elif' test ':' suite)* ['else' ':' suite]
while_stmt: 'while' test ':' suite ['else' ':' suite]
for_stmt: 'for' exprlist 'in' testlist ':' suite ['else' ':' suite]
try_stmt: ('try' ':' suite
((except_clause ':' suite)+
['else' ':' suite]
['finally' ':' suite] |
'finally' ':' suite))
with_stmt: 'with' with_item (',' with_item)* ':' suite
with_item: test ['as' expr]
# NB compile.c makes sure that the default except clause is last
except_clause: 'except' [test ['as' NAME]]
suite: simple_stmt | NEWLINE INDENT stmt+ DEDENT
test: or_test ['if' or_test 'else' test] | lambdef
test_nocond: or_test | lambdef_nocond
lambdef: 'lambda' [varargslist] ':' test
lambdef_nocond: 'lambda' [varargslist] ':' test_nocond
or_test: and_test ('or' and_test)*
and_test: not_test ('and' not_test)*
not_test: 'not' not_test | comparison
comparison: expr (comp_op expr)*
# <> isn't actually a valid comparison operator in Python. It's here for the
# sake of a __future__ import described in PEP 401 (which really works :-)
comp_op: '<'|'>'|'=='|'>='|'<='|'<>'|'!='|'in'|'not' 'in'|'is'|'is' 'not'
star_expr: '*' expr
expr: xor_expr ('|' xor_expr)*
xor_expr: and_expr ('^' and_expr)*
and_expr: shift_expr ('&' shift_expr)*
shift_expr: arith_expr (('<<'|'>>') arith_expr)*
arith_expr: term (('+'|'-') term)*
term: factor (('*'|'@'|'/'|'%'|'//') factor)*
factor: ('+'|'-'|'~') factor | power
power: atom_expr ['**' factor]
atom_expr: ['await'] atom trailer*
atom: ('(' [yield_expr|testlist_comp] ')' |
'[' [testlist_comp] ']' |
'{' [dictorsetmaker] '}' |
NAME | NUMBER | strings | '...' | 'None' | 'True' | 'False')
strings: STRING+
testlist_comp: (test|star_expr) ( sync_comp_for | (',' (test|star_expr))* [','] )
trailer: '(' [arglist] ')' | '[' subscriptlist ']' | '.' NAME
subscriptlist: subscript (',' subscript)* [',']
subscript: test | [test] ':' [test] [sliceop]
sliceop: ':' [test]
exprlist: (expr|star_expr) (',' (expr|star_expr))* [',']
testlist: test (',' test)* [',']
dictorsetmaker: ( ((test ':' test | '**' expr)
(sync_comp_for | (',' (test ':' test | '**' expr))* [','])) |
((test | star_expr)
(sync_comp_for | (',' (test | star_expr))* [','])) )
classdef: 'class' NAME ['(' [arglist] ')'] ':' suite
arglist: argument (',' argument)* [',']
# The reason that keywords are test nodes instead of NAME is that using NAME
# results in an ambiguity. ast.c makes sure it's a NAME.
# "test '=' test" is really "keyword '=' test", but we have no such token.
# These need to be in a single rule to avoid grammar that is ambiguous
# to our LL(1) parser. Even though 'test' includes '*expr' in star_expr,
# we explicitly match '*' here, too, to give it proper precedence.
# Illegal combinations and orderings are blocked in ast.c:
# multiple (test comp_for) arguments are blocked; keyword unpackings
# that precede iterable unpackings are blocked; etc.
argument: ( test [sync_comp_for] |
test '=' test |
'**' test |
'*' test )
comp_iter: sync_comp_for | comp_if
sync_comp_for: 'for' exprlist 'in' or_test [comp_iter]
comp_if: 'if' test_nocond [comp_iter]
# not used in grammar, but may appear in "node" passed from Parser to Compiler
encoding_decl: NAME
yield_expr: 'yield' [yield_arg]
yield_arg: 'from' test | testlist

View File

@@ -154,5 +154,5 @@ strings: (STRING | fstring)+
fstring: FSTRING_START fstring_content* FSTRING_END fstring: FSTRING_START fstring_content* FSTRING_END
fstring_content: FSTRING_STRING | fstring_expr fstring_content: FSTRING_STRING | fstring_expr
fstring_conversion: '!' NAME fstring_conversion: '!' NAME
fstring_expr: '{' testlist_comp [ fstring_conversion ] [ fstring_format_spec ] '}' fstring_expr: '{' (testlist_comp | yield_expr) [ fstring_conversion ] [ fstring_format_spec ] '}'
fstring_format_spec: ':' fstring_content* fstring_format_spec: ':' fstring_content*

View File

@@ -152,5 +152,5 @@ strings: (STRING | fstring)+
fstring: FSTRING_START fstring_content* FSTRING_END fstring: FSTRING_START fstring_content* FSTRING_END
fstring_content: FSTRING_STRING | fstring_expr fstring_content: FSTRING_STRING | fstring_expr
fstring_conversion: '!' NAME fstring_conversion: '!' NAME
fstring_expr: '{' testlist [ fstring_conversion ] [ fstring_format_spec ] '}' fstring_expr: '{' (testlist_comp | yield_expr) [ fstring_conversion ] [ fstring_format_spec ] '}'
fstring_format_spec: ':' fstring_content* fstring_format_spec: ':' fstring_content*

View File

@@ -167,5 +167,5 @@ strings: (STRING | fstring)+
fstring: FSTRING_START fstring_content* FSTRING_END fstring: FSTRING_START fstring_content* FSTRING_END
fstring_content: FSTRING_STRING | fstring_expr fstring_content: FSTRING_STRING | fstring_expr
fstring_conversion: '!' NAME fstring_conversion: '!' NAME
fstring_expr: '{' testlist ['='] [ fstring_conversion ] [ fstring_format_spec ] '}' fstring_expr: '{' (testlist_comp | yield_expr) ['='] [ fstring_conversion ] [ fstring_format_spec ] '}'
fstring_format_spec: ':' fstring_content* fstring_format_spec: ':' fstring_content*

View File

@@ -130,8 +130,8 @@ exprlist: (expr|star_expr) (',' (expr|star_expr))* [',']
testlist: test (',' test)* [','] testlist: test (',' test)* [',']
dictorsetmaker: ( ((test ':' test | '**' expr) dictorsetmaker: ( ((test ':' test | '**' expr)
(comp_for | (',' (test ':' test | '**' expr))* [','])) | (comp_for | (',' (test ':' test | '**' expr))* [','])) |
((test | star_expr) ((test [':=' test] | star_expr)
(comp_for | (',' (test | star_expr))* [','])) ) (comp_for | (',' (test [':=' test] | star_expr))* [','])) )
classdef: 'class' NAME ['(' [arglist] ')'] ':' suite classdef: 'class' NAME ['(' [arglist] ')'] ':' suite
@@ -167,5 +167,5 @@ strings: (STRING | fstring)+
fstring: FSTRING_START fstring_content* FSTRING_END fstring: FSTRING_START fstring_content* FSTRING_END
fstring_content: FSTRING_STRING | fstring_expr fstring_content: FSTRING_STRING | fstring_expr
fstring_conversion: '!' NAME fstring_conversion: '!' NAME
fstring_expr: '{' testlist ['='] [ fstring_conversion ] [ fstring_format_spec ] '}' fstring_expr: '{' (testlist_comp | yield_expr) ['='] [ fstring_conversion ] [ fstring_format_spec ] '}'
fstring_format_spec: ':' fstring_content* fstring_format_spec: ':' fstring_content*

View File

@@ -24,7 +24,6 @@ A list of syntax/indentation errors I've encountered in CPython.
# Just ignore this one, newer versions will not be affected anymore and # Just ignore this one, newer versions will not be affected anymore and
# it's a limit of 2^16 - 1. # it's a limit of 2^16 - 1.
"too many annotations" # Only python 3.0 - 3.5, 3.6 is not affected.
# Python/ast.c # Python/ast.c
# used with_item exprlist expr_stmt # used with_item exprlist expr_stmt
@@ -54,8 +53,8 @@ A list of syntax/indentation errors I've encountered in CPython.
"iterable unpacking cannot be used in comprehension" # [*[] for a in [1]] "iterable unpacking cannot be used in comprehension" # [*[] for a in [1]]
"dict unpacking cannot be used in dict comprehension" # {**{} for a in [1]} "dict unpacking cannot be used in dict comprehension" # {**{} for a in [1]}
"Generator expression must be parenthesized if not sole argument" # foo(x for x in [], b) "Generator expression must be parenthesized if not sole argument" # foo(x for x in [], b)
"positional argument follows keyword argument unpacking" # f(**x, y) >= 3.5 "positional argument follows keyword argument unpacking" # f(**x, y)
"positional argument follows keyword argument" # f(x=2, y) >= 3.5 "positional argument follows keyword argument" # f(x=2, y)
"iterable argument unpacking follows keyword argument unpacking" # foo(**kwargs, *args) "iterable argument unpacking follows keyword argument unpacking" # foo(**kwargs, *args)
"lambda cannot contain assignment" # f(lambda: 1=1) "lambda cannot contain assignment" # f(lambda: 1=1)
"keyword can't be an expression" # f(+x=1) "keyword can't be an expression" # f(+x=1)
@@ -167,10 +166,3 @@ A list of syntax/indentation errors I've encountered in CPython.
E_OVERFLOW: "expression too long" E_OVERFLOW: "expression too long"
E_DECODE: "unknown decode error" E_DECODE: "unknown decode error"
E_BADSINGLE: "multiple statements found while compiling a single statement" E_BADSINGLE: "multiple statements found while compiling a single statement"
Version specific:
Python 3.5:
'yield' inside async function
Python 3.4:
can use starred expression only as assignment target

View File

@@ -43,11 +43,10 @@ class Parser(BaseParser):
# Not sure if this is the best idea, but IMO it's the easiest way to # Not sure if this is the best idea, but IMO it's the easiest way to
# avoid extreme amounts of work around the subtle difference of 2/3 # avoid extreme amounts of work around the subtle difference of 2/3
# grammar in list comoprehensions. # grammar in list comoprehensions.
'list_for': tree.SyncCompFor,
'decorator': tree.Decorator, 'decorator': tree.Decorator,
'lambdef': tree.Lambda, 'lambdef': tree.Lambda,
'old_lambdef': tree.Lambda,
'lambdef_nocond': tree.Lambda, 'lambdef_nocond': tree.Lambda,
'namedexpr_test': tree.NamedExpr,
} }
default_node = tree.PythonNode default_node = tree.PythonNode
@@ -63,8 +62,8 @@ class Parser(BaseParser):
} }
def __init__(self, pgen_grammar, error_recovery=True, start_nonterminal='file_input'): def __init__(self, pgen_grammar, error_recovery=True, start_nonterminal='file_input'):
super(Parser, self).__init__(pgen_grammar, start_nonterminal, super().__init__(pgen_grammar, start_nonterminal,
error_recovery=error_recovery) error_recovery=error_recovery)
self.syntax_errors = [] self.syntax_errors = []
self._omit_dedent_list = [] self._omit_dedent_list = []
@@ -77,7 +76,7 @@ class Parser(BaseParser):
tokens = self._recovery_tokenize(tokens) tokens = self._recovery_tokenize(tokens)
return super(Parser, self).parse(tokens) return super().parse(tokens)
def convert_node(self, nonterminal, children): def convert_node(self, nonterminal, children):
""" """
@@ -96,12 +95,6 @@ class Parser(BaseParser):
# ones and therefore have pseudo start/end positions and no # ones and therefore have pseudo start/end positions and no
# prefixes. Just ignore them. # prefixes. Just ignore them.
children = [children[0]] + children[2:-1] children = [children[0]] + children[2:-1]
elif nonterminal == 'list_if':
# Make transitioning from 2 to 3 easier.
nonterminal = 'comp_if'
elif nonterminal == 'listmaker':
# Same as list_if above.
nonterminal = 'testlist_comp'
node = self.default_node(nonterminal, children) node = self.default_node(nonterminal, children)
for c in children: for c in children:
c.parent = node c.parent = node
@@ -146,7 +139,7 @@ class Parser(BaseParser):
return return
if not self._error_recovery: if not self._error_recovery:
return super(Parser, self).error_recovery(token) return super().error_recovery(token)
def current_suite(stack): def current_suite(stack):
# For now just discard everything that is not a suite or # For now just discard everything that is not a suite or

View File

@@ -1,5 +1,6 @@
import re import re
from contextlib import contextmanager from contextlib import contextmanager
from typing import Tuple
from parso.python.errors import ErrorFinder, ErrorFinderConfig from parso.python.errors import ErrorFinder, ErrorFinderConfig
from parso.normalizer import Rule from parso.normalizer import Rule
@@ -15,16 +16,17 @@ _CLOSING_BRACKETS = ')', ']', '}'
_FACTOR = '+', '-', '~' _FACTOR = '+', '-', '~'
_ALLOW_SPACE = '*', '+', '-', '**', '/', '//', '@' _ALLOW_SPACE = '*', '+', '-', '**', '/', '//', '@'
_BITWISE_OPERATOR = '<<', '>>', '|', '&', '^' _BITWISE_OPERATOR = '<<', '>>', '|', '&', '^'
_NEEDS_SPACE = ('=', '%', '->', _NEEDS_SPACE: Tuple[str, ...] = (
'<', '>', '==', '>=', '<=', '<>', '!=', '=', '%', '->',
'+=', '-=', '*=', '@=', '/=', '%=', '&=', '|=', '^=', '<<=', '<', '>', '==', '>=', '<=', '<>', '!=',
'>>=', '**=', '//=') '+=', '-=', '*=', '@=', '/=', '%=', '&=', '|=', '^=', '<<=',
'>>=', '**=', '//=')
_NEEDS_SPACE += _BITWISE_OPERATOR _NEEDS_SPACE += _BITWISE_OPERATOR
_IMPLICIT_INDENTATION_TYPES = ('dictorsetmaker', 'argument') _IMPLICIT_INDENTATION_TYPES = ('dictorsetmaker', 'argument')
_POSSIBLE_SLICE_PARENTS = ('subscript', 'subscriptlist', 'sliceop') _POSSIBLE_SLICE_PARENTS = ('subscript', 'subscriptlist', 'sliceop')
class IndentationTypes(object): class IndentationTypes:
VERTICAL_BRACKET = object() VERTICAL_BRACKET = object()
HANGING_BRACKET = object() HANGING_BRACKET = object()
BACKSLASH = object() BACKSLASH = object()
@@ -71,7 +73,6 @@ class BracketNode(IndentationNode):
n = n.parent n = n.parent
parent_indentation = n.indentation parent_indentation = n.indentation
next_leaf = leaf.get_next_leaf() next_leaf = leaf.get_next_leaf()
if '\n' in next_leaf.prefix: if '\n' in next_leaf.prefix:
# This implies code like: # This implies code like:
@@ -93,7 +94,7 @@ class BracketNode(IndentationNode):
if '\t' in config.indentation: if '\t' in config.indentation:
self.indentation = None self.indentation = None
else: else:
self.indentation = ' ' * expected_end_indent self.indentation = ' ' * expected_end_indent
self.bracket_indentation = self.indentation self.bracket_indentation = self.indentation
self.type = IndentationTypes.VERTICAL_BRACKET self.type = IndentationTypes.VERTICAL_BRACKET
@@ -111,7 +112,7 @@ class ImplicitNode(BracketNode):
annotations and dict values. annotations and dict values.
""" """
def __init__(self, config, leaf, parent): def __init__(self, config, leaf, parent):
super(ImplicitNode, self).__init__(config, leaf, parent) super().__init__(config, leaf, parent)
self.type = IndentationTypes.IMPLICIT self.type = IndentationTypes.IMPLICIT
next_leaf = leaf.get_next_leaf() next_leaf = leaf.get_next_leaf()
@@ -137,7 +138,7 @@ class BackslashNode(IndentationNode):
self.indentation = parent_indentation + config.indentation self.indentation = parent_indentation + config.indentation
else: else:
# +1 because there is a space. # +1 because there is a space.
self.indentation = ' ' * (equals.end_pos[1] + 1) self.indentation = ' ' * (equals.end_pos[1] + 1)
else: else:
self.indentation = parent_indentation + config.indentation self.indentation = parent_indentation + config.indentation
self.bracket_indentation = self.indentation self.bracket_indentation = self.indentation
@@ -150,7 +151,7 @@ def _is_magic_name(name):
class PEP8Normalizer(ErrorFinder): class PEP8Normalizer(ErrorFinder):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(PEP8Normalizer, self).__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self._previous_part = None self._previous_part = None
self._previous_leaf = None self._previous_leaf = None
self._on_newline = True self._on_newline = True
@@ -173,7 +174,7 @@ class PEP8Normalizer(ErrorFinder):
@contextmanager @contextmanager
def visit_node(self, node): def visit_node(self, node):
with super(PEP8Normalizer, self).visit_node(node): with super().visit_node(node):
with self._visit_node(node): with self._visit_node(node):
yield yield
@@ -190,7 +191,8 @@ class PEP8Normalizer(ErrorFinder):
expr_stmt = node.parent expr_stmt = node.parent
# Check if it's simply defining a single name, not something like # Check if it's simply defining a single name, not something like
# foo.bar or x[1], where using a lambda could make more sense. # foo.bar or x[1], where using a lambda could make more sense.
if expr_stmt.type == 'expr_stmt' and any(n.type == 'name' for n in expr_stmt.children[:-2:2]): if expr_stmt.type == 'expr_stmt' and any(n.type == 'name'
for n in expr_stmt.children[:-2:2]):
self.add_issue(node, 731, 'Do not assign a lambda expression, use a def') self.add_issue(node, 731, 'Do not assign a lambda expression, use a def')
elif typ == 'try_stmt': elif typ == 'try_stmt':
for child in node.children: for child in node.children:
@@ -221,7 +223,6 @@ class PEP8Normalizer(ErrorFinder):
if typ in _IMPORT_TYPES: if typ in _IMPORT_TYPES:
simple_stmt = node.parent simple_stmt = node.parent
module = simple_stmt.parent module = simple_stmt.parent
#if module.type == 'simple_stmt':
if module.type == 'file_input': if module.type == 'file_input':
index = module.children.index(simple_stmt) index = module.children.index(simple_stmt)
for child in module.children[:index]: for child in module.children[:index]:
@@ -341,7 +342,7 @@ class PEP8Normalizer(ErrorFinder):
self._newline_count = 0 self._newline_count = 0
def visit_leaf(self, leaf): def visit_leaf(self, leaf):
super(PEP8Normalizer, self).visit_leaf(leaf) super().visit_leaf(leaf)
for part in leaf._split_prefix(): for part in leaf._split_prefix():
if part.type == 'spacing': if part.type == 'spacing':
# This part is used for the part call after for. # This part is used for the part call after for.
@@ -406,7 +407,6 @@ class PEP8Normalizer(ErrorFinder):
and leaf.parent.parent.type == 'decorated': and leaf.parent.parent.type == 'decorated':
self.add_issue(part, 304, "Blank lines found after function decorator") self.add_issue(part, 304, "Blank lines found after function decorator")
self._newline_count += 1 self._newline_count += 1
if type_ == 'backslash': if type_ == 'backslash':
@@ -461,33 +461,62 @@ class PEP8Normalizer(ErrorFinder):
else: else:
should_be_indentation = node.indentation should_be_indentation = node.indentation
if self._in_suite_introducer and indentation == \ if self._in_suite_introducer and indentation == \
node.get_latest_suite_node().indentation \ node.get_latest_suite_node().indentation \
+ self._config.indentation: + self._config.indentation:
self.add_issue(part, 129, "Line with same indent as next logical block") self.add_issue(part, 129, "Line with same indent as next logical block")
elif indentation != should_be_indentation: elif indentation != should_be_indentation:
if not self._check_tabs_spaces(spacing) and part.value != '\n': if not self._check_tabs_spaces(spacing) and part.value != '\n':
if value in '])}': if value in '])}':
if node.type == IndentationTypes.VERTICAL_BRACKET: if node.type == IndentationTypes.VERTICAL_BRACKET:
self.add_issue(part, 124, "Closing bracket does not match visual indentation") self.add_issue(
part,
124,
"Closing bracket does not match visual indentation"
)
else: else:
self.add_issue(part, 123, "Losing bracket does not match indentation of opening bracket's line") self.add_issue(
part,
123,
"Losing bracket does not match "
"indentation of opening bracket's line"
)
else: else:
if len(indentation) < len(should_be_indentation): if len(indentation) < len(should_be_indentation):
if node.type == IndentationTypes.VERTICAL_BRACKET: if node.type == IndentationTypes.VERTICAL_BRACKET:
self.add_issue(part, 128, 'Continuation line under-indented for visual indent') self.add_issue(
part,
128,
'Continuation line under-indented for visual indent'
)
elif node.type == IndentationTypes.BACKSLASH: elif node.type == IndentationTypes.BACKSLASH:
self.add_issue(part, 122, 'Continuation line missing indentation or outdented') self.add_issue(
part,
122,
'Continuation line missing indentation or outdented'
)
elif node.type == IndentationTypes.IMPLICIT: elif node.type == IndentationTypes.IMPLICIT:
self.add_issue(part, 135, 'xxx') self.add_issue(part, 135, 'xxx')
else: else:
self.add_issue(part, 121, 'Continuation line under-indented for hanging indent') self.add_issue(
part,
121,
'Continuation line under-indented for hanging indent'
)
else: else:
if node.type == IndentationTypes.VERTICAL_BRACKET: if node.type == IndentationTypes.VERTICAL_BRACKET:
self.add_issue(part, 127, 'Continuation line over-indented for visual indent') self.add_issue(
part,
127,
'Continuation line over-indented for visual indent'
)
elif node.type == IndentationTypes.IMPLICIT: elif node.type == IndentationTypes.IMPLICIT:
self.add_issue(part, 136, 'xxx') self.add_issue(part, 136, 'xxx')
else: else:
self.add_issue(part, 126, 'Continuation line over-indented for hanging indent') self.add_issue(
part,
126,
'Continuation line over-indented for hanging indent'
)
else: else:
self._check_spacing(part, spacing) self._check_spacing(part, spacing)
@@ -524,7 +553,7 @@ class PEP8Normalizer(ErrorFinder):
else: else:
last_column = part.end_pos[1] last_column = part.end_pos[1]
if last_column > self._config.max_characters \ if last_column > self._config.max_characters \
and spacing.start_pos[1] <= self._config.max_characters : and spacing.start_pos[1] <= self._config.max_characters:
# Special case for long URLs in multi-line docstrings or comments, # Special case for long URLs in multi-line docstrings or comments,
# but still report the error when the 72 first chars are whitespaces. # but still report the error when the 72 first chars are whitespaces.
report = True report = True
@@ -538,7 +567,7 @@ class PEP8Normalizer(ErrorFinder):
part, part,
501, 501,
'Line too long (%s > %s characters)' % 'Line too long (%s > %s characters)' %
(last_column, self._config.max_characters), (last_column, self._config.max_characters),
) )
def _check_spacing(self, part, spacing): def _check_spacing(self, part, spacing):
@@ -573,11 +602,11 @@ class PEP8Normalizer(ErrorFinder):
message = "Whitespace before '%s'" % part.value message = "Whitespace before '%s'" % part.value
add_if_spaces(spacing, 202, message) add_if_spaces(spacing, 202, message)
elif part in (',', ';') or part == ':' \ elif part in (',', ';') or part == ':' \
and part.parent.type not in _POSSIBLE_SLICE_PARENTS: and part.parent.type not in _POSSIBLE_SLICE_PARENTS:
message = "Whitespace before '%s'" % part.value message = "Whitespace before '%s'" % part.value
add_if_spaces(spacing, 203, message) add_if_spaces(spacing, 203, message)
elif prev == ':' and prev.parent.type in _POSSIBLE_SLICE_PARENTS: elif prev == ':' and prev.parent.type in _POSSIBLE_SLICE_PARENTS:
pass # TODO pass # TODO
elif prev in (',', ';', ':'): elif prev in (',', ';', ':'):
add_not_spaces(spacing, 231, "missing whitespace after '%s'") add_not_spaces(spacing, 231, "missing whitespace after '%s'")
elif part == ':': # Is a subscript elif part == ':': # Is a subscript
@@ -602,9 +631,17 @@ class PEP8Normalizer(ErrorFinder):
if param.type == 'param' and param.annotation: if param.type == 'param' and param.annotation:
add_not_spaces(spacing, 252, 'Expected spaces around annotation equals') add_not_spaces(spacing, 252, 'Expected spaces around annotation equals')
else: else:
add_if_spaces(spacing, 251, 'Unexpected spaces around keyword / parameter equals') add_if_spaces(
spacing,
251,
'Unexpected spaces around keyword / parameter equals'
)
elif part in _BITWISE_OPERATOR or prev in _BITWISE_OPERATOR: elif part in _BITWISE_OPERATOR or prev in _BITWISE_OPERATOR:
add_not_spaces(spacing, 227, 'Missing whitespace around bitwise or shift operator') add_not_spaces(
spacing,
227,
'Missing whitespace around bitwise or shift operator'
)
elif part == '%' or prev == '%': elif part == '%' or prev == '%':
add_not_spaces(spacing, 228, 'Missing whitespace around modulo operator') add_not_spaces(spacing, 228, 'Missing whitespace around modulo operator')
else: else:
@@ -621,8 +658,7 @@ class PEP8Normalizer(ErrorFinder):
if spaces and part not in _ALLOW_SPACE and prev not in _ALLOW_SPACE: if spaces and part not in _ALLOW_SPACE and prev not in _ALLOW_SPACE:
message_225 = 'Missing whitespace between tokens' message_225 = 'Missing whitespace between tokens'
#print('xy', spacing) # self.add_issue(spacing, 225, message_225)
#self.add_issue(spacing, 225, message_225)
# TODO why only brackets? # TODO why only brackets?
if part in _OPENING_BRACKETS: if part in _OPENING_BRACKETS:
message = "Whitespace before '%s'" % part.value message = "Whitespace before '%s'" % part.value
@@ -664,7 +700,8 @@ class PEP8Normalizer(ErrorFinder):
self.add_issue(leaf, 711, message) self.add_issue(leaf, 711, message)
break break
elif node.value in ('True', 'False'): elif node.value in ('True', 'False'):
message = "comparison to False/True should be 'if cond is True:' or 'if cond:'" message = "comparison to False/True should be " \
"'if cond is True:' or 'if cond:'"
self.add_issue(leaf, 712, message) self.add_issue(leaf, 712, message)
break break
elif leaf.value in ('in', 'is'): elif leaf.value in ('in', 'is'):
@@ -680,6 +717,7 @@ class PEP8Normalizer(ErrorFinder):
indentation = re.match(r'[ \t]*', line).group(0) indentation = re.match(r'[ \t]*', line).group(0)
start_pos = leaf.line + i, len(indentation) start_pos = leaf.line + i, len(indentation)
# TODO check multiline indentation. # TODO check multiline indentation.
start_pos
elif typ == 'endmarker': elif typ == 'endmarker':
if self._newline_count >= 2: if self._newline_count >= 2:
self.add_issue(leaf, 391, 'Blank line at end of file') self.add_issue(leaf, 391, 'Blank line at end of file')
@@ -694,7 +732,7 @@ class PEP8Normalizer(ErrorFinder):
return return
if code in (901, 903): if code in (901, 903):
# 901 and 903 are raised by the ErrorFinder. # 901 and 903 are raised by the ErrorFinder.
super(PEP8Normalizer, self).add_issue(node, code, message) super().add_issue(node, code, message)
else: else:
# Skip ErrorFinder here, because it has custom behavior. # Skip ErrorFinder here, because it has custom behavior.
super(ErrorFinder, self).add_issue(node, code, message) super(ErrorFinder, self).add_issue(node, code, message)
@@ -718,7 +756,7 @@ class PEP8NormalizerConfig(ErrorFinderConfig):
# TODO this is not yet ready. # TODO this is not yet ready.
#@PEP8Normalizer.register_rule(type='endmarker') # @PEP8Normalizer.register_rule(type='endmarker')
class BlankLineAtEnd(Rule): class BlankLineAtEnd(Rule):
code = 392 code = 392
message = 'Blank line at end of file' message = 'Blank line at end of file'

View File

@@ -6,7 +6,7 @@ from parso.python.tokenize import group
unicode_bom = BOM_UTF8.decode('utf-8') unicode_bom = BOM_UTF8.decode('utf-8')
class PrefixPart(object): class PrefixPart:
def __init__(self, leaf, typ, value, spacing='', start_pos=None): def __init__(self, leaf, typ, value, spacing='', start_pos=None):
assert start_pos is not None assert start_pos is not None
self.parent = leaf self.parent = leaf
@@ -71,7 +71,7 @@ def split_prefix(leaf, start_pos):
value = spacing = '' value = spacing = ''
bom = False bom = False
while start != len(leaf.prefix): while start != len(leaf.prefix):
match =_regex.match(leaf.prefix, start) match = _regex.match(leaf.prefix, start)
spacing = match.group(1) spacing = match.group(1)
value = match.group(2) value = match.group(2)
if not value: if not value:

View File

@@ -1,8 +1,13 @@
from __future__ import absolute_import from __future__ import absolute_import
from enum import Enum
class TokenType(object):
def __init__(self, name, contains_syntax=False): class TokenType:
name: str
contains_syntax: bool
def __init__(self, name: str, contains_syntax: bool = False):
self.name = name self.name = name
self.contains_syntax = contains_syntax self.contains_syntax = contains_syntax
@@ -10,18 +15,17 @@ class TokenType(object):
return '%s(%s)' % (self.__class__.__name__, self.name) return '%s(%s)' % (self.__class__.__name__, self.name)
class TokenTypes(object): class PythonTokenTypes(Enum):
""" STRING = TokenType('STRING')
Basically an enum, but Python 2 doesn't have enums in the standard library. NUMBER = TokenType('NUMBER')
""" NAME = TokenType('NAME', contains_syntax=True)
def __init__(self, names, contains_syntax): ERRORTOKEN = TokenType('ERRORTOKEN')
for name in names: NEWLINE = TokenType('NEWLINE')
setattr(self, name, TokenType(name, contains_syntax=name in contains_syntax)) INDENT = TokenType('INDENT')
DEDENT = TokenType('DEDENT')
ERROR_DEDENT = TokenType('ERROR_DEDENT')
PythonTokenTypes = TokenTypes(( FSTRING_STRING = TokenType('FSTRING_STRING')
'STRING', 'NUMBER', 'NAME', 'ERRORTOKEN', 'NEWLINE', 'INDENT', 'DEDENT', FSTRING_START = TokenType('FSTRING_START')
'ERROR_DEDENT', 'FSTRING_STRING', 'FSTRING_START', 'FSTRING_END', 'OP', FSTRING_END = TokenType('FSTRING_END')
'ENDMARKER'), OP = TokenType('OP', contains_syntax=True)
contains_syntax=('NAME', 'OP'), ENDMARKER = TokenType('ENDMARKER')
)

View File

@@ -1,30 +0,0 @@
from typing import Container, Iterable
class TokenType:
name: str
contains_syntax: bool
def __init__(self, name: str, contains_syntax: bool) -> None: ...
class TokenTypes:
def __init__(
self, names: Iterable[str], contains_syntax: Container[str]
) -> None: ...
# not an actual class in the source code, but we need this class to type the fields of
# PythonTokenTypes
class _FakePythonTokenTypesClass(TokenTypes):
STRING: TokenType
NUMBER: TokenType
NAME: TokenType
ERRORTOKEN: TokenType
NEWLINE: TokenType
INDENT: TokenType
DEDENT: TokenType
ERROR_DEDENT: TokenType
FSTRING_STRING: TokenType
FSTRING_START: TokenType
FSTRING_END: TokenType
OP: TokenType
ENDMARKER: TokenType
PythonTokenTypes: _FakePythonTokenTypesClass = ...

View File

@@ -13,12 +13,13 @@ from __future__ import absolute_import
import sys import sys
import re import re
from collections import namedtuple
import itertools as _itertools import itertools as _itertools
from codecs import BOM_UTF8 from codecs import BOM_UTF8
from typing import NamedTuple, Tuple, Iterator, Iterable, List, Dict, \
Pattern, Set
from parso.python.token import PythonTokenTypes from parso.python.token import PythonTokenTypes
from parso.utils import split_lines from parso.utils import split_lines, PythonVersionInfo, parse_version_string
# Maximum code point of Unicode 6.0: 0x10ffff (1,114,111) # Maximum code point of Unicode 6.0: 0x10ffff (1,114,111)
@@ -38,31 +39,23 @@ FSTRING_START = PythonTokenTypes.FSTRING_START
FSTRING_STRING = PythonTokenTypes.FSTRING_STRING FSTRING_STRING = PythonTokenTypes.FSTRING_STRING
FSTRING_END = PythonTokenTypes.FSTRING_END FSTRING_END = PythonTokenTypes.FSTRING_END
TokenCollection = namedtuple(
'TokenCollection', class TokenCollection(NamedTuple):
'pseudo_token single_quoted triple_quoted endpats whitespace ' pseudo_token: Pattern
'fstring_pattern_map always_break_tokens', single_quoted: Set[str]
) triple_quoted: Set[str]
endpats: Dict[str, Pattern]
whitespace: Pattern
fstring_pattern_map: Dict[str, str]
always_break_tokens: Tuple[str]
BOM_UTF8_STRING = BOM_UTF8.decode('utf-8') BOM_UTF8_STRING = BOM_UTF8.decode('utf-8')
_token_collection_cache = {} _token_collection_cache: Dict[PythonVersionInfo, TokenCollection] = {}
if sys.version_info.major >= 3:
# Python 3 has str.isidentifier() to check if a char is a valid identifier
is_identifier = str.isidentifier
else:
# Python 2 doesn't, but it's not that important anymore and if you tokenize
# Python 2 code with this, it's still ok. It's just that parsing Python 3
# code with this function is not 100% correct.
# This just means that Python 2 code matches a few identifiers too much,
# but that doesn't really matter.
def is_identifier(s):
return True
def group(*choices, **kwargs): def group(*choices, capture=False, **kwargs):
capture = kwargs.pop('capture', False) # Python 2, arrghhhhh :(
assert not kwargs assert not kwargs
start = '(' start = '('
@@ -76,19 +69,17 @@ 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, include_fstring=False, only_fstring=False): def _all_string_prefixes(*, include_fstring=False, only_fstring=False):
def different_case_versions(prefix): def different_case_versions(prefix):
for s in _itertools.product(*[(c, c.upper()) for c in prefix]): for s in _itertools.product(*[(c, c.upper()) for c in prefix]):
yield ''.join(s) 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'] valid_string_prefixes = ['b', 'r', 'u', 'br']
if version_info.major >= 3:
valid_string_prefixes.append('br')
result = set(['']) result = {''}
if version_info >= (3, 6) and include_fstring: if include_fstring:
f = ['f', 'fr'] f = ['f', 'fr']
if only_fstring: if only_fstring:
valid_string_prefixes = f valid_string_prefixes = f
@@ -104,10 +95,6 @@ def _all_string_prefixes(version_info, include_fstring=False, only_fstring=False
# create a list with upper and lower versions of each # create a list with upper and lower versions of each
# character # character
result.update(different_case_versions(t)) result.update(different_case_versions(t))
if version_info.major == 2:
# 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
@@ -123,9 +110,14 @@ def _get_token_collection(version_info):
_create_token_collection(version_info) _create_token_collection(version_info)
return result return result
unicode_character_name = r'[A-Za-z0-9\-]+(?: [A-Za-z0-9\-]+)*'
fstring_string_single_line = _compile(r'(?:\{\{|\}\}|\\(?:\r\n?|\n)|[^{}\r\n])+') fstring_string_single_line = _compile(
fstring_string_multi_line = _compile(r'(?:[^{}]+|\{\{|\}\})+') r'(?:\{\{|\}\}|\\N\{' + unicode_character_name +
r'\}|\\(?:\r\n?|\n)|\\[^\r\nN]|[^{}\r\n\\])+'
)
fstring_string_multi_line = _compile(
r'(?:\{\{|\}\}|\\N\{' + unicode_character_name + r'\}|\\[^N]|[^{}\\])+'
)
fstring_format_spec_single_line = _compile(r'(?:\\(?:\r\n?|\n)|[^{}\r\n])+') fstring_format_spec_single_line = _compile(r'(?:\\(?:\r\n?|\n)|[^{}\r\n])+')
fstring_format_spec_multi_line = _compile(r'[^{}]+') fstring_format_spec_multi_line = _compile(r'[^{}]+')
@@ -136,53 +128,27 @@ def _create_token_collection(version_info):
Whitespace = r'[ \f\t]*' Whitespace = r'[ \f\t]*'
whitespace = _compile(Whitespace) whitespace = _compile(Whitespace)
Comment = r'#[^\r\n]*' Comment = r'#[^\r\n]*'
# Python 2 is pretty much not working properly anymore, we just ignore Name = '([A-Za-z_0-9\u0080-' + MAX_UNICODE + ']+)'
# parsing unicode properly, which is fine, I guess.
if version_info[0] == 2:
Name = r'([A-Za-z_0-9]+)'
elif sys.version_info[0] == 2:
# Unfortunately the regex engine cannot deal with the regex below, so
# just use this one.
Name = r'(\w+)'
else:
Name = u'([A-Za-z_0-9\u0080-' + MAX_UNICODE + ']+)'
if version_info >= (3, 6): Hexnumber = r'0[xX](?:_?[0-9a-fA-F])+'
Hexnumber = r'0[xX](?:_?[0-9a-fA-F])+' Binnumber = r'0[bB](?:_?[01])+'
Binnumber = r'0[bB](?:_?[01])+' Octnumber = r'0[oO](?:_?[0-7])+'
Octnumber = r'0[oO](?:_?[0-7])+' Decnumber = r'(?:0(?:_?0)*|[1-9](?:_?[0-9])*)'
Decnumber = r'(?:0(?:_?0)*|[1-9](?:_?[0-9])*)' Intnumber = group(Hexnumber, Binnumber, Octnumber, Decnumber)
Intnumber = group(Hexnumber, Binnumber, Octnumber, Decnumber) Exponent = r'[eE][-+]?[0-9](?:_?[0-9])*'
Exponent = r'[eE][-+]?[0-9](?:_?[0-9])*' Pointfloat = group(r'[0-9](?:_?[0-9])*\.(?:[0-9](?:_?[0-9])*)?',
Pointfloat = group(r'[0-9](?:_?[0-9])*\.(?:[0-9](?:_?[0-9])*)?', r'\.[0-9](?:_?[0-9])*') + maybe(Exponent)
r'\.[0-9](?:_?[0-9])*') + maybe(Exponent) Expfloat = r'[0-9](?:_?[0-9])*' + Exponent
Expfloat = r'[0-9](?:_?[0-9])*' + Exponent Floatnumber = group(Pointfloat, Expfloat)
Floatnumber = group(Pointfloat, Expfloat) Imagnumber = group(r'[0-9](?:_?[0-9])*[jJ]', Floatnumber + r'[jJ]')
Imagnumber = group(r'[0-9](?:_?[0-9])*[jJ]', Floatnumber + r'[jJ]')
else:
Hexnumber = r'0[xX][0-9a-fA-F]+'
Binnumber = r'0[bB][01]+'
if version_info.major >= 3:
Octnumber = r'0[oO][0-7]+'
else:
Octnumber = '0[oO]?[0-7]+'
Decnumber = r'(?:0+|[1-9][0-9]*)'
Intnumber = group(Hexnumber, Binnumber, Octnumber, Decnumber)
if version_info[0] < 3:
Intnumber += '[lL]?'
Exponent = r'[eE][-+]?[0-9]+'
Pointfloat = group(r'[0-9]+\.[0-9]*', r'\.[0-9]+') + maybe(Exponent)
Expfloat = r'[0-9]+' + Exponent
Floatnumber = group(Pointfloat, Expfloat)
Imagnumber = group(r'[0-9]+[jJ]', Floatnumber + r'[jJ]')
Number = group(Imagnumber, Floatnumber, Intnumber) Number = group(Imagnumber, Floatnumber, Intnumber)
# Note that since _all_string_prefixes includes the empty string, # Note that since _all_string_prefixes includes the empty string,
# StringPrefix can be the empty string (making it optional). # StringPrefix can be the empty string (making it optional).
possible_prefixes = _all_string_prefixes(version_info) possible_prefixes = _all_string_prefixes()
StringPrefix = group(*possible_prefixes) StringPrefix = group(*possible_prefixes)
StringPrefixWithF = group(*_all_string_prefixes(version_info, include_fstring=True)) StringPrefixWithF = group(*_all_string_prefixes(include_fstring=True))
fstring_prefixes = _all_string_prefixes(version_info, include_fstring=True, only_fstring=True) fstring_prefixes = _all_string_prefixes(include_fstring=True, only_fstring=True)
FStringStart = group(*fstring_prefixes) FStringStart = group(*fstring_prefixes)
# Tail end of ' string. # Tail end of ' string.
@@ -205,9 +171,7 @@ def _create_token_collection(version_info):
Bracket = '[][(){}]' Bracket = '[][(){}]'
special_args = [r'\r\n?', r'\n', r'[;.,@]'] special_args = [r'\.\.\.', r'\r\n?', r'\n', r'[;.,@]']
if version_info >= (3, 0):
special_args.insert(0, r'\.\.\.')
if version_info >= (3, 8): if version_info >= (3, 8):
special_args.insert(0, ":=?") special_args.insert(0, ":=?")
else: else:
@@ -258,9 +222,7 @@ def _create_token_collection(version_info):
ALWAYS_BREAK_TOKENS = (';', 'import', 'class', 'def', 'try', 'except', ALWAYS_BREAK_TOKENS = (';', 'import', 'class', 'def', 'try', 'except',
'finally', 'while', 'with', 'return', 'continue', 'finally', 'while', 'with', 'return', 'continue',
'break', 'del', 'pass', 'global', 'assert') 'break', 'del', 'pass', 'global', 'assert', 'nonlocal')
if version_info >= (3, 5):
ALWAYS_BREAK_TOKENS += ('nonlocal', )
pseudo_token_compiled = _compile(PseudoToken) pseudo_token_compiled = _compile(PseudoToken)
return TokenCollection( return TokenCollection(
pseudo_token_compiled, single_quoted, triple_quoted, endpats, pseudo_token_compiled, single_quoted, triple_quoted, endpats,
@@ -268,9 +230,14 @@ def _create_token_collection(version_info):
) )
class Token(namedtuple('Token', ['type', 'string', 'start_pos', 'prefix'])): class Token(NamedTuple):
type: PythonTokenTypes
string: str
start_pos: Tuple[int, int]
prefix: str
@property @property
def end_pos(self): def end_pos(self) -> Tuple[int, int]:
lines = split_lines(self.string) lines = split_lines(self.string)
if len(lines) > 1: if len(lines) > 1:
return self.start_pos[0] + len(lines) - 1, 0 return self.start_pos[0] + len(lines) - 1, 0
@@ -284,7 +251,7 @@ class PythonToken(Token):
self._replace(type=self.type.name)) self._replace(type=self.type.name))
class FStringNode(object): class FStringNode:
def __init__(self, quote): def __init__(self, quote):
self.quote = quote self.quote = quote
self.parentheses_count = 0 self.parentheses_count = 0
@@ -371,10 +338,12 @@ def _find_fstring_string(endpats, fstring_stack, line, lnum, pos):
return string, new_pos return string, new_pos
def tokenize(code, version_info, start_pos=(1, 0)): def tokenize(
code: str, *, version_info: PythonVersionInfo, start_pos: Tuple[int, int] = (1, 0)
) -> Iterator[PythonToken]:
"""Generate tokens from a the source code (string).""" """Generate tokens from a the source code (string)."""
lines = split_lines(code, keepends=True) lines = split_lines(code, keepends=True)
return tokenize_lines(lines, version_info, start_pos=start_pos) return tokenize_lines(lines, version_info=version_info, start_pos=start_pos)
def _print_tokens(func): def _print_tokens(func):
@@ -390,7 +359,14 @@ def _print_tokens(func):
# @_print_tokens # @_print_tokens
def tokenize_lines(lines, version_info, start_pos=(1, 0), indents=None, is_first_token=True): def tokenize_lines(
lines: Iterable[str],
*,
version_info: PythonVersionInfo,
indents: List[int] = None,
start_pos: Tuple[int, int] = (1, 0),
is_first_token=True,
) -> Iterator[PythonToken]:
""" """
A heavily modified Python standard library tokenizer. A heavily modified Python standard library tokenizer.
@@ -416,7 +392,9 @@ def tokenize_lines(lines, version_info, start_pos=(1, 0), indents=None, is_first
max_ = 0 max_ = 0
numchars = '0123456789' numchars = '0123456789'
contstr = '' contstr = ''
contline = None contline: str
contstr_start: Tuple[int, int]
endprog: Pattern
# We start with a newline. This makes indent at the first position # We start with a newline. This makes indent at the first position
# possible. It's not valid Python, but still better than an INDENT in the # possible. It's not valid Python, but still better than an INDENT in the
# second line (and not in the first). This makes quite a few things in # second line (and not in the first). This makes quite a few things in
@@ -425,7 +403,7 @@ def tokenize_lines(lines, version_info, start_pos=(1, 0), indents=None, is_first
prefix = '' # Should never be required, but here for safety prefix = '' # Should never be required, but here for safety
additional_prefix = '' additional_prefix = ''
lnum = start_pos[0] - 1 lnum = start_pos[0] - 1
fstring_stack = [] fstring_stack: List[FStringNode] = []
for line in lines: # loop over lines in stream for line in lines: # loop over lines in stream
lnum += 1 lnum += 1
pos = 0 pos = 0
@@ -444,14 +422,14 @@ def tokenize_lines(lines, version_info, start_pos=(1, 0), indents=None, is_first
is_first_token = False is_first_token = False
if contstr: # continued string if contstr: # continued string
endmatch = endprog.match(line) endmatch = endprog.match(line) # noqa: F821
if endmatch: if endmatch:
pos = endmatch.end(0) pos = endmatch.end(0)
yield PythonToken( yield PythonToken(
STRING, contstr + line[:pos], STRING, contstr + line[:pos],
contstr_start, prefix) contstr_start, prefix) # noqa: F821
contstr = '' contstr = ''
contline = None contline = ''
else: else:
contstr = contstr + line contstr = contstr + line
contline = contline + line contline = contline + line
@@ -528,14 +506,12 @@ def tokenize_lines(lines, version_info, start_pos=(1, 0), indents=None, is_first
if indent_start > indents[-1]: if indent_start > indents[-1]:
yield PythonToken(INDENT, '', spos, '') yield PythonToken(INDENT, '', spos, '')
indents.append(indent_start) indents.append(indent_start)
for t in dedent_if_necessary(indent_start): yield from dedent_if_necessary(indent_start)
yield t
if not pseudomatch: # scan for tokens if not pseudomatch: # scan for tokens
match = whitespace.match(line, pos) match = whitespace.match(line, pos)
if new_line and paren_level == 0 and not fstring_stack: if new_line and paren_level == 0 and not fstring_stack:
for t in dedent_if_necessary(match.end()): yield from dedent_if_necessary(match.end())
yield t
pos = match.end() pos = match.end()
new_line = False new_line = False
yield PythonToken( yield PythonToken(
@@ -556,18 +532,14 @@ def tokenize_lines(lines, version_info, start_pos=(1, 0), indents=None, is_first
# We only want to dedent if the token is on a new line. # We only want to dedent if the token is on a new line.
m = re.match(r'[ \f\t]*$', line[:start]) m = re.match(r'[ \f\t]*$', line[:start])
if m is not None: if m is not None:
for t in dedent_if_necessary(m.end()): yield from dedent_if_necessary(m.end())
yield t if token.isidentifier():
if is_identifier(token):
yield PythonToken(NAME, token, spos, prefix) yield PythonToken(NAME, token, spos, prefix)
else: else:
for t in _split_illegal_unicode_name(token, spos, prefix): yield from _split_illegal_unicode_name(token, spos, prefix)
yield t # yield from Python 2
elif initial in '\r\n': elif initial in '\r\n':
if any(not f.allow_multiline() for f in fstring_stack): if any(not f.allow_multiline() for f in fstring_stack):
# Would use fstring_stack.clear, but that's not available fstring_stack.clear()
# in Python 2.
fstring_stack[:] = []
if not new_line and paren_level == 0 and not fstring_stack: if not new_line and paren_level == 0 and not fstring_stack:
yield PythonToken(NEWLINE, token, spos, prefix) yield PythonToken(NEWLINE, token, spos, prefix)
@@ -681,7 +653,7 @@ def _split_illegal_unicode_name(token, start_pos, prefix):
pos = start_pos pos = start_pos
for i, char in enumerate(token): for i, char in enumerate(token):
if is_illegal: if is_illegal:
if is_identifier(char): if char.isidentifier():
yield create_token() yield create_token()
found = char found = char
is_illegal = False is_illegal = False
@@ -691,7 +663,7 @@ def _split_illegal_unicode_name(token, start_pos, prefix):
found += char found += char
else: else:
new_found = found + char new_found = found + char
if is_identifier(new_found): if new_found.isidentifier():
found = new_found found = new_found
else: else:
if found: if found:
@@ -706,17 +678,9 @@ def _split_illegal_unicode_name(token, start_pos, prefix):
if __name__ == "__main__": if __name__ == "__main__":
if len(sys.argv) >= 2: path = sys.argv[1]
path = sys.argv[1] with open(path) as f:
with open(path) as f: code = f.read()
code = f.read()
else:
code = sys.stdin.read()
from parso.utils import python_bytes_to_unicode, parse_version_string for token in tokenize(code, version_info=parse_version_string('3.10')):
if isinstance(code, bytes):
code = python_bytes_to_unicode(code)
for token in tokenize(code, parse_version_string()):
print(token) print(token)

View File

@@ -1,24 +0,0 @@
from typing import Generator, Iterable, NamedTuple, Tuple
from parso.python.token import TokenType
from parso.utils import PythonVersionInfo
class Token(NamedTuple):
type: TokenType
string: str
start_pos: Tuple[int, int]
prefix: str
@property
def end_pos(self) -> Tuple[int, int]: ...
class PythonToken(Token):
def __repr__(self) -> str: ...
def tokenize(
code: str, version_info: PythonVersionInfo, start_pos: Tuple[int, int] = (1, 0)
) -> Generator[PythonToken, None, None]: ...
def tokenize_lines(
lines: Iterable[str],
version_info: PythonVersionInfo,
start_pos: Tuple[int, int] = (1, 0),
) -> Generator[PythonToken, None, None]: ...

View File

@@ -1,5 +1,5 @@
""" """
This is the syntax tree for Python syntaxes (2 & 3). The classes represent This is the syntax tree for Python 3 syntaxes. The classes represent
syntax elements like functions and imports. syntax elements like functions and imports.
All of the nodes can be traced back to the `Python grammar file All of the nodes can be traced back to the `Python grammar file
@@ -48,7 +48,6 @@ try:
except ImportError: except ImportError:
from collections import Mapping from collections import Mapping
from parso._compatibility import utf8_repr, unicode
from parso.tree import Node, BaseNode, Leaf, ErrorNode, ErrorLeaf, \ 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
@@ -64,12 +63,12 @@ _FUNC_CONTAINERS = set(
_GET_DEFINITION_TYPES = set([ _GET_DEFINITION_TYPES = set([
'expr_stmt', 'sync_comp_for', 'with_stmt', 'for_stmt', 'import_name', 'expr_stmt', 'sync_comp_for', 'with_stmt', 'for_stmt', 'import_name',
'import_from', 'param', 'del_stmt', 'import_from', 'param', 'del_stmt', 'namedexpr_test',
]) ])
_IMPORTS = set(['import_name', 'import_from']) _IMPORTS = set(['import_name', 'import_from'])
class DocstringMixin(object): class DocstringMixin:
__slots__ = () __slots__ = ()
def get_doc_node(self): def get_doc_node(self):
@@ -97,7 +96,7 @@ class DocstringMixin(object):
return None return None
class PythonMixin(object): class PythonMixin:
""" """
Some Python specific utilities. Some Python specific utilities.
""" """
@@ -175,7 +174,6 @@ class EndMarker(_LeafWithoutNewlines):
__slots__ = () __slots__ = ()
type = 'endmarker' type = 'endmarker'
@utf8_repr
def __repr__(self): def __repr__(self):
return "<%s: prefix=%s end_pos=%s>" % ( return "<%s: prefix=%s end_pos=%s>" % (
type(self).__name__, repr(self.prefix), self.end_pos type(self).__name__, repr(self.prefix), self.end_pos
@@ -187,7 +185,6 @@ class Newline(PythonLeaf):
__slots__ = () __slots__ = ()
type = 'newline' type = 'newline'
@utf8_repr
def __repr__(self): def __repr__(self):
return "<%s: %s>" % (type(self).__name__, repr(self.value)) return "<%s: %s>" % (type(self).__name__, repr(self.value))
@@ -227,9 +224,6 @@ class Name(_LeafWithoutNewlines):
return None return None
if type_ == 'except_clause': 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': if self.get_previous_sibling() == 'as':
return node.parent # The try_stmt. return node.parent # The try_stmt.
return None return None
@@ -237,8 +231,6 @@ class Name(_LeafWithoutNewlines):
while node is not None: while node is not None:
if node.type == 'suite': if node.type == 'suite':
return None return None
if node.type == 'namedexpr_test':
return node.children[0]
if node.type in _GET_DEFINITION_TYPES: if node.type in _GET_DEFINITION_TYPES:
if self in node.get_defined_names(include_setitem): if self in node.get_defined_names(include_setitem):
return node return node
@@ -302,21 +294,17 @@ class FStringEnd(PythonLeaf):
__slots__ = () __slots__ = ()
class _StringComparisonMixin(object): class _StringComparisonMixin:
def __eq__(self, other): def __eq__(self, other):
""" """
Make comparisons with strings easy. Make comparisons with strings easy.
Improves the readability of the parser. Improves the readability of the parser.
""" """
if isinstance(other, (str, unicode)): if isinstance(other, str):
return self.value == other return self.value == other
return self is other return self is other
def __ne__(self, other):
"""Python 2 compatibility."""
return not self.__eq__(other)
def __hash__(self): def __hash__(self):
return hash(self.value) return hash(self.value)
@@ -340,7 +328,7 @@ class Scope(PythonBaseNode, DocstringMixin):
__slots__ = () __slots__ = ()
def __init__(self, children): def __init__(self, children):
super(Scope, self).__init__(children) super().__init__(children)
def iter_funcdefs(self): def iter_funcdefs(self):
""" """
@@ -366,8 +354,7 @@ class Scope(PythonBaseNode, DocstringMixin):
if element.type in names: if element.type in names:
yield element yield element
if element.type in _FUNC_CONTAINERS: if element.type in _FUNC_CONTAINERS:
for e in scan(element.children): yield from scan(element.children)
yield e
return scan(self.children) return scan(self.children)
@@ -397,7 +384,7 @@ class Module(Scope):
type = 'file_input' type = 'file_input'
def __init__(self, children): def __init__(self, children):
super(Module, self).__init__(children) super().__init__(children)
self._used_names = None self._used_names = None
def _iter_future_import_names(self): def _iter_future_import_names(self):
@@ -416,18 +403,6 @@ 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):
"""
Checks if imports in this module are explicitly absolute, i.e. there
is a ``__future__`` import.
Currently not public, might be in the future.
:return bool:
"""
for name in self._iter_future_import_names():
if name == 'absolute_import':
return True
return False
def get_used_names(self): def get_used_names(self):
""" """
Returns all the :class:`Name` leafs that exist in this module. This Returns all the :class:`Name` leafs that exist in this module. This
@@ -493,7 +468,7 @@ class Class(ClassOrFunc):
__slots__ = () __slots__ = ()
def __init__(self, children): def __init__(self, children):
super(Class, self).__init__(children) super().__init__(children)
def get_super_arglist(self): def get_super_arglist(self):
""" """
@@ -520,24 +495,13 @@ def _create_params(parent, argslist_list):
You could also say that this function replaces the argslist node with a You could also say that this function replaces the argslist node with a
list of Param objects. list of Param objects.
""" """
def check_python2_nested_param(node):
"""
Python 2 allows params to look like ``def x(a, (b, c))``, which is
basically a way of unpacking tuples in params. Python 3 has ditched
this behavior. Jedi currently just ignores those constructs.
"""
return node.type == 'fpdef' and node.children[0] == '('
try: try:
first = argslist_list[0] first = argslist_list[0]
except IndexError: except IndexError:
return [] return []
if first.type in ('name', 'fpdef'): if first.type in ('name', 'fpdef'):
if check_python2_nested_param(first): return [Param([first], parent)]
return [first]
else:
return [Param([first], parent)]
elif first == '*': elif first == '*':
return [first] return [first]
else: # argslist is a `typedargslist` or a `varargslist`. else: # argslist is a `typedargslist` or a `varargslist`.
@@ -555,7 +519,6 @@ def _create_params(parent, argslist_list):
if param_children[0] == '*' \ if param_children[0] == '*' \
and (len(param_children) == 1 and (len(param_children) == 1
or param_children[1] == ',') \ or param_children[1] == ',') \
or check_python2_nested_param(param_children[0]) \
or param_children[0] == '/': or param_children[0] == '/':
for p in param_children: for p in param_children:
p.parent = parent p.parent = parent
@@ -583,7 +546,7 @@ class Function(ClassOrFunc):
type = 'funcdef' type = 'funcdef'
def __init__(self, children): def __init__(self, children):
super(Function, self).__init__(children) super().__init__(children)
parameters = self.children[2] # After `def foo` parameters = self.children[2] # After `def foo`
parameters.children[1:-1] = _create_params(parameters, parameters.children[1:-1]) parameters.children[1:-1] = _create_params(parameters, parameters.children[1:-1])
@@ -618,8 +581,7 @@ class Function(ClassOrFunc):
else: else:
yield element yield element
else: else:
for result in scan(nested_children): yield from scan(nested_children)
yield result
return scan(self.children) return scan(self.children)
@@ -633,8 +595,7 @@ class Function(ClassOrFunc):
or element.type == 'keyword' and element.value == 'return': or element.type == 'keyword' and element.value == 'return':
yield element yield element
if element.type in _RETURN_STMT_CONTAINERS: if element.type in _RETURN_STMT_CONTAINERS:
for e in scan(element.children): yield from scan(element.children)
yield e
return scan(self.children) return scan(self.children)
@@ -648,8 +609,7 @@ class Function(ClassOrFunc):
or element.type == 'keyword' and element.value == 'raise': or element.type == 'keyword' and element.value == 'raise':
yield element yield element
if element.type in _RETURN_STMT_CONTAINERS: if element.type in _RETURN_STMT_CONTAINERS:
for e in scan(element.children): yield from scan(element.children)
yield e
return scan(self.children) return scan(self.children)
@@ -815,8 +775,8 @@ class WithStmt(Flow):
return names return names
def get_test_node_from_name(self, name): def get_test_node_from_name(self, name):
node = name.parent node = search_ancestor(name, "with_item")
if node.type != 'with_item': if node is None:
raise ValueError('The name is not actually part of a with statement.') raise ValueError('The name is not actually part of a with statement.')
return node.children[0] return node.children[0]
@@ -1101,8 +1061,14 @@ class ExprStmt(PythonBaseNode, DocstringMixin):
first = first.children[2] first = first.children[2]
yield first yield first
for operator in self.children[3::2]: yield from self.children[3::2]
yield operator
class NamedExpr(PythonBaseNode):
type = 'namedexpr_test'
def get_defined_names(self, include_setitem=False):
return _defined_names(self.children[0], include_setitem)
class Param(PythonBaseNode): class Param(PythonBaseNode):
@@ -1114,7 +1080,7 @@ class Param(PythonBaseNode):
type = 'param' type = 'param'
def __init__(self, children, parent): def __init__(self, children, parent):
super(Param, self).__init__(children) super().__init__(children)
self.parent = parent self.parent = parent
for child in children: for child in children:
child.parent = self child.parent = self
@@ -1214,7 +1180,7 @@ class Param(PythonBaseNode):
:param include_comma bool: If enabled includes the comma in the string output. :param include_comma bool: If enabled includes the comma in the string output.
""" """
if include_comma: if include_comma:
return super(Param, self).get_code(include_prefix) return super().get_code(include_prefix)
children = self.children children = self.children
if children[-1] == ',': if children[-1] == ',':

View File

@@ -1,7 +1,5 @@
import sys
from abc import abstractmethod, abstractproperty from abc import abstractmethod, abstractproperty
from parso._compatibility import utf8_repr, encoding
from parso.utils import split_lines from parso.utils import split_lines
@@ -20,12 +18,12 @@ def search_ancestor(node, *node_types):
return node return node
class NodeOrLeaf(object): class NodeOrLeaf:
""" """
The base class for nodes and leaves. The base class for nodes and leaves.
""" """
__slots__ = () __slots__ = ()
type = None type: str
''' '''
The type is a string that typically matches the types of the grammar file. The type is a string that typically matches the types of the grammar file.
''' '''
@@ -238,7 +236,6 @@ class Leaf(NodeOrLeaf):
end_pos_column = len(lines[-1]) end_pos_column = len(lines[-1])
return end_pos_line, end_pos_column return end_pos_line, end_pos_column
@utf8_repr
def __repr__(self): def __repr__(self):
value = self.value value = self.value
if not value: if not value:
@@ -250,7 +247,7 @@ class TypedLeaf(Leaf):
__slots__ = ('type',) __slots__ = ('type',)
def __init__(self, type, value, start_pos, prefix=''): def __init__(self, type, value, start_pos, prefix=''):
super(TypedLeaf, self).__init__(value, start_pos, prefix) super().__init__(value, start_pos, prefix)
self.type = type self.type = type
@@ -260,7 +257,6 @@ class BaseNode(NodeOrLeaf):
A node has children, a type and possibly a parent node. A node has children, a type and possibly a parent node.
""" """
__slots__ = ('children', 'parent') __slots__ = ('children', 'parent')
type = None
def __init__(self, children): def __init__(self, children):
self.children = children self.children = children
@@ -315,7 +311,6 @@ class BaseNode(NodeOrLeaf):
except AttributeError: except AttributeError:
return element return element
index = int((lower + upper) / 2) index = int((lower + upper) / 2)
element = self.children[index] element = self.children[index]
if position <= element.end_pos: if position <= element.end_pos:
@@ -333,11 +328,8 @@ class BaseNode(NodeOrLeaf):
def get_last_leaf(self): def get_last_leaf(self):
return self.children[-1].get_last_leaf() return self.children[-1].get_last_leaf()
@utf8_repr
def __repr__(self): def __repr__(self):
code = self.get_code().replace('\n', ' ').replace('\r', ' ').strip() code = self.get_code().replace('\n', ' ').replace('\r', ' ').strip()
if not sys.version_info.major >= 3:
code = code.encode(encoding, 'replace')
return "<%s: %s@%s,%s>" % \ return "<%s: %s@%s,%s>" % \
(type(self).__name__, code, self.start_pos[0], self.start_pos[1]) (type(self).__name__, code, self.start_pos[0], self.start_pos[1])
@@ -347,7 +339,7 @@ class Node(BaseNode):
__slots__ = ('type',) __slots__ = ('type',)
def __init__(self, type, children): def __init__(self, type, children):
super(Node, self).__init__(children) super().__init__(children)
self.type = type self.type = type
def __repr__(self): def __repr__(self):
@@ -373,7 +365,7 @@ class ErrorLeaf(Leaf):
type = 'error_leaf' type = 'error_leaf'
def __init__(self, token_type, value, start_pos, prefix=''): def __init__(self, token_type, value, start_pos, prefix=''):
super(ErrorLeaf, self).__init__(value, start_pos, prefix) super().__init__(value, start_pos, prefix)
self.token_type = token_type self.token_type = token_type
def __repr__(self): def __repr__(self):

View File

@@ -1,30 +1,32 @@
from collections import namedtuple
import re import re
import sys import sys
from ast import literal_eval from ast import literal_eval
from functools import total_ordering from functools import total_ordering
from typing import NamedTuple, Sequence, Union
from parso._compatibility import unicode
# The following is a list in Python that are line breaks in str.splitlines, but # The following is a list in Python that are line breaks in str.splitlines, but
# not in Python. In Python only \r (Carriage Return, 0xD) and \n (Line Feed, # not in Python. In Python only \r (Carriage Return, 0xD) and \n (Line Feed,
# 0xA) are allowed to split lines. # 0xA) are allowed to split lines.
_NON_LINE_BREAKS = ( _NON_LINE_BREAKS = (
u'\v', # Vertical Tabulation 0xB '\v', # Vertical Tabulation 0xB
u'\f', # Form Feed 0xC '\f', # Form Feed 0xC
u'\x1C', # File Separator '\x1C', # File Separator
u'\x1D', # Group Separator '\x1D', # Group Separator
u'\x1E', # Record Separator '\x1E', # Record Separator
u'\x85', # Next Line (NEL - Equivalent to CR+LF. '\x85', # Next Line (NEL - Equivalent to CR+LF.
# Used to mark end-of-line on some IBM mainframes.) # Used to mark end-of-line on some IBM mainframes.)
u'\u2028', # Line Separator '\u2028', # Line Separator
u'\u2029', # Paragraph Separator '\u2029', # Paragraph Separator
) )
Version = namedtuple('Version', 'major, minor, micro')
class Version(NamedTuple):
major: int
minor: int
micro: int
def split_lines(string, keepends=False): def split_lines(string: str, keepends: bool = False) -> Sequence[str]:
r""" r"""
Intended for Python code. In contrast to Python's :py:meth:`str.splitlines`, Intended for Python code. In contrast to Python's :py:meth:`str.splitlines`,
looks at form feeds and other special characters as normal text. Just looks at form feeds and other special characters as normal text. Just
@@ -68,7 +70,9 @@ def split_lines(string, keepends=False):
return re.split(r'\n|\r\n|\r', string) return re.split(r'\n|\r\n|\r', string)
def python_bytes_to_unicode(source, encoding='utf-8', errors='strict'): def python_bytes_to_unicode(
source: Union[str, bytes], encoding: str = 'utf-8', errors: str = 'strict'
) -> str:
""" """
Checks for unicode BOMs and PEP 263 encoding declarations. Then returns a Checks for unicode BOMs and PEP 263 encoding declarations. Then returns a
unicode object like in :py:meth:`bytes.decode`. unicode object like in :py:meth:`bytes.decode`.
@@ -92,33 +96,33 @@ def python_bytes_to_unicode(source, encoding='utf-8', errors='strict'):
possible_encoding = re.search(br"coding[=:]\s*([-\w.]+)", possible_encoding = re.search(br"coding[=:]\s*([-\w.]+)",
first_two_lines) first_two_lines)
if possible_encoding: if possible_encoding:
return possible_encoding.group(1) e = possible_encoding.group(1)
if not isinstance(e, str):
e = str(e, 'ascii', 'replace')
return e
else: else:
# the default if nothing else has been set -> PEP 263 # the default if nothing else has been set -> PEP 263
return encoding return encoding
if isinstance(source, unicode): if isinstance(source, str):
# only cast str/bytes # only cast str/bytes
return source return source
encoding = detect_encoding() encoding = detect_encoding()
if not isinstance(encoding, unicode):
encoding = unicode(encoding, 'utf-8', 'replace')
try: try:
# Cast to unicode # Cast to unicode
return unicode(source, encoding, errors) return str(source, encoding, errors)
except LookupError: except LookupError:
if errors == 'replace': if errors == 'replace':
# This is a weird case that can happen if the given encoding is not # This is a weird case that can happen if the given encoding is not
# a valid encoding. This usually shouldn't happen with provided # a valid encoding. This usually shouldn't happen with provided
# encodings, but can happen if somebody uses encoding declarations # encodings, but can happen if somebody uses encoding declarations
# like `# coding: foo-8`. # like `# coding: foo-8`.
return unicode(source, 'utf-8', errors) return str(source, 'utf-8', errors)
raise raise
def version_info(): def version_info() -> Version:
""" """
Returns a namedtuple of parso's version, similar to Python's Returns a namedtuple of parso's version, similar to Python's
``sys.version_info``. ``sys.version_info``.
@@ -128,7 +132,34 @@ def version_info():
return Version(*[x if i == 3 else int(x) for i, x in enumerate(tupl)]) return Version(*[x if i == 3 else int(x) for i, x in enumerate(tupl)])
def _parse_version(version): class _PythonVersionInfo(NamedTuple):
major: int
minor: int
@total_ordering
class PythonVersionInfo(_PythonVersionInfo):
def __gt__(self, other):
if isinstance(other, tuple):
if len(other) != 2:
raise ValueError("Can only compare to tuples of length 2.")
return (self.major, self.minor) > other
super().__gt__(other)
return (self.major, self.minor)
def __eq__(self, other):
if isinstance(other, tuple):
if len(other) != 2:
raise ValueError("Can only compare to tuples of length 2.")
return (self.major, self.minor) == other
super().__eq__(other)
def __ne__(self, other):
return not self.__eq__(other)
def _parse_version(version) -> PythonVersionInfo:
match = re.match(r'(\d+)(?:\.(\d{1,2})(?:\.\d+)?)?((a|b|rc)\d)?$', version) match = re.match(r'(\d+)(?:\.(\d{1,2})(?:\.\d+)?)?((a|b|rc)\d)?$', version)
if match is None: if match is None:
raise ValueError('The given version is not in the right format. ' raise ValueError('The given version is not in the right format. '
@@ -149,37 +180,15 @@ def _parse_version(version):
return PythonVersionInfo(major, minor) return PythonVersionInfo(major, minor)
@total_ordering def parse_version_string(version: str = None) -> PythonVersionInfo:
class PythonVersionInfo(namedtuple('Version', 'major, minor')):
def __gt__(self, other):
if isinstance(other, tuple):
if len(other) != 2:
raise ValueError("Can only compare to tuples of length 2.")
return (self.major, self.minor) > other
super(PythonVersionInfo, self).__gt__(other)
return (self.major, self.minor)
def __eq__(self, other):
if isinstance(other, tuple):
if len(other) != 2:
raise ValueError("Can only compare to tuples of length 2.")
return (self.major, self.minor) == other
super(PythonVersionInfo, self).__eq__(other)
def __ne__(self, other):
return not self.__eq__(other)
def parse_version_string(version=None):
""" """
Checks for a valid version number (e.g. `3.8` or `2.7.1` or `3`) and Checks for a valid version number (e.g. `3.8` or `3.10.1` or `3`) and
returns a corresponding version info that is always two characters long in returns a corresponding version info that is always two characters long in
decimal. decimal.
""" """
if version is None: if version is None:
version = '%s.%s' % sys.version_info[:2] version = '%s.%s' % sys.version_info[:2]
if not isinstance(version, (unicode, str)): if not isinstance(version, str):
raise TypeError('version must be a string like "3.8"') raise TypeError('version must be a string like "3.8"')
return _parse_version(version) return _parse_version(version)

View File

@@ -1,29 +0,0 @@
from typing import NamedTuple, Optional, Sequence, Union
class Version(NamedTuple):
major: int
minor: int
micro: int
def split_lines(string: str, keepends: bool = ...) -> Sequence[str]: ...
def python_bytes_to_unicode(
source: Union[str, bytes], encoding: str = ..., errors: str = ...
) -> str: ...
def version_info() -> Version:
"""
Returns a namedtuple of parso's version, similar to Python's
``sys.version_info``.
"""
...
class PythonVersionInfo(NamedTuple):
major: int
minor: int
def parse_version_string(version: Optional[str]) -> PythonVersionInfo:
"""
Checks for a valid version number (e.g. `3.2` or `2.7.1` or `3`) and
returns a corresponding version info that is always two characters long in
decimal.
"""
...

View File

@@ -18,7 +18,6 @@ from docopt import docopt
from jedi.parser.python import load_grammar from jedi.parser.python import load_grammar
from jedi.parser.diff import DiffParser from jedi.parser.diff import DiffParser
from jedi.parser.python import ParserWithRecovery from jedi.parser.python import ParserWithRecovery
from jedi._compatibility import u
from jedi.common import splitlines from jedi.common import splitlines
import jedi import jedi
@@ -37,14 +36,15 @@ def main(args):
with open(args['<file>']) as f: with open(args['<file>']) as f:
code = f.read() code = f.read()
grammar = load_grammar() grammar = load_grammar()
parser = ParserWithRecovery(grammar, u(code)) parser = ParserWithRecovery(grammar, code)
# Make sure used_names is loaded # Make sure used_names is loaded
parser.module.used_names parser.module.used_names
code = code + '\na\n' # Add something so the diff parser needs to run. code = code + '\na\n' # Add something so the diff parser needs to run.
lines = splitlines(code, keepends=True) lines = splitlines(code, keepends=True)
cProfile.runctx('run(parser, lines)', globals(), locals(), sort=args['-s']) cProfile.runctx('run(parser, lines)', globals(), locals(), sort=args['-s'])
if __name__ == '__main__': if __name__ == '__main__':
args = docopt(__doc__) args = docopt(__doc__)
main(args) main(args)

View File

@@ -10,3 +10,16 @@ ignore =
E226, E226,
# line break before binary operator # line break before binary operator
W503, W503,
[mypy]
disallow_subclassing_any = True
# Avoid creating future gotchas emerging from bad typing
warn_redundant_casts = True
warn_unused_ignores = True
warn_return_any = True
warn_unused_configs = True
warn_unreachable = True
strict_equality = True

View File

@@ -12,44 +12,47 @@ __AUTHOR_EMAIL__ = 'davidhalter88@gmail.com'
readme = open('README.rst').read() + '\n\n' + open('CHANGELOG.rst').read() readme = open('README.rst').read() + '\n\n' + open('CHANGELOG.rst').read()
setup(name='parso', setup(
version=parso.__version__, name='parso',
description='A Python Parser', version=parso.__version__,
author=__AUTHOR__, description='A Python Parser',
author_email=__AUTHOR_EMAIL__, author=__AUTHOR__,
include_package_data=True, author_email=__AUTHOR_EMAIL__,
maintainer=__AUTHOR__, include_package_data=True,
maintainer_email=__AUTHOR_EMAIL__, maintainer=__AUTHOR__,
url='https://github.com/davidhalter/parso', maintainer_email=__AUTHOR_EMAIL__,
license='MIT', url='https://github.com/davidhalter/parso',
keywords='python parser parsing', license='MIT',
long_description=readme, keywords='python parser parsing',
packages=find_packages(exclude=['test']), long_description=readme,
package_data={'parso': ['python/grammar*.txt', 'py.typed', '*.pyi', '**/*.pyi']}, packages=find_packages(exclude=['test']),
platforms=['any'], package_data={'parso': ['python/grammar*.txt', 'py.typed', '*.pyi', '**/*.pyi']},
python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*', platforms=['any'],
classifiers=[ python_requires='>=3.6',
'Development Status :: 4 - Beta', classifiers=[
'Environment :: Plugins', 'Development Status :: 4 - Beta',
'Intended Audience :: Developers', 'Environment :: Plugins',
'License :: OSI Approved :: MIT License', 'Intended Audience :: Developers',
'Operating System :: OS Independent', 'License :: OSI Approved :: MIT License',
'Programming Language :: Python :: 2', 'Operating System :: OS Independent',
'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.7', 'Topic :: Software Development :: Libraries :: Python Modules',
'Topic :: Software Development :: Libraries :: Python Modules', 'Topic :: Text Editors :: Integrated Development Environments (IDE)',
'Topic :: Text Editors :: Integrated Development Environments (IDE)', 'Topic :: Utilities',
'Topic :: Utilities', 'Typing :: Typed',
'Typing :: Typed', ],
], extras_require={
extras_require={ 'testing': [
'testing': [ 'pytest<6.0.0',
'pytest>=3.0.7', 'docopt',
'docopt', ],
], 'qa': [
}, 'flake8==3.8.3',
) 'mypy==0.782',
],
},
)

View File

@@ -34,7 +34,7 @@ FAILING_EXAMPLES = [
'lambda x=3, y: x', 'lambda x=3, y: x',
'__debug__ = 1', '__debug__ = 1',
'with x() as __debug__: pass', 'with x() as __debug__: pass',
# Mostly 3.6 relevant
'[]: int', '[]: int',
'[a, b]: int', '[a, b]: int',
'(): int', '(): int',
@@ -56,6 +56,7 @@ FAILING_EXAMPLES = [
'a, b += 3', 'a, b += 3',
'(a, b) += 3', '(a, b) += 3',
'[a, b] += 3', '[a, b] += 3',
'[a, 1] += 3',
'f() += 1', 'f() += 1',
'lambda x:None+=1', 'lambda x:None+=1',
'{} += 1', '{} += 1',
@@ -130,6 +131,8 @@ FAILING_EXAMPLES = [
r"u'\N{foo}'", r"u'\N{foo}'",
r'b"\x"', r'b"\x"',
r'b"\"', r'b"\"',
'b"ä"',
'*a, *b = 3, 3', '*a, *b = 3, 3',
'async def foo(): yield from []', 'async def foo(): yield from []',
'yield from []', 'yield from []',
@@ -138,6 +141,16 @@ FAILING_EXAMPLES = [
'def x(*): pass', 'def x(*): pass',
'(%s *d) = x' % ('a,' * 256), '(%s *d) = x' % ('a,' * 256),
'{**{} for a in [1]}', '{**{} for a in [1]}',
'(True,) = x',
'([False], a) = x',
'def x(): from math import *',
# str/bytes combinations
'"s" b""',
'"s" b"" ""',
'b"" "" b"" ""',
'f"s" b""',
'b"s" f""',
# Parser/tokenize.c # Parser/tokenize.c
r'"""', r'"""',
@@ -176,9 +189,17 @@ FAILING_EXAMPLES = [
"f'{1;1}'", "f'{1;1}'",
"f'{a;}'", "f'{a;}'",
"f'{b\"\" \"\"}'", "f'{b\"\" \"\"}'",
] # f-string expression part cannot include a backslash
r'''f"{'\n'}"''',
GLOBAL_NONLOCAL_ERROR = [ 'async def foo():\n yield x\n return 1',
'async def foo():\n yield x\n return 1',
'[*[] for a in [1]]',
'async def bla():\n def x(): await bla()',
'del None',
# Errors of global / nonlocal
dedent(''' dedent('''
def glob(): def glob():
x = 3 x = 3
@@ -277,65 +298,6 @@ GLOBAL_NONLOCAL_ERROR = [
'''), '''),
] ]
if sys.version_info >= (3, 6):
FAILING_EXAMPLES += GLOBAL_NONLOCAL_ERROR
if sys.version_info >= (3, 5):
FAILING_EXAMPLES += [
# Raises different errors so just ignore them for now.
'[*[] for a in [1]]',
# Raises multiple errors in previous versions.
'async def bla():\n def x(): await bla()',
]
if sys.version_info >= (3, 4):
# Before that del None works like del list, it gives a NameError.
FAILING_EXAMPLES.append('del None')
if sys.version_info >= (3,):
FAILING_EXAMPLES += [
# Unfortunately assigning to False and True do not raise an error in
# 2.x.
'(True,) = x',
'([False], a) = x',
# A symtable error that raises only a SyntaxWarning in Python 2.
'def x(): from math import *',
# unicode chars in bytes are allowed in python 2
'b"ä"',
# combining strings and unicode is allowed in Python 2.
'"s" b""',
'"s" b"" ""',
'b"" "" b"" ""',
]
if sys.version_info >= (3, 6):
FAILING_EXAMPLES += [
# Same as above, but for f-strings.
'f"s" b""',
'b"s" f""',
# f-string expression part cannot include a backslash
r'''f"{'\n'}"''',
]
FAILING_EXAMPLES.append('[a, 1] += 3')
if sys.version_info[:2] == (3, 5):
# yields are not allowed in 3.5 async functions. Therefore test them
# separately, here.
FAILING_EXAMPLES += [
'async def foo():\n yield x',
'async def foo():\n yield x',
]
else:
FAILING_EXAMPLES += [
'async def foo():\n yield x\n return 1',
'async def foo():\n yield x\n return 1',
]
if sys.version_info[:2] <= (3, 4):
# Python > 3.4 this is valid code.
FAILING_EXAMPLES += [
'a = *[1], 2',
'(*[1], 2)',
]
if sys.version_info[:2] >= (3, 7): if sys.version_info[:2] >= (3, 7):
# This is somehow ok in previous versions. # This is somehow ok in previous versions.
FAILING_EXAMPLES += [ FAILING_EXAMPLES += [
@@ -394,4 +356,12 @@ if sys.version_info[:2] >= (3, 8):
'(False := 1)', '(False := 1)',
'(None := 1)', '(None := 1)',
'(__debug__ := 1)', '(__debug__ := 1)',
# Unparenthesized walrus not allowed in dict literals, dict comprehensions and slices
'{a:="a": b:=1}',
'{y:=1: 2 for x in range(5)}',
'a[b:=0:1:2]',
]
# f-string debugging syntax with invalid conversion character
FAILING_EXAMPLES += [
"f'{1=!b}'",
] ]

View File

@@ -135,11 +135,11 @@ class FileModification:
# We cannot delete every line, that doesn't make sense to # We cannot delete every line, that doesn't make sense to
# fuzz and it would be annoying to rewrite everything here. # fuzz and it would be annoying to rewrite everything here.
continue continue
l = LineDeletion(random_line()) ld = LineDeletion(random_line())
elif rand == 2: elif rand == 2:
# Copy / Insertion # Copy / Insertion
# Make it possible to insert into the first and the last line # Make it possible to insert into the first and the last line
l = LineCopy(random_line(), random_line(include_end=True)) ld = LineCopy(random_line(), random_line(include_end=True))
elif rand in (3, 4): elif rand in (3, 4):
# Modify a line in some weird random ways. # Modify a line in some weird random ways.
line_nr = random_line() line_nr = random_line()
@@ -166,9 +166,9 @@ class FileModification:
# we really replace the line with something that has # we really replace the line with something that has
# indentation. # indentation.
line = ' ' * random.randint(0, 12) + random_string + '\n' line = ' ' * random.randint(0, 12) + random_string + '\n'
l = LineReplacement(line_nr, line) ld = LineReplacement(line_nr, line)
l.apply(lines) ld.apply(lines)
yield l yield ld
def __init__(self, modification_list, check_original): def __init__(self, modification_list, check_original):
self.modification_list = modification_list self.modification_list = modification_list

View File

@@ -44,3 +44,75 @@ a = 3
def x(b=a): def x(b=a):
global a global a
*foo, a = (1,)
*foo[0], a = (1,)
*[], a = (1,)
async def foo():
await bar()
#: E901
yield from []
return
#: E901
return ''
# With decorator it's a different statement.
@bla
async def foo():
await bar()
#: E901
yield from []
return
#: E901
return ''
foo: int = 4
(foo): int = 3
((foo)): int = 3
foo.bar: int
foo[3]: int
def glob():
global x
y: foo = x
def c():
a = 3
def d():
class X():
nonlocal a
def x():
a = 3
def y():
nonlocal a
def x():
def y():
nonlocal a
a = 3
def x():
a = 3
def y():
class z():
nonlocal a
a = *args, *args
error[(*args, *args)] = 3
*args, *args

View File

@@ -1,2 +0,0 @@
's' b''
u's' b'ä'

View File

@@ -1,3 +0,0 @@
*foo, a = (1,)
*foo[0], a = (1,)
*[], a = (1,)

View File

@@ -1,23 +0,0 @@
"""
Mostly allowed syntax in Python 3.5.
"""
async def foo():
await bar()
#: E901
yield from []
return
#: E901
return ''
# With decorator it's a different statement.
@bla
async def foo():
await bar()
#: E901
yield from []
return
#: E901
return ''

View File

@@ -1,45 +0,0 @@
foo: int = 4
(foo): int = 3
((foo)): int = 3
foo.bar: int
foo[3]: int
def glob():
global x
y: foo = x
def c():
a = 3
def d():
class X():
nonlocal a
def x():
a = 3
def y():
nonlocal a
def x():
def y():
nonlocal a
a = 3
def x():
a = 3
def y():
class z():
nonlocal a
a = *args, *args
error[(*args, *args)] = 3
*args, *args

View File

@@ -1,14 +0,0 @@
import sys
print 1, 2 >> sys.stdout
foo = ur'This is not possible in Python 3.'
# This is actually printing a tuple.
#: E275:5
print(1, 2)
# True and False are not keywords in Python 2 and therefore there's no need for
# a space.
norman = True+False

View File

@@ -1,29 +0,0 @@
"""
Tests ``from __future__ import absolute_import`` (only important for
Python 2.X)
"""
from parso import parse
def test_explicit_absolute_imports():
"""
Detect modules with ``from __future__ import absolute_import``.
"""
module = parse("from __future__ import absolute_import")
assert module._has_explicit_absolute_import()
def test_no_explicit_absolute_imports():
"""
Detect modules without ``from __future__ import absolute_import``.
"""
assert not parse("1")._has_explicit_absolute_import()
def test_dont_break_imports_without_namespaces():
"""
The code checking for ``from __future__ import absolute_import`` shouldn't
assume that all imports have non-``None`` namespaces.
"""
src = "from __future__ import absolute_import\nimport xyzzy"
assert parse(src)._has_explicit_absolute_import()

View File

@@ -3,17 +3,16 @@ Test all things related to the ``jedi.cache`` module.
""" """
import os import os
import os.path
import pytest import pytest
import time import time
from pathlib import Path
from parso.cache import (_CACHED_FILE_MAXIMUM_SURVIVAL, _VERSION_TAG, from parso.cache import (_CACHED_FILE_MAXIMUM_SURVIVAL, _VERSION_TAG,
_get_cache_clear_lock, _get_hashed_path, _get_cache_clear_lock_path, _get_hashed_path,
_load_from_file_system, _NodeCacheItem, _load_from_file_system, _NodeCacheItem,
_remove_cache_and_update_lock, _save_to_file_system, _remove_cache_and_update_lock, _save_to_file_system,
load_module, parser_cache, try_to_save_module) load_module, parser_cache, try_to_save_module)
from parso._compatibility import is_pypy, PermissionError from parso._compatibility import is_pypy
from parso import load_grammar from parso import load_grammar
from parso import cache from parso import cache
from parso import file_io from parso import file_io
@@ -30,9 +29,8 @@ skip_pypy = pytest.mark.skipif(
def isolated_parso_cache(monkeypatch, tmpdir): def isolated_parso_cache(monkeypatch, tmpdir):
"""Set `parso.cache._default_cache_path` to a temporary directory """Set `parso.cache._default_cache_path` to a temporary directory
during the test. """ during the test. """
cache_path = str(os.path.join(str(tmpdir), "__parso_cache")) cache_path = Path(str(tmpdir), "__parso_cache")
monkeypatch.setattr(cache, '_default_cache_path', cache_path) monkeypatch.setattr(cache, '_default_cache_path', cache_path)
monkeypatch.setattr(cache, '_get_default_cache_path', lambda *args, **kwargs: cache_path)
return cache_path return cache_path
@@ -42,13 +40,13 @@ def test_modulepickling_change_cache_dir(tmpdir):
See: `#168 <https://github.com/davidhalter/jedi/pull/168>`_ See: `#168 <https://github.com/davidhalter/jedi/pull/168>`_
""" """
dir_1 = str(tmpdir.mkdir('first')) dir_1 = Path(str(tmpdir.mkdir('first')))
dir_2 = str(tmpdir.mkdir('second')) dir_2 = Path(str(tmpdir.mkdir('second')))
item_1 = _NodeCacheItem('bla', []) item_1 = _NodeCacheItem('bla', [])
item_2 = _NodeCacheItem('bla', []) item_2 = _NodeCacheItem('bla', [])
path_1 = 'fake path 1' path_1 = Path('fake path 1')
path_2 = 'fake path 2' path_2 = Path('fake path 2')
hashed_grammar = load_grammar()._hashed hashed_grammar = load_grammar()._hashed
_save_to_file_system(hashed_grammar, path_1, item_1, cache_path=dir_1) _save_to_file_system(hashed_grammar, path_1, item_1, cache_path=dir_1)
@@ -81,12 +79,12 @@ def test_modulepickling_simulate_deleted_cache(tmpdir):
way. way.
__ https://developer.apple.com/library/content/documentation/FileManagement/Conceptual/FileSystemProgrammingGuide/FileSystemOverview/FileSystemOverview.html __ https://developer.apple.com/library/content/documentation/FileManagement/Conceptual/FileSystemProgrammingGuide/FileSystemOverview/FileSystemOverview.html
""" """ # noqa
grammar = load_grammar() grammar = load_grammar()
module = 'fake parser' module = 'fake parser'
# Create the file # Create the file
path = tmpdir.dirname + '/some_path' path = Path(str(tmpdir.dirname), 'some_path')
with open(path, 'w'): with open(path, 'w'):
pass pass
io = file_io.FileIO(path) io = file_io.FileIO(path)
@@ -124,7 +122,7 @@ def test_cache_limit():
class _FixedTimeFileIO(file_io.KnownContentFileIO): class _FixedTimeFileIO(file_io.KnownContentFileIO):
def __init__(self, path, content, last_modified): def __init__(self, path, content, last_modified):
super(_FixedTimeFileIO, self).__init__(path, content) super().__init__(path, content)
self._last_modified = last_modified self._last_modified = last_modified
def get_last_modified(self): def get_last_modified(self):
@@ -134,7 +132,7 @@ class _FixedTimeFileIO(file_io.KnownContentFileIO):
@pytest.mark.parametrize('diff_cache', [False, True]) @pytest.mark.parametrize('diff_cache', [False, True])
@pytest.mark.parametrize('use_file_io', [False, True]) @pytest.mark.parametrize('use_file_io', [False, True])
def test_cache_last_used_update(diff_cache, use_file_io): def test_cache_last_used_update(diff_cache, use_file_io):
p = '/path/last-used' p = Path('/path/last-used')
parser_cache.clear() # Clear, because then it's easier to find stuff. parser_cache.clear() # Clear, because then it's easier to find stuff.
parse('somecode', cache=True, path=p) parse('somecode', cache=True, path=p)
node_cache_item = next(iter(parser_cache.values()))[p] node_cache_item = next(iter(parser_cache.values()))[p]
@@ -157,21 +155,21 @@ def test_inactive_cache(tmpdir, isolated_parso_cache):
test_subjects = "abcdef" test_subjects = "abcdef"
for path in test_subjects: for path in test_subjects:
parse('somecode', cache=True, path=os.path.join(str(tmpdir), path)) parse('somecode', cache=True, path=os.path.join(str(tmpdir), path))
raw_cache_path = os.path.join(isolated_parso_cache, _VERSION_TAG) raw_cache_path = isolated_parso_cache.joinpath(_VERSION_TAG)
assert os.path.exists(raw_cache_path) assert raw_cache_path.exists()
paths = os.listdir(raw_cache_path) dir_names = os.listdir(raw_cache_path)
a_while_ago = time.time() - _CACHED_FILE_MAXIMUM_SURVIVAL a_while_ago = time.time() - _CACHED_FILE_MAXIMUM_SURVIVAL
old_paths = set() old_paths = set()
for path in paths[:len(test_subjects) // 2]: # make certain number of paths old for dir_name in dir_names[:len(test_subjects) // 2]: # make certain number of paths old
os.utime(os.path.join(raw_cache_path, path), (a_while_ago, a_while_ago)) os.utime(raw_cache_path.joinpath(dir_name), (a_while_ago, a_while_ago))
old_paths.add(path) old_paths.add(dir_name)
# nothing should be cleared while the lock is on # nothing should be cleared while the lock is on
assert os.path.exists(_get_cache_clear_lock().path) assert _get_cache_clear_lock_path().exists()
_remove_cache_and_update_lock() # it shouldn't clear anything _remove_cache_and_update_lock() # it shouldn't clear anything
assert len(os.listdir(raw_cache_path)) == len(test_subjects) assert len(os.listdir(raw_cache_path)) == len(test_subjects)
assert old_paths.issubset(os.listdir(raw_cache_path)) assert old_paths.issubset(os.listdir(raw_cache_path))
os.utime(_get_cache_clear_lock().path, (a_while_ago, a_while_ago)) os.utime(_get_cache_clear_lock_path(), (a_while_ago, a_while_ago))
_remove_cache_and_update_lock() _remove_cache_and_update_lock()
assert len(os.listdir(raw_cache_path)) == len(test_subjects) // 2 assert len(os.listdir(raw_cache_path)) == len(test_subjects) // 2
assert not old_paths.intersection(os.listdir(raw_cache_path)) assert not old_paths.intersection(os.listdir(raw_cache_path))
@@ -180,12 +178,13 @@ def test_inactive_cache(tmpdir, isolated_parso_cache):
@skip_pypy @skip_pypy
def test_permission_error(monkeypatch): def test_permission_error(monkeypatch):
def save(*args, **kwargs): def save(*args, **kwargs):
was_called[0] = True # Python 2... Use nonlocal instead nonlocal was_called
was_called = True
raise PermissionError raise PermissionError
was_called = [False] was_called = False
monkeypatch.setattr(cache, '_save_to_file_system', save) monkeypatch.setattr(cache, '_save_to_file_system', save)
with pytest.warns(Warning): with pytest.warns(Warning):
parse(path=__file__, cache=True, diff_cache=True) parse(path=__file__, cache=True, diff_cache=True)
assert was_called[0] assert was_called

View File

@@ -1,7 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from textwrap import dedent from textwrap import dedent
import logging import logging
import sys
import pytest import pytest
@@ -39,7 +38,7 @@ def _check_error_leaves_nodes(node):
return None return None
class Differ(object): class Differ:
grammar = load_grammar() grammar = load_grammar()
def initialize(self, code): def initialize(self, code):
@@ -934,7 +933,6 @@ def test_many_nested_ifs(differ):
differ.parse(code1, parsers=1, copies=1) differ.parse(code1, parsers=1, copies=1)
@pytest.mark.skipif(sys.version_info < (3, 5), reason="Async starts working in 3.5")
@pytest.mark.parametrize('prefix', ['', 'async ']) @pytest.mark.parametrize('prefix', ['', 'async '])
def test_with_and_funcdef_in_call(differ, prefix): def test_with_and_funcdef_in_call(differ, prefix):
code1 = prefix + dedent('''\ code1 = prefix + dedent('''\
@@ -973,17 +971,16 @@ def test_random_unicode_characters(differ):
Those issues were all found with the fuzzer. Those issues were all found with the fuzzer.
""" """
differ.initialize('') differ.initialize('')
differ.parse(u'\x1dĔBϞɛˁşʑ˳˻ȣſéÎ\x90̕ȟòwʘ\x1dĔBϞɛˁşʑ˳˻ȣſéÎ', parsers=1, differ.parse('\x1dĔBϞɛˁşʑ˳˻ȣſéÎ\x90̕ȟòwʘ\x1dĔBϞɛˁşʑ˳˻ȣſéÎ', parsers=1,
expect_error_leaves=True) expect_error_leaves=True)
differ.parse(u'\r\r', parsers=1) differ.parse('\r\r', parsers=1)
differ.parse(u"˟Ę\x05À\r rúƣ@\x8a\x15r()\n", parsers=1, expect_error_leaves=True) differ.parse("˟Ę\x05À\r rúƣ@\x8a\x15r()\n", parsers=1, expect_error_leaves=True)
differ.parse(u'a\ntaǁ\rGĒōns__\n\nb', parsers=1, differ.parse('a\ntaǁ\rGĒōns__\n\nb', parsers=1)
expect_error_leaves=sys.version_info[0] == 2)
s = ' if not (self, "_fi\x02\x0e\x08\n\nle"):' s = ' if not (self, "_fi\x02\x0e\x08\n\nle"):'
differ.parse(s, parsers=1, expect_error_leaves=True) differ.parse(s, parsers=1, expect_error_leaves=True)
differ.parse('') differ.parse('')
differ.parse(s + '\n', parsers=1, expect_error_leaves=True) differ.parse(s + '\n', parsers=1, expect_error_leaves=True)
differ.parse(u' result = (\r\f\x17\t\x11res)', parsers=1, expect_error_leaves=True) differ.parse(' result = (\r\f\x17\t\x11res)', parsers=1, expect_error_leaves=True)
differ.parse('') differ.parse('')
differ.parse(' a( # xx\ndef', parsers=1, expect_error_leaves=True) differ.parse(' a( # xx\ndef', parsers=1, expect_error_leaves=True)
@@ -996,7 +993,7 @@ def test_dedent_end_positions(differ):
c = { c = {
5} 5}
''') ''')
code2 = dedent(u'''\ code2 = dedent('''\
if 1: if 1:
if ⌟ഒᜈྡྷṭb: if ⌟ഒᜈྡྷṭb:
2 2
@@ -1269,7 +1266,6 @@ def test_some_weird_removals(differ):
differ.parse(code1, copies=1) differ.parse(code1, copies=1)
@pytest.mark.skipif(sys.version_info < (3, 5), reason="Async starts working in 3.5")
def test_async_copy(differ): def test_async_copy(differ):
code1 = dedent('''\ code1 = dedent('''\
async def main(): async def main():
@@ -1340,7 +1336,7 @@ def test_backslash_issue(differ):
pre = ( pre = (
'') '')
\\if \\if
''') ''') # noqa
differ.initialize(code1) differ.initialize(code1)
differ.parse(code2, parsers=1, copies=1, expect_error_leaves=True) differ.parse(code2, parsers=1, copies=1, expect_error_leaves=True)
differ.parse(code1, parsers=1, copies=1) differ.parse(code1, parsers=1, copies=1)
@@ -1420,7 +1416,7 @@ def test_with_formfeed(differ):
\x0cimport \x0cimport
return return
return '' return ''
''') ''') # noqa
differ.initialize(code1) differ.initialize(code1)
differ.parse(code2, parsers=ANY, copies=ANY, expect_error_leaves=True) differ.parse(code2, parsers=ANY, copies=ANY, expect_error_leaves=True)
@@ -1588,14 +1584,14 @@ def test_byte_order_mark(differ):
def test_byte_order_mark2(differ): def test_byte_order_mark2(differ):
code = u'\ufeff# foo' code = '\ufeff# foo'
differ.initialize(code) differ.initialize(code)
differ.parse(code + 'x', parsers=ANY) differ.parse(code + 'x', parsers=ANY)
def test_byte_order_mark3(differ): def test_byte_order_mark3(differ):
code1 = u"\ufeff#\ny\n" code1 = "\ufeff#\ny\n"
code2 = u'x\n\ufeff#\n\ufeff#\ny\n' code2 = 'x\n\ufeff#\n\ufeff#\ny\n'
differ.initialize(code1) differ.initialize(code1)
differ.parse(code2, expect_error_leaves=True, parsers=ANY, copies=ANY) differ.parse(code2, expect_error_leaves=True, parsers=ANY, copies=ANY)
differ.parse(code1, parsers=1) differ.parse(code1, parsers=1)

View File

@@ -74,7 +74,7 @@ def test_invalid_token():
def test_invalid_token_in_fstr(): def test_invalid_token_in_fstr():
module = load_grammar(version='3.6').parse('f"{a + ? + b}"') module = load_grammar(version='3.9').parse('f"{a + ? + b}"')
error_node, q, plus_b, error1, error2, endmarker = module.children error_node, q, plus_b, error1, error2, endmarker = module.children
assert error_node.get_code() == 'f"{a +' assert error_node.get_code() == 'f"{a +'
assert q.value == '?' assert q.value == '?'

View File

@@ -60,6 +60,24 @@ def grammar():
# a line continuation inside of an format spec # a line continuation inside of an format spec
'f"{123:.2\\\nf}"', 'f"{123:.2\\\nf}"',
# some unparenthesized syntactic structures
'f"{*x,}"',
'f"{*x, *y}"',
'f"{x, *y}"',
'f"{*x, y}"',
'f"{x for x in [1]}"',
# named unicode characters
'f"\\N{BULLET}"',
'f"\\N{FLEUR-DE-LIS}"',
'f"\\N{NO ENTRY}"',
'f"Combo {expr} and \\N{NO ENTRY}"',
'f"\\N{NO ENTRY} and {expr}"',
'f"\\N{no entry}"',
'f"\\N{SOYOMBO LETTER -A}"',
'f"\\N{DOMINO TILE HORIZONTAL-00-00}"',
'f"""\\N{NO ENTRY}"""',
] ]
) )
def test_valid(code, grammar): def test_valid(code, grammar):
@@ -79,6 +97,7 @@ def test_valid(code, grammar):
# invalid conversion characters # invalid conversion characters
'f"{1!{a}}"', 'f"{1!{a}}"',
'f"{1=!{a}}"',
'f"{!{a}}"', 'f"{!{a}}"',
# The curly braces must contain an expression # The curly braces must contain an expression
@@ -96,6 +115,11 @@ def test_valid(code, grammar):
# a newline without a line continuation inside a single-line string # a newline without a line continuation inside a single-line string
'f"abc\ndef"', 'f"abc\ndef"',
# various named unicode escapes that aren't name-shaped
'f"\\N{ BULLET }"',
'f"\\N{NO ENTRY}"',
'f"""\\N{NO\nENTRY}"""',
] ]
) )
def test_invalid(code, grammar): def test_invalid(code, grammar):
@@ -114,6 +138,8 @@ def test_invalid(code, grammar):
(1, 10), (1, 11), (1, 12), (1, 13)]), (1, 10), (1, 11), (1, 12), (1, 13)]),
('f"""\n {\nfoo\n }"""', [(1, 0), (1, 4), (2, 1), (3, 0), (4, 1), ('f"""\n {\nfoo\n }"""', [(1, 0), (1, 4), (2, 1), (3, 0), (4, 1),
(4, 2), (4, 5)]), (4, 2), (4, 5)]),
('f"\\N{NO ENTRY} and {expr}"', [(1, 0), (1, 2), (1, 19), (1, 20),
(1, 24), (1, 25), (1, 26)]),
] ]
) )
def test_tokenize_start_pos(code, positions): def test_tokenize_start_pos(code, positions):

View File

@@ -12,7 +12,7 @@ from parso.utils import python_bytes_to_unicode
@total_ordering @total_ordering
class WantedIssue(object): class WantedIssue:
def __init__(self, code, line, column): def __init__(self, code, line, column):
self.code = code self.code = code
self._line = line self._line = line
@@ -42,9 +42,9 @@ def collect_errors(code):
column = int(add_indent or len(match.group(1))) column = int(add_indent or len(match.group(1)))
code, _, add_line = code.partition('+') code, _, add_line = code.partition('+')
l = line_nr + 1 + int(add_line or 0) ln = line_nr + 1 + int(add_line or 0)
yield WantedIssue(code[1:], l, column) yield WantedIssue(code[1:], ln, column)
def test_normalizer_issue(normalizer_issue_case): def test_normalizer_issue(normalizer_issue_case):

View File

@@ -8,12 +8,11 @@ However the tests might still be relevant for the parser.
from textwrap import dedent from textwrap import dedent
from parso._compatibility import u
from parso import parse from parso import parse
def test_carriage_return_splitting(): def test_carriage_return_splitting():
source = u(dedent(''' source = dedent('''
@@ -21,7 +20,7 @@ def test_carriage_return_splitting():
class Foo(): class Foo():
pass pass
''')) ''')
source = source.replace('\n', '\r\n') source = source.replace('\n', '\r\n')
module = parse(source) module = parse(source)
assert [n.value for lst in module.get_used_names().values() for n in lst] == ['Foo'] assert [n.value for lst in module.get_used_names().values() for n in lst] == ['Foo']
@@ -136,7 +135,7 @@ def test_wrong_indentation():
b b
a a
""") """)
#check_p(src, 1) check_p(src, 1)
src = dedent("""\ src = dedent("""\
def complex(): def complex():

View File

@@ -8,13 +8,13 @@ from textwrap import dedent
from parso import parse from parso import parse
def assert_params(param_string, version=None, **wanted_dct): def assert_params(param_string, **wanted_dct):
source = dedent(''' source = dedent('''
def x(%s): def x(%s):
pass pass
''') % param_string ''') % param_string
module = parse(source, version=version) module = parse(source)
funcdef = next(module.iter_funcdefs()) funcdef = next(module.iter_funcdefs())
dct = dict((p.name.value, p.default and p.default.get_code()) dct = dict((p.name.value, p.default and p.default.get_code())
for p in funcdef.get_params()) for p in funcdef.get_params())
@@ -23,23 +23,23 @@ def assert_params(param_string, version=None, **wanted_dct):
def test_split_params_with_separation_star(): def test_split_params_with_separation_star():
assert_params(u'x, y=1, *, z=3', x=None, y='1', z='3', version='3.5') assert_params('x, y=1, *, z=3', x=None, y='1', z='3')
assert_params(u'*, x', x=None, version='3.5') assert_params('*, x', x=None)
assert_params(u'*', version='3.5') assert_params('*')
def test_split_params_with_stars(): def test_split_params_with_stars():
assert_params(u'x, *args', x=None, args=None) assert_params('x, *args', x=None, args=None)
assert_params(u'**kwargs', kwargs=None) assert_params('**kwargs', kwargs=None)
assert_params(u'*args, **kwargs', args=None, kwargs=None) assert_params('*args, **kwargs', args=None, kwargs=None)
def test_kw_only_no_kw(works_ge_py3): def test_kw_only_no_kw(works_in_py):
""" """
Parsing this should be working. In CPython the parser also parses this and Parsing this should be working. In CPython the parser also parses this and
in a later step the AST complains. in a later step the AST complains.
""" """
module = works_ge_py3.parse('def test(arg, *):\n pass') module = works_in_py.parse('def test(arg, *):\n pass')
if module is not None: if module is not None:
func = module.children[0] func = module.children[0]
open_, p1, asterisk, close = func._get_param_nodes() open_, p1, asterisk, close = func._get_param_nodes()

View File

@@ -3,7 +3,6 @@ from textwrap import dedent
import pytest import pytest
from parso._compatibility import u
from parso import parse from parso import parse
from parso.python import tree from parso.python import tree
from parso.utils import split_lines from parso.utils import split_lines
@@ -110,23 +109,15 @@ def test_param_splitting(each_version):
but Jedi does this to simplify argument parsing. but Jedi does this to simplify argument parsing.
""" """
def check(src, result): def check(src, result):
# Python 2 tuple params should be ignored for now.
m = parse(src, version=each_version) m = parse(src, version=each_version)
if each_version.startswith('2'): assert not list(m.iter_funcdefs())
# We don't want b and c to be a part of the param enumeration. Just
# ignore them, because it's not what we want to support in the
# future.
func = next(m.iter_funcdefs())
assert [param.name.value for param in func.get_params()] == result
else:
assert not list(m.iter_funcdefs())
check('def x(a, (b, c)):\n pass', ['a']) check('def x(a, (b, c)):\n pass', ['a'])
check('def x((b, c)):\n pass', []) check('def x((b, c)):\n pass', [])
def test_unicode_string(): def test_unicode_string():
s = tree.String(None, u(''), (0, 0)) s = tree.String(None, '', (0, 0))
assert repr(s) # Should not raise an Error! assert repr(s) # Should not raise an Error!
@@ -135,19 +126,10 @@ def test_backslash_dos_style(each_version):
def test_started_lambda_stmt(each_version): def test_started_lambda_stmt(each_version):
m = parse(u'lambda a, b: a i', version=each_version) m = parse('lambda a, b: a i', version=each_version)
assert m.children[0].type == 'error_node' assert m.children[0].type == 'error_node'
def test_python2_octal(each_version):
module = parse('0660', version=each_version)
first = module.children[0]
if each_version.startswith('2'):
assert first.type == 'number'
else:
assert first.type == 'error_node'
@pytest.mark.parametrize('code', ['foo "', 'foo """\n', 'foo """\nbar']) @pytest.mark.parametrize('code', ['foo "', 'foo """\n', 'foo """\nbar'])
def test_open_string_literal(each_version, code): def test_open_string_literal(each_version, code):
""" """
@@ -194,10 +176,12 @@ def test_no_error_nodes(each_version):
def test_named_expression(works_ge_py38): def test_named_expression(works_ge_py38):
works_ge_py38.parse("(a := 1, a + 1)") works_ge_py38.parse("(a := 1, a + 1)")
def test_extended_rhs_annassign(works_ge_py38): def test_extended_rhs_annassign(works_ge_py38):
works_ge_py38.parse("x: y = z,") works_ge_py38.parse("x: y = z,")
works_ge_py38.parse("x: Tuple[int, ...] = z, *q, w") works_ge_py38.parse("x: Tuple[int, ...] = z, *q, w")
@pytest.mark.parametrize( @pytest.mark.parametrize(
'param_code', [ 'param_code', [
'a=1, /', 'a=1, /',
@@ -212,6 +196,7 @@ def test_extended_rhs_annassign(works_ge_py38):
def test_positional_only_arguments(works_ge_py38, param_code): def test_positional_only_arguments(works_ge_py38, param_code):
works_ge_py38.parse("def x(%s): pass" % param_code) works_ge_py38.parse("def x(%s): pass" % param_code)
@pytest.mark.parametrize( @pytest.mark.parametrize(
'expression', [ 'expression', [
'a + a', 'a + a',

View File

@@ -8,7 +8,7 @@ from parso import parse
from parso.python import tree from parso.python import tree
class TestsFunctionAndLambdaParsing(object): class TestsFunctionAndLambdaParsing:
FIXTURES = [ FIXTURES = [
('def my_function(x, y, z) -> str:\n return x + y * z\n', { ('def my_function(x, y, z) -> str:\n return x + y * z\n', {
@@ -26,7 +26,7 @@ class TestsFunctionAndLambdaParsing(object):
@pytest.fixture(params=FIXTURES) @pytest.fixture(params=FIXTURES)
def node(self, request): def node(self, request):
parsed = parse(dedent(request.param[0]), version='3.5') parsed = parse(dedent(request.param[0]), version='3.10')
request.keywords['expected'] = request.param[1] request.keywords['expected'] = request.param[1]
child = parsed.children[0] child = parsed.children[0]
if child.type == 'simple_stmt': if child.type == 'simple_stmt':
@@ -79,16 +79,16 @@ def test_default_param(each_version):
assert not param.star_count assert not param.star_count
def test_annotation_param(each_py3_version): def test_annotation_param(each_version):
func = parse('def x(foo: 3): pass', version=each_py3_version).children[0] func = parse('def x(foo: 3): pass', version=each_version).children[0]
param, = func.get_params() param, = func.get_params()
assert param.default is None assert param.default is None
assert param.annotation.value == '3' assert param.annotation.value == '3'
assert not param.star_count assert not param.star_count
def test_annotation_params(each_py3_version): def test_annotation_params(each_version):
func = parse('def x(foo: 3, bar: 4): pass', version=each_py3_version).children[0] func = parse('def x(foo: 3, bar: 4): pass', version=each_version).children[0]
param1, param2 = func.get_params() param1, param2 = func.get_params()
assert param1.default is None assert param1.default is None
@@ -100,23 +100,14 @@ def test_annotation_params(each_py3_version):
assert not param2.star_count assert not param2.star_count
def test_default_and_annotation_param(each_py3_version): def test_default_and_annotation_param(each_version):
func = parse('def x(foo:3=42): pass', version=each_py3_version).children[0] func = parse('def x(foo:3=42): pass', version=each_version).children[0]
param, = func.get_params() param, = func.get_params()
assert param.default.value == '42' assert param.default.value == '42'
assert param.annotation.value == '3' assert param.annotation.value == '3'
assert not param.star_count assert not param.star_count
def test_ellipsis_py2(each_py2_version):
module = parse('[0][...]', version=each_py2_version, error_recovery=False)
expr = module.children[0]
trailer = expr.children[-1]
subscript = trailer.children[1]
assert subscript.type == 'subscript'
assert [leaf.value for leaf in subscript.children] == ['.', '.', '.']
def get_yield_exprs(code, version): def get_yield_exprs(code, version):
return list(parse(code, version=version).children[0].iter_yield_exprs()) return list(parse(code, version=version).children[0].iter_yield_exprs())
@@ -172,13 +163,13 @@ def top_function_three():
raise Exception raise Exception
""" """
r = get_raise_stmts(code, 0) # Lists in a simple Function r = get_raise_stmts(code, 0) # Lists in a simple Function
assert len(list(r)) == 1 assert len(list(r)) == 1
r = get_raise_stmts(code, 1) # Doesn't Exceptions list in closures r = get_raise_stmts(code, 1) # Doesn't Exceptions list in closures
assert len(list(r)) == 1 assert len(list(r)) == 1
r = get_raise_stmts(code, 2) # Lists inside try-catch r = get_raise_stmts(code, 2) # Lists inside try-catch
assert len(list(r)) == 2 assert len(list(r)) == 2
@@ -238,3 +229,13 @@ def test_iter_funcdefs():
module = parse(code, version='3.8') module = parse(code, version='3.8')
func_names = [f.name.value for f in module.iter_funcdefs()] func_names = [f.name.value for f in module.iter_funcdefs()]
assert func_names == ['normal', 'asyn', 'dec_normal', 'dec_async'] assert func_names == ['normal', 'asyn', 'dec_normal', 'dec_async']
def test_with_stmt_get_test_node_from_name():
code = "with A as X.Y, B as (Z), C as Q[0], D as Q['foo']: pass"
with_stmt = parse(code, version='3').children[0]
tests = [
with_stmt.get_test_node_from_name(name).value
for name in with_stmt.get_defined_names(include_setitem=True)
]
assert tests == ["A", "B", "C", "D"]

View File

@@ -33,6 +33,7 @@ def test_eof_blankline():
assert_issue('# foobar\n\n') assert_issue('# foobar\n\n')
assert_issue('\n\n') assert_issue('\n\n')
def test_shebang(): def test_shebang():
assert not issues('#!\n') assert not issues('#!\n')
assert not issues('#!/foo\n') assert not issues('#!/foo\n')

View File

@@ -1,11 +1,3 @@
"""Test suite for 2to3's parser and grammar files.
This is the place to add tests for changes to 2to3's grammar, such as those
merging the grammars for Python 2 and 3. In addition to specific tests for
parts of the grammar we've changed, we also make sure we can parse the
test_grammar.py files from both Python 2 and Python 3.
"""
from textwrap import dedent from textwrap import dedent
import pytest import pytest
@@ -30,35 +22,35 @@ def _invalid_syntax(code, version=None, **kwargs):
def test_formfeed(each_version): def test_formfeed(each_version):
s = u"foo\n\x0c\nfoo\n" s = "foo\n\x0c\nfoo\n"
t = _parse(s, each_version) t = _parse(s, each_version)
assert t.children[0].children[0].type == 'name' assert t.children[0].children[0].type == 'name'
assert t.children[1].children[0].type == 'name' assert t.children[1].children[0].type == 'name'
s = u"1\n\x0c\x0c\n2\n" s = "1\n\x0c\x0c\n2\n"
t = _parse(s, each_version) t = _parse(s, each_version)
with pytest.raises(ParserSyntaxError): with pytest.raises(ParserSyntaxError):
s = u"\n\x0c2\n" s = "\n\x0c2\n"
_parse(s, each_version) _parse(s, each_version)
def test_matrix_multiplication_operator(works_ge_py35): def test_matrix_multiplication_operator(works_in_py):
works_ge_py35.parse("a @ b") works_in_py.parse("a @ b")
works_ge_py35.parse("a @= b") works_in_py.parse("a @= b")
def test_yield_from(works_ge_py3, each_version): def test_yield_from(works_in_py, each_version):
works_ge_py3.parse("yield from x") works_in_py.parse("yield from x")
works_ge_py3.parse("(yield from x) + y") works_in_py.parse("(yield from x) + y")
_invalid_syntax("yield from", each_version) _invalid_syntax("yield from", each_version)
def test_await_expr(works_ge_py35): def test_await_expr(works_in_py):
works_ge_py35.parse("""async def foo(): works_in_py.parse("""async def foo():
await x await x
""") """)
works_ge_py35.parse("""async def foo(): works_in_py.parse("""async def foo():
def foo(): pass def foo(): pass
@@ -67,24 +59,27 @@ def test_await_expr(works_ge_py35):
await x await x
""") """)
works_ge_py35.parse("""async def foo(): return await a""") works_in_py.parse("""async def foo(): return await a""")
works_ge_py35.parse("""def foo(): works_in_py.parse("""def foo():
def foo(): pass def foo(): pass
async def foo(): await x async def foo(): await x
""") """)
@pytest.mark.skipif('sys.version_info[:2] < (3, 5)') @pytest.mark.parametrize(
@pytest.mark.xfail(reason="acting like python 3.7") 'code', [
def test_async_var(): "async = 1",
_parse("""async = 1""", "3.5") "await = 1",
_parse("""await = 1""", "3.5") "def async(): pass",
_parse("""def async(): pass""", "3.5") ]
)
def test_async_var(works_not_in_py, code):
works_not_in_py.parse(code)
def test_async_for(works_ge_py35): def test_async_for(works_in_py):
works_ge_py35.parse("async def foo():\n async for a in b: pass") works_in_py.parse("async def foo():\n async for a in b: pass")
@pytest.mark.parametrize("body", [ @pytest.mark.parametrize("body", [
@@ -114,77 +109,89 @@ def test_async_for(works_ge_py35):
1 async for a in b 1 async for a in b
]""", ]""",
]) ])
def test_async_for_comprehension_newline(works_ge_py36, body): def test_async_for_comprehension_newline(works_in_py, body):
# Issue #139 # Issue #139
works_ge_py36.parse("""async def foo(): works_in_py.parse("""async def foo():
{}""".format(body)) {}""".format(body))
def test_async_with(works_ge_py35): def test_async_with(works_in_py):
works_ge_py35.parse("async def foo():\n async with a: pass") works_in_py.parse("async def foo():\n async with a: pass")
@pytest.mark.skipif('sys.version_info[:2] < (3, 5)')
@pytest.mark.xfail(reason="acting like python 3.7") def test_async_with_invalid(works_in_py):
def test_async_with_invalid(): works_in_py.parse("""def foo():\n async with a: pass""")
_invalid_syntax("""def foo():
async with a: pass""", version="3.5")
def test_raise_3x_style_1(each_version): def test_raise_3x_style_1(each_version):
_parse("raise", each_version) _parse("raise", each_version)
def test_raise_2x_style_2(works_in_py2): def test_raise_2x_style_2(works_not_in_py):
works_in_py2.parse("raise E, V") works_not_in_py.parse("raise E, V")
def test_raise_2x_style_3(works_not_in_py):
works_not_in_py.parse("raise E, V, T")
def test_raise_2x_style_3(works_in_py2):
works_in_py2.parse("raise E, V, T")
def test_raise_2x_style_invalid_1(each_version): def test_raise_2x_style_invalid_1(each_version):
_invalid_syntax("raise E, V, T, Z", version=each_version) _invalid_syntax("raise E, V, T, Z", version=each_version)
def test_raise_3x_style(works_ge_py3):
works_ge_py3.parse("raise E1 from E2") def test_raise_3x_style(works_in_py):
works_in_py.parse("raise E1 from E2")
def test_raise_3x_style_invalid_1(each_version): def test_raise_3x_style_invalid_1(each_version):
_invalid_syntax("raise E, V from E1", each_version) _invalid_syntax("raise E, V from E1", each_version)
def test_raise_3x_style_invalid_2(each_version): def test_raise_3x_style_invalid_2(each_version):
_invalid_syntax("raise E from E1, E2", each_version) _invalid_syntax("raise E from E1, E2", each_version)
def test_raise_3x_style_invalid_3(each_version): def test_raise_3x_style_invalid_3(each_version):
_invalid_syntax("raise from E1, E2", each_version) _invalid_syntax("raise from E1, E2", each_version)
def test_raise_3x_style_invalid_4(each_version): def test_raise_3x_style_invalid_4(each_version):
_invalid_syntax("raise E from", each_version) _invalid_syntax("raise E from", each_version)
# Adapted from Python 3's Lib/test/test_grammar.py:GrammarTests.testFuncdef # Adapted from Python 3's Lib/test/test_grammar.py:GrammarTests.testFuncdef
def test_annotation_1(works_ge_py3): def test_annotation_1(works_in_py):
works_ge_py3.parse("""def f(x) -> list: pass""") works_in_py.parse("""def f(x) -> list: pass""")
def test_annotation_2(works_ge_py3):
works_ge_py3.parse("""def f(x:int): pass""")
def test_annotation_3(works_ge_py3): def test_annotation_2(works_in_py):
works_ge_py3.parse("""def f(*x:str): pass""") works_in_py.parse("""def f(x:int): pass""")
def test_annotation_4(works_ge_py3):
works_ge_py3.parse("""def f(**x:float): pass""")
def test_annotation_5(works_ge_py3): def test_annotation_3(works_in_py):
works_ge_py3.parse("""def f(x, y:1+2): pass""") works_in_py.parse("""def f(*x:str): pass""")
def test_annotation_6(each_py3_version):
_invalid_syntax("""def f(a, (b:1, c:2, d)): pass""", each_py3_version)
def test_annotation_7(each_py3_version): def test_annotation_4(works_in_py):
_invalid_syntax("""def f(a, (b:1, c:2, d), e:3=4, f=5, *g:6): pass""", each_py3_version) works_in_py.parse("""def f(**x:float): pass""")
def test_annotation_8(each_py3_version):
def test_annotation_5(works_in_py):
works_in_py.parse("""def f(x, y:1+2): pass""")
def test_annotation_6(each_version):
_invalid_syntax("""def f(a, (b:1, c:2, d)): pass""", each_version)
def test_annotation_7(each_version):
_invalid_syntax("""def f(a, (b:1, c:2, d), e:3=4, f=5, *g:6): pass""", each_version)
def test_annotation_8(each_version):
s = """def f(a, (b:1, c:2, d), e:3=4, f=5, s = """def f(a, (b:1, c:2, d), e:3=4, f=5,
*g:6, h:7, i=8, j:9=10, **k:11) -> 12: pass""" *g:6, h:7, i=8, j:9=10, **k:11) -> 12: pass"""
_invalid_syntax(s, each_py3_version) _invalid_syntax(s, each_version)
def test_except_new(each_version): def test_except_new(each_version):
@@ -195,27 +202,31 @@ def test_except_new(each_version):
y""") y""")
_parse(s, each_version) _parse(s, each_version)
def test_except_old(works_in_py2):
def test_except_old(works_not_in_py):
s = dedent(""" s = dedent("""
try: try:
x x
except E, N: except E, N:
y""") y""")
works_in_py2.parse(s) works_not_in_py.parse(s)
# Adapted from Python 3's Lib/test/test_grammar.py:GrammarTests.testAtoms # Adapted from Python 3's Lib/test/test_grammar.py:GrammarTests.testAtoms
def test_set_literal_1(works_ge_py27): def test_set_literal_1(works_in_py):
works_ge_py27.parse("""x = {'one'}""") works_in_py.parse("""x = {'one'}""")
def test_set_literal_2(works_ge_py27):
works_ge_py27.parse("""x = {'one', 1,}""")
def test_set_literal_3(works_ge_py27): def test_set_literal_2(works_in_py):
works_ge_py27.parse("""x = {'one', 'two', 'three'}""") works_in_py.parse("""x = {'one', 1,}""")
def test_set_literal_4(works_ge_py27):
works_ge_py27.parse("""x = {2, 3, 4,}""") def test_set_literal_3(works_in_py):
works_in_py.parse("""x = {'one', 'two', 'three'}""")
def test_set_literal_4(works_in_py):
works_in_py.parse("""x = {2, 3, 4,}""")
def test_new_octal_notation(each_version): def test_new_octal_notation(each_version):
@@ -223,21 +234,21 @@ def test_new_octal_notation(each_version):
_invalid_syntax("""0o7324528887""", each_version) _invalid_syntax("""0o7324528887""", each_version)
def test_old_octal_notation(works_in_py2): def test_old_octal_notation(works_not_in_py):
works_in_py2.parse("07") works_not_in_py.parse("07")
def test_long_notation(works_in_py2): def test_long_notation(works_not_in_py):
works_in_py2.parse("0xFl") works_not_in_py.parse("0xFl")
works_in_py2.parse("0xFL") works_not_in_py.parse("0xFL")
works_in_py2.parse("0b1l") works_not_in_py.parse("0b1l")
works_in_py2.parse("0B1L") works_not_in_py.parse("0B1L")
works_in_py2.parse("0o7l") works_not_in_py.parse("0o7l")
works_in_py2.parse("0O7L") works_not_in_py.parse("0O7L")
works_in_py2.parse("0l") works_not_in_py.parse("0l")
works_in_py2.parse("0L") works_not_in_py.parse("0L")
works_in_py2.parse("10l") works_not_in_py.parse("10l")
works_in_py2.parse("10L") works_not_in_py.parse("10L")
def test_new_binary_notation(each_version): def test_new_binary_notation(each_version):
@@ -245,28 +256,24 @@ def test_new_binary_notation(each_version):
_invalid_syntax("""0b0101021""", each_version) _invalid_syntax("""0b0101021""", each_version)
def test_class_new_syntax(works_ge_py3): def test_class_new_syntax(works_in_py):
works_ge_py3.parse("class B(t=7): pass") works_in_py.parse("class B(t=7): pass")
works_ge_py3.parse("class B(t, *args): pass") works_in_py.parse("class B(t, *args): pass")
works_ge_py3.parse("class B(t, **kwargs): pass") works_in_py.parse("class B(t, **kwargs): pass")
works_ge_py3.parse("class B(t, *args, **kwargs): pass") works_in_py.parse("class B(t, *args, **kwargs): pass")
works_ge_py3.parse("class B(t, y=9, *args, **kwargs): pass") works_in_py.parse("class B(t, y=9, *args, **kwargs): pass")
def test_parser_idempotency_extended_unpacking(works_ge_py3): def test_parser_idempotency_extended_unpacking(works_in_py):
"""A cut-down version of pytree_idempotency.py.""" """A cut-down version of pytree_idempotency.py."""
works_ge_py3.parse("a, *b, c = x\n") works_in_py.parse("a, *b, c = x\n")
works_ge_py3.parse("[*a, b] = x\n") works_in_py.parse("[*a, b] = x\n")
works_ge_py3.parse("(z, *y, w) = m\n") works_in_py.parse("(z, *y, w) = m\n")
works_ge_py3.parse("for *z, m in d: pass\n") works_in_py.parse("for *z, m in d: pass\n")
def test_multiline_bytes_literals(each_version): def test_multiline_bytes_literals(each_version):
""" s = """
It's not possible to get the same result when using \xaa in Python 2/3,
because it's treated differently.
"""
s = u"""
md5test(b"\xaa" * 80, md5test(b"\xaa" * 80,
(b"Test Using Larger Than Block-Size Key " (b"Test Using Larger Than Block-Size Key "
b"and Larger Than One Block-Size Data"), b"and Larger Than One Block-Size Data"),
@@ -285,17 +292,17 @@ def test_multiline_bytes_tripquote_literals(each_version):
_parse(s, each_version) _parse(s, each_version)
def test_ellipsis(works_ge_py3, each_version): def test_ellipsis(works_in_py, each_version):
works_ge_py3.parse("...") works_in_py.parse("...")
_parse("[0][...]", version=each_version) _parse("[0][...]", version=each_version)
def test_dict_unpacking(works_ge_py35): def test_dict_unpacking(works_in_py):
works_ge_py35.parse("{**dict(a=3), foo:2}") works_in_py.parse("{**dict(a=3), foo:2}")
def test_multiline_str_literals(each_version): def test_multiline_str_literals(each_version):
s = u""" s = """
md5test("\xaa" * 80, md5test("\xaa" * 80,
("Test Using Larger Than Block-Size Key " ("Test Using Larger Than Block-Size Key "
"and Larger Than One Block-Size Data"), "and Larger Than One Block-Size Data"),
@@ -304,24 +311,24 @@ def test_multiline_str_literals(each_version):
_parse(s, each_version) _parse(s, each_version)
def test_py2_backticks(works_in_py2): def test_py2_backticks(works_not_in_py):
works_in_py2.parse("`1`") works_not_in_py.parse("`1`")
def test_py2_string_prefixes(works_in_py2): def test_py2_string_prefixes(works_not_in_py):
works_in_py2.parse("ur'1'") works_not_in_py.parse("ur'1'")
works_in_py2.parse("Ur'1'") works_not_in_py.parse("Ur'1'")
works_in_py2.parse("UR'1'") works_not_in_py.parse("UR'1'")
_invalid_syntax("ru'1'", works_in_py2.version) _invalid_syntax("ru'1'", works_not_in_py.version)
def py_br(each_version): def py_br(each_version):
_parse('br""', each_version) _parse('br""', each_version)
def test_py3_rb(works_ge_py3): def test_py3_rb(works_in_py):
works_ge_py3.parse("rb'1'") works_in_py.parse("rb'1'")
works_ge_py3.parse("RB'1'") works_in_py.parse("RB'1'")
def test_left_recursion(): def test_left_recursion():
@@ -332,7 +339,7 @@ def test_left_recursion():
@pytest.mark.parametrize( @pytest.mark.parametrize(
'grammar, error_match', [ 'grammar, error_match', [
['foo: bar | baz\nbar: NAME\nbaz: NAME\n', ['foo: bar | baz\nbar: NAME\nbaz: NAME\n',
r"foo is ambiguous.*given a TokenType\(NAME\).*bar or baz"], r"foo is ambiguous.*given a PythonTokenTypes\.NAME.*bar or baz"],
['''foo: bar | baz\nbar: 'x'\nbaz: "x"\n''', ['''foo: bar | baz\nbar: 'x'\nbaz: "x"\n''',
r"foo is ambiguous.*given a ReservedString\(x\).*bar or baz"], r"foo is ambiguous.*given a ReservedString\(x\).*bar or baz"],
['''foo: bar | 'x'\nbar: 'x'\n''', ['''foo: bar | 'x'\nbar: 'x'\n''',

View File

@@ -1,9 +1,4 @@
try: from itertools import zip_longest
from itertools import zip_longest
except ImportError:
# Python 2
from itertools import izip_longest as zip_longest
from codecs import BOM_UTF8 from codecs import BOM_UTF8
import pytest import pytest
@@ -44,7 +39,7 @@ def test_simple_prefix_splitting(string, tokens):
else: else:
end_pos = start_pos[0], start_pos[1] + len(expected) + len(pt.spacing) end_pos = start_pos[0], start_pos[1] + len(expected) + len(pt.spacing)
#assert start_pos == pt.start_pos # assert start_pos == pt.start_pos
assert end_pos == pt.end_pos assert end_pos == pt.end_pos
start_pos = end_pos start_pos = end_pos

View File

@@ -48,10 +48,7 @@ def test_non_async_in_async():
This example doesn't work with FAILING_EXAMPLES, because the line numbers This example doesn't work with FAILING_EXAMPLES, because the line numbers
are not always the same / incorrect in Python 3.8. are not always the same / incorrect in Python 3.8.
""" """
if sys.version_info[:2] < (3, 5): # Raises multiple errors in previous versions.
pytest.skip()
# Raises multiple errors in previous versions.
code = 'async def foo():\n def nofoo():[x async for x in []]' code = 'async def foo():\n def nofoo():[x async for x in []]'
wanted, line_nr = _get_actual_exception(code) wanted, line_nr = _get_actual_exception(code)
@@ -120,16 +117,9 @@ def _get_actual_exception(code):
assert False, "The piece of code should raise an exception." assert False, "The piece of code should raise an exception."
# SyntaxError # SyntaxError
if wanted == 'SyntaxError: non-keyword arg after keyword arg': if wanted == 'SyntaxError: assignment to keyword':
# The python 3.5+ way, a bit nicer.
wanted = 'SyntaxError: positional argument follows keyword argument'
elif wanted == 'SyntaxError: assignment to keyword':
return [wanted, "SyntaxError: can't assign to keyword", return [wanted, "SyntaxError: can't assign to keyword",
'SyntaxError: cannot assign to __debug__'], line_nr 'SyntaxError: cannot assign to __debug__'], line_nr
elif wanted == 'SyntaxError: can use starred expression only as assignment target':
# Python 3.4/3.4 have a bit of a different warning than 3.5/3.6 in
# certain places. But in others this error makes sense.
return [wanted, "SyntaxError: can't use starred expression here"], line_nr
elif wanted == 'SyntaxError: f-string: unterminated string': elif wanted == 'SyntaxError: f-string: unterminated string':
wanted = 'SyntaxError: EOL while scanning string literal' wanted = 'SyntaxError: EOL while scanning string literal'
elif wanted == 'SyntaxError: f-string expression part cannot include a backslash': elif wanted == 'SyntaxError: f-string expression part cannot include a backslash':
@@ -259,10 +249,7 @@ def test_escape_decode_literals(each_version):
# Finally bytes. # Finally bytes.
error, = _get_error_list(r'b"\x"', version=each_version) error, = _get_error_list(r'b"\x"', version=each_version)
wanted = r'SyntaxError: (value error) invalid \x escape' wanted = r'SyntaxError: (value error) invalid \x escape at position 0'
if sys.version_info >= (3, 0):
# The positioning information is only available in Python 3.
wanted += ' at position 0'
assert error.message == wanted assert error.message == wanted
@@ -273,10 +260,12 @@ def test_too_many_levels_of_indentation():
assert not _get_error_list(build_nested('pass', 49, base=base)) assert not _get_error_list(build_nested('pass', 49, base=base))
assert _get_error_list(build_nested('pass', 50, base=base)) assert _get_error_list(build_nested('pass', 50, base=base))
def test_paren_kwarg(): def test_paren_kwarg():
assert _get_error_list("print((sep)=seperator)", version="3.8") assert _get_error_list("print((sep)=seperator)", version="3.8")
assert not _get_error_list("print((sep)=seperator)", version="3.7") assert not _get_error_list("print((sep)=seperator)", version="3.7")
@pytest.mark.parametrize( @pytest.mark.parametrize(
'code', [ 'code', [
"f'{*args,}'", "f'{*args,}'",
@@ -285,6 +274,8 @@ def test_paren_kwarg():
r'fr"\""', r'fr"\""',
r'fr"\\\""', r'fr"\\\""',
r"print(f'Some {x:.2f} and some {y}')", r"print(f'Some {x:.2f} and some {y}')",
# Unparenthesized yield expression
'def foo(): return f"{yield 1}"',
] ]
) )
def test_valid_fstrings(code): def test_valid_fstrings(code):
@@ -298,12 +289,37 @@ def test_valid_fstrings(code):
'[total := total + v for v in range(10)]', '[total := total + v for v in range(10)]',
'while chunk := file.read(2):\n pass', 'while chunk := file.read(2):\n pass',
'numbers = [y := math.factorial(x), y**2, y**3]', 'numbers = [y := math.factorial(x), y**2, y**3]',
'{(a:="a"): (b:=1)}',
'{(y:=1): 2 for x in range(5)}',
'a[(b:=0)]',
'a[(b:=0, c:=0)]',
'a[(b:=0):1:2]',
] ]
) )
def test_valid_namedexpr(code): def test_valid_namedexpr(code):
assert not _get_error_list(code, version='3.8') assert not _get_error_list(code, version='3.8')
@pytest.mark.parametrize(
'code', [
'{x := 1, 2, 3}',
'{x4 := x ** 5 for x in range(7)}',
]
)
def test_valid_namedexpr_set(code):
assert not _get_error_list(code, version='3.9')
@pytest.mark.parametrize(
'code', [
'a[b:=0]',
'a[b:=0, c:=0]',
]
)
def test_valid_namedexpr_index(code):
assert not _get_error_list(code, version='3.10')
@pytest.mark.parametrize( @pytest.mark.parametrize(
('code', 'message'), [ ('code', 'message'), [
("f'{1+}'", ('invalid syntax')), ("f'{1+}'", ('invalid syntax')),
@@ -330,6 +346,7 @@ def test_trailing_comma(code):
errors = _get_error_list(code) errors = _get_error_list(code)
assert not errors assert not errors
def test_continue_in_finally(): def test_continue_in_finally():
code = dedent('''\ code = dedent('''\
for a in [1]: for a in [1]:
@@ -341,7 +358,7 @@ def test_continue_in_finally():
assert not _get_error_list(code, version="3.8") assert not _get_error_list(code, version="3.8")
assert _get_error_list(code, version="3.7") assert _get_error_list(code, version="3.7")
@pytest.mark.parametrize( @pytest.mark.parametrize(
'template', [ 'template', [
"a, b, {target}, c = d", "a, b, {target}, c = d",
@@ -392,6 +409,7 @@ def test_repeated_kwarg():
def test_unparenthesized_genexp(source, no_errors): def test_unparenthesized_genexp(source, no_errors):
assert bool(_get_error_list(source)) ^ no_errors assert bool(_get_error_list(source)) ^ no_errors
@pytest.mark.parametrize( @pytest.mark.parametrize(
('source', 'no_errors'), [ ('source', 'no_errors'), [
('*x = 2', False), ('*x = 2', False),

View File

@@ -1,6 +1,5 @@
# -*- coding: utf-8 # This file contains Unicode characters. # -*- coding: utf-8 # This file contains Unicode characters.
import sys
from textwrap import dedent from textwrap import dedent
import pytest import pytest
@@ -31,7 +30,7 @@ FSTRING_END = PythonTokenTypes.FSTRING_END
def _get_token_list(string, version=None): def _get_token_list(string, version=None):
# Load the current version. # Load the current version.
version_info = parse_version_string(version) version_info = parse_version_string(version)
return list(tokenize.tokenize(string, version_info)) return list(tokenize.tokenize(string, version_info=version_info))
def test_end_pos_one_line(): def test_end_pos_one_line():
@@ -108,7 +107,7 @@ def test_tokenize_multiline_I():
fundef = '''""""\n''' fundef = '''""""\n'''
token_list = _get_token_list(fundef) token_list = _get_token_list(fundef)
assert token_list == [PythonToken(ERRORTOKEN, '""""\n', (1, 0), ''), assert token_list == [PythonToken(ERRORTOKEN, '""""\n', (1, 0), ''),
PythonToken(ENDMARKER , '', (2, 0), '')] PythonToken(ENDMARKER, '', (2, 0), '')]
def test_tokenize_multiline_II(): def test_tokenize_multiline_II():
@@ -117,7 +116,7 @@ def test_tokenize_multiline_II():
fundef = '''""""''' fundef = '''""""'''
token_list = _get_token_list(fundef) token_list = _get_token_list(fundef)
assert token_list == [PythonToken(ERRORTOKEN, '""""', (1, 0), ''), assert token_list == [PythonToken(ERRORTOKEN, '""""', (1, 0), ''),
PythonToken(ENDMARKER, '', (1, 4), '')] PythonToken(ENDMARKER, '', (1, 4), '')]
def test_tokenize_multiline_III(): def test_tokenize_multiline_III():
@@ -126,7 +125,7 @@ def test_tokenize_multiline_III():
fundef = '''""""\n\n''' fundef = '''""""\n\n'''
token_list = _get_token_list(fundef) token_list = _get_token_list(fundef)
assert token_list == [PythonToken(ERRORTOKEN, '""""\n\n', (1, 0), ''), assert token_list == [PythonToken(ERRORTOKEN, '""""\n\n', (1, 0), ''),
PythonToken(ENDMARKER, '', (3, 0), '')] PythonToken(ENDMARKER, '', (3, 0), '')]
def test_identifier_contains_unicode(): def test_identifier_contains_unicode():
@@ -136,12 +135,7 @@ def test_identifier_contains_unicode():
''') ''')
token_list = _get_token_list(fundef) token_list = _get_token_list(fundef)
unicode_token = token_list[1] unicode_token = token_list[1]
if sys.version_info.major >= 3: assert unicode_token[0] == NAME
assert unicode_token[0] == NAME
else:
# Unicode tokens in Python 2 seem to be identified as operators.
# They will be ignored in the parser, that's ok.
assert unicode_token[0] == ERRORTOKEN
def test_quoted_strings(): def test_quoted_strings():
@@ -184,19 +178,16 @@ def test_ur_literals():
assert typ == NAME assert typ == NAME
check('u""') check('u""')
check('ur""', is_literal=not sys.version_info.major >= 3) check('ur""', is_literal=False)
check('Ur""', is_literal=not sys.version_info.major >= 3) check('Ur""', is_literal=False)
check('UR""', is_literal=not sys.version_info.major >= 3) check('UR""', is_literal=False)
check('bR""') check('bR""')
# Starting with Python 3.3 this ordering is also possible. check('Rb""')
if sys.version_info.major >= 3:
check('Rb""')
# Starting with Python 3.6 format strings where introduced. check('fr""')
check('fr""', is_literal=sys.version_info >= (3, 6)) check('rF""')
check('rF""', is_literal=sys.version_info >= (3, 6)) check('f""')
check('f""', is_literal=sys.version_info >= (3, 6)) check('F""')
check('F""', is_literal=sys.version_info >= (3, 6))
def test_error_literal(): def test_error_literal():
@@ -229,9 +220,6 @@ def test_endmarker_end_pos():
check('a\\') check('a\\')
xfail_py2 = dict(marks=[pytest.mark.xfail(sys.version_info[0] == 2, reason='Python 2')])
@pytest.mark.parametrize( @pytest.mark.parametrize(
('code', 'types'), [ ('code', 'types'), [
# Indentation # Indentation
@@ -243,12 +231,10 @@ xfail_py2 = dict(marks=[pytest.mark.xfail(sys.version_info[0] == 2, reason='Pyth
# Name stuff # Name stuff
('1foo1', [NUMBER, NAME]), ('1foo1', [NUMBER, NAME]),
pytest.param( ('மெல்லினம்', [NAME]),
u'மெல்லினம்', [NAME], ('²', [ERRORTOKEN]),
**xfail_py2), ('ä²ö', [NAME, ERRORTOKEN, NAME]),
pytest.param(u'²', [ERRORTOKEN], **xfail_py2), ('ää²¹öö', [NAME, ERRORTOKEN, NAME]),
pytest.param(u'ä²ö', [NAME, ERRORTOKEN, NAME], **xfail_py2),
pytest.param(u'ää²¹öö', [NAME, ERRORTOKEN, NAME], **xfail_py2),
(' \x00a', [INDENT, ERRORTOKEN, NAME, DEDENT]), (' \x00a', [INDENT, ERRORTOKEN, NAME, DEDENT]),
(dedent('''\ (dedent('''\
class BaseCache: class BaseCache:
@@ -411,8 +397,8 @@ def test_backslash():
]), ]),
] ]
) )
def test_fstring_token_types(code, types, version_ge_py36): def test_fstring_token_types(code, types, each_version):
actual_types = [t.type for t in _get_token_list(code, version_ge_py36)] actual_types = [t.type for t in _get_token_list(code, each_version)]
assert types + [ENDMARKER] == actual_types assert types + [ENDMARKER] == actual_types

View File

@@ -83,6 +83,7 @@ def test_bytes_to_unicode_failing_encoding(code, errors):
else: else:
python_bytes_to_unicode(code, errors=errors) python_bytes_to_unicode(code, errors=errors)
@pytest.mark.parametrize( @pytest.mark.parametrize(
('version_str', 'version'), [ ('version_str', 'version'), [
('3', (3,)), ('3', (3,)),

15
tox.ini
View File

@@ -1,15 +0,0 @@
[tox]
envlist = {py27,py34,py35,py36,py37,py38}
[testenv]
extras = testing
deps =
py27,py34: pytest<3.3
coverage: coverage
setenv =
# https://github.com/tomchristie/django-rest-framework/issues/1957
# tox corrupts __pycache__, solution from here:
PYTHONDONTWRITEBYTECODE=1
coverage: TOX_TESTENV_COMMAND=coverage run -m pytest
commands =
{env:TOX_TESTENV_COMMAND:pytest} {posargs}
coverage: coverage report