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

Python 3.12 support #10894

Merged
merged 6 commits into from
Jun 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
28 changes: 20 additions & 8 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ jobs:
"windows-py39",
"windows-py310",
"windows-py311",
"windows-py312",

"ubuntu-py37",
"ubuntu-py37-pluggy",
Expand All @@ -51,12 +52,13 @@ jobs:
"ubuntu-py39",
"ubuntu-py310",
"ubuntu-py311",
"ubuntu-py312",
"ubuntu-pypy3",

"macos-py37",
"macos-py38",
Copy link
Member

Choose a reason for hiding this comment

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

Probably good to leave a comment here explaining why we are skipping macos-py38.

Copy link
Member Author

Choose a reason for hiding this comment

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

I noticed macos-py311 was missing, so I added py312 and removed py38 to keep the job count the same. With the faster Python release cycle, it is starting to become a lot.

"macos-py39",
"macos-py310",
"macos-py312",

"docs",
"doctesting",
Expand Down Expand Up @@ -86,9 +88,13 @@ jobs:
os: windows-latest
tox_env: "py310-xdist"
- name: "windows-py311"
python: "3.11-dev"
python: "3.11"
os: windows-latest
tox_env: "py311"
- name: "windows-py312"
python: "3.12-dev"
os: windows-latest
tox_env: "py312"

- name: "ubuntu-py37"
python: "3.7"
Expand Down Expand Up @@ -116,10 +122,15 @@ jobs:
os: ubuntu-latest
tox_env: "py310-xdist"
- name: "ubuntu-py311"
python: "3.11-dev"
python: "3.11"
os: ubuntu-latest
tox_env: "py311"
use_coverage: true
- name: "ubuntu-py312"
python: "3.12-dev"
os: ubuntu-latest
tox_env: "py312"
use_coverage: true
- name: "ubuntu-pypy3"
python: "pypy-3.7"
os: ubuntu-latest
Expand All @@ -129,19 +140,19 @@ jobs:
python: "3.7"
os: macos-latest
tox_env: "py37-xdist"
- name: "macos-py38"
python: "3.8"
os: macos-latest
tox_env: "py38-xdist"
use_coverage: true
- name: "macos-py39"
python: "3.9"
os: macos-latest
tox_env: "py39-xdist"
use_coverage: true
- name: "macos-py310"
python: "3.10"
os: macos-latest
tox_env: "py310-xdist"
- name: "macos-py312"
python: "3.12-dev"
os: macos-latest
tox_env: "py312-xdist"

- name: "plugins"
python: "3.9"
Expand All @@ -168,6 +179,7 @@ jobs:
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python }}
check-latest: ${{ endsWith(matrix.python, '-dev') }}

- name: Install dependencies
run: |
Expand Down
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ repos:
rev: v2.3.0
hooks:
- id: setup-cfg-fmt
args: ["--max-py-version=3.11", "--include-version-classifiers"]
args: ["--max-py-version=3.12", "--include-version-classifiers"]
- repo: https://github.com/pre-commit/pygrep-hooks
rev: v1.10.0
hooks:
Expand Down
1 change: 1 addition & 0 deletions changelog/10894.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Support for Python 3.12 (beta at the time of writing).
2 changes: 2 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ classifiers =
Programming Language :: Python :: 3.9
Programming Language :: Python :: 3.10
Programming Language :: Python :: 3.11
Programming Language :: Python :: 3.12
Topic :: Software Development :: Libraries
Topic :: Software Development :: Testing
Topic :: Utilities
Expand Down Expand Up @@ -73,6 +74,7 @@ testing =
nose
pygments>=2.7.2
requests
setuptools
xmlschema

[options.package_data]
Expand Down
49 changes: 29 additions & 20 deletions src/_pytest/assertion/rewrite.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,14 @@

if sys.version_info >= (3, 8):
namedExpr = ast.NamedExpr
astNameConstant = ast.Constant
astStr = ast.Constant
astNum = ast.Constant

Check warning on line 51 in src/_pytest/assertion/rewrite.py

View check run for this annotation

Codecov / codecov/patch

src/_pytest/assertion/rewrite.py#L49-L51

Added lines #L49 - L51 were not covered by tests
else:
namedExpr = ast.Expr
astNameConstant = ast.NameConstant
astStr = ast.Str
astNum = ast.Num


assertstate_key = StashKey["AssertionState"]()
Expand Down Expand Up @@ -680,9 +686,12 @@
if (
expect_docstring
and isinstance(item, ast.Expr)
and isinstance(item.value, ast.Str)
and isinstance(item.value, astStr)
):
doc = item.value.s
if sys.version_info >= (3, 8):
doc = item.value.value

Check warning on line 692 in src/_pytest/assertion/rewrite.py

View check run for this annotation

Codecov / codecov/patch

src/_pytest/assertion/rewrite.py#L692

Added line #L692 was not covered by tests
else:
doc = item.value.s
if self.is_rewrite_disabled(doc):
return
expect_docstring = False
Expand Down Expand Up @@ -814,7 +823,7 @@
current = self.stack.pop()
if self.stack:
self.explanation_specifiers = self.stack[-1]
keys = [ast.Str(key) for key in current.keys()]
keys = [astStr(key) for key in current.keys()]
format_dict = ast.Dict(keys, list(current.values()))
form = ast.BinOp(expl_expr, ast.Mod(), format_dict)
name = "@py_format" + str(next(self.variable_counter))
Expand Down Expand Up @@ -868,16 +877,16 @@
negation = ast.UnaryOp(ast.Not(), top_condition)

if self.enable_assertion_pass_hook: # Experimental pytest_assertion_pass hook
msg = self.pop_format_context(ast.Str(explanation))
msg = self.pop_format_context(astStr(explanation))

Check warning on line 880 in src/_pytest/assertion/rewrite.py

View check run for this annotation

Codecov / codecov/patch

src/_pytest/assertion/rewrite.py#L880

Added line #L880 was not covered by tests

# Failed
if assert_.msg:
assertmsg = self.helper("_format_assertmsg", assert_.msg)
gluestr = "\n>assert "
else:
assertmsg = ast.Str("")
assertmsg = astStr("")

Check warning on line 887 in src/_pytest/assertion/rewrite.py

View check run for this annotation

Codecov / codecov/patch

src/_pytest/assertion/rewrite.py#L887

Added line #L887 was not covered by tests
gluestr = "assert "
err_explanation = ast.BinOp(ast.Str(gluestr), ast.Add(), msg)
err_explanation = ast.BinOp(astStr(gluestr), ast.Add(), msg)

Check warning on line 889 in src/_pytest/assertion/rewrite.py

View check run for this annotation

Codecov / codecov/patch

src/_pytest/assertion/rewrite.py#L889

Added line #L889 was not covered by tests
err_msg = ast.BinOp(assertmsg, ast.Add(), err_explanation)
err_name = ast.Name("AssertionError", ast.Load())
fmt = self.helper("_format_explanation", err_msg)
Expand All @@ -893,8 +902,8 @@
hook_call_pass = ast.Expr(
self.helper(
"_call_assertion_pass",
ast.Num(assert_.lineno),
ast.Str(orig),
astNum(assert_.lineno),
astStr(orig),
fmt_pass,
)
)
Expand All @@ -913,7 +922,7 @@
variables = [
ast.Name(name, ast.Store()) for name in self.format_variables
]
clear_format = ast.Assign(variables, ast.NameConstant(None))
clear_format = ast.Assign(variables, astNameConstant(None))

Check warning on line 925 in src/_pytest/assertion/rewrite.py

View check run for this annotation

Codecov / codecov/patch

src/_pytest/assertion/rewrite.py#L925

Added line #L925 was not covered by tests
self.statements.append(clear_format)

else: # Original assertion rewriting
Expand All @@ -924,9 +933,9 @@
assertmsg = self.helper("_format_assertmsg", assert_.msg)
explanation = "\n>assert " + explanation
else:
assertmsg = ast.Str("")
assertmsg = astStr("")
explanation = "assert " + explanation
template = ast.BinOp(assertmsg, ast.Add(), ast.Str(explanation))
template = ast.BinOp(assertmsg, ast.Add(), astStr(explanation))
msg = self.pop_format_context(template)
fmt = self.helper("_format_explanation", msg)
err_name = ast.Name("AssertionError", ast.Load())
Expand All @@ -938,7 +947,7 @@
# Clear temporary variables by setting them to None.
if self.variables:
variables = [ast.Name(name, ast.Store()) for name in self.variables]
clear = ast.Assign(variables, ast.NameConstant(None))
clear = ast.Assign(variables, astNameConstant(None))
self.statements.append(clear)
# Fix locations (line numbers/column offsets).
for stmt in self.statements:
Expand All @@ -952,20 +961,20 @@
# thinks it's acceptable.
locs = ast.Call(self.builtin("locals"), [], [])
target_id = name.target.id # type: ignore[attr-defined]
inlocs = ast.Compare(ast.Str(target_id), [ast.In()], [locs])
inlocs = ast.Compare(astStr(target_id), [ast.In()], [locs])

Check warning on line 964 in src/_pytest/assertion/rewrite.py

View check run for this annotation

Codecov / codecov/patch

src/_pytest/assertion/rewrite.py#L964

Added line #L964 was not covered by tests
dorepr = self.helper("_should_repr_global_name", name)
test = ast.BoolOp(ast.Or(), [inlocs, dorepr])
expr = ast.IfExp(test, self.display(name), ast.Str(target_id))
expr = ast.IfExp(test, self.display(name), astStr(target_id))

Check warning on line 967 in src/_pytest/assertion/rewrite.py

View check run for this annotation

Codecov / codecov/patch

src/_pytest/assertion/rewrite.py#L967

Added line #L967 was not covered by tests
return name, self.explanation_param(expr)

def visit_Name(self, name: ast.Name) -> Tuple[ast.Name, str]:
# Display the repr of the name if it's a local variable or
# _should_repr_global_name() thinks it's acceptable.
locs = ast.Call(self.builtin("locals"), [], [])
inlocs = ast.Compare(ast.Str(name.id), [ast.In()], [locs])
inlocs = ast.Compare(astStr(name.id), [ast.In()], [locs])
dorepr = self.helper("_should_repr_global_name", name)
test = ast.BoolOp(ast.Or(), [inlocs, dorepr])
expr = ast.IfExp(test, self.display(name), ast.Str(name.id))
expr = ast.IfExp(test, self.display(name), astStr(name.id))
return name, self.explanation_param(expr)

def visit_BoolOp(self, boolop: ast.BoolOp) -> Tuple[ast.Name, str]:
Expand Down Expand Up @@ -1003,7 +1012,7 @@
self.push_format_context()
res, expl = self.visit(v)
body.append(ast.Assign([ast.Name(res_var, ast.Store())], res))
expl_format = self.pop_format_context(ast.Str(expl))
expl_format = self.pop_format_context(astStr(expl))

Check warning on line 1015 in src/_pytest/assertion/rewrite.py

View check run for this annotation

Codecov / codecov/patch

src/_pytest/assertion/rewrite.py#L1015

Added line #L1015 was not covered by tests
call = ast.Call(app, [expl_format], [])
self.expl_stmts.append(ast.Expr(call))
if i < levels:
Expand All @@ -1015,7 +1024,7 @@
self.statements = body = inner
self.statements = save
self.expl_stmts = fail_save
expl_template = self.helper("_format_boolop", expl_list, ast.Num(is_or))
expl_template = self.helper("_format_boolop", expl_list, astNum(is_or))

Check warning on line 1027 in src/_pytest/assertion/rewrite.py

View check run for this annotation

Codecov / codecov/patch

src/_pytest/assertion/rewrite.py#L1027

Added line #L1027 was not covered by tests
expl = self.pop_format_context(expl_template)
return ast.Name(res_var, ast.Load()), self.explanation_param(expl)

Expand Down Expand Up @@ -1118,9 +1127,9 @@
next_expl = f"({next_expl})"
results.append(next_res)
sym = BINOP_MAP[op.__class__]
syms.append(ast.Str(sym))
syms.append(astStr(sym))
expl = f"{left_expl} {sym} {next_expl}"
expls.append(ast.Str(expl))
expls.append(astStr(expl))
res_expr = ast.Compare(left_res, [op], [next_res])
self.statements.append(ast.Assign([store_names[i]], res_expr))
left_res, left_expl = next_res, next_expl
Expand Down
8 changes: 7 additions & 1 deletion src/_pytest/mark/expression.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import dataclasses
import enum
import re
import sys
import types
from typing import Callable
from typing import Iterator
Expand All @@ -26,6 +27,11 @@
from typing import Optional
from typing import Sequence

if sys.version_info >= (3, 8):
astNameConstant = ast.Constant

Check warning on line 31 in src/_pytest/mark/expression.py

View check run for this annotation

Codecov / codecov/patch

src/_pytest/mark/expression.py#L31

Added line #L31 was not covered by tests
else:
astNameConstant = ast.NameConstant


__all__ = [
"Expression",
Expand Down Expand Up @@ -132,7 +138,7 @@

def expression(s: Scanner) -> ast.Expression:
if s.accept(TokenType.EOF):
ret: ast.expr = ast.NameConstant(False)
ret: ast.expr = astNameConstant(False)

Check warning on line 141 in src/_pytest/mark/expression.py

View check run for this annotation

Codecov / codecov/patch

src/_pytest/mark/expression.py#L141

Added line #L141 was not covered by tests
else:
ret = expr(s)
s.accept(TokenType.EOF, reject=True)
Expand Down
26 changes: 15 additions & 11 deletions testing/python/collect.py
Original file line number Diff line number Diff line change
Expand Up @@ -897,25 +897,29 @@ def pytest_pycollect_makeitem(collector, name, obj):
def test_issue2369_collect_module_fileext(self, pytester: Pytester) -> None:
"""Ensure we can collect files with weird file extensions as Python
modules (#2369)"""
# We'll implement a little finder and loader to import files containing
# Implement a little meta path finder to import files containing
# Python source code whose file extension is ".narf".
pytester.makeconftest(
"""
import sys, os, imp
import sys
import os.path
from importlib.util import spec_from_loader
from importlib.machinery import SourceFileLoader
from _pytest.python import Module

class Loader(object):
def load_module(self, name):
return imp.load_source(name, name + ".narf")
class Finder(object):
def find_module(self, name, path=None):
if os.path.exists(name + ".narf"):
return Loader()
sys.meta_path.append(Finder())
class MetaPathFinder:
def find_spec(self, fullname, path, target=None):
if os.path.exists(fullname + ".narf"):
return spec_from_loader(
fullname,
SourceFileLoader(fullname, fullname + ".narf"),
)
sys.meta_path.append(MetaPathFinder())

def pytest_collect_file(file_path, parent):
if file_path.suffix == ".narf":
return Module.from_parent(path=file_path, parent=parent)"""
return Module.from_parent(path=file_path, parent=parent)
"""
)
pytester.makefile(
".narf",
Expand Down