Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
7876a87
Type Annotations for RestrictedPython
loechel Oct 18, 2025
1cd6198
isinstance check with ExtSlice and Tuple as for older Python Versions
loechel Oct 18, 2025
e2599ab
liniting
loechel Oct 18, 2025
4915d8e
Remove Python 3.9 as it end of life
loechel Oct 18, 2025
3299a8b
Remove License Cassifier, as they are deprecated
loechel Oct 18, 2025
4d56e39
Add Comment for TryStar Annotation
loechel Oct 18, 2025
935a960
Add Comment for TryStar Annotation
loechel Oct 18, 2025
e67da67
Add Comment for TryStar Annotation
loechel Oct 18, 2025
2a3d728
Add Changelog Entry
loechel Oct 18, 2025
3373795
Base for Python 3.14 Updates
loechel Oct 18, 2025
f86d895
Update docs for Python 3.14
loechel Oct 18, 2025
b4ceb35
add provisional visit_TempalteStr and visit_Interpolation to transfor…
loechel Oct 18, 2025
43941ee
Disable t-strings
loechel Oct 18, 2025
f0e2a06
Apply pre-commit code formatting
pre-commit-ci-lite[bot] Oct 18, 2025
26a218c
reactivate Template-Strings
loechel Oct 18, 2025
a4e189e
Update Documentation for TemplateStr and Interploation
loechel Oct 18, 2025
fd0328a
Apply pre-commit code formatting
pre-commit-ci-lite[bot] Oct 18, 2025
aceca07
conditional import
loechel Oct 18, 2025
3533c0f
fix coverage numbers
loechel Oct 18, 2025
e750331
readd Python 3.9 support
loechel Oct 18, 2025
44f9842
Merge branch 'master' into typing
dataflake Oct 19, 2025
5b8285c
- updating package files with zope/meta and fixing tests
dataflake Oct 19, 2025
ac53335
- fix last test
dataflake Oct 19, 2025
6ecdf5a
Merge branch 'master' into typing
dataflake Oct 20, 2025
0e1408c
- expand change log entry to be more clear.
dataflake Oct 20, 2025
1ce3ce1
Merge branch 'master' into typing
zedzhen May 13, 2026
652776e
Merge branch 'master' into typing
zedzhen Jun 2, 2026
0a292d3
fix return type
zedzhen Jun 2, 2026
444760e
style update (autopep8)
zedzhen Jun 2, 2026
0e92a0b
add type hints for RestrictingNodeTransformer attributes
zedzhen Jun 2, 2026
be5ad38
update type hints for RestrictingNodeTransformer methods
zedzhen Jun 2, 2026
37a4bec
update type hint for `policy` argument
zedzhen Jun 2, 2026
55985ad
add type hints for RestrictingNodeTransformer.visit_Interpolation
zedzhen Jun 2, 2026
99fd0be
update CHANGES.rst
zedzhen Jun 2, 2026
fd28e28
add types to CompileResult
zedzhen Jun 2, 2026
501299a
fix type hint for used_names
zedzhen Jun 2, 2026
b56ad85
add None to the return type hints
zedzhen Jun 2, 2026
2dd1fb5
add py.typed
zedzhen Jun 2, 2026
ea394dc
use "normal import"
zedzhen Jun 3, 2026
b1d1303
add list[ast.AST] to the return type hints
zedzhen Jun 3, 2026
23ec481
add config for mypy
zedzhen Jun 3, 2026
838d222
replace `compile(flags=ast.PyCF_ONLY_AST)` to `ast.parse`
zedzhen Jun 3, 2026
2913cb6
update type hints
zedzhen Jun 3, 2026
22ced90
add mypy check in pre-commit
zedzhen Jun 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,8 @@ repos:
- id: flake8
additional_dependencies:
- flake8-debugger == 4.1.2
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v2.1.0
hooks:
- id: mypy
pass_filenames: false
3 changes: 2 additions & 1 deletion CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ Changes
8.3 (unreleased)
----------------

- Nothing changed yet.
- Add type annotations to the package code.
For clarification, restricted Python code does not support type annotations.


8.3a1.dev0 (2026-05-29)
Expand Down
25 changes: 25 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ classifiers = [
"Programming Language :: Python :: 3.14",
"Programming Language :: Python :: Implementation :: CPython",
"Topic :: Security",
"Typing :: Typed",
]
dynamic = ["readme"]
requires-python = ">=3.10, <3.16"
Expand All @@ -50,6 +51,10 @@ docs = [
"Sphinx",
"furo",
]
typecheck = [
"mypy",
"typeshed",
]

[project.urls]
Comment thread
zedzhen marked this conversation as resolved.
Documentation = "https://restrictedpython.readthedocs.io/"
Expand Down Expand Up @@ -83,3 +88,23 @@ directory = "parts/htmlcov"
[tool.setuptools.dynamic]
readme = {file = ["README.rst", "CHANGES.rst"]}

[tool.mypy]
mypy_path = "src"
packages = ["RestrictedPython"]
python_version = "3.10"
warn_unreachable = true
implicit_reexport = false
strict = true

[[tool.mypy.overrides]]
module = ["DateTime"]
ignore_missing_imports = true

[[tool.mypy.overrides]]
module = ["RestrictedPython.Guards"]
check_untyped_defs = false
disallow_untyped_defs = false

[[tool.mypy.overrides]]
module = ["RestrictedPython.transformer"]
warn_no_return = false
48 changes: 31 additions & 17 deletions src/RestrictedPython/Eval.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,40 +13,53 @@
"""Restricted Python Expressions."""

import ast
import collections
import types
import typing

from .compile import compile_restricted_eval
from RestrictedPython._types import _cast_not_none
from RestrictedPython.compile import compile_restricted_eval


nltosp = str.maketrans('\r\n', ' ')

# No restrictions.
default_guarded_getattr = getattr

_T = typing.TypeVar('_T')
_TK = typing.TypeVar('_TK', contravariant=True)
_TV = typing.TypeVar('_TV', covariant=True)

def default_guarded_getitem(ob, index):

class _GetItem(typing.Protocol[_TK, _TV]):
def __getitem__(self, key: _TK) -> _TV: ...


def default_guarded_getitem(ob: _GetItem[_TK, _TV], index: _TK) -> _TV:
# No restrictions.
return ob[index]


def default_guarded_getiter(ob):
def default_guarded_getiter(ob: _T) -> _T:
# No restrictions.
return ob


class RestrictionCapableEval:
"""A base class for restricted code."""

globals = {'__builtins__': None}
globals: dict[str, typing.Any] = {'__builtins__': None}

# restricted
rcode = None
rcode: types.CodeType | None = None

# unrestricted
ucode = None
ucode: types.CodeType | None = None

# Names used by the expression
used = None
used: tuple[str, ...] | None = None

def __init__(self, expr):
def __init__(self, expr: str):
"""Create a restricted expression

where:
Expand All @@ -60,21 +73,20 @@ def __init__(self, expr):
# Catch syntax errors.
self.prepUnrestrictedCode()

def prepRestrictedCode(self):
def prepRestrictedCode(self) -> None:
if self.rcode is None:
result = compile_restricted_eval(self.expr, '<string>')
if result.errors:
raise SyntaxError(result.errors[0])
self.used = tuple(result.used_names)
self.rcode = result.code

def prepUnrestrictedCode(self):
def prepUnrestrictedCode(self) -> None:
if self.ucode is None:
exp_node = compile(
exp_node = ast.parse(
self.expr,
'<string>',
'eval',
ast.PyCF_ONLY_AST)
'eval')

co = compile(exp_node, '<string>', 'eval')

Expand All @@ -90,7 +102,9 @@ def prepUnrestrictedCode(self):

self.ucode = co

def eval(self, mapping):
def eval(self,
mapping: collections.abc.Mapping[str,
typing.Any]) -> typing.Any:
# This default implementation is probably not very useful. :-(
# This is meant to be overridden.
self.prepRestrictedCode()
Expand All @@ -103,11 +117,11 @@ def eval(self, mapping):

global_scope.update(self.globals)

for name in self.used:
for name in _cast_not_none(self.used):
if (name not in global_scope) and (name in mapping):
global_scope[name] = mapping[name]

return eval(self.rcode, global_scope)
return eval(_cast_not_none(self.rcode), global_scope)

def __call__(self, **kw):
def __call__(self, **kw: typing.Any) -> typing.Any:
return self.eval(kw)
2 changes: 1 addition & 1 deletion src/RestrictedPython/Guards.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ def guard(ob):
return guard


full_write_guard = _full_write_guard()
full_write_guard = _full_write_guard() # type: ignore[no-untyped-call]


def guarded_setattr(object, name, value):
Expand Down
27 changes: 23 additions & 4 deletions src/RestrictedPython/Limits.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,28 @@
# FOR A PARTICULAR PURPOSE
#
##############################################################################
import collections.abc
import typing

limited_builtins = {}

limited_builtins: dict[str, typing.Any] = {}

def limited_range(iFirst, *args):

@typing.overload
def limited_range(iFirst: int) -> collections.abc.Sequence[int]: ...


@typing.overload
def limited_range(iStart: int, iEnd: int, /
) -> collections.abc.Sequence[int]: ...


@typing.overload
def limited_range(iStart: int, iEnd: int, iStep: int, /
) -> collections.abc.Sequence[int]: ...


def limited_range(iFirst: int, *args: int) -> collections.abc.Sequence[int]:
# limited range function from Martijn Pieters
RANGELIMIT = 1000
if not len(args):
Expand All @@ -41,8 +58,10 @@ def limited_range(iFirst, *args):

limited_builtins['range'] = limited_range

_T = typing.TypeVar('_T')


def limited_list(seq):
def limited_list(seq: collections.abc.Iterable[_T]) -> list[_T]:
if isinstance(seq, str):
raise TypeError('cannot convert string to list')
return list(seq)
Expand All @@ -51,7 +70,7 @@ def limited_list(seq):
limited_builtins['list'] = limited_list


def limited_tuple(seq):
def limited_tuple(seq: collections.abc.Iterable[_T]) -> tuple[_T, ...]:
if isinstance(seq, str):
raise TypeError('cannot convert string to tuple')
return tuple(seq)
Expand Down
8 changes: 4 additions & 4 deletions src/RestrictedPython/PrintCollector.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,17 @@
class PrintCollector:
"""Collect written text, and return it when called."""

def __init__(self, _getattr_=None):
def __init__(self, _getattr_=None): # type: ignore[no-untyped-def]
self.txt = []
self._getattr_ = _getattr_

def write(self, text):
def write(self, text: str) -> None:
self.txt.append(text)

def __call__(self):
def __call__(self) -> str:
return ''.join(self.txt)

def _call_print(self, *objects, **kwargs):
def _call_print(self, *objects, **kwargs): # type: ignore[no-untyped-def]
if kwargs.get('file', None) is None:
kwargs['file'] = self
else:
Expand Down
27 changes: 21 additions & 6 deletions src/RestrictedPython/Utilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,24 @@
#
##############################################################################

import collections.abc
import math
import random
import string
import types
import typing


utility_builtins = {}
utility_builtins: dict[str, typing.Any] = {}


class _AttributeDelegator:
def __init__(self, mod, *excludes):
def __init__(self, mod: types.ModuleType, *excludes: str):
"""delegate attribute lookups outside *excludes* to module *mod*."""
self.__mod = mod
self.__excludes = excludes

def __getattr__(self, attr):
def __getattr__(self, attr: str) -> typing.Any:
if attr in self.__excludes:
raise NotImplementedError(
f"{self.__mod.__name__}.{attr} is not safe")
Expand All @@ -50,7 +53,7 @@ def __getattr__(self, attr):
pass


def same_type(arg1, *args):
def same_type(arg1: object, *args: object) -> bool:
"""Compares the class or type of two or more objects."""
t = getattr(arg1, '__class__', type(arg1))
for arg in args:
Expand All @@ -61,21 +64,33 @@ def same_type(arg1, *args):

utility_builtins['same_type'] = same_type

_T = typing.TypeVar('_T')

def test(*args):

def test(*args: _T) -> _T | None:
length = len(args)
for i in range(1, length, 2):
if args[i - 1]:
return args[i]

if length % 2:
return args[-1]
return None


utility_builtins['test'] = test

_TK = typing.TypeVar('_TK')
_TV = typing.TypeVar('_TV')
_T_in: typing.TypeAlias = collections.abc.Iterable[_TK | tuple[_TK, _TV]]
_T_out: typing.TypeAlias = list[tuple[_TK, _TK | _TV]]


def reorder(s, with_=None, without=()):
def reorder(
s: _T_in[_TK, _TV],
with_: collections.abc.Iterable[typing.Any] | None = None,
without: collections.abc.Iterable[typing.Any] = ()
) -> _T_out[_TK, _TV]:
# 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).
Expand Down
8 changes: 8 additions & 0 deletions src/RestrictedPython/_types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import typing


_T = typing.TypeVar('_T')


def _cast_not_none(var: _T | None) -> _T:
return var # type: ignore[return-value]
Loading
Loading