Skip to content

Commit 8e1bbe1

Browse files
bluetechpytestbot
authored andcommitted
[7.3.x] Python 3.12 support
1 parent d054a68 commit 8e1bbe1

File tree

7 files changed

+75
-41
lines changed

7 files changed

+75
-41
lines changed

.github/workflows/test.yml

+20-8
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ jobs:
4343
"windows-py39",
4444
"windows-py310",
4545
"windows-py311",
46+
"windows-py312",
4647

4748
"ubuntu-py37",
4849
"ubuntu-py37-pluggy",
@@ -51,12 +52,13 @@ jobs:
5152
"ubuntu-py39",
5253
"ubuntu-py310",
5354
"ubuntu-py311",
55+
"ubuntu-py312",
5456
"ubuntu-pypy3",
5557

5658
"macos-py37",
57-
"macos-py38",
5859
"macos-py39",
5960
"macos-py310",
61+
"macos-py312",
6062

6163
"docs",
6264
"doctesting",
@@ -86,9 +88,13 @@ jobs:
8688
os: windows-latest
8789
tox_env: "py310-xdist"
8890
- name: "windows-py311"
89-
python: "3.11-dev"
91+
python: "3.11"
9092
os: windows-latest
9193
tox_env: "py311"
94+
- name: "windows-py312"
95+
python: "3.12-dev"
96+
os: windows-latest
97+
tox_env: "py312"
9298

9399
- name: "ubuntu-py37"
94100
python: "3.7"
@@ -116,10 +122,15 @@ jobs:
116122
os: ubuntu-latest
117123
tox_env: "py310-xdist"
118124
- name: "ubuntu-py311"
119-
python: "3.11-dev"
125+
python: "3.11"
120126
os: ubuntu-latest
121127
tox_env: "py311"
122128
use_coverage: true
129+
- name: "ubuntu-py312"
130+
python: "3.12-dev"
131+
os: ubuntu-latest
132+
tox_env: "py312"
133+
use_coverage: true
123134
- name: "ubuntu-pypy3"
124135
python: "pypy-3.7"
125136
os: ubuntu-latest
@@ -129,19 +140,19 @@ jobs:
129140
python: "3.7"
130141
os: macos-latest
131142
tox_env: "py37-xdist"
132-
- name: "macos-py38"
133-
python: "3.8"
134-
os: macos-latest
135-
tox_env: "py38-xdist"
136-
use_coverage: true
137143
- name: "macos-py39"
138144
python: "3.9"
139145
os: macos-latest
140146
tox_env: "py39-xdist"
147+
use_coverage: true
141148
- name: "macos-py310"
142149
python: "3.10"
143150
os: macos-latest
144151
tox_env: "py310-xdist"
152+
- name: "macos-py312"
153+
python: "3.12-dev"
154+
os: macos-latest
155+
tox_env: "py312-xdist"
145156

146157
- name: "plugins"
147158
python: "3.9"
@@ -168,6 +179,7 @@ jobs:
168179
uses: actions/setup-python@v4
169180
with:
170181
python-version: ${{ matrix.python }}
182+
check-latest: ${{ endsWith(matrix.python, '-dev') }}
171183

172184
- name: Install dependencies
173185
run: |

.pre-commit-config.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ repos:
5252
rev: v2.2.0
5353
hooks:
5454
- id: setup-cfg-fmt
55-
args: ["--max-py-version=3.11", "--include-version-classifiers"]
55+
args: ["--max-py-version=3.12", "--include-version-classifiers"]
5656
- repo: https://github.com/pre-commit/pygrep-hooks
5757
rev: v1.10.0
5858
hooks:

changelog/10894.bugfix.rst

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Support for Python 3.12 (beta at the time of writing).

setup.cfg

+2
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ classifiers =
2222
Programming Language :: Python :: 3.9
2323
Programming Language :: Python :: 3.10
2424
Programming Language :: Python :: 3.11
25+
Programming Language :: Python :: 3.12
2526
Topic :: Software Development :: Libraries
2627
Topic :: Software Development :: Testing
2728
Topic :: Utilities
@@ -73,6 +74,7 @@ testing =
7374
nose
7475
pygments>=2.7.2
7576
requests
77+
setuptools
7678
xmlschema
7779

7880
[options.package_data]

src/_pytest/assertion/rewrite.py

+29-20
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,14 @@
4646

4747
if sys.version_info >= (3, 8):
4848
namedExpr = ast.NamedExpr
49+
astNameConstant = ast.Constant
50+
astStr = ast.Constant
51+
astNum = ast.Constant
4952
else:
5053
namedExpr = ast.Expr
54+
astNameConstant = ast.NameConstant
55+
astStr = ast.Str
56+
astNum = ast.Num
5157

5258

5359
assertstate_key = StashKey["AssertionState"]()
@@ -680,9 +686,12 @@ def run(self, mod: ast.Module) -> None:
680686
if (
681687
expect_docstring
682688
and isinstance(item, ast.Expr)
683-
and isinstance(item.value, ast.Str)
689+
and isinstance(item.value, astStr)
684690
):
685-
doc = item.value.s
691+
if sys.version_info >= (3, 8):
692+
doc = item.value.value
693+
else:
694+
doc = item.value.s
686695
if self.is_rewrite_disabled(doc):
687696
return
688697
expect_docstring = False
@@ -814,7 +823,7 @@ def pop_format_context(self, expl_expr: ast.expr) -> ast.Name:
814823
current = self.stack.pop()
815824
if self.stack:
816825
self.explanation_specifiers = self.stack[-1]
817-
keys = [ast.Str(key) for key in current.keys()]
826+
keys = [astStr(key) for key in current.keys()]
818827
format_dict = ast.Dict(keys, list(current.values()))
819828
form = ast.BinOp(expl_expr, ast.Mod(), format_dict)
820829
name = "@py_format" + str(next(self.variable_counter))
@@ -868,16 +877,16 @@ def visit_Assert(self, assert_: ast.Assert) -> List[ast.stmt]:
868877
negation = ast.UnaryOp(ast.Not(), top_condition)
869878

870879
if self.enable_assertion_pass_hook: # Experimental pytest_assertion_pass hook
871-
msg = self.pop_format_context(ast.Str(explanation))
880+
msg = self.pop_format_context(astStr(explanation))
872881

873882
# Failed
874883
if assert_.msg:
875884
assertmsg = self.helper("_format_assertmsg", assert_.msg)
876885
gluestr = "\n>assert "
877886
else:
878-
assertmsg = ast.Str("")
887+
assertmsg = astStr("")
879888
gluestr = "assert "
880-
err_explanation = ast.BinOp(ast.Str(gluestr), ast.Add(), msg)
889+
err_explanation = ast.BinOp(astStr(gluestr), ast.Add(), msg)
881890
err_msg = ast.BinOp(assertmsg, ast.Add(), err_explanation)
882891
err_name = ast.Name("AssertionError", ast.Load())
883892
fmt = self.helper("_format_explanation", err_msg)
@@ -893,8 +902,8 @@ def visit_Assert(self, assert_: ast.Assert) -> List[ast.stmt]:
893902
hook_call_pass = ast.Expr(
894903
self.helper(
895904
"_call_assertion_pass",
896-
ast.Num(assert_.lineno),
897-
ast.Str(orig),
905+
astNum(assert_.lineno),
906+
astStr(orig),
898907
fmt_pass,
899908
)
900909
)
@@ -913,7 +922,7 @@ def visit_Assert(self, assert_: ast.Assert) -> List[ast.stmt]:
913922
variables = [
914923
ast.Name(name, ast.Store()) for name in self.format_variables
915924
]
916-
clear_format = ast.Assign(variables, ast.NameConstant(None))
925+
clear_format = ast.Assign(variables, astNameConstant(None))
917926
self.statements.append(clear_format)
918927

919928
else: # Original assertion rewriting
@@ -924,9 +933,9 @@ def visit_Assert(self, assert_: ast.Assert) -> List[ast.stmt]:
924933
assertmsg = self.helper("_format_assertmsg", assert_.msg)
925934
explanation = "\n>assert " + explanation
926935
else:
927-
assertmsg = ast.Str("")
936+
assertmsg = astStr("")
928937
explanation = "assert " + explanation
929-
template = ast.BinOp(assertmsg, ast.Add(), ast.Str(explanation))
938+
template = ast.BinOp(assertmsg, ast.Add(), astStr(explanation))
930939
msg = self.pop_format_context(template)
931940
fmt = self.helper("_format_explanation", msg)
932941
err_name = ast.Name("AssertionError", ast.Load())
@@ -938,7 +947,7 @@ def visit_Assert(self, assert_: ast.Assert) -> List[ast.stmt]:
938947
# Clear temporary variables by setting them to None.
939948
if self.variables:
940949
variables = [ast.Name(name, ast.Store()) for name in self.variables]
941-
clear = ast.Assign(variables, ast.NameConstant(None))
950+
clear = ast.Assign(variables, astNameConstant(None))
942951
self.statements.append(clear)
943952
# Fix locations (line numbers/column offsets).
944953
for stmt in self.statements:
@@ -952,20 +961,20 @@ def visit_NamedExpr(self, name: namedExpr) -> Tuple[namedExpr, str]:
952961
# thinks it's acceptable.
953962
locs = ast.Call(self.builtin("locals"), [], [])
954963
target_id = name.target.id # type: ignore[attr-defined]
955-
inlocs = ast.Compare(ast.Str(target_id), [ast.In()], [locs])
964+
inlocs = ast.Compare(astStr(target_id), [ast.In()], [locs])
956965
dorepr = self.helper("_should_repr_global_name", name)
957966
test = ast.BoolOp(ast.Or(), [inlocs, dorepr])
958-
expr = ast.IfExp(test, self.display(name), ast.Str(target_id))
967+
expr = ast.IfExp(test, self.display(name), astStr(target_id))
959968
return name, self.explanation_param(expr)
960969

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

971980
def visit_BoolOp(self, boolop: ast.BoolOp) -> Tuple[ast.Name, str]:
@@ -1003,7 +1012,7 @@ def visit_BoolOp(self, boolop: ast.BoolOp) -> Tuple[ast.Name, str]:
10031012
self.push_format_context()
10041013
res, expl = self.visit(v)
10051014
body.append(ast.Assign([ast.Name(res_var, ast.Store())], res))
1006-
expl_format = self.pop_format_context(ast.Str(expl))
1015+
expl_format = self.pop_format_context(astStr(expl))
10071016
call = ast.Call(app, [expl_format], [])
10081017
self.expl_stmts.append(ast.Expr(call))
10091018
if i < levels:
@@ -1015,7 +1024,7 @@ def visit_BoolOp(self, boolop: ast.BoolOp) -> Tuple[ast.Name, str]:
10151024
self.statements = body = inner
10161025
self.statements = save
10171026
self.expl_stmts = fail_save
1018-
expl_template = self.helper("_format_boolop", expl_list, ast.Num(is_or))
1027+
expl_template = self.helper("_format_boolop", expl_list, astNum(is_or))
10191028
expl = self.pop_format_context(expl_template)
10201029
return ast.Name(res_var, ast.Load()), self.explanation_param(expl)
10211030

@@ -1118,9 +1127,9 @@ def visit_Compare(self, comp: ast.Compare) -> Tuple[ast.expr, str]:
11181127
next_expl = f"({next_expl})"
11191128
results.append(next_res)
11201129
sym = BINOP_MAP[op.__class__]
1121-
syms.append(ast.Str(sym))
1130+
syms.append(astStr(sym))
11221131
expl = f"{left_expl} {sym} {next_expl}"
1123-
expls.append(ast.Str(expl))
1132+
expls.append(astStr(expl))
11241133
res_expr = ast.Compare(left_res, [op], [next_res])
11251134
self.statements.append(ast.Assign([store_names[i]], res_expr))
11261135
left_res, left_expl = next_res, next_expl

src/_pytest/mark/expression.py

+7-1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import dataclasses
1919
import enum
2020
import re
21+
import sys
2122
import types
2223
from typing import Callable
2324
from typing import Iterator
@@ -26,6 +27,11 @@
2627
from typing import Optional
2728
from typing import Sequence
2829

30+
if sys.version_info >= (3, 8):
31+
astNameConstant = ast.Constant
32+
else:
33+
astNameConstant = ast.NameConstant
34+
2935

3036
__all__ = [
3137
"Expression",
@@ -132,7 +138,7 @@ def reject(self, expected: Sequence[TokenType]) -> NoReturn:
132138

133139
def expression(s: Scanner) -> ast.Expression:
134140
if s.accept(TokenType.EOF):
135-
ret: ast.expr = ast.NameConstant(False)
141+
ret: ast.expr = astNameConstant(False)
136142
else:
137143
ret = expr(s)
138144
s.accept(TokenType.EOF, reject=True)

testing/python/collect.py

+15-11
Original file line numberDiff line numberDiff line change
@@ -897,25 +897,29 @@ def pytest_pycollect_makeitem(collector, name, obj):
897897
def test_issue2369_collect_module_fileext(self, pytester: Pytester) -> None:
898898
"""Ensure we can collect files with weird file extensions as Python
899899
modules (#2369)"""
900-
# We'll implement a little finder and loader to import files containing
900+
# Implement a little meta path finder to import files containing
901901
# Python source code whose file extension is ".narf".
902902
pytester.makeconftest(
903903
"""
904-
import sys, os, imp
904+
import sys
905+
import os.path
906+
from importlib.util import spec_from_loader
907+
from importlib.machinery import SourceFileLoader
905908
from _pytest.python import Module
906909
907-
class Loader(object):
908-
def load_module(self, name):
909-
return imp.load_source(name, name + ".narf")
910-
class Finder(object):
911-
def find_module(self, name, path=None):
912-
if os.path.exists(name + ".narf"):
913-
return Loader()
914-
sys.meta_path.append(Finder())
910+
class MetaPathFinder:
911+
def find_spec(self, fullname, path, target=None):
912+
if os.path.exists(fullname + ".narf"):
913+
return spec_from_loader(
914+
fullname,
915+
SourceFileLoader(fullname, fullname + ".narf"),
916+
)
917+
sys.meta_path.append(MetaPathFinder())
915918
916919
def pytest_collect_file(file_path, parent):
917920
if file_path.suffix == ".narf":
918-
return Module.from_parent(path=file_path, parent=parent)"""
921+
return Module.from_parent(path=file_path, parent=parent)
922+
"""
919923
)
920924
pytester.makefile(
921925
".narf",

0 commit comments

Comments
 (0)