Skip to content

Commit

Permalink
Python 3.7 last-rites x 2.
Browse files Browse the repository at this point in the history
This commit is the next in a commit chain **last-riting** (i.e.,
permanently removing) support for Python 3.7, which recently hit its
official End-of-Life (EoL) date and thus constitutes a security risk.
Since Python 3.7 substantially differed from Python 3.8 with respect to
type hints in general and the standard `typing` module in specific, this
commit chain is likely to take longer than anyone wants or expects. This
is why @leycec does @beartype: so you don't have to. (*Inundated nuns date Inuit, innit?*)
  • Loading branch information
leycec committed Sep 8, 2023
1 parent ddf6233 commit d4e0aaa
Show file tree
Hide file tree
Showing 22 changed files with 1,079 additions and 1,312 deletions.
195 changes: 92 additions & 103 deletions beartype/_check/code/codemake.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
# See "LICENSE" for further details.

'''
**Beartype type-checking code factories** (i.e., low-level callables dynamically
Beartype **type-checking code factories** (i.e., low-level callables dynamically
generating pure-Python code snippets type-checking arbitrary objects against
PEP-compliant type hints).
Expand Down Expand Up @@ -138,7 +138,6 @@
from beartype._check.convert.convsanify import sanify_hint_any
from beartype._util.hint.utilhinttest import is_hint_ignorable
from beartype._util.kind.utilkinddict import update_mapping
from beartype._util.py.utilpyversion import IS_PYTHON_AT_LEAST_3_8
from beartype._util.text.utiltextmagic import (
CODE_INDENT_1,
CODE_INDENT_2,
Expand Down Expand Up @@ -380,7 +379,7 @@ class variable or method annotated by this hint *or* :data:`None`).
# visited hint (e.g., "(int, str)" if "hint_curr == Union[int, str]").
hint_childs: tuple = None # type: ignore[assignment]

# Current list of all output PEP-compliant child hints to replace the
# Current list of all output PEP-compliant child hints to replace the
# Current tuple of all input PEP-compliant child hints subscripting the
# currently visited hint with.
hint_childs_new: list = None # type: ignore[assignment]
Expand Down Expand Up @@ -985,109 +984,99 @@ def _enqueue_hint_child(pith_child_expr: str) -> str:
#* In the "beartype._decor.decormain" submodule:
# * Do... something? Oh, boy. Why didn't we finish this comment?

# If the active Python interpreter targets Python >= 3.8 and
# thus supports assignment expressions...
if IS_PYTHON_AT_LEAST_3_8:
# If...
if (
# The current pith is not the root pith *AND*...
#
# Note that we explicitly test against piths rather
# than seemingly equivalent metadata to account for
# edge cases. Notably, child hints of unions (and
# possibly other "typing" objects) do *NOT* narrow the
# current pith and are *NOT* the root hint. Ergo, a
# seemingly equivalent test like "hints_meta_index_curr
# != 0" would generate false positives and thus
# unnecessarily inefficient code.
pith_curr_expr is not VAR_NAME_PITH_ROOT and

#FIXME: Overly ambiguous, unfortunately. This suffices
#for now but absolutely *WILL* fail with inscrutable
#errors under some future release. The issue is that
#this trivial test reports false negatives for
#sufficiently complex "pith_curr_expr" strings.
#
#For example, if "pith_curr_expr ==
#'(yam := yum[0])[1]'", the detection below would
#incorrectly detect that as being an assignment
#expression. It isn't. It *CONTAINS* an embedded
#assignment expression, but it itself is *NOT* an
#assignment expression. Ergo, that "pith_curr_expr"
#should be assigned via an assignment expression here.
#
#To handle embedded assignment expressions like that,
#we'll probably need to generalize this yet again:
#* Define a new "HINT_META_INDEX_IS_PITH_EXPR_ASSIGN"
# global.
#* Define a new "is_pith_curr_expr_assign" local,
# "True" only if "pith_curr_expr" itself is an
# assignment expression, defaulting to "False":
# is_pith_curr_expr_assign = False
#* Assign above:
# is_pith_curr_expr_assign = hint_curr_meta[
# HINT_META_INDEX_IS_PITH_EXPR_ASSIGN]
#* Assign below in the body of this "if" conditional:
# is_pith_curr_expr_assign = True
#* Assign below in the body of this "else" branch:
# is_pith_curr_expr_assign = False
#* Pass "is_pith_curr_expr_assign" in the
# _enqueue_hint_child() closure above.
#* Replace this "':=' not in pith_curr_expr" test here
# with "not is_pith_curr_expr_assign" instead.
#
#Voila! What could be simpler? O_o
# If...
if (
# The current pith is not the root pith *AND*...
#
# Note that we explicitly test against piths rather than
# seemingly equivalent metadata to account for edge cases.
# Notably, child hints of unions (and possibly other
# "typing" objects) do *NOT* narrow the current pith and are
# *NOT* the root hint. Ergo, a seemingly equivalent test
# like "hints_meta_index_curr != 0" would generate false
# positives and thus unnecessarily inefficient code.
pith_curr_expr is not VAR_NAME_PITH_ROOT and

#FIXME: Overly ambiguous, unfortunately. This suffices
#for now but absolutely *WILL* fail with inscrutable
#errors under some future release. The issue is that
#this trivial test reports false negatives for
#sufficiently complex "pith_curr_expr" strings.
#
#For example, if "pith_curr_expr ==
#'(yam := yum[0])[1]'", the detection below would
#incorrectly detect that as being an assignment
#expression. It isn't. It *CONTAINS* an embedded
#assignment expression, but it itself is *NOT* an
#assignment expression. Ergo, that "pith_curr_expr"
#should be assigned via an assignment expression here.
#
#To handle embedded assignment expressions like that,
#we'll probably need to generalize this yet again:
#* Define a new "HINT_META_INDEX_IS_PITH_EXPR_ASSIGN"
# global.
#* Define a new "is_pith_curr_expr_assign" local,
# "True" only if "pith_curr_expr" itself is an
# assignment expression, defaulting to "False":
# is_pith_curr_expr_assign = False
#* Assign above:
# is_pith_curr_expr_assign = hint_curr_meta[
# HINT_META_INDEX_IS_PITH_EXPR_ASSIGN]
#* Assign below in the body of this "if" conditional:
# is_pith_curr_expr_assign = True
#* Assign below in the body of this "else" branch:
# is_pith_curr_expr_assign = False
#* Pass "is_pith_curr_expr_assign" in the
# _enqueue_hint_child() closure above.
#* Replace this "':=' not in pith_curr_expr" test here
# with "not is_pith_curr_expr_assign" instead.
#
#Voila! What could be simpler? O_o

# The current pith expression does *NOT* already
# perform an assignment expression...
#
# If the current pith expression already performs an
# assignment expression, there's no benefit to
# assigning that to another local variable via another
# assignment expression, which would just be an alias
# of the existing local variable assigned via the
# existing assignment expression. Moreover, whereas
# chained assignments are syntactically valid, chained
# assignment expressions are syntactically invalid
# unless protected with parens:
# >>> a = b = 'Mother*Teacher*Destroyer' # <-- fine
# >>> (a := "Mother's Abomination") # <-- fine
# >>> (a := (b := "Mother's Illumination")) # <-- fine
# >>> (a := b := "Mother's Illumination") # <-- not fine
# SyntaxError: invalid syntax
':=' not in pith_curr_expr
):
# Then all conditions needed to assign the current pith to a
# unique local variable via a Python >= 3.8-specific
# assignment expression are satisfied. In this case...
# Increment the integer suffixing the name of this
# variable *BEFORE* defining this local variable.
pith_curr_assign_expr_name_counter += 1

# Reduce the current pith expression to the name of
# this local variable.
pith_curr_var_name = (
f'{VAR_NAME_PREFIX_PITH}'
f'{pith_curr_assign_expr_name_counter}'
)
# The current pith expression does *NOT* already perform an
# assignment expression...
#
# If the current pith expression already performs an
# assignment expression, there's no benefit to assigning
# that to another local variable via another assignment
# expression, which would just be an alias of the existing
# local variable assigned via the existing assignment
# expression. Moreover, whereas chained assignments are
# syntactically valid, chained assignment expressions are
# syntactically invalid unless protected with parens:
# >>> a = b = 'Mother*Teacher*Destroyer' # <-- fine
# >>> (a := "Mother's Abomination") # <-- fine
# >>> (a := (b := "Mother's Illumination")) # <-- fine
# >>> (a := b := "Mother's Illumination") # <-- not fine
# SyntaxError: invalid syntax
':=' not in pith_curr_expr
):
# Then all conditions needed to assign the current pith to a
# unique local variable via an assignment expression are
# satisfied. In this case...
# Increment the integer suffixing the name of this variable
# *BEFORE* defining this local variable.
pith_curr_assign_expr_name_counter += 1

# Reduce the current pith expression to the name of this
# local variable.
pith_curr_var_name = (
f'{VAR_NAME_PREFIX_PITH}'
f'{pith_curr_assign_expr_name_counter}'
)

# Python >= 3.8-specific assignment expression
# assigning this full expression to this variable.
pith_curr_assign_expr = (
PEP_CODE_PITH_ASSIGN_EXPR_format(
pith_curr_var_name=pith_curr_var_name,
pith_curr_expr=pith_curr_expr,
))
# Else, one or more of the above conditions have *NOT* been
# satisfied. In this case, preserve the Python code snippet
# evaluating to the current pith as is.
else:
pith_curr_assign_expr = pith_curr_expr
# Else, the active Python interpreter targets Python < 3.8 and
# thus does *NOT* support assignment expressions. In this case,
# assign the variables assigned above to sane expressions.
# Assignment expression assigning this full expression to
# this variable.
pith_curr_assign_expr = (
PEP_CODE_PITH_ASSIGN_EXPR_format(
pith_curr_var_name=pith_curr_var_name,
pith_curr_expr=pith_curr_expr,
))
# Else, one or more of the above conditions have *NOT* been
# satisfied. In this case, preserve the Python code snippet
# evaluating to the current pith as is.
else:
pith_curr_assign_expr = pith_curr_var_name = pith_curr_expr
pith_curr_assign_expr = pith_curr_expr

# Tuple of all arguments subscripting this hint if any *OR* the
# empty tuple otherwise (e.g., if this hint is its own
Expand Down
11 changes: 3 additions & 8 deletions beartype/_util/ast/utilastmunge.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
# See "LICENSE" for further details.

'''
Beartype **abstract syntax tree (AST) mungers** (i.e., low-level callables
Project-wide **abstract syntax tree (AST) mungers** (i.e., low-level callables
modifying various properties of various nodes in the currently visited AST).
This private submodule is *not* intended for importation by downstream callers.
Expand All @@ -16,7 +16,6 @@
Iterable,
Union,
)
from beartype._util.py.utilpyversion import IS_PYTHON_AT_LEAST_3_8

# ....................{ COPIERS }....................
#FIXME: Unit test us up, please.
Expand Down Expand Up @@ -107,9 +106,5 @@ def copy_node_metadata(
# Copy all source code metadata from this source to target node.
node_trg_cur.lineno = node_src.lineno
node_trg_cur.col_offset = node_src.col_offset

# If the active Python interpreter targets Python >= 3.8, then also copy
# all source code metadata exposed by Python >= 3.8.
if IS_PYTHON_AT_LEAST_3_8:
node_trg_cur.end_lineno = node_src.end_lineno # type: ignore[attr-defined]
node_trg_cur.end_col_offset = node_src.end_col_offset # type: ignore[attr-defined]
node_trg_cur.end_lineno = node_src.end_lineno # type: ignore[attr-defined]
node_trg_cur.end_col_offset = node_src.end_col_offset # type: ignore[attr-defined]
73 changes: 25 additions & 48 deletions beartype/_util/cls/pep/utilpep557.py
Original file line number Diff line number Diff line change
@@ -1,63 +1,24 @@
#!/usr/bin/env python3
# --------------------( LICENSE )--------------------
# --------------------( LICENSE )--------------------
# Copyright (c) 2014-2023 Beartype authors.
# See "LICENSE" for further details.

'''
Project-wide :pep:`557`-compliant **class tester** (i.e., callable testing
various properties of dataclasses standardized by :pep:`557`) utilities.
Project-wide :pep:`557`-compliant **testers** (i.e., low-level callables testing
various properties of dataclasses standardized by :pep:`557`).
This private submodule is *not* intended for importation by downstream callers.
'''

# ....................{ IMPORTS }....................
from beartype._util.py.utilpyversion import IS_PYTHON_AT_LEAST_3_8
from dataclasses import is_dataclass

# ....................{ TESTERS }....................
# If the active Python interpreter targets Python >= 3.8 and thus supports PEP
# 557-compliant dataclasses, define this tester properly.
if IS_PYTHON_AT_LEAST_3_8:
# Defer version-specific imports.
from dataclasses import is_dataclass

def is_type_pep557(cls: type) -> bool:

# Avoid circular import dependencies.
from beartype._util.cls.utilclstest import die_unless_type

# If this object is *NOT* a type, raise an exception.
die_unless_type(cls)
# Else, this object is a type.

# Return true only if this type is a dataclass.
#
# Note that the is_dataclass() tester was intentionally implemented
# ambiguously to return true for both actual dataclasses *AND*
# instances of dataclasses. Since the prior validation omits the
# latter, this call unambiguously returns true *ONLY* if this object is
# an actual dataclass. (Dodged a misfired bullet there, folks.)
return is_dataclass(cls)
# Else, the active Python interpreter targets Python < 3.8 and thus fails to
# support PEP 557-compliant dataclasses. In this case, reduce this tester to
# unconditionally return false.
else:
def is_type_pep557(cls: type) -> bool:

# Avoid circular import dependencies.
from beartype._util.cls.utilclstest import die_unless_type

# If this object is *NOT* a type, raise an exception.
die_unless_type(cls)
# Else, this object is a type.

# Unconditionally return false.
return False


is_type_pep557.__doc__ = '''
``True`` only if the passed class is a **dataclass** (i.e.,
def is_type_pep557(cls: type) -> bool:
'''
:data:`True` only if the passed class is a **dataclass** (i.e.,
:pep:`557`-compliant class decorated by the standard
:func:`dataclasses.dataclass` decorator introduced by Python 3.8.0).
:func:`dataclasses.dataclass` decorator).
This tester is intentionally *not* memoized (e.g., by the
:func:`callable_cached` decorator), as the implementation trivially reduces
Expand All @@ -71,10 +32,26 @@ def is_type_pep557(cls: type) -> bool:
Returns
----------
bool
``True`` only if this class is a dataclass.
:data:`True` only if this class is a dataclass.
Raises
----------
_BeartypeUtilTypeException
If this object is *not* a class.
'''

# Avoid circular import dependencies.
from beartype._util.cls.utilclstest import die_unless_type

# If this object is *NOT* a type, raise an exception.
die_unless_type(cls)
# Else, this object is a type.

# Return true only if this type is a dataclass.
#
# Note that the is_dataclass() tester was intentionally implemented
# ambiguously to return true for both actual dataclasses *AND*
# instances of dataclasses. Since the prior validation omits the
# latter, this call unambiguously returns true *ONLY* if this object is
# an actual dataclass. (Dodged a misfired bullet there, folks.)
return is_dataclass(cls)
6 changes: 3 additions & 3 deletions beartype/_util/func/arg/utilfuncargiter.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@
# See "LICENSE" for further details.

'''
Project-wide **callable parameter iterator utilities** (i.e., callables
introspectively iterating over parameters accepted by arbitrary callables).
Project-wide **callable parameter iterator utilities** (i.e., low-level
callables introspectively iterating over parameters accepted by arbitrary
callables).
This private submodule is *not* intended for importation by downstream callers.
'''
Expand All @@ -25,7 +26,6 @@
from beartype._data.kind.datakinddict import DICT_EMPTY
from beartype._util.func.utilfunccodeobj import get_func_codeobj
from beartype._util.func.utilfuncwrap import unwrap_func_all_closures_isomorphic
from beartype._util.py.utilpyversion import IS_PYTHON_AT_LEAST_3_8
from collections.abc import Callable
from enum import (
Enum,
Expand Down

0 comments on commit d4e0aaa

Please sign in to comment.