Skip to content

Commit 60b1316

Browse files
committed
Manual fixes for py39
1 parent 1516bfb commit 60b1316

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+136
-352
lines changed

.github/workflows/main.yml

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,6 @@ jobs:
2929
- check-format
3030
- check-coverage
3131
- check-conjecture-coverage
32-
- check-py38-cover
33-
- check-py38-nocover
34-
- check-py38-niche
35-
- check-pypy38-cover
3632
- check-py39-cover
3733
- check-pypy39-cover
3834
- check-py310-cover
@@ -91,7 +87,7 @@ jobs:
9187
# - check-crosshair-custom-pytest/test_*
9288
# - check-crosshair-nocover
9389
# - check-crosshair-niche
94-
- check-py38-oldestnumpy
90+
- check-py39-oldestnumpy
9591
- check-numpy-nightly
9692
fail-fast: false
9793
steps:
@@ -197,7 +193,6 @@ jobs:
197193
strategy:
198194
matrix:
199195
task:
200-
- check-py38-cover
201196
- check-py310-cover
202197
- check-py310-nocover
203198
- check-py310-niche

CONTRIBUTING.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -208,11 +208,11 @@ high level, the task takes the form:
208208
Namely, first provide the tox environment (see ``tox.ini``), then the python
209209
version to test with, then any ``tox`` or ``pytest`` args as needed. For
210210
example, to run all of the tests in the file
211-
``tests/nocover/test_conjecture_engine.py`` with python 3.8:
211+
``tests/nocover/test_conjecture_engine.py`` with python 3.12:
212212

213213
.. code-block::
214214
215-
./build.sh tox py38-custom 3.8.13 -- tests/nocover/test_conjecture_engine.py
215+
./build.sh tox py312-custom 3.12.7 -- tests/nocover/test_conjecture_engine.py
216216
217217
See the ``tox`` docs and ``pytest`` docs for more information:
218218
* https://docs.pytest.org/en/latest/how-to/usage.html

hypothesis-python/RELEASE.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
RELEASE_TYPE: minor
2+
3+
This release drops support for Python 3.8, `which reached end of life on
4+
2024-10-07 <https://devguide.python.org/versions/>`__.

hypothesis-python/docs/supported.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ exactly where an error came from, or turn only our warnings into errors.
4545
Python versions
4646
---------------
4747

48-
Hypothesis is supported and tested on CPython 3.8+, i.e.
48+
Hypothesis is supported and tested on CPython 3.9+, i.e.
4949
`all versions of CPython with upstream support <https://devguide.python.org/versions/>`_,
5050
along with PyPy for the same versions.
5151
32-bit builds of CPython also work, though we only test them on Windows.

hypothesis-python/scripts/basic-test.sh

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,9 @@ if [ "$(python -c $'import platform, sys; print(sys.version_info.releaselevel ==
4747
$PYTEST tests/codemods/
4848
pip uninstall -y libcst click
4949

50-
if [ "$(python -c 'import sys; print(sys.version_info[:2] == (3, 8))')" = "True" ] ; then
51-
# Per NEP-29, this is the last version to support Python 3.8
52-
pip install numpy==1.24.3
50+
if [ "$(python -c 'import sys; print(sys.version_info[:2] == (3, 9))')" = "True" ] ; then
51+
# Per NEP-29, this is the last version to support Python 3.9
52+
pip install numpy==2.0.2
5353
else
5454
pip install "$(grep 'numpy==' ../requirements/coverage.txt)"
5555
fi

hypothesis-python/scripts/other-tests.sh

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,9 @@ if [[ "$HYPOTHESIS_PROFILE" != "crosshair" ]]; then
3535
pip uninstall -y typing_extensions
3636
fi
3737

38-
if [ "$(python -c 'import sys; print(sys.version_info[:2] >= (3, 9))')" = "True" ] ; then
39-
pip install "$(grep 'annotated-types==' ../requirements/coverage.txt)"
40-
$PYTEST tests/test_annotated_types.py
41-
pip uninstall -y annotated-types
42-
fi
38+
pip install "$(grep 'annotated-types==' ../requirements/coverage.txt)"
39+
$PYTEST tests/test_annotated_types.py
40+
pip uninstall -y annotated-types
4341

4442
pip install ".[lark]"
4543
pip install "$(grep -oE 'lark>=([0-9.]+)' ../hypothesis-python/setup.py | tr '>' =)"
@@ -52,17 +50,15 @@ if [ "$(python -c $'import platform, sys; print(sys.version_info.releaselevel ==
5250
pip install ".[codemods,cli]"
5351
$PYTEST tests/codemods/
5452

55-
if [ "$(python -c 'import sys; print(sys.version_info[:2] == (3, 8))')" = "True" ] ; then
56-
# Per NEP-29, this is the last version to support Python 3.8
57-
pip install numpy==1.24.3
53+
if [ "$(python -c 'import sys; print(sys.version_info[:2] == (3, 9))')" = "True" ] ; then
54+
# Per NEP-29, this is the last version to support Python 3.9
55+
pip install numpy==2.0.2
5856
else
5957
pip install "$(grep 'numpy==' ../requirements/coverage.txt)"
6058
fi
6159

6260
pip install "$(grep -E 'black(==| @)' ../requirements/coverage.txt)"
63-
if [ "$(python -c 'import sys; print(sys.version_info[:2] >= (3, 9))')" = "True" ] ; then
64-
$PYTEST tests/patching/
65-
fi
61+
$PYTEST tests/patching/
6662
pip uninstall -y libcst
6763

6864
$PYTEST tests/ghostwriter/

hypothesis-python/setup.py

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,10 @@
1414

1515
import setuptools
1616

17-
if sys.version_info[:2] < (3, 8): # noqa # "unreachable" sanity check
17+
if sys.version_info[:2] < (3, 9): # "unreachable" sanity check
1818
raise Exception(
1919
"You are trying to install Hypothesis using Python "
20-
f"{sys.version.split()[0]}, but it requires Python 3.8 or later."
20+
f"{sys.version.split()[0]}, but it requires Python 3.9 or later."
2121
"Update `pip` and `setuptools`, try again, and you will automatically "
2222
"get the latest compatible version of Hypothesis instead. "
2323
"See also https://python3statement.org/practicalities/"
@@ -55,17 +55,15 @@ def local_file(name):
5555
"pytz": ["pytz>=2014.1"],
5656
"dateutil": ["python-dateutil>=1.4"],
5757
"lark": ["lark>=0.10.1"], # probably still works with old `lark-parser` too
58-
"numpy": ["numpy>=1.17.3"], # oldest with wheels for non-EOL Python (for now)
58+
"numpy": ["numpy>=1.19.3"], # oldest with wheels for non-EOL Python (for now)
5959
"pandas": ["pandas>=1.1"],
6060
"pytest": ["pytest>=4.6"],
6161
"dpcontracts": ["dpcontracts>=0.4"],
6262
"redis": ["redis>=3.0.0"],
6363
"crosshair": ["hypothesis-crosshair>=0.0.14", "crosshair-tool>=0.0.73"],
64-
# zoneinfo is an odd one: every dependency is conditional, because they're
65-
# only necessary on old versions of Python or Windows systems or emscripten.
64+
# zoneinfo is an odd one: every dependency is platform-conditional.
6665
"zoneinfo": [
6766
"tzdata>=2024.2 ; sys_platform == 'win32' or sys_platform == 'emscripten'",
68-
"backports.zoneinfo>=0.2.1 ; python_version<'3.9'",
6967
],
7068
# We only support Django versions with upstream support - see
7169
# https://www.djangoproject.com/download/#supported-versions
@@ -101,7 +99,7 @@ def local_file(name):
10199
"exceptiongroup>=1.0.0 ; python_version<'3.11'",
102100
"sortedcontainers>=2.1.0,<3.0.0",
103101
],
104-
python_requires=">=3.8",
102+
python_requires=">=3.9",
105103
classifiers=[
106104
"Development Status :: 5 - Production/Stable",
107105
"Framework :: Hypothesis",
@@ -114,7 +112,6 @@ def local_file(name):
114112
"Programming Language :: Python",
115113
"Programming Language :: Python :: 3",
116114
"Programming Language :: Python :: 3 :: Only",
117-
"Programming Language :: Python :: 3.8",
118115
"Programming Language :: Python :: 3.9",
119116
"Programming Language :: Python :: 3.10",
120117
"Programming Language :: Python :: 3.11",

hypothesis-python/src/hypothesis/core.py

Lines changed: 2 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@ def xfail(
209209
@example(...).xfail()
210210
@example(...).xfail(reason="Prices must be non-negative")
211211
@example(...).xfail(raises=(KeyError, ValueError))
212-
@example(...).xfail(sys.version_info[:2] >= (3, 9), reason="needs py39+")
212+
@example(...).xfail(sys.version_info[:2] >= (3, 12), reason="needs py 3.12")
213213
@example(...).xfail(condition=sys.platform != "linux", raises=OSError)
214214
def test(x):
215215
pass
@@ -229,21 +229,6 @@ def test_fraction(x, y):
229229
# strategy. If we happen to generate y=0, the test will fail
230230
# because only the explicit example is treated as xfailing.
231231
x / y
232-
233-
Note that this "method chaining" syntax requires Python 3.9 or later, for
234-
:pep:`614` relaxing grammar restrictions on decorators. If you need to
235-
support older versions of Python, you can use an identity function:
236-
237-
.. code-block:: python
238-
239-
def identity(x):
240-
return x
241-
242-
243-
@identity(example(...).xfail())
244-
def test(x):
245-
pass
246-
247232
"""
248233
check_type(bool, condition, "condition")
249234
check_type(str, reason, "reason")
@@ -284,21 +269,6 @@ def via(self, whence: str, /) -> "example":
284269
@example(...).via("hy-target-$label")
285270
def test(x):
286271
pass
287-
288-
Note that this "method chaining" syntax requires Python 3.9 or later, for
289-
:pep:`614` relaxing grammar restrictions on decorators. If you need to
290-
support older versions of Python, you can use an identity function:
291-
292-
.. code-block:: python
293-
294-
def identity(x):
295-
return x
296-
297-
298-
@identity(example(...).via("label"))
299-
def test(x):
300-
pass
301-
302272
"""
303273
if not isinstance(whence, str):
304274
raise InvalidArgument(".via() must be passed a string")
@@ -1359,7 +1329,7 @@ def _raise_to_user(
13591329
for note in fragments:
13601330
add_note(err, note)
13611331
if note.startswith(failing_prefix):
1362-
ls.append(note[len(failing_prefix) :])
1332+
ls.append(note.removeprefix(failing_prefix))
13631333
if current_pytest_item.value:
13641334
current_pytest_item.value._hypothesis_failing_examples = ls
13651335

hypothesis-python/src/hypothesis/extra/_patching.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,8 +112,6 @@ def __call_node_to_example_dec(self, node, via):
112112
else node.args
113113
),
114114
)
115-
# Note: calling a method on a decorator requires PEP-614, i.e. Python 3.9+,
116-
# but plumbing two cases through doesn't seem worth the trouble :-/
117115
via = cst.Call(
118116
func=cst.Attribute(node, cst.Name("via")),
119117
args=[cst.Arg(cst.SimpleString(repr(via)))],

hypothesis-python/src/hypothesis/extra/ghostwriter.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -290,9 +290,8 @@ def _strategy_for(param: inspect.Parameter, docstring: str) -> st.SearchStrategy
290290
types = []
291291
for token in re.split(r",? +or +| *, *", doc_type):
292292
for prefix in ("default ", "python "):
293-
# `str or None, default "auto"`; `python int or numpy.int64`
294-
if token.startswith(prefix):
295-
token = token[len(prefix) :]
293+
# e.g. `str or None, default "auto"` or `python int or numpy.int64`
294+
token = token.removeprefix(prefix)
296295
if not token:
297296
continue
298297
try:
@@ -636,8 +635,6 @@ def _imports_for_strategy(strategy):
636635
for imp in _imports_for_object(_strip_typevars(arg))
637636
}
638637
if re.match(r"from_(type|regex)\(", repr(strategy)):
639-
if repr(strategy).startswith("from_type("):
640-
return {module for module, _ in imports}
641638
return imports
642639
elif _get_module(strategy.function).startswith("hypothesis.extra."):
643640
module = _get_module(strategy.function).replace("._array_helpers", ".numpy")
@@ -1228,7 +1225,7 @@ def magic(
12281225
for k in sorted(sys.modules, key=len):
12291226
if (
12301227
k.startswith(f"{thing.__name__}.")
1231-
and "._" not in k[len(thing.__name__) :]
1228+
and "._" not in k.removeprefix(thing.__name__)
12321229
and not k.startswith(tuple(f"{m}." for m in mods))
12331230
and _get_testable_functions(sys.modules[k])
12341231
):

hypothesis-python/src/hypothesis/internal/compat.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -150,16 +150,14 @@ def get_type_hints(thing):
150150
)
151151
return {k: v for k, v in get_type_hints(thing.func).items() if k not in bound}
152152

153-
kwargs = {} if sys.version_info[:2] < (3, 9) else {"include_extras": True}
154-
155153
try:
156-
hints = typing.get_type_hints(thing, **kwargs)
154+
hints = typing.get_type_hints(thing, include_extras=True)
157155
except (AttributeError, TypeError, NameError): # pragma: no cover
158156
hints = {}
159157

160158
if inspect.isclass(thing):
161159
try:
162-
hints.update(typing.get_type_hints(thing.__init__, **kwargs))
160+
hints.update(typing.get_type_hints(thing.__init__, include_extras=True))
163161
except (TypeError, NameError, AttributeError):
164162
pass
165163

@@ -232,7 +230,7 @@ def extract_bits(x: int, /, width: Optional[int] = None) -> List[int]:
232230
return result
233231

234232

235-
# int.bit_count was added sometime around python 3.9
233+
# int.bit_count was added in python 3.10
236234
try:
237235
bit_count = int.bit_count
238236
except AttributeError: # pragma: no cover

hypothesis-python/src/hypothesis/internal/conjecture/engine.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,9 @@ def timing_report(self) -> str:
117117
"""Return a terminal report describing what was slow."""
118118
if not self.draw_times:
119119
return ""
120-
width = max(len(k[len("generate:") :].strip(": ")) for k in self.draw_times)
120+
width = max(
121+
len(k.removeprefix("generate:").removesuffix(": ")) for k in self.draw_times
122+
)
121123
out = [f"\n {'':^{width}} count | fraction | slowest draws (seconds)"]
122124
args_in_order = sorted(self.draw_times.items(), key=lambda kv: -sum(kv[1]))
123125
for i, (argname, times) in enumerate(args_in_order): # pragma: no branch
@@ -132,7 +134,7 @@ def timing_report(self) -> str:
132134
# Compute the row to report, omitting times <1ms to focus on slow draws
133135
reprs = [f"{t:>6.3f}," for t in sorted(times)[-5:] if t > 5e-4]
134136
desc = " ".join(([" -- "] * 5 + reprs)[-5:]).rstrip(",")
135-
arg = argname[len("generate:") :].strip(": ") # removeprefix in py3.9
137+
arg = argname.removeprefix("generate:").removesuffix(": ")
136138
out.append(
137139
f" {arg:^{width}} | {len(times):>4} | "
138140
f"{math.fsum(times)/self.total_draw_time:>7.0%} | {desc}"

hypothesis-python/src/hypothesis/internal/reflection.py

Lines changed: 3 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
from weakref import WeakKeyDictionary
3333

3434
from hypothesis.errors import HypothesisWarning
35-
from hypothesis.internal.compat import PYPY, is_typed_named_tuple
35+
from hypothesis.internal.compat import is_typed_named_tuple
3636
from hypothesis.utils.conventions import not_set
3737
from hypothesis.vendor.pretty import pretty
3838

@@ -67,14 +67,7 @@ def _clean_source(src: str) -> bytes:
6767
# lines - i.e. any decorators, so that adding `@example()`s keeps the same key.
6868
try:
6969
funcdef = ast.parse(src).body[0]
70-
if sys.version_info[:2] == (3, 8) and PYPY:
71-
# We can't get a line number of the (async) def here, so as a best-effort
72-
# approximation we'll use str.split instead and hope for the best.
73-
tag = "async def " if isinstance(funcdef, ast.AsyncFunctionDef) else "def "
74-
if tag in src:
75-
src = tag + src.split(tag, maxsplit=1)[1]
76-
else:
77-
src = "".join(src.splitlines(keepends=True)[funcdef.lineno - 1 :])
70+
src = "".join(src.splitlines(keepends=True)[funcdef.lineno - 1 :])
7871
except Exception:
7972
pass
8073
# Remove blank lines and use the tokenize module to strip out comments,
@@ -166,16 +159,6 @@ def get_signature(
166159
parameters=[v for k, v in sig.parameters.items() if k != "self"]
167160
)
168161
return sig
169-
if sys.version_info[:2] <= (3, 8) and inspect.isclass(target):
170-
# Workaround for subclasses of typing.Generic on Python <= 3.8
171-
from hypothesis.strategies._internal.types import is_generic_type
172-
173-
if is_generic_type(target):
174-
sig = inspect.signature(target.__init__)
175-
check_signature(sig)
176-
return sig.replace(
177-
parameters=[v for k, v in sig.parameters.items() if k != "self"]
178-
)
179162
# eval_str is only supported by Python 3.10 and newer
180163
if sys.version_info[:2] >= (3, 10):
181164
sig = inspect.signature(
@@ -257,7 +240,7 @@ def ast_arguments_matches_signature(args, sig):
257240
assert isinstance(args, ast.arguments)
258241
assert isinstance(sig, inspect.Signature)
259242
expected = []
260-
for node in getattr(args, "posonlyargs", ()): # New in Python 3.8
243+
for node in args.posonlyargs:
261244
expected.append((node.arg, inspect.Parameter.POSITIONAL_ONLY))
262245
for node in args.args:
263246
expected.append((node.arg, inspect.Parameter.POSITIONAL_OR_KEYWORD))

hypothesis-python/src/hypothesis/internal/scrutineer.py

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -250,15 +250,6 @@ def _get_git_repo_root() -> Path:
250250
return Path(where)
251251

252252

253-
if sys.version_info[:2] <= (3, 8):
254-
255-
def is_relative_to(self, other):
256-
return other == self or other in self.parents
257-
258-
else:
259-
is_relative_to = Path.is_relative_to
260-
261-
262253
def tractable_coverage_report(trace: Trace) -> Dict[str, List[int]]:
263254
"""Report a simple coverage map which is (probably most) of the user's code."""
264255
coverage: dict = {}
@@ -272,6 +263,6 @@ def tractable_coverage_report(trace: Trace) -> Dict[str, List[int]]:
272263
k: sorted(v)
273264
for k, v in coverage.items()
274265
if stdlib_fragment not in k
275-
and is_relative_to(p := Path(k), _get_git_repo_root())
266+
and (p := Path(k)).is_relative_to(_get_git_repo_root())
276267
and "site-packages" not in p.parts
277268
}

hypothesis-python/src/hypothesis/provisional.py

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,8 @@
3333

3434
# This file is sourced from http://data.iana.org/TLD/tlds-alpha-by-domain.txt
3535
# The file contains additional information about the date that it was last updated.
36-
try: # pragma: no cover
37-
traversable = resources.files("hypothesis.vendor") / "tlds-alpha-by-domain.txt"
38-
_comment, *_tlds = traversable.read_text(encoding="utf-8").splitlines()
39-
except (AttributeError, ValueError): # pragma: no cover # .files() was added in 3.9
40-
_comment, *_tlds = resources.read_text(
41-
"hypothesis.vendor", "tlds-alpha-by-domain.txt", encoding="utf-8"
42-
).splitlines()
36+
traversable = resources.files("hypothesis.vendor") / "tlds-alpha-by-domain.txt"
37+
_comment, *_tlds = traversable.read_text(encoding="utf-8").splitlines()
4338
assert _comment.startswith("#")
4439

4540
# Remove special-use domain names from the list. For more discussion

0 commit comments

Comments
 (0)