Skip to content

Commit

Permalink
update codebase for minimum Python version 3.8
Browse files Browse the repository at this point in the history
- Remove some ImportError checks imports that would fail on Python 3.6 and 3.7
- Remove check for availability of `exc.with_traceback` (was added in 3.7)
- Update comments and docstrings

Some of the old support code in complex, rarely used parts (e.g. the
typechecker we use to implement multiple dispatch) has been left as-is.
  • Loading branch information
Technologicat committed Sep 26, 2024
1 parent 5e3fed7 commit 9660f4b
Show file tree
Hide file tree
Showing 14 changed files with 47 additions and 95 deletions.
2 changes: 1 addition & 1 deletion unpythonic/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
from .gmemo import * # noqa: F401, F403
from .gtco import * # noqa: F401, F403
from .it import * # noqa: F401, F403
from .let import * # no guarantees on evaluation order (before Python 3.6), nice syntax # noqa: F401, F403
from .let import * # # noqa: F401, F403

# As of 0.15.0, lispylet is nowadays primarily a code generation target API for macros.
from .lispylet import (let as ordered_let, letrec as ordered_letrec, # noqa: F401
Expand Down
12 changes: 6 additions & 6 deletions unpythonic/arity.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class UnknownArity(ValueError):
"""Raised when the arity of a function cannot be inspected."""

# HACK: some built-ins report incorrect arities (0, 0) at least in Python 3.4
# TODO: re-test on 3.8 and on PyPy3 (3.7), just to be sure.
# TODO: re-test on 3.8, 3.9, 3.10, 3.11, 3.12 and on PyPy3 (3.8 and later), just to be sure.
#
# Full list of built-ins:
# https://docs.python.org/3/library/functions.html
Expand Down Expand Up @@ -208,7 +208,7 @@ def arities(f):
This uses inspect.signature; note that the signature of builtin functions
cannot be inspected. This is worked around to some extent, but e.g.
methods of built-in classes (such as ``list``) might not be inspectable
(at least on CPython < 3.7).
(at least on old CPython < 3.7).
For bound methods, ``self`` or ``cls`` does not count toward the arity,
because these are passed implicitly by Python. Note a `@classmethod` becomes
Expand Down Expand Up @@ -352,10 +352,10 @@ def resolve_bindings(f, *args, **kwargs):
This is an inspection tool, which does not actually call `f`. This is useful for memoizers
and other similar decorators that need a canonical representation of `f`'s parameter bindings.
**NOTE**: As of v0.15.0, this is a thin wrapper on top of `inspect.Signature.bind`,
which was added in Python 3.5. In `unpythonic` 0.14.2 and 0.14.3, we used to have
our own implementation of the parameter binding algorithm (that ran also on Python 3.4),
but it is no longer needed, since now we support only Python 3.6 and later.
**NOTE**: This is a thin wrapper on top of `inspect.Signature.bind`, which was added in Python 3.5.
In `unpythonic` 0.14.2 and 0.14.3, we used to have our own implementation of the parameter binding
algorithm (that ran also on Python 3.4), but it is no longer needed, since as of v0.15.3,
we support only Python 3.8 and later.
The only thing we do beside call `inspect.Signature.bind` is that we apply default values
(from the definition of `f`) automatically.
Expand Down
21 changes: 8 additions & 13 deletions unpythonic/conditions.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,10 +123,10 @@ def signal(condition, *, cause=None, protocol=None):
The return value is the input `condition`, canonized to an instance
(even if originally, an exception *type* was passed to `signal`),
with its `__cause__` and `__protocol__` attributes filled in,
and with a traceback attached (on Python 3.7+). For example, the
`error` protocol uses the return value to chain the unhandled signal
properly into a `ControlError` exception; as a result, the error report
looks like a standard exception chain, with nice-looking tracebacks.
and with a traceback attached. For example, the `error` protocol
uses the return value to chain the unhandled signal properly into
a `ControlError` exception; as a result, the error report looks
like a standard exception chain, with nice-looking tracebacks.
If you want to error out on unhandled conditions, see `error`, which is
otherwise the same as `signal`, except it raises if `signal` would have
Expand Down Expand Up @@ -162,9 +162,8 @@ def signal(condition, *, cause=None, protocol=None):
You can signal any exception or warning object, both builtins and any
custom ones.
On Python 3.7 and later, the exception object representing the signaled
condition is equipped with a traceback, just like a raised exception.
On Python 3.6 this is not possible, so the traceback is `None`.
The exception object representing the signaled condition is equipped
with a traceback, just like a raised exception.
"""
# Since the handler is called normally, we don't unwind the call stack,
# remaining inside the `signal()` call in the low-level code.
Expand Down Expand Up @@ -225,12 +224,8 @@ def canonize(exc, err_reason):
condition.__cause__ = cause
condition.__protocol__ = protocol

# Embed a stack trace in the signal, like Python does for raised exceptions.
# This only works on Python 3.7 and later, because we need to create a traceback object in pure Python code.
try:
condition = equip_with_traceback(condition, stacklevel=stacklevel)
except NotImplementedError: # pragma: no cover
pass # well, we tried!
# Embed a stack trace in the signal, like Python does for raised exceptions. This API was added in Python 3.7.
condition = equip_with_traceback(condition, stacklevel=stacklevel)

return condition

Expand Down
29 changes: 11 additions & 18 deletions unpythonic/excutil.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,10 +166,6 @@ def equip_with_traceback(exc, stacklevel=1): # Python 3.7+
The return value is `exc`, with its traceback set to the produced
traceback.
Python 3.7 and later only.
When not supported, raises `NotImplementedError`.
This is useful mainly in special cases, where `raise` cannot be used for
some reason, and a manually created exception instance needs a traceback.
(The `signal` function in the conditions-and-restarts system uses this.)
Expand Down Expand Up @@ -207,20 +203,17 @@ def equip_with_traceback(exc, stacklevel=1): # Python 3.7+
break

# Python 3.7+ allows creating `types.TracebackType` objects in Python code.
try:
tracebacks = []
nxt = None # tb_next should point toward the level where the exception occurred.
for frame in frames: # walk from top of call stack toward the root
tb = TracebackType(nxt, frame, frame.f_lasti, frame.f_lineno)
tracebacks.append(tb)
nxt = tb
if tracebacks:
tb = tracebacks[-1] # root level
else:
tb = None
except TypeError as err: # Python 3.6 or earlier
raise NotImplementedError("Need Python 3.7 or later to create traceback objects") from err
return exc.with_traceback(tb) # Python 3.7+
tracebacks = []
nxt = None # tb_next should point toward the level where the exception occurred.
for frame in frames: # walk from top of call stack toward the root
tb = TracebackType(nxt, frame, frame.f_lasti, frame.f_lineno)
tracebacks.append(tb)
nxt = tb
if tracebacks:
tb = tracebacks[-1] # root level
else:
tb = None
return exc.with_traceback(tb)

# TODO: To reduce the risk of spaghetti user code, we could require a non-main thread's entrypoint to declare
# via a decorator that it's willing to accept asynchronous exceptions, and check that mark here, making this
Expand Down
27 changes: 5 additions & 22 deletions unpythonic/let.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,25 +106,8 @@ def letrec(body, **bindings):
body=lambda e:
e.b * e.f(1)) # --> 84
**CAUTION**:
Simple values (non-callables) may depend on earlier definitions
in the same letrec **only in Python 3.6 and later**.
Until Python 3.6, initialization of the bindings occurs
**in an arbitrary order**, because of the ``kwargs`` mechanism.
See PEP 468:
https://www.python.org/dev/peps/pep-0468/
In Python < 3.6, in the first example above, trying to reference ``env.a``
on the RHS of ``b`` may get either the ``lambda e: ...``, or the value ``1``,
depending on whether the binding ``a`` has been initialized at that point or not.
If you need left-to-right initialization of bindings in Python < 3.6,
see ``unpythonic.lispylet``.
The following applies regardless of Python version.
Simple values (non-callables) may depend on earlier definitions
in the same letrec.
A callable value may depend on **any** binding, also later ones. This allows
mutually recursive functions::
Expand All @@ -151,9 +134,9 @@ def letrec(body, **bindings):
L = [1, 1, 3, 1, 3, 2, 3, 2, 2, 2, 4, 4, 1, 2, 3]
print(u(L)) # [1, 3, 2, 4]
Works also in Python < 3.6, because here ``see`` is a callable. Hence, ``e.seen``
doesn't have to exist when the *definition* of ``see`` is evaluated; it only has to
exist when ``e.see(x)`` is *called*.
Note that ``see`` is a callable. Hence, strictly speaking it doesn't matter
if ``e.seen`` exists when the *definition* of ``see`` is evaluated; it only
has to exist when ``e.see(x)`` is *called*.
Parameters:
`body`: function
Expand Down
2 changes: 1 addition & 1 deletion unpythonic/syntax/astcompat.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
"""Conditionally import AST node types only supported by recent enough Python versions (3.7+)."""
"""Conditionally import AST node types only supported by recent enough Python versions."""

__all__ = ["NamedExpr",
"Num", "Str", "Bytes", "NameConstant", "Ellipsis",
Expand Down
2 changes: 1 addition & 1 deletion unpythonic/syntax/scopeanalyzer.py
Original file line number Diff line number Diff line change
Expand Up @@ -369,7 +369,7 @@ class DelNamesCollector(ASTVisitor):
def examine(self, tree):
# We want to detect things like "del x":
# Delete(targets=[Name(id='x', ctx=Del()),])
# We don't currently care about "del myobj.x" or "del mydict['x']" (these examples in Python 3.6):
# We don't currently care about "del myobj.x" or "del mydict['x']" (these old examples in Python 3.6):
# Delete(targets=[Attribute(value=Name(id='myobj', ctx=Load()), attr='x', ctx=Del()),])
# Delete(targets=[Subscript(value=Name(id='mydict', ctx=Load()), slice=Index(value=Str(s='x')), ctx=Del()),])
if type(tree) is Name and hasattr(tree, "ctx") and type(tree.ctx) is Del:
Expand Down
5 changes: 2 additions & 3 deletions unpythonic/syntax/tests/test_scopeanalyzer.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,8 @@ def sleep():

# Assignment
#
# At least up to Python 3.7, all assignments produce Name nodes in
# Store context on their LHS, so we don't need to care what kind of
# assignment it is.
# All assignments produce Name nodes in #tore context on their LHS,
# so we don't need to care what kind of assignment it is.
test[get_names_in_store_context(getnames_store_simple) == ["x"]]
with q as getnames_tuple:
x, y = 1, 2 # noqa: F841
Expand Down
8 changes: 0 additions & 8 deletions unpythonic/tests/test_arity.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,14 +104,6 @@ def instmeth(self):
test[arities(target.classmeth) == (1, 1)]
test[arities(target.staticmeth) == (1, 1)]

# Methods of builtin types have uninspectable arity up to Python 3.6.
# Python 3.7 seems to fix this at least for `list`, and PyPy3 (7.3.0; Python 3.6.9)
# doesn't have this error either.
if sys.version_info < (3, 7, 0) and sys.implementation.name == "cpython": # pragma: no cover
with testset("uninspectable builtin methods"):
lst = []
test_raises[UnknownArity, arities(lst.append)]

# resolve_bindings: resolve parameter bindings established by a function
# when it is called with the given args and kwargs.
#
Expand Down
1 change: 0 additions & 1 deletion unpythonic/tests/test_conditions.py
Original file line number Diff line number Diff line change
Expand Up @@ -408,7 +408,6 @@ def warn_protocol():
# An unhandled `error` or `cerror`, when it **raises** `ControlError`,
# sets the cause of that `ControlError` to the original unhandled signal.
# In Python 3.7+, this will also produce nice stack traces.
# In up to Python 3.6, it will at least show the chain of causes.
with catch_signals(False):
try:
exc1 = JustTesting("Hullo")
Expand Down
2 changes: 1 addition & 1 deletion unpythonic/tests/test_dispatch.py
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,7 @@ def blubnify2(x: float, y: float):

with testset("list_methods"):
def check_formatted_multimethods(result, expected):
def _remove_space_before_typehint(string): # Python 3.6 doesn't print a space there
def _remove_space_before_typehint(string): # Python 3.6 didn't print a space there, later versions do
return string.replace(": ", ":")
result_list = result.split("\n")
human_readable_header, *multimethod_descriptions = result_list
Expand Down
9 changes: 2 additions & 7 deletions unpythonic/tests/test_excutil.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,13 +78,8 @@ def runtests():

with testset("equip_with_traceback"):
e = Exception("just testing")
try:
e = equip_with_traceback(e)
except NotImplementedError:
warn["equip_with_traceback only supported on Python 3.7+, skipping test."]
else:
# Can't do meaningful testing on the result, so just check it's there.
test[e.__traceback__ is not None]
e = equip_with_traceback(e)
test[e.__traceback__ is not None] # Can't do meaningful testing on the result, so just check it's there.

test_raises[TypeError, equip_with_traceback("not an exception")]

Expand Down
2 changes: 1 addition & 1 deletion unpythonic/tests/test_symbol.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ def runtests():
# Symbol interning has nothing to do with string interning.
many = 5000
test[the[sym("λ" * many) is sym("λ" * many)]]
# To defeat string interning, used to be that 80 exotic characters
# To defeat string interning, it used to be that 80 exotic characters
# would be enough in Python 3.6 to make CPython decide not to intern it,
# but Python 3.7 bumped that up.
test[the["λ" * many is not "λ" * many]]
Expand Down
20 changes: 8 additions & 12 deletions unpythonic/typecheck.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,8 @@
import sys
import typing

try:
_MyGenericAlias = typing._GenericAlias # Python 3.7+
except AttributeError: # Python 3.6 and earlier # pragma: no cover
class _MyGenericAlias: # unused, but must be a class to support isinstance() check.
pass

try:
_MySupportsIndex = typing.SupportsIndex # Python 3.8+
except AttributeError: # Python 3.7 and earlier # pragma: no cover
class _MySupportsIndex: # unused, but must be a class to support isinstance() check.
pass
_MyGenericAlias = typing._GenericAlias # Python 3.7+
_MySupportsIndex = typing.SupportsIndex # Python 3.8+

from .misc import safeissubclass

Expand Down Expand Up @@ -111,6 +102,11 @@ def isoftype(value, T):
# TODO: as of Python 3.8 (March 2020). https://docs.python.org/3/library/typing.html
# TODO: If you add a feature to the type checker, please update this list.
#
# TODO: Update this list for Python 3.9
# TODO: Update this list for Python 3.10
# TODO: Update this list for Python 3.11
# TODO: Update this list for Python 3.12
#
# Python 3.6+:
# NamedTuple, DefaultDict, Counter, ChainMap,
# IO, TextIO, BinaryIO,
Expand Down Expand Up @@ -190,7 +186,7 @@ def isNewType(T):
# In Python 3.10, an instance of `typing.NewType` is now actually such and not just a function. Nice!
if sys.version_info >= (3, 10, 0):
return isinstance(T, typing.NewType)
# Python 3.6 through Python 3.9
# Python 3.6, Python 3.7, Python 3.8, Python 3.9
# TODO: in Python 3.7+, what is the mysterious callable that doesn't have a `__qualname__`?
return callable(T) and hasattr(T, "__qualname__") and T.__qualname__ == "NewType.<locals>.new_type"
if isNewType(T):
Expand Down

0 comments on commit 9660f4b

Please sign in to comment.