Skip to content

Commit

Permalink
Add and Update tests for specifying target python versions
Browse files Browse the repository at this point in the history
  • Loading branch information
dflook committed Sep 13, 2024
1 parent 58d04fd commit b944028
Show file tree
Hide file tree
Showing 7 changed files with 354 additions and 31 deletions.
2 changes: 1 addition & 1 deletion src/python_minifier/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ def minify(
if remove_pass:
module = RemovePass()(module)

if target_python.minimum > (3, 0) and remove_object_base:
if target_python.minimum >= (3, 0) and remove_object_base:
module = RemoveObject()(module)

if remove_asserts:
Expand Down
2 changes: 2 additions & 0 deletions src/python_minifier/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ class UnstableMinification(RuntimeError):
class TargetPythonOptions(object):
def __init__(self, minimum: Optional[Tuple[int, int]], maximum: Optional[Tuple[int, int]]): ...

def apply_constraint(self, minimum: Tuple[int, int], maximum: Tuple[int, int]) -> None: ...

def minify(
source: AnyStr,
filename: Optional[str] = ...,
Expand Down
21 changes: 13 additions & 8 deletions src/python_minifier/compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ def __init__(self):
self._max_version = sys.version_info[1], sys.version_info[2]

self.f_string_nesting = 0
self.pep701_required = False

def set_minimum(self, major, minor):
if (major, minor) > self._min_version:
Expand All @@ -42,9 +41,6 @@ def __call__(self, module):
try:
self.visit(module)

if self.pep701_required:
self.set_minimum(3, 12)

return self._min_version, self._max_version
except self.Version as v:
return v.version, v.version
Expand All @@ -54,21 +50,30 @@ def visit_JoinedStr(self, node):
self.set_minimum(3, 6)
self.f_string_nesting += 1
if self.f_string_nesting > 4:
self.pep701_required = True
raise self.Version((3, 12))
self.generic_visit(node)
self.f_string_nesting -= 1

def visit_FormattedValue(self, node):
# Do not visit the format_spec
self.generic_visit(node.value)
for field, value in ast.iter_fields(node):
if field == 'format_spec':
continue

if isinstance(value, list):
for item in value:
if isinstance(item, ast.AST):
self.visit(item)
elif isinstance(value, ast.AST):
self.visit(value)

def visit_Str(self, node):
if self.f_string_nesting + 1 > 4:
self.pep701_required = True
raise self.Version((3, 12))

def visit_Bytes(self, node):
if self.f_string_nesting + 1 > 4:
self.pep701_required = True
raise self.Version((3, 12))

# endregion

Expand Down
260 changes: 260 additions & 0 deletions test/test_compat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,260 @@
import ast
import sys

import pytest

from python_minifier.compat import find_syntax_versions

def test_no_special_syntax():
source = '''
a = 'Hello'
'''

min_version, max_version = find_syntax_versions(ast.parse(source))
assert min_version, max_version == ((2, 7), sys.version_info[:2])

def test_named_expr():
if sys.version_info < (3, 8):
pytest.skip('Python < 3.8 does not have named expressions')

source = '''
if a := 1:
pass
'''

min_version, max_version = find_syntax_versions(ast.parse(source))
assert min_version, max_version == ((3, 8), sys.version_info[:2])

def test_matmult():
if sys.version_info < (3, 5):
pytest.skip('Python < 3.5 does not have matmult')

source = '''
a @ b
'''

min_version, max_version = find_syntax_versions(ast.parse(source))
assert min_version, max_version == ((3, 5), sys.version_info[:2])

def test_annassign():
if sys.version_info < (3, 6):
pytest.skip('Python < 3.6 does not have annotated assignments')

source = '''
a: int = 1
'''

min_version, max_version = find_syntax_versions(ast.parse(source))
assert min_version, max_version == ((3, 6), sys.version_info[:2])

def test_kwonlyargs():
if sys.version_info < (3, 0):
pytest.skip('Python 2 does not have kwonlyargs')

source = '''
def f(a, b, *, c):
pass
'''

min_version, max_version = find_syntax_versions(ast.parse(source))
assert min_version, max_version == ((3, 0), sys.version_info[:2])

def test_posonlyargs():
if sys.version_info < (3, 8):
pytest.skip('Python < 3.8 does not have posonlyargs')

source = '''
def f(a, b, /, c):
pass
'''

min_version, max_version = find_syntax_versions(ast.parse(source))
assert min_version, max_version == ((3, 8), sys.version_info[:2])

def test_vararg_annotation():
if sys.version_info < (3, 0):
pytest.skip('Python < 3.0 does not have annotations')

source = '''
def f(*args: int):
pass
'''

min_version, max_version = find_syntax_versions(ast.parse(source))
assert min_version, max_version == ((3, 0), sys.version_info[:2])

def test_kwarg_annotation():
if sys.version_info < (3, 0):
pytest.skip('Python < 3.0 does not have annotations')

source = '''
def f(**kwargs: int):
pass
'''

min_version, max_version = find_syntax_versions(ast.parse(source))
assert min_version, max_version == ((3, 0), sys.version_info[:2])

def test_arg_annotation():
if sys.version_info < (3, 0):
pytest.skip('Python < 3.0 does not have annotations')

source = '''
def f(a: int):
pass
'''

min_version, max_version = find_syntax_versions(ast.parse(source))
assert min_version, max_version == ((3, 0), sys.version_info[:2])

def test_nonlocal():
if sys.version_info < (3, 0):
pytest.skip('Python < 3.0 does not have nonlocal')

source = '''
def f():
nonlocal a
'''

min_version, max_version = find_syntax_versions(ast.parse(source))
assert min_version, max_version == ((3, 0), sys.version_info[:2])

def test_async_function():
if sys.version_info < (3, 5):
pytest.skip('Python < 3.5 does not have async functions')

source = '''
async def f():
pass
'''

min_version, max_version = find_syntax_versions(ast.parse(source))
assert min_version, max_version == ((3, 5), sys.version_info[:2])

def test_async_with():
if sys.version_info < (3, 5):
pytest.skip('Python < 3.5 does not have async with')

source = '''
async with a:
pass
'''

min_version, max_version = find_syntax_versions(ast.parse(source))
assert min_version, max_version == ((3, 5), sys.version_info[:2])

def test_async_for():
if sys.version_info < (3, 5):
pytest.skip('Python < 3.5 does not have async for')

source = '''
async for a in b:
pass
'''

min_version, max_version = find_syntax_versions(ast.parse(source))
assert min_version, max_version == ((3, 5), sys.version_info[:2])

def test_async_comprehension():
if sys.version_info < (3, 6):
pytest.skip('Python < 3.6 does not have async comprehensions')

source = '''
[a async for a in b]
'''

min_version, max_version = find_syntax_versions(ast.parse(source))
assert min_version, max_version == ((3, 6), sys.version_info[:2])

def test_await():
if sys.version_info < (3, 5):
pytest.skip('Python < 3.5 does not have await')

source = '''
await a
'''

min_version, max_version = find_syntax_versions(ast.parse(source))
assert min_version, max_version == ((3, 5), sys.version_info[:2])

def test_match():
if sys.version_info < (3, 10):
pytest.skip('Python < 3.10 does not have match')

source = '''
match a:
case b:
pass
'''

min_version, max_version = find_syntax_versions(ast.parse(source))
assert min_version, max_version == ((3, 10), sys.version_info[:2])

def test_repr():
if sys.version_info > (2, 7):
pytest.skip('Python 3 does not have backtick syntax')

source = '''
`1+2`
'''

min_version, max_version = find_syntax_versions(ast.parse(source))
assert min_version, max_version == ((2, 7), (2,7))

def test_try_star():
if sys.version_info < (3, 11):
pytest.skip('Python < 3.11 does not have try star')

source = '''
try:
pass
except* Error as e:
pass
'''

min_version, max_version = find_syntax_versions(ast.parse(source))
assert min_version, max_version == ((3, 11), sys.version_info[:2])

def test_function_type_var():
if sys.version_info < (3, 12):
pytest.skip('Python < 3.12 does not have type vars')

source = '''
def a[T](): pass
'''

min_version, max_version = find_syntax_versions(ast.parse(source))
assert min_version, max_version == ((3, 12), sys.version_info[:2])

def test_class_type_var():
if sys.version_info < (3, 12):
pytest.skip('Python < 3.12 does not have type vars')

source = '''
class a[T]: pass
'''

min_version, max_version = find_syntax_versions(ast.parse(source))
assert min_version, max_version == ((3, 12), sys.version_info[:2])

def test_typevar_tuple():
if sys.version_info < (3, 12):
pytest.skip('Python < 3.12 does not have type vars')

source = '''
class a[*T]: pass
'''

min_version, max_version = find_syntax_versions(ast.parse(source))
assert min_version, max_version == ((3, 12), sys.version_info[:2])

def test_paramspec():
if sys.version_info < (3, 12):
pytest.skip('Python < 3.12 does not have type vars')

source = '''
class a[**T]: pass
'''

min_version, max_version = find_syntax_versions(ast.parse(source))
assert min_version, max_version == ((3, 12), sys.version_info[:2])
Loading

0 comments on commit b944028

Please sign in to comment.