From 376f303dd80ae2a408c1b1f5fd785e66d7239acb Mon Sep 17 00:00:00 2001 From: Kurt McKee Date: Tue, 18 Jun 2024 19:53:40 -0500 Subject: [PATCH 01/20] Support Python 3.9+ --- .github/workflows/ci.yml | 11 ++++++++++- README.rst | 3 +-- setup.cfg | 4 +--- simpleeval.py | 1 + 4 files changed, 13 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2fd41c6..bb2c2f2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,7 +18,14 @@ jobs: strategy: matrix: os: [ubuntu-latest, windows-latest] - python: ['2.7', '3.7', '3.8', '3.9', '3.10', '3.11', '3.12-dev', 'pypy-3.8'] + python: + - '3.9' + - '3.10' + - '3.11' + - '3.12' + - '3.13' + - 'pypy-3.9' + - 'pypy-3.10' env: OS: ${{ matrix.os }} PYTHON: ${{ matrix.python }} @@ -29,6 +36,8 @@ jobs: uses: actions/setup-python@master with: python-version: ${{ matrix.python }} + allow-prereleases: true + - name: Generate Report run: | pip install coverage diff --git a/README.rst b/README.rst index 6a98f50..9b03c26 100644 --- a/README.rst +++ b/README.rst @@ -405,8 +405,7 @@ and then use ``EvalNoMethods`` instead of the ``SimpleEval`` class. Other... -------- -The library supports python 3 - but should be mostly compatible (and tested before 0.9.11) -with python 2.7 as well. +The library supports Python 3.9 and higher. Object attributes that start with ``_`` or ``func_`` are disallowed by default. If you really need that (BE CAREFUL!), then modify the module global diff --git a/setup.cfg b/setup.cfg index ed290d4..db34688 100644 --- a/setup.cfg +++ b/setup.cfg @@ -17,9 +17,7 @@ classifiers = [options] py_modules = simpleeval - -[bdist_wheel] -universal=1 +python_requires = >=3.9 [pycodestyle] max_line_length = 99 diff --git a/simpleeval.py b/simpleeval.py index df75d6a..e0f5741 100644 --- a/simpleeval.py +++ b/simpleeval.py @@ -58,6 +58,7 @@ - daxamin (Dax Amin) Better error for attempting to eval empty string - smurfix (Matthias Urlichs) Allow clearing functions / operators / etc completely - koenigsley (Mikhail Yeremeyev) documentation typos correction. +- kurtmckee (Kurt McKee) Infrastructure updates ------------------------------------- Basic Usage: From 47fdf5ec01ec5b8c2389c5c6e5056c13f8c2fafa Mon Sep 17 00:00:00 2001 From: Kurt McKee Date: Mon, 5 Aug 2024 09:51:55 -0500 Subject: [PATCH 02/20] Remove old Python compatibility conditionals and references --- README.rst | 2 +- simpleeval.py | 36 ++++++++---------------------------- test_simpleeval.py | 21 ++++----------------- 3 files changed, 13 insertions(+), 46 deletions(-) diff --git a/README.rst b/README.rst index 9b03c26..041fd61 100644 --- a/README.rst +++ b/README.rst @@ -246,7 +246,7 @@ are provided in the ``DEFAULT_FUNCTIONS`` dict: +----------------+--------------------------------------------------+ | ``float(x)`` | Convert ``x`` to a ``float``. | +----------------+--------------------------------------------------+ -| ``str(x)`` | Convert ``x`` to a ``str`` (``unicode`` in py2) | +| ``str(x)`` | Convert ``x`` to a ``str`` | +----------------+--------------------------------------------------+ If you want to provide a list of functions, but want to keep these as well, diff --git a/simpleeval.py b/simpleeval.py index e0f5741..a98623f 100644 --- a/simpleeval.py +++ b/simpleeval.py @@ -104,9 +104,6 @@ import warnings from random import random -PYTHON3 = sys.version_info[0] == 3 -PYTHON35 = PYTHON3 and sys.version_info > (3, 5) - ######################################## # Module wide 'globals' @@ -125,7 +122,7 @@ # their functionality is required, then please wrap them up in a safe container. And think # very hard about it first. And don't say I didn't warn you. # builtins is a dict in python >3.6 but a module before -DISALLOW_FUNCTIONS = {type, isinstance, eval, getattr, setattr, repr, compile, open} +DISALLOW_FUNCTIONS = {type, isinstance, eval, getattr, setattr, repr, compile, open, exec} if hasattr(__builtins__, "help") or ( hasattr(__builtins__, "__contains__") and "help" in __builtins__ # type: ignore ): @@ -133,11 +130,6 @@ DISALLOW_FUNCTIONS.add(help) -if PYTHON3: - # exec is not a function in Python2... - exec("DISALLOW_FUNCTIONS.add(exec)") # pylint: disable=exec-used - - ######################################## # Exceptions: @@ -318,8 +310,7 @@ def safe_lshift(a, b): # pylint: disable=invalid-name "randint": random_int, "int": int, "float": float, - # pylint: disable=undefined-variable - "str": str if PYTHON3 else unicode, # type: ignore + "str": str, } DEFAULT_NAMES = {"True": True, "False": False, "None": None} @@ -375,23 +366,12 @@ def __init__(self, operators=None, functions=None, names=None): ast.Attribute: self._eval_attribute, ast.Index: self._eval_index, ast.Slice: self._eval_slice, + ast.NameConstant: self._eval_constant, + ast.JoinedStr: self._eval_joinedstr, + ast.FormattedValue: self._eval_formattedvalue, + ast.Constant: self._eval_constant, } - # py3k stuff: - if hasattr(ast, "NameConstant"): - self.nodes[ast.NameConstant] = self._eval_constant - - # py3.6, f-strings - if hasattr(ast, "JoinedStr"): - self.nodes[ast.JoinedStr] = self._eval_joinedstr # f-string - self.nodes[ - ast.FormattedValue - ] = self._eval_formattedvalue # formatted value in f-string - - # py3.8 uses ast.Constant instead of ast.Num, ast.Str, ast.NameConstant - if hasattr(ast, "Constant"): - self.nodes[ast.Constant] = self._eval_constant - # Defaults: self.ATTR_INDEX_FALLBACK = ATTR_INDEX_FALLBACK @@ -669,7 +649,7 @@ def _eval_dict(self, node): result = {} for key, value in zip(node.keys, node.values): - if PYTHON35 and key is None: + if key is None: # "{**x}" gets parsed as a key-value pair of (None, Name(x)) result.update(self._eval(value)) else: @@ -681,7 +661,7 @@ def _eval_list(self, node): result = [] for item in node.elts: - if PYTHON3 and isinstance(item, ast.Starred): + if isinstance(item, ast.Starred): result.extend(self._eval(item.value)) else: result.append(self._eval(item)) diff --git a/test_simpleeval.py b/test_simpleeval.py index bc83d50..0454f70 100644 --- a/test_simpleeval.py +++ b/test_simpleeval.py @@ -429,9 +429,8 @@ def test_large_shifts(self): def test_encode_bignums(self): # thanks gk - if hasattr(1, "from_bytes"): # python3 only - with self.assertRaises(simpleeval.IterableTooLong): - self.t('(1).from_bytes(("123123123123123123123123").encode()*999999, "big")', 0) + with self.assertRaises(simpleeval.IterableTooLong): + self.t('(1).from_bytes(("123123123123123123123123").encode()*999999, "big")', 0) def test_string_length(self): with self.assertRaises(simpleeval.IterableTooLong): @@ -612,7 +611,6 @@ def test_dict_contains(self): self.t('{"a": 24}.get("b", 11)', 11) self.t('"a" in {"a": 24}', True) - @unittest.skipIf(not simpleeval.PYTHON35, "feature not supported") def test_dict_star_expression(self): self.s.names["x"] = {"a": 1, "b": 2} self.t('{"a": 0, **x, "c": 3}', {"a": 1, "b": 2, "c": 3}) @@ -621,7 +619,6 @@ def test_dict_star_expression(self): self.s.names["y"] = {"x": 1, "y": 2} self.t('{"a": 0, **x, **y, "c": 3}', {"a": 1, "b": 2, "c": 3, "x": 1, "y": 2}) - @unittest.skipIf(not simpleeval.PYTHON35, "feature not supported") def test_dict_invalid_star_expression(self): self.s.names["x"] = {"a": 1, "b": 2} self.s.names["y"] = {"x": 1, "y": 2} @@ -660,12 +657,10 @@ def test_list_contains(self): self.t('"b" in ["a","b"]', True) - @unittest.skipIf(not simpleeval.PYTHON3, "feature not supported") def test_list_star_expression(self): self.s.names["x"] = [1, 2, 3] self.t('["a", *x, "b"]', ["a", 1, 2, 3, "b"]) - @unittest.skipIf(not simpleeval.PYTHON3, "feature not supported") def test_list_invalid_star_expression(self): self.s.names["x"] = [1, 2, 3] self.s.names["y"] = 42 @@ -1200,10 +1195,7 @@ def foo(y): class TestDisallowedFunctions(DRYTest): def test_functions_are_disallowed_at_init(self): - DISALLOWED = [type, isinstance, eval, getattr, setattr, help, repr, compile, open] - if simpleeval.PYTHON3: - # pylint: disable=exec-used - exec("DISALLOWED.append(exec)") # exec is not a function in Python2... + DISALLOWED = [type, isinstance, eval, getattr, setattr, help, repr, compile, open, exec] for f in simpleeval.DISALLOW_FUNCTIONS: assert f in DISALLOWED @@ -1213,11 +1205,7 @@ def test_functions_are_disallowed_at_init(self): SimpleEval(functions={"foo": x}) def test_functions_are_disallowed_in_expressions(self): - DISALLOWED = [type, isinstance, eval, getattr, setattr, help, repr, compile, open] - - if simpleeval.PYTHON3: - # pylint: disable=exec-used - exec("DISALLOWED.append(exec)") # exec is not a function in Python2... + DISALLOWED = [type, isinstance, eval, getattr, setattr, help, repr, compile, open, exec] for f in simpleeval.DISALLOW_FUNCTIONS: assert f in DISALLOWED @@ -1234,7 +1222,6 @@ def test_functions_are_disallowed_in_expressions(self): simpleeval.DEFAULT_FUNCTIONS = DF.copy() -@unittest.skipIf(simpleeval.PYTHON3 is not True, "Python2 fails - but it's not supported anyway.") @unittest.skipIf(platform.python_implementation() == "PyPy", "GC set_debug not available in PyPy") class TestReferenceCleanup(DRYTest): """Test cleanup without cyclic references""" From b915ec81420dc24375b9a07339f229f0c3a1d071 Mon Sep 17 00:00:00 2001 From: Daniel Fairhead Date: Fri, 4 Oct 2024 20:50:28 +0100 Subject: [PATCH 03/20] Faster formatting & linting via ruff & update mypy --- Makefile | 9 ++++----- pyproject.toml | 3 +++ requirements/dev.txt | 6 ++---- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Makefile b/Makefile index 7f6e9f3..c33fabf 100644 --- a/Makefile +++ b/Makefile @@ -22,11 +22,10 @@ coverage: coverage run test_simpleeval.py lint: - black --check --diff simpleeval.py test_simpleeval.py - isort --check-only --diff simpleeval.py test_simpleeval.py - pylint simpleeval.py test_simpleeval.py + ruff check simpleeval.py test_simpleeval.py + ruff format --check simpleeval.py test_simpleeval.py mypy simpleeval.py test_simpleeval.py format: - black simpleeval.py test_simpleeval.py - isort simpleeval.py test_simpleeval.py + ruff check --fix-only simpleeval.py test_simpleeval.py + ruff format simpleeval.py test_simpleeval.py diff --git a/pyproject.toml b/pyproject.toml index 330c483..c0eb5eb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,6 +6,9 @@ build-backend = "setuptools.build_meta" line-length = 99 target-version = ['py310'] +[tool.ruff] +line-length = 99 + [tool.isort] combine_as_imports = true float_to_top = true diff --git a/requirements/dev.txt b/requirements/dev.txt index 20b576a..60e68af 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -1,4 +1,2 @@ -black==23.1.0 -isort==5.10.1 -pylint==2.12.2 -mypy==0.931 +ruff==0.6.9 +mypy==1.11.2 From a7c9fb7da17cf2dd82e7105993381173daca35b4 Mon Sep 17 00:00:00 2001 From: Daniel Fairhead Date: Fri, 4 Oct 2024 20:51:01 +0100 Subject: [PATCH 04/20] Minor linting tweaks that ruff wants... --- simpleeval.py | 10 ++++++---- test_simpleeval.py | 7 ++++--- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/simpleeval.py b/simpleeval.py index a98623f..26639eb 100644 --- a/simpleeval.py +++ b/simpleeval.py @@ -448,8 +448,9 @@ def _eval_num(node): def _eval_str(node): if len(node.s) > MAX_STRING_LENGTH: raise IterableTooLong( - "String Literal in statement is too long!" - " ({0}, when {1} is max)".format(len(node.s), MAX_STRING_LENGTH) + "String Literal in statement is too long! ({0}, when {1} is max)".format( + len(node.s), MAX_STRING_LENGTH + ) ) return node.s @@ -457,8 +458,9 @@ def _eval_str(node): def _eval_constant(node): if hasattr(node.value, "__len__") and len(node.value) > MAX_STRING_LENGTH: raise IterableTooLong( - "Literal in statement is too long!" - " ({0}, when {1} is max)".format(len(node.value), MAX_STRING_LENGTH) + "Literal in statement is too long! ({0}, when {1} is max)".format( + len(node.value), MAX_STRING_LENGTH + ) ) return node.value diff --git a/test_simpleeval.py b/test_simpleeval.py index 0454f70..6152706 100644 --- a/test_simpleeval.py +++ b/test_simpleeval.py @@ -1,12 +1,13 @@ # pylint: disable=too-many-public-methods, missing-docstring, eval-used, too-many-lines, no-self-use, disallowed-name, unspecified-encoding """ - Unit tests for simpleeval. - -------------------------- +Unit tests for simpleeval. +-------------------------- - Most of this stuff is pretty basic. +Most of this stuff is pretty basic. """ + import ast import gc import operator From d23ad096f78c9850afb9bdf5f192afab8a15416e Mon Sep 17 00:00:00 2001 From: Daniel Fairhead Date: Fri, 4 Oct 2024 21:05:33 +0100 Subject: [PATCH 05/20] Fix deprecation warnings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Edgar Ramírez Mondragón <16805946+edgarrmondragon@users.noreply.github.com> --- simpleeval.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/simpleeval.py b/simpleeval.py index 26639eb..2839129 100644 --- a/simpleeval.py +++ b/simpleeval.py @@ -59,6 +59,7 @@ - smurfix (Matthias Urlichs) Allow clearing functions / operators / etc completely - koenigsley (Mikhail Yeremeyev) documentation typos correction. - kurtmckee (Kurt McKee) Infrastructure updates +- edgarrmondragon (Edgar Ramírez-Mondragón) Address Python 3.12+ deprecation warnings ------------------------------------- Basic Usage: @@ -352,8 +353,6 @@ def __init__(self, operators=None, functions=None, names=None): ast.Assign: self._eval_assign, ast.AugAssign: self._eval_aug_assign, ast.Import: self._eval_import, - ast.Num: self._eval_num, - ast.Str: self._eval_str, ast.Name: self._eval_name, ast.UnaryOp: self._eval_unaryop, ast.BinOp: self._eval_binop, @@ -366,12 +365,22 @@ def __init__(self, operators=None, functions=None, names=None): ast.Attribute: self._eval_attribute, ast.Index: self._eval_index, ast.Slice: self._eval_slice, - ast.NameConstant: self._eval_constant, ast.JoinedStr: self._eval_joinedstr, ast.FormattedValue: self._eval_formattedvalue, ast.Constant: self._eval_constant, } + # py3.12 deprecated ast.Num, ast.Str, ast.NameConstant + # https://docs.python.org/3.12/whatsnew/3.12.html#deprecated + if Num := getattr(ast, "Num"): + self.nodes[Num] = self._eval_num + + if Str := getattr(ast, "Str"): + self.nodes[Str] = self._eval_str + + if NameConstant := getattr(ast, "NameConstant"): + self.nodes[NameConstant] = self._eval_constant + # Defaults: self.ATTR_INDEX_FALLBACK = ATTR_INDEX_FALLBACK From 32c047b0266c53f4dd5278aaedcfce3b10b3f930 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Krier?= Date: Sat, 8 Jun 2024 17:01:53 +0200 Subject: [PATCH 06/20] Run tests with warnings as error --- Makefile | 2 +- test_simpleeval.py | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index c33fabf..bcc6ba1 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ test: - python test_simpleeval.py + python -Werror test_simpleeval.py autotest: find . -name \*.py -not -path .\/.v\* | entr make test diff --git a/test_simpleeval.py b/test_simpleeval.py index 6152706..e9466ba 100644 --- a/test_simpleeval.py +++ b/test_simpleeval.py @@ -210,6 +210,7 @@ class TestEvaluator(DRYTest): def test_only_evalutate_first_statement(self): # it only evaluates the first statement: with warnings.catch_warnings(record=True) as ws: + warnings.simplefilter('always') self.t("11; x = 21; x + x", 11) self.assertIsInstance(ws[0].message, simpleeval.MultipleExpressions) @@ -791,6 +792,7 @@ def test_none(self): # or if you attempt to assign an unknown name to another with self.assertRaises(NameNotDefined): with warnings.catch_warnings(record=True) as ws: + warnings.simplefilter('always') self.t("s += a", 21) self.assertIsInstance(ws[0].message, simpleeval.AssignmentAttempted) @@ -816,6 +818,7 @@ def test_dict(self): # however, you can't assign to those names: with warnings.catch_warnings(record=True) as ws: + warnings.simplefilter('always') self.t("a = 200", 200) self.assertIsInstance(ws[0].message, simpleeval.AssignmentAttempted) @@ -826,6 +829,7 @@ def test_dict(self): self.s.names["b"] = [0] with warnings.catch_warnings(record=True) as ws: + warnings.simplefilter('always') self.t("b[0] = 11", 11) self.assertIsInstance(ws[0].message, simpleeval.AssignmentAttempted) @@ -849,6 +853,7 @@ def test_dict(self): # you still can't assign though: with warnings.catch_warnings(record=True) as ws: + warnings.simplefilter('always') self.t("c['b'] = 99", 99) self.assertIsInstance(ws[0].message, simpleeval.AssignmentAttempted) @@ -859,6 +864,7 @@ def test_dict(self): self.s.names["c"]["c"] = {"c": 11} with warnings.catch_warnings(record=True) as ws: + warnings.simplefilter('always') self.t("c['c']['c'] = 21", 21) self.assertIsInstance(ws[0].message, simpleeval.AssignmentAttempted) @@ -874,6 +880,7 @@ def test_dict_attr_access(self): self.t("a.b.c*2", 84) with warnings.catch_warnings(record=True) as ws: + warnings.simplefilter('always') self.t("a.b.c = 11", 11) self.assertIsInstance(ws[0].message, simpleeval.AssignmentAttempted) @@ -881,6 +888,7 @@ def test_dict_attr_access(self): # TODO: Wat? with warnings.catch_warnings(record=True) as ws: + warnings.simplefilter('always') self.t("a.d = 11", 11) with self.assertRaises(KeyError): From 061acec65ab787bcb969548c93094bbb52c73e5b Mon Sep 17 00:00:00 2001 From: Daniel Fairhead Date: Fri, 4 Oct 2024 21:23:52 +0100 Subject: [PATCH 07/20] Attribution cedk --- simpleeval.py | 1 + 1 file changed, 1 insertion(+) diff --git a/simpleeval.py b/simpleeval.py index 2839129..e912d47 100644 --- a/simpleeval.py +++ b/simpleeval.py @@ -60,6 +60,7 @@ - koenigsley (Mikhail Yeremeyev) documentation typos correction. - kurtmckee (Kurt McKee) Infrastructure updates - edgarrmondragon (Edgar Ramírez-Mondragón) Address Python 3.12+ deprecation warnings +- cedk (Cédric Krier) Allow running tests with Werror ------------------------------------- Basic Usage: From 6deac2f0ca945051ffc9f60cc082c09c1593bf6e Mon Sep 17 00:00:00 2001 From: Daniel Fairhead Date: Fri, 4 Oct 2024 21:29:15 +0100 Subject: [PATCH 08/20] And catch warnings from the deprecated AST nodes. --- simpleeval.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/simpleeval.py b/simpleeval.py index e912d47..57e27b8 100644 --- a/simpleeval.py +++ b/simpleeval.py @@ -371,16 +371,18 @@ def __init__(self, operators=None, functions=None, names=None): ast.Constant: self._eval_constant, } - # py3.12 deprecated ast.Num, ast.Str, ast.NameConstant - # https://docs.python.org/3.12/whatsnew/3.12.html#deprecated - if Num := getattr(ast, "Num"): - self.nodes[Num] = self._eval_num - - if Str := getattr(ast, "Str"): - self.nodes[Str] = self._eval_str - - if NameConstant := getattr(ast, "NameConstant"): - self.nodes[NameConstant] = self._eval_constant + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + # py3.12 deprecated ast.Num, ast.Str, ast.NameConstant + # https://docs.python.org/3.12/whatsnew/3.12.html#deprecated + if Num := getattr(ast, "Num"): + self.nodes[Num] = self._eval_num + + if Str := getattr(ast, "Str"): + self.nodes[Str] = self._eval_str + + if NameConstant := getattr(ast, "NameConstant"): + self.nodes[NameConstant] = self._eval_constant # Defaults: From c14afc09e8f9fac299233dedf0508262e7ec7d27 Mon Sep 17 00:00:00 2001 From: Daniel Fairhead Date: Fri, 4 Oct 2024 21:30:32 +0100 Subject: [PATCH 09/20] lint --- test_simpleeval.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/test_simpleeval.py b/test_simpleeval.py index e9466ba..26c0d3a 100644 --- a/test_simpleeval.py +++ b/test_simpleeval.py @@ -210,7 +210,7 @@ class TestEvaluator(DRYTest): def test_only_evalutate_first_statement(self): # it only evaluates the first statement: with warnings.catch_warnings(record=True) as ws: - warnings.simplefilter('always') + warnings.simplefilter("always") self.t("11; x = 21; x + x", 11) self.assertIsInstance(ws[0].message, simpleeval.MultipleExpressions) @@ -792,7 +792,7 @@ def test_none(self): # or if you attempt to assign an unknown name to another with self.assertRaises(NameNotDefined): with warnings.catch_warnings(record=True) as ws: - warnings.simplefilter('always') + warnings.simplefilter("always") self.t("s += a", 21) self.assertIsInstance(ws[0].message, simpleeval.AssignmentAttempted) @@ -818,7 +818,7 @@ def test_dict(self): # however, you can't assign to those names: with warnings.catch_warnings(record=True) as ws: - warnings.simplefilter('always') + warnings.simplefilter("always") self.t("a = 200", 200) self.assertIsInstance(ws[0].message, simpleeval.AssignmentAttempted) @@ -829,7 +829,7 @@ def test_dict(self): self.s.names["b"] = [0] with warnings.catch_warnings(record=True) as ws: - warnings.simplefilter('always') + warnings.simplefilter("always") self.t("b[0] = 11", 11) self.assertIsInstance(ws[0].message, simpleeval.AssignmentAttempted) @@ -853,7 +853,7 @@ def test_dict(self): # you still can't assign though: with warnings.catch_warnings(record=True) as ws: - warnings.simplefilter('always') + warnings.simplefilter("always") self.t("c['b'] = 99", 99) self.assertIsInstance(ws[0].message, simpleeval.AssignmentAttempted) @@ -864,7 +864,7 @@ def test_dict(self): self.s.names["c"]["c"] = {"c": 11} with warnings.catch_warnings(record=True) as ws: - warnings.simplefilter('always') + warnings.simplefilter("always") self.t("c['c']['c'] = 21", 21) self.assertIsInstance(ws[0].message, simpleeval.AssignmentAttempted) @@ -880,7 +880,7 @@ def test_dict_attr_access(self): self.t("a.b.c*2", 84) with warnings.catch_warnings(record=True) as ws: - warnings.simplefilter('always') + warnings.simplefilter("always") self.t("a.b.c = 11", 11) self.assertIsInstance(ws[0].message, simpleeval.AssignmentAttempted) @@ -888,7 +888,7 @@ def test_dict_attr_access(self): # TODO: Wat? with warnings.catch_warnings(record=True) as ws: - warnings.simplefilter('always') + warnings.simplefilter("always") self.t("a.d = 11", 11) with self.assertRaises(KeyError): From 95423430eb7a4a37d2abd0aa0e8ecd1c1c5c741e Mon Sep 17 00:00:00 2001 From: Daniel Fairhead Date: Fri, 4 Oct 2024 21:38:45 +0100 Subject: [PATCH 10/20] Attempt to fix codecov stats --- .github/workflows/ci.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bb2c2f2..48a27a1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,4 +1,4 @@ -name: Codecov +name: CI on: [push] jobs: lint: @@ -44,7 +44,8 @@ jobs: coverage run -m test_simpleeval coverage xml - name: Upload to codecov - uses: codecov/codecov-action@v2 + uses: codecov/codecov-action@v4.0.1 with: files: coverage.xml + token: ${{ secrets.CODECOV_TOKEN }} verbose: true From 570476bbc5db1770965d01fa612abc7e12727c64 Mon Sep 17 00:00:00 2001 From: Daniel Fairhead Date: Fri, 4 Oct 2024 21:44:05 +0100 Subject: [PATCH 11/20] Bump version info --- setup.cfg | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.cfg b/setup.cfg index db34688..2a89439 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = simpleeval -version = 0.9.13 +version = 1.0.0 author = Daniel Fairhead author_email = danthedeckie@gmail.com description = A simple, safe single expression evaluator library. @@ -9,7 +9,7 @@ url = https://github.com/danthedeckie/simpleeval long_description = file: README.rst long_description_content_type = text/x-rst classifiers = - Development Status :: 4 - Beta + Development Status :: Development Status :: 5 - Production/Stable Intended Audience :: Developers License :: OSI Approved :: MIT License Topic :: Software Development :: Libraries :: Python Modules From 1ff1bda88e893065e4a05f38802a8683b09e578a Mon Sep 17 00:00:00 2001 From: Daniel Fairhead Date: Fri, 4 Oct 2024 21:56:57 +0100 Subject: [PATCH 12/20] Fix escape via generators etc. Yes - we need to do allow-lists not deny-lists... 2.0 Co-authored-by: decorator-factory --- simpleeval.py | 12 +++++++++++- test_simpleeval.py | 11 +++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/simpleeval.py b/simpleeval.py index 57e27b8..dda99a3 100644 --- a/simpleeval.py +++ b/simpleeval.py @@ -61,6 +61,7 @@ - kurtmckee (Kurt McKee) Infrastructure updates - edgarrmondragon (Edgar Ramírez-Mondragón) Address Python 3.12+ deprecation warnings - cedk (Cédric Krier) Allow running tests with Werror +- decorator-factory More security fixes ------------------------------------- Basic Usage: @@ -115,7 +116,16 @@ MAX_SHIFT = 10000 # highest << or >> (lshift / rshift) MAX_SHIFT_BASE = int(sys.float_info.max) # highest on left side of << or >> DISALLOW_PREFIXES = ["_", "func_"] -DISALLOW_METHODS = ["format", "format_map", "mro"] +DISALLOW_METHODS = [ + "format", + "format_map", + "mro", + "tb_frame", + "gi_frame", + "ag_frame", + "cr_frame", + "exec", +] # Disallow functions: # This, strictly speaking, is not necessary. These /should/ never be accessable anyway, diff --git a/test_simpleeval.py b/test_simpleeval.py index 26c0d3a..9747c84 100644 --- a/test_simpleeval.py +++ b/test_simpleeval.py @@ -1230,6 +1230,17 @@ def test_functions_are_disallowed_in_expressions(self): simpleeval.DEFAULT_FUNCTIONS = DF.copy() + def test_breakout_via_generator(self): + # Thanks decorator-factory + class Foo: + def bar(self): + yield "Hello, world!" + + evil = "foo.bar().gi_frame.f_globals['__builtins__'].exec('raise RuntimeError(\"Oh no\")')" + + with self.assertRaises(FeatureNotAvailable): + simple_eval(evil, names={"foo": Foo()}) + @unittest.skipIf(platform.python_implementation() == "PyPy", "GC set_debug not available in PyPy") class TestReferenceCleanup(DRYTest): From 4835603d48c82fac493209c523418aaea88eb92a Mon Sep 17 00:00:00 2001 From: Lucas Kruitwagen Date: Sun, 10 Sep 2023 22:50:08 +0100 Subject: [PATCH 13/20] add support for dictcomp --- simpleeval.py | 12 ++++++++++-- test_simpleeval.py | 18 ++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/simpleeval.py b/simpleeval.py index dda99a3..10afe7d 100644 --- a/simpleeval.py +++ b/simpleeval.py @@ -661,6 +661,7 @@ def __init__(self, operators=None, functions=None, names=None): ast.Set: self._eval_set, ast.ListComp: self._eval_comprehension, ast.GeneratorExp: self._eval_comprehension, + ast.DictComp: self._eval_comprehension, } ) @@ -699,7 +700,11 @@ def _eval_set(self, node): return set(self._eval(x) for x in node.elts) def _eval_comprehension(self, node): - to_return = [] + + if isinstance(node, ast.DictComp): + to_return = {} + else: + to_return = [] extra_names = {} @@ -738,7 +743,10 @@ def do_generator(gi=0): if len(node.generators) > gi + 1: do_generator(gi + 1) else: - to_return.append(self._eval(node.elt)) + if isinstance(to_return, dict): + to_return[self._eval(node.key)] = self._eval(node.value) + elif isinstance(to_return, list): + to_return.append(self._eval(node.elt)) try: do_generator() diff --git a/test_simpleeval.py b/test_simpleeval.py index 9747c84..9586404 100644 --- a/test_simpleeval.py +++ b/test_simpleeval.py @@ -729,6 +729,24 @@ def test_unpack(self): def test_nested_unpack(self): self.t("[a+b+c for a, (b, c) in ((1,(1,1)),(3,(2,2)))]", [3, 7]) + def test_dictcomp_basic(self): + self.t("{a:a + 1 for a in [1,2,3]}", {1:2, 2:3, 3:4}) + + def test_dictcomp_with_self_reference(self): + self.t("{a:a + a for a in [1,2,3]}", {1:2, 2:4, 3:6}) + + def test_dictcomp_with_if(self): + self.t("{a:a for a in [1,2,3,4,5] if a <= 3}", {1:1, 2:2, 3:3}) + + def test_dictcomp_with_multiple_if(self): + self.t("{a:a for a in [1,2,3,4,5] if a <= 3 and a > 1 }", {2:2, 3:3}) + + def test_dictcomp_unpack(self): + self.t("{a:a+b for a,b in ((1,2),(3,4))}", {1:3, 3:7}) + + def test_dictcomp_nested_unpack(self): + self.t("{a:a+b+c for a, (b, c) in ((1,(1,1)),(3,(2,2)))}", {1:3, 3:7}) + def test_other_places(self): self.s.functions = {"sum": sum} self.t("sum([a+1 for a in [1,2,3,4,5]])", 20) From c9dcca1de5882d2fa754d5b5a879c2a2353b4186 Mon Sep 17 00:00:00 2001 From: Lucas Kruitwagen Date: Sun, 10 Sep 2023 22:57:11 +0100 Subject: [PATCH 14/20] delint and add contib to README --- simpleeval.py | 2 +- test_simpleeval.py | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/simpleeval.py b/simpleeval.py index 10afe7d..c14eaeb 100644 --- a/simpleeval.py +++ b/simpleeval.py @@ -62,6 +62,7 @@ - edgarrmondragon (Edgar Ramírez-Mondragón) Address Python 3.12+ deprecation warnings - cedk (Cédric Krier) Allow running tests with Werror - decorator-factory More security fixes +- lkruitwagen (Lucas Kruitwagen) Adding support for dict comprehensions ------------------------------------- Basic Usage: @@ -700,7 +701,6 @@ def _eval_set(self, node): return set(self._eval(x) for x in node.elts) def _eval_comprehension(self, node): - if isinstance(node, ast.DictComp): to_return = {} else: diff --git a/test_simpleeval.py b/test_simpleeval.py index 9586404..cd4918a 100644 --- a/test_simpleeval.py +++ b/test_simpleeval.py @@ -730,22 +730,22 @@ def test_nested_unpack(self): self.t("[a+b+c for a, (b, c) in ((1,(1,1)),(3,(2,2)))]", [3, 7]) def test_dictcomp_basic(self): - self.t("{a:a + 1 for a in [1,2,3]}", {1:2, 2:3, 3:4}) + self.t("{a:a + 1 for a in [1,2,3]}", {1: 2, 2: 3, 3: 4}) def test_dictcomp_with_self_reference(self): - self.t("{a:a + a for a in [1,2,3]}", {1:2, 2:4, 3:6}) + self.t("{a:a + a for a in [1,2,3]}", {1: 2, 2: 4, 3: 6}) def test_dictcomp_with_if(self): - self.t("{a:a for a in [1,2,3,4,5] if a <= 3}", {1:1, 2:2, 3:3}) + self.t("{a:a for a in [1,2,3,4,5] if a <= 3}", {1: 1, 2: 2, 3: 3}) def test_dictcomp_with_multiple_if(self): - self.t("{a:a for a in [1,2,3,4,5] if a <= 3 and a > 1 }", {2:2, 3:3}) + self.t("{a:a for a in [1,2,3,4,5] if a <= 3 and a > 1 }", {2: 2, 3: 3}) def test_dictcomp_unpack(self): - self.t("{a:a+b for a,b in ((1,2),(3,4))}", {1:3, 3:7}) + self.t("{a:a+b for a,b in ((1,2),(3,4))}", {1: 3, 3: 7}) def test_dictcomp_nested_unpack(self): - self.t("{a:a+b+c for a, (b, c) in ((1,(1,1)),(3,(2,2)))}", {1:3, 3:7}) + self.t("{a:a+b+c for a, (b, c) in ((1,(1,1)),(3,(2,2)))}", {1: 3, 3: 7}) def test_other_places(self): self.s.functions = {"sum": sum} From 983f4e04ed70c1f6147112fcf48b51cacb4d9c7d Mon Sep 17 00:00:00 2001 From: Daniel Fairhead Date: Wed, 8 Feb 2023 09:16:20 +0000 Subject: [PATCH 15/20] Don't misuse KeyError for the custom `names` function. --- README.rst | 29 +++++++++++++++++++++++++++-- simpleeval.py | 22 ++++++++++++++-------- test_simpleeval.py | 30 ++++++++++++++++++++++++++++++ 3 files changed, 71 insertions(+), 10 deletions(-) diff --git a/README.rst b/README.rst index 041fd61..925a923 100644 --- a/README.rst +++ b/README.rst @@ -284,9 +284,34 @@ You can also hand the handling of names over to a function, if you prefer: 3 That was a bit of a silly example, but you could use this for pulling values -from a database or file, say, or doing some kind of caching system. +from a database or file, looking up spreadsheet cells, say, or doing some kind of caching system. -The two default names that are provided are ``True`` and ``False``. So if you want to provide your own names, but want ``True`` and ``False`` to keep working, either provide them yourself, or ``.copy()`` and ``.update`` the ``DEFAULT_NAMES``. (See functions example above). +In general, when it attempts to find a variable by name, if it cannot find one, +then it will look in the ``functions`` for a function of that name. If you want your name handler +function to return a "I can't find that name!", then it should raise a ``simpleeval.NameNotDefined`` +exception. Eg: + +.. code-block:: python + + >>> def name_handler(node): + ... if node.id[0] == 'a': + ... return 21 + ... raise NameNotDefined(node.id[0], "Not found") + ... + ... simple_eval('a + a', names=name_handler, functions={"b": 100}) + + 42 + + >>> simple_eval('a + b', names=name_handler, functions={'b': 100}) + 121 + +(Note: in that example, putting a number directly into the functions dict was done just to +show the fall-back to functions. Normally only put actual callables in there.) + + +The two default names that are provided are ``True`` and ``False``. So if you want to provide +your own names, but want ``True`` and ``False`` to keep working, either provide them yourself, +or ``.copy()`` and ``.update`` the ``DEFAULT_NAMES``. (See functions example above). Creating an Evaluator Class --------------------------- diff --git a/simpleeval.py b/simpleeval.py index c14eaeb..3104c68 100644 --- a/simpleeval.py +++ b/simpleeval.py @@ -554,24 +554,30 @@ def _eval_name(self, node): try: # This happens at least for slicing # This is a safe thing to do because it is impossible - # that there is a true exression assigning to none + # that there is a true expression assigning to none # (the compiler rejects it, so you can't even # pass that to ast.parse) - if hasattr(self.names, "__getitem__"): - return self.names[node.id] - if callable(self.names): + return self.names[node.id] + + except (TypeError, KeyError): + pass + + if callable(self.names): + try: return self.names(node) + except NameNotDefined: + pass + elif not hasattr(self.names, "__getitem__"): raise InvalidExpression( 'Trying to use name (variable) "{0}"' ' when no "names" defined for' " evaluator".format(node.id) ) - except KeyError: - if node.id in self.functions: - return self.functions[node.id] + if node.id in self.functions: + return self.functions[node.id] - raise NameNotDefined(node.id, self.expr) + raise NameNotDefined(node.id, self.expr) def _eval_subscript(self, node): container = self._eval(node.value) diff --git a/test_simpleeval.py b/test_simpleeval.py index cd4918a..1f0cd89 100644 --- a/test_simpleeval.py +++ b/test_simpleeval.py @@ -987,6 +987,36 @@ def name_handler(node): self.t("a", 1) self.t("a + b", 3) + def test_name_handler_name_not_found(self): + def name_handler(node): + if node.id[0] == "a": + return 21 + raise NameNotDefined(node.id[0], "not found") + + self.s.names = name_handler + self.s.functions = {"b": lambda: 100} + self.t("a + a", 42) + + self.t("b()", 100) + + with self.assertRaises(NameNotDefined): + self.t("c", None) + + def test_name_handler_raises_error(self): + # What happens if our name-handler raises a different kind of error? + # we want it to ripple up all the way... + + def name_handler(_node): + return {}["test"] + + self.s.names = name_handler + + # This should never be accessed: + self.s.functions = {"c": 42} + + with self.assertRaises(KeyError): + self.t("c", None) + class TestWhitespace(DRYTest): """test that incorrect whitespace (preceding/trailing) doesn't matter.""" From ee16fd3d857f830fd0fdaadca59324e90b009fe7 Mon Sep 17 00:00:00 2001 From: Daniel Fairhead Date: Fri, 4 Oct 2024 22:12:58 +0100 Subject: [PATCH 16/20] README fixes Co-authored-by: Kurt McKee --- README.rst | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/README.rst b/README.rst index 925a923..8f5133c 100644 --- a/README.rst +++ b/README.rst @@ -157,7 +157,7 @@ The ``^`` operator is often mistaken for a exponent operator, not the bitwise operation that it is in python, so if you want ``3 ^ 2`` to equal ``9``, you can replace the operator like this: -.. code-block:: python +.. code-block:: pycon >>> import ast >>> from simpleeval import safe_power @@ -200,7 +200,7 @@ If Expressions You can use python style ``if x then y else z`` type expressions: -.. code-block:: python +.. code-block:: pycon >>> simple_eval("'equal' if x == y else 'not equal'", names={"x": 1, "y": 2}) @@ -208,7 +208,7 @@ You can use python style ``if x then y else z`` type expressions: which, of course, can be nested: -.. code-block:: python +.. code-block:: pycon >>> simple_eval("'a' if 1 == 2 else 'b' if 2 == 3 else 'c'") 'c' @@ -219,7 +219,7 @@ Functions You can define functions which you'd like the expresssions to have access to: -.. code-block:: python +.. code-block:: pycon >>> simple_eval("double(21)", functions={"double": lambda x:x*2}) 42 @@ -227,7 +227,7 @@ You can define functions which you'd like the expresssions to have access to: You can define "real" functions to pass in rather than lambdas, of course too, and even re-name them so that expressions can be shorter -.. code-block:: python +.. code-block:: pycon >>> def double(x): return x * 2 @@ -252,7 +252,7 @@ are provided in the ``DEFAULT_FUNCTIONS`` dict: If you want to provide a list of functions, but want to keep these as well, then you can do a normal python ``.copy()`` & ``.update``: -.. code-block:: python +.. code-block:: pycon >>> my_functions = simpleeval.DEFAULT_FUNCTIONS.copy() >>> my_functions.update( @@ -267,7 +267,7 @@ Names Sometimes it's useful to have variables available, which in python terminology are called 'names'. -.. code-block:: python +.. code-block:: pycon >>> simple_eval("a + b", names={"a": 11, "b": 100}) 111 @@ -275,7 +275,7 @@ are called 'names'. You can also hand the handling of names over to a function, if you prefer: -.. code-block:: python +.. code-block:: pycon >>> def name_handler(node): return ord(node.id[0].lower(a))-96 @@ -288,10 +288,10 @@ from a database or file, looking up spreadsheet cells, say, or doing some kind o In general, when it attempts to find a variable by name, if it cannot find one, then it will look in the ``functions`` for a function of that name. If you want your name handler -function to return a "I can't find that name!", then it should raise a ``simpleeval.NameNotDefined`` +function to return an "I can't find that name!", then it should raise a ``simpleeval.NameNotDefined`` exception. Eg: -.. code-block:: python +.. code-block:: pycon >>> def name_handler(node): ... if node.id[0] == 'a': @@ -299,13 +299,13 @@ exception. Eg: ... raise NameNotDefined(node.id[0], "Not found") ... ... simple_eval('a + a', names=name_handler, functions={"b": 100}) - + 42 >>> simple_eval('a + b', names=name_handler, functions={'b': 100}) 121 -(Note: in that example, putting a number directly into the functions dict was done just to +(Note: in that example, putting a number directly into the ``functions`` dict was done just to show the fall-back to functions. Normally only put actual callables in there.) @@ -321,7 +321,7 @@ evaluations, you can create a SimpleEval object, and pass it expressions each time (which should be a bit quicker, and certainly more convenient for some use cases): -.. code-block:: python +.. code-block:: pycon >>> s = SimpleEval() @@ -400,7 +400,7 @@ comprehensions. Since the primary intention of this library is short expressions - an extra 'sweetener' is enabled by default. You can access a dict (or similar's) keys using the .attr syntax: -.. code-block:: python +.. code-block:: pycon >>> simple_eval("foo.bar", names={"foo": {"bar": 42}}) 42 From 5c38a5ca751666910e18c701627d88326ada7b0d Mon Sep 17 00:00:00 2001 From: Daniel Fairhead Date: Fri, 4 Oct 2024 22:22:38 +0100 Subject: [PATCH 17/20] Bump copyright year. --- simpleeval.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/simpleeval.py b/simpleeval.py index 3104c68..9b0de5b 100644 --- a/simpleeval.py +++ b/simpleeval.py @@ -1,5 +1,5 @@ """ -SimpleEval - (C) 2013-2023 Daniel Fairhead +SimpleEval - (C) 2013-2024 Daniel Fairhead ------------------------------------- An short, easy to use, safe and reasonably extensible expression evaluator. From 07f33637cc2f8f06ea1985cf68e718535b330a83 Mon Sep 17 00:00:00 2001 From: Daniel Fairhead Date: Fri, 4 Oct 2024 22:30:22 +0100 Subject: [PATCH 18/20] Hacky make codecov see new lines are tested. --- test_simpleeval.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test_simpleeval.py b/test_simpleeval.py index 1f0cd89..53a0a72 100644 --- a/test_simpleeval.py +++ b/test_simpleeval.py @@ -1284,6 +1284,9 @@ class Foo: def bar(self): yield "Hello, world!" + # Test the genertor does work - also adds the `yield` to codecov... + assert list(Foo().bar()) == ["Hello, world!"] + evil = "foo.bar().gi_frame.f_globals['__builtins__'].exec('raise RuntimeError(\"Oh no\")')" with self.assertRaises(FeatureNotAvailable): From eced404105c0974b7da313bd4d0b3dde7c6858f8 Mon Sep 17 00:00:00 2001 From: Daniel Fairhead Date: Sat, 5 Oct 2024 05:51:43 +0100 Subject: [PATCH 19/20] README bump badges --- README.rst | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/README.rst b/README.rst index 8f5133c..3bced32 100644 --- a/README.rst +++ b/README.rst @@ -1,23 +1,28 @@ simpleeval (Simple Eval) ======================== -.. image:: https://github.com/danthedeckie/simpleeval/actions/workflows/ci.yml/badge.svg?branch=gh-actions-build +.. |build-status| image:: https://github.com/danthedeckie/simpleeval/actions/workflows/ci.yml/badge.svg?branch=gh-actions-build :target: https://github.com/danthedeckie/simpleeval/actions/ :alt: Build Status -.. image:: https://codecov.io/gh/danthedeckie/simpleeval/branch/master/graph/badge.svg?token=isRnN1yrca +.. |code-coverage| image:: https://codecov.io/gh/danthedeckie/simpleeval/branch/master/graph/badge.svg?token=isRnN1yrca :target: https://codecov.io/gh/danthedeckie/simpleeval - :alt: Code Coverage + :alt: Code Coverage Status -.. image:: https://badge.fury.io/py/simpleeval.svg +.. |pypi-version| image:: https://badge.fury.io/py/simpleeval.svg :target: https://badge.fury.io/py/simpleeval :alt: PyPI Version -.. image:: https://img.shields.io/badge/code%20style-black-000000.svg - :target: https://github.com/psf/black +.. |python-versions| image:: https://img.shields.io/badge/python-3.9_%7C_3.10_%7C_3.11_%7C_3.12_%7C_3.13_%7C_PyPy3.9_%7C_PyPy3.10-blue + :alt: Static Badge -.. image:: https://img.shields.io/badge/linting-pylint-yellowgreen - :target: https://github.com/PyCQA/pylint +.. |pypi-monthly-downloads| image:: https://img.shields.io/pypi/dm/SimpleEval + :alt: PyPI - Downloads + +.. |formatting-with-ruff| image:: https://img.shields.io/badge/-ruff-black?logo=lightning&logoColor=%2300ff00&link=https%3A%2F%2Fdocs.astral.sh%2Fruff%2F + :alt: Static Badge + +|build-status| |code-coverage| |pypi-version| |python-versions| |pypi-monthly-downloads| |formatting-with-ruff| A single file library for easily adding evaluatable expressions into From 0fe45bb3569637377577b8bc1123e6ef6aab7651 Mon Sep 17 00:00:00 2001 From: Daniel Fairhead Date: Sat, 5 Oct 2024 06:24:09 +0100 Subject: [PATCH 20/20] Fix licence & classifier info for pypi --- LICENCE | 2 +- setup.cfg | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/LICENCE b/LICENCE index a71b854..0502c20 100644 --- a/LICENCE +++ b/LICENCE @@ -1,4 +1,4 @@ -simpleeval - Copyright (c) 2013-2017 Daniel Fairhead +simpleeval - Copyright (c) 2013-2024 Daniel Fairhead (MIT Licence) diff --git a/setup.cfg b/setup.cfg index 2a89439..52eba7b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -6,10 +6,11 @@ author_email = danthedeckie@gmail.com description = A simple, safe single expression evaluator library. keywords = eval, simple, expression, parse, ast url = https://github.com/danthedeckie/simpleeval +license = MIT long_description = file: README.rst long_description_content_type = text/x-rst classifiers = - Development Status :: Development Status :: 5 - Production/Stable + Development Status :: 5 - Production/Stable Intended Audience :: Developers License :: OSI Approved :: MIT License Topic :: Software Development :: Libraries :: Python Modules