diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ace0d07..3dc2273 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -25,7 +25,6 @@ jobs: config: # [Python version, tox env] - ["3.11", "release-check"] - - ["3.9", "py39"] - ["3.10", "py310"] - ["3.11", "py311"] - ["3.12", "py312"] diff --git a/.meta.toml b/.meta.toml index 04360f6..f919277 100644 --- a/.meta.toml +++ b/.meta.toml @@ -2,7 +2,7 @@ # https://github.com/zopefoundation/meta/tree/master/config/pure-python [meta] template = "pure-python" -commit-id = "72252845" +commit-id = "9d049229" [python] with-pypy = false @@ -11,6 +11,7 @@ with-sphinx-doctests = true with-windows = true with-future-python = true with-macos = false +oldest-python = "3.10" [tox] use-flake8 = true @@ -47,7 +48,7 @@ testenv-additional = [ " coverage combine", " coverage html", " coverage report -m --fail-under=100", - "depends = py39,py310,py311,py311-datetime,py312,py313,py314,coverage", + "depends = py310,py311,py311-datetime,py312,py313,py314,coverage", ] coverage-command = "pytest --cov=src --cov=tests --cov-report= tests {posargs}" coverage-setenv = [ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index cbb2541..5f86332 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,7 +15,7 @@ repos: rev: v3.21.0 hooks: - id: pyupgrade - args: [--py39-plus] + args: [--py310-plus] - repo: https://github.com/isidentical/teyit rev: 0.4.3 hooks: diff --git a/CHANGES.rst b/CHANGES.rst index eddc785..9a50d66 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,7 +4,9 @@ Changes 8.2 (unreleased) ---------------- -- Nothing changed yet. +- Add type annotations to the package code. + For clarification, restricted Python code does not support type annotations. + 8.1 (2025-10-19) ---------------- diff --git a/docs/contributing/index.rst b/docs/contributing/index.rst index 69909e4..1a5cbdc 100644 --- a/docs/contributing/index.rst +++ b/docs/contributing/index.rst @@ -241,7 +241,6 @@ Technical Backgrounds - Links to External Documentation * `Python 3.12 AST`_ (EOL 2028-10) * `Python 3.11 AST`_ (EOL 2027-10) * `Python 3.10 AST`_ (EOL 2026-10) - * `Python 3.9 AST`_ (EOL 2025-10) * `AST NodeVistiors Class`_ * `AST NodeTransformer Class`_ diff --git a/setup.py b/setup.py index 9217802..b8b6f65 100644 --- a/setup.py +++ b/setup.py @@ -39,7 +39,6 @@ def read(*rnames): 'Programming Language :: Python', 'Operating System :: OS Independent', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.12', @@ -47,10 +46,13 @@ def read(*rnames): 'Programming Language :: Python :: 3.14', 'Programming Language :: Python :: Implementation :: CPython', 'Topic :: Security', + 'Typing :: Typed', ], keywords='restricted execution security untrusted code', author='Zope Foundation and Contributors', author_email='zope-dev@zope.dev', + maintainer='Zope Foundation, Plone Foundation and Contributors', + maintainer_email='security@plone.org', project_urls={ "Documentation": "https://restrictedpython.readthedocs.io/", "Source": "https://github.com/zopefoundation/RestrictedPython", @@ -60,9 +62,10 @@ def read(*rnames): packages=find_packages('src'), package_dir={'': 'src'}, install_requires=[], - python_requires=">=3.9, <3.15", + python_requires=">=3.10, <3.15", extras_require={ 'test': ['pytest', 'pytest-mock'], + 'typecheck': ['mypy', 'typeshed'], 'docs': ['Sphinx', 'furo'], }, include_package_data=True, diff --git a/src/RestrictedPython/Utilities.py b/src/RestrictedPython/Utilities.py index 26d73d1..b966b4e 100644 --- a/src/RestrictedPython/Utilities.py +++ b/src/RestrictedPython/Utilities.py @@ -14,6 +14,7 @@ import math import random import string +from collections.abc import Iterable utility_builtins = {} @@ -75,7 +76,8 @@ def test(*args): utility_builtins['test'] = test -def reorder(s, with_=None, without=()): +def reorder(s: Iterable, with_: Iterable | None = None, + without: Iterable = ()) -> Iterable: # s, with_, and without are sequences treated as sets. # The result is subtract(intersect(s, with_), without), # unless with_ is None, in which case it is subtract(s, without). diff --git a/src/RestrictedPython/compile.py b/src/RestrictedPython/compile.py index 3253b8c..8d29f3a 100644 --- a/src/RestrictedPython/compile.py +++ b/src/RestrictedPython/compile.py @@ -1,11 +1,24 @@ +from __future__ import annotations + import ast import warnings +from ast import Expression +from ast import Interactive +from ast import Module +from ast import NodeTransformer from collections import namedtuple +from os import PathLike +from typing import Any +from typing import Literal +from typing import TypeAlias from RestrictedPython._compat import IS_CPYTHON from RestrictedPython.transformer import RestrictingNodeTransformer +# Temporary workaround for missing _typeshed +ReadableBuffer: TypeAlias = bytes | bytearray + CompileResult = namedtuple( 'CompileResult', 'code, errors, warnings, used_names') syntax_error_template = ( @@ -18,12 +31,13 @@ def _compile_restricted_mode( - source, - filename='', - mode="exec", - flags=0, - dont_inherit=False, - policy=RestrictingNodeTransformer): + source: str | ReadableBuffer | Module | Expression | Interactive, + filename: str | ReadableBuffer | PathLike[Any] = '', + mode: Literal["exec", "eval", "single"] = "exec", + flags: int = 0, + dont_inherit: bool = False, + policy: NodeTransformer = RestrictingNodeTransformer, +) -> CompileResult: if not IS_CPYTHON: warnings.warn_explicit( @@ -39,13 +53,13 @@ def _compile_restricted_mode( dont_inherit=dont_inherit) elif issubclass(policy, RestrictingNodeTransformer): c_ast = None - allowed_source_types = [str, ast.Module] + allowed_source_types = [str, Module] if not issubclass(type(source), tuple(allowed_source_types)): raise TypeError('Not allowed source type: ' '"{0.__class__.__name__}".'.format(source)) c_ast = None # workaround for pypy issue https://bitbucket.org/pypy/pypy/issues/2552 - if isinstance(source, ast.Module): + if isinstance(source, Module): c_ast = source else: try: @@ -78,11 +92,12 @@ def _compile_restricted_mode( def compile_restricted_exec( - source, - filename='', - flags=0, - dont_inherit=False, - policy=RestrictingNodeTransformer): + source: str | ReadableBuffer | Module | Expression | Interactive, + filename: str | ReadableBuffer | PathLike[Any] = '', + flags: int = 0, + dont_inherit: bool = False, + policy: NodeTransformer = RestrictingNodeTransformer, +) -> CompileResult: """Compile restricted for the mode `exec`.""" return _compile_restricted_mode( source, @@ -94,11 +109,12 @@ def compile_restricted_exec( def compile_restricted_eval( - source, - filename='', - flags=0, - dont_inherit=False, - policy=RestrictingNodeTransformer): + source: str | ReadableBuffer | Module | Expression | Interactive, + filename: str | ReadableBuffer | PathLike[Any] = '', + flags: int = 0, + dont_inherit: bool = False, + policy: NodeTransformer = RestrictingNodeTransformer, +) -> CompileResult: """Compile restricted for the mode `eval`.""" return _compile_restricted_mode( source, @@ -110,11 +126,12 @@ def compile_restricted_eval( def compile_restricted_single( - source, - filename='', - flags=0, - dont_inherit=False, - policy=RestrictingNodeTransformer): + source: str | ReadableBuffer | Module | Expression | Interactive, + filename: str | ReadableBuffer | PathLike[Any] = '', + flags: int = 0, + dont_inherit: bool = False, + policy: NodeTransformer = RestrictingNodeTransformer, +) -> CompileResult: """Compile restricted for the mode `single`.""" return _compile_restricted_mode( source, @@ -128,12 +145,13 @@ def compile_restricted_single( def compile_restricted_function( p, # parameters body, - name, - filename='', + name: str, + filename: str | ReadableBuffer | PathLike[Any] = '', globalize=None, # List of globals (e.g. ['here', 'context', ...]) - flags=0, - dont_inherit=False, - policy=RestrictingNodeTransformer): + flags: int = 0, + dont_inherit: bool = False, + policy: ast.NodeTransformer = RestrictingNodeTransformer, +) -> CompileResult: """Compile a restricted code object for a function. Documentation see: @@ -181,12 +199,13 @@ def compile_restricted_function( def compile_restricted( - source, - filename='', - mode='exec', - flags=0, - dont_inherit=False, - policy=RestrictingNodeTransformer): + source: str | ReadableBuffer | Module | Expression | Interactive, + filename: str | ReadableBuffer | PathLike[Any] = '', + mode: str = 'exec', + flags: int = 0, + dont_inherit: bool = False, + policy: NodeTransformer = RestrictingNodeTransformer, +) -> CompileResult: """Replacement for the built-in compile() function. policy ... `ast.NodeTransformer` class defining the restrictions. diff --git a/src/RestrictedPython/transformer.py b/src/RestrictedPython/transformer.py index fa71e48..cbb0d59 100644 --- a/src/RestrictedPython/transformer.py +++ b/src/RestrictedPython/transformer.py @@ -115,7 +115,7 @@ # When new ast nodes are generated they have no 'lineno', 'end_lineno', # 'col_offset' and 'end_col_offset'. This function copies these fields from the # incoming node: -def copy_locations(new_node, old_node): +def copy_locations(new_node: ast.AST, old_node: ast.AST) -> None: assert 'lineno' in new_node._attributes new_node.lineno = old_node.lineno @@ -153,7 +153,11 @@ def new_print_scope(self): class RestrictingNodeTransformer(ast.NodeTransformer): - def __init__(self, errors=None, warnings=None, used_names=None): + def __init__(self, + errors: list[str] | None = None, + warnings: list[str] | None = None, + used_names: dict[str, + str] | None = None): super().__init__() self.errors = [] if errors is None else errors self.warnings = [] if warnings is None else warnings @@ -170,26 +174,26 @@ def __init__(self, errors=None, warnings=None, used_names=None): self.print_info = PrintInfo() - def gen_tmp_name(self): + def gen_tmp_name(self) -> str: # 'check_name' ensures that no variable is prefixed with '_'. # => Its safe to use '_tmp..' as a temporary variable. name = '_tmp%i' % self._tmp_idx self._tmp_idx += 1 return name - def error(self, node, info): + def error(self, node: ast.AST, info: str) -> None: """Record a security error discovered during transformation.""" lineno = getattr(node, 'lineno', None) self.errors.append( f'Line {lineno}: {info}') - def warn(self, node, info): - """Record a security error discovered during transformation.""" + def warn(self, node: ast.AST, info: str) -> None: + """Record a security warning discovered during transformation.""" lineno = getattr(node, 'lineno', None) self.warnings.append( f'Line {lineno}: {info}') - def guard_iter(self, node): + def guard_iter(self, node: ast.AST) -> ast.AST: """ Converts: for x in expr @@ -220,10 +224,10 @@ def guard_iter(self, node): node.iter = new_iter return node - def is_starred(self, ob): + def is_starred(self, ob: ast.AST) -> bool: return isinstance(ob, ast.Starred) - def gen_unpack_spec(self, tpl): + def gen_unpack_spec(self, tpl: ast.Tuple) -> ast.Dict: """Generate a specification for 'guarded_unpack_sequence'. This spec is used to protect sequence unpacking. @@ -299,14 +303,18 @@ def gen_unpack_spec(self, tpl): return spec - def protect_unpack_sequence(self, target, value): + def protect_unpack_sequence( + self, + target: ast.Tuple, + value: ast.AST) -> ast.Call: spec = self.gen_unpack_spec(target) return ast.Call( func=ast.Name('_unpack_sequence_', ast.Load()), args=[value, spec, ast.Name('_getiter_', ast.Load())], keywords=[]) - def gen_unpack_wrapper(self, node, target): + def gen_unpack_wrapper(self, node: ast.AST, + target: ast.Tuple) -> tuple[ast.Name, ast.Try]: """Helper function to protect tuple unpacks. node: used to copy the locations for the new nodes. @@ -355,13 +363,13 @@ def gen_unpack_wrapper(self, node, target): return (tmp_target, cleanup) - def gen_none_node(self): + def gen_none_node(self) -> ast.NameConstant: return ast.NameConstant(value=None) - def gen_del_stmt(self, name_to_del): + def gen_del_stmt(self, name_to_del: str) -> ast.Delete: return ast.Delete(targets=[ast.Name(name_to_del, ast.Del())]) - def transform_slice(self, slice_): + def transform_slice(self, slice_: ast.AST) -> ast.AST: """Transform slices into function parameters. ast.Slice nodes are only allowed within a ast.Subscript node. @@ -400,7 +408,7 @@ def transform_slice(self, slice_): args=args, keywords=[]) - elif isinstance(slice_, ast.ExtSlice): + elif isinstance(slice_, (ast.Tuple, ast.ExtSlice)): dims = ast.Tuple([], ast.Load()) for item in slice_.dims: dims.elts.append(self.transform_slice(item)) @@ -410,7 +418,11 @@ def transform_slice(self, slice_): # Index, Slice and ExtSlice are only defined Slice types. raise NotImplementedError(f"Unknown slice type: {slice_}") - def check_name(self, node, name, allow_magic_methods=False): + def check_name( + self, + node: ast.AST, + name: str, + allow_magic_methods: bool = False) -> None: """Check names if they are allowed. If ``allow_magic_methods is True`` names in `ALLOWED_FUNC_NAMES` @@ -435,7 +447,7 @@ def check_name(self, node, name, allow_magic_methods=False): elif name in FORBIDDEN_FUNC_NAMES: self.error(node, f'"{name}" is a reserved name.') - def check_function_argument_names(self, node): + def check_function_argument_names(self, node: ast.FunctionDef) -> None: for arg in node.args.args: self.check_name(node, arg.arg) @@ -448,7 +460,7 @@ def check_function_argument_names(self, node): for arg in node.args.kwonlyargs: self.check_name(node, arg.arg) - def check_import_names(self, node): + def check_import_names(self, node: ast.ImportFrom | ast.Import) -> ast.AST: """Check the names being imported. This is a protection against rebinding dunder names like @@ -465,7 +477,7 @@ def check_import_names(self, node): return self.node_contents_visit(node) - def inject_print_collector(self, node, position=0): + def inject_print_collector(self, node: ast.AST, position: int = 0) -> None: print_used = self.print_info.print_used printed_used = self.print_info.printed_used @@ -498,7 +510,7 @@ def inject_print_collector(self, node, position=0): # Special Functions for an ast.NodeTransformer - def generic_visit(self, node): + def generic_visit(self, node: ast.AST) -> ast.AST: """Reject ast nodes which do not have a corresponding `visit_` method. This is needed to prevent new ast nodes from new Python versions to be @@ -513,18 +525,18 @@ def generic_visit(self, node): ) self.not_allowed(node) - def not_allowed(self, node): + def not_allowed(self, node: ast.AST) -> None: self.error( node, f'{node.__class__.__name__} statements are not allowed.') - def node_contents_visit(self, node): + def node_contents_visit(self, node: ast.AST) -> ast.AST: """Visit the contents of a node.""" return super().generic_visit(node) # ast for Literals - def visit_Constant(self, node): + def visit_Constant(self, node: ast.Constant) -> ast.Constant | None: """Allow constant literals with restriction for Ellipsis. Constant replaces Num, Str, Bytes, NameConstant and Ellipsis in @@ -541,37 +553,40 @@ def visit_Constant(self, node): return return self.node_contents_visit(node) - def visit_Interactive(self, node): + def visit_Interactive(self, node: ast.Interactive) -> ast.AST: """Allow single mode without restrictions.""" return self.node_contents_visit(node) - def visit_List(self, node): + def visit_List(self, node: ast.List) -> ast.AST: """Allow list literals without restrictions.""" return self.node_contents_visit(node) - def visit_Tuple(self, node): + def visit_Tuple(self, node: ast.Tuple) -> ast.AST: """Allow tuple literals without restrictions.""" return self.node_contents_visit(node) - def visit_Set(self, node): + def visit_Set(self, node: ast.Set) -> ast.AST: """Allow set literals without restrictions.""" return self.node_contents_visit(node) - def visit_Dict(self, node): + def visit_Dict(self, node: ast.Dict) -> ast.AST: """Allow dict literals without restrictions.""" return self.node_contents_visit(node) - def visit_FormattedValue(self, node): + def visit_FormattedValue(self, node: ast.FormattedValue) -> ast.AST: """Allow f-strings without restrictions.""" return self.node_contents_visit(node) - def visit_TemplateStr(self, node): + def visit_TemplateStr(self, node: ast.AST) -> ast.AST: """Template strings are allowed by default. As Template strings are a very basic template mechanism, that needs additional rendering logic to be useful, they are not blocked by default. Those rendering logic would be affected by RestrictedPython as well. + + TODO: Change Type Annotation to ast.TemplateStr when + Support for Python 3.13 is dropped. """ return self.node_contents_visit(node) @@ -583,16 +598,19 @@ def visit_Interpolation(self, node): are allowed. As a user has to provide additional rendering logic to make use of Template Strings, the security implications of Interpolations are limited in the context of RestrictedPython. + + TODO: Change Type Annotation to ast.Interpolation when + Support for Python 3.13 is dropped. """ return self.node_contents_visit(node) - def visit_JoinedStr(self, node): + def visit_JoinedStr(self, node: ast.JoinedStr) -> ast.AST: """Allow joined string without restrictions.""" return self.node_contents_visit(node) # ast for Variables - def visit_Name(self, node): + def visit_Name(self, node: ast.Name) -> ast.Name | None: """Prevents access to protected names. Converts use of the name 'printed' to this expression: '_print()' @@ -626,25 +644,25 @@ def visit_Name(self, node): self.check_name(node, node.id) return node - def visit_Load(self, node): + def visit_Load(self, node: ast.Load) -> ast.Load | None: """ """ return self.node_contents_visit(node) - def visit_Store(self, node): + def visit_Store(self, node: ast.Store) -> ast.AST: """ """ return self.node_contents_visit(node) - def visit_Del(self, node): + def visit_Del(self, node: ast.Del) -> ast.Del: """ """ return self.node_contents_visit(node) - def visit_Starred(self, node): + def visit_Starred(self, node: ast.Starred) -> ast.AST: """ """ @@ -652,18 +670,18 @@ def visit_Starred(self, node): # Expressions - def visit_Expression(self, node): + def visit_Expression(self, node: ast.Expression) -> ast.AST: """Allow Expression statements without restrictions. They are in the AST when using the `eval` compile mode. """ return self.node_contents_visit(node) - def visit_Expr(self, node): + def visit_Expr(self, node: ast.Expr) -> ast.AST: """Allow Expr statements (any expression) without restrictions.""" return self.node_contents_visit(node) - def visit_UnaryOp(self, node): + def visit_UnaryOp(self, node: ast.UnaryOp) -> ast.AST: """ UnaryOp (Unary Operations) is the overall element for: * Not --> which should be allowed @@ -672,135 +690,135 @@ def visit_UnaryOp(self, node): """ return self.node_contents_visit(node) - def visit_UAdd(self, node): + def visit_UAdd(self, node: ast.UAdd) -> ast.AST: """Allow positive notation of variables. (e.g. +var)""" return self.node_contents_visit(node) - def visit_USub(self, node): + def visit_USub(self, node: ast.USub) -> ast.AST: """Allow negative notation of variables. (e.g. -var)""" return self.node_contents_visit(node) - def visit_Not(self, node): + def visit_Not(self, node: ast.Not) -> ast.AST: """Allow the `not` operator.""" return self.node_contents_visit(node) - def visit_Invert(self, node): + def visit_Invert(self, node: ast.Invert) -> ast.AST: """Allow `~` expressions.""" return self.node_contents_visit(node) - def visit_BinOp(self, node): + def visit_BinOp(self, node: ast.BinOp) -> ast.AST: """Allow binary operations.""" return self.node_contents_visit(node) - def visit_Add(self, node): + def visit_Add(self, node: ast.Add) -> ast.AST: """Allow `+` expressions.""" return self.node_contents_visit(node) - def visit_Sub(self, node): + def visit_Sub(self, node: ast.Sub) -> ast.AST: """Allow `-` expressions.""" return self.node_contents_visit(node) - def visit_Mult(self, node): + def visit_Mult(self, node: ast.Mult) -> ast.AST: """Allow `*` expressions.""" return self.node_contents_visit(node) - def visit_Div(self, node): + def visit_Div(self, node: ast.Div) -> ast.AST: """Allow `/` expressions.""" return self.node_contents_visit(node) - def visit_FloorDiv(self, node): + def visit_FloorDiv(self, node: ast.FloorDiv) -> ast.AST: """Allow `//` expressions.""" return self.node_contents_visit(node) - def visit_Mod(self, node): + def visit_Mod(self, node: ast.Mod) -> ast.AST: """Allow `%` expressions.""" return self.node_contents_visit(node) - def visit_Pow(self, node): + def visit_Pow(self, node: ast.Pow) -> ast.AST: """Allow `**` expressions.""" return self.node_contents_visit(node) - def visit_LShift(self, node): + def visit_LShift(self, node: ast.LShift) -> ast.AST: """Allow `<<` expressions.""" return self.node_contents_visit(node) - def visit_RShift(self, node): + def visit_RShift(self, node: ast.RShift) -> ast.AST: """Allow `>>` expressions.""" return self.node_contents_visit(node) - def visit_BitOr(self, node): + def visit_BitOr(self, node: ast.BitOr) -> ast.AST: """Allow `|` expressions.""" return self.node_contents_visit(node) - def visit_BitXor(self, node): + def visit_BitXor(self, node: ast.BitXor) -> ast.AST: """Allow `^` expressions.""" return self.node_contents_visit(node) - def visit_BitAnd(self, node): + def visit_BitAnd(self, node: ast.BitAnd) -> ast.AST: """Allow `&` expressions.""" return self.node_contents_visit(node) - def visit_MatMult(self, node): + def visit_MatMult(self, node: ast.MatMult) -> ast.AST: """Allow multiplication (`@`).""" return self.node_contents_visit(node) - def visit_BoolOp(self, node): + def visit_BoolOp(self, node: ast.BoolOp) -> ast.AST: """Allow bool operator without restrictions.""" return self.node_contents_visit(node) - def visit_And(self, node): + def visit_And(self, node: ast.And) -> ast.AST: """Allow bool operator `and` without restrictions.""" return self.node_contents_visit(node) - def visit_Or(self, node): + def visit_Or(self, node: ast.Or) -> ast.AST: """Allow bool operator `or` without restrictions.""" return self.node_contents_visit(node) - def visit_Compare(self, node): + def visit_Compare(self, node: ast.Compare) -> ast.AST: """Allow comparison expressions without restrictions.""" return self.node_contents_visit(node) - def visit_Eq(self, node): + def visit_Eq(self, node: ast.Eq) -> ast.AST: """Allow == expressions.""" return self.node_contents_visit(node) - def visit_NotEq(self, node): + def visit_NotEq(self, node: ast.NotEq) -> ast.AST: """Allow != expressions.""" return self.node_contents_visit(node) - def visit_Lt(self, node): + def visit_Lt(self, node: ast.Lt) -> ast.AST: """Allow < expressions.""" return self.node_contents_visit(node) - def visit_LtE(self, node): + def visit_LtE(self, node: ast.LtE) -> ast.AST: """Allow <= expressions.""" return self.node_contents_visit(node) - def visit_Gt(self, node): + def visit_Gt(self, node: ast.Gt) -> ast.AST: """Allow > expressions.""" return self.node_contents_visit(node) - def visit_GtE(self, node): + def visit_GtE(self, node: ast.GtE) -> ast.AST: """Allow >= expressions.""" return self.node_contents_visit(node) - def visit_Is(self, node): + def visit_Is(self, node: ast.Is) -> ast.AST: """Allow `is` expressions.""" return self.node_contents_visit(node) - def visit_IsNot(self, node): + def visit_IsNot(self, node: ast.IsNot) -> ast.AST: """Allow `is not` expressions.""" return self.node_contents_visit(node) - def visit_In(self, node): + def visit_In(self, node: ast.In) -> ast.AST: """Allow `in` expressions.""" return self.node_contents_visit(node) - def visit_NotIn(self, node): + def visit_NotIn(self, node: ast.NotIn) -> ast.AST: """Allow `not in` expressions.""" return self.node_contents_visit(node) - def visit_Call(self, node): + def visit_Call(self, node: ast.Call) -> ast.AST: """Checks calls with '*args' and '**kwargs'. Note: The following happens only if '*args' or '**kwargs' is used. @@ -842,17 +860,17 @@ def visit_Call(self, node): copy_locations(node.func, node.args[0]) return node - def visit_keyword(self, node): + def visit_keyword(self, node: ast.keyword) -> ast.AST: """ """ return self.node_contents_visit(node) - def visit_IfExp(self, node): + def visit_IfExp(self, node: ast.IfExp) -> ast.AST: """Allow `if` expressions without restrictions.""" return self.node_contents_visit(node) - def visit_Attribute(self, node): + def visit_Attribute(self, node: ast.Attribute) -> ast.AST: """Checks and mutates attribute access/assignment. 'a.b' becomes '_getattr_(a, "b")' @@ -908,7 +926,7 @@ def visit_Attribute(self, node): # Subscripting - def visit_Subscript(self, node): + def visit_Subscript(self, node: ast.Subscript) -> ast.AST: """Transforms all kinds of subscripts. 'foo[bar]' becomes '_getitem_(foo, bar)' @@ -953,19 +971,19 @@ def visit_Subscript(self, node): raise NotImplementedError( f"Unknown ctx type: {type(node.ctx)}") - def visit_Index(self, node): + def visit_Index(self, node: ast.Index) -> ast.AST: """ """ return self.node_contents_visit(node) - def visit_Slice(self, node): + def visit_Slice(self, node: ast.Slice) -> ast.AST: """ """ return self.node_contents_visit(node) - def visit_ExtSlice(self, node): + def visit_ExtSlice(self, node: ast.ExtSlice) -> ast.AST: """ """ @@ -973,31 +991,31 @@ def visit_ExtSlice(self, node): # Comprehensions - def visit_ListComp(self, node): + def visit_ListComp(self, node: ast.ListComp) -> ast.AST: """ """ return self.node_contents_visit(node) - def visit_SetComp(self, node): + def visit_SetComp(self, node: ast.SetComp) -> ast.AST: """ """ return self.node_contents_visit(node) - def visit_GeneratorExp(self, node): + def visit_GeneratorExp(self, node: ast.GeneratorExp) -> ast.AST: """ """ return self.node_contents_visit(node) - def visit_DictComp(self, node): + def visit_DictComp(self, node: ast.DictComp) -> ast.AST: """ """ return self.node_contents_visit(node) - def visit_comprehension(self, node): + def visit_comprehension(self, node: ast.comprehension) -> ast.AST: """ """ @@ -1005,7 +1023,7 @@ def visit_comprehension(self, node): # Statements - def visit_Assign(self, node): + def visit_Assign(self, node: ast.Assign) -> ast.AST: """ """ @@ -1054,7 +1072,7 @@ def visit_Assign(self, node): return new_nodes - def visit_AugAssign(self, node): + def visit_AugAssign(self, node: ast.AugAssign) -> ast.AST: """Forbid certain kinds of AugAssign According to the language reference (and ast.c) the following nodes @@ -1105,75 +1123,79 @@ def visit_AugAssign(self, node): raise NotImplementedError( f"Unknown target type: {type(node.target)}") - def visit_Raise(self, node): + def visit_Raise(self, node: ast.Raise) -> ast.AST: """Allow `raise` statements without restrictions.""" return self.node_contents_visit(node) - def visit_Assert(self, node): + def visit_Assert(self, node: ast.Assert) -> ast.AST: """Allow assert statements without restrictions.""" return self.node_contents_visit(node) - def visit_Delete(self, node): + def visit_Delete(self, node: ast.Delete) -> ast.AST: """Allow `del` statements without restrictions.""" return self.node_contents_visit(node) - def visit_Pass(self, node): + def visit_Pass(self, node: ast.Pass) -> ast.AST: """Allow `pass` statements without restrictions.""" return self.node_contents_visit(node) # Imports - def visit_Import(self, node): + def visit_Import(self, node: ast.Import) -> ast.AST: """Allow `import` statements with restrictions. See check_import_names.""" return self.check_import_names(node) - def visit_ImportFrom(self, node): + def visit_ImportFrom(self, node: ast.ImportFrom) -> ast.AST: """Allow `import from` statements with restrictions. See check_import_names.""" return self.check_import_names(node) - def visit_alias(self, node): + def visit_alias(self, node: ast.alias) -> ast.AST: """Allow `as` statements in import and import from statements.""" return self.node_contents_visit(node) # Control flow - def visit_If(self, node): + def visit_If(self, node: ast.If) -> ast.AST: """Allow `if` statements without restrictions.""" return self.node_contents_visit(node) - def visit_For(self, node): + def visit_For(self, node: ast.For) -> ast.AST: """Allow `for` statements with some restrictions.""" return self.guard_iter(node) - def visit_While(self, node): + def visit_While(self, node: ast.While) -> ast.AST: """Allow `while` statements.""" return self.node_contents_visit(node) - def visit_Break(self, node): + def visit_Break(self, node: ast.Break) -> ast.AST: """Allow `break` statements without restrictions.""" return self.node_contents_visit(node) - def visit_Continue(self, node): + def visit_Continue(self, node: ast.Continue) -> ast.AST: """Allow `continue` statements without restrictions.""" return self.node_contents_visit(node) - def visit_Try(self, node): + def visit_Try(self, node: ast.Try) -> ast.AST: """Allow `try` without restrictions.""" return self.node_contents_visit(node) - def visit_TryStar(self, node): - """Disallow `ExceptionGroup` due to a potential sandbox escape.""" + def visit_TryStar(self, node: ast.AST) -> ast.AST: + """Disallow `ExceptionGroup` due to a potential sandbox escape. + + TODO: Type Annotation for node when dropping support + for Python < 3.11 should be ast.TryStar. + """ self.not_allowed(node) - def visit_ExceptHandler(self, node): + def visit_ExceptHandler(self, node: ast.ExceptHandler) -> ast.AST: """Protect exception handlers.""" node = self.node_contents_visit(node) self.check_name(node, node.name) return node - def visit_With(self, node): + def visit_With(self, node: ast.With) -> ast.AST: """Protect tuple unpacking on with statements.""" node = self.node_contents_visit(node) @@ -1188,13 +1210,13 @@ def visit_With(self, node): return node - def visit_withitem(self, node): + def visit_withitem(self, node: ast.withitem) -> ast.AST: """Allow `with` statements (context managers) without restrictions.""" return self.node_contents_visit(node) # Function and class definitions - def visit_FunctionDef(self, node): + def visit_FunctionDef(self, node: ast.FunctionDef) -> ast.AST: """Allow function definitions (`def`) with some restrictions.""" self.check_name(node, node.name, allow_magic_methods=True) self.check_function_argument_names(node) @@ -1204,44 +1226,44 @@ def visit_FunctionDef(self, node): self.inject_print_collector(node) return node - def visit_Lambda(self, node): + def visit_Lambda(self, node: ast.Lambda) -> ast.AST: """Allow lambda with some restrictions.""" self.check_function_argument_names(node) return self.node_contents_visit(node) - def visit_arguments(self, node): + def visit_arguments(self, node: ast.arguments) -> ast.AST: """ """ return self.node_contents_visit(node) - def visit_arg(self, node): + def visit_arg(self, node: ast.arg) -> ast.AST: """ """ return self.node_contents_visit(node) - def visit_Return(self, node): + def visit_Return(self, node: ast.Return) -> ast.AST: """Allow `return` statements without restrictions.""" return self.node_contents_visit(node) - def visit_Yield(self, node): + def visit_Yield(self, node: ast.Yield) -> ast.AST: """Allow `yield`statements without restrictions.""" return self.node_contents_visit(node) - def visit_YieldFrom(self, node): + def visit_YieldFrom(self, node: ast.YieldFrom) -> ast.AST: """Allow `yield`statements without restrictions.""" return self.node_contents_visit(node) - def visit_Global(self, node): + def visit_Global(self, node: ast.Global) -> ast.AST: """Allow `global` statements without restrictions.""" return self.node_contents_visit(node) - def visit_Nonlocal(self, node): + def visit_Nonlocal(self, node: ast.Nonlocal) -> ast.AST: """Deny `nonlocal` statements.""" self.not_allowed(node) - def visit_ClassDef(self, node): + def visit_ClassDef(self, node: ast.ClassDef) -> ast.AST: """Check the name of a class definition.""" self.check_name(node, node.name) node = self.node_contents_visit(node) @@ -1258,7 +1280,7 @@ class {0.name}(metaclass=__metaclass__): new_class_node.decorator_list = node.decorator_list return new_class_node - def visit_Module(self, node): + def visit_Module(self, node: ast.Module) -> ast.AST: """Add the print_collector (only if print is used) at the top.""" node = self.node_contents_visit(node) @@ -1276,25 +1298,25 @@ def visit_Module(self, node): # Async und await - def visit_AsyncFunctionDef(self, node): + def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> ast.AST: """Deny async functions.""" self.not_allowed(node) - def visit_Await(self, node): + def visit_Await(self, node: ast.Await) -> ast.AST: """Deny async functionality.""" self.not_allowed(node) - def visit_AsyncFor(self, node): + def visit_AsyncFor(self, node: ast.AsyncFor) -> ast.AST: """Deny async functionality.""" self.not_allowed(node) - def visit_AsyncWith(self, node): + def visit_AsyncWith(self, node: ast.AsyncWith) -> ast.AST: """Deny async functionality.""" self.not_allowed(node) # Assignment expressions (walrus operator ``:=``) # New in 3.8 - def visit_NamedExpr(self, node): + def visit_NamedExpr(self, node: ast.NamedExpr) -> ast.AST: """Allow assignment expressions under some circumstances.""" # while the grammar requires ``node.target`` to be a ``Name`` # the abstract syntax is more permissive and allows an ``expr``. diff --git a/tox.ini b/tox.ini index 4015b3a..990f260 100644 --- a/tox.ini +++ b/tox.ini @@ -5,7 +5,6 @@ minversion = 3.18 envlist = release-check lint - py39 py310 py311 py312 @@ -52,7 +51,7 @@ commands = coverage combine coverage html coverage report -m --fail-under=100 -depends = py39,py310,py311,py311-datetime,py312,py313,py314,coverage +depends = py310,py311,py311-datetime,py312,py313,py314,coverage [testenv:setuptools-latest] basepython = python3