Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pump Version python to 3.12, 3.13. Allow pip install for python 3.12, 3.13 #210

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
Open
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"]
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
steps:
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ corresponding $\LaTeX$ expression:

1. *Which Python versions are supported?*

Syntaxes on **Pythons 3.7 to 3.11** are officially supported, or will be supported.
Syntaxes on **Pythons 3.9 to 3.13** are officially supported, or will be supported.

2. *Which technique is used?*

Expand Down
16 changes: 8 additions & 8 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ build-backend = "hatchling.build"
name = "latexify-py"
description = "Generates LaTeX math description from Python functions."
readme = "README.md"
requires-python = ">=3.7, <3.12"
requires-python = ">=3.9, <3.14"
license = {text = "Apache Software License 2.0"}
authors = [
{name = "Yusuke Oda", email = "[email protected]"}
Expand All @@ -24,11 +24,11 @@ classifiers = [
"Framework :: IPython",
"Framework :: Jupyter",
"License :: OSI Approved :: Apache Software License",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Topic :: Scientific/Engineering :: Mathematics",
"Topic :: Software Development :: Code Generators",
"Topic :: Text Processing :: Markup :: LaTeX",
Expand All @@ -43,17 +43,17 @@ dynamic = [
[project.optional-dependencies]
dev = [
"build>=0.8",
"black>=22.10",
"flake8>=5.0",
"black>=24.3",
"flake8>=6.0",
"isort>=5.10",
"mypy>=0.991",
"mypy>=1.9",
"notebook>=6.5.1",
"pyproject-flake8>=5.0",
"pyproject-flake8>=6.0",
"pytest>=7.1",
"twine>=4.0",
]
mypy = [
"mypy>=0.991",
"mypy>=1.9",
"pytest>=7.1",
]

Expand Down
1 change: 0 additions & 1 deletion src/latexify/analyzers_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
from latexify import analyzers, ast_utils, exceptions, test_utils


@test_utils.require_at_least(8)
@pytest.mark.parametrize(
"code,start,stop,step,start_int,stop_int,step_int",
[
Expand Down
114 changes: 75 additions & 39 deletions src/latexify/ast_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,24 +56,12 @@ def make_constant(value: Any) -> ast.expr:
Raises:
ValueError: Unsupported value type.
"""
if sys.version_info.minor < 8:
if value is None or value is False or value is True:
return ast.NameConstant(value=value)
if value is ...:
return ast.Ellipsis()
if isinstance(value, (int, float, complex)):
return ast.Num(n=value)
if isinstance(value, str):
return ast.Str(s=value)
if isinstance(value, bytes):
return ast.Bytes(s=value)
else:
if (
value is None
or value is ...
or isinstance(value, (bool, int, float, complex, str, bytes))
):
return ast.Constant(value=value)
if (
value is None
or value is ...
or isinstance(value, (bool, int, float, complex, str, bytes))
):
return ast.Constant(value=value)

raise ValueError(f"Unsupported type to generate Constant: {type(value).__name__}")

Expand All @@ -87,13 +75,7 @@ def is_constant(node: ast.AST) -> bool:
Returns:
True if the node is a constant, False otherwise.
"""
if sys.version_info.minor < 8:
return isinstance(
node,
(ast.Bytes, ast.Constant, ast.Ellipsis, ast.NameConstant, ast.Num, ast.Str),
)
else:
return isinstance(node, ast.Constant)
return isinstance(node, ast.Constant)


def is_str(node: ast.AST) -> bool:
Expand All @@ -120,20 +102,12 @@ def extract_int_or_none(node: ast.expr) -> int | None:
Returns:
Extracted int value, or None if extraction failed.
"""
if sys.version_info.minor < 8:
if (
isinstance(node, ast.Num)
and isinstance(node.n, int)
and not isinstance(node.n, bool)
):
return node.n
else:
if (
isinstance(node, ast.Constant)
and isinstance(node.value, int)
and not isinstance(node.n, bool)
):
return node.value
if (
isinstance(node, ast.Constant)
and isinstance(node.value, int)
and not isinstance(node.value, bool)
):
return node.value

return None

Expand Down Expand Up @@ -173,3 +147,65 @@ def extract_function_name_or_none(node: ast.Call) -> str | None:
return node.func.attr

return None


def create_function_def(
name,
args,
body,
decorator_list,
returns=None,
type_comment=None,
type_params=None,
lineno=None,
col_offset=None,
end_lineno=None,
end_col_offset=None,
Comment on lines +153 to +163
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please add type hints.

) -> ast.FunctionDef:
"""Creates a FunctionDef node.

This function generates an `ast.FunctionDef` node, optionally removing
the `type_params` keyword argument for Python versions below 3.12.

Args:
name: Name of the function.
args: Arguments of the function.
body: Body of the function.
decorator_list: List of decorators.
returns: Return type of the function.
type_comment: Type comment of the function.
type_params: Type parameters of the function.
lineno: Line number of the function definition.
col_offset: Column offset of the function definition.
end_lineno: End line number of the function definition.
end_col_offset: End column offset of the function definition.

Returns:
ast.FunctionDef: The generated FunctionDef node.
"""
if sys.version_info.minor < 12:
return ast.FunctionDef(
name=name,
args=args,
body=body,
decorator_list=decorator_list,
returns=returns,
type_comment=type_comment,
lineno=lineno,
col_offset=col_offset,
end_lineno=end_lineno,
end_col_offset=end_col_offset,
) # type: ignore
return ast.FunctionDef(
name=name,
args=args,
body=body,
decorator_list=decorator_list,
returns=returns,
type_comment=type_comment,
type_params=type_params,
lineno=lineno,
col_offset=col_offset,
end_lineno=end_lineno,
end_col_offset=end_col_offset,
) # type: ignore
100 changes: 58 additions & 42 deletions src/latexify/ast_utils_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from __future__ import annotations

import ast
import sys
from typing import Any

import pytest
Expand Down Expand Up @@ -34,29 +35,6 @@ def test_make_attribute() -> None:
)


@test_utils.require_at_most(7)
maycuatroi marked this conversation as resolved.
Show resolved Hide resolved
@pytest.mark.parametrize(
"value,expected",
[
(None, ast.NameConstant(value=None)),
(False, ast.NameConstant(value=False)),
(True, ast.NameConstant(value=True)),
(..., ast.Ellipsis()),
(123, ast.Num(n=123)),
(4.5, ast.Num(n=4.5)),
(6 + 7j, ast.Num(n=6 + 7j)),
("foo", ast.Str(s="foo")),
(b"bar", ast.Bytes(s=b"bar")),
],
)
def test_make_constant_legacy(value: Any, expected: ast.Constant) -> None:
test_utils.assert_ast_equal(
observed=ast_utils.make_constant(value),
expected=expected,
)


@test_utils.require_at_least(8)
@pytest.mark.parametrize(
"value,expected",
[
Expand All @@ -83,25 +61,23 @@ def test_make_constant_invalid() -> None:
ast_utils.make_constant(object())


@test_utils.require_at_most(7)
@pytest.mark.parametrize(
"value,expected",
[
(ast.Bytes(s=b"foo"), True),
(ast.Constant("bar"), True),
(ast.Ellipsis(), True),
(ast.NameConstant(value=None), True),
(ast.Num(n=123), True),
(ast.Str(s="baz"), True),
(ast.Expr(value=ast.Num(456)), False),
(ast.Constant(value=b"foo"), True),
(ast.Constant(value="bar"), True),
(ast.Constant(value=...), True),
(ast.Constant(value=None), True),
(ast.Constant(value=123), True),
(ast.Constant(value="baz"), True),
(ast.Expr(value=ast.Constant(value=456)), False),
(ast.Global(names=["qux"]), False),
],
)
def test_is_constant_legacy(value: ast.AST, expected: bool) -> None:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks there exists some test cases from that only @test_utils.require_at_most(7) was removed like this. Could you check the changes entirely and remove them?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The decorator (@test_utils.require_at_most(7)) was previously used to indicate that the test case would run on Python versions >= 3.7. So, even when we were on Python 3.9, the test case was still executed. Now that we've upgraded to at least Python 3.9, the test case should still run as intended. I believe it’s best to keep the test case as it is.

Copy link
Collaborator

@odashi odashi Oct 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

require_at_most(k) requires versions equal or less than 3.k. the decorated function does nothing (skip everything) otherwise.

if sys.version_info.minor > minor:
return

assert ast_utils.is_constant(value) is expected


@test_utils.require_at_least(8)
@pytest.mark.parametrize(
"value,expected",
[
Expand All @@ -114,25 +90,23 @@ def test_is_constant(value: ast.AST, expected: bool) -> None:
assert ast_utils.is_constant(value) is expected


@test_utils.require_at_most(7)
@pytest.mark.parametrize(
"value,expected",
[
(ast.Bytes(s=b"foo"), False),
(ast.Constant("bar"), True),
(ast.Ellipsis(), False),
(ast.NameConstant(value=None), False),
(ast.Num(n=123), False),
(ast.Str(s="baz"), True),
(ast.Expr(value=ast.Num(456)), False),
(ast.Constant(value=b"foo"), False),
(ast.Constant(value="bar"), True),
(ast.Constant(value=...), False),
(ast.Constant(value=None), False),
(ast.Constant(value=123), False),
(ast.Constant(value="baz"), True),
(ast.Expr(value=ast.Constant(value=456)), False),
(ast.Global(names=["qux"]), False),
],
)
def test_is_str_legacy(value: ast.AST, expected: bool) -> None:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this test case as well

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same with decorator @test_utils.require_at_most(7)

assert ast_utils.is_str(value) is expected


@test_utils.require_at_least(8)
@pytest.mark.parametrize(
"value,expected",
[
Expand Down Expand Up @@ -194,6 +168,7 @@ def test_extract_int_invalid() -> None:
ast.Call(
func=ast.Name(id="hypot", ctx=ast.Load()),
args=[],
keywords=[],
),
"hypot",
),
Expand All @@ -205,17 +180,58 @@ def test_extract_int_invalid() -> None:
ctx=ast.Load(),
),
args=[],
keywords=[],
),
"hypot",
),
(
ast.Call(
func=ast.Call(func=ast.Name(id="foo", ctx=ast.Load()), args=[]),
func=ast.Call(
func=ast.Name(id="foo", ctx=ast.Load()), args=[], keywords=[]
),
args=[],
keywords=[],
),
None,
),
],
)
def test_extract_function_name_or_none(value: ast.Call, expected: str | None) -> None:
assert ast_utils.extract_function_name_or_none(value) == expected


def test_create_function_def() -> None:
expected_args = ast.arguments(
posonlyargs=[],
args=[ast.arg(arg="x")],
vararg=None,
kwonlyargs=[],
kw_defaults=[],
kwarg=None,
defaults=[],
)

kwargs = {
"name": "test_func",
"args": expected_args,
"body": [ast.Return(value=ast.Name(id="x", ctx=ast.Load()))],
"decorator_list": [],
"returns": None,
"type_comment": None,
"lineno": 1,
"col_offset": 0,
"end_lineno": 2,
"end_col_offset": 0,
}
if sys.version_info.minor >= 12:
kwargs["type_params"] = []

func_def = ast_utils.create_function_def(**kwargs)
assert isinstance(func_def, ast.FunctionDef)
assert func_def.name == "test_func"

assert func_def.args.posonlyargs == expected_args.posonlyargs
assert func_def.args.args == expected_args.args
assert func_def.args.kwonlyargs == expected_args.kwonlyargs
assert func_def.args.kw_defaults == expected_args.kw_defaults
assert func_def.args.defaults == expected_args.defaults
Loading
Loading