From e662dd902119cb14f420f3edfff9e17016d7b3fb Mon Sep 17 00:00:00 2001 From: Haleemur Ali Date: Mon, 9 Oct 2023 00:15:06 -0400 Subject: [PATCH] parse expressions only once --- poetry.lock | 160 ++++++- pyproject.toml | 1 + singer_sdk/helpers/_simpleeval.py | 679 ------------------------------ singer_sdk/mapper.py | 67 +-- tests/core/test_simpleeval.py | 4 +- 5 files changed, 183 insertions(+), 728 deletions(-) delete mode 100644 singer_sdk/helpers/_simpleeval.py diff --git a/poetry.lock b/poetry.lock index 29dc4a5dc4..66eb49044b 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,9 +1,10 @@ -# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.4.1 and should not be changed by hand. [[package]] name = "alabaster" version = "0.7.13" description = "A configurable sidebar-enabled Sphinx theme" +category = "main" optional = true python-versions = ">=3.6" files = [ @@ -15,6 +16,7 @@ files = [ name = "appdirs" version = "1.4.4" description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +category = "main" optional = false python-versions = "*" files = [ @@ -26,6 +28,7 @@ files = [ name = "argcomplete" version = "3.1.2" description = "Bash tab completion for argparse" +category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -43,6 +46,7 @@ test = ["coverage", "mypy", "pexpect", "ruff", "wheel"] name = "arrow" version = "1.2.3" description = "Better dates & times for Python" +category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -58,6 +62,7 @@ typing-extensions = {version = "*", markers = "python_version < \"3.8\""} name = "attrs" version = "23.1.0" description = "Classes Without Boilerplate" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -79,6 +84,7 @@ tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=1.1.1)", "pympler", "pyte name = "babel" version = "2.12.1" description = "Internationalization utilities" +category = "main" optional = true python-versions = ">=3.7" files = [ @@ -93,6 +99,7 @@ pytz = {version = ">=2015.7", markers = "python_version < \"3.9\""} name = "backoff" version = "2.2.1" description = "Function decoration for backoff and retry" +category = "main" optional = false python-versions = ">=3.7,<4.0" files = [ @@ -104,6 +111,7 @@ files = [ name = "beautifulsoup4" version = "4.12.2" description = "Screen-scraping library" +category = "main" optional = true python-versions = ">=3.6.0" files = [ @@ -122,6 +130,7 @@ lxml = ["lxml"] name = "binaryornot" version = "0.4.4" description = "Ultra-lightweight pure Python package to check if a file is binary or text." +category = "dev" optional = false python-versions = "*" files = [ @@ -136,6 +145,7 @@ chardet = ">=3.0.2" name = "boto3" version = "1.28.52" description = "The AWS SDK for Python" +category = "main" optional = true python-versions = ">= 3.7" files = [ @@ -155,6 +165,7 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] name = "botocore" version = "1.31.52" description = "Low-level, data-driven core of boto 3." +category = "main" optional = true python-versions = ">= 3.7" files = [ @@ -174,6 +185,7 @@ crt = ["awscrt (==0.16.26)"] name = "certifi" version = "2023.7.22" description = "Python package for providing Mozilla's CA Bundle." +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -185,6 +197,7 @@ files = [ name = "cffi" version = "1.15.1" description = "Foreign Function Interface for Python calling C code." +category = "main" optional = false python-versions = "*" files = [ @@ -261,6 +274,7 @@ pycparser = "*" name = "chardet" version = "5.2.0" description = "Universal encoding detector for Python 3" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -272,6 +286,7 @@ files = [ name = "charset-normalizer" version = "3.2.0" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +category = "main" optional = false python-versions = ">=3.7.0" files = [ @@ -356,6 +371,7 @@ files = [ name = "click" version = "8.1.7" description = "Composable command line interface toolkit" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -371,6 +387,7 @@ importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} name = "colorama" version = "0.4.6" description = "Cross-platform colored terminal text." +category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" files = [ @@ -382,6 +399,7 @@ files = [ name = "commitizen" version = "3.9.0" description = "Python commitizen client tool" +category = "dev" optional = false python-versions = ">=3.7,<4.0" files = [ @@ -407,6 +425,7 @@ typing-extensions = {version = ">=4.0.1,<5.0.0", markers = "python_version < \"3 name = "commitizen-version-bump" version = "0.1.0" description = "Commitizen customized for Meltano projects (https://commitizen-tools.github.io/commitizen/customization)" +category = "dev" optional = false python-versions = "^3.7" files = [] @@ -426,6 +445,7 @@ resolved_reference = "32fa072821bff397466b55be2abda62bd93e6a8d" name = "cookiecutter" version = "2.4.0" description = "A command-line utility that creates projects from project templates, e.g. creating a Python package project from a Python package project template." +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -447,6 +467,7 @@ rich = "*" name = "coverage" version = "7.2.7" description = "Code coverage measurement for Python" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -522,6 +543,7 @@ toml = ["tomli"] name = "cryptography" version = "41.0.4" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -567,6 +589,7 @@ test-randomorder = ["pytest-randomly"] name = "decli" version = "0.6.1" description = "Minimal, easy-to-use, declarative cli tool" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -578,6 +601,7 @@ files = [ name = "deprecated" version = "1.2.14" description = "Python @deprecated decorator to deprecate old python classes, functions or methods." +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -595,6 +619,7 @@ dev = ["PyTest", "PyTest-Cov", "bump2version (<1)", "sphinx (<2)", "tox"] name = "docutils" version = "0.19" description = "Docutils -- Python Documentation Utilities" +category = "main" optional = true python-versions = ">=3.7" files = [ @@ -606,6 +631,7 @@ files = [ name = "duckdb" version = "0.9.0" description = "DuckDB embedded database" +category = "dev" optional = false python-versions = ">=3.7.0" files = [ @@ -654,6 +680,7 @@ files = [ name = "duckdb-engine" version = "0.9.2" description = "SQLAlchemy driver for duckdb" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -669,6 +696,7 @@ sqlalchemy = ">=1.3.22" name = "exceptiongroup" version = "1.1.3" description = "Backport of PEP 654 (exception groups)" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -683,6 +711,7 @@ test = ["pytest (>=6)"] name = "fs" version = "2.4.16" description = "Python's filesystem abstraction layer" +category = "main" optional = false python-versions = "*" files = [ @@ -702,6 +731,7 @@ scandir = ["scandir (>=1.5,<2.0)"] name = "fs-s3fs" version = "1.1.1" description = "Amazon S3 filesystem for PyFilesystem2" +category = "main" optional = true python-versions = "*" files = [ @@ -718,6 +748,7 @@ six = ">=1.10,<2.0" name = "furo" version = "2023.3.27" description = "A clean customisable Sphinx documentation theme." +category = "main" optional = true python-versions = ">=3.7" files = [ @@ -735,6 +766,7 @@ sphinx-basic-ng = "*" name = "greenlet" version = "2.0.2" description = "Lightweight in-process concurrent programming" +category = "main" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*" files = [ @@ -743,7 +775,6 @@ files = [ {file = "greenlet-2.0.2-cp27-cp27m-win32.whl", hash = "sha256:6c3acb79b0bfd4fe733dff8bc62695283b57949ebcca05ae5c129eb606ff2d74"}, {file = "greenlet-2.0.2-cp27-cp27m-win_amd64.whl", hash = "sha256:283737e0da3f08bd637b5ad058507e578dd462db259f7f6e4c5c365ba4ee9343"}, {file = "greenlet-2.0.2-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:d27ec7509b9c18b6d73f2f5ede2622441de812e7b1a80bbd446cb0633bd3d5ae"}, - {file = "greenlet-2.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d967650d3f56af314b72df7089d96cda1083a7fc2da05b375d2bc48c82ab3f3c"}, {file = "greenlet-2.0.2-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:30bcf80dda7f15ac77ba5af2b961bdd9dbc77fd4ac6105cee85b0d0a5fcf74df"}, {file = "greenlet-2.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26fbfce90728d82bc9e6c38ea4d038cba20b7faf8a0ca53a9c07b67318d46088"}, {file = "greenlet-2.0.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9190f09060ea4debddd24665d6804b995a9c122ef5917ab26e1566dcc712ceeb"}, @@ -752,7 +783,6 @@ files = [ {file = "greenlet-2.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:76ae285c8104046b3a7f06b42f29c7b73f77683df18c49ab5af7983994c2dd91"}, {file = "greenlet-2.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:2d4686f195e32d36b4d7cf2d166857dbd0ee9f3d20ae349b6bf8afc8485b3645"}, {file = "greenlet-2.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c4302695ad8027363e96311df24ee28978162cdcdd2006476c43970b384a244c"}, - {file = "greenlet-2.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d4606a527e30548153be1a9f155f4e283d109ffba663a15856089fb55f933e47"}, {file = "greenlet-2.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c48f54ef8e05f04d6eff74b8233f6063cb1ed960243eacc474ee73a2ea8573ca"}, {file = "greenlet-2.0.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a1846f1b999e78e13837c93c778dcfc3365902cfb8d1bdb7dd73ead37059f0d0"}, {file = "greenlet-2.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a06ad5312349fec0ab944664b01d26f8d1f05009566339ac6f63f56589bc1a2"}, @@ -782,7 +812,6 @@ files = [ {file = "greenlet-2.0.2-cp37-cp37m-win32.whl", hash = "sha256:3f6ea9bd35eb450837a3d80e77b517ea5bc56b4647f5502cd28de13675ee12f7"}, {file = "greenlet-2.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:7492e2b7bd7c9b9916388d9df23fa49d9b88ac0640db0a5b4ecc2b653bf451e3"}, {file = "greenlet-2.0.2-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:b864ba53912b6c3ab6bcb2beb19f19edd01a6bfcbdfe1f37ddd1778abfe75a30"}, - {file = "greenlet-2.0.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1087300cf9700bbf455b1b97e24db18f2f77b55302a68272c56209d5587c12d1"}, {file = "greenlet-2.0.2-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:ba2956617f1c42598a308a84c6cf021a90ff3862eddafd20c3333d50f0edb45b"}, {file = "greenlet-2.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc3a569657468b6f3fb60587e48356fe512c1754ca05a564f11366ac9e306526"}, {file = "greenlet-2.0.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8eab883b3b2a38cc1e050819ef06a7e6344d4a990d24d45bc6f2cf959045a45b"}, @@ -791,7 +820,6 @@ files = [ {file = "greenlet-2.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b0ef99cdbe2b682b9ccbb964743a6aca37905fda5e0452e5ee239b1654d37f2a"}, {file = "greenlet-2.0.2-cp38-cp38-win32.whl", hash = "sha256:b80f600eddddce72320dbbc8e3784d16bd3fb7b517e82476d8da921f27d4b249"}, {file = "greenlet-2.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:4d2e11331fc0c02b6e84b0d28ece3a36e0548ee1a1ce9ddde03752d9b79bba40"}, - {file = "greenlet-2.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8512a0c38cfd4e66a858ddd1b17705587900dd760c6003998e9472b77b56d417"}, {file = "greenlet-2.0.2-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:88d9ab96491d38a5ab7c56dd7a3cc37d83336ecc564e4e8816dbed12e5aaefc8"}, {file = "greenlet-2.0.2-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:561091a7be172ab497a3527602d467e2b3fbe75f9e783d8b8ce403fa414f71a6"}, {file = "greenlet-2.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:971ce5e14dc5e73715755d0ca2975ac88cfdaefcaab078a284fea6cfabf866df"}, @@ -812,6 +840,7 @@ test = ["objgraph", "psutil"] name = "idna" version = "3.4" description = "Internationalized Domain Names in Applications (IDNA)" +category = "main" optional = false python-versions = ">=3.5" files = [ @@ -823,6 +852,7 @@ files = [ name = "imagesize" version = "1.4.1" description = "Getting image size from png/jpeg/jpeg2000/gif file" +category = "main" optional = true python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -834,6 +864,7 @@ files = [ name = "importlib-metadata" version = "6.7.0" description = "Read metadata from Python packages" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -854,6 +885,7 @@ testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs name = "importlib-resources" version = "5.12.0" description = "Read resources from Python packages" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -872,6 +904,7 @@ testing = ["flake8 (<5)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-chec name = "inflection" version = "0.5.1" description = "A port of Ruby on Rails inflector to Python" +category = "main" optional = false python-versions = ">=3.5" files = [ @@ -883,6 +916,7 @@ files = [ name = "iniconfig" version = "2.0.0" description = "brain-dead simple config-ini parsing" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -894,6 +928,7 @@ files = [ name = "jinja2" version = "3.1.2" description = "A very fast and expressive template engine." +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -911,6 +946,7 @@ i18n = ["Babel (>=2.7)"] name = "jmespath" version = "1.0.1" description = "JSON Matching Expressions" +category = "main" optional = true python-versions = ">=3.7" files = [ @@ -922,6 +958,7 @@ files = [ name = "joblib" version = "1.3.2" description = "Lightweight pipelining with Python functions" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -933,6 +970,7 @@ files = [ name = "jsonpath-ng" version = "1.6.0" description = "A final implementation of JSONPath for Python that aims to be standard compliant, including arithmetic and binary comparison operators and providing clear AST for metaprogramming." +category = "main" optional = false python-versions = "*" files = [ @@ -947,6 +985,7 @@ ply = "*" name = "jsonschema" version = "4.17.3" description = "An implementation of JSON Schema validation for Python" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -970,6 +1009,7 @@ format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339- name = "livereload" version = "2.6.3" description = "Python LiveReload is an awesome tool for web developers" +category = "main" optional = true python-versions = "*" files = [ @@ -985,6 +1025,7 @@ tornado = {version = "*", markers = "python_version > \"2.7\""} name = "markdown-it-py" version = "2.2.0" description = "Python port of markdown-it. Markdown parsing, done right!" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1010,6 +1051,7 @@ testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] name = "markupsafe" version = "2.1.3" description = "Safely add untrusted strings to HTML/XML markup." +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1033,16 +1075,6 @@ files = [ {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"}, {file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"}, {file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-win32.whl", hash = "sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"}, @@ -1079,6 +1111,7 @@ files = [ name = "mdit-py-plugins" version = "0.3.5" description = "Collection of plugins for markdown-it-py" +category = "main" optional = true python-versions = ">=3.7" files = [ @@ -1098,6 +1131,7 @@ testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] name = "mdurl" version = "0.1.2" description = "Markdown URL utilities" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1109,6 +1143,7 @@ files = [ name = "memoization" version = "0.4.0" description = "A powerful caching library for Python, with TTL support and multiple algorithm options. (https://github.com/lonelyenvoy/python-memoization)" +category = "main" optional = false python-versions = ">=3, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, <4" files = [ @@ -1119,6 +1154,7 @@ files = [ name = "multidict" version = "6.0.4" description = "multidict implementation" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1202,6 +1238,7 @@ files = [ name = "mypy" version = "1.4.1" description = "Optional static typing for Python" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1249,6 +1286,7 @@ reports = ["lxml"] name = "mypy-extensions" version = "1.0.0" description = "Type system extensions for programs checked with the mypy type checker." +category = "dev" optional = false python-versions = ">=3.5" files = [ @@ -1260,6 +1298,7 @@ files = [ name = "myst-parser" version = "1.0.0" description = "An extended [CommonMark](https://spec.commonmark.org/) compliant parser," +category = "main" optional = true python-versions = ">=3.7" files = [ @@ -1287,6 +1326,7 @@ testing-docutils = ["pygments", "pytest (>=7,<8)", "pytest-param-files (>=0.3.4, name = "numpy" version = "1.21.6" description = "NumPy is the fundamental package for array computing with Python." +category = "dev" optional = false python-versions = ">=3.7,<3.11" files = [ @@ -1327,6 +1367,7 @@ files = [ name = "numpy" version = "1.24.4" description = "Fundamental package for array computing in Python" +category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -1364,6 +1405,7 @@ files = [ name = "packaging" version = "23.2" description = "Core utilities for Python packages" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1375,6 +1417,7 @@ files = [ name = "pendulum" version = "2.1.2" description = "Python datetimes made easy" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [ @@ -1409,6 +1452,7 @@ pytzdata = ">=2020.1" name = "pkgutil-resolve-name" version = "1.3.10" description = "Resolve a name to an object." +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -1420,6 +1464,7 @@ files = [ name = "pluggy" version = "1.2.0" description = "plugin and hook calling mechanisms for python" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1438,6 +1483,7 @@ testing = ["pytest", "pytest-benchmark"] name = "ply" version = "3.11" description = "Python Lex & Yacc" +category = "main" optional = false python-versions = "*" files = [ @@ -1449,6 +1495,7 @@ files = [ name = "prompt-toolkit" version = "3.0.39" description = "Library for building powerful interactive command lines in Python" +category = "dev" optional = false python-versions = ">=3.7.0" files = [ @@ -1463,6 +1510,7 @@ wcwidth = "*" name = "pyarrow" version = "12.0.1" description = "Python library for Apache Arrow" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1500,6 +1548,7 @@ numpy = ">=1.16.6" name = "pycparser" version = "2.21" description = "C parser in Python" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -1511,6 +1560,7 @@ files = [ name = "pygithub" version = "1.59.1" description = "Use the full Github API v3" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1528,6 +1578,7 @@ requests = ">=2.14.0" name = "pygments" version = "2.16.1" description = "Pygments is a syntax highlighting package written in Python." +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1542,6 +1593,7 @@ plugins = ["importlib-metadata"] name = "pyjwt" version = "2.8.0" description = "JSON Web Token implementation in Python" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1563,6 +1615,7 @@ tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"] name = "pynacl" version = "1.5.0" description = "Python binding to the Networking and Cryptography (NaCl) library" +category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -1589,6 +1642,7 @@ tests = ["hypothesis (>=3.27.0)", "pytest (>=3.2.1,!=3.3.0)"] name = "pyrsistent" version = "0.19.3" description = "Persistent/Functional/Immutable data structures" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1625,6 +1679,7 @@ files = [ name = "pytest" version = "7.4.2" description = "pytest: simple powerful testing with Python" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1648,6 +1703,7 @@ testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "no name = "pytest-durations" version = "1.2.0" description = "Pytest plugin reporting fixtures and test functions execution time." +category = "main" optional = true python-versions = ">=3.6.2" files = [ @@ -1662,6 +1718,7 @@ pytest = ">=4.6" name = "pytest-snapshot" version = "0.9.0" description = "A plugin for snapshot testing with pytest." +category = "dev" optional = false python-versions = ">=3.5" files = [ @@ -1676,6 +1733,7 @@ pytest = ">=3.0.0" name = "python-dateutil" version = "2.8.2" description = "Extensions to the standard Python datetime module" +category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" files = [ @@ -1690,6 +1748,7 @@ six = ">=1.5" name = "python-dotenv" version = "0.21.1" description = "Read key-value pairs from a .env file and set them as environment variables" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1704,6 +1763,7 @@ cli = ["click (>=5.0)"] name = "python-slugify" version = "8.0.1" description = "A Python slugify application that also handles Unicode" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1721,6 +1781,7 @@ unidecode = ["Unidecode (>=1.1.1)"] name = "pytz" version = "2023.3.post1" description = "World timezone definitions, modern and historical" +category = "main" optional = false python-versions = "*" files = [ @@ -1732,6 +1793,7 @@ files = [ name = "pytzdata" version = "2020.1" description = "The Olson timezone database for Python." +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -1743,6 +1805,7 @@ files = [ name = "pyyaml" version = "6.0.1" description = "YAML parser and emitter for Python" +category = "main" optional = false python-versions = ">=3.6" files = [ @@ -1802,6 +1865,7 @@ files = [ name = "questionary" version = "1.10.0" description = "Python library to build pretty command line user prompts ⭐️" +category = "dev" optional = false python-versions = ">=3.6,<4.0" files = [ @@ -1819,6 +1883,7 @@ docs = ["Sphinx (>=3.3,<4.0)", "sphinx-autobuild (>=2020.9.1,<2021.0.0)", "sphin name = "referencing" version = "0.8.9" description = "JSON Referencing + Python" +category = "dev" optional = false python-versions = "*" files = [ @@ -1835,6 +1900,7 @@ yarl = "*" name = "requests" version = "2.31.0" description = "Python HTTP for Humans." +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1856,6 +1922,7 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] name = "requests-mock" version = "1.11.0" description = "Mock out responses from the requests package" +category = "dev" optional = false python-versions = "*" files = [ @@ -1875,6 +1942,7 @@ test = ["fixtures", "mock", "purl", "pytest", "requests-futures", "sphinx", "tes name = "rich" version = "13.5.3" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +category = "dev" optional = false python-versions = ">=3.7.0" files = [ @@ -1894,6 +1962,7 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"] name = "s3transfer" version = "0.6.2" description = "An Amazon S3 Transfer Manager" +category = "main" optional = true python-versions = ">= 3.7" files = [ @@ -1911,6 +1980,7 @@ crt = ["botocore[crt] (>=1.20.29,<2.0a.0)"] name = "setuptools" version = "68.0.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1923,10 +1993,23 @@ docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-g testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] +[[package]] +name = "simpleeval" +version = "0.9.13" +description = "A simple, safe single expression evaluator library." +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "simpleeval-0.9.13-py2.py3-none-any.whl", hash = "sha256:22a2701a5006e4188d125d34accf2405c2c37c93f6b346f2484b6422415ae54a"}, + {file = "simpleeval-0.9.13.tar.gz", hash = "sha256:4a30f9cc01825fe4c719c785e3762623e350c4840d5e6855c2a8496baaa65fac"}, +] + [[package]] name = "simplejson" version = "3.19.2" description = "Simple, fast, extensible JSON encoder/decoder for Python" +category = "main" optional = false python-versions = ">=2.5, !=3.0.*, !=3.1.*, !=3.2.*" files = [ @@ -2034,6 +2117,7 @@ files = [ name = "six" version = "1.16.0" description = "Python 2 and 3 compatibility utilities" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" files = [ @@ -2045,6 +2129,7 @@ files = [ name = "snowballstemmer" version = "2.2.0" description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." +category = "main" optional = true python-versions = "*" files = [ @@ -2056,6 +2141,7 @@ files = [ name = "soupsieve" version = "2.4.1" description = "A modern CSS selector implementation for Beautiful Soup." +category = "main" optional = true python-versions = ">=3.7" files = [ @@ -2067,6 +2153,7 @@ files = [ name = "sphinx" version = "5.3.0" description = "Python documentation generator" +category = "main" optional = true python-versions = ">=3.6" files = [ @@ -2102,6 +2189,7 @@ test = ["cython", "html5lib", "pytest (>=4.6)", "typed_ast"] name = "sphinx-autobuild" version = "2021.3.14" description = "Rebuild Sphinx documentation on changes, with live-reload in the browser." +category = "main" optional = true python-versions = ">=3.6" files = [ @@ -2121,6 +2209,7 @@ test = ["pytest", "pytest-cov"] name = "sphinx-basic-ng" version = "1.0.0b2" description = "A modern skeleton for Sphinx themes." +category = "main" optional = true python-versions = ">=3.7" files = [ @@ -2138,6 +2227,7 @@ docs = ["furo", "ipython", "myst-parser", "sphinx-copybutton", "sphinx-inline-ta name = "sphinx-copybutton" version = "0.5.2" description = "Add a copy button to each of your code cells." +category = "main" optional = true python-versions = ">=3.7" files = [ @@ -2156,6 +2246,7 @@ rtd = ["ipython", "myst-nb", "sphinx", "sphinx-book-theme", "sphinx-examples"] name = "sphinx-inline-tabs" version = "2023.4.21" description = "Add inline tabbed content to your Sphinx documentation." +category = "main" optional = true python-versions = ">=3.8" files = [ @@ -2174,6 +2265,7 @@ test = ["pytest", "pytest-cov", "pytest-xdist"] name = "sphinx-reredirects" version = "0.1.2" description = "Handles redirects for moved pages in Sphinx documentation projects" +category = "main" optional = true python-versions = ">=3.5" files = [ @@ -2188,6 +2280,7 @@ sphinx = "*" name = "sphinxcontrib-applehelp" version = "1.0.2" description = "sphinxcontrib-applehelp is a sphinx extension which outputs Apple help books" +category = "main" optional = true python-versions = ">=3.5" files = [ @@ -2203,6 +2296,7 @@ test = ["pytest"] name = "sphinxcontrib-devhelp" version = "1.0.2" description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp document." +category = "main" optional = true python-versions = ">=3.5" files = [ @@ -2218,6 +2312,7 @@ test = ["pytest"] name = "sphinxcontrib-htmlhelp" version = "2.0.0" description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" +category = "main" optional = true python-versions = ">=3.6" files = [ @@ -2233,6 +2328,7 @@ test = ["html5lib", "pytest"] name = "sphinxcontrib-jsmath" version = "1.0.1" description = "A sphinx extension which renders display math in HTML via JavaScript" +category = "main" optional = true python-versions = ">=3.5" files = [ @@ -2247,6 +2343,7 @@ test = ["flake8", "mypy", "pytest"] name = "sphinxcontrib-qthelp" version = "1.0.3" description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp document." +category = "main" optional = true python-versions = ">=3.5" files = [ @@ -2262,6 +2359,7 @@ test = ["pytest"] name = "sphinxcontrib-serializinghtml" version = "1.1.5" description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)." +category = "main" optional = true python-versions = ">=3.5" files = [ @@ -2277,6 +2375,7 @@ test = ["pytest"] name = "sqlalchemy" version = "2.0.21" description = "Database Abstraction Library" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -2296,6 +2395,14 @@ files = [ {file = "SQLAlchemy-2.0.21-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:b69f1f754d92eb1cc6b50938359dead36b96a1dcf11a8670bff65fd9b21a4b09"}, {file = "SQLAlchemy-2.0.21-cp311-cp311-win32.whl", hash = "sha256:af520a730d523eab77d754f5cf44cc7dd7ad2d54907adeb3233177eeb22f271b"}, {file = "SQLAlchemy-2.0.21-cp311-cp311-win_amd64.whl", hash = "sha256:141675dae56522126986fa4ca713739d00ed3a6f08f3c2eb92c39c6dfec463ce"}, + {file = "SQLAlchemy-2.0.21-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:56628ca27aa17b5890391ded4e385bf0480209726f198799b7e980c6bd473bd7"}, + {file = "SQLAlchemy-2.0.21-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:db726be58837fe5ac39859e0fa40baafe54c6d54c02aba1d47d25536170b690f"}, + {file = "SQLAlchemy-2.0.21-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e7421c1bfdbb7214313919472307be650bd45c4dc2fcb317d64d078993de045b"}, + {file = "SQLAlchemy-2.0.21-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:632784f7a6f12cfa0e84bf2a5003b07660addccf5563c132cd23b7cc1d7371a9"}, + {file = "SQLAlchemy-2.0.21-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:f6f7276cf26145a888f2182a98f204541b519d9ea358a65d82095d9c9e22f917"}, + {file = "SQLAlchemy-2.0.21-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2a1f7ffac934bc0ea717fa1596f938483fb8c402233f9b26679b4f7b38d6ab6e"}, + {file = "SQLAlchemy-2.0.21-cp312-cp312-win32.whl", hash = "sha256:bfece2f7cec502ec5f759bbc09ce711445372deeac3628f6fa1c16b7fb45b682"}, + {file = "SQLAlchemy-2.0.21-cp312-cp312-win_amd64.whl", hash = "sha256:526b869a0f4f000d8d8ee3409d0becca30ae73f494cbb48801da0129601f72c6"}, {file = "SQLAlchemy-2.0.21-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:7614f1eab4336df7dd6bee05bc974f2b02c38d3d0c78060c5faa4cd1ca2af3b8"}, {file = "SQLAlchemy-2.0.21-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d59cb9e20d79686aa473e0302e4a82882d7118744d30bb1dfb62d3c47141b3ec"}, {file = "SQLAlchemy-2.0.21-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a95aa0672e3065d43c8aa80080cdd5cc40fe92dc873749e6c1cf23914c4b83af"}, @@ -2356,6 +2463,7 @@ sqlcipher = ["sqlcipher3-binary"] name = "termcolor" version = "2.3.0" description = "ANSI color formatting for output in terminal" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -2370,6 +2478,7 @@ tests = ["pytest", "pytest-cov"] name = "text-unidecode" version = "1.3" description = "The most basic Text::Unidecode port" +category = "dev" optional = false python-versions = "*" files = [ @@ -2381,6 +2490,7 @@ files = [ name = "time-machine" version = "2.10.0" description = "Travel through time in your tests." +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -2447,6 +2557,7 @@ python-dateutil = "*" name = "tomli" version = "2.0.1" description = "A lil' TOML parser" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -2458,6 +2569,7 @@ files = [ name = "tomlkit" version = "0.12.1" description = "Style preserving TOML library" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -2469,6 +2581,7 @@ files = [ name = "tornado" version = "6.2" description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed." +category = "main" optional = true python-versions = ">= 3.7" files = [ @@ -2489,6 +2602,7 @@ files = [ name = "typed-ast" version = "1.5.5" description = "a fork of Python 2 and 3 ast modules with type comment support" +category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -2539,6 +2653,7 @@ files = [ name = "types-jsonschema" version = "4.19.0.1" description = "Typing stubs for jsonschema" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -2553,6 +2668,7 @@ referencing = "*" name = "types-python-dateutil" version = "2.8.19.14" description = "Typing stubs for python-dateutil" +category = "dev" optional = false python-versions = "*" files = [ @@ -2564,6 +2680,7 @@ files = [ name = "types-pytz" version = "2023.3.1.1" description = "Typing stubs for pytz" +category = "dev" optional = false python-versions = "*" files = [ @@ -2575,6 +2692,7 @@ files = [ name = "types-pyyaml" version = "6.0.12.12" description = "Typing stubs for PyYAML" +category = "dev" optional = false python-versions = "*" files = [ @@ -2586,6 +2704,7 @@ files = [ name = "types-requests" version = "2.31.0.6" description = "Typing stubs for requests" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -2600,6 +2719,7 @@ types-urllib3 = "*" name = "types-simplejson" version = "3.19.0.2" description = "Typing stubs for simplejson" +category = "dev" optional = false python-versions = "*" files = [ @@ -2611,6 +2731,7 @@ files = [ name = "types-urllib3" version = "1.26.25.14" description = "Typing stubs for urllib3" +category = "dev" optional = false python-versions = "*" files = [ @@ -2622,6 +2743,7 @@ files = [ name = "typing-extensions" version = "4.7.1" description = "Backported and Experimental Type Hints for Python 3.7+" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -2633,6 +2755,7 @@ files = [ name = "urllib3" version = "1.26.17" description = "HTTP library with thread-safe connection pooling, file post, and more." +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" files = [ @@ -2649,6 +2772,7 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] name = "wcwidth" version = "0.2.6" description = "Measures the displayed width of unicode strings in a terminal" +category = "dev" optional = false python-versions = "*" files = [ @@ -2660,6 +2784,7 @@ files = [ name = "wrapt" version = "1.15.0" description = "Module for decorators, wrappers and monkey patching." +category = "dev" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" files = [ @@ -2744,6 +2869,7 @@ files = [ name = "xdoctest" version = "1.1.1" description = "A rewrite of the builtin doctest module" +category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -2771,6 +2897,7 @@ tests-strict = ["codecov (==2.0.15)", "pytest (==4.6.0)", "pytest (==4.6.0)", "p name = "yarl" version = "1.9.2" description = "Yet another URL library" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -2859,6 +2986,7 @@ typing-extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""} name = "zipp" version = "3.15.0" description = "Backport of pathlib-compatible object wrapper for zip files" +category = "main" optional = false python-versions = ">=3.7" files = [ @@ -2878,4 +3006,4 @@ testing = ["pytest", "pytest-durations"] [metadata] lock-version = "2.0" python-versions = ">=3.7.1,<4" -content-hash = "7dda84407ad4237de30a7a840872e9c3979680b6ef4128ebc5d959d1805b635c" +content-hash = "b1e49eacb32e9a38016d024dc71b58408c92b2a41c5b6c0de7f935b9449f91c0" diff --git a/pyproject.toml b/pyproject.toml index 1d878b830f..000dd672d6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -79,6 +79,7 @@ fs-s3fs = {version = ">=1.1.1", optional = true} # Testing dependencies installed as optional 'testing' extras pytest = {version=">=7.2.1", optional = true} pytest-durations = {version = ">=1.2.0", optional = true} +simpleeval = "^0.9.13" [tool.poetry.extras] docs = [ diff --git a/singer_sdk/helpers/_simpleeval.py b/singer_sdk/helpers/_simpleeval.py deleted file mode 100644 index c3fb41c3fe..0000000000 --- a/singer_sdk/helpers/_simpleeval.py +++ /dev/null @@ -1,679 +0,0 @@ -""" -Simpleeval module originally imported on 2021-09-16 from: -- https://github.com/danthedeckie/simpleeval - -For more information: -- https://gitlab.com/meltano/sdk/-/issues/213 - -------------------------------------- -SimpleEval - (C) 2013-2019 Daniel Fairhead -------------------------------------- - -An short, easy to use, safe and reasonably extensible expression evaluator. -Designed for things like in a website where you want to allow the user to -generate a string, or a number from some other input, without allowing full -eval() or other unsafe or needlessly complex linguistics. - -------------------------------------- - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -------------------------------------- - -Initial idea copied from J.F. Sebastian on Stack Overflow -( http://stackoverflow.com/a/9558001/1973500 ) with -modifications and many improvements. - -------------------------------------- -Contributors: -- corro (Robin Baumgartner) (py3k) -- dratchkov (David R) (nested dicts) -- marky1991 (Mark Young) (slicing) -- T045T (Nils Berg) (!=, py3kstr, obj. -- perkinslr (Logan Perkins) (.__globals__ or .func_ breakouts) -- impala2 (Kirill Stepanov) (massive _eval refactor) -- gk (ugik) (Other iterables than str can DOS too, and can be made) -- daveisfera (Dave Johansen) 'not' Boolean op, Pycharm, pep8, various other fixes -- xaled (Khalid Grandi) method chaining correctly, double-eval bugfix. -- EdwardBetts (Edward Betts) spelling correction. -- charlax (Charles-Axel Dein charlax) Makefile and cleanups -- mommothazaz123 (Andrew Zhu) f"string" support, Python 3.8 support -- lubieowoce (Uryga) various potential vulnerabilities -- JCavallo (Jean Cavallo) names dict shouldn't be modified -- Birne94 (Daniel Birnstiel) for fixing leaking generators. -- patricksurry (Patrick Surry) or should return last value, even if falsy. -- shughes-uk (Samantha Hughes) python w/o 'site' should not fail to import. - -------------------------------------- -Basic Usage: - ->>> s = SimpleEval() ->>> s.eval("20 + 30") -50 - -You can add your own functions easily too: - -if file.txt contents is "11" - ->>> def get_file(): -... with open("file.txt", 'r') as f: -... return f.read() - ->>> s.functions["get_file"] = get_file ->>> s.eval("int(get_file()) + 31") -42 - -For more information, see the full package documentation on pypi, or the github -repo. - ------------ - -If you don't need to re-use the evaluator (with it's names, functions, etc), -then you can use the simple_eval() function: - ->>> simple_eval("21 + 19") -40 - -You can pass names, operators and functions to the simple_eval function as -well: - ->>> simple_eval("40 + two", names={"two": 2}) -42 - -""" -# flake8: noqa # Ignoring flake errors in imported module -# isort: dont-add-imports - -import ast -import operator as op -import sys -import warnings -from random import random - -PYTHON3 = sys.version_info[0] == 3 - -######################################## -# Module wide 'globals' - -MAX_STRING_LENGTH = 100000 -MAX_COMPREHENSION_LENGTH = 10000 -MAX_POWER = 4000000 # highest exponent -DISALLOW_PREFIXES = ["_", "func_"] -DISALLOW_METHODS = ["format", "format_map", "mro"] - -# Disallow functions: -# This, strictly speaking, is not necessary. These /should/ never be accessable anyway, -# if DISALLOW_PREFIXES and DISALLOW_METHODS are all right. This is here to try and help -# people not be stupid. Allowing these functions opens up all sorts of holes - if any of -# 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} -if hasattr(__builtins__, "help") or ( - hasattr(__builtins__, "__contains__") and "help" in __builtins__ -): - # PyInstaller environment doesn't include this module. - DISALLOW_FUNCTIONS.add(help) - - -if PYTHON3: - exec("DISALLOW_FUNCTIONS.add(exec)") # exec is not a function in Python2... - - -######################################## -# Exceptions: - - -class InvalidExpression(Exception): - """ Generic Exception """ - - pass - - -class FunctionNotDefined(InvalidExpression): - """ sorry! That function isn't defined! """ - - def __init__(self, func_name, expression): - self.message = "Function '{0}' not defined," " for expression '{1}'.".format( - func_name, expression - ) - setattr(self, "func_name", func_name) # bypass 2to3 confusion. - self.expression = expression - - # pylint: disable=bad-super-call - super(InvalidExpression, self).__init__(self.message) - - -class NameNotDefined(InvalidExpression): - """ a name isn't defined. """ - - def __init__(self, name, expression): - self.name = name - self.message = "'{0}' is not defined for expression '{1}'".format( - name, expression - ) - self.expression = expression - - # pylint: disable=bad-super-call - super(InvalidExpression, self).__init__(self.message) - - -class AttributeDoesNotExist(InvalidExpression): - """attribute does not exist""" - - def __init__(self, attr, expression): - self.message = "Attribute '{0}' does not exist in expression '{1}'".format( - attr, expression - ) - self.attr = attr - self.expression = expression - - -class FeatureNotAvailable(InvalidExpression): - """ What you're trying to do is not allowed. """ - - pass - - -class NumberTooHigh(InvalidExpression): - """Sorry! That number is too high. I don't want to spend the - next 10 years evaluating this expression!""" - - pass - - -class IterableTooLong(InvalidExpression): - """ That iterable is **way** too long, baby. """ - - pass - - -class AssignmentAttempted(UserWarning): - pass - - -######################################## -# Default simple functions to include: - - -def random_int(top): - """ return a random int below """ - - return int(random() * top) - - -def safe_power(a, b): # pylint: disable=invalid-name - """ a limited exponent/to-the-power-of function, for safety reasons """ - - if abs(a) > MAX_POWER or abs(b) > MAX_POWER: - raise NumberTooHigh("Sorry! I don't want to evaluate {0} ** {1}".format(a, b)) - return a ** b - - -def safe_mult(a, b): # pylint: disable=invalid-name - """ limit the number of times an iterable can be repeated... """ - - if hasattr(a, "__len__") and b * len(a) > MAX_STRING_LENGTH: - raise IterableTooLong("Sorry, I will not evalute something that long.") - if hasattr(b, "__len__") and a * len(b) > MAX_STRING_LENGTH: - raise IterableTooLong("Sorry, I will not evalute something that long.") - - return a * b - - -def safe_add(a, b): # pylint: disable=invalid-name - """ iterable length limit again """ - - if hasattr(a, "__len__") and hasattr(b, "__len__"): - if len(a) + len(b) > MAX_STRING_LENGTH: - raise IterableTooLong( - "Sorry, adding those two together would" " make something too long." - ) - return a + b - - -######################################## -# Defaults for the evaluator: - -DEFAULT_OPERATORS = { - ast.Add: safe_add, - ast.Sub: op.sub, - ast.Mult: safe_mult, - ast.Div: op.truediv, - ast.FloorDiv: op.floordiv, - ast.Pow: safe_power, - ast.Mod: op.mod, - ast.Eq: op.eq, - ast.NotEq: op.ne, - ast.Gt: op.gt, - ast.Lt: op.lt, - ast.GtE: op.ge, - ast.LtE: op.le, - ast.Not: op.not_, - ast.USub: op.neg, - ast.UAdd: op.pos, - ast.In: lambda x, y: op.contains(y, x), - ast.NotIn: lambda x, y: not op.contains(y, x), - ast.Is: lambda x, y: x is y, - ast.IsNot: lambda x, y: x is not y, -} - -DEFAULT_FUNCTIONS = { - "rand": random, - "randint": random_int, - "int": int, - "float": float, - "str": str if PYTHON3 else unicode, # type: ignore # 'unicode' not defined -} - -DEFAULT_NAMES = {"True": True, "False": False, "None": None} - -ATTR_INDEX_FALLBACK = True - - -######################################## -# And the actual evaluator: - - -class SimpleEval(object): # pylint: disable=too-few-public-methods - """A very simple expression parser. - >>> s = SimpleEval() - >>> s.eval("20 + 30 - ( 10 * 5)") - 0 - """ - - expr = "" - - def __init__(self, operators=None, functions=None, names=None): - """ - Create the evaluator instance. Set up valid operators (+,-, etc) - functions (add, random, get_val, whatever) and names.""" - - if not operators: - operators = DEFAULT_OPERATORS.copy() - if not functions: - functions = DEFAULT_FUNCTIONS.copy() - if not names: - names = DEFAULT_NAMES.copy() - - self.operators = operators - self.functions = functions - self.names = names - - self.nodes = { - ast.Expr: self._eval_expr, - 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, - ast.BoolOp: self._eval_boolop, - ast.Compare: self._eval_compare, - ast.IfExp: self._eval_ifexp, - ast.Call: self._eval_call, - ast.keyword: self._eval_keyword, - ast.Subscript: self._eval_subscript, - ast.Attribute: self._eval_attribute, - ast.Index: self._eval_index, - ast.Slice: self._eval_slice, - } - - # 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 - - # Check for forbidden functions: - - for f in self.functions.values(): - if f in DISALLOW_FUNCTIONS: - raise FeatureNotAvailable( - "This function {} is a really bad idea.".format(f) - ) - - def eval(self, expr): - """evaluate an expresssion, using the operators, functions and - names previously set up.""" - - # set a copy of the expression aside, so we can give nice errors... - - self.expr = expr - - # and evaluate: - return self._eval(ast.parse(expr.strip()).body[0]) - - def _eval(self, node): - """ The internal evaluator used on each node in the parsed tree. """ - - try: - handler = self.nodes[type(node)] - except KeyError: - raise FeatureNotAvailable( - "Sorry, {0} is not available in this " - "evaluator".format(type(node).__name__) - ) - - return handler(node) - - def _eval_expr(self, node): - return self._eval(node.value) - - def _eval_assign(self, node): - warnings.warn( - "Assignment ({}) attempted, but this is ignored".format(self.expr), - AssignmentAttempted, - ) - return self._eval(node.value) - - def _eval_aug_assign(self, node): - warnings.warn( - "Assignment ({}) attempted, but this is ignored".format(self.expr), - AssignmentAttempted, - ) - return self._eval(node.value) - - def _eval_import(self, node): - raise FeatureNotAvailable("Sorry, 'import' is not allowed.") - return self._eval(node.value) - - @staticmethod - def _eval_num(node): - return node.n - - @staticmethod - 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) - ) - return node.s - - @staticmethod - 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) - ) - return node.value - - def _eval_unaryop(self, node): - return self.operators[type(node.op)](self._eval(node.operand)) - - def _eval_binop(self, node): - return self.operators[type(node.op)]( - self._eval(node.left), self._eval(node.right) - ) - - def _eval_boolop(self, node): - if isinstance(node.op, ast.And): - vout = False - for value in node.values: - vout = self._eval(value) - if not vout: - return vout - return vout - elif isinstance(node.op, ast.Or): - for value in node.values: - vout = self._eval(value) - if vout: - return vout - return vout - - def _eval_compare(self, node): - right = self._eval(node.left) - to_return = True - for operation, comp in zip(node.ops, node.comparators): - if not to_return: - break - left = right - right = self._eval(comp) - to_return = self.operators[type(operation)](left, right) - return to_return - - def _eval_ifexp(self, node): - return ( - self._eval(node.body) if self._eval(node.test) else self._eval(node.orelse) - ) - - def _eval_call(self, node): - if isinstance(node.func, ast.Attribute): - func = self._eval(node.func) - else: - try: - func = self.functions[node.func.id] - except KeyError: - raise FunctionNotDefined(node.func.id, self.expr) - except AttributeError as e: - raise FeatureNotAvailable("Lambda Functions not implemented") - - if func in DISALLOW_FUNCTIONS: - raise FeatureNotAvailable("This function is forbidden") - - return func( - *(self._eval(a) for a in node.args), - **dict(self._eval(k) for k in node.keywords) - ) - - def _eval_keyword(self, node): - return node.arg, self._eval(node.value) - - 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 - # (the compiler rejects it, so you can't even - # pass that to ast.parse) - if hasattr(self.names, "__getitem__"): - return self.names[node.id] - elif callable(self.names): - return self.names(node) - else: - 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] - - raise NameNotDefined(node.id, self.expr) - - def _eval_subscript(self, node): - container = self._eval(node.value) - key = self._eval(node.slice) - try: - return container[key] - except KeyError: - raise - - def _eval_attribute(self, node): - for prefix in DISALLOW_PREFIXES: - if node.attr.startswith(prefix): - raise FeatureNotAvailable( - "Sorry, access to __attributes " - " or func_ attributes is not available. " - "({0})".format(node.attr) - ) - if node.attr in DISALLOW_METHODS: - raise FeatureNotAvailable( - "Sorry, this method is not available. " "({0})".format(node.attr) - ) - # eval node - node_evaluated = self._eval(node.value) - - # Maybe the base object is an actual object, not just a dict - try: - return getattr(node_evaluated, node.attr) - except (AttributeError, TypeError): - pass - - # TODO: is this a good idea? Try and look for [x] if .x doesn't work? - if self.ATTR_INDEX_FALLBACK: - try: - return node_evaluated[node.attr] - except (KeyError, TypeError): - pass - - # If it is neither, raise an exception - raise AttributeDoesNotExist(node.attr, self.expr) - - def _eval_index(self, node): - return self._eval(node.value) - - def _eval_slice(self, node): - lower = upper = step = None - if node.lower is not None: - lower = self._eval(node.lower) - if node.upper is not None: - upper = self._eval(node.upper) - if node.step is not None: - step = self._eval(node.step) - return slice(lower, upper, step) - - def _eval_joinedstr(self, node): - length = 0 - evaluated_values = [] - for n in node.values: - val = str(self._eval(n)) - if len(val) + length > MAX_STRING_LENGTH: - raise IterableTooLong("Sorry, I will not evaluate something this long.") - evaluated_values.append(val) - return "".join(evaluated_values) - - def _eval_formattedvalue(self, node): - if node.format_spec: - fmt = "{:" + self._eval(node.format_spec) + "}" - return fmt.format(self._eval(node.value)) - return self._eval(node.value) - - -class EvalWithCompoundTypes(SimpleEval): - """ - SimpleEval with additional Compound Types, and their respective - function editions. (list, tuple, dict, set). - """ - - def __init__(self, operators=None, functions=None, names=None): - super(EvalWithCompoundTypes, self).__init__(operators, functions, names) - - self.functions.update(list=list, tuple=tuple, dict=dict, set=set) - - self.nodes.update( - { - ast.Dict: self._eval_dict, - ast.Tuple: self._eval_tuple, - ast.List: self._eval_list, - ast.Set: self._eval_set, - ast.ListComp: self._eval_comprehension, - ast.GeneratorExp: self._eval_comprehension, - } - ) - - def eval(self, expr): - self._max_count = 0 - return super(EvalWithCompoundTypes, self).eval(expr) - - def _eval_dict(self, node): - return {self._eval(k): self._eval(v) for (k, v) in zip(node.keys, node.values)} - - def _eval_tuple(self, node): - return tuple(self._eval(x) for x in node.elts) - - def _eval_list(self, node): - return list(self._eval(x) for x in node.elts) - - def _eval_set(self, node): - return set(self._eval(x) for x in node.elts) - - def _eval_comprehension(self, node): - to_return = [] - - extra_names = {} - - previous_name_evaller = self.nodes[ast.Name] - - def eval_names_extra(node): - """ - Here we hide our extra scope for within this comprehension - """ - if node.id in extra_names: - return extra_names[node.id] - return previous_name_evaller(node) - - self.nodes.update({ast.Name: eval_names_extra}) - - def recurse_targets(target, value): - """ - Recursively (enter, (into, (nested, name), unpacking)) = \ - and, (assign, (values, to), each - """ - if isinstance(target, ast.Name): - extra_names[target.id] = value - else: - for t, v in zip(target.elts, value): - recurse_targets(t, v) - - def do_generator(gi=0): - g = node.generators[gi] - for i in self._eval(g.iter): - self._max_count += 1 - - if self._max_count > MAX_COMPREHENSION_LENGTH: - raise IterableTooLong("Comprehension generates too many elements") - recurse_targets(g.target, i) - if all(self._eval(iff) for iff in g.ifs): - if len(node.generators) > gi + 1: - do_generator(gi + 1) - else: - to_return.append(self._eval(node.elt)) - - try: - do_generator() - finally: - self.nodes.update({ast.Name: previous_name_evaller}) - - return to_return - - -def simple_eval(expr, operators=None, functions=None, names=None): - """ Simply evaluate an expresssion """ - s = SimpleEval(operators=operators, functions=functions, names=names) - return s.eval(expr) diff --git a/singer_sdk/mapper.py b/singer_sdk/mapper.py index 9b56d5089f..b6e8b11dba 100644 --- a/singer_sdk/mapper.py +++ b/singer_sdk/mapper.py @@ -6,15 +6,17 @@ from __future__ import annotations import abc +import ast import copy import datetime import hashlib import logging import typing as t +import simpleeval + import singer_sdk.typing as th from singer_sdk.exceptions import MapExpressionError, StreamMapConfigError -from singer_sdk.helpers import _simpleeval as simpleeval from singer_sdk.helpers._catalog import get_selected_schema from singer_sdk.helpers._flattening import ( FlatteningOptions, @@ -54,27 +56,6 @@ def md5(string: str) -> str: return hashlib.md5(string.encode("utf-8")).hexdigest() # noqa: S324 -def compound_eval( - expr: str, - operators: t.Dict[str, t.Callable[[dict], dict | None]] = None, - functions: t.Dict[str, t.Callable[[dict], dict | None]] = None, - names=t.Dict[str, t.Any] - ) -> t.Union[str, int, float, t.List, t.Set, t.Dict]: - """Evaluate inline maps using the `EvalWithCompoundTypes` class - - Args: - expr: expression to evaluate - operators: dictionary of operators and the functions they map to in the evaluation context - functions: dictionary of function names and definitions available in the evaluation context - names: dictionary of variable names available in the evaluation context - - Returns: - result of the evaluated expression - """ - s = simpleeval.EvalWithCompoundTypes(operators=operators, functions=functions, names=names) - return s.eval(expr) - - StreamMapsDict: TypeAlias = t.Dict[str, t.Union[str, dict, None]] @@ -280,6 +261,7 @@ def __init__( self._transform_fn, self.transformed_schema, ) = self._init_functions_and_schema(stream_map=map_transform) + self.expr_evaluator = simpleeval.EvalWithCompoundTypes(functions=self.functions) def transform(self, record: dict) -> dict | None: """Return a transformed record. @@ -323,13 +305,15 @@ def functions(self) -> dict[str, t.Callable]: def _eval( self, expr: str, + expr_parsed: ast.Expr, record: dict, property_name: str | None, ) -> str | int | float: """Solve an expression. Args: - expr: String expression to evaluate. + expr: String expression to evaluate (used to raise human readable errors). + expr_parsed: Parsed expression abstract syntax tree. record: Individual stream record. property_name: Name of property to transform in the record. @@ -347,10 +331,10 @@ def _eval( # Allow access to original property value if applicable names["self"] = record[property_name] try: - result: str | int | float = compound_eval( + self.expr_evaluator.names = names + result: str | int | float = self.expr_evaluator.eval( expr, - functions=self.functions, - names=names, + previously_parsed=expr_parsed, ) except (simpleeval.InvalidExpression, SyntaxError) as ex: msg = f"Failed to evaluate simpleeval expressions {expr}." @@ -398,7 +382,7 @@ def _eval_type( if expr.startswith("bool("): return th.BooleanType() - + return th.StringType() if expr[0] == "'" and expr[-1] == "'" else default def _init_functions_and_schema( # noqa: PLR0912, PLR0915, C901 @@ -416,6 +400,7 @@ def _init_functions_and_schema( # noqa: PLR0912, PLR0915, C901 Raises: NotImplementedError: TODO StreamMapConfigError: TODO + MapExpressionError: TODO """ stream_map = copy.copy(stream_map) @@ -423,6 +408,12 @@ def _init_functions_and_schema( # noqa: PLR0912, PLR0915, C901 include_by_default = True if stream_map and MAPPER_FILTER_OPTION in stream_map: filter_rule = stream_map.pop(MAPPER_FILTER_OPTION) + try: + filter_rule_parsed: ast.Expr = ast.parse(filter_rule).body[0] + except (SyntaxError, IndexError) as ex: + msg = f"Failed to parse expression {filter_rule}." + raise MapExpressionError(msg) from ex + logging.info( "Found '%s' filter rule: %s", self.stream_alias, @@ -465,6 +456,7 @@ def _init_functions_and_schema( # noqa: PLR0912, PLR0915, C901 if "properties" not in transformed_schema: transformed_schema["properties"] = {} + stream_map_parsed: list[tuple[str, str | None, ast.Expr | None]] = [] for prop_key, prop_def in list(stream_map.items()): if prop_def in {None, NULL_STRING}: if prop_key in (self.transformed_key_properties or []): @@ -477,6 +469,7 @@ def _init_functions_and_schema( # noqa: PLR0912, PLR0915, C901 ) raise StreamMapConfigError(msg) transformed_schema["properties"].pop(prop_key, None) + stream_map_parsed.append((prop_key, prop_def, None)) elif isinstance(prop_def, str): default_type: th.JSONTypeHelper = th.StringType() # Fallback to string existing_schema: dict = ( @@ -495,6 +488,13 @@ def _init_functions_and_schema( # noqa: PLR0912, PLR0915, C901 self._eval_type(prop_def, default=default_type), ).to_dict(), ) + try: + parsed_def: ast.Expr = ast.parse(prop_def).body[0] + stream_map_parsed.append((prop_key, prop_def, parsed_def)) + except (SyntaxError, IndexError) as ex: + msg = f"Failed to parse expression {prop_def}." + raise MapExpressionError(msg) from ex + else: msg = ( f"Unexpected type '{type(prop_def).__name__}' in stream map for " @@ -516,10 +516,14 @@ def _init_functions_and_schema( # noqa: PLR0912, PLR0915, C901 # Declare function variables - def eval_filter(filter_rule: str) -> t.Callable[[dict], bool]: + def eval_filter( + filter_rule: str, + filter_rule_parsed: ast.Expr, + ) -> t.Callable[[dict], bool]: def _inner(record: dict) -> bool: filter_result = self._eval( expr=filter_rule, + expr_parsed=filter_rule_parsed, record=record, property_name=None, ) @@ -541,7 +545,7 @@ def always_true(record: dict) -> bool: return True if isinstance(filter_rule, str): - filter_fn = eval_filter(filter_rule) + filter_fn = eval_filter(filter_rule, filter_rule_parsed) elif filter_rule is None: filter_fn = always_true else: @@ -566,16 +570,17 @@ def transform_fn(record: dict) -> dict | None: if key_property in record: result[key_property] = record[key_property] - for prop_key, prop_def in list(stream_map.items()): + for prop_key, prop_def, prop_def_parsed in stream_map_parsed: if prop_def in {None, NULL_STRING}: # Remove property from result result.pop(prop_key, None) continue - if isinstance(prop_def, str): + if isinstance(prop_def_parsed, ast.Expr): # Apply property transform result[prop_key] = self._eval( expr=prop_def, + expr_parsed=prop_def_parsed, record=record, property_name=prop_key, ) diff --git a/tests/core/test_simpleeval.py b/tests/core/test_simpleeval.py index d5cacb30f1..8e7c06f526 100644 --- a/tests/core/test_simpleeval.py +++ b/tests/core/test_simpleeval.py @@ -18,8 +18,8 @@ import unittest import warnings -from singer_sdk.helpers import _simpleeval as simpleeval -from singer_sdk.helpers._simpleeval import ( +import simpleeval +from simpleeval import ( AttributeDoesNotExist, EvalWithCompoundTypes, FeatureNotAvailable,