├── tests ├── __init__.py ├── test_dispatch.py └── test_core.py ├── .github ├── FUNDING.yml └── workflows │ ├── pypi.yml │ └── tests.yml ├── .gitattributes ├── setup.cfg ├── etuples ├── __init__.py ├── dispatch.py └── core.py ├── requirements.txt ├── MANIFEST.in ├── .pre-commit-config.yaml ├── Makefile ├── pyproject.toml ├── .gitignore ├── README.md ├── LICENSE └── .pylintrc /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [brandonwillard] 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | etuples/_version.py export-subst 2 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 88 3 | extend-ignore = E203, W503 4 | per-file-ignores = 5 | **/__init__.py:F401,E402,F403 6 | -------------------------------------------------------------------------------- /etuples/__init__.py: -------------------------------------------------------------------------------- 1 | import importlib.metadata 2 | 3 | from .core import etuple 4 | from .dispatch import apply, arguments, etuplize, operator, rands, rator, term 5 | 6 | __version__ = importlib.metadata.version("etuples") 7 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | -e ./ 2 | logical-unification 3 | coveralls 4 | pydocstyle>=3.0.0 5 | pytest>=5.0.0 6 | pytest-cov>=2.6.1 7 | pytest-html>=1.20.0 8 | pylint>=2.3.1 9 | black>=19.3b0; platform.python_implementation!='PyPy' 10 | diff-cover 11 | versioneer 12 | isort 13 | coverage>=5.1 14 | pre-commit 15 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | # Exclude entire directories 2 | prune .github 3 | prune .pytest_cache 4 | prune .tox 5 | prune .nox 6 | prune build 7 | prune dist 8 | prune .ropeproject 9 | prune .vscode 10 | prune .idea 11 | prune .venv 12 | prune venv 13 | prune env 14 | 15 | # Exclude file patterns 16 | global-exclude .DS_Store 17 | global-exclude __pycache__ 18 | global-exclude *.py[cod] 19 | global-exclude *.so 20 | global-exclude .git* 21 | global-exclude .coverage 22 | global-exclude *.egg-info 23 | global-exclude *.swp 24 | global-exclude *.swo 25 | global-exclude *~ 26 | global-exclude .env 27 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | exclude: | 2 | (?x)^( 3 | doc/.*| 4 | bin/.* 5 | )$ 6 | repos: 7 | - repo: https://github.com/pre-commit/pre-commit-hooks 8 | rev: v4.5.0 9 | hooks: 10 | - id: debug-statements 11 | - id: check-merge-conflict 12 | - repo: https://github.com/psf/black 13 | rev: 23.12.1 14 | hooks: 15 | - id: black 16 | language_version: python3 17 | - repo: https://github.com/pycqa/flake8 18 | rev: 7.0.0 19 | hooks: 20 | - id: flake8 21 | - repo: https://github.com/pycqa/isort 22 | rev: 5.13.2 23 | hooks: 24 | - id: isort 25 | - repo: https://github.com/PyCQA/autoflake 26 | rev: v2.2.1 27 | hooks: 28 | - id: autoflake 29 | exclude: | 30 | (?x)^( 31 | .*/?__init__\.py| 32 | )$ 33 | args: 34 | - --in-place 35 | - --remove-all-unused-imports 36 | - --remove-unused-variables 37 | - repo: https://github.com/pre-commit/mirrors-mypy 38 | rev: v1.8.0 39 | hooks: 40 | - id: mypy 41 | additional_dependencies: 42 | - types-setuptools 43 | -------------------------------------------------------------------------------- /.github/workflows/pypi.yml: -------------------------------------------------------------------------------- 1 | name: PyPI 2 | on: 3 | push: 4 | branches: 5 | - main 6 | - auto-release 7 | pull_request: 8 | branches: [main] 9 | release: 10 | types: [published] 11 | 12 | # Cancels all previous workflow runs for pull requests that have not completed. 13 | concurrency: 14 | # The concurrency group contains the workflow name and the branch name for pull requests 15 | # or the commit hash for any other events. 16 | group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.head_ref || github.sha }} 17 | cancel-in-progress: true 18 | 19 | jobs: 20 | build: 21 | name: Build distributions 22 | runs-on: ubuntu-latest 23 | steps: 24 | - uses: actions/checkout@v4 25 | with: 26 | fetch-depth: 0 27 | - uses: actions/setup-python@v4 28 | with: 29 | python-version: "3.9" 30 | - name: Install build dependencies 31 | run: | 32 | python -m pip install --upgrade pip 33 | python -m pip install build 34 | - name: Build distributions 35 | run: | 36 | python -m build 37 | - name: Check the sdist installs and imports 38 | run: | 39 | mkdir -p test-sdist 40 | cd test-sdist 41 | python -m venv venv-sdist 42 | venv-sdist/bin/python -m pip install ../dist/etuples-*.tar.gz 43 | venv-sdist/bin/python -c "import etuples; print(etuples.__version__)" 44 | - name: Check the wheel installs and imports 45 | run: | 46 | mkdir -p test-wheel 47 | cd test-wheel 48 | python -m venv venv-wheel 49 | venv-wheel/bin/python -m pip install ../dist/etuples-*.whl 50 | venv-wheel/bin/python -c "import etuples; print(etuples.__version__)" 51 | - uses: actions/upload-artifact@v4 52 | with: 53 | name: artifact 54 | path: dist/* 55 | 56 | upload_pypi: 57 | name: Upload to PyPI on release 58 | needs: [build] 59 | runs-on: ubuntu-latest 60 | if: github.event_name == 'release' && github.event.action == 'published' 61 | steps: 62 | - uses: actions/download-artifact@v4 63 | with: 64 | name: artifact 65 | path: dist 66 | - uses: pypa/gh-action-pypi-publish@release/v1 67 | with: 68 | user: __token__ 69 | password: ${{ secrets.pypi_secret }} 70 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: help venv conda docker docstyle format style black test lint check coverage pypi 2 | .DEFAULT_GOAL = help 3 | 4 | PYTHON = python 5 | PIP = pip 6 | CONDA = conda 7 | SHELL = bash 8 | 9 | help: 10 | @printf "Usage:\n" 11 | @grep -E '^[a-zA-Z_-]+:.*?# .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?# "}; {printf "\033[1;34mmake %-10s\033[0m%s\n", $$1, $$2}' 12 | 13 | conda: # Set up a conda environment for development. 14 | @printf "Creating conda environment...\n" 15 | ${CONDA} create --yes --name etuples-env python=3.6 16 | ( \ 17 | ${CONDA} activate etuples-env; \ 18 | ${PIP} install -U pip; \ 19 | ${PIP} install -r requirements.txt; \ 20 | ${PIP} install -r requirements-dev.txt; \ 21 | ${CONDA} deactivate; \ 22 | ) 23 | @printf "\n\nConda environment created! \033[1;34mRun \`conda activate etuples-env\` to activate it.\033[0m\n\n\n" 24 | 25 | venv: # Set up a Python virtual environment for development. 26 | @printf "Creating Python virtual environment...\n" 27 | rm -rf etuples-venv 28 | ${PYTHON} -m venv etuples-venv 29 | ( \ 30 | source etuples-venv/bin/activate; \ 31 | ${PIP} install -U pip; \ 32 | ${PIP} install -r requirements.txt; \ 33 | ${PIP} install -r requirements-dev.txt; \ 34 | deactivate; \ 35 | ) 36 | @printf "\n\nVirtual environment created! \033[1;34mRun \`source etuples-venv/bin/activate\` to activate it.\033[0m\n\n\n" 37 | 38 | docker: # Set up a Docker image for development. 39 | @printf "Creating Docker image...\n" 40 | ${SHELL} ./scripts/container.sh --build 41 | 42 | docstyle: 43 | @printf "Checking documentation with pydocstyle...\n" 44 | pydocstyle etuples/ 45 | @printf "\033[1;34mPydocstyle passes!\033[0m\n\n" 46 | 47 | format: 48 | @printf "Checking code style with black...\n" 49 | black --check etuples/ 50 | @printf "\033[1;34mBlack passes!\033[0m\n\n" 51 | 52 | style: 53 | @printf "Checking code style with pylint...\n" 54 | pylint etuples/ tests/ 55 | @printf "\033[1;34mPylint passes!\033[0m\n\n" 56 | 57 | black: # Format code in-place using black. 58 | black etuples/ tests/ 59 | 60 | test: # Test code using pytest. 61 | pytest -v tests/ etuples/ --cov=etuples/ --cov-report=xml --html=testing-report.html --self-contained-html 62 | 63 | coverage: test 64 | diff-cover coverage.xml --compare-branch=main --fail-under=100 65 | 66 | pypi: 67 | ${PYTHON} setup.py clean --all; \ 68 | ${PYTHON} setup.py rotate --match=.tar.gz,.whl,.egg,.zip --keep=0; \ 69 | ${PYTHON} setup.py sdist bdist_wheel; \ 70 | twine upload --skip-existing dist/*; 71 | 72 | lint: docstyle format style # Lint code using pydocstyle, black and pylint. 73 | 74 | check: lint test coverage # Both lint and test code. Runs `make lint` followed by `make test`. 75 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=80.0.0", "setuptools-scm"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "etuples" 7 | description = "Python S-expression emulation using tuple-like objects." 8 | authors = [ 9 | { name = "Brandon T. Willard", email = "brandonwillard+etuples@gmail.com" }, 10 | ] 11 | maintainers = [ 12 | { name = "Brandon T. Willard", email = "brandonwillard+etuples@gmail.com" }, 13 | ] 14 | readme = "README.md" 15 | requires-python = ">=3.9" 16 | license = "Apache-2.0" 17 | classifiers = [ 18 | "Development Status :: 4 - Beta", 19 | "Intended Audience :: Science/Research", 20 | "Intended Audience :: Developers", 21 | "Operating System :: OS Independent", 22 | "Programming Language :: Python", 23 | "Programming Language :: Python :: 3", 24 | "Programming Language :: Python :: 3.9", 25 | "Programming Language :: Python :: 3.10", 26 | "Programming Language :: Python :: 3.11", 27 | "Programming Language :: Python :: 3.12", 28 | "Programming Language :: Python :: Implementation :: CPython", 29 | "Programming Language :: Python :: Implementation :: PyPy", 30 | ] 31 | dependencies = ["cons", "multipledispatch"] 32 | dynamic = ["version"] 33 | 34 | [project.urls] 35 | Homepage = "http://github.com/pythological/etuples" 36 | Repository = "http://github.com/pythological/etuples" 37 | "Bug Tracker" = "http://github.com/pythological/etuples/issues" 38 | 39 | [tool.setuptools] 40 | packages = ["etuples"] 41 | 42 | [tool.setuptools.package-data] 43 | etuples = ["py.typed"] 44 | 45 | [tool.setuptools_scm] 46 | version_scheme = "guess-next-dev" 47 | local_scheme = "dirty-tag" 48 | 49 | [tool.pydocstyle] 50 | # Ignore errors for missing docstrings. 51 | # Ignore D202 (No blank lines allowed after function docstring) 52 | # due to bug in black: https://github.com/ambv/black/issues/355 53 | add-ignore = [ 54 | "D100", 55 | "D101", 56 | "D102", 57 | "D103", 58 | "D104", 59 | "D105", 60 | "D106", 61 | "D107", 62 | "D202", 63 | ] 64 | convention = "numpy" 65 | 66 | [tool.pytest.ini_options] 67 | python_files = ["test*.py"] 68 | testpaths = ["tests"] 69 | 70 | [tool.coverage.run] 71 | relative_files = true 72 | omit = ["tests/*"] 73 | branch = true 74 | 75 | [tool.coverage.report] 76 | exclude_lines = [ 77 | "pragma: no cover", 78 | "def __repr__", 79 | "raise NotImplementedError", 80 | "if __name__ == .__main__.:", 81 | "assert False", 82 | "ModuleNotFoundError", 83 | ] 84 | 85 | [tool.isort] 86 | multi_line_output = 3 87 | include_trailing_comma = true 88 | force_grid_wrap = 0 89 | use_parentheses = true 90 | ensure_newline_before_comments = true 91 | line_length = 88 92 | 93 | [tool.pylint] 94 | max-line-length = 88 95 | 96 | [tool.pylint.messages_control] 97 | disable = ["C0330", "C0326"] 98 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | # Cancels all previous workflow runs for pull requests that have not completed. 12 | concurrency: 13 | # The concurrency group contains the workflow name and the branch name for pull requests 14 | # or the commit hash for any other events. 15 | group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.head_ref || github.sha }} 16 | cancel-in-progress: true 17 | 18 | jobs: 19 | changes: 20 | name: "Check for changes" 21 | runs-on: ubuntu-latest 22 | outputs: 23 | changes: ${{ steps.changes.outputs.src }} 24 | steps: 25 | - uses: actions/checkout@v4 26 | with: 27 | fetch-depth: 0 28 | - uses: dorny/paths-filter@v2 29 | id: changes 30 | with: 31 | filters: | 32 | python: &python 33 | - 'etuples/**/*.py' 34 | - 'tests/**/*.py' 35 | - '*.py' 36 | src: 37 | - *python 38 | - '.github/**/*.yml' 39 | - 'setup.cfg' 40 | - 'pyproject.toml' 41 | - 'requirements.txt' 42 | - '.coveragerc' 43 | - '.pre-commit-config.yaml' 44 | - 'coverage.xml' 45 | 46 | style: 47 | name: Check code style 48 | needs: changes 49 | runs-on: ubuntu-latest 50 | if: ${{ needs.changes.outputs.changes == 'true' }} 51 | steps: 52 | - uses: actions/checkout@v4 53 | - uses: actions/setup-python@v4 54 | with: 55 | python-version: 3.13 56 | - uses: pre-commit/action@v3.0.0 57 | 58 | test: 59 | needs: 60 | - changes 61 | - style 62 | runs-on: ubuntu-latest 63 | if: ${{ needs.changes.outputs.changes == 'true' && needs.style.result == 'success' }} 64 | strategy: 65 | matrix: 66 | python-version: 67 | - 3.9 68 | - "3.10" 69 | - "3.11" 70 | - "3.12" 71 | steps: 72 | - uses: actions/checkout@v4 73 | - uses: actions/setup-python@v4 74 | with: 75 | python-version: ${{ matrix.python-version }} 76 | - name: Install dependencies 77 | run: | 78 | python -m pip install --upgrade pip 79 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 80 | - name: Test with pytest 81 | run: | 82 | pytest -v tests/ --cov=etuples --cov-report=xml:./coverage.xml 83 | - name: Coveralls 84 | uses: coverallsapp/github-action@v2 85 | with: 86 | github-token: ${{ secrets.GITHUB_TOKEN }} 87 | parallel: true 88 | flag-name: run-${{ matrix.python-version }} 89 | 90 | all-checks: 91 | if: ${{ always() }} 92 | runs-on: ubuntu-latest 93 | name: "All tests" 94 | needs: [changes, style, test] 95 | steps: 96 | - name: Check build matrix status 97 | if: ${{ needs.changes.outputs.changes == 'true' && (needs.style.result != 'success' || needs.test.result != 'success') }} 98 | run: exit 1 99 | 100 | upload-coverage: 101 | name: "Upload coverage" 102 | needs: [changes, all-checks] 103 | if: ${{ needs.changes.outputs.changes == 'true' && needs.all-checks.result == 'success' }} 104 | runs-on: ubuntu-latest 105 | steps: 106 | - name: Coveralls Finished 107 | uses: coverallsapp/github-action@v2 108 | with: 109 | github-token: ${{ secrets.GITHUB_TOKEN }} 110 | parallel-finished: true 111 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io/api/vim,emacs,python 2 | # Edit at https://www.gitignore.io/?templates=vim,emacs,python 3 | 4 | ### Emacs ### 5 | # -*- mode: gitignore; -*- 6 | *~ 7 | \#*\# 8 | /.emacs.desktop 9 | /.emacs.desktop.lock 10 | *.elc 11 | auto-save-list 12 | tramp 13 | .\#* 14 | 15 | # Org-mode 16 | .org-id-locations 17 | *_archive 18 | 19 | # flymake-mode 20 | *_flymake.* 21 | 22 | # eshell files 23 | /eshell/history 24 | /eshell/lastdir 25 | 26 | # elpa packages 27 | /elpa/ 28 | 29 | # reftex files 30 | *.rel 31 | 32 | # AUCTeX auto folder 33 | /auto/ 34 | 35 | # cask packages 36 | .cask/ 37 | dist/ 38 | 39 | # Flycheck 40 | flycheck_*.el 41 | 42 | # server auth directory 43 | /server/ 44 | 45 | # projectiles files 46 | .projectile 47 | 48 | # directory configuration 49 | .dir-locals.el 50 | 51 | # network security 52 | /network-security.data 53 | 54 | 55 | ### Python ### 56 | # Byte-compiled / optimized / DLL files 57 | __pycache__/ 58 | *.py[cod] 59 | *$py.class 60 | 61 | # C extensions 62 | *.so 63 | 64 | # Distribution / packaging 65 | .Python 66 | build/ 67 | develop-eggs/ 68 | downloads/ 69 | eggs/ 70 | .eggs/ 71 | lib/ 72 | lib64/ 73 | parts/ 74 | sdist/ 75 | var/ 76 | wheels/ 77 | pip-wheel-metadata/ 78 | share/python-wheels/ 79 | *.egg-info/ 80 | .installed.cfg 81 | *.egg 82 | MANIFEST 83 | 84 | # PyInstaller 85 | # Usually these files are written by a python script from a template 86 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 87 | *.manifest 88 | *.spec 89 | 90 | # Installer logs 91 | pip-log.txt 92 | pip-delete-this-directory.txt 93 | 94 | # Unit test / coverage reports 95 | htmlcov/ 96 | .tox/ 97 | .nox/ 98 | .coverage 99 | .coverage.* 100 | .cache 101 | nosetests.xml 102 | coverage.xml 103 | *.cover 104 | .hypothesis/ 105 | .pytest_cache/ 106 | testing-report.html 107 | 108 | # Translations 109 | *.mo 110 | *.pot 111 | 112 | # Django stuff: 113 | *.log 114 | local_settings.py 115 | db.sqlite3 116 | db.sqlite3-journal 117 | 118 | # Flask stuff: 119 | instance/ 120 | .webassets-cache 121 | 122 | # Scrapy stuff: 123 | .scrapy 124 | 125 | # Sphinx documentation 126 | docs/_build/ 127 | 128 | # PyBuilder 129 | target/ 130 | 131 | # Jupyter Notebook 132 | .ipynb_checkpoints 133 | 134 | # IPython 135 | profile_default/ 136 | ipython_config.py 137 | 138 | # pyenv 139 | .python-version 140 | 141 | # pipenv 142 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 143 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 144 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 145 | # install all needed dependencies. 146 | #Pipfile.lock 147 | 148 | # celery beat schedule file 149 | celerybeat-schedule 150 | 151 | # SageMath parsed files 152 | *.sage.py 153 | 154 | # Environments 155 | .env 156 | .venv 157 | env/ 158 | venv/ 159 | ENV/ 160 | env.bak/ 161 | venv.bak/ 162 | 163 | # Spyder project settings 164 | .spyderproject 165 | .spyproject 166 | 167 | # Rope project settings 168 | .ropeproject 169 | 170 | # mkdocs documentation 171 | /site 172 | 173 | # mypy 174 | .mypy_cache/ 175 | .dmypy.json 176 | dmypy.json 177 | 178 | # Pyre type checker 179 | .pyre/ 180 | 181 | ### Vim ### 182 | # Swap 183 | [._]*.s[a-v][a-z] 184 | [._]*.sw[a-p] 185 | [._]s[a-rt-v][a-z] 186 | [._]ss[a-gi-z] 187 | [._]sw[a-p] 188 | 189 | # Session 190 | Session.vim 191 | Sessionx.vim 192 | 193 | # Temporary 194 | .netrwhist 195 | # Auto-generated tag files 196 | tags 197 | # Persistent undo 198 | [._]*.un~ 199 | 200 | # End of https://www.gitignore.io/api/vim,emacs,python -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `etuples` 2 | [![Tests](https://github.com/pythological/etuples/actions/workflows/tests.yml/badge.svg)](https://github.com/pythological/etuples/actions/workflows/tests.yml) [![Coverage Status](https://coveralls.io/repos/github/pythological/etuples/badge.svg?branch=main)](https://coveralls.io/github/pythological/etuples?branch=main) [![PyPI](https://img.shields.io/pypi/v/etuples)](https://pypi.org/project/etuples/) 3 | 4 | Python [S-expression](https://en.wikipedia.org/wiki/S-expression) emulation using tuple-like objects. 5 | 6 | ## Examples 7 | 8 | `etuple`s are like tuples: 9 | 10 | ```python 11 | >>> from operator import add 12 | >>> from etuples import etuple, etuplize 13 | 14 | >>> et = etuple(add, 1, 2) 15 | >>> et 16 | ExpressionTuple((, 1, 2)) 17 | 18 | >>> from IPython.lib.pretty import pprint 19 | >>> pprint(et) 20 | e(, 1, 2) 21 | 22 | >>> et[0:2] 23 | ExpressionTuple((, 1)) 24 | ``` 25 | 26 | `etuple`s can also be evaluated: 27 | 28 | ```python 29 | >>> et.evaled_obj 30 | 3 31 | ``` 32 | 33 | Evaluated `etuple`s are cached: 34 | ```python 35 | >>> et = etuple(add, "a", "b") 36 | >>> et.evaled_obj 37 | 'ab' 38 | 39 | >>> et.evaled_obj is et.evaled_obj 40 | True 41 | ``` 42 | 43 | Reconstructed `etuple`s and their evaluation results are preserved across tuple operations: 44 | ```python 45 | >>> et_new = (et[0],) + et[1:] 46 | >>> et_new is et 47 | True 48 | >>> et_new.evaled_obj is et.evaled_obj 49 | True 50 | ``` 51 | 52 | `rator`, `rands`, and `apply` will return the operator, the operands, and apply the operation to the operands: 53 | ```python 54 | >>> from etuples import rator, rands, apply 55 | >>> et = etuple(add, 1, 2) 56 | 57 | >>> rator(et) 58 | 59 | 60 | >>> rands(et) 61 | ExpressionTuple((1, 2)) 62 | 63 | >>> apply(rator(et), rands(et)) 64 | 3 65 | ``` 66 | 67 | 68 | `rator` and `rands` are [`multipledispatch`](https://github.com/mrocklin/multipledispatch) functions that can be extended to handle arbitrary objects: 69 | ```python 70 | from etuples.core import ExpressionTuple 71 | from collections.abc import Sequence 72 | 73 | 74 | class Node: 75 | def __init__(self, rator, rands): 76 | self.rator, self.rands = rator, rands 77 | 78 | def __eq__(self, other): 79 | return self.rator == other.rator and self.rands == other.rands 80 | 81 | 82 | class Operator: 83 | def __init__(self, op_name): 84 | self.op_name = op_name 85 | 86 | def __call__(self, *args): 87 | return Node(Operator(self.op_name), args) 88 | 89 | def __repr__(self): 90 | return self.op_name 91 | 92 | def __eq__(self, other): 93 | return self.op_name == other.op_name 94 | 95 | 96 | rands.add((Node,), lambda x: x.rands) 97 | rator.add((Node,), lambda x: x.rator) 98 | 99 | 100 | @apply.register(Operator, (Sequence, ExpressionTuple)) 101 | def apply_Operator(rator, rands): 102 | return Node(rator, rands) 103 | ``` 104 | 105 | ```python 106 | >>> mul_op, add_op = Operator("*"), Operator("+") 107 | >>> mul_node = Node(mul_op, [1, 2]) 108 | >>> add_node = Node(add_op, [mul_node, 3]) 109 | ``` 110 | 111 | `etuplize` will convert non-tuple objects into their corresponding `etuple` form: 112 | ```python 113 | >>> et = etuplize(add_node) 114 | >>> pprint(et) 115 | e(+, e(*, 1, 2), 3) 116 | 117 | >>> et.evaled_obj is add_node 118 | True 119 | ``` 120 | 121 | `etuplize` can also do shallow object-to-`etuple` conversions: 122 | ```python 123 | >>> et = etuplize(add_node, shallow=True) 124 | >>> pprint(et) 125 | e(+, <__main__.Node at 0x7f347361a080>, 3) 126 | ``` 127 | 128 | ## Installation 129 | 130 | Using `pip`: 131 | ```bash 132 | pip install etuples 133 | ``` 134 | 135 | ### Development 136 | 137 | First obtain the project source: 138 | ```bash 139 | git clone git@github.com:pythological/etuples.git 140 | ``` 141 | 142 | Create a virtual environment and install the development dependencies: 143 | ```bash 144 | $ pip install -r requirements.txt 145 | ``` 146 | 147 | Set up `pre-commit` hooks: 148 | 149 | ```bash 150 | $ pre-commit install --install-hooks 151 | ``` 152 | 153 | Tests can be run with the provided `Makefile`: 154 | ```bash 155 | make check 156 | ``` 157 | -------------------------------------------------------------------------------- /tests/test_dispatch.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Sequence 2 | from operator import add 3 | 4 | from pytest import importorskip, raises 5 | 6 | from etuples.core import ExpressionTuple, KwdPair, etuple 7 | from etuples.dispatch import apply, etuplize, rands, rator 8 | 9 | 10 | class Node: 11 | __slots__ = ("rator", "rands") 12 | 13 | def __init__(self, rator, rands): 14 | self.rator, self.rands = rator, rands 15 | 16 | def __eq__(self, other): 17 | return ( 18 | type(self) is type(other) 19 | and self.rator == other.rator 20 | and self.rands == other.rands 21 | ) 22 | 23 | 24 | class Operator: 25 | def __init__(self, op_name): 26 | self.op_name = op_name 27 | 28 | def __call__(self, *args): 29 | pass 30 | 31 | 32 | rands.add((Node,), lambda x: x.rands) 33 | rator.add((Node,), lambda x: x.rator) 34 | 35 | 36 | @apply.register(Operator, (Sequence, ExpressionTuple)) 37 | def apply_Operator(rator, rands): 38 | return Node(rator, rands) 39 | 40 | 41 | def test_etuple_apply(): 42 | """Test `etuplize` and `etuple` interactions with `apply`.""" 43 | 44 | assert apply(add, (1, 2)) == 3 45 | assert apply(1, (2,)) == (1, 2) 46 | 47 | # Make sure that we don't lose underlying `evaled_obj`s 48 | # when taking apart and re-creating expression tuples 49 | # using `kanren`'s `operator`, `arguments` and `term` 50 | # functions. 51 | e1 = etuple(add, (object(),), (object(),)) 52 | e1_obj = e1.evaled_obj 53 | 54 | e1_dup = (rator(e1),) + rands(e1) 55 | 56 | assert isinstance(e1_dup, ExpressionTuple) 57 | assert e1_dup.evaled_obj == e1_obj 58 | 59 | e1_dup_2 = apply(rator(e1), rands(e1)) 60 | assert e1_dup_2 == e1_obj 61 | 62 | 63 | def test_rator_rands_apply(): 64 | op = Operator("*") 65 | node = Node(op, [1, 2]) 66 | node_rtr = rator(node) 67 | node_rnd = rands(node) 68 | 69 | assert node_rtr == op 70 | assert node_rnd == [1, 2] 71 | assert apply(node_rtr, node_rnd) == node 72 | 73 | 74 | def test_etuplize(): 75 | e0 = etuple(add, 1) 76 | e1 = etuplize(e0) 77 | 78 | assert e0 is e1 79 | 80 | assert etuple(1, 2) == etuplize((1, 2)) 81 | 82 | with raises(TypeError): 83 | etuplize("ab") 84 | 85 | assert "ab" == etuplize("ab", return_bad_args=True) 86 | 87 | op_1, op_2 = Operator("*"), Operator("+") 88 | node_1 = Node(op_2, [1, 2]) 89 | node_2 = Node(op_1, [node_1, 3, ()]) 90 | 91 | assert etuplize(node_2) == etuple(op_1, etuple(op_2, 1, 2), 3, ()) 92 | assert type(etuplize(node_2)[-1]) == tuple 93 | assert etuplize(node_2, shallow=True) == etuple(op_1, node_1, 3, ()) 94 | 95 | def rands_transform(x): 96 | if x == 1: 97 | return 4 98 | return x 99 | 100 | assert etuplize(node_2, rands_transform_fn=rands_transform) == etuple( 101 | op_1, etuple(op_2, 4, 2), 3, () 102 | ) 103 | 104 | def rator_transform(x): 105 | if x == op_1: 106 | return op_2 107 | return x 108 | 109 | assert etuplize(node_2, rator_transform_fn=rator_transform) == etuple( 110 | op_2, etuple(op_2, 1, 2), 3, () 111 | ) 112 | 113 | 114 | def test_unification(): 115 | from cons import cons 116 | 117 | uni = importorskip("unification") 118 | 119 | var, unify, reify = uni.var, uni.unify, uni.reify 120 | 121 | a_lv, b_lv = var(), var() 122 | assert unify(etuple(add, 1, 2), etuple(add, 1, 2), {}) == {} 123 | assert unify(etuple(add, 1, 2), etuple(a_lv, 1, 2), {}) == {a_lv: add} 124 | assert reify(etuple(a_lv, 1, 2), {a_lv: add}) == etuple(add, 1, 2) 125 | 126 | res = unify(etuple(add, 1, 2), cons(a_lv, b_lv), {}) 127 | assert res == {a_lv: add, b_lv: etuple(1, 2)} 128 | 129 | res = reify(cons(a_lv, b_lv), res) 130 | assert isinstance(res, ExpressionTuple) 131 | assert res == etuple(add, 1, 2) 132 | 133 | et = etuple( 134 | a_lv, 135 | ) 136 | res = reify(et, {a_lv: 1}) 137 | assert isinstance(res, ExpressionTuple) 138 | 139 | et = etuple( 140 | a_lv, 141 | ) 142 | # We choose to allow unification with regular tuples. 143 | if etuple(1) == (1,): 144 | res = unify(et, (1,)) 145 | assert res == {a_lv: 1} 146 | 147 | et = etuple(add, 1, 2) 148 | assert et.evaled_obj == 3 149 | 150 | res = unify(et, cons(a_lv, b_lv)) 151 | assert res == {a_lv: add, b_lv: et[1:]} 152 | 153 | # Make sure we've preserved the original object after deconstruction via 154 | # `unify` 155 | assert res[b_lv]._parent is et 156 | assert ((res[a_lv],) + res[b_lv])._evaled_obj == 3 157 | 158 | # Make sure we've preserved the original object after reconstruction via 159 | # `reify` 160 | rf_res = reify(cons(a_lv, b_lv), res) 161 | assert rf_res is et 162 | 163 | et_lv = etuple(add, a_lv, 2) 164 | assert reify(et_lv[1:], {})._parent is et_lv 165 | 166 | # Reify a logic variable to another logic variable 167 | assert reify(et_lv[1:], {a_lv: b_lv})._parent is et_lv 168 | 169 | # TODO: We could propagate the parent etuple when a sub-etuple is `cons`ed 170 | # with logic variables. 171 | # et_1 = et[2:] 172 | # et_2 = cons(b_lv, et_1) 173 | # assert et_2._parent is et 174 | # assert reify(et_2, {a_lv: 1})._parent is et 175 | 176 | e1 = KwdPair("name", "blah") 177 | e2 = KwdPair("name", a_lv) 178 | assert unify(e1, e2, {}) == {a_lv: "blah"} 179 | assert reify(e2, {a_lv: "blah"}) == e1 180 | 181 | e1 = etuple(add, 1, name="blah") 182 | e2 = etuple(add, 1, name=a_lv) 183 | assert unify(e1, e2, {}) == {a_lv: "blah"} 184 | assert reify(e2, {a_lv: "blah"}) == e1 185 | -------------------------------------------------------------------------------- /etuples/dispatch.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Callable, Mapping, Sequence 2 | 3 | from cons.core import ConsError, ConsNull, ConsPair, car, cdr, cons 4 | from multipledispatch import dispatch 5 | 6 | from .core import ExpressionTuple, KwdPair, etuple, trampoline_eval 7 | 8 | try: # noqa: C901 9 | import unification 10 | from packaging import version 11 | 12 | if version.parse(unification.__version__) < version.parse("0.4.0"): 13 | raise ModuleNotFoundError() 14 | 15 | from unification.core import _reify, _unify, construction_sentinel, isvar 16 | except ModuleNotFoundError: 17 | pass 18 | else: 19 | 20 | def _unify_ExpressionTuple(u, v, s): 21 | yield _unify(getattr(u, "_tuple", u), getattr(v, "_tuple", v), s) 22 | 23 | _unify.add((ExpressionTuple, ExpressionTuple, Mapping), _unify_ExpressionTuple) 24 | _unify.add((tuple, ExpressionTuple, Mapping), _unify_ExpressionTuple) 25 | _unify.add((ExpressionTuple, tuple, Mapping), _unify_ExpressionTuple) 26 | 27 | def _unify_KwdPair(u, v, s): 28 | s = yield _unify(u.arg, v.arg, s) 29 | if s is not False: 30 | s = yield _unify(u.value, v.value, s) 31 | yield s 32 | 33 | _unify.add((KwdPair, KwdPair, Mapping), _unify_KwdPair) 34 | 35 | def _reify_ExpressionTuple(u, s): 36 | # The point of all this: we don't want to lose the expression 37 | # tracking/caching information. 38 | res = yield _reify(u._tuple, s) 39 | 40 | yield construction_sentinel 41 | 42 | res_same = tuple( 43 | a == b for a, b in zip(u, res) if not isvar(a) and not isvar(b) 44 | ) 45 | 46 | if len(res_same) == len(u) and all(res_same): 47 | # Everything is equal and there are no logic variables 48 | yield u 49 | return 50 | 51 | if getattr(u, "_parent", None) and all(res_same): 52 | # If we simply swapped-out logic variables, then we don't want to 53 | # lose the parent etuple information. 54 | res = type(u)(res) 55 | res._parent = u._parent 56 | yield res 57 | return 58 | 59 | yield type(u)(res) 60 | 61 | _reify.add((ExpressionTuple, Mapping), _reify_ExpressionTuple) 62 | 63 | def _reify_KwdPair(u, s): 64 | arg = yield _reify(u.arg, s) 65 | value = yield _reify(u.value, s) 66 | 67 | yield construction_sentinel 68 | 69 | yield KwdPair(arg, value) 70 | 71 | _reify.add((KwdPair, Mapping), _reify_KwdPair) 72 | 73 | 74 | @dispatch(object) 75 | def rator(x): 76 | return car(x) 77 | 78 | 79 | @dispatch(object) 80 | def rands(x): 81 | return cdr(x) 82 | 83 | 84 | @dispatch(object, Sequence) 85 | def apply(rator, rands): 86 | res = cons(rator, rands) 87 | return etuple(*res) 88 | 89 | 90 | @apply.register(Callable, Sequence) 91 | def apply_Sequence(rator, rands): 92 | return rator(*rands) 93 | 94 | 95 | @apply.register(Callable, ExpressionTuple) 96 | def apply_ExpressionTuple(rator, rands): 97 | return ((rator,) + rands).evaled_obj 98 | 99 | 100 | # These are used to maintain some parity with the old `kanren.term` API 101 | operator, arguments, term = rator, rands, apply 102 | 103 | 104 | @dispatch(object) 105 | def etuplize_fn(op): 106 | return etuple 107 | 108 | 109 | @dispatch(object) 110 | def etuplize( 111 | x, 112 | shallow=False, 113 | return_bad_args=False, 114 | convert_ConsPairs=True, 115 | rator_transform_fn=lambda x: x, 116 | rands_transform_fn=lambda x: x, 117 | ): 118 | r"""Return an expression-tuple for an object (i.e. a tuple of rand and rators). 119 | 120 | When evaluated, the rand and rators should [re-]construct the object. When 121 | the object cannot be given such a form, it is simply converted to an 122 | `ExpressionTuple` and returned. 123 | 124 | Parameters 125 | ---------- 126 | x: object 127 | Object to convert to expression-tuple form. 128 | shallow: bool 129 | Whether or not to do a shallow conversion. 130 | return_bad_args: bool 131 | Return the passed argument when its type is not appropriate, instead 132 | of raising an exception. 133 | rator_transform_fn: callable 134 | A function to be applied to each rator/CAR element of each constructed 135 | `ExpressionTuple`. The returned value is used in place of the input, and 136 | the function is not applied to existing `ExpressionTuple`\s. 137 | rands_transform_fn: callable 138 | The same as `rator_transform_fn`, but for rands/CDR elements. 139 | 140 | """ 141 | 142 | def etuplize_step( 143 | x, 144 | shallow=shallow, 145 | return_bad_args=return_bad_args, 146 | convert_ConsPairs=convert_ConsPairs, 147 | ): 148 | if isinstance(x, ExpressionTuple): 149 | yield x 150 | return 151 | elif ( 152 | convert_ConsPairs and x is not None and isinstance(x, (ConsNull, ConsPair)) 153 | ): 154 | yield etuple( 155 | *( 156 | (rator_transform_fn(rator(x)),) 157 | + tuple(rands_transform_fn(e) for e in rands(x)) 158 | ) 159 | ) 160 | return 161 | 162 | try: 163 | op, args = rator(x), rands(x) 164 | except ConsError: 165 | op, args = None, None 166 | 167 | if not callable(op) or not isinstance(args, (ConsNull, ConsPair)): 168 | if return_bad_args: 169 | yield x 170 | return 171 | else: 172 | raise TypeError(f"x is neither a non-str Sequence nor term: {type(x)}") 173 | 174 | op = rator_transform_fn(op) 175 | args = etuple(*tuple(rands_transform_fn(a) for a in args)) 176 | 177 | if shallow: 178 | et_op = op 179 | et_args = args 180 | else: 181 | et_op = yield etuplize_step(op, return_bad_args=True) 182 | et_args = [] 183 | for a in args: 184 | e = yield etuplize_step( 185 | a, return_bad_args=True, convert_ConsPairs=False 186 | ) 187 | et_args.append(e) 188 | 189 | yield etuplize_fn(op)(et_op, *et_args, evaled_obj=x) 190 | 191 | return trampoline_eval(etuplize_step(x)) 192 | -------------------------------------------------------------------------------- /tests/test_core.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from operator import add 3 | from types import GeneratorType 4 | 5 | import pytest 6 | 7 | from etuples.core import ExpressionTuple, InvalidExpression, KwdPair, etuple 8 | 9 | 10 | def test_ExpressionTuple(capsys): 11 | e0 = ExpressionTuple((add, 1, 2)) 12 | assert hash(e0) == hash((add, 1, 2)) 13 | assert e0 == ExpressionTuple(e0) 14 | 15 | e5 = ExpressionTuple((1, ExpressionTuple((2, 3)))) 16 | e6 = ExpressionTuple((1, ExpressionTuple((2, 3)))) 17 | 18 | assert e5 == e6 19 | assert hash(e5) == hash(e6) 20 | assert ExpressionTuple((ExpressionTuple((1,)), 2)) != ExpressionTuple((1, 2)) 21 | 22 | # Not sure if we really want this; it's more 23 | # common to have a copy constructor, no? 24 | assert e0 is ExpressionTuple(e0) 25 | 26 | e0_2 = e0[:2] 27 | assert e0_2 == (add, 1) 28 | assert e0_2._parent is e0 29 | assert e0_2 + (2,) is e0 30 | assert (add, 1) + e0[-1:] is e0 31 | assert 1 in e0 32 | assert e0[1:] > (1,) and e0[1:] >= (1,) 33 | assert e0[1:] < (3, 3) and e0[1:] <= (3, 3) 34 | assert isinstance(e0 * 2, ExpressionTuple) 35 | assert e0 * 2 == ExpressionTuple((add, 1, 2, add, 1, 2)) 36 | assert isinstance(2 * e0, ExpressionTuple) 37 | assert 2 * e0 == ExpressionTuple((add, 1, 2, add, 1, 2)) 38 | 39 | e1 = ExpressionTuple((add, e0, 3)) 40 | assert e1.evaled_obj == 6 41 | 42 | # ("_evaled_obj", "_tuple", "_parent") 43 | e2 = e1[1:2] 44 | assert e2._parent is e1 45 | 46 | assert e2 == ExpressionTuple((e0,)) 47 | 48 | ExpressionTuple((print, "hi")).evaled_obj 49 | captured = capsys.readouterr() 50 | assert captured.out == "hi\n" 51 | 52 | e3 = ExpressionTuple(()) 53 | 54 | with pytest.raises(InvalidExpression): 55 | e3.evaled_obj 56 | 57 | e4 = ExpressionTuple((1,)) 58 | 59 | with pytest.raises(InvalidExpression): 60 | e4.evaled_obj 61 | 62 | assert ExpressionTuple((ExpressionTuple((lambda: add,)), 1, 1)).evaled_obj == 2 63 | assert ExpressionTuple((1, 2)) != ExpressionTuple((1,)) 64 | assert ExpressionTuple((1, 2)) != ExpressionTuple((1, 3)) 65 | 66 | with pytest.warns(DeprecationWarning): 67 | ExpressionTuple((print, "hi")).eval_obj 68 | 69 | 70 | def test_eval_apply_fn(): 71 | class Add(object): 72 | def __call__(self): 73 | return None 74 | 75 | def add(self, x, y): 76 | return x + y 77 | 78 | class AddExpressionTuple(ExpressionTuple): 79 | def _eval_apply_fn(self, op): 80 | return op.add 81 | 82 | op = Add() 83 | assert AddExpressionTuple((op, 1, 2)).evaled_obj == 3 84 | 85 | 86 | def test_etuple(): 87 | """Test basic `etuple` functionality.""" 88 | 89 | def test_op(*args): 90 | return tuple(object() for i in range(sum(args))) 91 | 92 | e1 = etuple(test_op, 1, 2) 93 | 94 | assert e1._evaled_obj is ExpressionTuple.null 95 | 96 | with pytest.raises(ValueError): 97 | e1.evaled_obj = 1 98 | 99 | e1_obj = e1.evaled_obj 100 | assert len(e1_obj) == 3 101 | assert all(type(o) is object for o in e1_obj) 102 | 103 | # Make sure we don't re-create the cached `evaled_obj` 104 | e1_obj_2 = e1.evaled_obj 105 | assert e1_obj == e1_obj_2 106 | 107 | # Confirm that evaluation is recursive 108 | e2 = etuple(add, (object(),), e1) 109 | 110 | # Make sure we didn't convert this single tuple value to 111 | # an `etuple` 112 | assert type(e2[1]) is tuple 113 | 114 | # Slices should be `etuple`s, though. 115 | assert isinstance(e2[:1], ExpressionTuple) 116 | assert e2[1] == e2[1:2][0] 117 | 118 | e2_obj = e2.evaled_obj 119 | 120 | assert type(e2_obj) is tuple 121 | assert len(e2_obj) == 4 122 | assert all(type(o) is object for o in e2_obj) 123 | # Make sure that it used `e1`'s original `evaled_obj` 124 | assert e2_obj[1:] == e1_obj 125 | 126 | # Confirm that any combination of `tuple`s/`etuple`s in 127 | # concatenation result in an `etuple` 128 | e_radd = (1,) + etuple(2, 3) 129 | assert isinstance(e_radd, ExpressionTuple) 130 | assert e_radd == (1, 2, 3) 131 | 132 | e_ladd = etuple(1, 2) + (3,) 133 | assert isinstance(e_ladd, ExpressionTuple) 134 | assert e_ladd == (1, 2, 3) 135 | 136 | 137 | def test_etuple_generator(): 138 | e_gen = etuple(lambda v: (i for i in v), range(3)) 139 | e_gen_res = e_gen.evaled_obj 140 | assert isinstance(e_gen_res, GeneratorType) 141 | assert tuple(e_gen_res) == tuple(range(3)) 142 | 143 | 144 | def test_etuple_kwargs(): 145 | """Test keyword arguments and default argument values.""" 146 | 147 | e = etuple(a=1, b=2) 148 | assert e._tuple == (KwdPair("a", 1), KwdPair("b", 2)) 149 | assert KwdPair("a", 1) in e._tuple 150 | assert hash(KwdPair("a", 1)) == hash(KwdPair("a", 1)) 151 | 152 | def test_func(a, b, c=None, d="d-arg", **kwargs): 153 | assert isinstance(c, (type(None), int)) 154 | return [a, b, c, d] 155 | 156 | e1 = etuple(test_func, 1, 2) 157 | assert e1.evaled_obj == [1, 2, None, "d-arg"] 158 | 159 | # Make sure we handle variadic args properly 160 | def test_func2(*args, c=None, d="d-arg", **kwargs): 161 | assert isinstance(c, (type(None), int)) 162 | return list(args) + [c, d] 163 | 164 | e0 = etuple(test_func2, c=3) 165 | assert e0.evaled_obj == [3, "d-arg"] 166 | 167 | e11 = etuple(test_func2, 1, 2) 168 | assert e11.evaled_obj == [1, 2, None, "d-arg"] 169 | 170 | e2 = etuple(test_func, 1, 2, 3) 171 | assert e2.evaled_obj == [1, 2, 3, "d-arg"] 172 | 173 | e3 = etuple(test_func, 1, 2, 3, 4) 174 | assert e3.evaled_obj == [1, 2, 3, 4] 175 | 176 | e4 = etuple(test_func, 1, 2, c=3) 177 | assert e4.evaled_obj == [1, 2, 3, "d-arg"] 178 | 179 | e5 = etuple(test_func, 1, 2, d=3) 180 | assert e5.evaled_obj == [1, 2, None, 3] 181 | 182 | e6 = etuple(test_func, 1, 2, 3, d=4) 183 | assert e6.evaled_obj == [1, 2, 3, 4] 184 | 185 | # Try evaluating nested etuples 186 | e7 = etuple(test_func, etuple(add, 1, 0), 2, c=etuple(add, 1, etuple(add, 1, 1))) 187 | assert e7.evaled_obj == [1, 2, 3, "d-arg"] 188 | 189 | # Try a function without an obtainable signature object 190 | e8 = etuple( 191 | enumerate, 192 | etuple(list, ["a", "b", "c", "d"]), 193 | start=etuple(add, 1, etuple(add, 1, 1)), 194 | ) 195 | assert list(e8.evaled_obj) == [(3, "a"), (4, "b"), (5, "c"), (6, "d")] 196 | 197 | # Use "evaled_obj" kwarg and make sure it doesn't end up in the `_tuple` object 198 | e9 = etuple(add, 1, 2, evaled_obj=3) 199 | assert e9._tuple == (add, 1, 2) 200 | assert e9._evaled_obj == 3 201 | 202 | 203 | def test_str(): 204 | et = etuple(1, etuple("a", 2), etuple(3, "b")) 205 | assert ( 206 | repr(et) 207 | == "ExpressionTuple((1, ExpressionTuple(('a', 2)), ExpressionTuple((3, 'b'))))" 208 | ) 209 | assert str(et) == "e(1, e(a, 2), e(3, b))" 210 | 211 | kw = KwdPair("a", 1) 212 | 213 | assert repr(kw) == "KwdPair('a', 1)" 214 | assert str(kw) == "a=1" 215 | 216 | 217 | def test_pprint(): 218 | pretty_mod = pytest.importorskip("IPython.lib.pretty") 219 | et = etuple(1, etuple("a", *range(20)), etuple(3, "b"), blah=etuple("c", 0)) 220 | assert ( 221 | pretty_mod.pretty(et) 222 | == "e(\n 1,\n e('a', 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19),\n e(3, 'b'),\n blah=e(c, 0))" # noqa: E501 223 | ) 224 | 225 | 226 | def gen_long_add_chain(N=None, num=1): 227 | b_struct = num 228 | if N is None: 229 | N = sys.getrecursionlimit() 230 | for i in range(0, N): 231 | b_struct = etuple(add, num, b_struct) 232 | return b_struct 233 | 234 | 235 | def test_reify_recursion_limit(): 236 | a = gen_long_add_chain(10) 237 | assert a.evaled_obj == 11 238 | 239 | r_limit = sys.getrecursionlimit() 240 | 241 | try: 242 | sys.setrecursionlimit(100) 243 | 244 | a = gen_long_add_chain(200) 245 | assert a.evaled_obj == 201 246 | 247 | b = gen_long_add_chain(200, num=2) 248 | assert b.evaled_obj == 402 249 | 250 | c = gen_long_add_chain(200) 251 | assert a == c 252 | 253 | finally: 254 | sys.setrecursionlimit(r_limit) 255 | 256 | 257 | @pytest.mark.skip( 258 | reason=( 259 | "This will cause an unrecoverable stack overflow" 260 | " in some cases (e.g. GitHub Actions' default ubuntu-latest runners)" 261 | ) 262 | ) 263 | @pytest.mark.xfail(strict=True) 264 | def test_reify_recursion_limit_hash(): 265 | r_limit = sys.getrecursionlimit() 266 | 267 | try: 268 | sys.setrecursionlimit(100) 269 | a = gen_long_add_chain(200) 270 | # CPython uses the call stack and fails 271 | assert hash(a) 272 | finally: 273 | sys.setrecursionlimit(r_limit) 274 | -------------------------------------------------------------------------------- /etuples/core.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | import reprlib 3 | import warnings 4 | from collections import deque 5 | from collections.abc import Generator, Sequence 6 | from typing import Callable 7 | 8 | from multipledispatch import dispatch 9 | 10 | etuple_repr = reprlib.Repr() 11 | etuple_repr.maxstring = 100 12 | etuple_repr.maxother = 100 13 | 14 | 15 | class IgnoredGenerator: 16 | __slots__ = ("gen",) 17 | 18 | def __init__(self, gen): 19 | self.gen = gen 20 | 21 | 22 | def trampoline_eval(z, res_filter=None): 23 | """Evaluate a stream of generators. 24 | 25 | This implementation consists of a deque that simulates an evaluation stack 26 | of generator-produced operations. We're able to overcome `RecursionError`s 27 | this way. 28 | """ 29 | 30 | if not isinstance(z, Generator): # pragma: no cover 31 | return z 32 | elif isinstance(z, IgnoredGenerator): 33 | return z.gen 34 | 35 | stack = deque() 36 | z_args, z_out = None, None 37 | stack.append(z) 38 | 39 | while stack: 40 | z = stack[-1] 41 | try: 42 | z_out = z.send(z_args) 43 | 44 | if res_filter: # pragma: no cover 45 | _ = res_filter(z, z_out) 46 | 47 | if isinstance(z_out, Generator): 48 | stack.append(z_out) 49 | z_args = None 50 | else: 51 | z_args = z_out 52 | 53 | except StopIteration: 54 | _ = stack.pop() 55 | 56 | if isinstance(z_out, IgnoredGenerator): 57 | return z_out.gen 58 | else: 59 | return z_out 60 | 61 | 62 | class InvalidExpression(Exception): 63 | """An exception indicating that an `ExpressionTuple` is not a valid [S-]expression. 64 | 65 | This exception is raised when an attempt is made to evaluate an 66 | `ExpressionTuple` that does not have a valid operator (e.g. not a 67 | `callable`). 68 | 69 | """ 70 | 71 | 72 | class KwdPair(object): 73 | """A class used to indicate a keyword + value mapping. 74 | 75 | TODO: Could subclass `ast.keyword`. 76 | 77 | """ 78 | 79 | __slots__ = ("arg", "value") 80 | 81 | def __init__(self, arg, value): 82 | assert isinstance(arg, str) 83 | self.arg = arg 84 | self.value = value 85 | 86 | def _eval_step(self): 87 | if isinstance(self.value, (ExpressionTuple, KwdPair)): 88 | value = yield self.value._eval_step() 89 | else: 90 | value = self.value 91 | 92 | yield KwdPair(self.arg, value) 93 | 94 | def __repr__(self): 95 | return f"{self.__class__.__name__}({repr(self.arg)}, {repr(self.value)})" 96 | 97 | def __str__(self): 98 | return f"{self.arg}={self.value}" 99 | 100 | def _repr_pretty_(self, p, cycle): 101 | p.text(str(self)) 102 | 103 | def __eq__(self, other): 104 | return ( 105 | type(self) is type(other) 106 | and self.arg == other.arg 107 | and self.value == other.value 108 | ) 109 | 110 | def __hash__(self): 111 | return hash((type(self), self.arg, self.value)) 112 | 113 | 114 | class ExpressionTuple(Sequence): 115 | """A tuple-like object that represents an expression. 116 | 117 | This object caches the return value resulting from evaluation of the 118 | expression it represents. Likewise, it holds onto the "parent" expression 119 | from which it was derived (e.g. as a slice), if any, so that it can 120 | preserve the return value through limited forms of concatenation/cons-ing 121 | that would reproduce the parent expression. 122 | 123 | TODO: Should probably use weakrefs for that. 124 | """ 125 | 126 | __slots__ = ("_evaled_obj", "_tuple", "_parent") 127 | null = object() 128 | 129 | def __new__(cls, seq=None, **kwargs): 130 | # XXX: This doesn't actually remove the entry from the kwargs 131 | # passed to __init__! 132 | # It does, however, remove it for the check below. 133 | kwargs.pop("evaled_obj", None) 134 | 135 | if seq is not None and not kwargs and type(seq) is cls: 136 | return seq 137 | 138 | res = super().__new__(cls) 139 | 140 | return res 141 | 142 | def __init__(self, seq=None, **kwargs): 143 | """Create an expression tuple. 144 | 145 | If the keyword 'evaled_obj' is given, the `ExpressionTuple`'s 146 | evaluated object is set to the corresponding value. 147 | XXX: There is no verification/check that the arguments evaluate to the 148 | user-specified 'evaled_obj', so be careful. 149 | """ 150 | 151 | _evaled_obj = kwargs.pop("evaled_obj", self.null) 152 | etuple_kwargs = tuple(KwdPair(k, v) for k, v in kwargs.items()) 153 | 154 | if seq: 155 | self._tuple = tuple(seq) + etuple_kwargs 156 | else: 157 | self._tuple = etuple_kwargs 158 | 159 | # TODO: Consider making these a weakrefs. 160 | self._evaled_obj = _evaled_obj 161 | self._parent = None 162 | 163 | @property 164 | def evaled_obj(self): 165 | """Return the evaluation of this expression tuple.""" 166 | res = self._eval_step() 167 | return trampoline_eval(res) 168 | 169 | @evaled_obj.setter 170 | def evaled_obj(self, obj): 171 | raise ValueError("Value of evaluated expression cannot be set!") 172 | 173 | @property 174 | def eval_obj(self): 175 | warnings.warn( 176 | "`eval_obj` is deprecated; use `evaled_obj`.", 177 | DeprecationWarning, 178 | stacklevel=2, 179 | ) 180 | return trampoline_eval(self._eval_step()) 181 | 182 | def _eval_apply_fn(self, op: Callable) -> Callable: 183 | """Return the callable used to evaluate the expression tuple. 184 | 185 | The expression tuple's operator can be any `Callable`, i.e. either 186 | a function or an instance of a class that defines `__call__`. In 187 | the latter case, one can evalute the expression tuple using a 188 | method other than `__call__` by overloading this method. 189 | 190 | """ 191 | return op 192 | 193 | def _eval_step(self): 194 | if len(self._tuple) == 0: 195 | raise InvalidExpression("Empty expression.") 196 | 197 | if self._evaled_obj is not self.null: 198 | yield self._evaled_obj 199 | else: 200 | op = self._tuple[0] 201 | 202 | if isinstance(op, (ExpressionTuple, KwdPair)): 203 | op = yield op._eval_step() 204 | 205 | if not callable(op): 206 | raise InvalidExpression( 207 | "ExpressionTuple does not have a callable operator." 208 | ) 209 | 210 | evaled_args = [] 211 | evaled_kwargs = [] 212 | for i in self._tuple[1:]: 213 | if isinstance(i, (ExpressionTuple, KwdPair)): 214 | i = yield i._eval_step() 215 | 216 | if isinstance(i, KwdPair): 217 | evaled_kwargs.append(i) 218 | else: 219 | evaled_args.append(i) 220 | 221 | try: 222 | op_sig = inspect.signature(self._eval_apply_fn(op)) 223 | except ValueError: 224 | # This handles some builtin function types 225 | _evaled_obj = op(*(evaled_args + [kw.value for kw in evaled_kwargs])) 226 | else: 227 | op_args = op_sig.bind( 228 | *evaled_args, **{kw.arg: kw.value for kw in evaled_kwargs} 229 | ) 230 | op_args.apply_defaults() 231 | 232 | _evaled_obj = self._eval_apply_fn(op)(*op_args.args, **op_args.kwargs) 233 | 234 | if isinstance(_evaled_obj, Generator): 235 | self._evaled_obj = _evaled_obj 236 | yield IgnoredGenerator(_evaled_obj) 237 | else: 238 | self._evaled_obj = _evaled_obj 239 | yield self._evaled_obj 240 | 241 | def __add__(self, x): 242 | res = self._tuple + x 243 | if self._parent is not None and res == self._parent._tuple: 244 | return self._parent 245 | return type(self)(res) 246 | 247 | def __contains__(self, *args): 248 | return self._tuple.__contains__(*args) 249 | 250 | def __ge__(self, *args): 251 | return self._tuple.__ge__(*args) 252 | 253 | def __getitem__(self, key): 254 | tuple_res = self._tuple[key] 255 | if isinstance(key, slice) and isinstance(tuple_res, tuple): 256 | tuple_res = type(self)(tuple_res) 257 | tuple_res._parent = self 258 | return tuple_res 259 | 260 | def __gt__(self, *args): 261 | return self._tuple.__gt__(*args) 262 | 263 | def __iter__(self, *args): 264 | return self._tuple.__iter__(*args) 265 | 266 | def __le__(self, *args): 267 | return self._tuple.__le__(*args) 268 | 269 | def __len__(self, *args): 270 | return self._tuple.__len__(*args) 271 | 272 | def __lt__(self, *args): 273 | return self._tuple.__lt__(*args) 274 | 275 | def __mul__(self, *args): 276 | return type(self)(self._tuple.__mul__(*args)) 277 | 278 | def __rmul__(self, *args): 279 | return type(self)(self._tuple.__rmul__(*args)) 280 | 281 | def __radd__(self, x): 282 | res = x + self._tuple # type(self)(x + self._tuple) 283 | if self._parent is not None and res == self._parent._tuple: 284 | return self._parent 285 | return type(self)(res) 286 | 287 | def __str__(self): 288 | return f"e({', '.join(tuple(str(i) for i in self._tuple))})" 289 | 290 | def __repr__(self): 291 | return f"ExpressionTuple({etuple_repr.repr(self._tuple)})" 292 | 293 | def _repr_pretty_(self, p, cycle): 294 | if cycle: 295 | p.text("e(...)") # pragma: no cover 296 | else: 297 | with p.group(2, "e(", ")"): 298 | p.breakable(sep="") 299 | for idx, item in enumerate(self._tuple): 300 | if idx: 301 | p.text(",") 302 | p.breakable() 303 | p.pretty(item) 304 | 305 | def __eq__(self, other): 306 | # Built-in `==` won't work in CPython for deeply nested structures. 307 | 308 | # TODO: We could track the level of `ExpressionTuple`-only nesting and 309 | # apply TCO only when it reaches a certain level. 310 | 311 | if not isinstance(other, Sequence): 312 | return NotImplemented 313 | 314 | if len(other) != len(self): 315 | return False 316 | 317 | queue = deque(zip(self._tuple, other)) 318 | 319 | while queue: 320 | i_s, i_o = queue.pop() 321 | 322 | if ( 323 | isinstance(i_s, Sequence) 324 | and isinstance(i_o, Sequence) 325 | and ( 326 | isinstance(i_s, ExpressionTuple) or isinstance(i_o, ExpressionTuple) 327 | ) 328 | ): 329 | queue.extend(zip(i_s, i_o)) 330 | elif i_s != i_o: 331 | return False 332 | 333 | return True 334 | 335 | def __hash__(self): 336 | # XXX: CPython fails for deeply nested tuples! 337 | return hash(self._tuple) 338 | 339 | 340 | @dispatch([object]) 341 | def etuple(*args, **kwargs): 342 | """Create an ExpressionTuple from the argument list. 343 | 344 | In other words: 345 | etuple(1, 2, 3) == ExpressionTuple((1, 2, 3)) 346 | 347 | """ 348 | return ExpressionTuple(args, **kwargs) 349 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ======= 2 | License 3 | ======= 4 | 5 | etuples is distributed under the Apache License, Version 2.0 6 | 7 | Copyright (c) 2019 Brandon T. Willard (Academic Free License) 8 | All rights reserved. 9 | 10 | Apache License 11 | Version 2.0, January 2004 12 | http://www.apache.org/licenses/ 13 | 14 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 15 | 16 | 1. Definitions. 17 | 18 | "License" shall mean the terms and conditions for use, reproduction, 19 | and distribution as defined by Sections 1 through 9 of this document. 20 | 21 | "Licensor" shall mean the copyright owner or entity authorized by 22 | the copyright owner that is granting the License. 23 | 24 | "Legal Entity" shall mean the union of the acting entity and all 25 | other entities that control, are controlled by, or are under common 26 | control with that entity. For the purposes of this definition, 27 | "control" means (i) the power, direct or indirect, to cause the 28 | direction or management of such entity, whether by contract or 29 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 30 | outstanding shares, or (iii) beneficial ownership of such entity. 31 | 32 | "You" (or "Your") shall mean an individual or Legal Entity 33 | exercising permissions granted by this License. 34 | 35 | "Source" form shall mean the preferred form for making modifications, 36 | including but not limited to software source code, documentation 37 | source, and configuration files. 38 | 39 | "Object" form shall mean any form resulting from mechanical 40 | transformation or translation of a Source form, including but 41 | not limited to compiled object code, generated documentation, 42 | and conversions to other media types. 43 | 44 | "Work" shall mean the work of authorship, whether in Source or 45 | Object form, made available under the License, as indicated by a 46 | copyright notice that is included in or attached to the work 47 | (an example is provided in the Appendix below). 48 | 49 | "Derivative Works" shall mean any work, whether in Source or Object 50 | form, that is based on (or derived from) the Work and for which the 51 | editorial revisions, annotations, elaborations, or other modifications 52 | represent, as a whole, an original work of authorship. For the purposes 53 | of this License, Derivative Works shall not include works that remain 54 | separable from, or merely link (or bind by name) to the interfaces of, 55 | the Work and Derivative Works thereof. 56 | 57 | "Contribution" shall mean any work of authorship, including 58 | the original version of the Work and any modifications or additions 59 | to that Work or Derivative Works thereof, that is intentionally 60 | submitted to Licensor for inclusion in the Work by the copyright owner 61 | or by an individual or Legal Entity authorized to submit on behalf of 62 | the copyright owner. For the purposes of this definition, "submitted" 63 | means any form of electronic, verbal, or written communication sent 64 | to the Licensor or its representatives, including but not limited to 65 | communication on electronic mailing lists, source code control systems, 66 | and issue tracking systems that are managed by, or on behalf of, the 67 | Licensor for the purpose of discussing and improving the Work, but 68 | excluding communication that is conspicuously marked or otherwise 69 | designated in writing by the copyright owner as "Not a Contribution." 70 | 71 | "Contributor" shall mean Licensor and any individual or Legal Entity 72 | on behalf of whom a Contribution has been received by Licensor and 73 | subsequently incorporated within the Work. 74 | 75 | 2. Grant of Copyright License. Subject to the terms and conditions of 76 | this License, each Contributor hereby grants to You a perpetual, 77 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 78 | copyright license to reproduce, prepare Derivative Works of, 79 | publicly display, publicly perform, sublicense, and distribute the 80 | Work and such Derivative Works in Source or Object form. 81 | 82 | 3. Grant of Patent License. Subject to the terms and conditions of 83 | this License, each Contributor hereby grants to You a perpetual, 84 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 85 | (except as stated in this section) patent license to make, have made, 86 | use, offer to sell, sell, import, and otherwise transfer the Work, 87 | where such license applies only to those patent claims licensable 88 | by such Contributor that are necessarily infringed by their 89 | Contribution(s) alone or by combination of their Contribution(s) 90 | with the Work to which such Contribution(s) was submitted. If You 91 | institute patent litigation against any entity (including a 92 | cross-claim or counterclaim in a lawsuit) alleging that the Work 93 | or a Contribution incorporated within the Work constitutes direct 94 | or contributory patent infringement, then any patent licenses 95 | granted to You under this License for that Work shall terminate 96 | as of the date such litigation is filed. 97 | 98 | 4. Redistribution. You may reproduce and distribute copies of the 99 | Work or Derivative Works thereof in any medium, with or without 100 | modifications, and in Source or Object form, provided that You 101 | meet the following conditions: 102 | 103 | (a) You must give any other recipients of the Work or 104 | Derivative Works a copy of this License; and 105 | 106 | (b) You must cause any modified files to carry prominent notices 107 | stating that You changed the files; and 108 | 109 | (c) You must retain, in the Source form of any Derivative Works 110 | that You distribute, all copyright, patent, trademark, and 111 | attribution notices from the Source form of the Work, 112 | excluding those notices that do not pertain to any part of 113 | the Derivative Works; and 114 | 115 | (d) If the Work includes a "NOTICE" text file as part of its 116 | distribution, then any Derivative Works that You distribute must 117 | include a readable copy of the attribution notices contained 118 | within such NOTICE file, excluding those notices that do not 119 | pertain to any part of the Derivative Works, in at least one 120 | of the following places: within a NOTICE text file distributed 121 | as part of the Derivative Works; within the Source form or 122 | documentation, if provided along with the Derivative Works; or, 123 | within a display generated by the Derivative Works, if and 124 | wherever such third-party notices normally appear. The contents 125 | of the NOTICE file are for informational purposes only and 126 | do not modify the License. You may add Your own attribution 127 | notices within Derivative Works that You distribute, alongside 128 | or as an addendum to the NOTICE text from the Work, provided 129 | that such additional attribution notices cannot be construed 130 | as modifying the License. 131 | 132 | You may add Your own copyright statement to Your modifications and 133 | may provide additional or different license terms and conditions 134 | for use, reproduction, or distribution of Your modifications, or 135 | for any such Derivative Works as a whole, provided Your use, 136 | reproduction, and distribution of the Work otherwise complies with 137 | the conditions stated in this License. 138 | 139 | 5. Submission of Contributions. Unless You explicitly state otherwise, 140 | any Contribution intentionally submitted for inclusion in the Work 141 | by You to the Licensor shall be under the terms and conditions of 142 | this License, without any additional terms or conditions. 143 | Notwithstanding the above, nothing herein shall supersede or modify 144 | the terms of any separate license agreement you may have executed 145 | with Licensor regarding such Contributions. 146 | 147 | 6. Trademarks. This License does not grant permission to use the trade 148 | names, trademarks, service marks, or product names of the Licensor, 149 | except as required for reasonable and customary use in describing the 150 | origin of the Work and reproducing the content of the NOTICE file. 151 | 152 | 7. Disclaimer of Warranty. Unless required by applicable law or 153 | agreed to in writing, Licensor provides the Work (and each 154 | Contributor provides its Contributions) on an "AS IS" BASIS, 155 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 156 | implied, including, without limitation, any warranties or conditions 157 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 158 | PARTICULAR PURPOSE. You are solely responsible for determining the 159 | appropriateness of using or redistributing the Work and assume any 160 | risks associated with Your exercise of permissions under this License. 161 | 162 | 8. Limitation of Liability. In no event and under no legal theory, 163 | whether in tort (including negligence), contract, or otherwise, 164 | unless required by applicable law (such as deliberate and grossly 165 | negligent acts) or agreed to in writing, shall any Contributor be 166 | liable to You for damages, including any direct, indirect, special, 167 | incidental, or consequential damages of any character arising as a 168 | result of this License or out of the use or inability to use the 169 | Work (including but not limited to damages for loss of goodwill, 170 | work stoppage, computer failure or malfunction, or any and all 171 | other commercial damages or losses), even if such Contributor 172 | has been advised of the possibility of such damages. 173 | 174 | 9. Accepting Warranty or Additional Liability. While redistributing 175 | the Work or Derivative Works thereof, You may choose to offer, 176 | and charge a fee for, acceptance of support, warranty, indemnity, 177 | or other liability obligations and/or rights consistent with this 178 | License. However, in accepting such obligations, You may act only 179 | on Your own behalf and on Your sole responsibility, not on behalf 180 | of any other Contributor, and only if You agree to indemnify, 181 | defend, and hold each Contributor harmless for any liability 182 | incurred by, or claims asserted against, such Contributor by reason 183 | of your accepting any such warranty or additional liability. 184 | 185 | END OF TERMS AND CONDITIONS 186 | 187 | APPENDIX: How to apply the Apache License to your work. 188 | 189 | To apply the Apache License to your work, attach the following 190 | boilerplate notice, with the fields enclosed by brackets "[]" 191 | replaced with your own identifying information. (Don't include 192 | the brackets!) The text should be enclosed in the appropriate 193 | comment syntax for the file format. We also recommend that a 194 | file or class name and description of purpose be included on the 195 | same "printed page" as the copyright notice for easier 196 | identification within third-party archives. 197 | 198 | Copyright [yyyy] [name of copyright owner] 199 | 200 | Licensed under the Apache License, Version 2.0 (the "License"); 201 | you may not use this file except in compliance with the License. 202 | You may obtain a copy of the License at 203 | 204 | http://www.apache.org/licenses/LICENSE-2.0 205 | 206 | Unless required by applicable law or agreed to in writing, software 207 | distributed under the License is distributed on an "AS IS" BASIS, 208 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 209 | See the License for the specific language governing permissions and 210 | limitations under the License. -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | # Use multiple processes to speed up Pylint. 3 | jobs=0 4 | 5 | # Allow loading of arbitrary C extensions. Extensions are imported into the 6 | # active Python interpreter and may run arbitrary code. 7 | unsafe-load-any-extension=no 8 | 9 | # Allow optimization of some AST trees. This will activate a peephole AST 10 | # optimizer, which will apply various small optimizations. For instance, it can 11 | # be used to obtain the result of joining multiple strings with the addition 12 | # operator. Joining a lot of strings can lead to a maximum recursion error in 13 | # Pylint and this flag can prevent that. It has one side effect, the resulting 14 | # AST will be different than the one from reality. 15 | optimize-ast=no 16 | 17 | [MESSAGES CONTROL] 18 | 19 | # Only show warnings with the listed confidence levels. Leave empty to show 20 | # all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED 21 | confidence= 22 | 23 | # Disable the message, report, category or checker with the given id(s). You 24 | # can either give multiple identifiers separated by comma (,) or put this 25 | # option multiple times (only on the command line, not in the configuration 26 | # file where it should appear only once).You can also use "--disable=all" to 27 | # disable everything first and then reenable specific checks. For example, if 28 | # you want to run only the similarities checker, you can use "--disable=all 29 | # --enable=similarities". If you want to run only the classes checker, but have 30 | # no Warning level messages displayed, use"--disable=all --enable=classes 31 | # --disable=W" 32 | disable=all 33 | 34 | # Enable the message, report, category or checker with the given id(s). You can 35 | # either give multiple identifier separated by comma (,) or put this option 36 | # multiple time. See also the "--disable" option for examples. 37 | enable=import-error, 38 | import-self, 39 | reimported, 40 | wildcard-import, 41 | misplaced-future, 42 | relative-import, 43 | deprecated-module, 44 | unpacking-non-sequence, 45 | invalid-all-object, 46 | undefined-all-variable, 47 | used-before-assignment, 48 | cell-var-from-loop, 49 | global-variable-undefined, 50 | dangerous-default-value, 51 | # redefined-builtin, 52 | redefine-in-handler, 53 | unused-import, 54 | unused-wildcard-import, 55 | global-variable-not-assigned, 56 | undefined-loop-variable, 57 | global-at-module-level, 58 | bad-open-mode, 59 | redundant-unittest-assert, 60 | boolean-datetime, 61 | # unused-variable 62 | 63 | 64 | [REPORTS] 65 | 66 | # Set the output format. Available formats are text, parseable, colorized, msvs 67 | # (visual studio) and html. You can also give a reporter class, eg 68 | # mypackage.mymodule.MyReporterClass. 69 | output-format=parseable 70 | 71 | # Put messages in a separate file for each module / package specified on the 72 | # command line instead of printing them on stdout. Reports (if any) will be 73 | # written in a file name "pylint_global.[txt|html]". 74 | files-output=no 75 | 76 | # Tells whether to display a full report or only the messages 77 | reports=no 78 | 79 | # Python expression which should return a note less than 10 (10 is the highest 80 | # note). You have access to the variables errors warning, statement which 81 | # respectively contain the number of errors / warnings messages and the total 82 | # number of statements analyzed. This is used by the global evaluation report 83 | # (RP0004). 84 | evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) 85 | 86 | [BASIC] 87 | 88 | # List of builtins function names that should not be used, separated by a comma 89 | bad-functions=map,filter,input 90 | 91 | # Good variable names which should always be accepted, separated by a comma 92 | good-names=i,j,k,ex,Run,_ 93 | 94 | # Bad variable names which should always be refused, separated by a comma 95 | bad-names=foo,bar,baz,toto,tutu,tata 96 | 97 | # Colon-delimited sets of names that determine each other's naming style when 98 | # the name regexes allow several styles. 99 | name-group= 100 | 101 | # Include a hint for the correct naming format with invalid-name 102 | include-naming-hint=yes 103 | 104 | # Regular expression matching correct method names 105 | method-rgx=[a-z_][a-z0-9_]{2,30}$ 106 | 107 | # Naming hint for method names 108 | method-name-hint=[a-z_][a-z0-9_]{2,30}$ 109 | 110 | # Regular expression matching correct function names 111 | function-rgx=[a-z_][a-z0-9_]{2,30}$ 112 | 113 | # Naming hint for function names 114 | function-name-hint=[a-z_][a-z0-9_]{2,30}$ 115 | 116 | # Regular expression matching correct module names 117 | module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ 118 | 119 | # Naming hint for module names 120 | module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ 121 | 122 | # Regular expression matching correct attribute names 123 | attr-rgx=[a-z_][a-z0-9_]{2,30}$ 124 | 125 | # Naming hint for attribute names 126 | attr-name-hint=[a-z_][a-z0-9_]{2,30}$ 127 | 128 | # Regular expression matching correct class attribute names 129 | class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ 130 | 131 | # Naming hint for class attribute names 132 | class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ 133 | 134 | # Regular expression matching correct constant names 135 | const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ 136 | 137 | # Naming hint for constant names 138 | const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$ 139 | 140 | # Regular expression matching correct class names 141 | class-rgx=[A-Z_][a-zA-Z0-9]+$ 142 | 143 | # Naming hint for class names 144 | class-name-hint=[A-Z_][a-zA-Z0-9]+$ 145 | 146 | # Regular expression matching correct argument names 147 | argument-rgx=[a-z_][a-z0-9_]{2,30}$ 148 | 149 | # Naming hint for argument names 150 | argument-name-hint=[a-z_][a-z0-9_]{2,30}$ 151 | 152 | # Regular expression matching correct inline iteration names 153 | inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ 154 | 155 | # Naming hint for inline iteration names 156 | inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$ 157 | 158 | # Regular expression matching correct variable names 159 | variable-rgx=[a-z_][a-z0-9_]{2,30}$ 160 | 161 | # Naming hint for variable names 162 | variable-name-hint=[a-z_][a-z0-9_]{2,30}$ 163 | 164 | # Regular expression which should only match function or class names that do 165 | # not require a docstring. 166 | no-docstring-rgx=^_ 167 | 168 | # Minimum line length for functions/classes that require docstrings, shorter 169 | # ones are exempt. 170 | docstring-min-length=-1 171 | 172 | 173 | [ELIF] 174 | 175 | # Maximum number of nested blocks for function / method body 176 | max-nested-blocks=5 177 | 178 | 179 | [FORMAT] 180 | 181 | # Maximum number of characters on a single line. 182 | max-line-length=100 183 | 184 | # Regexp for a line that is allowed to be longer than the limit. 185 | ignore-long-lines=^\s*(# )??$ 186 | 187 | # Allow the body of an if to be on the same line as the test if there is no 188 | # else. 189 | single-line-if-stmt=no 190 | 191 | # List of optional constructs for which whitespace checking is disabled. `dict- 192 | # separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. 193 | # `trailing-comma` allows a space between comma and closing bracket: (a, ). 194 | # `empty-line` allows space-only lines. 195 | no-space-check=trailing-comma,dict-separator 196 | 197 | # Maximum number of lines in a module 198 | max-module-lines=1000 199 | 200 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 201 | # tab). 202 | indent-string=' ' 203 | 204 | # Number of spaces of indent required inside a hanging or continued line. 205 | indent-after-paren=4 206 | 207 | # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. 208 | expected-line-ending-format= 209 | 210 | 211 | [LOGGING] 212 | 213 | # Logging modules to check that the string format arguments are in logging 214 | # function parameter format 215 | logging-modules=logging 216 | 217 | 218 | [MISCELLANEOUS] 219 | 220 | # List of note tags to take in consideration, separated by a comma. 221 | notes=FIXME,XXX,TODO 222 | 223 | 224 | [SIMILARITIES] 225 | 226 | # Minimum lines number of a similarity. 227 | min-similarity-lines=4 228 | 229 | # Ignore comments when computing similarities. 230 | ignore-comments=yes 231 | 232 | # Ignore docstrings when computing similarities. 233 | ignore-docstrings=yes 234 | 235 | # Ignore imports when computing similarities. 236 | ignore-imports=no 237 | 238 | 239 | [SPELLING] 240 | 241 | # Spelling dictionary name. Available dictionaries: none. To make it working 242 | # install python-enchant package. 243 | spelling-dict= 244 | 245 | # List of comma separated words that should not be checked. 246 | spelling-ignore-words= 247 | 248 | # A path to a file that contains private dictionary; one word per line. 249 | spelling-private-dict-file= 250 | 251 | # Tells whether to store unknown words to indicated private dictionary in 252 | # --spelling-private-dict-file option instead of raising a message. 253 | spelling-store-unknown-words=no 254 | 255 | 256 | [TYPECHECK] 257 | 258 | # Tells whether missing members accessed in mixin class should be ignored. A 259 | # mixin class is detected if its name ends with "mixin" (case insensitive). 260 | ignore-mixin-members=yes 261 | 262 | # List of module names for which member attributes should not be checked 263 | # (useful for modules/projects where namespaces are manipulated during runtime 264 | # and thus existing member attributes cannot be deduced by static analysis. It 265 | # supports qualified module names, as well as Unix pattern matching. 266 | ignored-modules= 267 | 268 | # List of classes names for which member attributes should not be checked 269 | # (useful for classes with attributes dynamically set). This supports can work 270 | # with qualified names. 271 | ignored-classes= 272 | 273 | # List of members which are set dynamically and missed by pylint inference 274 | # system, and so shouldn't trigger E1101 when accessed. Python regular 275 | # expressions are accepted. 276 | generated-members= 277 | 278 | 279 | [VARIABLES] 280 | 281 | # Tells whether we should check for unused import in __init__ files. 282 | init-import=no 283 | 284 | # A regular expression matching the name of dummy variables (i.e. expectedly 285 | # not used). 286 | dummy-variables-rgx=_$|dummy 287 | 288 | # List of additional names supposed to be defined in builtins. Remember that 289 | # you should avoid to define new builtins when possible. 290 | additional-builtins= 291 | 292 | # List of strings which can identify a callback function by name. A callback 293 | # name must start or end with one of those strings. 294 | callbacks=cb_,_cb 295 | 296 | 297 | [CLASSES] 298 | 299 | # List of method names used to declare (i.e. assign) instance attributes. 300 | defining-attr-methods=__init__,__new__,setUp 301 | 302 | # List of valid names for the first argument in a class method. 303 | valid-classmethod-first-arg=cls 304 | 305 | # List of valid names for the first argument in a metaclass class method. 306 | valid-metaclass-classmethod-first-arg=mcs 307 | 308 | # List of member names, which should be excluded from the protected access 309 | # warning. 310 | exclude-protected=_asdict,_fields,_replace,_source,_make 311 | 312 | 313 | [DESIGN] 314 | 315 | # Maximum number of arguments for function / method 316 | max-args=5 317 | 318 | # Argument names that match this expression will be ignored. Default to name 319 | # with leading underscore 320 | ignored-argument-names=_.* 321 | 322 | # Maximum number of locals for function / method body 323 | max-locals=15 324 | 325 | # Maximum number of return / yield for function / method body 326 | max-returns=6 327 | 328 | # Maximum number of branch for function / method body 329 | max-branches=12 330 | 331 | # Maximum number of statements in function / method body 332 | max-statements=50 333 | 334 | # Maximum number of parents for a class (see R0901). 335 | max-parents=7 336 | 337 | # Maximum number of attributes for a class (see R0902). 338 | max-attributes=7 339 | 340 | # Minimum number of public methods for a class (see R0903). 341 | min-public-methods=2 342 | 343 | # Maximum number of public methods for a class (see R0904). 344 | max-public-methods=20 345 | 346 | # Maximum number of boolean expressions in a if statement 347 | max-bool-expr=5 348 | 349 | 350 | [IMPORTS] 351 | 352 | # Deprecated modules which should not be used, separated by a comma 353 | deprecated-modules=optparse 354 | 355 | # Create a graph of every (i.e. internal and external) dependencies in the 356 | # given file (report RP0402 must not be disabled) 357 | import-graph= 358 | 359 | # Create a graph of external dependencies in the given file (report RP0402 must 360 | # not be disabled) 361 | ext-import-graph= 362 | 363 | # Create a graph of internal dependencies in the given file (report RP0402 must 364 | # not be disabled) 365 | int-import-graph= 366 | 367 | 368 | [EXCEPTIONS] 369 | 370 | # Exceptions that will emit a warning when being caught. Defaults to 371 | # "Exception" 372 | overgeneral-exceptions=Exception 373 | --------------------------------------------------------------------------------