diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml new file mode 100644 index 0000000..0f06e16 --- /dev/null +++ b/.github/workflows/pre-commit.yml @@ -0,0 +1,33 @@ +# Generated from: +# https://github.com/zopefoundation/meta/tree/master/config/pure-python +name: pre-commit + +on: + pull_request: + push: + branches: + - master + # Allow to run this workflow manually from the Actions tab + workflow_dispatch: + +env: + FORCE_COLOR: 1 + +jobs: + pre-commit: + name: linting + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: 3.x + - uses: pre-commit/action@v3.0.1 + with: + extra_args: --all-files --show-diff-on-failure + env: + PRE_COMMIT_COLOR: always + - uses: pre-commit-ci/lite-action@v1.0.2 + if: always() + with: + msg: Apply pre-commit code formatting diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 4217b1b..6f69029 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -17,39 +17,37 @@ jobs: fail-fast: false matrix: os: - - ["ubuntu", "ubuntu-20.04"] + - ["ubuntu", "ubuntu-latest"] - ["windows", "windows-latest"] config: # [Python version, tox env] - - ["3.9", "release-check"] - - ["3.9", "lint"] - - ["3.7", "py37"] - - ["3.8", "py38"] - - ["3.9", "py39"] - - ["3.10", "py310"] - - ["3.11", "py311"] - - ["3.12", "py312"] - - ["3.13.0-alpha - 3.13.0", "py313"] - - ["3.9", "docs"] - - ["3.9", "coverage"] - - ["3.9", "py39-datetime"] + - ["3.11", "release-check"] + - ["3.8", "py38"] + - ["3.9", "py39"] + - ["3.10", "py310"] + - ["3.11", "py311"] + - ["3.12", "py312"] + - ["3.13", "py313"] + - ["3.11", "docs"] + - ["3.11", "coverage"] + - ["3.11", "py311-datetime"] exclude: - - { os: ["windows", "windows-latest"], config: ["3.9", "release-check"] } - - { os: ["windows", "windows-latest"], config: ["3.9", "lint"] } - - { os: ["windows", "windows-latest"], config: ["3.9", "docs"] } - - { os: ["windows", "windows-latest"], config: ["3.9", "coverage"] } + - { os: ["windows", "windows-latest"], config: ["3.11", "release-check"] } + - { os: ["windows", "windows-latest"], config: ["3.11", "docs"] } + - { os: ["windows", "windows-latest"], config: ["3.11", "coverage"] } runs-on: ${{ matrix.os[1] }} if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name name: ${{ matrix.os[0] }}-${{ matrix.config[1] }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.config[0] }} + allow-prereleases: true - name: Pip cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ~/.cache/pip key: ${{ runner.os }}-pip-${{ matrix.config[0] }}-${{ hashFiles('setup.*', 'tox.ini') }} @@ -61,7 +59,11 @@ jobs: python -m pip install --upgrade pip pip install tox - name: Test + if: ${{ !startsWith(runner.os, 'Mac') }} run: tox -e ${{ matrix.config[1] }} + - name: Test (macOS) + if: ${{ startsWith(runner.os, 'Mac') }} + run: tox -e ${{ matrix.config[1] }}-universal2 - name: Coverage if: matrix.config[1] == 'coverage' run: | diff --git a/.meta.toml b/.meta.toml index af696b7..9814f9a 100644 --- a/.meta.toml +++ b/.meta.toml @@ -2,20 +2,20 @@ # https://github.com/zopefoundation/meta/tree/master/config/pure-python [meta] template = "pure-python" -commit-id = "cb0568c7" +commit-id = "d3455844" [python] with-pypy = false with-docs = true with-sphinx-doctests = true with-windows = true -with-future-python = true +with-future-python = false with-macos = false [tox] use-flake8 = true additional-envlist = [ - "py39-datetime", + "py311-datetime", "combined-coverage", ] testenv-deps = [ @@ -28,7 +28,7 @@ testenv-setenv = [ ] testenv-commands = [ "python -V", - "pytest --cov=src --cov=tests --cov-report= {posargs}", + "pytest --cov=src --cov=tests --cov-report= tests {posargs}", ] testenv-additional = [ "", @@ -47,16 +47,15 @@ testenv-additional = [ " coverage combine", " coverage html", " coverage report -m --fail-under=100", - "depends = py37,py38,py39,py39-datetime,py310,py311,py312,coverage", + "depends = py38,py39,py310,py311,py311-datetime,py312,py313,coverage", ] -coverage-basepython = "python3.8" -coverage-command = "pytest --cov=src --cov=tests --cov-report= {posargs}" +coverage-command = "pytest --cov=src --cov=tests --cov-report= tests {posargs}" coverage-setenv = [ "COVERAGE_FILE=.coverage", ] [coverage] -fail-under = 98.4 +fail-under = 97.9 [isort] additional-sources = "{toxinidir}/tests" @@ -82,9 +81,10 @@ additional-ignores = [ "docs/_build/html/_sources/roadmap/*", "docs/_build/html/_sources/upgrade_dependencies/*", "docs/_build/html/_sources/usage/*", + "docs/_build/html/_static/scripts/*", ] [github-actions] additional-config = [ - "- [\"3.9\", \"py39-datetime\"]", + "- [\"3.11\", \"py311-datetime\"]", ] diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..7ab398c --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,28 @@ +# Generated from: +# https://github.com/zopefoundation/meta/tree/master/config/pure-python +minimum_pre_commit_version: '3.6' +repos: + - repo: https://github.com/pycqa/isort + rev: "5.13.2" + hooks: + - id: isort + - repo: https://github.com/hhatto/autopep8 + rev: "v2.3.1" + hooks: + - id: autopep8 + args: [--in-place, --aggressive, --aggressive] + - repo: https://github.com/asottile/pyupgrade + rev: v3.17.0 + hooks: + - id: pyupgrade + args: [--py38-plus] + - repo: https://github.com/isidentical/teyit + rev: 0.4.3 + hooks: + - id: teyit + - repo: https://github.com/PyCQA/flake8 + rev: "7.1.1" + hooks: + - id: flake8 + additional_dependencies: + - flake8-debugger == 4.1.2 diff --git a/CHANGES.rst b/CHANGES.rst index 43226ba..d2922f6 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,8 +4,9 @@ Changes 7.4 (unreleased) ---------------- -- Allow to use the package with Python 3.13 -- Caution: No security - audit has been done so far. +- Allow to use the package with Python 3.13. + +- Drop support for Python 3.7. - Provide new function ``RestrictedPython.Guards.safer_getattr_raise``. It is similar to ``safer_getattr`` but handles its parameter diff --git a/MANIFEST.in b/MANIFEST.in index 3c482de..7f0aa88 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -5,6 +5,7 @@ include *.rst include *.txt include buildout.cfg include tox.ini +include .pre-commit-config.yaml recursive-include docs *.py recursive-include docs *.rst diff --git a/docs/conf.py b/docs/conf.py index ec291c3..3ea6a2c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -16,8 +16,13 @@ # documentation root, use os.path.abspath to make it absolute, like shown here. # sys.path.insert(0, os.path.abspath('.')) +import datetime + + # -- General configuration ------------------------------------------------ +year = datetime.datetime.now().year + # If your documentation needs a minimal Sphinx version, state it here. # needs_sphinx = '1.0' @@ -37,7 +42,7 @@ # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # source_suffix = ['.rst', '.md'] -source_suffix = '.rst' +source_suffix = {'.rst': 'restructuredtext'} # The encoding of source files. # source_encoding = 'utf-8-sig' @@ -47,7 +52,7 @@ # General information about the project. project = 'RestrictedPython' -copyright = '2017-2023, Zope Foundation and Contributors' +copyright = f'2017-{year}, Zope Foundation and Contributors' author = 'The Zope & Plone developer community' # The version info for the project you're documenting, acts as replacement for @@ -55,9 +60,9 @@ # built documents. # # The short X.Y version. -version = '7.0' +version = '7.4' # The full version, including alpha/beta/rc tags. -release = '7.0' +release = '7.4' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. @@ -107,12 +112,12 @@ # Intersphinx Mapping for Links between different Documentations intersphinx_mapping = { 'python3': ('https://docs.python.org/3', None), - 'python37': ('https://docs.python.org/3.7', None), 'python38': ('https://docs.python.org/3.8', None), 'python39': ('https://docs.python.org/3.9', None), 'python310': ('https://docs.python.org/3.10', None), 'python311': ('https://docs.python.org/3.11', None), 'python312': ('https://docs.python.org/3.12', None), + 'python313': ('https://docs.python.org/3.13', None), } # Options for sphinx.ext.todo: @@ -123,7 +128,7 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'default' +html_theme = 'furo' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the @@ -274,8 +279,11 @@ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ - (master_doc, 'restrictedpython', 'RestrictedPython Documentation', [author], 1) -] + (master_doc, + 'restrictedpython', + 'RestrictedPython Documentation', + [author], + 1)] # If true, show URL addresses after external links. # man_show_urls = False diff --git a/docs/contributing/ast/python3_6.ast b/docs/contributing/ast/python3_13.ast old mode 100755 new mode 100644 similarity index 55% rename from docs/contributing/ast/python3_6.ast rename to docs/contributing/ast/python3_13.ast index 8c87864..e23ee03 --- a/docs/contributing/ast/python3_6.ast +++ b/docs/contributing/ast/python3_13.ast @@ -1,53 +1,57 @@ --- Python 3.6 AST --- ASDL's 7 builtin types are: --- identifier, int, string, bytes, object, singleton, constant --- --- singleton: None, True or False --- constant can be None, whereas None means "no value" for object. - -module Python version "3.6" +-- Python 3.13 AST +-- ASDL's 4 builtin types are: +-- identifier, int, string, constant + +module Python version "3.13" { - mod = Module(stmt* body) + mod = Module(stmt* body, type_ignore* type_ignores) | Interactive(stmt* body) | Expression(expr body) - - -- not really an actual node but useful in Jython's typesystem. - | Suite(stmt* body) + | FunctionType(expr* argtypes, expr returns) stmt = FunctionDef(identifier name, arguments args, stmt* body, expr* decorator_list, - expr? returns) + expr? returns, + string? type_comment, + type_param* type_params) | AsyncFunctionDef(identifier name, arguments args, stmt* body, expr* decorator_list, - expr? returns) + expr? returns, + string? type_comment, + type_param* type_params) | ClassDef(identifier name, expr* bases, keyword* keywords, stmt* body, - expr* decorator_list) + expr* decorator_list, + type_param* type_params) | Return(expr? value) | Delete(expr* targets) - | Assign(expr* targets, expr value) + | Assign(expr* targets, expr value, string? type_comment) + | TypeAlias(expr name, type_param* type_params, expr value) | AugAssign(expr target, operator op, expr value) -- 'simple' indicates that we annotate simple name without parens | AnnAssign(expr target, expr annotation, expr? value, int simple) -- use 'orelse' because else is a keyword in target languages - | For(expr target, expr iter, stmt* body, stmt* orelse) - | AsyncFor(expr target, expr iter, stmt* body, stmt* orelse) + | For(expr target, expr iter, stmt* body, stmt* orelse, string? type_comment) + | AsyncFor(expr target, expr iter, stmt* body, stmt* orelse, string? type_comment) | While(expr test, stmt* body, stmt* orelse) | If(expr test, stmt* body, stmt* orelse) - | With(withitem* items, stmt* body) - | AsyncWith(withitem* items, stmt* body) + | With(withitem* items, stmt* body, string? type_comment) + | AsyncWith(withitem* items, stmt* body, string? type_comment) + + | Match(expr subject, match_case* cases) | Raise(expr? exc, expr? cause) | Try(stmt* body, excepthandler* handlers, stmt* orelse, stmt* finalbody) + | TryStar(stmt* body, excepthandler* handlers, stmt* orelse, stmt* finalbody) | Assert(expr test, expr? msg) | Import(alias* names) @@ -60,12 +64,12 @@ module Python version "3.6" | Break | Continue - -- XXX Jython will be different -- col_offset is the byte offset in the utf8 string the parser uses - attributes (int lineno, int col_offset) + attributes (int lineno, int col_offset, int? end_lineno, int? end_col_offset) -- BoolOp() can use left & right? expr = BoolOp(boolop op, expr* values) + | NamedExpr(expr target, expr value) | BinOp(expr left, operator op, expr right) | UnaryOp(unaryop op, expr operand) | Lambda(arguments args, expr body) @@ -83,39 +87,28 @@ module Python version "3.6" -- need sequences for compare to distinguish between -- x < 4 < 3 and (x < 4) < 3 | Compare(expr left, cmpop* ops, expr* comparators) - | Call(expr func, - expr* args, - keyword* keywords) - | Num(object n) -- a number as a PyObject. - | Str(string s) -- need to specify raw, unicode, etc? - | FormattedValue(expr value, int? conversion, expr? format_spec) + | Call(expr func, expr* args, keyword* keywords) + | FormattedValue(expr value, int conversion, expr? format_spec) | JoinedStr(expr* values) - | Bytes(bytes s) - | NameConstant(singleton value) - | Ellipsis - | Constant(constant value) + | Constant(constant value, string? kind) -- the following expression can appear in assignment context | Attribute(expr value, identifier attr, expr_context ctx) - | Subscript(expr value, slice slice, expr_context ctx) + | Subscript(expr value, expr slice, expr_context ctx) | Starred(expr value, expr_context ctx) | Name(identifier id, expr_context ctx) | List(expr* elts, expr_context ctx) | Tuple(expr* elts, expr_context ctx) - -- col_offset is the byte offset in the utf8 string the parser uses - attributes (int lineno, int col_offset) + -- can appear only in Subscript + | Slice(expr? lower, expr? upper, expr? step) + + -- col_offset is the byte offset in the utf8 string the parser uses + attributes (int lineno, int col_offset, int? end_lineno, int? end_col_offset) expr_context = Load | Store | Del - | AugLoad - | AugStore - | Param - - slice = Slice(expr? lower, expr? upper, expr? step) - | ExtSlice(slice* dims) - | Index(expr value) boolop = And | Or @@ -153,23 +146,49 @@ module Python version "3.6" comprehension = (expr target, expr iter, expr* ifs, int is_async) excepthandler = ExceptHandler(expr? type, identifier? name, stmt* body) - attributes (int lineno, int col_offset) + attributes (int lineno, int col_offset, int? end_lineno, int? end_col_offset) - arguments = (arg* args, + arguments = (arg* posonlyargs, + arg* args, arg? vararg, arg* kwonlyargs, expr* kw_defaults, arg? kwarg, expr* defaults) - arg = (identifier arg, expr? annotation) - attributes (int lineno, int col_offset) + arg = (identifier arg, expr? annotation, string? type_comment) + attributes (int lineno, int col_offset, int? end_lineno, int? end_col_offset) -- keyword arguments supplied to call (NULL identifier for **kwargs) keyword = (identifier? arg, expr value) + attributes (int lineno, int col_offset, int? end_lineno, int? end_col_offset) -- import name with optional 'as' alias. alias = (identifier name, identifier? asname) + attributes (int lineno, int col_offset, int? end_lineno, int? end_col_offset) withitem = (expr context_expr, expr? optional_vars) + + match_case = (pattern pattern, expr? guard, stmt* body) + + pattern = MatchValue(expr value) + | MatchSingleton(constant value) + | MatchSequence(pattern* patterns) + | MatchMapping(expr* keys, pattern* patterns, identifier? rest) + | MatchClass(expr cls, pattern* patterns, identifier* kwd_attrs, pattern* kwd_patterns) + + | MatchStar(identifier? name) + -- The optional "rest" MatchMapping parameter handles capturing extra mapping keys + + | MatchAs(pattern? pattern, identifier? name) + | MatchOr(pattern* patterns) + + attributes (int lineno, int col_offset, int end_lineno, int end_col_offset) + + type_ignore = TypeIgnore(int lineno, string tag) + + type_param = TypeVar(identifier name, expr? bound, expr? default_value) + | ParamSpec(identifier name, expr? default_value) + | TypeVarTuple(identifier name, expr? default_value) + attributes (int lineno, int col_offset, int end_lineno, int end_col_offset) } diff --git a/docs/contributing/changes_from312to313.rst b/docs/contributing/changes_from312to313.rst new file mode 100644 index 0000000..412082a --- /dev/null +++ b/docs/contributing/changes_from312to313.rst @@ -0,0 +1,5 @@ +Changes from Python 3.12 to Python 3.13 +--------------------------------------- + +.. literalinclude:: ast/python3_13.ast + :diff: ast/python3_12.ast diff --git a/docs/contributing/changes_from36to37.rst b/docs/contributing/changes_from36to37.rst deleted file mode 100644 index 42160bf..0000000 --- a/docs/contributing/changes_from36to37.rst +++ /dev/null @@ -1,5 +0,0 @@ -Changes from Python 3.6 to Python 3.7 -------------------------------------- - -.. literalinclude:: ast/python3_7.ast - :diff: ast/python3_6.ast diff --git a/docs/contributing/index.rst b/docs/contributing/index.rst index f88dfa9..c9eef77 100644 --- a/docs/contributing/index.rst +++ b/docs/contributing/index.rst @@ -1,7 +1,6 @@ Contributing ============ -.. contents:: Contributing to RestrictedPython -------------------------------- @@ -99,12 +98,12 @@ A (modified style) Copy of all Abstract Grammar Definitions for the Python versi .. toctree:: :maxdepth: 2 - changes_from36to37 changes_from37to38 changes_from38to39 changes_from39to310 changes_from310to311 changes_from311to312 + changes_from312to313 .. _understand: diff --git a/docs/index.rst b/docs/index.rst index e840f5d..7e77a28 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -29,8 +29,6 @@ Contents usage/index usage/api - upgrade_dependencies/index - roadmap/index contributing/index diff --git a/docs/requirements.txt b/docs/requirements.txt index 2806c16..de8fca9 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1 +1,2 @@ Sphinx +furo diff --git a/docs/upgrade_dependencies/index.rst b/docs/upgrade_dependencies/index.rst deleted file mode 100644 index 4d295d1..0000000 --- a/docs/upgrade_dependencies/index.rst +++ /dev/null @@ -1,30 +0,0 @@ -Upgrade from 3.x ----------------- - -For packages that use RestrictedPython the upgrade path differs on the actual usage. -If it uses pure RestrictedPython without any additional checks it should be enough to check the imports. -RestrictedPython did move some of the imports to the base namespace, so you should only import directly from ``RestrictedPython``. - -* compile_restricted methods: - - * ``from RestrictedPython import compile_restricted`` - * ``from RestrictedPython import compile_restricted_eval`` - * ``from RestrictedPython import compile_restricted_exec`` - * ``from RestrictedPython import compile_restricted_function`` - * ``from RestrictedPython import compile_restricted_single`` - -* predefined built-ins: - - * ``from RestrictedPython import safe_globals`` - * ``from RestrictedPython import safe_builtins`` - * ``from RestrictedPython import limited_builtins`` - * ``from RestrictedPython import utility_builtins`` - -* helper methods: - - * ``from RestrictedPython import PrintCollector`` - -Any import from ``RestrictedPython.RCompile`` indicates that there have been advanced checks implemented. -Those advanced checks where implemented via a ``MutatingWalker``. -Any checks needs to be reimplemented as a subclass of -``RestrictingNodeTransformer``. diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..75267ab --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,21 @@ +# +# Generated from: +# https://github.com/zopefoundation/meta/tree/master/config/pure-python + +[build-system] +requires = ["setuptools<74"] +build-backend = "setuptools.build_meta" + +[tool.coverage.run] +branch = true +source = ["RestrictedPython"] + +[tool.coverage.report] +fail_under = 97.9 +precision = 2 +ignore_errors = true +show_missing = true +exclude_lines = ["pragma: no cover", "pragma: nocover", "except ImportError:", "raise NotImplementedError", "if __name__ == '__main__':", "self.fail", "raise AssertionError", "raise unittest.Skip"] + +[tool.coverage.html] +directory = "parts/htmlcov" diff --git a/setup.cfg b/setup.cfg index c0470e3..ae12100 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,7 +1,5 @@ # Generated from: # https://github.com/zopefoundation/meta/tree/master/config/pure-python -[bdist_wheel] -universal = 0 [flake8] doctests = 1 @@ -19,6 +17,7 @@ ignore = docs/_build/html/_sources/roadmap/* docs/_build/html/_sources/upgrade_dependencies/* docs/_build/html/_sources/usage/* + docs/_build/html/_static/scripts/* [isort] force_single_line = True diff --git a/setup.py b/setup.py index 79f9bc3..fb92b3d 100644 --- a/setup.py +++ b/setup.py @@ -24,11 +24,6 @@ def read(*rnames): return f.read() -tests_require = [ - 'pytest', - 'pytest-mock', -] - setup(name='RestrictedPython', version='7.4.dev0', url='https://github.com/zopefoundation/RestrictedPython', @@ -44,18 +39,18 @@ def read(*rnames): 'Programming Language :: Python', 'Operating System :: OS Independent', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.12', + 'Programming Language :: Python :: 3.13', 'Programming Language :: Python :: Implementation :: CPython', 'Topic :: Security', ], keywords='restricted execution security untrusted code', author='Zope Foundation and Contributors', - author_email='zope-dev@zope.org', + author_email='zope-dev@zope.dev', project_urls={ "Documentation": "https://restrictedpython.readthedocs.io/", "Source": "https://github.com/zopefoundation/RestrictedPython", @@ -65,10 +60,10 @@ def read(*rnames): packages=find_packages('src'), package_dir={'': 'src'}, install_requires=[], - python_requires=">=3.7, <3.14", + python_requires=">=3.8, <3.14", extras_require={ - 'test': tests_require, - 'docs': ['Sphinx', 'sphinx_rtd_theme'], + 'test': ['pytest', 'pytest-mock'], + 'docs': ['Sphinx', 'furo'], }, include_package_data=True, zip_safe=False) diff --git a/src/RestrictedPython/_compat.py b/src/RestrictedPython/_compat.py index 56fe03d..2d85cc4 100644 --- a/src/RestrictedPython/_compat.py +++ b/src/RestrictedPython/_compat.py @@ -3,7 +3,6 @@ _version = sys.version_info -IS_PY38_OR_GREATER = _version.major == 3 and _version.minor >= 8 IS_PY310_OR_GREATER = _version.major == 3 and _version.minor >= 10 IS_PY311_OR_GREATER = _version.major == 3 and _version.minor >= 11 IS_PY312_OR_GREATER = _version.major == 3 and _version.minor >= 12 diff --git a/src/RestrictedPython/transformer.py b/src/RestrictedPython/transformer.py index 9a205cc..2034205 100644 --- a/src/RestrictedPython/transformer.py +++ b/src/RestrictedPython/transformer.py @@ -22,16 +22,6 @@ import contextlib import textwrap -from ._compat import IS_PY38_OR_GREATER - - -# Avoid DeprecationWarnings under Python 3.12 and up -if IS_PY38_OR_GREATER: - astStr = ast.Constant - astNum = ast.Constant -else: # pragma: no cover - astStr = ast.Str - astNum = ast.Num # For AugAssign the operator must be converted to a string. IOPERATOR_TO_STR = { @@ -127,16 +117,14 @@ def copy_locations(new_node, old_node): assert 'lineno' in new_node._attributes new_node.lineno = old_node.lineno - if IS_PY38_OR_GREATER: - assert 'end_lineno' in new_node._attributes - new_node.end_lineno = old_node.end_lineno + assert 'end_lineno' in new_node._attributes + new_node.end_lineno = old_node.end_lineno assert 'col_offset' in new_node._attributes new_node.col_offset = old_node.col_offset - if IS_PY38_OR_GREATER: - assert 'end_col_offset' in new_node._attributes - new_node.end_col_offset = old_node.end_col_offset + assert 'end_col_offset' in new_node._attributes + new_node.end_col_offset = old_node.end_col_offset ast.fix_missing_locations(new_node) @@ -280,7 +268,7 @@ def gen_unpack_spec(self, tpl): """ spec = ast.Dict(keys=[], values=[]) - spec.keys.append(astStr('childs')) + spec.keys.append(ast.Constant('childs')) spec.values.append(ast.Tuple([], ast.Load())) # starred elements in a sequence do not contribute into the min_len. @@ -300,12 +288,12 @@ def gen_unpack_spec(self, tpl): elif isinstance(val, ast.Tuple): el = ast.Tuple([], ast.Load()) - el.elts.append(astNum(idx - offset)) + el.elts.append(ast.Constant(idx - offset)) el.elts.append(self.gen_unpack_spec(val)) spec.values[0].elts.append(el) - spec.keys.append(astStr('min_len')) - spec.values.append(astNum(min_len)) + spec.keys.append(ast.Constant('min_len')) + spec.values.append(ast.Constant(min_len)) return spec @@ -492,9 +480,8 @@ def inject_print_collector(self, node, position=0): if isinstance(node, ast.Module): _print.lineno = position _print.col_offset = position - if IS_PY38_OR_GREATER: - _print.end_lineno = position - _print.end_col_offset = position + _print.end_lineno = position + _print.end_col_offset = position ast.fix_missing_locations(_print) else: copy_locations(_print, node) @@ -535,63 +522,22 @@ def node_contents_visit(self, node): # ast for Literals - if IS_PY38_OR_GREATER: - - def visit_Constant(self, node): - """Allow constant literals with restriction for Ellipsis. - - Constant replaces Num, Str, Bytes, NameConstant and Ellipsis in - Python 3.8+. - :see: https://docs.python.org/dev/whatsnew/3.8.html#deprecated - """ - if node.value is Ellipsis: - # Deny using `...`. - # Special handling necessary as ``self.not_allowed(node)`` - # would return the Error Message: - # 'Constant statements are not allowed.' - # which is only partial true. - self.error(node, 'Ellipsis statements are not allowed.') - return - return self.node_contents_visit(node) - - else: - - def visit_Num(self, node): - """Allow integer numbers without restrictions. - - Replaced by Constant in Python 3.8. - """ - return self.node_contents_visit(node) - - def visit_Str(self, node): - """Allow string literals without restrictions. - - Replaced by Constant in Python 3.8. - """ - return self.node_contents_visit(node) + def visit_Constant(self, node): + """Allow constant literals with restriction for Ellipsis. - def visit_Bytes(self, node): - """Allow bytes literals without restrictions. - - Replaced by Constant in Python 3.8. - """ - return self.node_contents_visit(node) - - def visit_Ellipsis(self, node): - """Deny using `...`. - - Replaced by Constant in Python 3.8. - """ - return self.not_allowed(node) - - def visit_NameConstant(self, node): - """Allow constant literals (True, False, None) without ... - - restrictions. - - Replaced by Constant in Python 3.8. - """ - return self.node_contents_visit(node) + Constant replaces Num, Str, Bytes, NameConstant and Ellipsis in + Python 3.8+. + :see: https://docs.python.org/dev/whatsnew/3.8.html#deprecated + """ + if node.value is Ellipsis: + # Deny using `...`. + # Special handling necessary as ``self.not_allowed(node)`` + # would return the Error Message: + # 'Constant statements are not allowed.' + # which is only partial true. + self.error(node, 'Ellipsis statements are not allowed.') + return + return self.node_contents_visit(node) def visit_Interactive(self, node): """Allow single mode without restrictions.""" @@ -915,7 +861,7 @@ def visit_Attribute(self, node): node = self.node_contents_visit(node) new_node = ast.Call( func=ast.Name('_getattr_', ast.Load()), - args=[node.value, astStr(node.attr)], + args=[node.value, ast.Constant(node.attr)], keywords=[]) copy_locations(new_node, node) @@ -1119,7 +1065,7 @@ def visit_AugAssign(self, node): value=ast.Call( func=ast.Name('_inplacevar_', ast.Load()), args=[ - astStr(IOPERATOR_TO_STR[type(node.op)]), + ast.Constant(IOPERATOR_TO_STR[type(node.op)]), ast.Name(node.target.id, ast.Load()), node.value ], diff --git a/tests/test_NamedExpr.py b/tests/test_NamedExpr.py index 308d0dd..2da22ad 100644 --- a/tests/test_NamedExpr.py +++ b/tests/test_NamedExpr.py @@ -4,14 +4,11 @@ from ast import NodeTransformer from ast import parse from unittest import TestCase -from unittest import skipUnless from RestrictedPython import compile_restricted from RestrictedPython import safe_globals -from RestrictedPython._compat import IS_PY38_OR_GREATER -@skipUnless(IS_PY38_OR_GREATER, "Feature available for Python 3.8+") class TestNamedExpr(TestCase): def test_works(self): code, gs = compile_str("if x:= x + 1: True\n") diff --git a/tests/test_compile.py b/tests/test_compile.py index 603ad84..c4ea7de 100644 --- a/tests/test_compile.py +++ b/tests/test_compile.py @@ -8,7 +8,6 @@ from RestrictedPython import compile_restricted_eval from RestrictedPython import compile_restricted_exec from RestrictedPython import compile_restricted_single -from RestrictedPython._compat import IS_PY38_OR_GREATER from RestrictedPython._compat import IS_PY310_OR_GREATER from RestrictedPython._compat import IS_PY311_OR_GREATER from tests.helper import restricted_eval @@ -43,10 +42,8 @@ def test_compile__invalid_syntax(): compile_restricted(INVALID_ASSINGMENT, '', 'exec') if IS_PY310_OR_GREATER: assert "SyntaxError: cannot assign to literal here." in str(err.value) - elif IS_PY38_OR_GREATER: - assert "cannot assign to literal at statement:" in str(err.value) else: - assert "can't assign to literal at statement:" in str(err.value) + assert "cannot assign to literal at statement:" in str(err.value) def test_compile__compile_restricted_exec__1(): diff --git a/tests/test_compile_restricted_function.py b/tests/test_compile_restricted_function.py index d5b89b3..0bf0437 100644 --- a/tests/test_compile_restricted_function.py +++ b/tests/test_compile_restricted_function.py @@ -3,7 +3,6 @@ from RestrictedPython import PrintCollector from RestrictedPython import compile_restricted_function from RestrictedPython import safe_builtins -from RestrictedPython._compat import IS_PY38_OR_GREATER from RestrictedPython._compat import IS_PY310_OR_GREATER @@ -242,11 +241,7 @@ def test_compile_restricted_function_invalid_syntax(): assert error_msg.startswith( "Line 1: SyntaxError: cannot assign to literal here. Maybe " ) - elif IS_PY38_OR_GREATER: - assert error_msg.startswith( - "Line 1: SyntaxError: cannot assign to literal at statement:" - ) else: assert error_msg.startswith( - "Line 1: SyntaxError: can't assign to literal at statement:" + "Line 1: SyntaxError: cannot assign to literal at statement:" ) diff --git a/tests/transformer/test_dict_comprehension.py b/tests/transformer/test_dict_comprehension.py index 17b8c45..c573510 100644 --- a/tests/transformer/test_dict_comprehension.py +++ b/tests/transformer/test_dict_comprehension.py @@ -1,4 +1,3 @@ -from RestrictedPython._compat import IS_PY38_OR_GREATER from tests.helper import restricted_exec @@ -28,20 +27,11 @@ def test_dict_comprehension_with_attrs(mocker): calls = [mocker.call(seq, 'z')] - # Note: Order changed in PEP 572, starting with Python 3.8. - if IS_PY38_OR_GREATER: - calls.extend([ - mocker.call(z[0], 'k'), - mocker.call(z[1], 'k'), - mocker.call(z[1], 'k'), - mocker.call(z[1], 'v'), - ]) - else: - calls.extend([ - mocker.call(z[0], 'k'), - mocker.call(z[1], 'k'), - mocker.call(z[1], 'v'), - mocker.call(z[1], 'k'), - ]) + calls.extend([ + mocker.call(z[0], 'k'), + mocker.call(z[1], 'k'), + mocker.call(z[1], 'k'), + mocker.call(z[1], 'v'), + ]) _getattr_.assert_has_calls(calls) diff --git a/tests/transformer/test_fstring.py b/tests/transformer/test_fstring.py index 68b1743..2b31c83 100644 --- a/tests/transformer/test_fstring.py +++ b/tests/transformer/test_fstring.py @@ -1,7 +1,4 @@ -import pytest - from RestrictedPython import compile_restricted_exec -from RestrictedPython._compat import IS_PY38_OR_GREATER from RestrictedPython.PrintCollector import PrintCollector @@ -37,10 +34,6 @@ def test_visit_invalid_variable_name(): """ -@pytest.mark.skipif( - not IS_PY38_OR_GREATER, - reason="f-string self-documenting expressions added in Python 3.8.", -) def test_f_string_self_documenting_expressions(): """Checks if f-string self-documenting expressions is checked.""" result = compile_restricted_exec( diff --git a/tox.ini b/tox.ini index 0adda1e..171abb2 100644 --- a/tox.ini +++ b/tox.ini @@ -5,7 +5,6 @@ minversion = 3.18 envlist = release-check lint - py37 py38 py39 py310 @@ -14,26 +13,23 @@ envlist = py313 docs coverage - py39-datetime + py311-datetime combined-coverage [testenv] usedevelop = true package = wheel wheel_build_env = .pkg -pip_pre = py313: true deps = + setuptools <74 datetime: DateTime -cconstraints.txt pytest-cov - Sphinx setenv = COVERAGE_FILE=.coverage.{envname} - py312: VIRTUALENV_PIP=23.1.2 - py312: PIP_REQUIRE_VIRTUALENV=0 commands = python -V - pytest --cov=src --cov=tests --cov-report= {posargs} + pytest --cov=src --cov=tests --cov-report= tests {posargs} sphinx-build -b doctest -d {envdir}/.cache/doctrees docs {envdir}/.cache/doctest extras = test @@ -54,41 +50,43 @@ commands = coverage combine coverage html coverage report -m --fail-under=100 -depends = py37,py38,py39,py39-datetime,py310,py311,py312,coverage +depends = py38,py39,py310,py311,py311-datetime,py312,py313,coverage + +[testenv:setuptools-latest] +basepython = python3 +deps = + git+https://github.com/pypa/setuptools.git\#egg=setuptools + datetime: DateTime + -cconstraints.txt + pytest-cov + [testenv:release-check] description = ensure that the distribution is ready to release basepython = python3 skip_install = true deps = + setuptools <74 twine build check-manifest check-python-versions >= 0.20.0 wheel +commands_pre = commands = check-manifest - check-python-versions + check-python-versions --only setup.py,tox.ini,.github/workflows/tests.yml python -m build --sdist --no-isolation twine check dist/* [testenv:lint] +description = This env runs all linters configured in .pre-commit-config.yaml basepython = python3 skip_install = true deps = - isort - flake8 -commands = - isort --check-only --diff {toxinidir}/src {toxinidir}/setup.py {toxinidir}/tests - flake8 src setup.py tests - -[testenv:isort-apply] -basepython = python3 -skip_install = true + pre-commit commands_pre = -deps = - isort commands = - isort {toxinidir}/src {toxinidir}/setup.py {toxinidir}/tests [] + pre-commit run --all-files --show-diff-on-failure [testenv:docs] basepython = python3 @@ -99,38 +97,19 @@ commands = sphinx-build -b doctest -d docs/_build/doctrees docs docs/_build/doctest [testenv:coverage] -basepython = python3.8 +basepython = python3 allowlist_externals = mkdir deps = - coverage + coverage[toml] datetime: DateTime -cconstraints.txt pytest-cov - Sphinx setenv = COVERAGE_FILE=.coverage commands = mkdir -p {toxinidir}/parts/htmlcov - pytest --cov=src --cov=tests --cov-report= {posargs} + pytest --cov=src --cov=tests --cov-report= tests {posargs} coverage run -a -m sphinx -b doctest -d {envdir}/.cache/doctrees docs {envdir}/.cache/doctest - coverage html --ignore-errors - coverage report --ignore-errors --show-missing --fail-under=98.4 - -[coverage:run] -branch = True -source = RestrictedPython - -[coverage:report] -precision = 2 -exclude_lines = - pragma: no cover - pragma: nocover - except ImportError: - raise NotImplementedError - if __name__ == '__main__': - self.fail - raise AssertionError - -[coverage:html] -directory = parts/htmlcov + coverage html + coverage report