├── .circleci ├── config.yml └── merge_pr.sh ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ ├── config.yml │ └── feature_request.yml └── pull_request_template.md ├── .gitignore ├── .pre-commit-config.yaml ├── .project-template ├── fill_template_vars.py ├── refill_template_vars.py └── template_vars.txt ├── .readthedocs.yaml ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.md ├── docs ├── Makefile ├── api.rst ├── code_of_conduct.rst ├── conf.py ├── contributing.rst ├── index.rst ├── quickstart.rst ├── release_notes.rst ├── rlp.rst ├── rlp.sedes.rst └── tutorial.rst ├── newsfragments ├── README.md └── validate_files.py ├── pyproject.toml ├── rlp ├── __init__.py ├── atomic.py ├── codec.py ├── exceptions.py ├── lazy.py ├── sedes │ ├── __init__.py │ ├── big_endian_int.py │ ├── binary.py │ ├── boolean.py │ ├── lists.py │ ├── raw.py │ ├── serializable.py │ └── text.py └── utils.py ├── scripts └── release │ └── test_package.py ├── setup.py ├── tests └── core │ ├── rlptest.json │ ├── speed.py │ ├── test_benchmark.py │ ├── test_big_endian.py │ ├── test_binary_sedes.py │ ├── test_boolean_serializer.py │ ├── test_bytearray.py │ ├── test_codec.py │ ├── test_countablelist.py │ ├── test_import_and_version.py │ ├── test_invalid.py │ ├── test_json.py │ ├── test_lazy.py │ ├── test_raw_sedes.py │ ├── test_sedes.py │ ├── test_serializable.py │ └── test_text_sedes.py └── tox.ini /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | # heavily inspired by https://raw.githubusercontent.com/pinax/pinax-wiki/6bd2a99ab6f702e300d708532a6d1d9aa638b9f8/.circleci/config.yml 4 | 5 | common: &common 6 | working_directory: ~/repo 7 | steps: 8 | - checkout 9 | - run: 10 | name: checkout fixtures submodule 11 | command: git submodule update --init --recursive 12 | - run: 13 | name: merge pull request base 14 | command: ./.circleci/merge_pr.sh 15 | - run: 16 | name: merge pull request base (2nd try) 17 | command: ./.circleci/merge_pr.sh 18 | when: on_fail 19 | - run: 20 | name: merge pull request base (3rd try) 21 | command: ./.circleci/merge_pr.sh 22 | when: on_fail 23 | - restore_cache: 24 | keys: 25 | - cache-v1-{{ arch }}-{{ .Environment.CIRCLE_JOB }}-{{ checksum "setup.py" }}-{{ checksum "tox.ini" }} 26 | - run: 27 | name: install dependencies 28 | command: | 29 | python -m pip install --upgrade pip 30 | python -m pip install tox 31 | - run: 32 | name: run tox 33 | command: python -m tox run -r 34 | - save_cache: 35 | paths: 36 | - .hypothesis 37 | - .tox 38 | - ~/.cache/pip 39 | - ~/.local 40 | key: cache-v1-{{ arch }}-{{ .Environment.CIRCLE_JOB }}-{{ checksum "setup.py" }}-{{ checksum "tox.ini" }} 41 | 42 | orbs: 43 | win: circleci/windows@5.0.0 44 | 45 | windows-wheel-steps: 46 | windows-wheel-setup: &windows-wheel-setup 47 | executor: 48 | name: win/default 49 | shell: bash.exe 50 | working_directory: C:\Users\circleci\project\pyrlp 51 | environment: 52 | TOXENV: windows-wheel 53 | restore-cache-step: &restore-cache-step 54 | restore_cache: 55 | keys: 56 | - cache-v1-{{ arch }}-{{ .Environment.CIRCLE_JOB }}-{{ checksum "setup.py" }}-{{ checksum "tox.ini" }} 57 | install-pyenv-step: &install-pyenv-step 58 | run: 59 | name: install pyenv 60 | command: | 61 | pip install pyenv-win --target $HOME/.pyenv 62 | echo 'export PYENV="$HOME/.pyenv/pyenv-win/"' >> $BASH_ENV 63 | echo 'export PYENV_ROOT="$HOME/.pyenv/pyenv-win/"' >> $BASH_ENV 64 | echo 'export PYENV_USERPROFILE="$HOME/.pyenv/pyenv-win/"' >> $BASH_ENV 65 | echo 'export PATH="$PATH:$HOME/.pyenv/pyenv-win/bin"' >> $BASH_ENV 66 | echo 'export PATH="$PATH:$HOME/.pyenv/pyenv-win/shims"' >> $BASH_ENV 67 | source $BASH_ENV 68 | pyenv update 69 | install-latest-python-step: &install-latest-python-step 70 | run: 71 | name: install latest python version and tox 72 | command: | 73 | LATEST_VERSION=$(pyenv install --list | grep -E "${MINOR_VERSION}\.[0-9]+$" | tail -1) 74 | echo "installing python version $LATEST_VERSION" 75 | pyenv install $LATEST_VERSION 76 | pyenv global $LATEST_VERSION 77 | python3 -m pip install --upgrade pip 78 | python3 -m pip install tox 79 | run-tox-step: &run-tox-step 80 | run: 81 | name: run tox 82 | command: | 83 | echo 'running tox with' $(python3 --version) 84 | python3 -m tox run -r 85 | save-cache-step: &save-cache-step 86 | save_cache: 87 | paths: 88 | - .tox 89 | key: cache-v1-{{ arch }}-{{ .Environment.CIRCLE_JOB }}-{{ checksum "setup.py" }}-{{ checksum "tox.ini" }} 90 | 91 | docs: &docs 92 | working_directory: ~/repo 93 | steps: 94 | - checkout 95 | - restore_cache: 96 | keys: 97 | - cache-v1-{{ arch }}-{{ .Environment.CIRCLE_JOB }}-{{ checksum "setup.py" }}-{{ checksum "tox.ini" }} 98 | - run: 99 | name: install dependencies 100 | command: | 101 | python -m pip install --upgrade pip 102 | python -m pip install tox 103 | - run: 104 | name: install latexpdf dependencies 105 | command: | 106 | sudo apt-get update 107 | sudo apt-get install latexmk tex-gyre texlive-fonts-extra texlive-xetex xindy 108 | - run: 109 | name: run tox 110 | command: python -m tox run -r 111 | - store_artifacts: 112 | path: /home/circleci/repo/docs/_build 113 | - save_cache: 114 | paths: 115 | - .tox 116 | - ~/.cache/pip 117 | - ~/.local 118 | key: cache-v1-{{ arch }}-{{ .Environment.CIRCLE_JOB }}-{{ checksum "setup.py" }}-{{ checksum "tox.ini" }} 119 | resource_class: xlarge 120 | 121 | jobs: 122 | docs: 123 | <<: *docs 124 | docker: 125 | - image: cimg/python:3.10 126 | environment: 127 | TOXENV: docs 128 | 129 | py38-core: 130 | <<: *common 131 | docker: 132 | - image: cimg/python:3.8 133 | environment: 134 | TOXENV: py38-core 135 | py39-core: 136 | <<: *common 137 | docker: 138 | - image: cimg/python:3.9 139 | environment: 140 | TOXENV: py39-core 141 | py310-core: 142 | <<: *common 143 | docker: 144 | - image: cimg/python:3.10 145 | environment: 146 | TOXENV: py310-core 147 | py311-core: 148 | <<: *common 149 | docker: 150 | - image: cimg/python:3.11 151 | environment: 152 | TOXENV: py311-core 153 | py312-core: 154 | <<: *common 155 | docker: 156 | - image: cimg/python:3.12 157 | environment: 158 | TOXENV: py312-core 159 | py313-core: 160 | <<: *common 161 | docker: 162 | - image: cimg/python:3.13 163 | environment: 164 | TOXENV: py313-core 165 | 166 | py38-lint: 167 | <<: *common 168 | docker: 169 | - image: cimg/python:3.8 170 | environment: 171 | TOXENV: py38-lint 172 | py39-lint: 173 | <<: *common 174 | docker: 175 | - image: cimg/python:3.9 176 | environment: 177 | TOXENV: py39-lint 178 | py310-lint: 179 | <<: *common 180 | docker: 181 | - image: cimg/python:3.10 182 | environment: 183 | TOXENV: py310-lint 184 | py311-lint: 185 | <<: *common 186 | docker: 187 | - image: cimg/python:3.11 188 | environment: 189 | TOXENV: py311-lint 190 | py312-lint: 191 | <<: *common 192 | docker: 193 | - image: cimg/python:3.12 194 | environment: 195 | TOXENV: py312-lint 196 | py313-lint: 197 | <<: *common 198 | docker: 199 | - image: cimg/python:3.13 200 | environment: 201 | TOXENV: py313-lint 202 | 203 | py38-wheel: 204 | <<: *common 205 | docker: 206 | - image: cimg/python:3.8 207 | environment: 208 | TOXENV: py38-wheel 209 | py39-wheel: 210 | <<: *common 211 | docker: 212 | - image: cimg/python:3.9 213 | environment: 214 | TOXENV: py39-wheel 215 | py310-wheel: 216 | <<: *common 217 | docker: 218 | - image: cimg/python:3.10 219 | environment: 220 | TOXENV: py310-wheel 221 | py311-wheel: 222 | <<: *common 223 | docker: 224 | - image: cimg/python:3.11 225 | environment: 226 | TOXENV: py311-wheel 227 | py312-wheel: 228 | <<: *common 229 | docker: 230 | - image: cimg/python:3.12 231 | environment: 232 | TOXENV: py312-wheel 233 | py313-wheel: 234 | <<: *common 235 | docker: 236 | - image: cimg/python:3.13 237 | environment: 238 | TOXENV: py313-wheel 239 | 240 | py311-windows-wheel: 241 | <<: *windows-wheel-setup 242 | steps: 243 | - checkout 244 | - <<: *restore-cache-step 245 | - <<: *install-pyenv-step 246 | - run: 247 | name: set minor version 248 | command: echo "export MINOR_VERSION='3.11'" >> $BASH_ENV 249 | - <<: *install-latest-python-step 250 | - <<: *run-tox-step 251 | - <<: *save-cache-step 252 | 253 | py312-windows-wheel: 254 | <<: *windows-wheel-setup 255 | steps: 256 | - checkout 257 | - <<: *restore-cache-step 258 | - <<: *install-pyenv-step 259 | - run: 260 | name: set minor version 261 | command: echo "export MINOR_VERSION='3.12'" >> $BASH_ENV 262 | - <<: *install-latest-python-step 263 | - <<: *run-tox-step 264 | - <<: *save-cache-step 265 | 266 | py313-windows-wheel: 267 | <<: *windows-wheel-setup 268 | steps: 269 | - checkout 270 | - <<: *restore-cache-step 271 | - <<: *install-pyenv-step 272 | - run: 273 | name: set minor version 274 | command: echo "export MINOR_VERSION='3.13'" >> $BASH_ENV 275 | - <<: *install-latest-python-step 276 | - <<: *run-tox-step 277 | - <<: *save-cache-step 278 | 279 | py38-rust-backend: 280 | <<: *common 281 | docker: 282 | - image: cimg/python:3.8 283 | environment: 284 | TOXENV: py38-rust-backend 285 | py39-rust-backend: 286 | <<: *common 287 | docker: 288 | - image: cimg/python:3.9 289 | environment: 290 | TOXENV: py39-rust-backend 291 | py310-rust-backend: 292 | <<: *common 293 | docker: 294 | - image: cimg/python:3.10 295 | environment: 296 | TOXENV: py310-rust-backend 297 | py311-rust-backend: 298 | <<: *common 299 | docker: 300 | - image: cimg/python:3.11 301 | environment: 302 | TOXENV: py311-rust-backend 303 | py312-rust-backend: 304 | <<: *common 305 | docker: 306 | - image: cimg/python:3.12 307 | environment: 308 | TOXENV: py312-rust-backend 309 | 310 | define: &all_jobs 311 | - docs 312 | - py38-core 313 | - py39-core 314 | - py310-core 315 | - py311-core 316 | - py312-core 317 | - py313-core 318 | - py38-lint 319 | - py39-lint 320 | - py310-lint 321 | - py311-lint 322 | - py312-lint 323 | - py313-lint 324 | - py38-wheel 325 | - py39-wheel 326 | - py310-wheel 327 | - py311-wheel 328 | - py312-wheel 329 | - py313-wheel 330 | - py311-windows-wheel 331 | - py312-windows-wheel 332 | - py313-windows-wheel 333 | - py38-rust-backend 334 | - py39-rust-backend 335 | - py310-rust-backend 336 | - py311-rust-backend 337 | - py312-rust-backend 338 | 339 | workflows: 340 | version: 2 341 | test: 342 | jobs: *all_jobs 343 | nightly: 344 | triggers: 345 | - schedule: 346 | # Weekdays 12:00p UTC 347 | cron: "0 12 * * 1,2,3,4,5" 348 | filters: 349 | branches: 350 | only: 351 | - main 352 | jobs: *all_jobs 353 | -------------------------------------------------------------------------------- /.circleci/merge_pr.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if [[ -n "${CIRCLE_PR_NUMBER}" ]]; then 4 | PR_INFO_URL=https://api.github.com/repos/$CIRCLE_PROJECT_USERNAME/$CIRCLE_PROJECT_REPONAME/pulls/$CIRCLE_PR_NUMBER 5 | PR_BASE_BRANCH=$(curl -L "$PR_INFO_URL" | python -c 'import json, sys; obj = json.load(sys.stdin); sys.stdout.write(obj["base"]["ref"])') 6 | git fetch origin +"$PR_BASE_BRANCH":circleci/pr-base 7 | # We need these config values or git complains when creating the 8 | # merge commit 9 | git config --global user.name "Circle CI" 10 | git config --global user.email "circleci@example.com" 11 | git merge --no-edit circleci/pr-base 12 | fi 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: File a bug report 3 | labels: ["bug"] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: "## What was wrong" 8 | - type: textarea 9 | id: what-happened 10 | attributes: 11 | label: What happened? 12 | description: Also tell us what you expected to happen 13 | validations: 14 | required: true 15 | - type: textarea 16 | id: code-that-caused 17 | attributes: 18 | label: Code that produced the error 19 | description: Formats to Python, no backticks needed 20 | render: python 21 | validations: 22 | required: false 23 | - type: textarea 24 | id: error-output 25 | attributes: 26 | label: Full error output 27 | description: Formats to shell, no backticks needed 28 | render: shell 29 | validations: 30 | required: false 31 | - type: markdown 32 | attributes: 33 | value: "## Potential Solutions" 34 | - type: textarea 35 | id: how-to-fix 36 | attributes: 37 | label: Fill this section in if you know how this could or should be fixed 38 | description: Include any relevant examples or reference material 39 | validations: 40 | required: false 41 | - type: input 42 | id: lib-version 43 | attributes: 44 | label: rlp Version 45 | description: Which version of rlp are you using? 46 | placeholder: x.x.x 47 | validations: 48 | required: false 49 | - type: input 50 | id: py-version 51 | attributes: 52 | label: Python Version 53 | description: Which version of Python are you using? 54 | placeholder: x.x.x 55 | validations: 56 | required: false 57 | - type: input 58 | id: os 59 | attributes: 60 | label: Operating System 61 | description: Which operating system are you using? 62 | placeholder: macos/linux/win 63 | validations: 64 | required: false 65 | - type: textarea 66 | id: pip-freeze 67 | attributes: 68 | label: Output from `pip freeze` 69 | description: Run `python -m pip freeze` and paste the output below 70 | render: shell 71 | validations: 72 | required: false 73 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true 2 | contact_links: 3 | - name: Questions about using pyrlp? 4 | url: https://discord.gg/GHryRvPB84 5 | about: You can ask and answer usage questions on the Ethereum Python Community Discord 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: Feature Request 2 | description: Request a new feature 3 | labels: ["feature_request"] 4 | body: 5 | - type: textarea 6 | id: feature-description 7 | attributes: 8 | label: What feature should we add? 9 | description: Include any relevant examples or reference material 10 | validations: 11 | required: true 12 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ### What was wrong? 2 | 3 | Related to Issue # 4 | Closes # 5 | 6 | ### How was it fixed? 7 | 8 | ### Todo: 9 | 10 | - [ ] Clean up commit history 11 | - [ ] Add or update documentation related to these changes 12 | - [ ] Add entry to the [release notes](https://github.com/ethereum/pyrlp/blob/main/newsfragments/README.md) 13 | 14 | #### Cute Animal Picture 15 | 16 | ![Put a link to a cute animal picture inside the parenthesis-->](<>) 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | .build 12 | eggs 13 | .eggs 14 | parts 15 | bin 16 | var 17 | sdist 18 | develop-eggs 19 | .installed.cfg 20 | lib 21 | lib64 22 | pip-wheel-metadata 23 | venv* 24 | .venv* 25 | 26 | # Installer logs 27 | pip-log.txt 28 | 29 | # Unit test / coverage reports 30 | .coverage 31 | .tox 32 | nosetests.xml 33 | 34 | # Translations 35 | *.mo 36 | 37 | # Mr Developer 38 | .mr.developer.cfg 39 | .project 40 | .pydevproject 41 | 42 | # Complexity 43 | output/*.html 44 | output/*/index.html 45 | 46 | # Sphinx 47 | docs/_build 48 | docs/modules.rst 49 | docs/*.internal.rst 50 | docs/*.utils.rst 51 | docs/*._utils.* 52 | 53 | # Blockchain 54 | chains 55 | 56 | # Hypothesis Property base testing 57 | .hypothesis 58 | 59 | # tox/pytest cache 60 | .cache 61 | .pytest_cache 62 | 63 | # pycache 64 | __pycache__/ 65 | 66 | # Test output logs 67 | logs 68 | 69 | # VIM temp files 70 | *.sw[op] 71 | 72 | # mypy 73 | .mypy_cache 74 | 75 | # macOS 76 | .DS_Store 77 | 78 | # pyenv 79 | .python-version 80 | 81 | # vs-code 82 | .vscode 83 | 84 | # jupyter notebook files 85 | *.ipynb 86 | 87 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 88 | # For a more precise, explicit template, see: 89 | # https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 90 | 91 | ## General 92 | .idea/* 93 | .idea_modules/* 94 | 95 | ## File-based project format: 96 | *.iws 97 | 98 | ## IntelliJ 99 | out/ 100 | 101 | ## Plugin-specific files: 102 | 103 | ### JIRA plugin 104 | atlassian-ide-plugin.xml 105 | 106 | ### Crashlytics plugin (for Android Studio and IntelliJ) 107 | com_crashlytics_export_strings.xml 108 | crashlytics.properties 109 | crashlytics-build.properties 110 | fabric.properties 111 | 112 | # END JetBrains section 113 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | exclude: '.project-template|docs/conf.py' 2 | repos: 3 | - repo: https://github.com/pre-commit/pre-commit-hooks 4 | rev: v4.5.0 5 | hooks: 6 | - id: check-yaml 7 | - id: check-toml 8 | - id: end-of-file-fixer 9 | - id: trailing-whitespace 10 | - repo: https://github.com/asottile/pyupgrade 11 | rev: v3.15.0 12 | hooks: 13 | - id: pyupgrade 14 | args: [--py38-plus] 15 | - repo: https://github.com/psf/black 16 | rev: 23.9.1 17 | hooks: 18 | - id: black 19 | - repo: https://github.com/PyCQA/flake8 20 | rev: 6.1.0 21 | hooks: 22 | - id: flake8 23 | additional_dependencies: 24 | - flake8-bugbear==23.9.16 25 | exclude: setup.py 26 | - repo: https://github.com/PyCQA/autoflake 27 | rev: v2.2.1 28 | hooks: 29 | - id: autoflake 30 | - repo: https://github.com/pycqa/isort 31 | rev: 5.12.0 32 | hooks: 33 | - id: isort 34 | - repo: https://github.com/pycqa/pydocstyle 35 | rev: 6.3.0 36 | hooks: 37 | - id: pydocstyle 38 | additional_dependencies: 39 | - tomli # required until >= python311 40 | - repo: https://github.com/executablebooks/mdformat 41 | rev: 0.7.17 42 | hooks: 43 | - id: mdformat 44 | additional_dependencies: 45 | - mdformat-gfm 46 | - repo: https://github.com/PrincetonUniversity/blocklint 47 | rev: v0.2.5 48 | hooks: 49 | - id: blocklint 50 | exclude: 'docs/Makefile|docs/release_notes.rst|tox.ini' 51 | - repo: local 52 | hooks: 53 | - id: check-rst-files 54 | name: Check for .rst files in the top-level directory 55 | entry: sh -c 'ls *.rst 1>/dev/null 2>&1 && { echo "found .rst file in top-level folder"; exit 1; } || exit 0' 56 | language: system 57 | always_run: true 58 | pass_filenames: false 59 | -------------------------------------------------------------------------------- /.project-template/fill_template_vars.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | import sys 5 | import re 6 | from pathlib import Path 7 | 8 | 9 | def _find_files(project_root): 10 | path_exclude_pattern = r"\.git($|\/)|venv|_build" 11 | file_exclude_pattern = r"fill_template_vars\.py|\.swp$" 12 | filepaths = [] 13 | for dir_path, _dir_names, file_names in os.walk(project_root): 14 | if not re.search(path_exclude_pattern, dir_path): 15 | for file in file_names: 16 | if not re.search(file_exclude_pattern, file): 17 | filepaths.append(str(Path(dir_path, file))) 18 | 19 | return filepaths 20 | 21 | 22 | def _replace(pattern, replacement, project_root): 23 | print(f"Replacing values: {pattern}") 24 | for file in _find_files(project_root): 25 | try: 26 | with open(file) as f: 27 | content = f.read() 28 | content = re.sub(pattern, replacement, content) 29 | with open(file, "w") as f: 30 | f.write(content) 31 | except UnicodeDecodeError: 32 | pass 33 | 34 | 35 | def main(): 36 | project_root = Path(os.path.realpath(sys.argv[0])).parent.parent 37 | 38 | module_name = input("What is your python module name? ") 39 | 40 | pypi_input = input(f"What is your pypi package name? (default: {module_name}) ") 41 | pypi_name = pypi_input or module_name 42 | 43 | repo_input = input(f"What is your github project name? (default: {pypi_name}) ") 44 | repo_name = repo_input or pypi_name 45 | 46 | rtd_input = input( 47 | f"What is your readthedocs.org project name? (default: {pypi_name}) " 48 | ) 49 | rtd_name = rtd_input or pypi_name 50 | 51 | project_input = input( 52 | f"What is your project name (ex: at the top of the README)? (default: {repo_name}) " 53 | ) 54 | project_name = project_input or repo_name 55 | 56 | short_description = input("What is a one-liner describing the project? ") 57 | 58 | _replace("", module_name, project_root) 59 | _replace("", pypi_name, project_root) 60 | _replace("", repo_name, project_root) 61 | _replace("", rtd_name, project_root) 62 | _replace("", project_name, project_root) 63 | _replace("", short_description, project_root) 64 | 65 | os.makedirs(project_root / module_name, exist_ok=True) 66 | Path(project_root / module_name / "__init__.py").touch() 67 | Path(project_root / module_name / "py.typed").touch() 68 | 69 | 70 | if __name__ == "__main__": 71 | main() 72 | -------------------------------------------------------------------------------- /.project-template/refill_template_vars.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | import sys 5 | from pathlib import Path 6 | import subprocess 7 | 8 | 9 | def main(): 10 | template_dir = Path(os.path.dirname(sys.argv[0])) 11 | template_vars_file = template_dir / "template_vars.txt" 12 | fill_template_vars_script = template_dir / "fill_template_vars.py" 13 | 14 | with open(template_vars_file, "r") as input_file: 15 | content_lines = input_file.readlines() 16 | 17 | process = subprocess.Popen( 18 | [sys.executable, str(fill_template_vars_script)], 19 | stdin=subprocess.PIPE, 20 | stdout=subprocess.PIPE, 21 | stderr=subprocess.PIPE, 22 | text=True, 23 | ) 24 | 25 | for line in content_lines: 26 | process.stdin.write(line) 27 | process.stdin.flush() 28 | 29 | stdout, stderr = process.communicate() 30 | 31 | if process.returncode != 0: 32 | print(f"Error occurred: {stderr}") 33 | sys.exit(1) 34 | 35 | print(stdout) 36 | 37 | 38 | if __name__ == "__main__": 39 | main() 40 | -------------------------------------------------------------------------------- /.project-template/template_vars.txt: -------------------------------------------------------------------------------- 1 | rlp 2 | rlp 3 | pyrlp 4 | pyrlp 5 | pyrlp 6 | A package for Recursive Length Prefix encoding and decoding 7 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | build: 4 | os: ubuntu-22.04 5 | tools: 6 | python: "3.10" 7 | 8 | sphinx: 9 | configuration: docs/conf.py 10 | fail_on_warning: true 11 | 12 | python: 13 | install: 14 | - method: pip 15 | path: . 16 | extra_requirements: 17 | - docs 18 | 19 | # Build all formats for RTD Downloads - htmlzip, pdf, epub 20 | formats: all 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Jnnk, Vitalik Buterin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.md 3 | 4 | recursive-include scripts * 5 | recursive-include tests * 6 | 7 | global-include *.pyi 8 | 9 | recursive-exclude * __pycache__ 10 | recursive-exclude * *.py[co] 11 | prune .tox 12 | prune venv* 13 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | CURRENT_SIGN_SETTING := $(shell git config commit.gpgSign) 2 | 3 | .PHONY: clean-pyc clean-build docs 4 | 5 | help: 6 | @echo "clean-build - remove build artifacts" 7 | @echo "clean-pyc - remove Python file artifacts" 8 | @echo "clean - run clean-build and clean-pyc" 9 | @echo "dist - build package and cat contents of the dist directory" 10 | @echo "lint - fix linting issues with pre-commit" 11 | @echo "test - run tests quickly with the default Python" 12 | @echo "docs - generate docs and open in browser (linux-docs for version on linux)" 13 | @echo "autobuild-docs - live update docs when changes are saved" 14 | @echo "package-test - build package and install it in a venv for manual testing" 15 | @echo "notes - consume towncrier newsfragments and update release notes in docs - requires bump to be set" 16 | @echo "release - package and upload a release (does not run notes target) - requires bump to be set" 17 | 18 | clean-build: 19 | rm -fr build/ 20 | rm -fr dist/ 21 | rm -fr *.egg-info 22 | 23 | clean-pyc: 24 | find . -name '*.pyc' -exec rm -f {} + 25 | find . -name '*.pyo' -exec rm -f {} + 26 | find . -name '*~' -exec rm -f {} + 27 | find . -name '__pycache__' -exec rm -rf {} + 28 | 29 | clean: clean-build clean-pyc 30 | 31 | dist: clean 32 | python -m build 33 | ls -l dist 34 | 35 | lint: 36 | @pre-commit run --all-files --show-diff-on-failure || ( \ 37 | echo "\n\n\n * pre-commit should have fixed the errors above. Running again to make sure everything is good..." \ 38 | && pre-commit run --all-files --show-diff-on-failure \ 39 | ) 40 | 41 | test: 42 | python -m pytest tests 43 | 44 | # docs commands 45 | 46 | docs: check-docs 47 | open docs/_build/html/index.html 48 | 49 | linux-docs: check-docs 50 | xdg-open docs/_build/html/index.html 51 | 52 | autobuild-docs: 53 | sphinx-autobuild --open-browser docs docs/_build/html 54 | 55 | # docs helpers 56 | 57 | validate-newsfragments: 58 | python ./newsfragments/validate_files.py 59 | towncrier build --draft --version preview 60 | 61 | check-docs: build-docs validate-newsfragments 62 | 63 | build-docs: 64 | sphinx-apidoc -o docs/ . setup.py "*conftest*" 65 | $(MAKE) -C docs clean 66 | $(MAKE) -C docs html 67 | $(MAKE) -C docs doctest 68 | 69 | check-docs-ci: build-docs build-docs-ci validate-newsfragments 70 | 71 | build-docs-ci: 72 | $(MAKE) -C docs latexpdf 73 | $(MAKE) -C docs epub 74 | 75 | # release commands 76 | 77 | package-test: clean 78 | python -m build 79 | python scripts/release/test_package.py 80 | 81 | notes: check-bump 82 | # Let UPCOMING_VERSION be the version that is used for the current bump 83 | $(eval UPCOMING_VERSION=$(shell bump-my-version bump --dry-run $(bump) -v | awk -F"'" '/New version will be / {print $$2}')) 84 | # Now generate the release notes to have them included in the release commit 85 | towncrier build --yes --version $(UPCOMING_VERSION) 86 | # Before we bump the version, make sure that the towncrier-generated docs will build 87 | make build-docs 88 | git commit -m "Compile release notes for v$(UPCOMING_VERSION)" 89 | 90 | release: check-bump check-git clean 91 | # verify that notes command ran correctly 92 | ./newsfragments/validate_files.py is-empty 93 | CURRENT_SIGN_SETTING=$(git config commit.gpgSign) 94 | git config commit.gpgSign true 95 | bump-my-version bump $(bump) 96 | python -m build 97 | git config commit.gpgSign "$(CURRENT_SIGN_SETTING)" 98 | git push upstream && git push upstream --tags 99 | twine upload dist/* 100 | 101 | # release helpers 102 | 103 | check-bump: 104 | ifndef bump 105 | $(error bump must be set, typically: major, minor, patch, or devnum) 106 | endif 107 | 108 | check-git: 109 | # require that upstream is configured for ethereum/pyrlp 110 | @if ! git remote -v | grep "upstream[[:space:]]git@github.com:ethereum/pyrlp.git (push)\|upstream[[:space:]]https://github.com/ethereum/pyrlp (push)"; then \ 111 | echo "Error: You must have a remote named 'upstream' that points to 'pyrlp'"; \ 112 | exit 1; \ 113 | fi 114 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pyrlp 2 | 3 | [![Join the conversation on Discord](https://img.shields.io/discord/809793915578089484?color=blue&label=chat&logo=discord&logoColor=white)](https://discord.gg/GHryRvPB84) 4 | [![Build Status](https://circleci.com/gh/ethereum/pyrlp.svg?style=shield)](https://circleci.com/gh/ethereum/pyrlp) 5 | [![PyPI version](https://badge.fury.io/py/rlp.svg)](https://badge.fury.io/py/rlp) 6 | [![Python versions](https://img.shields.io/pypi/pyversions/rlp.svg)](https://pypi.python.org/pypi/rlp) 7 | [![Docs build](https://readthedocs.org/projects/pyrlp/badge/?version=latest)](https://pyrlp.readthedocs.io/en/latest/?badge=latest) 8 | 9 | A package for Recursive Length Prefix encoding and decoding 10 | 11 | Read the [documentation](https://pyrlp.readthedocs.io/). 12 | 13 | View the [change log](https://pyrlp.readthedocs.io/en/latest/release_notes.html). 14 | 15 | ## Installation 16 | 17 | ```sh 18 | python -m pip install rlp 19 | ``` 20 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | 49 | clean: 50 | rm -rf $(BUILDDIR)/* 51 | 52 | html: 53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 54 | @echo 55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 56 | 57 | dirhtml: 58 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 59 | @echo 60 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 61 | 62 | singlehtml: 63 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 64 | @echo 65 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 66 | 67 | pickle: 68 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 69 | @echo 70 | @echo "Build finished; now you can process the pickle files." 71 | 72 | json: 73 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 74 | @echo 75 | @echo "Build finished; now you can process the JSON files." 76 | 77 | htmlhelp: 78 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 79 | @echo 80 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 81 | ".hhp project file in $(BUILDDIR)/htmlhelp." 82 | 83 | qthelp: 84 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 85 | @echo 86 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 87 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 88 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/pyrlp.qhcp" 89 | @echo "To view the help file:" 90 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/pyrlp.qhc" 91 | 92 | devhelp: 93 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 94 | @echo 95 | @echo "Build finished." 96 | @echo "To view the help file:" 97 | @echo "# mkdir -p $$HOME/.local/share/devhelp/pyrlp" 98 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/pyrlp" 99 | @echo "# devhelp" 100 | 101 | epub: 102 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 103 | @echo 104 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 105 | 106 | latex: 107 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 108 | @echo 109 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 110 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 111 | "(use \`make latexpdf' here to do that automatically)." 112 | 113 | latexpdf: 114 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 115 | @echo "Running LaTeX files through pdflatex..." 116 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 117 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 118 | 119 | latexpdfja: 120 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 121 | @echo "Running LaTeX files through platex and dvipdfmx..." 122 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 123 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 124 | 125 | text: 126 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 127 | @echo 128 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 129 | 130 | man: 131 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 132 | @echo 133 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 134 | 135 | texinfo: 136 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 137 | @echo 138 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 139 | @echo "Run \`make' in that directory to run these through makeinfo" \ 140 | "(use \`make info' here to do that automatically)." 141 | 142 | info: 143 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 144 | @echo "Running Texinfo files through makeinfo..." 145 | make -C $(BUILDDIR)/texinfo info 146 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 147 | 148 | gettext: 149 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 150 | @echo 151 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 152 | 153 | changes: 154 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 155 | @echo 156 | @echo "The overview file is in $(BUILDDIR)/changes." 157 | 158 | linkcheck: 159 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 160 | @echo 161 | @echo "Link check complete; look for any errors in the above output " \ 162 | "or in $(BUILDDIR)/linkcheck/output.txt." 163 | 164 | doctest: 165 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 166 | @echo "Testing of doctests in the sources finished, look at the " \ 167 | "results in $(BUILDDIR)/doctest/output.txt." 168 | 169 | xml: 170 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 171 | @echo 172 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 173 | 174 | pseudoxml: 175 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 176 | @echo 177 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 178 | -------------------------------------------------------------------------------- /docs/api.rst: -------------------------------------------------------------------------------- 1 | .. _api-reference: 2 | 3 | API Reference 4 | ============= 5 | 6 | Functions 7 | --------- 8 | 9 | .. autofunction:: rlp.encode 10 | 11 | .. autofunction:: rlp.decode 12 | 13 | .. autofunction:: rlp.decode_lazy 14 | 15 | .. autoclass:: rlp.LazyList 16 | 17 | .. autofunction:: rlp.infer_sedes 18 | 19 | 20 | Sedes Objects 21 | ------------- 22 | 23 | .. data:: rlp.sedes.raw 24 | 25 | A sedes object that does nothing. Thus, it can serialize everything that can 26 | be directly encoded in RLP (nested lists of strings). This sedes can be used 27 | as a placeholder when deserializing larger structures. 28 | 29 | .. autoclass:: rlp.sedes.Binary 30 | 31 | .. automethod:: rlp.sedes.Binary.fixed_length 32 | 33 | .. data:: rlp.sedes.binary 34 | 35 | A sedes object for binary data of arbitrary length (an instance of 36 | :class:`rlp.sedes.Binary` with default arguments). 37 | 38 | .. autoclass:: rlp.sedes.Boolean 39 | 40 | .. data:: rlp.sedes.boolean 41 | 42 | A sedes object for boolean types. 43 | 44 | .. autoclass:: rlp.sedes.Text 45 | 46 | .. automethod:: rlp.sedes.Text.fixed_length 47 | 48 | .. data:: rlp.sedes.text 49 | 50 | A sedes object for utf encoded text data of arbitrary length (an instance of 51 | :class:`rlp.sedes.Text` with default arguments). 52 | 53 | .. autoclass:: rlp.sedes.BigEndianInt 54 | 55 | .. data:: rlp.sedes.big_endian_int 56 | 57 | A sedes object for integers encoded in big endian without any leading zeros 58 | (an instance of :class:`rlp.sedes.BigEndianInt` with default arguments). 59 | 60 | .. autoclass:: rlp.sedes.List 61 | 62 | .. autoclass:: rlp.sedes.CountableList 63 | 64 | .. autoclass:: rlp.Serializable 65 | :members: 66 | 67 | Exceptions 68 | ---------- 69 | 70 | .. autoexception:: rlp.RLPException 71 | 72 | .. autoexception:: rlp.EncodingError 73 | 74 | .. autoexception:: rlp.DecodingError 75 | 76 | .. autoexception:: rlp.SerializationError 77 | 78 | .. autoexception:: rlp.DeserializationError 79 | -------------------------------------------------------------------------------- /docs/code_of_conduct.rst: -------------------------------------------------------------------------------- 1 | Code of Conduct 2 | --------------- 3 | 4 | Our Pledge 5 | ~~~~~~~~~~ 6 | 7 | In the interest of fostering an open and welcoming environment, we as 8 | contributors and maintainers pledge to make participation in our project and 9 | our community a harassment-free experience for everyone, regardless of age, body 10 | size, disability, ethnicity, gender identity and expression, level of experience, 11 | education, socio-economic status, nationality, personal appearance, race, 12 | religion, or sexual identity and orientation. 13 | 14 | Our Standards 15 | ~~~~~~~~~~~~~ 16 | 17 | Examples of behavior that contributes to creating a positive environment 18 | include: 19 | 20 | * Using welcoming and inclusive language 21 | * Being respectful of differing viewpoints and experiences 22 | * Gracefully accepting constructive criticism 23 | * Focusing on what is best for the community 24 | * Showing empathy towards other community members 25 | 26 | Examples of unacceptable behavior by participants include: 27 | 28 | * The use of sexualized language or imagery and unwelcome sexual attention or 29 | advances 30 | * Trolling, insulting/derogatory comments, and personal or political attacks 31 | * Public or private harassment 32 | * Publishing others' private information, such as a physical or electronic 33 | address, without explicit permission 34 | * Other conduct which could reasonably be considered inappropriate in a 35 | professional setting 36 | 37 | Our Responsibilities 38 | ~~~~~~~~~~~~~~~~~~~~ 39 | 40 | Project maintainers are responsible for clarifying the standards of acceptable 41 | behavior and are expected to take appropriate and fair corrective action in 42 | response to any instances of unacceptable behavior. 43 | 44 | Project maintainers have the right and responsibility to remove, edit, or 45 | reject comments, commits, code, wiki edits, issues, and other contributions 46 | that are not aligned to this Code of Conduct, or to ban temporarily or 47 | permanently any contributor for other behaviors that they deem inappropriate, 48 | threatening, offensive, or harmful. 49 | 50 | Scope 51 | ~~~~~ 52 | 53 | This Code of Conduct applies both within project spaces and in public spaces 54 | when an individual is representing the project or its community. Examples of 55 | representing a project or community include using an official project e-mail 56 | address, posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. Representation of a project may be 58 | further defined and clarified by project maintainers. 59 | 60 | Enforcement 61 | ~~~~~~~~~~~ 62 | 63 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 64 | reported by contacting the project team at snakecharmers@ethereum.org. All 65 | complaints will be reviewed and investigated and will result in a response that 66 | is deemed necessary and appropriate to the circumstances. The project team is 67 | obligated to maintain confidentiality with regard to the reporter of an incident. 68 | Further details of specific enforcement policies may be posted separately. 69 | 70 | Project maintainers who do not follow or enforce the Code of Conduct in good 71 | faith may face temporary or permanent repercussions as determined by other 72 | members of the project's leadership. 73 | 74 | Attribution 75 | ~~~~~~~~~~~ 76 | 77 | This Code of Conduct is adapted from the `Contributor Covenant `_, version 1.4, 78 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 79 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # pyrlp documentation build configuration file, created by 2 | # sphinx-quickstart on Thu Oct 16 20:43:24 2014. 3 | # 4 | # This file is execfile()d with the current directory set to its 5 | # containing dir. 6 | # 7 | # Note that not all possible configuration values are present in this 8 | # autogenerated file. 9 | # 10 | # All configuration values have a default; values that are commented out 11 | # serve to show the default. 12 | 13 | # If extensions (or modules to document with autodoc) are in another directory, 14 | # add these directories to sys.path here. If the directory is relative to the 15 | # documentation root, use os.path.abspath to make it absolute, like shown here. 16 | # sys.path.insert(0, os.path.abspath('.')) 17 | 18 | import os 19 | 20 | DIR = os.path.dirname(__file__) 21 | with open(os.path.join(DIR, "../setup.py"), "r") as f: 22 | for line in f: 23 | if "version=" in line: 24 | setup_version = line.split('"')[1] 25 | break 26 | 27 | # -- General configuration ------------------------------------------------ 28 | 29 | # If your documentation needs a minimal Sphinx version, state it here. 30 | # needs_sphinx = '1.0' 31 | 32 | # Add any Sphinx extension module names here, as strings. They can be 33 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 34 | # ones. 35 | extensions = [ 36 | "sphinx.ext.autodoc", 37 | "sphinx.ext.doctest", 38 | "sphinx.ext.intersphinx", 39 | "sphinx_rtd_theme", 40 | ] 41 | 42 | # Add any paths that contain templates here, relative to this directory. 43 | templates_path = ["_templates"] 44 | 45 | # The suffix of source filenames. 46 | source_suffix = ".rst" 47 | 48 | # The encoding of source files. 49 | # source_encoding = 'utf-8-sig' 50 | 51 | # The master toctree document. 52 | master_doc = "index" 53 | 54 | # General information about the project. 55 | project = "pyrlp" 56 | copyright = u'2015, jnnk' 57 | 58 | __version__ = setup_version 59 | # The version info for the project you're documenting, acts as replacement for 60 | # |version| and |release|, also used in various other places throughout the 61 | # built documents. 62 | # 63 | # The short X.Y version. 64 | version = ".".join(__version__.split(".")[:2]) 65 | # The full version, including alpha/beta/rc tags. 66 | release = __version__ 67 | 68 | # The language for content autogenerated by Sphinx. Refer to documentation 69 | # for a list of supported languages. 70 | # language = None 71 | 72 | # There are two options for replacing |today|: either, you set today to some 73 | # non-false value, then it is used: 74 | # today = '' 75 | # Else, today_fmt is used as the format for a strftime call. 76 | # today_fmt = '%B %d, %Y' 77 | 78 | # List of patterns, relative to source directory, that match files and 79 | # directories to ignore when looking for source files. 80 | exclude_patterns = [ 81 | "_build", 82 | "modules.rst", 83 | ] 84 | 85 | # The reST default role (used for this markup: `text`) to use for all 86 | # documents. 87 | # default_role = None 88 | 89 | # If true, '()' will be appended to :func: etc. cross-reference text. 90 | # add_function_parentheses = True 91 | 92 | # If true, the current module name will be prepended to all description 93 | # unit titles (such as .. function::). 94 | # add_module_names = True 95 | 96 | # If true, sectionauthor and moduleauthor directives will be shown in the 97 | # output. They are ignored by default. 98 | # show_authors = False 99 | 100 | # The name of the Pygments (syntax highlighting) style to use. 101 | pygments_style = "sphinx" 102 | 103 | # A list of ignored prefixes for module index sorting. 104 | # modindex_common_prefix = [] 105 | 106 | # If true, keep warnings as "system message" paragraphs in the built documents. 107 | # keep_warnings = False 108 | 109 | 110 | # -- Options for HTML output ---------------------------------------------- 111 | 112 | # The theme to use for HTML and HTML Help pages. See the documentation for 113 | # a list of builtin themes. 114 | html_theme = "sphinx_rtd_theme" 115 | 116 | # Theme options are theme-specific and customize the look and feel of a theme 117 | # further. For a list of options available for each theme, see the 118 | # documentation. 119 | # html_theme_options = {} 120 | 121 | # Add any paths that contain custom themes here, relative to this directory. 122 | 123 | # The name for this set of Sphinx documents. If None, it defaults to 124 | # " v documentation". 125 | # html_title = None 126 | 127 | # A shorter title for the navigation bar. Default is the same as html_title. 128 | # html_short_title = None 129 | 130 | # The name of an image file (relative to this directory) to place at the top 131 | # of the sidebar. 132 | # html_logo = None 133 | 134 | # The name of an image file (within the static path) to use as favicon of the 135 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 136 | # pixels large. 137 | # html_favicon = None 138 | 139 | # Add any paths that contain custom static files (such as style sheets) here, 140 | # relative to this directory. They are copied after the builtin static files, 141 | # so a file named "default.css" will overwrite the builtin "default.css". 142 | # html_static_path = ["_static"] 143 | 144 | # Add any extra paths that contain custom files (such as robots.txt or 145 | # .htaccess) here, relative to this directory. These files are copied 146 | # directly to the root of the documentation. 147 | # html_extra_path = [] 148 | 149 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 150 | # using the given strftime format. 151 | # html_last_updated_fmt = '%b %d, %Y' 152 | 153 | # If true, SmartyPants will be used to convert quotes and dashes to 154 | # typographically correct entities. 155 | # html_use_smartypants = True 156 | 157 | # Custom sidebar templates, maps document names to template names. 158 | # html_sidebars = {} 159 | 160 | # Additional templates that should be rendered to pages, maps page names to 161 | # template names. 162 | # html_additional_pages = {} 163 | 164 | # If false, no module index is generated. 165 | # html_domain_indices = True 166 | 167 | # If false, no index is generated. 168 | # html_use_index = True 169 | 170 | # If true, the index is split into individual pages for each letter. 171 | # html_split_index = False 172 | 173 | # If true, links to the reST sources are added to the pages. 174 | # html_show_sourcelink = True 175 | 176 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 177 | # html_show_sphinx = True 178 | 179 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 180 | # html_show_copyright = True 181 | 182 | # If true, an OpenSearch description file will be output, and all pages will 183 | # contain a tag referring to it. The value of this option must be the 184 | # base URL from which the finished HTML is served. 185 | # html_use_opensearch = '' 186 | 187 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 188 | # html_file_suffix = None 189 | 190 | # Output file base name for HTML help builder. 191 | htmlhelp_basename = "rlpdocs" 192 | 193 | 194 | # -- Options for LaTeX output --------------------------------------------- 195 | 196 | latex_engine = "xelatex" 197 | 198 | latex_elements = { 199 | # The paper size ('letterpaper' or 'a4paper'). 200 | #'papersize': 'letterpaper', 201 | # The font size ('10pt', '11pt' or '12pt'). 202 | #'pointsize': '10pt', 203 | # Additional stuff for the LaTeX preamble. 204 | #'preamble': '', 205 | } 206 | 207 | # Grouping the document tree into LaTeX files. List of tuples 208 | # (source start file, target name, title, 209 | # author, documentclass [howto, manual, or own class]). 210 | latex_documents = [ 211 | ( 212 | "index", 213 | "rlp.tex", 214 | "pyrlp Documentation", 215 | "The Ethereum Foundation", 216 | "manual", 217 | ), 218 | ] 219 | 220 | # The name of an image file (relative to this directory) to place at the top of 221 | # the title page. 222 | # latex_logo = None 223 | 224 | # For "manual" documents, if this is true, then toplevel headings are parts, 225 | # not chapters. 226 | # latex_use_parts = False 227 | 228 | # If true, show page references after internal links. 229 | # latex_show_pagerefs = False 230 | 231 | # If true, show URL addresses after external links. 232 | # latex_show_urls = False 233 | 234 | # Documents to append as an appendix to all manuals. 235 | # latex_appendices = [] 236 | 237 | # If false, no module index is generated. 238 | # latex_domain_indices = True 239 | 240 | 241 | # -- Options for manual page output --------------------------------------- 242 | 243 | # One entry per manual page. List of tuples 244 | # (source start file, name, description, authors, manual section). 245 | man_pages = [ 246 | ( 247 | "index", 248 | "rlp", 249 | "pyrlp Documentation", 250 | [u"jnnk"], 251 | 1, 252 | ) 253 | ] 254 | 255 | # If true, show URL addresses after external links. 256 | # man_show_urls = False 257 | 258 | 259 | # -- Options for Texinfo output ------------------------------------------- 260 | 261 | # Grouping the document tree into Texinfo files. List of tuples 262 | # (source start file, target name, title, author, 263 | # dir menu entry, description, category) 264 | texinfo_documents = [ 265 | ( 266 | "index", 267 | "pyrlp", 268 | "pyrlp Documentation", 269 | "The Ethereum Foundation", 270 | "pyrlp", 271 | "A package for Recursive Length Prefix encoding and decoding", 272 | "Miscellaneous", 273 | ), 274 | ] 275 | 276 | # Documents to append as an appendix to all manuals. 277 | # texinfo_appendices = [] 278 | 279 | # If false, no module index is generated. 280 | # texinfo_domain_indices = True 281 | 282 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 283 | # texinfo_show_urls = 'footnote' 284 | 285 | # If true, do not generate a @detailmenu in the "Top" node's menu. 286 | # texinfo_no_detailmenu = False 287 | 288 | # -- Intersphinx configuration ------------------------------------------------ 289 | 290 | intersphinx_mapping = { 291 | "python": ("https://docs.python.org/3.10", None), 292 | } 293 | 294 | # -- Doctest configuration ---------------------------------------- 295 | 296 | import doctest 297 | 298 | doctest_default_flags = ( 299 | 0 300 | | doctest.DONT_ACCEPT_TRUE_FOR_1 301 | | doctest.ELLIPSIS 302 | | doctest.IGNORE_EXCEPTION_DETAIL 303 | | doctest.NORMALIZE_WHITESPACE 304 | ) 305 | -------------------------------------------------------------------------------- /docs/contributing.rst: -------------------------------------------------------------------------------- 1 | Contributing 2 | ------------ 3 | 4 | Thank you for your interest in contributing! We welcome all contributions no matter 5 | their size. Please read along to learn how to get started. If you get stuck, feel free 6 | to ask for help in `Ethereum Python Discord server `_. 7 | 8 | Setting the stage 9 | ~~~~~~~~~~~~~~~~~ 10 | 11 | To get started, fork the repository to your own github account, then clone it to your 12 | development machine: 13 | 14 | .. code:: sh 15 | 16 | git clone git@github.com:your-github-username/pyrlp.git 17 | 18 | Next, install the development dependencies. We recommend using a virtual environment, 19 | such as `virtualenv `_. 20 | 21 | .. code:: sh 22 | 23 | cd pyrlp 24 | virtualenv -p python venv 25 | . venv/bin/activate 26 | python -m pip install -e ".[dev]" 27 | pre-commit install 28 | 29 | Running the tests 30 | ~~~~~~~~~~~~~~~~~ 31 | 32 | A great way to explore the code base is to run the tests. 33 | 34 | We can run all tests with: 35 | 36 | .. code:: sh 37 | 38 | pytest tests 39 | 40 | Code Style 41 | ~~~~~~~~~~ 42 | 43 | We use `pre-commit `_ to enforce a consistent code style across 44 | the library. This tool runs automatically with every commit, but you can also run it 45 | manually with: 46 | 47 | .. code:: sh 48 | 49 | make lint 50 | 51 | If you need to make a commit that skips the ``pre-commit`` checks, you can do so with 52 | ``git commit --no-verify``. 53 | 54 | This library uses type hints, which are enforced by the ``mypy`` tool (part of the 55 | ``pre-commit`` checks). All new code is required to land with type hints, with the 56 | exception of code within the ``tests`` directory. 57 | 58 | Documentation 59 | ~~~~~~~~~~~~~ 60 | 61 | Good documentation will lead to quicker adoption and happier users. Please check out our 62 | guide on 63 | `how to create documentation for the Python Ethereum ecosystem `_. 64 | 65 | Pull Requests 66 | ~~~~~~~~~~~~~ 67 | 68 | It's a good idea to make pull requests early on. A pull request represents the start of 69 | a discussion, and doesn't necessarily need to be the final, finished submission. 70 | 71 | GitHub's documentation for working on pull requests is 72 | `available here `_. 73 | 74 | Once you've made a pull request, take a look at the Circle CI build status in the 75 | GitHub interface and make sure all tests are passing. In general pull requests that 76 | do not pass the CI build yet won't get reviewed unless explicitly requested. 77 | 78 | If the pull request introduces changes that should be reflected in the release notes, 79 | please add a newsfragment file as explained 80 | `here `_. 81 | 82 | If possible, the change to the release notes file should be included in the commit that 83 | introduces the feature or bugfix. 84 | 85 | Releasing 86 | ~~~~~~~~~ 87 | 88 | Releases are typically done from the ``main`` branch, except when releasing a beta (in 89 | which case the beta is released from ``main``, and the previous stable branch is 90 | released from said branch). 91 | 92 | Final test before each release 93 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 94 | 95 | Before releasing a new version, build and test the package that will be released: 96 | 97 | .. code:: sh 98 | 99 | git checkout main && git pull 100 | make package-test 101 | 102 | This will build the package and install it in a temporary virtual environment. Follow 103 | the instructions to activate the venv and test whatever you think is important. 104 | 105 | You can also preview the release notes: 106 | 107 | .. code:: sh 108 | 109 | towncrier --draft 110 | 111 | Build the release notes 112 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 113 | 114 | Before bumping the version number, build the release notes. You must include the part of 115 | the version to bump (see below), which changes how the version number will show in the 116 | release notes. 117 | 118 | .. code:: sh 119 | 120 | make notes bump=$$VERSION_PART_TO_BUMP$$ 121 | 122 | If there are any errors, be sure to re-run make notes until it works. 123 | 124 | Push the release to github & pypi 125 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 126 | 127 | After confirming that the release package looks okay, release a new version: 128 | 129 | .. code:: sh 130 | 131 | make release bump=$$VERSION_PART_TO_BUMP$$ 132 | 133 | This command will: 134 | 135 | - Bump the version number as specified in ``.pyproject.toml`` and ``setup.py``. 136 | - Create a git commit and tag for the new version. 137 | - Build the package. 138 | - Push the commit and tag to github. 139 | - Push the new package files to pypi. 140 | 141 | Which version part to bump 142 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 143 | 144 | ``$$VERSION_PART_TO_BUMP$$`` must be one of: ``major``, ``minor``, ``patch``, ``stage``, 145 | or ``devnum``. 146 | 147 | The version format for this repo is ``{major}.{minor}.{patch}`` for stable, and 148 | ``{major}.{minor}.{patch}-{stage}.{devnum}`` for unstable (``stage`` can be alpha or 149 | beta). 150 | 151 | If you are in a beta version, ``make release bump=stage`` will switch to a stable. 152 | 153 | To issue an unstable version when the current version is stable, specify the new version 154 | explicitly, like ``make release bump="--new-version 4.0.0-alpha.1"`` 155 | 156 | You can see what the result of bumping any particular version part would be with 157 | ``bump-my-version show-bump`` 158 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | pyrlp 2 | ============================== 3 | 4 | A package for Recursive Length Prefix encoding and decoding 5 | 6 | Installation 7 | ------------ 8 | 9 | .. code-block:: bash 10 | 11 | python -m pip install pyrlp 12 | 13 | 14 | .. toctree:: 15 | :maxdepth: 1 16 | :caption: General 17 | 18 | quickstart 19 | tutorial 20 | api 21 | rlp 22 | release_notes 23 | 24 | .. toctree:: 25 | :maxdepth: 1 26 | :caption: Community 27 | 28 | contributing 29 | code_of_conduct 30 | -------------------------------------------------------------------------------- /docs/quickstart.rst: -------------------------------------------------------------------------------- 1 | Quickstart 2 | ========== 3 | 4 | :: 5 | 6 | >>> import rlp 7 | >>> from rlp.sedes import big_endian_int, text, List 8 | 9 | :: 10 | 11 | >>> rlp.encode(1234) 12 | b'\x82\x04\xd2' 13 | >>> rlp.decode(b'\x82\x04\xd2', big_endian_int) 14 | 1234 15 | 16 | :: 17 | 18 | >>> rlp.encode([1, [2, []]]) 19 | b'\xc4\x01\xc2\x02\xc0' 20 | >>> list_sedes = List([big_endian_int, [big_endian_int, []]]) 21 | >>> rlp.decode(b'\xc4\x01\xc2\x02\xc0', list_sedes) 22 | (1, (2, ())) 23 | 24 | :: 25 | 26 | >>> class Tx(rlp.Serializable): 27 | ... fields = [ 28 | ... ('from', text), 29 | ... ('to', text), 30 | ... ('amount', big_endian_int) 31 | ... ] 32 | ... 33 | >>> tx = Tx('me', 'you', 255) 34 | >>> rlp.encode(tx) 35 | b'\xc9\x82me\x83you\x81\xff' 36 | >>> rlp.decode(b'\xc9\x82me\x83you\x81\xff', Tx) == tx 37 | True 38 | -------------------------------------------------------------------------------- /docs/release_notes.rst: -------------------------------------------------------------------------------- 1 | Release Notes 2 | ============= 3 | 4 | .. towncrier release notes start 5 | 6 | pyrlp v4.1.0 (2025-02-04) 7 | ------------------------- 8 | 9 | Features 10 | ~~~~~~~~ 11 | 12 | - Merge template, adding ``py313`` support and replacing ``bumpversion`` with ``bump-my-version``. 13 | ``rust-backend`` still only supported up to ``py312``. (`#156 `__) 14 | 15 | 16 | pyrlp v4.0.1 (2024-04-24) 17 | ------------------------- 18 | 19 | Internal Changes - for pyrlp Contributors 20 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 21 | 22 | - Add python 3.12 support, ``rust-backend`` now works with python 3.11 and 3.12 (`#150 `__) 23 | 24 | 25 | Miscellaneous Changes 26 | ~~~~~~~~~~~~~~~~~~~~~ 27 | 28 | - `#151 `__ 29 | 30 | 31 | pyrlp v4.0.0 (2023-11-29) 32 | ------------------------- 33 | 34 | Features 35 | ~~~~~~~~ 36 | 37 | - ``repr()`` now returns an evaluatable string, like ``MyRLPObj(my_int_field=1, my_str_field="a_str")`` (`#117 `__) 38 | 39 | 40 | Internal Changes - for pyrlp Contributors 41 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 42 | 43 | - Convert ``.format`` strings to ``f-strings`` (`#144 `__) 44 | - Add ``autoflake`` linting and move config to ``pyproject.toml`` (`#145 `__) 45 | 46 | 47 | 0.4.8 48 | ----- 49 | 50 | - Implement ``Serializable.make_mutable`` and ``rlp.sedes.make_mutable`` API. 51 | - Add ``mutable`` flag to ``Serializable.deserialize`` to allow deserialization into mutable objects. 52 | -------------------------------------------------------------------------------- /docs/rlp.rst: -------------------------------------------------------------------------------- 1 | rlp package 2 | =========== 3 | 4 | Subpackages 5 | ----------- 6 | 7 | .. toctree:: 8 | :maxdepth: 4 9 | 10 | rlp.sedes 11 | 12 | Submodules 13 | ---------- 14 | 15 | rlp.atomic module 16 | ----------------- 17 | 18 | .. automodule:: rlp.atomic 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | rlp.codec module 24 | ---------------- 25 | 26 | .. automodule:: rlp.codec 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | 31 | rlp.exceptions module 32 | --------------------- 33 | 34 | .. automodule:: rlp.exceptions 35 | :members: 36 | :undoc-members: 37 | :show-inheritance: 38 | 39 | rlp.lazy module 40 | --------------- 41 | 42 | .. automodule:: rlp.lazy 43 | :members: 44 | :undoc-members: 45 | :show-inheritance: 46 | 47 | rlp.utils module 48 | ---------------- 49 | 50 | .. automodule:: rlp.utils 51 | :members: 52 | :undoc-members: 53 | :show-inheritance: 54 | 55 | Module contents 56 | --------------- 57 | 58 | .. automodule:: rlp 59 | :members: 60 | :undoc-members: 61 | :show-inheritance: 62 | -------------------------------------------------------------------------------- /docs/rlp.sedes.rst: -------------------------------------------------------------------------------- 1 | rlp.sedes package 2 | ================= 3 | 4 | Submodules 5 | ---------- 6 | 7 | rlp.sedes.big\_endian\_int module 8 | --------------------------------- 9 | 10 | .. automodule:: rlp.sedes.big_endian_int 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | :noindex: 15 | 16 | rlp.sedes.binary module 17 | ----------------------- 18 | 19 | .. automodule:: rlp.sedes.binary 20 | :members: 21 | :undoc-members: 22 | :show-inheritance: 23 | :noindex: 24 | 25 | rlp.sedes.boolean module 26 | ------------------------ 27 | 28 | .. automodule:: rlp.sedes.boolean 29 | :members: 30 | :undoc-members: 31 | :show-inheritance: 32 | :noindex: 33 | 34 | rlp.sedes.lists module 35 | ---------------------- 36 | 37 | .. automodule:: rlp.sedes.lists 38 | :members: 39 | :undoc-members: 40 | :show-inheritance: 41 | 42 | rlp.sedes.raw module 43 | -------------------- 44 | 45 | .. automodule:: rlp.sedes.raw 46 | :members: 47 | :undoc-members: 48 | :show-inheritance: 49 | :noindex: 50 | 51 | rlp.sedes.serializable module 52 | ----------------------------- 53 | 54 | .. automodule:: rlp.sedes.serializable 55 | :members: 56 | :undoc-members: 57 | :show-inheritance: 58 | 59 | rlp.sedes.text module 60 | --------------------- 61 | 62 | .. automodule:: rlp.sedes.text 63 | :members: 64 | :undoc-members: 65 | :show-inheritance: 66 | :noindex: 67 | 68 | Module contents 69 | --------------- 70 | 71 | .. automodule:: rlp.sedes 72 | :members: 73 | :undoc-members: 74 | :show-inheritance: 75 | -------------------------------------------------------------------------------- /docs/tutorial.rst: -------------------------------------------------------------------------------- 1 | Tutorial 2 | ======== 3 | 4 | Basics 5 | ------ 6 | 7 | There are two types of fundamental items one can encode in RLP: 8 | 9 | 1) Strings of bytes 10 | 2) Lists of other items 11 | 12 | In this package, byte strings are represented either as Python strings or as 13 | ``bytearrays``. Lists can be any sequence, e.g. ``lists`` or ``tuples``. To 14 | encode these kinds of objects, use :func:`rlp.encode`:: 15 | 16 | >>> from rlp import encode 17 | >>> encode('ethereum') 18 | b'\x88ethereum' 19 | >>> encode('') 20 | b'\x80' 21 | >>> encode('Lorem ipsum dolor sit amet, consetetur sadipscing elitr.') 22 | b'\xb88Lorem ipsum dolor sit amet, consetetur sadipscing elitr.' 23 | >>> encode([]) 24 | b'\xc0' 25 | >>> encode(['this', ['is', ('a', ('nested', 'list', []))]]) 26 | b'\xd9\x84this\xd3\x82is\xcfa\xcd\x86nested\x84list\xc0' 27 | 28 | 29 | Decoding is just as simple:: 30 | 31 | >>> from rlp import decode 32 | >>> decode(b'\x88ethereum') 33 | b'ethereum' 34 | >>> decode(b'\x80') 35 | b'' 36 | >>> decode(b'\xc0') 37 | [] 38 | >>> decode(b'\xd9\x84this\xd3\x82is\xcfa\xcd\x86nested\x84list\xc0') 39 | [b'this', [b'is', [b'a', [b'nested', b'list', []]]]] 40 | 41 | 42 | Now, what if we want to encode a different object, say, an integer? Let's try:: 43 | 44 | >>> encode(1503) 45 | b'\x82\x05\xdf' 46 | >>> decode(b'\x82\x05\xdf') 47 | b'\x05\xdf' 48 | 49 | 50 | Oops, what happened? Encoding worked fine, but :func:`rlp.decode` refused to 51 | give an integer back. The reason is that RLP is typeless. It doesn't know if the 52 | encoded data represents a number, a string, or a more complicated object. It 53 | only distinguishes between byte strings and lists. Therefore, *pyrlp* guesses 54 | how to serialize the object into a byte string (here, in big endian notation). 55 | When encoded however, the type information is lost and :func:`rlp.decode` 56 | returned the result in its most generic form, as a string. Thus, what we need 57 | to do is deserialize the result afterwards. 58 | 59 | 60 | Sedes objects 61 | ------------- 62 | 63 | Serialization and its couterpart, deserialization, is done by, what we call, 64 | *sedes objects* (borrowing from the word "codec"). For integers, the sedes 65 | :mod:`rlp.sedes.big_endian_int` is in charge. To decode our integer, we can 66 | pass this sedes to :func:`rlp.decode`:: 67 | 68 | >>> from rlp.sedes import big_endian_int 69 | >>> decode(b'\x82\x05\xdf', big_endian_int) 70 | 1503 71 | 72 | 73 | For unicode strings, there's the sedes :mod:`rlp.sedes.binary`, which uses UTF-8 74 | to convert to and from byte strings:: 75 | 76 | >>> from rlp.sedes import binary 77 | >>> encode(u'Ðapp') 78 | b'\x85\xc3\x90app' 79 | >>> decode(b'\x85\xc3\x90app', binary) 80 | b'\xc3\x90app' 81 | >>> print(decode(b'\x85\xc3\x90app', binary).decode('utf-8')) 82 | Ðapp 83 | 84 | 85 | Lists are a bit more difficult as they can contain arbitrarily complex 86 | combinations of types. Therefore, we need to create a sedes object specific for 87 | each list type. As base class for this we can use 88 | :class:`rlp.sedes.List`:: 89 | 90 | >>> from rlp.sedes import List 91 | >>> encode([5, 'fdsa', 0]) 92 | b'\xc7\x05\x84fdsa\x80' 93 | >>> sedes = List([big_endian_int, binary, big_endian_int]) 94 | >>> decode(b'\xc7\x05\x84fdsa\x80', sedes) 95 | (5, b'fdsa', 0) 96 | 97 | 98 | Unsurprisingly, it is also possible to nest :class:`rlp.List` objects:: 99 | 100 | >>> inner = List([binary, binary]) 101 | >>> outer = List([inner, inner, inner]) 102 | >>> decode(encode(['asdf', 'fdsa']), inner) 103 | (b'asdf', b'fdsa') 104 | >>> decode(encode([['a1', 'a2'], ['b1', 'b2'], ['c1', 'c2']]), outer) 105 | ((b'a1', b'a2'), (b'b1', b'b2'), (b'c1', b'c2')) 106 | 107 | 108 | What Sedes Objects Actually Are 109 | ------------------------------- 110 | 111 | We saw how to use sedes objects, but what exactly are they? They are 112 | characterized by providing the following three member functions: 113 | 114 | - ``serializable(obj)`` 115 | - ``serialize(obj)`` 116 | - ``deserialize(serial)`` 117 | 118 | The latter two are used to convert between a Python object and its 119 | representation as byte strings or sequences. The former one may be called by 120 | :func:`rlp.encode` to infer which sedes object to use for a given object (see 121 | :ref:`inference-section`). 122 | 123 | For basic types, the sedes object is usually a module (e.g. 124 | :mod:`rlp.sedes.big_endian_int` and :mod:`rlp.sedes.binary`). Instances of 125 | :class:`rlp.sedes.List` provide the sedes interface too, as well as the 126 | class :class:`rlp.Serializable` which is discussed in the following section. 127 | 128 | 129 | Encoding Custom Objects 130 | ----------------------- 131 | 132 | Often, we want to encode our own objects in RLP. Examples from the Ethereum 133 | world are transactions, blocks or anything send over the Wire. With *pyrlp*, 134 | this is as easy as subclassing :class:`rlp.Serializable`:: 135 | 136 | >>> import rlp 137 | >>> class Transaction(rlp.Serializable): 138 | ... fields = ( 139 | ... ('sender', binary), 140 | ... ('receiver', binary), 141 | ... ('amount', big_endian_int) 142 | ... ) 143 | 144 | 145 | The class attribute :attr:`~rlp.Serializable.fields` is a sequence of 2-tuples 146 | defining the field names and the corresponding sedes. For each name an instance 147 | attribute is created, that can conveniently be initialized with 148 | :meth:`~rlp.Serializable.__init__`:: 149 | 150 | >>> tx1 = Transaction(b'me', b'you', 255) 151 | >>> tx2 = Transaction(amount=255, sender=b'you', receiver=b'me') 152 | >>> tx1.amount 153 | 255 154 | 155 | 156 | At serialization, the field names are dropped and the object is converted to a 157 | list, where the provided sedes objects are used to serialize the object 158 | attributes:: 159 | 160 | >>> Transaction.serialize(tx1) 161 | [b'me', b'you', b'\xff'] 162 | >>> tx1 == Transaction.deserialize([b'me', b'you', b'\xff']) 163 | True 164 | 165 | 166 | As we can see, each subclass of :class:`rlp.Serializable` implements the sedes 167 | responsible for its instances. Therefore, we can use :func:`rlp.encode` and 168 | :func:`rlp.decode` as expected:: 169 | 170 | >>> encode(tx1) 171 | b'\xc9\x82me\x83you\x81\xff' 172 | >>> decode(b'\xc9\x82me\x83you\x81\xff', Transaction) == tx1 173 | True 174 | 175 | 176 | .. _inference-section: 177 | 178 | Sedes Inference 179 | --------------- 180 | 181 | As we have seen, :func:`rlp.encode` (or, rather, :func:`rlp.infer_sedes`) 182 | tries to guess a sedes capable of serializing the object before encoding. In 183 | this process, it follows the following steps: 184 | 185 | 1) Check if the object's class is a sedes object (like every subclass of 186 | :class:`rlp.Serializable`). If so, its class is the sedes. 187 | 2) Check if one of the entries in :attr:`rlp.sedes.sedes_list` can serialize 188 | the object (via ``serializable(obj)``). If so, this is the sedes. 189 | 3) Check if the object is a sequence. If so, build a 190 | :class:`rlp.sedes.List` by recursively infering a sedes for each of its 191 | elements. 192 | 4) If none of these steps was successful, sedes inference has failed. 193 | 194 | If you have build your own basic sedes (e.g. for ``dicts`` or ``floats``), you 195 | might want to hook in at step 2 and add it to :attr:`rlp.sedes.sedes_list`, 196 | whereby it will be automatically be used by :func:`rlp.encode`. 197 | 198 | 199 | Further Reading 200 | --------------- 201 | 202 | This was basically everything there is to about this package. The technical 203 | specification of RLP can be found either in the 204 | `Ethereum wiki `_ or in Appendix B of 205 | Gavin Woods `Yellow Paper `_. For more detailed 206 | information about this package, have a look at the :ref:`API-reference` or the 207 | source code. 208 | -------------------------------------------------------------------------------- /newsfragments/README.md: -------------------------------------------------------------------------------- 1 | This directory collects "newsfragments": short files that each contain 2 | a snippet of ReST-formatted text that will be added to the next 3 | release notes. This should be a description of aspects of the change 4 | (if any) that are relevant to users. (This contrasts with the 5 | commit message and PR description, which are a description of the change as 6 | relevant to people working on the code itself.) 7 | 8 | Each file should be named like `..rst`, where 9 | `` is an issue number, and `` is one of: 10 | 11 | - `breaking` 12 | - `bugfix` 13 | - `deprecation` 14 | - `docs` 15 | - `feature` 16 | - `internal` 17 | - `misc` 18 | - `performance` 19 | - `removal` 20 | 21 | So for example: `123.feature.rst`, `456.bugfix.rst` 22 | 23 | If the PR fixes an issue, use that number here. If there is no issue, 24 | then open up the PR first and use the PR number for the newsfragment. 25 | 26 | Note that the `towncrier` tool will automatically 27 | reflow your text, so don't try to do any fancy formatting. Run 28 | `towncrier build --draft` to get a preview of what the release notes entry 29 | will look like in the final release notes. 30 | -------------------------------------------------------------------------------- /newsfragments/validate_files.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Towncrier silently ignores files that do not match the expected ending. 4 | # We use this script to ensure we catch these as errors in CI. 5 | 6 | import pathlib 7 | import sys 8 | 9 | ALLOWED_EXTENSIONS = { 10 | ".breaking.rst", 11 | ".bugfix.rst", 12 | ".deprecation.rst", 13 | ".docs.rst", 14 | ".feature.rst", 15 | ".internal.rst", 16 | ".misc.rst", 17 | ".performance.rst", 18 | ".removal.rst", 19 | } 20 | 21 | ALLOWED_FILES = { 22 | "validate_files.py", 23 | "README.md", 24 | } 25 | 26 | THIS_DIR = pathlib.Path(__file__).parent 27 | 28 | num_args = len(sys.argv) - 1 29 | assert num_args in {0, 1} 30 | if num_args == 1: 31 | assert sys.argv[1] in ("is-empty",) 32 | 33 | for fragment_file in THIS_DIR.iterdir(): 34 | if fragment_file.name in ALLOWED_FILES: 35 | continue 36 | elif num_args == 0: 37 | full_extension = "".join(fragment_file.suffixes) 38 | if full_extension not in ALLOWED_EXTENSIONS: 39 | raise Exception(f"Unexpected file: {fragment_file}") 40 | elif sys.argv[1] == "is-empty": 41 | raise Exception(f"Unexpected file: {fragment_file}") 42 | else: 43 | raise RuntimeError( 44 | f"Strange: arguments {sys.argv} were validated, but not found" 45 | ) 46 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.autoflake] 2 | exclude = "__init__.py" 3 | remove_all_unused_imports = true 4 | 5 | [tool.isort] 6 | combine_as_imports = true 7 | extra_standard_library = "pytest" 8 | force_grid_wrap = 1 9 | force_sort_within_sections = true 10 | force_to_top = "pytest" 11 | honor_noqa = true 12 | known_first_party = "rlp" 13 | known_third_party = "hypothesis" 14 | multi_line_output = 3 15 | profile = "black" 16 | use_parentheses = true 17 | 18 | [tool.mypy] 19 | check_untyped_defs = true 20 | disallow_any_generics = true 21 | disallow_incomplete_defs = true 22 | disallow_subclassing_any = true 23 | disallow_untyped_calls = true 24 | disallow_untyped_decorators = true 25 | disallow_untyped_defs = true 26 | ignore_missing_imports = true 27 | strict_equality = true 28 | strict_optional = true 29 | warn_redundant_casts = true 30 | warn_return_any = true 31 | warn_unused_configs = true 32 | warn_unused_ignores = true 33 | 34 | 35 | [tool.pydocstyle] 36 | # All error codes found here: 37 | # http://www.pydocstyle.org/en/3.0.0/error_codes.html 38 | # 39 | # Ignored: 40 | # D1 - Missing docstring error codes 41 | # 42 | # Selected: 43 | # D2 - Whitespace error codes 44 | # D3 - Quote error codes 45 | # D4 - Content related error codes 46 | select = "D2,D3,D4" 47 | 48 | # Extra ignores: 49 | # D200 - One-line docstring should fit on one line with quotes 50 | # D203 - 1 blank line required before class docstring 51 | # D204 - 1 blank line required after class docstring 52 | # D205 - 1 blank line required between summary line and description 53 | # D212 - Multi-line docstring summary should start at the first line 54 | # D302 - Use u""" for Unicode docstrings 55 | # D400 - First line should end with a period 56 | # D401 - First line should be in imperative mood 57 | # D412 - No blank lines allowed between a section header and its content 58 | # D415 - First line should end with a period, question mark, or exclamation point 59 | add-ignore = "D200,D203,D204,D205,D212,D302,D400,D401,D412,D415" 60 | 61 | # Explanation: 62 | # D400 - Enabling this error code seems to make it a requirement that the first 63 | # sentence in a docstring is not split across two lines. It also makes it a 64 | # requirement that no docstring can have a multi-sentence description without a 65 | # summary line. Neither one of those requirements seem appropriate. 66 | 67 | [tool.pytest.ini_options] 68 | addopts = "-v --showlocals --durations 10" 69 | log_date_format = "%m-%d %H:%M:%S" 70 | log_format = "%(levelname)8s %(asctime)s %(filename)20s %(message)s" 71 | markers = "benchmark" 72 | xfail_strict = true 73 | 74 | [tool.towncrier] 75 | # Read https://github.com/ethereum/pyrlp/blob/main/newsfragments/README.md for instructions 76 | directory = "newsfragments" 77 | filename = "docs/release_notes.rst" 78 | issue_format = "`#{issue} `__" 79 | package = "rlp" 80 | title_format = "pyrlp v{version} ({project_date})" 81 | underlines = ["-", "~", "^"] 82 | 83 | [[tool.towncrier.type]] 84 | directory = "breaking" 85 | name = "Breaking Changes" 86 | showcontent = true 87 | 88 | [[tool.towncrier.type]] 89 | directory = "bugfix" 90 | name = "Bugfixes" 91 | showcontent = true 92 | 93 | [[tool.towncrier.type]] 94 | directory = "deprecation" 95 | name = "Deprecations" 96 | showcontent = true 97 | 98 | [[tool.towncrier.type]] 99 | directory = "docs" 100 | name = "Improved Documentation" 101 | showcontent = true 102 | 103 | [[tool.towncrier.type]] 104 | directory = "feature" 105 | name = "Features" 106 | showcontent = true 107 | 108 | [[tool.towncrier.type]] 109 | directory = "internal" 110 | name = "Internal Changes - for pyrlp Contributors" 111 | showcontent = true 112 | 113 | [[tool.towncrier.type]] 114 | directory = "misc" 115 | name = "Miscellaneous Changes" 116 | showcontent = false 117 | 118 | [[tool.towncrier.type]] 119 | directory = "performance" 120 | name = "Performance Improvements" 121 | showcontent = true 122 | 123 | [[tool.towncrier.type]] 124 | directory = "removal" 125 | name = "Removals" 126 | showcontent = true 127 | 128 | [tool.bumpversion] 129 | current_version = "4.1.0" 130 | parse = """ 131 | (?P\\d+) 132 | \\.(?P\\d+) 133 | \\.(?P\\d+) 134 | (- 135 | (?P[^.]*) 136 | \\.(?P\\d+) 137 | )? 138 | """ 139 | serialize = [ 140 | "{major}.{minor}.{patch}-{stage}.{devnum}", 141 | "{major}.{minor}.{patch}", 142 | ] 143 | search = "{current_version}" 144 | replace = "{new_version}" 145 | regex = false 146 | ignore_missing_version = false 147 | tag = true 148 | sign_tags = true 149 | tag_name = "v{new_version}" 150 | tag_message = "Bump version: {current_version} → {new_version}" 151 | allow_dirty = false 152 | commit = true 153 | message = "Bump version: {current_version} → {new_version}" 154 | 155 | [tool.bumpversion.parts.stage] 156 | optional_value = "stable" 157 | first_value = "stable" 158 | values = [ 159 | "alpha", 160 | "beta", 161 | "stable", 162 | ] 163 | 164 | [tool.bumpversion.part.devnum] 165 | 166 | [[tool.bumpversion.files]] 167 | filename = "setup.py" 168 | search = "version=\"{current_version}\"" 169 | replace = "version=\"{new_version}\"" 170 | -------------------------------------------------------------------------------- /rlp/__init__.py: -------------------------------------------------------------------------------- 1 | from importlib.metadata import ( 2 | version as __version, 3 | ) 4 | 5 | from . import ( 6 | sedes, 7 | ) 8 | from .codec import ( 9 | decode, 10 | encode, 11 | infer_sedes, 12 | ) 13 | from .exceptions import ( 14 | DecodingError, 15 | DeserializationError, 16 | EncodingError, 17 | RLPException, 18 | SerializationError, 19 | ) 20 | from .lazy import ( 21 | LazyList, 22 | decode_lazy, 23 | peek, 24 | ) 25 | from .sedes import ( 26 | Serializable, 27 | ) 28 | 29 | __version__ = __version("rlp") 30 | -------------------------------------------------------------------------------- /rlp/atomic.py: -------------------------------------------------------------------------------- 1 | import abc 2 | 3 | 4 | class Atomic(metaclass=abc.ABCMeta): 5 | """ABC for objects that can be RLP encoded as is.""" 6 | 7 | 8 | Atomic.register(bytes) 9 | Atomic.register(bytearray) 10 | -------------------------------------------------------------------------------- /rlp/codec.py: -------------------------------------------------------------------------------- 1 | import collections 2 | 3 | from eth_utils import ( 4 | big_endian_to_int, 5 | int_to_big_endian, 6 | is_bytes, 7 | ) 8 | 9 | from rlp.exceptions import ( 10 | DecodingError, 11 | EncodingError, 12 | ) 13 | from rlp.sedes import ( 14 | big_endian_int, 15 | binary, 16 | boolean, 17 | text, 18 | ) 19 | from rlp.sedes.binary import ( 20 | Binary as BinaryClass, 21 | ) 22 | from rlp.sedes.lists import ( 23 | List, 24 | is_sedes, 25 | is_sequence, 26 | ) 27 | from rlp.sedes.serializable import ( 28 | Serializable, 29 | ) 30 | from rlp.utils import ( 31 | ALL_BYTES, 32 | ) 33 | 34 | try: 35 | import rusty_rlp 36 | except ImportError: 37 | import logging 38 | 39 | from rlp.atomic import ( 40 | Atomic, 41 | ) 42 | 43 | logger = logging.getLogger("rlp.codec") 44 | logger.debug( 45 | "Consider installing rusty-rlp to improve pyrlp performance with a rust based" 46 | "backend. Not currently functional for Python 3.11" 47 | ) 48 | 49 | def encode_raw(item): 50 | r"""RLP encode (a nested sequence of) :class:`Atomic`\s.""" 51 | if isinstance(item, Atomic): 52 | if len(item) == 1 and item[0] < 128: 53 | return item 54 | payload = item 55 | prefix_offset = 128 # string 56 | elif not isinstance(item, str) and isinstance(item, collections.abc.Sequence): 57 | payload = b"".join(encode_raw(x) for x in item) 58 | prefix_offset = 192 # list 59 | else: 60 | msg = f"Cannot encode object of type {type(item).__name__}" 61 | raise EncodingError(msg, item) 62 | 63 | try: 64 | prefix = length_prefix(len(payload), prefix_offset) 65 | except ValueError: 66 | raise EncodingError("Item too big to encode", item) 67 | 68 | return prefix + payload 69 | 70 | def decode_raw(item, strict, _): 71 | try: 72 | result, per_item_rlp, end = consume_item(item, 0) 73 | except IndexError: 74 | raise DecodingError("RLP string too short", item) 75 | if end != len(item) and strict: 76 | msg = f"RLP string ends with {len(item) - end} superfluous bytes" 77 | raise DecodingError(msg, item) 78 | 79 | return result, per_item_rlp 80 | 81 | else: 82 | 83 | def decode_raw(item, strict, preserve_per_item_rlp): 84 | try: 85 | return rusty_rlp.decode_raw(item, strict, preserve_per_item_rlp) 86 | except (TypeError, rusty_rlp.DecodingError) as e: 87 | raise DecodingError(e, item) 88 | 89 | def encode_raw(obj): 90 | try: 91 | if isinstance(obj, bytearray): 92 | obj = bytes(obj) 93 | return rusty_rlp.encode_raw(obj) 94 | except rusty_rlp.EncodingError as e: 95 | raise EncodingError(e, obj) 96 | 97 | 98 | def encode(obj, sedes=None, infer_serializer=True, cache=True): 99 | """ 100 | Encode a Python object in RLP format. 101 | 102 | By default, the object is serialized in a suitable way first (using 103 | :func:`rlp.infer_sedes`) and then encoded. Serialization can be explicitly 104 | suppressed by setting `infer_serializer` to ``False`` and not passing an 105 | alternative as `sedes`. 106 | 107 | If `obj` has an attribute :attr:`_cached_rlp` (as, notably, 108 | :class:`rlp.Serializable`) and its value is not `None`, this value is 109 | returned bypassing serialization and encoding, unless `sedes` is given (as 110 | the cache is assumed to refer to the standard serialization which can be 111 | replaced by specifying `sedes`). 112 | 113 | If `obj` is a :class:`rlp.Serializable` and `cache` is true, the result of 114 | the encoding will be stored in :attr:`_cached_rlp` if it is empty. 115 | 116 | :param sedes: an object implementing a function ``serialize(obj)`` which will be 117 | used to serialize ``obj`` before encoding, or ``None`` to use the 118 | infered one (if any) 119 | :param infer_serializer: if ``True`` an appropriate serializer will be selected 120 | using :func:`rlp.infer_sedes` to serialize `obj` before 121 | encoding 122 | :param cache: cache the return value in `obj._cached_rlp` if possible 123 | (default `True`) 124 | :returns: the RLP encoded item 125 | :raises: :exc:`rlp.EncodingError` in the rather unlikely case that the item is too 126 | big to encode (will not happen) 127 | :raises: :exc:`rlp.SerializationError` if the serialization fails 128 | """ 129 | if isinstance(obj, Serializable): 130 | cached_rlp = obj._cached_rlp 131 | if sedes is None and cached_rlp: 132 | return cached_rlp 133 | else: 134 | really_cache = cache and sedes is None 135 | else: 136 | really_cache = False 137 | 138 | if sedes: 139 | item = sedes.serialize(obj) 140 | elif infer_serializer: 141 | item = infer_sedes(obj).serialize(obj) 142 | else: 143 | item = obj 144 | 145 | result = encode_raw(item) 146 | if really_cache: 147 | obj._cached_rlp = result 148 | return result 149 | 150 | 151 | LONG_LENGTH = 256**8 152 | 153 | 154 | def length_prefix(length, offset): 155 | """ 156 | Construct the prefix to lists or strings denoting their length. 157 | 158 | :param length: the length of the item in bytes 159 | :param offset: ``0x80`` when encoding raw bytes, ``0xc0`` when encoding a 160 | list 161 | """ 162 | if length < 56: 163 | return ALL_BYTES[offset + length] 164 | elif length < LONG_LENGTH: 165 | length_string = int_to_big_endian(length) 166 | return ALL_BYTES[offset + 56 - 1 + len(length_string)] + length_string 167 | else: 168 | raise ValueError("Length greater than 256**8") 169 | 170 | 171 | SHORT_STRING = 128 + 56 172 | 173 | 174 | def consume_length_prefix(rlp, start): 175 | """ 176 | Read a length prefix from an RLP string. 177 | 178 | :param rlp: the rlp byte string to read from 179 | :param start: the position at which to start reading 180 | :returns: a tuple ``(prefix, type, length, end)``, where ``type`` is either ``str`` 181 | or ``list`` depending on the type of the following payload, 182 | ``length`` is the length of the payload in bytes, and ``end`` is 183 | the position of the first payload byte in the rlp string 184 | """ 185 | b0 = rlp[start] 186 | if b0 < 128: # single byte 187 | return (b"", bytes, 1, start) 188 | elif b0 < SHORT_STRING: # short string 189 | if b0 - 128 == 1 and rlp[start + 1] < 128: 190 | raise DecodingError( 191 | "Encoded as short string although single byte was possible", rlp 192 | ) 193 | return (rlp[start : start + 1], bytes, b0 - 128, start + 1) 194 | elif b0 < 192: # long string 195 | ll = b0 - 183 # - (128 + 56 - 1) 196 | if rlp[start + 1 : start + 2] == b"\x00": 197 | raise DecodingError("Length starts with zero bytes", rlp) 198 | len_prefix = rlp[start + 1 : start + 1 + ll] 199 | l = big_endian_to_int(len_prefix) # noqa: E741 200 | if l < 56: 201 | raise DecodingError("Long string prefix used for short string", rlp) 202 | return (rlp[start : start + 1] + len_prefix, bytes, l, start + 1 + ll) 203 | elif b0 < 192 + 56: # short list 204 | return (rlp[start : start + 1], list, b0 - 192, start + 1) 205 | else: # long list 206 | ll = b0 - 192 - 56 + 1 207 | if rlp[start + 1 : start + 2] == b"\x00": 208 | raise DecodingError("Length starts with zero bytes", rlp) 209 | len_prefix = rlp[start + 1 : start + 1 + ll] 210 | l = big_endian_to_int(len_prefix) # noqa: E741 211 | if l < 56: 212 | raise DecodingError("Long list prefix used for short list", rlp) 213 | return (rlp[start : start + 1] + len_prefix, list, l, start + 1 + ll) 214 | 215 | 216 | def consume_payload(rlp, prefix, start, type_, length): 217 | """ 218 | Read the payload of an item from an RLP string. 219 | 220 | :param rlp: the rlp string to read from 221 | :param type_: the type of the payload (``bytes`` or ``list``) 222 | :param start: the position at which to start reading 223 | :param length: the length of the payload in bytes 224 | :returns: a tuple ``(item, per_item_rlp, end)``, where ``item`` is 225 | the read item, per_item_rlp is a list containing the RLP 226 | encoding of each item and ``end`` is the position of the 227 | first unprocessed byte 228 | """ 229 | if type_ is bytes: 230 | item = rlp[start : start + length] 231 | return (item, [prefix + item], start + length) 232 | elif type_ is list: 233 | items = [] 234 | per_item_rlp = [] 235 | list_rlp = prefix 236 | next_item_start = start 237 | end = next_item_start + length 238 | while next_item_start < end: 239 | p, t, l, s = consume_length_prefix(rlp, next_item_start) 240 | item, item_rlp, next_item_start = consume_payload(rlp, p, s, t, l) 241 | per_item_rlp.append(item_rlp) 242 | # When the item returned above is a single element, item_rlp will also 243 | # contain a single element, but when it's a list, the first element will be 244 | # the RLP of the whole List, which is what we want here. 245 | list_rlp += item_rlp[0] 246 | items.append(item) 247 | per_item_rlp.insert(0, list_rlp) 248 | if next_item_start > end: 249 | raise DecodingError( 250 | "List length prefix announced a too small " "length", rlp 251 | ) 252 | return (items, per_item_rlp, next_item_start) 253 | else: 254 | raise TypeError("Type must be either list or bytes") 255 | 256 | 257 | def consume_item(rlp, start): 258 | """ 259 | Read an item from an RLP string. 260 | 261 | :param rlp: the rlp string to read from 262 | :param start: the position at which to start reading 263 | :returns: a tuple ``(item, per_item_rlp, end)``, where ``item`` is 264 | the read item, per_item_rlp is a list containing the RLP 265 | encoding of each item and ``end`` is the position of the 266 | first unprocessed byte 267 | """ 268 | p, t, l, s = consume_length_prefix(rlp, start) 269 | return consume_payload(rlp, p, s, t, l) 270 | 271 | 272 | def decode(rlp, sedes=None, strict=True, recursive_cache=False, **kwargs): 273 | """ 274 | Decode an RLP encoded object. 275 | 276 | If the deserialized result `obj` has an attribute :attr:`_cached_rlp` (e.g. if 277 | `sedes` is a subclass of :class:`rlp.Serializable`) it will be set to `rlp`, which 278 | will improve performance on subsequent :func:`rlp.encode` calls. Bear in mind 279 | however that `obj` needs to make sure that this value is updated whenever one of its 280 | fields changes or prevent such changes entirely (:class:`rlp.sedes.Serializable` 281 | does the latter). 282 | 283 | :param sedes: an object implementing a function ``deserialize(code)`` which will be 284 | applied after decoding, or ``None`` if no deserialization should be 285 | performed 286 | :param `**kwargs`: additional keyword arguments that will be passed to the 287 | deserializer 288 | :param strict: if false inputs that are longer than necessary don't cause an 289 | exception 290 | :returns: the decoded and maybe deserialized Python object 291 | :raises: :exc:`rlp.DecodingError` if the input string does not end after the root 292 | item and `strict` is true 293 | :raises: :exc:`rlp.DeserializationError` if the deserialization fails 294 | """ 295 | if not is_bytes(rlp): 296 | raise DecodingError( 297 | "Can only decode RLP bytes, got type %s" % type(rlp).__name__, rlp 298 | ) 299 | 300 | item, per_item_rlp = decode_raw(rlp, strict, recursive_cache) 301 | 302 | if len(per_item_rlp) == 0: 303 | per_item_rlp = [rlp] 304 | 305 | if sedes: 306 | obj = sedes.deserialize(item, **kwargs) 307 | if is_sequence(obj) or hasattr(obj, "_cached_rlp"): 308 | _apply_rlp_cache(obj, per_item_rlp, recursive_cache) 309 | return obj 310 | else: 311 | return item 312 | 313 | 314 | def _apply_rlp_cache(obj, split_rlp, recursive): 315 | item_rlp = split_rlp.pop(0) 316 | if isinstance(obj, (int, bool, str, bytes, bytearray)): 317 | return 318 | elif hasattr(obj, "_cached_rlp"): 319 | obj._cached_rlp = item_rlp 320 | if not recursive: 321 | return 322 | for sub in obj: 323 | if isinstance(sub, (int, bool, str, bytes, bytearray)): 324 | split_rlp.pop(0) 325 | else: 326 | sub_rlp = split_rlp.pop(0) 327 | _apply_rlp_cache(sub, sub_rlp, recursive) 328 | 329 | 330 | def infer_sedes(obj): 331 | """ 332 | Try to find a sedes objects suitable for a given Python object. 333 | 334 | The sedes objects considered are `obj`'s class, `big_endian_int` and 335 | `binary`. If `obj` is a sequence, a :class:`rlp.sedes.List` will be 336 | constructed recursively. 337 | 338 | :param obj: the python object for which to find a sedes object 339 | :raises: :exc:`TypeError` if no appropriate sedes could be found 340 | """ 341 | if is_sedes(obj.__class__): 342 | return obj.__class__ 343 | elif not isinstance(obj, bool) and isinstance(obj, int) and obj >= 0: 344 | return big_endian_int 345 | elif BinaryClass.is_valid_type(obj): 346 | return binary 347 | elif not isinstance(obj, str) and isinstance(obj, collections.abc.Sequence): 348 | return List(map(infer_sedes, obj)) 349 | elif isinstance(obj, bool): 350 | return boolean 351 | elif isinstance(obj, str): 352 | return text 353 | msg = f"Did not find sedes handling type {type(obj).__name__}" 354 | raise TypeError(msg) 355 | -------------------------------------------------------------------------------- /rlp/exceptions.py: -------------------------------------------------------------------------------- 1 | class RLPException(Exception): 2 | """Base class for exceptions raised by this package.""" 3 | 4 | 5 | class EncodingError(RLPException): 6 | """ 7 | Exception raised if encoding fails. 8 | 9 | :ivar obj: the object that could not be encoded 10 | """ 11 | 12 | def __init__(self, message, obj): 13 | super().__init__(message) 14 | self.obj = obj 15 | 16 | 17 | class DecodingError(RLPException): 18 | """ 19 | Exception raised if decoding fails. 20 | 21 | :ivar rlp: the RLP string that could not be decoded 22 | """ 23 | 24 | def __init__(self, message, rlp): 25 | super().__init__(message) 26 | self.rlp = rlp 27 | 28 | 29 | class SerializationError(RLPException): 30 | """ 31 | Exception raised if serialization fails. 32 | 33 | :ivar obj: the object that could not be serialized 34 | """ 35 | 36 | def __init__(self, message, obj): 37 | super().__init__(message) 38 | self.obj = obj 39 | 40 | 41 | class ListSerializationError(SerializationError): 42 | """ 43 | Exception raised if serialization by a :class:`sedes.List` fails. 44 | 45 | :ivar element_exception: the exception that occurred during the serialization of one 46 | of the elements, or `None` if the error is unrelated to a 47 | specific element 48 | :ivar index: the index in the list that produced the error or `None` if the error is 49 | unrelated to a specific element 50 | """ 51 | 52 | def __init__(self, message=None, obj=None, element_exception=None, index=None): 53 | if message is None: 54 | assert index is not None 55 | assert element_exception is not None 56 | message = ( 57 | f"Serialization failed because of element at index {index} " 58 | f'("{str(element_exception)}")' 59 | ) 60 | super().__init__(message, obj) 61 | self.index = index 62 | self.element_exception = element_exception 63 | 64 | 65 | class ObjectSerializationError(SerializationError): 66 | """ 67 | Exception raised if serialization of a :class:`sedes.Serializable` object fails. 68 | 69 | :ivar sedes: the :class:`sedes.Serializable` that failed 70 | :ivar list_exception: exception raised by the underlying list sedes, or `None` if no 71 | such exception has been raised 72 | :ivar field: name of the field of the object that produced the error, or `None` if 73 | no field responsible for the error 74 | """ 75 | 76 | def __init__(self, message=None, obj=None, sedes=None, list_exception=None): 77 | if message is None: 78 | assert list_exception is not None 79 | if list_exception.element_exception is None: 80 | field = None 81 | message = ( 82 | "Serialization failed because of underlying list " 83 | f'("{str(list_exception)}")' 84 | ) 85 | else: 86 | assert sedes is not None 87 | field = sedes._meta.field_names[list_exception.index] 88 | message = ( 89 | f"Serialization failed because of field {field} " 90 | f'("{str(list_exception.element_exception)}")' 91 | ) 92 | else: 93 | field = None 94 | super().__init__(message, obj) 95 | self.field = field 96 | self.list_exception = list_exception 97 | 98 | 99 | class DeserializationError(RLPException): 100 | """ 101 | Exception raised if deserialization fails. 102 | 103 | :ivar serial: the decoded RLP string that could not be deserialized 104 | """ 105 | 106 | def __init__(self, message, serial): 107 | super().__init__(message) 108 | self.serial = serial 109 | 110 | 111 | class ListDeserializationError(DeserializationError): 112 | """ 113 | Exception raised if deserialization by a :class:`sedes.List` fails. 114 | 115 | :ivar element_exception: the exception that occurred during the deserialization of 116 | one of the elements, or `None` if the error is unrelated to 117 | a specific element 118 | :ivar index: the index in the list that produced the error or `None` if the error is 119 | unrelated to a specific element 120 | """ 121 | 122 | def __init__(self, message=None, serial=None, element_exception=None, index=None): 123 | if not message: 124 | assert index is not None 125 | assert element_exception is not None 126 | message = ( 127 | f"Deserialization failed because of element at index {index} " 128 | f'("{str(element_exception)}")' 129 | ) 130 | super().__init__(message, serial) 131 | self.index = index 132 | self.element_exception = element_exception 133 | 134 | 135 | class ObjectDeserializationError(DeserializationError): 136 | """ 137 | Exception raised if deserialization by a :class:`sedes.Serializable` fails. 138 | 139 | :ivar sedes: the :class:`sedes.Serializable` that failed 140 | :ivar list_exception: exception raised by the underlying list sedes, or `None` if no 141 | such exception has been raised 142 | :ivar field: name of the field of the object that produced the error, or `None` if 143 | no field responsible for the error 144 | """ 145 | 146 | def __init__(self, message=None, serial=None, sedes=None, list_exception=None): 147 | if not message: 148 | assert list_exception is not None 149 | if list_exception.element_exception is None: 150 | field = None 151 | message = ( 152 | "Deserialization failed because of underlying list " 153 | f'("{str(list_exception)}")' 154 | ) 155 | else: 156 | assert sedes is not None 157 | field = sedes._meta.field_names[list_exception.index] 158 | message = ( 159 | f"Deserialization failed because of field {field} " 160 | f'("{str(list_exception.element_exception)}")' 161 | ) 162 | super().__init__(message, serial) 163 | self.sedes = sedes 164 | self.list_exception = list_exception 165 | self.field = field 166 | -------------------------------------------------------------------------------- /rlp/lazy.py: -------------------------------------------------------------------------------- 1 | from collections.abc import ( 2 | Iterable, 3 | Sequence, 4 | ) 5 | 6 | from .atomic import ( 7 | Atomic, 8 | ) 9 | from .codec import ( 10 | consume_length_prefix, 11 | consume_payload, 12 | ) 13 | from .exceptions import ( 14 | DecodingError, 15 | ) 16 | 17 | 18 | def decode_lazy(rlp, sedes=None, **sedes_kwargs): 19 | """ 20 | Decode an RLP encoded object in a lazy fashion. 21 | 22 | If the encoded object is a bytestring, this function acts similar to 23 | :func:`rlp.decode`. If it is a list however, a :class:`LazyList` is 24 | returned instead. This object will decode the string lazily, avoiding 25 | both horizontal and vertical traversing as much as possible. 26 | 27 | The way `sedes` is applied depends on the decoded object: If it is a string 28 | `sedes` deserializes it as a whole; if it is a list, each element is 29 | deserialized individually. In both cases, `sedes_kwargs` are passed on. 30 | Note that, if a deserializer is used, only "horizontal" but not 31 | "vertical lazyness" can be preserved. 32 | 33 | :param rlp: the RLP string to decode 34 | :param sedes: an object implementing a method ``deserialize(code)`` which 35 | is used as described above, or ``None`` if no 36 | deserialization should be performed 37 | :param `**sedes_kwargs`: additional keyword arguments that will be passed 38 | to the deserializers 39 | :returns: either the already decoded and deserialized object (if encoded as 40 | a string) or an instance of :class:`rlp.LazyList` 41 | """ 42 | item, end = consume_item_lazy(rlp, 0) 43 | if end != len(rlp): 44 | raise DecodingError("RLP length prefix announced wrong length", rlp) 45 | if isinstance(item, LazyList): 46 | item.sedes = sedes 47 | item.sedes_kwargs = sedes_kwargs 48 | return item 49 | elif sedes: 50 | return sedes.deserialize(item, **sedes_kwargs) 51 | else: 52 | return item 53 | 54 | 55 | def consume_item_lazy(rlp, start): 56 | """ 57 | Read an item from an RLP string lazily. 58 | 59 | If the length prefix announces a string, the string is read; if it 60 | announces a list, a :class:`LazyList` is created. 61 | 62 | :param rlp: the rlp string to read from 63 | :param start: the position at which to start reading 64 | :returns: a tuple ``(item, end)`` where ``item`` is the read string or a 65 | :class:`LazyList` and ``end`` is the position of the first 66 | unprocessed byte. 67 | """ 68 | p, t, l, s = consume_length_prefix(rlp, start) 69 | if t is bytes: 70 | item, _, end = consume_payload(rlp, p, s, bytes, l) 71 | return item, end 72 | else: 73 | assert t is list 74 | return LazyList(rlp, s, s + l), s + l 75 | 76 | 77 | class LazyList(Sequence): 78 | """ 79 | A RLP encoded list which decodes itself when necessary. 80 | 81 | Both indexing with positive indices and iterating are supported. 82 | Getting the length with :func:`len` is possible as well but requires full 83 | horizontal encoding. 84 | 85 | :param rlp: the rlp string in which the list is encoded 86 | :param start: the position of the first payload byte of the encoded list 87 | :param end: the position of the last payload byte of the encoded list 88 | :param sedes: a sedes object which deserializes each element of the list, 89 | or ``None`` for no deserialization 90 | :param `**sedes_kwargs`: keyword arguments which will be passed on to the 91 | deserializer 92 | """ 93 | 94 | def __init__(self, rlp, start, end, sedes=None, **sedes_kwargs): 95 | self.rlp = rlp 96 | self.start = start 97 | self.end = end 98 | self.index = start 99 | self._elements = [] 100 | self._len = None 101 | self.sedes = sedes 102 | self.sedes_kwargs = sedes_kwargs 103 | 104 | def next(self): 105 | if self.index == self.end: 106 | self._len = len(self._elements) 107 | raise StopIteration 108 | assert self.index < self.end 109 | item, end = consume_item_lazy(self.rlp, self.index) 110 | self.index = end 111 | if self.sedes: 112 | item = self.sedes.deserialize(item, **self.sedes_kwargs) 113 | self._elements.append(item) 114 | return item 115 | 116 | def __getitem__(self, i): 117 | if isinstance(i, slice): 118 | if i.step is not None: 119 | raise TypeError("Step not supported") 120 | start = i.start 121 | stop = i.stop 122 | else: 123 | start = i 124 | stop = i + 1 125 | 126 | if stop is None: 127 | stop = self.end - 1 128 | 129 | try: 130 | while len(self._elements) < stop: 131 | self.next() 132 | except StopIteration: 133 | assert self.index == self.end 134 | raise IndexError("Index %s out of range" % i) 135 | 136 | if isinstance(i, slice): 137 | return self._elements[start:stop] 138 | else: 139 | return self._elements[start] 140 | 141 | def __len__(self): 142 | if not self._len: 143 | try: 144 | while True: 145 | self.next() 146 | except StopIteration: 147 | self._len = len(self._elements) 148 | return self._len 149 | 150 | 151 | def peek(rlp, index, sedes=None): 152 | """ 153 | Get a specific element from an rlp encoded nested list. 154 | 155 | This function uses :func:`rlp.decode_lazy` and, thus, decodes only the 156 | necessary parts of the string. 157 | 158 | Usage example:: 159 | 160 | >>> import rlp 161 | >>> rlpdata = rlp.encode([1, 2, [3, [4, 5]]]) 162 | >>> rlp.peek(rlpdata, 0, rlp.sedes.big_endian_int) 163 | 1 164 | >>> rlp.peek(rlpdata, [2, 0], rlp.sedes.big_endian_int) 165 | 3 166 | 167 | :param rlp: the rlp string 168 | :param index: the index of the element to peek at (can be a list for 169 | nested data) 170 | :param sedes: a sedes used to deserialize the peeked at object, or `None` 171 | if no deserialization should be performed 172 | :raises: :exc:`IndexError` if `index` is invalid (out of range or too many 173 | levels) 174 | """ 175 | ll = decode_lazy(rlp) 176 | if not isinstance(index, Iterable): 177 | index = [index] 178 | for i in index: 179 | if isinstance(ll, Atomic): 180 | raise IndexError("Too many indices given") 181 | ll = ll[i] 182 | if sedes: 183 | return sedes.deserialize(ll) 184 | else: 185 | return ll 186 | -------------------------------------------------------------------------------- /rlp/sedes/__init__.py: -------------------------------------------------------------------------------- 1 | from . import ( 2 | raw, 3 | ) 4 | from .big_endian_int import ( 5 | BigEndianInt, 6 | big_endian_int, 7 | ) 8 | from .binary import ( 9 | Binary, 10 | binary, 11 | ) 12 | from .boolean import ( 13 | Boolean, 14 | boolean, 15 | ) 16 | from .lists import ( 17 | CountableList, 18 | List, 19 | ) 20 | from .serializable import ( 21 | Serializable, 22 | ) 23 | from .text import ( 24 | Text, 25 | text, 26 | ) 27 | -------------------------------------------------------------------------------- /rlp/sedes/big_endian_int.py: -------------------------------------------------------------------------------- 1 | from eth_utils import ( 2 | big_endian_to_int, 3 | int_to_big_endian, 4 | ) 5 | 6 | from rlp.exceptions import ( 7 | DeserializationError, 8 | SerializationError, 9 | ) 10 | 11 | 12 | class BigEndianInt: 13 | """ 14 | A sedes for big endian integers. 15 | 16 | :param l: the size of the serialized representation in bytes or `None` to 17 | use the shortest possible one 18 | """ 19 | 20 | def __init__(self, length=None): 21 | self.length = length 22 | 23 | def serialize(self, obj): 24 | if isinstance(obj, bool) or not isinstance(obj, int): 25 | raise SerializationError("Can only serialize integers", obj) 26 | if self.length is not None and obj >= 256**self.length: 27 | raise SerializationError( 28 | f"Integer too large (does not fit in {self.length} bytes)", 29 | obj, 30 | ) 31 | if obj < 0: 32 | raise SerializationError("Cannot serialize negative integers", obj) 33 | 34 | if obj == 0: 35 | s = b"" 36 | else: 37 | s = int_to_big_endian(obj) 38 | 39 | if self.length is not None: 40 | return b"\x00" * max(0, self.length - len(s)) + s 41 | else: 42 | return s 43 | 44 | def deserialize(self, serial): 45 | if self.length is not None and len(serial) != self.length: 46 | raise DeserializationError("Invalid serialization (wrong size)", serial) 47 | if self.length is None and len(serial) > 0 and serial[0:1] == b"\x00": 48 | raise DeserializationError( 49 | "Invalid serialization (not minimal " "length)", serial 50 | ) 51 | 52 | serial = serial or b"\x00" 53 | return big_endian_to_int(serial) 54 | 55 | 56 | big_endian_int = BigEndianInt() 57 | -------------------------------------------------------------------------------- /rlp/sedes/binary.py: -------------------------------------------------------------------------------- 1 | from rlp.atomic import ( 2 | Atomic, 3 | ) 4 | from rlp.exceptions import ( 5 | DeserializationError, 6 | SerializationError, 7 | ) 8 | 9 | 10 | class Binary: 11 | """ 12 | A sedes object for binary data of certain length. 13 | 14 | :param min_length: the minimal length in bytes or `None` for no lower limit 15 | :param max_length: the maximal length in bytes or `None` for no upper limit 16 | :param allow_empty: if true, empty strings are considered valid even if 17 | a minimum length is required otherwise 18 | """ 19 | 20 | def __init__(self, min_length=None, max_length=None, allow_empty=False): 21 | self.min_length = min_length or 0 22 | if max_length is None: 23 | self.max_length = float("inf") 24 | else: 25 | self.max_length = max_length 26 | self.allow_empty = allow_empty 27 | 28 | @classmethod 29 | def fixed_length(cls, length, allow_empty=False): 30 | """Create a sedes for binary data with exactly `length` bytes.""" 31 | return cls(length, length, allow_empty=allow_empty) 32 | 33 | @classmethod 34 | def is_valid_type(cls, obj): 35 | return isinstance(obj, (bytes, bytearray)) 36 | 37 | def is_valid_length(self, length): 38 | return any( 39 | ( 40 | self.min_length <= length <= self.max_length, 41 | self.allow_empty and length == 0, 42 | ) 43 | ) 44 | 45 | def serialize(self, obj): 46 | if not Binary.is_valid_type(obj): 47 | raise SerializationError(f"Object is not a serializable ({type(obj)})", obj) 48 | 49 | if not self.is_valid_length(len(obj)): 50 | raise SerializationError("Object has invalid length", obj) 51 | 52 | return obj 53 | 54 | def deserialize(self, serial): 55 | if not isinstance(serial, Atomic): 56 | raise DeserializationError( 57 | f"Objects of type {type(serial).__name__} cannot be deserialized", 58 | serial, 59 | ) 60 | 61 | if self.is_valid_length(len(serial)): 62 | return serial 63 | else: 64 | raise DeserializationError(f"{type(serial)} has invalid length", serial) 65 | 66 | 67 | binary = Binary() 68 | -------------------------------------------------------------------------------- /rlp/sedes/boolean.py: -------------------------------------------------------------------------------- 1 | from rlp.exceptions import ( 2 | DeserializationError, 3 | SerializationError, 4 | ) 5 | 6 | 7 | class Boolean: 8 | """A sedes for booleans""" 9 | 10 | def serialize(self, obj): 11 | if not isinstance(obj, bool): 12 | raise SerializationError("Can only serialize integers", obj) 13 | 14 | if obj is False: 15 | return b"" 16 | elif obj is True: 17 | return b"\x01" 18 | else: 19 | raise Exception("Invariant: no other options for boolean values") 20 | 21 | def deserialize(self, serial): 22 | if serial == b"": 23 | return False 24 | elif serial == b"\x01": 25 | return True 26 | else: 27 | raise DeserializationError( 28 | "Invalid serialized boolean. Must be either 0x01 or 0x00", serial 29 | ) 30 | 31 | 32 | boolean = Boolean() 33 | -------------------------------------------------------------------------------- /rlp/sedes/lists.py: -------------------------------------------------------------------------------- 1 | """ 2 | Module for sedes objects that use lists as serialization format. 3 | """ 4 | from collections.abc import ( 5 | Sequence, 6 | ) 7 | 8 | from eth_utils import ( 9 | to_list, 10 | to_tuple, 11 | ) 12 | 13 | from rlp.exceptions import ( 14 | DeserializationError, 15 | ListDeserializationError, 16 | ListSerializationError, 17 | SerializationError, 18 | ) 19 | 20 | from .binary import ( 21 | Binary as BinaryClass, 22 | ) 23 | 24 | 25 | def is_sedes(obj): 26 | """ 27 | Check if `obj` is a sedes object. 28 | 29 | A sedes object is characterized by having the methods `serialize(obj)` and 30 | `deserialize(serial)`. 31 | """ 32 | return hasattr(obj, "serialize") and hasattr(obj, "deserialize") 33 | 34 | 35 | def is_sequence(obj): 36 | """Check if `obj` is a sequence, but not a string or bytes.""" 37 | return isinstance(obj, Sequence) and not ( 38 | isinstance(obj, str) or BinaryClass.is_valid_type(obj) 39 | ) 40 | 41 | 42 | class List(list): 43 | """ 44 | A sedes for lists, implemented as a list of other sedes objects. 45 | 46 | :param strict: If true (de)serializing lists that have a length not 47 | matching the sedes length will result in an error. If false 48 | (de)serialization will stop as soon as either one of the 49 | lists runs out of elements. 50 | """ 51 | 52 | def __init__(self, elements=None, strict=True): 53 | super().__init__() 54 | self.strict = strict 55 | 56 | if elements: 57 | for e in elements: 58 | if is_sedes(e): 59 | self.append(e) 60 | elif isinstance(e, Sequence): 61 | self.append(List(e)) 62 | else: 63 | raise TypeError( 64 | "Instances of List must only contain sedes objects or " 65 | "nested sequences thereof." 66 | ) 67 | 68 | @to_list 69 | def serialize(self, obj): 70 | if not is_sequence(obj): 71 | raise ListSerializationError("Can only serialize sequences", obj) 72 | if self.strict and len(self) != len(obj): 73 | raise ListSerializationError( 74 | "Serializing list length (%d) does not match sedes (%d)" 75 | % (len(obj), len(self)), 76 | obj, 77 | ) 78 | 79 | for index, (element, sedes) in enumerate(zip(obj, self)): 80 | try: 81 | yield sedes.serialize(element) 82 | except SerializationError as e: 83 | raise ListSerializationError(obj=obj, element_exception=e, index=index) 84 | 85 | @to_tuple 86 | def deserialize(self, serial): 87 | if not is_sequence(serial): 88 | raise ListDeserializationError("Can only deserialize sequences", serial) 89 | 90 | if self.strict and len(serial) != len(self): 91 | raise ListDeserializationError( 92 | "Deserializing list length (%d) does not match sedes (%d)" 93 | % (len(serial), len(self)), 94 | serial, 95 | ) 96 | 97 | for idx, (sedes, element) in enumerate(zip(self, serial)): 98 | try: 99 | yield sedes.deserialize(element) 100 | except DeserializationError as e: 101 | raise ListDeserializationError( 102 | serial=serial, element_exception=e, index=idx 103 | ) 104 | 105 | 106 | class CountableList: 107 | """ 108 | A sedes for lists of arbitrary length. 109 | 110 | :param element_sedes: when (de-)serializing a list, this sedes will be 111 | applied to all of its elements 112 | :param max_length: maximum number of allowed elements, or `None` for no limit 113 | """ 114 | 115 | def __init__(self, element_sedes, max_length=None): 116 | self.element_sedes = element_sedes 117 | self.max_length = max_length 118 | 119 | @to_list 120 | def serialize(self, obj): 121 | if not is_sequence(obj): 122 | raise ListSerializationError("Can only serialize sequences", obj) 123 | 124 | if self.max_length is not None and len(obj) > self.max_length: 125 | raise ListSerializationError( 126 | f"Too many elements ({len(obj)}, allowed {self.max_length})", 127 | obj=obj, 128 | ) 129 | 130 | for index, element in enumerate(obj): 131 | try: 132 | yield self.element_sedes.serialize(element) 133 | except SerializationError as e: 134 | raise ListSerializationError(obj=obj, element_exception=e, index=index) 135 | 136 | @to_tuple 137 | def deserialize(self, serial): 138 | if not is_sequence(serial): 139 | raise ListDeserializationError( 140 | "Can only deserialize sequences", serial=serial 141 | ) 142 | for index, element in enumerate(serial): 143 | if self.max_length is not None and index >= self.max_length: 144 | raise ListDeserializationError( 145 | f"Too many elements (more than {self.max_length})", 146 | serial=serial, 147 | ) 148 | 149 | try: 150 | yield self.element_sedes.deserialize(element) 151 | except DeserializationError as e: 152 | raise ListDeserializationError( 153 | serial=serial, element_exception=e, index=index 154 | ) 155 | -------------------------------------------------------------------------------- /rlp/sedes/raw.py: -------------------------------------------------------------------------------- 1 | """ 2 | A sedes that does nothing. Thus, everything that can be directly encoded by RLP 3 | is serializable. This sedes can be used as a placeholder when deserializing 4 | larger structures. 5 | """ 6 | from collections.abc import ( 7 | Sequence, 8 | ) 9 | 10 | from rlp.atomic import ( 11 | Atomic, 12 | ) 13 | from rlp.exceptions import ( 14 | SerializationError, 15 | ) 16 | 17 | 18 | def serializable(obj): 19 | if isinstance(obj, Atomic): 20 | return True 21 | elif not isinstance(obj, str) and isinstance(obj, Sequence): 22 | return all(map(serializable, obj)) 23 | else: 24 | return False 25 | 26 | 27 | def serialize(obj): 28 | if not serializable(obj): 29 | raise SerializationError("Can only serialize nested lists of strings", obj) 30 | return obj 31 | 32 | 33 | def deserialize(serial): 34 | return serial 35 | -------------------------------------------------------------------------------- /rlp/sedes/serializable.py: -------------------------------------------------------------------------------- 1 | import abc 2 | import collections 3 | import copy 4 | import enum 5 | import re 6 | 7 | from eth_utils import ( 8 | to_dict, 9 | to_set, 10 | to_tuple, 11 | ) 12 | 13 | from rlp.exceptions import ( 14 | ListDeserializationError, 15 | ListSerializationError, 16 | ObjectDeserializationError, 17 | ObjectSerializationError, 18 | ) 19 | 20 | from .lists import ( 21 | List, 22 | ) 23 | 24 | 25 | class MetaBase: 26 | fields = None 27 | field_names = None 28 | field_attrs = None 29 | sedes = None 30 | 31 | 32 | def _get_duplicates(values): 33 | counts = collections.Counter(values) 34 | return tuple(item for item, num in counts.items() if num > 1) 35 | 36 | 37 | def validate_args_and_kwargs(args, kwargs, arg_names, allow_missing=False): 38 | duplicate_arg_names = _get_duplicates(arg_names) 39 | if duplicate_arg_names: 40 | raise TypeError(f"Duplicate argument names: {sorted(duplicate_arg_names)}") 41 | 42 | needed_kwargs = arg_names[len(args) :] 43 | used_kwargs = set(arg_names[: len(args)]) 44 | 45 | duplicate_kwargs = used_kwargs.intersection(kwargs.keys()) 46 | if duplicate_kwargs: 47 | raise TypeError(f"Duplicate kwargs: {sorted(duplicate_kwargs)}") 48 | 49 | unknown_kwargs = set(kwargs.keys()).difference(arg_names) 50 | if unknown_kwargs: 51 | raise TypeError(f"Unknown kwargs: {sorted(unknown_kwargs)}") 52 | 53 | missing_kwargs = set(needed_kwargs).difference(kwargs.keys()) 54 | if not allow_missing and missing_kwargs: 55 | raise TypeError(f"Missing kwargs: {sorted(missing_kwargs)}") 56 | 57 | 58 | @to_tuple 59 | def merge_kwargs_to_args(args, kwargs, arg_names, allow_missing=False): 60 | validate_args_and_kwargs(args, kwargs, arg_names, allow_missing=allow_missing) 61 | 62 | needed_kwargs = arg_names[len(args) :] 63 | 64 | yield from args 65 | for arg_name in needed_kwargs: 66 | yield kwargs[arg_name] 67 | 68 | 69 | @to_dict 70 | def merge_args_to_kwargs(args, kwargs, arg_names, allow_missing=False): 71 | validate_args_and_kwargs(args, kwargs, arg_names, allow_missing=allow_missing) 72 | 73 | yield from kwargs.items() 74 | for value, name in zip(args, arg_names): 75 | yield name, value 76 | 77 | 78 | def _eq(left, right): 79 | """ 80 | Equality comparison that allows for equality between tuple and list types with 81 | equivalent elements. 82 | """ 83 | if isinstance(left, (tuple, list)) and isinstance(right, (tuple, list)): 84 | return len(left) == len(right) and all(_eq(*pair) for pair in zip(left, right)) 85 | else: 86 | return left == right 87 | 88 | 89 | class ChangesetState(enum.Enum): 90 | INITIALIZED = "INITIALIZED" 91 | OPEN = "OPEN" 92 | CLOSED = "CLOSED" 93 | 94 | 95 | class ChangesetField: 96 | field = None 97 | 98 | def __init__(self, field): 99 | self.field = field 100 | 101 | def __get__(self, instance, type=None): 102 | if instance is None: 103 | return self 104 | elif instance.__state__ is not ChangesetState.OPEN: 105 | raise AttributeError( 106 | "Changeset is not active. Attribute access not allowed" 107 | ) 108 | else: 109 | try: 110 | return instance.__diff__[self.field] 111 | except KeyError: 112 | return getattr(instance.__original__, self.field) 113 | 114 | def __set__(self, instance, value): 115 | if instance.__state__ is not ChangesetState.OPEN: 116 | raise AttributeError( 117 | "Changeset is not active. Attribute access not allowed" 118 | ) 119 | instance.__diff__[self.field] = value 120 | 121 | 122 | class BaseChangeset: 123 | # reference to the original Serializable instance. 124 | __original__ = None 125 | # the state of this fieldset. Initialized -> Open -> Closed 126 | __state__ = None 127 | # the field changes that have been made in this change 128 | __diff__ = None 129 | 130 | def __init__(self, obj, changes=None): 131 | self.__original__ = obj 132 | self.__state__ = ChangesetState.INITIALIZED 133 | self.__diff__ = changes or {} 134 | 135 | def commit(self): 136 | obj = self.build_rlp() 137 | self.close() 138 | return obj 139 | 140 | def build_rlp(self): 141 | if self.__state__ == ChangesetState.OPEN: 142 | field_kwargs = { 143 | name: self.__diff__.get(name, self.__original__[name]) 144 | for name in self.__original__._meta.field_names 145 | } 146 | return type(self.__original__)(**field_kwargs) 147 | else: 148 | raise ValueError("Cannot open Changeset which is not in the OPEN state") 149 | 150 | def open(self): 151 | if self.__state__ == ChangesetState.INITIALIZED: 152 | self.__state__ = ChangesetState.OPEN 153 | else: 154 | raise ValueError( 155 | "Cannot open Changeset which is not in the INITIALIZED state" 156 | ) 157 | 158 | def close(self): 159 | if self.__state__ == ChangesetState.OPEN: 160 | self.__state__ = ChangesetState.CLOSED 161 | else: 162 | raise ValueError("Cannot close Changeset which is not in the OPEN state") 163 | 164 | def __enter__(self): 165 | if self.__state__ == ChangesetState.INITIALIZED: 166 | self.open() 167 | return self 168 | else: 169 | raise ValueError( 170 | "Cannot open Changeset which is not in the INITIALIZED state" 171 | ) 172 | 173 | def __exit__(self, exc_type, exc_value, traceback): 174 | if self.__state__ == ChangesetState.OPEN: 175 | self.close() 176 | 177 | 178 | def Changeset(obj, changes): 179 | namespace = {name: ChangesetField(name) for name in obj._meta.field_names} 180 | cls = type( 181 | f"{obj.__class__.__name__}Changeset", 182 | (BaseChangeset,), 183 | namespace, 184 | ) 185 | return cls(obj, changes) 186 | 187 | 188 | class BaseSerializable(collections.abc.Sequence): 189 | def __init__(self, *args, **kwargs): 190 | if kwargs: 191 | field_values = merge_kwargs_to_args(args, kwargs, self._meta.field_names) 192 | else: 193 | field_values = args 194 | 195 | if len(field_values) != len(self._meta.field_names): 196 | raise TypeError( 197 | f"Argument count mismatch. expected {len(self._meta.field_names)} - " 198 | f"got {len(field_values)} - " 199 | f"missing {','.join(self._meta.field_names[len(field_values) :])}" 200 | ) 201 | 202 | for value, attr in zip(field_values, self._meta.field_attrs): 203 | setattr(self, attr, make_immutable(value)) 204 | 205 | _cached_rlp = None 206 | 207 | def as_dict(self): 208 | return {field: value for field, value in zip(self._meta.field_names, self)} 209 | 210 | def __iter__(self): 211 | for attr in self._meta.field_attrs: 212 | yield getattr(self, attr) 213 | 214 | def __getitem__(self, idx): 215 | if isinstance(idx, int): 216 | attr = self._meta.field_attrs[idx] 217 | return getattr(self, attr) 218 | elif isinstance(idx, slice): 219 | field_slice = self._meta.field_attrs[idx] 220 | return tuple(getattr(self, field) for field in field_slice) 221 | elif isinstance(idx, str): 222 | return getattr(self, idx) 223 | else: 224 | raise IndexError(f"Unsupported type for __getitem__: {type(idx)}") 225 | 226 | def __len__(self): 227 | return len(self._meta.fields) 228 | 229 | def __eq__(self, other): 230 | return isinstance(other, Serializable) and hash(self) == hash(other) 231 | 232 | def __getstate__(self): 233 | state = self.__dict__.copy() 234 | # The hash() builtin is not stable across processes 235 | # (https://docs.python.org/3/reference/datamodel.html#object.__hash__), so we do 236 | # this here to ensure pickled instances don't carry the cached hash() as that 237 | # may cause issues like https://github.com/ethereum/py-evm/issues/1318 238 | state["_hash_cache"] = None 239 | return state 240 | 241 | _hash_cache = None 242 | 243 | def __hash__(self): 244 | if self._hash_cache is None: 245 | self._hash_cache = hash(tuple(self)) 246 | 247 | return self._hash_cache 248 | 249 | def __repr__(self): 250 | keyword_args = tuple(f"{k}={v!r}" for k, v in self.as_dict().items()) 251 | return f"{type(self).__name__}({', '.join(keyword_args)})" 252 | 253 | @classmethod 254 | def serialize(cls, obj): 255 | try: 256 | return cls._meta.sedes.serialize(obj) 257 | except ListSerializationError as e: 258 | raise ObjectSerializationError(obj=obj, sedes=cls, list_exception=e) 259 | 260 | @classmethod 261 | def deserialize(cls, serial, **extra_kwargs): 262 | try: 263 | values = cls._meta.sedes.deserialize(serial) 264 | except ListDeserializationError as e: 265 | raise ObjectDeserializationError(serial=serial, sedes=cls, list_exception=e) 266 | 267 | args_as_kwargs = merge_args_to_kwargs(values, {}, cls._meta.field_names) 268 | return cls(**args_as_kwargs, **extra_kwargs) 269 | 270 | def copy(self, *args, **kwargs): 271 | missing_overrides = ( 272 | set(self._meta.field_names) 273 | .difference(kwargs.keys()) 274 | .difference(self._meta.field_names[: len(args)]) 275 | ) 276 | unchanged_kwargs = { 277 | key: copy.deepcopy(value) 278 | for key, value in self.as_dict().items() 279 | if key in missing_overrides 280 | } 281 | combined_kwargs = dict(**unchanged_kwargs, **kwargs) 282 | all_kwargs = merge_args_to_kwargs(args, combined_kwargs, self._meta.field_names) 283 | return type(self)(**all_kwargs) 284 | 285 | def __copy__(self): 286 | return self.copy() 287 | 288 | def __deepcopy__(self, *args): 289 | return self.copy() 290 | 291 | _in_mutable_context = False 292 | 293 | def build_changeset(self, *args, **kwargs): 294 | args_as_kwargs = merge_args_to_kwargs( 295 | args, 296 | kwargs, 297 | self._meta.field_names, 298 | allow_missing=True, 299 | ) 300 | return Changeset(self, changes=args_as_kwargs) 301 | 302 | 303 | def make_immutable(value): 304 | if isinstance(value, list): 305 | return tuple(make_immutable(item) for item in value) 306 | else: 307 | return value 308 | 309 | 310 | @to_tuple 311 | def _mk_field_attrs(field_names, extra_namespace): 312 | namespace = set(field_names).union(extra_namespace) 313 | for field in field_names: 314 | while True: 315 | field = "_" + field 316 | if field not in namespace: 317 | namespace.add(field) 318 | yield field 319 | break 320 | 321 | 322 | def _mk_field_property(field, attr): 323 | def field_fn_getter(self): 324 | return getattr(self, attr) 325 | 326 | def field_fn_setter(self, value): 327 | if not self._in_mutable_context: 328 | raise AttributeError("can't set attribute") 329 | setattr(self, attr, value) 330 | 331 | return property(field_fn_getter, field_fn_setter) 332 | 333 | 334 | IDENTIFIER_REGEX = re.compile(r"^[^\d\W]\w*\Z", re.UNICODE) 335 | 336 | 337 | def _is_valid_identifier(value): 338 | # Source: https://stackoverflow.com/questions/5474008/regular-expression-to-confirm-whether-a-string-is-a-valid-identifier-in-python # noqa: E501 339 | if not isinstance(value, str): 340 | return False 341 | return bool(IDENTIFIER_REGEX.match(value)) 342 | 343 | 344 | @to_set 345 | def _get_class_namespace(cls): 346 | if hasattr(cls, "__dict__"): 347 | yield from cls.__dict__.keys() 348 | if hasattr(cls, "__slots__"): 349 | yield from cls.__slots__ 350 | 351 | 352 | class SerializableBase(abc.ABCMeta): 353 | def __new__(cls, name, bases, attrs): 354 | super_new = super().__new__ 355 | 356 | serializable_bases = tuple(b for b in bases if isinstance(b, SerializableBase)) 357 | has_multiple_serializable_parents = len(serializable_bases) > 1 358 | is_serializable_subclass = any(serializable_bases) 359 | declares_fields = "fields" in attrs 360 | 361 | if not is_serializable_subclass: 362 | # If this is the original creation of the `Serializable` class, 363 | # just create the class. 364 | return super_new(cls, name, bases, attrs) 365 | elif not declares_fields: 366 | if has_multiple_serializable_parents: 367 | raise TypeError( 368 | "Cannot create subclass from multiple parent `Serializable` " 369 | "classes without explicit `fields` declaration." 370 | ) 371 | else: 372 | # This is just a vanilla subclass of a `Serializable` parent class. 373 | parent_serializable = serializable_bases[0] 374 | 375 | if hasattr(parent_serializable, "_meta"): 376 | fields = parent_serializable._meta.fields 377 | else: 378 | # This is a subclass of `Serializable` which has no 379 | # `fields`, likely intended for further subclassing. 380 | fields = () 381 | else: 382 | # ensure that the `fields` property is a tuple of tuples to ensure 383 | # immutability. 384 | fields = tuple(tuple(field) for field in attrs.pop("fields")) 385 | 386 | # split the fields into names and sedes 387 | if fields: 388 | field_names, sedes = zip(*fields) 389 | else: 390 | field_names, sedes = (), () 391 | 392 | # check that field names are unique 393 | duplicate_field_names = _get_duplicates(field_names) 394 | if duplicate_field_names: 395 | raise TypeError( 396 | "The following fields are duplicated in the `fields` " 397 | f"declaration: {','.join(sorted(duplicate_field_names))}" 398 | ) 399 | 400 | # check that field names are valid identifiers 401 | invalid_field_names = { 402 | field_name 403 | for field_name in field_names 404 | if not _is_valid_identifier(field_name) 405 | } 406 | if invalid_field_names: 407 | raise TypeError( 408 | "The following field names are not valid python identifiers: " 409 | f"{','.join(f'`{item}`' for item in sorted(invalid_field_names))}" 410 | ) 411 | 412 | # extract all of the fields from parent `Serializable` classes. 413 | parent_field_names = { 414 | field_name 415 | for base in serializable_bases 416 | if hasattr(base, "_meta") 417 | for field_name in base._meta.field_names 418 | } 419 | 420 | # check that all fields from parent serializable classes are 421 | # represented on this class. 422 | missing_fields = parent_field_names.difference(field_names) 423 | if missing_fields: 424 | raise TypeError( 425 | "Subclasses of `Serializable` **must** contain a full superset " 426 | "of the fields defined in their parent classes. The following " 427 | f"fields are missing: {','.join(sorted(missing_fields))}" 428 | ) 429 | 430 | # the actual field values are stored in separate *private* attributes. 431 | # This computes attribute names that don't conflict with other 432 | # attributes already present on the class. 433 | reserved_namespace = set(attrs.keys()).union( 434 | attr 435 | for base in bases 436 | for parent_cls in base.__mro__ 437 | for attr in _get_class_namespace(parent_cls) 438 | ) 439 | field_attrs = _mk_field_attrs(field_names, reserved_namespace) 440 | 441 | # construct the Meta object to store field information for the class 442 | meta_namespace = { 443 | "fields": fields, 444 | "field_attrs": field_attrs, 445 | "field_names": field_names, 446 | "sedes": List(sedes), 447 | } 448 | 449 | meta_base = attrs.pop("_meta", MetaBase) 450 | meta = type( 451 | "Meta", 452 | (meta_base,), 453 | meta_namespace, 454 | ) 455 | attrs["_meta"] = meta 456 | 457 | # construct `property` attributes for read only access to the fields. 458 | field_props = tuple( 459 | (field, _mk_field_property(field, attr)) 460 | for field, attr in zip(meta.field_names, meta.field_attrs) 461 | ) 462 | 463 | return super_new( 464 | cls, 465 | name, 466 | bases, 467 | dict(field_props + tuple(attrs.items())), 468 | ) 469 | 470 | 471 | class Serializable(BaseSerializable, metaclass=SerializableBase): 472 | """ 473 | The base class for serializable objects. 474 | """ 475 | -------------------------------------------------------------------------------- /rlp/sedes/text.py: -------------------------------------------------------------------------------- 1 | from rlp.atomic import ( 2 | Atomic, 3 | ) 4 | from rlp.exceptions import ( 5 | DeserializationError, 6 | SerializationError, 7 | ) 8 | 9 | 10 | class Text: 11 | """ 12 | A sedes object for encoded text data of certain length. 13 | 14 | :param min_length: the minimal length in encoded characters or `None` for no lower 15 | limit 16 | :param max_length: the maximal length in encoded characters or `None` for no upper 17 | limit 18 | :param allow_empty: if true, empty strings are considered valid even if 19 | a minimum length is required otherwise 20 | """ 21 | 22 | def __init__( 23 | self, min_length=None, max_length=None, allow_empty=False, encoding="utf8" 24 | ): 25 | self.min_length = min_length or 0 26 | if max_length is None: 27 | self.max_length = float("inf") 28 | else: 29 | self.max_length = max_length 30 | self.allow_empty = allow_empty 31 | self.encoding = encoding 32 | 33 | @classmethod 34 | def fixed_length(cls, length, allow_empty=False): 35 | """Create a sedes for text data with exactly `length` encoded characters.""" 36 | return cls(length, length, allow_empty=allow_empty) 37 | 38 | @classmethod 39 | def is_valid_type(cls, obj): 40 | return isinstance(obj, str) 41 | 42 | def is_valid_length(self, length): 43 | return any( 44 | ( 45 | self.min_length <= length <= self.max_length, 46 | self.allow_empty and length == 0, 47 | ) 48 | ) 49 | 50 | def serialize(self, obj): 51 | if not self.is_valid_type(obj): 52 | raise SerializationError(f"Object is not a serializable ({type(obj)})", obj) 53 | 54 | if not self.is_valid_length(len(obj)): 55 | raise SerializationError("Object has invalid length", obj) 56 | 57 | return obj.encode(self.encoding) 58 | 59 | def deserialize(self, serial): 60 | if not isinstance(serial, Atomic): 61 | raise DeserializationError( 62 | f"Objects of type {type(serial).__name__} cannot be deserialized", 63 | serial, 64 | ) 65 | 66 | try: 67 | text_value = serial.decode(self.encoding) 68 | except UnicodeDecodeError as err: 69 | raise DeserializationError(str(err), serial) 70 | 71 | if self.is_valid_length(len(text_value)): 72 | return text_value 73 | else: 74 | raise DeserializationError(f"{type(serial)} has invalid length", serial) 75 | 76 | 77 | text = Text() 78 | -------------------------------------------------------------------------------- /rlp/utils.py: -------------------------------------------------------------------------------- 1 | import struct 2 | 3 | ALL_BYTES = tuple(struct.pack("B", i) for i in range(256)) 4 | -------------------------------------------------------------------------------- /scripts/release/test_package.py: -------------------------------------------------------------------------------- 1 | from pathlib import ( 2 | Path, 3 | ) 4 | import subprocess 5 | from tempfile import ( 6 | TemporaryDirectory, 7 | ) 8 | import venv 9 | 10 | 11 | def create_venv(parent_path: Path) -> Path: 12 | venv_path = parent_path / "package-smoke-test" 13 | venv.create(venv_path, with_pip=True) 14 | subprocess.run( 15 | [venv_path / "bin" / "pip", "install", "-U", "pip", "setuptools"], check=True 16 | ) 17 | return venv_path 18 | 19 | 20 | def find_wheel(project_path: Path) -> Path: 21 | wheels = list(project_path.glob("dist/*.whl")) 22 | 23 | if len(wheels) != 1: 24 | raise Exception( 25 | f"Expected one wheel. Instead found: {wheels} " 26 | f"in project {project_path.absolute()}" 27 | ) 28 | 29 | return wheels[0] 30 | 31 | 32 | def install_wheel(venv_path: Path, wheel_path: Path) -> None: 33 | subprocess.run( 34 | [venv_path / "bin" / "pip", "install", f"{wheel_path}"], 35 | check=True, 36 | ) 37 | 38 | 39 | def test_install_local_wheel() -> None: 40 | with TemporaryDirectory() as tmpdir: 41 | venv_path = create_venv(Path(tmpdir)) 42 | wheel_path = find_wheel(Path(".")) 43 | install_wheel(venv_path, wheel_path) 44 | print("Installed", wheel_path.absolute(), "to", venv_path) 45 | print(f"Activate with `source {venv_path}/bin/activate`") 46 | input("Press enter when the test has completed. The directory will be deleted.") 47 | 48 | 49 | if __name__ == "__main__": 50 | test_install_local_wheel() 51 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from setuptools import ( 3 | find_packages, 4 | setup, 5 | ) 6 | 7 | extras_require = { 8 | "dev": [ 9 | "build>=0.9.0", 10 | "bump_my_version>=0.19.0", 11 | "ipython", 12 | "pre-commit>=3.4.0", 13 | "tox>=4.0.0", 14 | "twine", 15 | "wheel", 16 | ], 17 | "docs": [ 18 | "sphinx>=6.0.0", 19 | "sphinx-autobuild>=2021.3.14", 20 | "sphinx_rtd_theme>=1.0.0", 21 | "towncrier>=24,<25", 22 | ], 23 | "test": [ 24 | "pytest>=7.0.0", 25 | "pytest-xdist>=2.4.0", 26 | "hypothesis>=6.22.0,<6.108.7", 27 | ], 28 | "rust-backend": ["rusty-rlp>=0.2.1"], 29 | } 30 | 31 | 32 | extras_require["dev"] = ( 33 | extras_require["dev"] + extras_require["docs"] + extras_require["test"] 34 | ) 35 | 36 | with open("./README.md") as readme: 37 | long_description = readme.read() 38 | 39 | setup( 40 | name="rlp", 41 | # *IMPORTANT*: Don't manually change the version here. See Contributing docs for the release process. 42 | version="4.1.0", 43 | description="""rlp: A package for Recursive Length Prefix encoding and decoding""", 44 | long_description=long_description, 45 | long_description_content_type="text/markdown", 46 | author="jnnk", 47 | author_email="jnnknnj@gmail.com", 48 | url="https://github.com/ethereum/pyrlp", 49 | include_package_data=True, 50 | install_requires=[ 51 | "eth-utils>=2", 52 | ], 53 | python_requires=">=3.8, <4", 54 | extras_require=extras_require, 55 | py_modules=["rlp"], 56 | license="MIT", 57 | zip_safe=False, 58 | keywords="rlp ethereum", 59 | packages=find_packages(exclude=["scripts", "scripts.*", "tests", "tests.*"]), 60 | classifiers=[ 61 | "Development Status :: 3 - Alpha", 62 | "Intended Audience :: Developers", 63 | "License :: OSI Approved :: MIT License", 64 | "Natural Language :: English", 65 | "Programming Language :: Python :: 3", 66 | "Programming Language :: Python :: 3.8", 67 | "Programming Language :: Python :: 3.9", 68 | "Programming Language :: Python :: 3.10", 69 | "Programming Language :: Python :: 3.11", 70 | "Programming Language :: Python :: 3.12", 71 | "Programming Language :: Python :: 3.13", 72 | ], 73 | ) 74 | -------------------------------------------------------------------------------- /tests/core/rlptest.json: -------------------------------------------------------------------------------- 1 | { 2 | "emptystring": { 3 | "in": "", 4 | "out": "80" 5 | }, 6 | "shortstring": { 7 | "in": "dog", 8 | "out": "83646f67" 9 | }, 10 | "shortstring2": { 11 | "in": "Lorem ipsum dolor sit amet, consectetur adipisicing eli", 12 | "out": "b74c6f72656d20697073756d20646f6c6f722073697420616d65742c20636f6e7365637465747572206164697069736963696e6720656c69" 13 | }, 14 | "longstring": { 15 | "in": "Lorem ipsum dolor sit amet, consectetur adipisicing elit", 16 | "out": "b8384c6f72656d20697073756d20646f6c6f722073697420616d65742c20636f6e7365637465747572206164697069736963696e6720656c6974" 17 | }, 18 | "longstring2": { 19 | "in": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur mauris magna, suscipit sed vehicula non, iaculis faucibus tortor. Proin suscipit ultricies malesuada. Duis tortor elit, dictum quis tristique eu, ultrices at risus. Morbi a est imperdiet mi ullamcorper aliquet suscipit nec lorem. Aenean quis leo mollis, vulputate elit varius, consequat enim. Nulla ultrices turpis justo, et posuere urna consectetur nec. Proin non convallis metus. Donec tempor ipsum in mauris congue sollicitudin. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Suspendisse convallis sem vel massa faucibus, eget lacinia lacus tempor. Nulla quis ultricies purus. Proin auctor rhoncus nibh condimentum mollis. Aliquam consequat enim at metus luctus, a eleifend purus egestas. Curabitur at nibh metus. Nam bibendum, neque at auctor tristique, lorem libero aliquet arcu, non interdum tellus lectus sit amet eros. Cras rhoncus, metus ac ornare cursus, dolor justo ultrices metus, at ullamcorper volutpat", 20 | "out": "b904004c6f72656d20697073756d20646f6c6f722073697420616d65742c20636f6e73656374657475722061646970697363696e6720656c69742e20437572616269747572206d6175726973206d61676e612c20737573636970697420736564207665686963756c61206e6f6e2c20696163756c697320666175636962757320746f72746f722e2050726f696e20737573636970697420756c74726963696573206d616c6573756164612e204475697320746f72746f7220656c69742c2064696374756d2071756973207472697374697175652065752c20756c7472696365732061742072697375732e204d6f72626920612065737420696d70657264696574206d6920756c6c616d636f7270657220616c6971756574207375736369706974206e6563206c6f72656d2e2041656e65616e2071756973206c656f206d6f6c6c69732c2076756c70757461746520656c6974207661726975732c20636f6e73657175617420656e696d2e204e756c6c6120756c74726963657320747572706973206a7573746f2c20657420706f73756572652075726e6120636f6e7365637465747572206e65632e2050726f696e206e6f6e20636f6e76616c6c6973206d657475732e20446f6e65632074656d706f7220697073756d20696e206d617572697320636f6e67756520736f6c6c696369747564696e2e20566573746962756c756d20616e746520697073756d207072696d697320696e206661756369627573206f726369206c756374757320657420756c74726963657320706f737565726520637562696c69612043757261653b2053757370656e646973736520636f6e76616c6c69732073656d2076656c206d617373612066617563696275732c2065676574206c6163696e6961206c616375732074656d706f722e204e756c6c61207175697320756c747269636965732070757275732e2050726f696e20617563746f722072686f6e637573206e69626820636f6e64696d656e74756d206d6f6c6c69732e20416c697175616d20636f6e73657175617420656e696d206174206d65747573206c75637475732c206120656c656966656e6420707572757320656765737461732e20437572616269747572206174206e696268206d657475732e204e616d20626962656e64756d2c206e6571756520617420617563746f72207472697374697175652c206c6f72656d206c696265726f20616c697175657420617263752c206e6f6e20696e74657264756d2074656c6c7573206c65637475732073697420616d65742065726f732e20437261732072686f6e6375732c206d65747573206163206f726e617265206375727375732c20646f6c6f72206a7573746f20756c747269636573206d657475732c20617420756c6c616d636f7270657220766f6c7574706174" 21 | }, 22 | "zero": { 23 | "in": "", 24 | "out": "80" 25 | }, 26 | "smallint": { 27 | "in": 1, 28 | "out": "01" 29 | }, 30 | "smallint2": { 31 | "in": 16, 32 | "out": "10" 33 | }, 34 | "smallint3": { 35 | "in": 79, 36 | "out": "4f" 37 | }, 38 | "smallint4": { 39 | "in": 127, 40 | "out": "7f" 41 | }, 42 | "mediumint1": { 43 | "in": 128, 44 | "out": "8180" 45 | }, 46 | "mediumint2": { 47 | "in": 1000, 48 | "out": "8203e8" 49 | }, 50 | "mediumint3": { 51 | "in": 100000, 52 | "out": "830186a0" 53 | }, 54 | "mediumint4": { 55 | "in": 83729609699884896815286331701780722, 56 | "out": "8F102030405060708090A0B0C0D0E0F2" 57 | }, 58 | "mediumint5": { 59 | "in": 105315505618206987246253880190783558935785933862974822347068935681, 60 | "out": "9C0100020003000400050006000700080009000A000B000C000D000E01" 61 | }, 62 | "emptylist": { 63 | "in": [], 64 | "out": "c0" 65 | }, 66 | "stringlist": { 67 | "in": [ "dog", "god", "cat" ], 68 | "out": "cc83646f6783676f6483636174" 69 | }, 70 | "multilist": { 71 | "in": [ "zw", [ 4 ], 1 ], 72 | "out": "c6827a77c10401" 73 | }, 74 | "shortListMax1": { 75 | "in": [ "asdf", "qwer", "zxcv", "asdf","qwer", "zxcv", "asdf", "qwer", "zxcv", "asdf", "qwer"], 76 | "out": "F784617364668471776572847a78637684617364668471776572847a78637684617364668471776572847a78637684617364668471776572" 77 | }, 78 | "longList1" : { 79 | "in" : [ 80 | ["asdf","qwer","zxcv"], 81 | ["asdf","qwer","zxcv"], 82 | ["asdf","qwer","zxcv"], 83 | ["asdf","qwer","zxcv"] 84 | ], 85 | "out": "F840CF84617364668471776572847a786376CF84617364668471776572847a786376CF84617364668471776572847a786376CF84617364668471776572847a786376" 86 | }, 87 | "longList2" : { 88 | "in" : [ 89 | ["asdf","qwer","zxcv"], 90 | ["asdf","qwer","zxcv"], 91 | ["asdf","qwer","zxcv"], 92 | ["asdf","qwer","zxcv"], 93 | ["asdf","qwer","zxcv"], 94 | ["asdf","qwer","zxcv"], 95 | ["asdf","qwer","zxcv"], 96 | ["asdf","qwer","zxcv"], 97 | ["asdf","qwer","zxcv"], 98 | ["asdf","qwer","zxcv"], 99 | ["asdf","qwer","zxcv"], 100 | ["asdf","qwer","zxcv"], 101 | ["asdf","qwer","zxcv"], 102 | ["asdf","qwer","zxcv"], 103 | ["asdf","qwer","zxcv"], 104 | ["asdf","qwer","zxcv"], 105 | ["asdf","qwer","zxcv"], 106 | ["asdf","qwer","zxcv"], 107 | ["asdf","qwer","zxcv"], 108 | ["asdf","qwer","zxcv"], 109 | ["asdf","qwer","zxcv"], 110 | ["asdf","qwer","zxcv"], 111 | ["asdf","qwer","zxcv"], 112 | ["asdf","qwer","zxcv"], 113 | ["asdf","qwer","zxcv"], 114 | ["asdf","qwer","zxcv"], 115 | ["asdf","qwer","zxcv"], 116 | ["asdf","qwer","zxcv"], 117 | ["asdf","qwer","zxcv"], 118 | ["asdf","qwer","zxcv"], 119 | ["asdf","qwer","zxcv"], 120 | ["asdf","qwer","zxcv"] 121 | ], 122 | "out": "F90200CF84617364668471776572847a786376CF84617364668471776572847a786376CF84617364668471776572847a786376CF84617364668471776572847a786376CF84617364668471776572847a786376CF84617364668471776572847a786376CF84617364668471776572847a786376CF84617364668471776572847a786376CF84617364668471776572847a786376CF84617364668471776572847a786376CF84617364668471776572847a786376CF84617364668471776572847a786376CF84617364668471776572847a786376CF84617364668471776572847a786376CF84617364668471776572847a786376CF84617364668471776572847a786376CF84617364668471776572847a786376CF84617364668471776572847a786376CF84617364668471776572847a786376CF84617364668471776572847a786376CF84617364668471776572847a786376CF84617364668471776572847a786376CF84617364668471776572847a786376CF84617364668471776572847a786376CF84617364668471776572847a786376CF84617364668471776572847a786376CF84617364668471776572847a786376CF84617364668471776572847a786376CF84617364668471776572847a786376CF84617364668471776572847a786376CF84617364668471776572847a786376CF84617364668471776572847a786376" 123 | }, 124 | 125 | "listsoflists": { 126 | "in": [ [ [], [] ], [] ], 127 | "out": "c4c2c0c0c0" 128 | }, 129 | "listsoflists2": { 130 | "in": [ [], [[]], [ [], [[]] ] ], 131 | "out": "c7c0c1c0c3c0c1c0" 132 | }, 133 | "dictTest1" : { 134 | "in" : [ 135 | ["key1", "val1"], 136 | ["key2", "val2"], 137 | ["key3", "val3"], 138 | ["key4", "val4"] 139 | ], 140 | "out" : "ECCA846b6579318476616c31CA846b6579328476616c32CA846b6579338476616c33CA846b6579348476616c34" 141 | }, 142 | "bigint": { 143 | "in": 115792089237316195423570985008687907853269984665640564039457584007913129639936, 144 | "out": "a1010000000000000000000000000000000000000000000000000000000000000000" 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /tests/core/speed.py: -------------------------------------------------------------------------------- 1 | """ 2 | util to benchmark known usecase 3 | """ 4 | import random 5 | import time 6 | 7 | import rlp 8 | from rlp.sedes import ( 9 | BigEndianInt, 10 | Binary, 11 | CountableList, 12 | big_endian_int, 13 | binary, 14 | ) 15 | from rlp.sedes.serializable import ( 16 | Serializable, 17 | ) 18 | 19 | address = Binary.fixed_length(20, allow_empty=True) 20 | int20 = BigEndianInt(20) 21 | int32 = BigEndianInt(32) 22 | int256 = BigEndianInt(256) 23 | hash32 = Binary.fixed_length(32) 24 | trie_root = Binary.fixed_length(32, allow_empty=True) 25 | 26 | 27 | def zpad(x, length): 28 | return b"\x00" * max(0, length - len(x)) + x 29 | 30 | 31 | class Transaction(Serializable): 32 | fields = [ 33 | ("nonce", big_endian_int), 34 | ("gasprice", big_endian_int), 35 | ("startgas", big_endian_int), 36 | ("to", address), 37 | ("value", big_endian_int), 38 | ("data", binary), 39 | ("v", big_endian_int), 40 | ("r", big_endian_int), 41 | ("s", big_endian_int), 42 | ] 43 | 44 | def __init__( 45 | self, nonce, gasprice, startgas, to, value, data, v=0, r=0, s=0, **kwargs 46 | ): 47 | super().__init__(nonce, gasprice, startgas, to, value, data, v, r, s, **kwargs) 48 | 49 | 50 | class BlockHeader(Serializable): 51 | fields = [ 52 | ("prevhash", hash32), 53 | ("uncles_hash", hash32), 54 | ("coinbase", address), 55 | ("state_root", trie_root), 56 | ("tx_list_root", trie_root), 57 | ("receipts_root", trie_root), 58 | ("bloom", int256), 59 | ("difficulty", big_endian_int), 60 | ("number", big_endian_int), 61 | ("gas_limit", big_endian_int), 62 | ("gas_used", big_endian_int), 63 | ("timestamp", big_endian_int), 64 | ("extra_data", binary), 65 | ("mixhash", binary), 66 | ("nonce", binary), 67 | ] 68 | 69 | 70 | class Block(Serializable): 71 | fields = [ 72 | ("header", BlockHeader), 73 | ("transaction_list", CountableList(Transaction)), 74 | ("uncles", CountableList(BlockHeader)), 75 | ] 76 | 77 | def __init__(self, header, transaction_list=None, uncles=None, **kwargs): 78 | super().__init__(header, transaction_list or [], uncles or [], **kwargs) 79 | 80 | 81 | def rand_bytes(num=32): 82 | return zpad(big_endian_int.serialize(random.getrandbits(num * 8)), num) 83 | 84 | 85 | rand_bytes32 = rand_bytes 86 | 87 | 88 | def rand_address(): 89 | return rand_bytes(20) 90 | 91 | 92 | def rand_bytes8(): 93 | return rand_bytes(8) 94 | 95 | 96 | def rand_bigint(): 97 | return random.getrandbits(256) 98 | 99 | 100 | def rand_int(): 101 | return random.getrandbits(32) 102 | 103 | 104 | rand_map = { 105 | hash32: rand_bytes32, 106 | trie_root: rand_bytes32, 107 | binary: rand_bytes32, 108 | address: rand_address, 109 | Binary: rand_bytes8, 110 | big_endian_int: rand_int, 111 | int256: rand_bigint, 112 | } 113 | 114 | assert Binary in rand_map 115 | 116 | 117 | def mk_transaction(): 118 | return Transaction( 119 | rand_int(), 120 | rand_int(), 121 | rand_int(), 122 | rand_address(), 123 | rand_int(), 124 | rand_bytes32(), 125 | 27, 126 | rand_bigint(), 127 | rand_bigint(), 128 | ) 129 | 130 | 131 | rlp.decode(rlp.encode(mk_transaction()), Transaction) 132 | 133 | 134 | def mk_block_header(): 135 | return BlockHeader(*[rand_map[t]() for _, t in BlockHeader._meta.fields]) 136 | 137 | 138 | rlp.decode(rlp.encode(mk_block_header()), BlockHeader) 139 | 140 | 141 | def mk_block(num_transactions=10, num_uncles=1): 142 | return Block( 143 | mk_block_header(), 144 | [mk_transaction() for _ in range(num_transactions)], 145 | [mk_block_header() for _ in range(num_uncles)], 146 | ) 147 | 148 | 149 | rlp.decode(rlp.encode(mk_block()), Block) 150 | 151 | 152 | def do_test_serialize(block, rounds=100): 153 | for _ in range(rounds): 154 | x = rlp.encode(block, cache=False) 155 | return x 156 | 157 | 158 | def do_test_deserialize(data, rounds=100, sedes=Block): 159 | for _ in range(rounds): 160 | x = rlp.decode(data, sedes) 161 | return x 162 | 163 | 164 | def main(rounds=10000): 165 | st = time.time() 166 | d = do_test_serialize(mk_block(), rounds) 167 | elapsed = time.time() - st 168 | print("Block serializations / sec: %.2f" % (rounds / elapsed)) 169 | 170 | st = time.time() 171 | d = do_test_deserialize(d, rounds) 172 | elapsed = time.time() - st 173 | print("Block deserializations / sec: %.2f" % (rounds / elapsed)) 174 | 175 | st = time.time() 176 | d = do_test_serialize(mk_transaction(), rounds) 177 | elapsed = time.time() - st 178 | print("TX serializations / sec: %.2f" % (rounds / elapsed)) 179 | 180 | st = time.time() 181 | d = do_test_deserialize(d, rounds, sedes=Transaction) 182 | elapsed = time.time() - st 183 | print("TX deserializations / sec: %.2f" % (rounds / elapsed)) 184 | 185 | 186 | if __name__ == "__main__": 187 | main() 188 | """ 189 | py2 190 | serializations / sec: 658.64 191 | deserializations / sec: 1331.62 192 | 193 | pypy2 194 | serializations / sec: 4628.81 : x7 speedup 195 | deserializations / sec: 4753.84 : x3.5 speedup 196 | """ 197 | -------------------------------------------------------------------------------- /tests/core/test_benchmark.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from itertools import ( 3 | chain, 4 | repeat, 5 | ) 6 | import sys 7 | 8 | import rlp 9 | from rlp.exceptions import ( 10 | DecodingError, 11 | DeserializationError, 12 | ) 13 | from rlp.sedes import ( 14 | CountableList, 15 | binary, 16 | ) 17 | 18 | try: 19 | import pytest_benchmark # noqa: F401 20 | except ImportError: 21 | do_benchmark = False 22 | else: 23 | do_benchmark = True 24 | 25 | 26 | # speed up setup in case tests aren't run anyway 27 | if do_benchmark: 28 | SIZE = int(1e6) 29 | else: 30 | SIZE = 1 31 | 32 | 33 | class Message(rlp.Serializable): 34 | fields = [ 35 | ("field1", binary), 36 | ("field2", binary), 37 | ("field3", CountableList(binary, max_length=100)), 38 | ] 39 | 40 | 41 | def lazy_test_factory(s, valid): 42 | @pytest.mark.benchmark(group="lazy") 43 | def f(benchmark): 44 | @benchmark 45 | def result(): 46 | try: 47 | Message.deserialize(rlp.decode_lazy(s)) 48 | except (DecodingError, DeserializationError): 49 | return not valid 50 | else: 51 | return valid 52 | 53 | assert result 54 | 55 | return f 56 | 57 | 58 | def eager_test_factory(s, valid): 59 | @pytest.mark.benchmark(group="eager") 60 | def f(benchmark): 61 | @benchmark 62 | def result(): 63 | try: 64 | rlp.decode(s, Message) 65 | except (DecodingError, DeserializationError): 66 | return not valid 67 | else: 68 | return valid 69 | 70 | assert result 71 | 72 | return f 73 | 74 | 75 | def generate_test_functions(): 76 | valid = {} 77 | invalid = {} 78 | long_string = bytes(bytearray(i % 256 for i in range(SIZE))) 79 | long_list = rlp.encode([c for c in long_string]) 80 | invalid["long_string"] = long_string 81 | invalid["long_list"] = long_list 82 | 83 | nested_list = rlp.encode(b"\x00") 84 | for _ in repeat(None, SIZE): 85 | nested_list += rlp.codec.length_prefix(len(nested_list), 0xC0) 86 | invalid["nested_list"] = nested_list 87 | 88 | valid["long_string_object"] = rlp.encode([b"\x00", long_string, []]) 89 | 90 | prefix = rlp.codec.length_prefix(1 + 1 + len(long_list), 0xC0) 91 | invalid["long_list_object"] = ( 92 | prefix + rlp.encode(b"\x00") + rlp.encode(b"\x00") + long_list 93 | ) 94 | 95 | valid["friendly"] = rlp.encode( 96 | Message( 97 | b"hello", 98 | b"I'm friendly", 99 | [b"not", b"many", b"elements"], 100 | ) 101 | ) 102 | 103 | invalid = invalid.items() 104 | valid = valid.items() 105 | rlp_strings = [i[1] for i in chain(valid, invalid)] 106 | valids = [True] * len(valid) + [False] * len(invalid) 107 | names = [i[0] for i in chain(valid, invalid)] 108 | 109 | current_module = sys.modules[__name__] 110 | for rlp_string, valid, name in zip(rlp_strings, valids, names): 111 | f_eager = pytest.mark.skipif("not do_benchmark")( 112 | eager_test_factory(rlp_string, valid) 113 | ) 114 | f_lazy = pytest.mark.skipif("not do_benchmark")( 115 | lazy_test_factory(rlp_string, valid) 116 | ) 117 | setattr(current_module, "test_eager_" + name, f_eager) 118 | setattr(current_module, "test_lazy_" + name, f_lazy) 119 | 120 | 121 | generate_test_functions() 122 | -------------------------------------------------------------------------------- /tests/core/test_big_endian.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import binascii 3 | 4 | from eth_utils import ( 5 | int_to_big_endian, 6 | ) 7 | 8 | from rlp import ( 9 | SerializationError, 10 | ) 11 | from rlp.sedes import ( 12 | BigEndianInt, 13 | big_endian_int, 14 | ) 15 | from rlp.utils import ( 16 | ALL_BYTES, 17 | ) 18 | 19 | valid_data = ( 20 | (256, b"\x01\x00"), 21 | (1024, b"\x04\x00"), 22 | (65535, b"\xff\xff"), 23 | ) 24 | 25 | single_bytes = ((n, ALL_BYTES[n]) for n in range(1, 256)) 26 | 27 | random_integers = ( 28 | 256, 29 | 257, 30 | 4839, 31 | 849302, 32 | 483290432, 33 | 483290483290482039482039, 34 | 48930248348219540325894323584235894327865439258743754893066, 35 | ) 36 | assert random_integers[-1] < 2**256 37 | 38 | negative_ints = (-1, -100, -255, -256, -2342423) 39 | 40 | 41 | def test_neg(): 42 | for n in negative_ints: 43 | with pytest.raises(SerializationError): 44 | big_endian_int.serialize(n) 45 | 46 | 47 | @pytest.mark.parametrize("value", [True, False]) 48 | def test_rejects_bool(value): 49 | with pytest.raises(SerializationError): 50 | big_endian_int.serialize(value) 51 | 52 | 53 | def test_serialization(): 54 | for n in random_integers: 55 | serial = big_endian_int.serialize(n) 56 | deserialized = big_endian_int.deserialize(serial) 57 | assert deserialized == n 58 | if n != 0: 59 | assert serial[0] != b"\x00" # is not checked 60 | 61 | 62 | def test_single_byte(): 63 | for n, s in single_bytes: 64 | serial = big_endian_int.serialize(n) 65 | assert serial == s 66 | deserialized = big_endian_int.deserialize(serial) 67 | assert deserialized == n 68 | 69 | 70 | def test_valid_data(): 71 | for n, serial in valid_data: 72 | serialized = big_endian_int.serialize(n) 73 | deserialized = big_endian_int.deserialize(serial) 74 | assert serialized == serial 75 | assert deserialized == n 76 | 77 | 78 | def test_fixedlength(): 79 | s = BigEndianInt(4) 80 | for i in (0, 1, 255, 256, 256**3, 256**4 - 1): 81 | assert len(s.serialize(i)) == 4 82 | assert s.deserialize(s.serialize(i)) == i 83 | for i in (256**4, 256**4 + 1, 256**5, -1, -256, "asdf"): 84 | with pytest.raises(SerializationError): 85 | s.serialize(i) 86 | 87 | 88 | def packl(lnum): 89 | """Packs the lnum (which must be convertable to a long) into a 90 | byte string 0 padded to a multiple of padmultiple bytes in size. 0 91 | means no padding whatsoever, so that packing 0 result in an empty 92 | string. The resulting byte string is the big-endian two's 93 | complement representation of the passed in long.""" 94 | 95 | if lnum == 0: 96 | return b"\0" 97 | s = hex(lnum)[2:] 98 | s = s.rstrip("L") 99 | if len(s) & 1: 100 | s = "0" + s 101 | s = binascii.unhexlify(s) 102 | return s 103 | 104 | 105 | try: 106 | import ctypes 107 | 108 | PyLong_AsByteArray = ctypes.pythonapi._PyLong_AsByteArray 109 | PyLong_AsByteArray.argtypes = [ 110 | ctypes.py_object, 111 | ctypes.c_char_p, 112 | ctypes.c_size_t, 113 | ctypes.c_int, 114 | ctypes.c_int, 115 | ] 116 | import sys 117 | 118 | long_start = sys.maxint + 1 119 | 120 | def packl_ctypes(lnum): 121 | if lnum < long_start: 122 | return int_to_big_endian(lnum) 123 | a = ctypes.create_string_buffer(lnum.bit_length() // 8 + 1) 124 | PyLong_AsByteArray(lnum, a, len(a), 0, 1) 125 | return a.raw.lstrip(b"\0") 126 | 127 | except AttributeError: 128 | packl_ctypes = packl 129 | 130 | 131 | def test_packl(): 132 | for i in range(256): 133 | v = 2**i - 1 134 | rc = packl_ctypes(v) 135 | assert rc == int_to_big_endian(v) 136 | r = packl(v) 137 | assert r == int_to_big_endian(v) 138 | 139 | 140 | def perf(): 141 | import time 142 | 143 | st = time.time() 144 | for _ in range(100000): 145 | for i in random_integers: 146 | packl(i) 147 | print(f"packl elapsed {time.time() - st}") 148 | 149 | st = time.time() 150 | for _ in range(100000): 151 | for i in random_integers: 152 | packl_ctypes(i) 153 | print(f"ctypes elapsed {time.time() - st}") 154 | 155 | st = time.time() 156 | for _ in range(100000): 157 | for i in random_integers: 158 | int_to_big_endian(i) 159 | print(f"py elapsed {time.time() - st}") 160 | 161 | 162 | if __name__ == "__main__": 163 | # test_packl() 164 | perf() 165 | -------------------------------------------------------------------------------- /tests/core/test_binary_sedes.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from rlp import ( 4 | SerializationError, 5 | ) 6 | from rlp.sedes import ( 7 | Binary, 8 | ) 9 | 10 | 11 | @pytest.mark.parametrize( 12 | "value,expected", 13 | ( 14 | (b"", b""), 15 | (b"asdf", b"asdf"), 16 | (b"\x00" * 20, b"\x00" * 20), 17 | (b"fdsa", b"fdsa"), 18 | ), 19 | ) 20 | def test_simple_binary_serialization(value, expected): 21 | sedes = Binary() 22 | assert sedes.serialize(value) == expected 23 | 24 | 25 | @pytest.mark.parametrize( 26 | "value", 27 | ([], 5, str, "", "arst"), 28 | ) 29 | def test_binary_unserializable_values(value): 30 | sedes = Binary() 31 | with pytest.raises(SerializationError): 32 | sedes.serialize(value) 33 | 34 | 35 | @pytest.mark.parametrize( 36 | "value,expected", 37 | ( 38 | (b"asdfg", b"asdfg"), 39 | (b"\x00\x01\x02\x03\x04", b"\x00\x01\x02\x03\x04"), 40 | (b"ababa", b"ababa"), 41 | ), 42 | ) 43 | def test_binary_fixed_length_serialization(value, expected): 44 | sedes = Binary.fixed_length(5) 45 | assert sedes.serialize(value) == expected 46 | 47 | 48 | def test_binary_fixed_lenght_of_zero(): 49 | sedes = Binary.fixed_length(0) 50 | assert sedes.serialize(b"") == b"" 51 | 52 | with pytest.raises(SerializationError): 53 | sedes.serialize(b"a") 54 | with pytest.raises(SerializationError): 55 | sedes.serialize(b"arst") 56 | 57 | 58 | @pytest.mark.parametrize( 59 | "value", 60 | (b"asdf", b"asdfgh", b"", b"bababa"), 61 | ) 62 | def test_binary_fixed_length_serialization_with_wrong_length(value): 63 | sedes = Binary.fixed_length(5) 64 | with pytest.raises(SerializationError): 65 | sedes.serialize(value) 66 | 67 | 68 | @pytest.mark.parametrize( 69 | "value,expected", 70 | ( 71 | (b"as", b"as"), 72 | (b"dfg", b"dfg"), 73 | (b"hjkl", b"hjkl"), 74 | (b"\x00\x01\x02", b"\x00\x01\x02"), 75 | ), 76 | ) 77 | def test_binary_variable_length_serialization(value, expected): 78 | sedes = Binary(2, 4) 79 | assert sedes.serialize(value) == expected 80 | 81 | 82 | @pytest.mark.parametrize( 83 | "value", 84 | (b"", b"a", b"abcde"), 85 | ) 86 | def test_binary_variable_length_serialization_wrong_length(value): 87 | sedes = Binary(2, 4) 88 | with pytest.raises(SerializationError): 89 | sedes.serialize(value) 90 | 91 | 92 | @pytest.mark.parametrize( 93 | "value,expected", 94 | ( 95 | (b"abc", b"abc"), 96 | (b"abcd", b"abcd"), 97 | (b"x" * 132, b"x" * 132), 98 | ), 99 | ) 100 | def test_binary_min_length_serialization(value, expected): 101 | sedes = Binary(min_length=3) 102 | assert sedes.serialize(value) == expected 103 | 104 | 105 | @pytest.mark.parametrize( 106 | "value", 107 | (b"ab", b"", b"a", b"xy"), 108 | ) 109 | def test_binary_min_length_serialization_wrong_length(value): 110 | sedes = Binary(min_length=3) 111 | with pytest.raises(SerializationError): 112 | sedes.serialize(value) 113 | 114 | 115 | @pytest.mark.parametrize( 116 | "value,expected", 117 | ( 118 | (b"", b""), 119 | (b"ab", b"ab"), 120 | (b"abc", b"abc"), 121 | ), 122 | ) 123 | def test_binary_max_length_serialization(value, expected): 124 | sedes = Binary(max_length=3) 125 | assert sedes.serialize(value) == expected 126 | 127 | 128 | @pytest.mark.parametrize( 129 | "value", 130 | (b"abcd", b"vwxyz", b"a" * 32), 131 | ) 132 | def test_binary_max_length_serialization_wrong_length(value): 133 | sedes = Binary(max_length=3) 134 | with pytest.raises(SerializationError): 135 | sedes.serialize(value) 136 | 137 | 138 | @pytest.mark.parametrize( 139 | "value,expected", 140 | ( 141 | (b"", b""), 142 | (b"abc", b"abc"), 143 | (b"abcd", b"abcd"), 144 | (b"abcde", b"abcde"), 145 | ), 146 | ) 147 | def test_binary_min_and_max_length_with_allow_empty(value, expected): 148 | sedes = Binary(min_length=3, max_length=5, allow_empty=True) 149 | assert sedes.serialize(value) == expected 150 | 151 | 152 | @pytest.mark.parametrize( 153 | "value", 154 | (b"a", b"ab", b"abcdef", b"abcdefgh" * 10), 155 | ) 156 | def test_binary_min_and_max_length_with_allow_empty_wrong_length(value): 157 | sedes = Binary(min_length=3, max_length=5, allow_empty=True) 158 | with pytest.raises(SerializationError): 159 | sedes.serialize(value) 160 | -------------------------------------------------------------------------------- /tests/core/test_boolean_serializer.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from rlp import ( 4 | DeserializationError, 5 | SerializationError, 6 | ) 7 | from rlp.sedes import ( 8 | Boolean, 9 | ) 10 | 11 | 12 | @pytest.mark.parametrize( 13 | "value,expected", 14 | ( 15 | (True, b"\x01"), 16 | (False, b""), 17 | ), 18 | ) 19 | def test_boolean_serialize_values(value, expected): 20 | sedes = Boolean() 21 | assert sedes.serialize(value) == expected 22 | 23 | 24 | @pytest.mark.parametrize( 25 | "value", 26 | ( 27 | None, 28 | 1, 29 | 0, 30 | "True", 31 | b"True", 32 | ), 33 | ) 34 | def test_boolean_serialize_bad_values(value): 35 | sedes = Boolean() 36 | with pytest.raises(SerializationError): 37 | sedes.serialize(value) 38 | 39 | 40 | @pytest.mark.parametrize( 41 | "value,expected", 42 | ( 43 | (b"\x01", True), 44 | (b"", False), 45 | ), 46 | ) 47 | def test_boolean_deserialization(value, expected): 48 | sedes = Boolean() 49 | assert sedes.deserialize(value) == expected 50 | 51 | 52 | @pytest.mark.parametrize( 53 | "value", 54 | ( 55 | b" ", 56 | b"\x02", 57 | b"\x00\x00", 58 | b"\x01\x00", 59 | b"\x00\x01", 60 | b"\x01\x01", 61 | ), 62 | ) 63 | def test_boolean_deserialization_bad_value(value): 64 | sedes = Boolean() 65 | with pytest.raises(DeserializationError): 66 | sedes.deserialize(value) 67 | -------------------------------------------------------------------------------- /tests/core/test_bytearray.py: -------------------------------------------------------------------------------- 1 | from rlp import ( 2 | decode, 3 | decode_lazy, 4 | encode, 5 | ) 6 | 7 | 8 | def test_bytearray(): 9 | e = encode(b"abc") 10 | expected = decode(e) 11 | actual = decode(bytearray(e)) 12 | assert actual == expected 13 | 14 | 15 | def test_bytearray_lazy(): 16 | e = encode(b"abc") 17 | expected = decode(e) 18 | actual = decode_lazy(bytearray(e)) 19 | assert expected == actual 20 | 21 | 22 | def test_encoding_bytearray(): 23 | s = b"abcdef" 24 | direct = encode(s) 25 | from_bytearray = encode(bytearray(s)) 26 | assert direct == from_bytearray 27 | assert decode(direct) == s 28 | -------------------------------------------------------------------------------- /tests/core/test_codec.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from eth_utils import ( 4 | decode_hex, 5 | ) 6 | 7 | from rlp import ( 8 | decode, 9 | encode, 10 | ) 11 | from rlp.codec import ( 12 | consume_item, 13 | consume_length_prefix, 14 | ) 15 | from rlp.exceptions import ( 16 | DecodingError, 17 | ) 18 | 19 | EMPTYLIST = encode([]) 20 | 21 | 22 | def compare_length(rlpdata, length): 23 | _, _typ, _len, _pos = consume_length_prefix(rlpdata, 0) 24 | assert _typ is list 25 | lenlist = 0 26 | if rlpdata == EMPTYLIST: 27 | return -1 if length > 0 else 1 if length < 0 else 0 28 | while 1: 29 | if lenlist > length: 30 | return 1 31 | _, _, _l, _p = consume_length_prefix(rlpdata, _pos) 32 | lenlist += 1 33 | if _l + _p >= len(rlpdata): 34 | break 35 | _pos = _l + _p 36 | return 0 if lenlist == length else -1 37 | 38 | 39 | def test_compare_length(): 40 | data = encode([1, 2, 3, 4, 5]) 41 | assert compare_length(data, 100) == -1 42 | assert compare_length(data, 5) == 0 43 | assert compare_length(data, 1) == 1 44 | 45 | data = encode([]) 46 | assert compare_length(data, 100) == -1 47 | assert compare_length(data, 0) == 0 48 | assert compare_length(data, -1) == 1 49 | 50 | 51 | def test_favor_short_string_form(): 52 | data = decode_hex("b8056d6f6f7365") 53 | with pytest.raises(DecodingError): 54 | decode(data) 55 | 56 | data = decode_hex("856d6f6f7365") 57 | assert decode(data) == b"moose" 58 | 59 | 60 | def test_consume_item(): 61 | obj = [b"f", b"bar", b"a" * 100, 105, [b"nested", b"list"]] 62 | rlp = encode(obj) 63 | item, per_item_rlp, end = consume_item(rlp, 0) 64 | assert per_item_rlp == [ 65 | ( 66 | b"\xf8y" 67 | b"f" + b"\x83bar" + b"\xb8d" + b"a" * 100 + b"i" + b"\xcc\x86nested\x84list" 68 | ), 69 | [b"f"], 70 | [b"\x83bar"], 71 | [b"\xb8d" + b"a" * 100], 72 | [b"i"], 73 | [b"\xcc\x86nested\x84list", [b"\x86nested"], [b"\x84list"]], 74 | ] 75 | assert end == 123 76 | assert per_item_rlp[0] == rlp 77 | -------------------------------------------------------------------------------- /tests/core/test_countablelist.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | import rlp 4 | from rlp import ( 5 | DeserializationError, 6 | SerializationError, 7 | ) 8 | from rlp.sedes import ( 9 | big_endian_int, 10 | ) 11 | from rlp.sedes.lists import ( 12 | CountableList, 13 | ) 14 | 15 | 16 | def test_countable_list(): 17 | l1 = CountableList(big_endian_int) 18 | serializable = [(), (1, 2), tuple(range(500))] 19 | for s in serializable: 20 | r = l1.serialize(s) 21 | assert l1.deserialize(r) == s 22 | not_serializable = ([1, "asdf"], ["asdf"], [1, [2]], [[]]) 23 | for n in not_serializable: 24 | with pytest.raises(SerializationError): 25 | l1.serialize(n) 26 | 27 | l2 = CountableList(CountableList(big_endian_int)) 28 | serializable = ((), ((),), ((1, 2, 3), (4,)), ((5,), (6, 7, 8)), ((), (), (9, 0))) 29 | for s in serializable: 30 | r = l2.serialize(s) 31 | assert l2.deserialize(r) == s 32 | not_serializable = ([[[]]], [1, 2], [1, ["asdf"], ["fdsa"]]) 33 | for n in not_serializable: 34 | with pytest.raises(SerializationError): 35 | l2.serialize(n) 36 | 37 | l3 = CountableList(big_endian_int, max_length=3) 38 | serializable = [(), (1,), (1, 2), (1, 2, 3)] 39 | for s in serializable: 40 | r = l3.serialize(s) 41 | assert r == l1.serialize(s) 42 | assert l3.deserialize(r) == s 43 | not_serializable = [(1, 2, 3, 4), (1, 2, 3, 4, 5, 6, 7), range(500)] 44 | for s in not_serializable: 45 | with pytest.raises(SerializationError): 46 | l3.serialize(s) 47 | r = l1.serialize(s) 48 | with pytest.raises(DeserializationError): 49 | l3.deserialize(r) 50 | ll = rlp.decode_lazy(rlp.encode(r)) 51 | with pytest.raises(DeserializationError): 52 | l3.deserialize(ll) 53 | assert len(ll._elements) == 3 + 1 # failed early, did not consume fully 54 | -------------------------------------------------------------------------------- /tests/core/test_import_and_version.py: -------------------------------------------------------------------------------- 1 | def test_import_and_version(): 2 | import rlp 3 | 4 | assert isinstance(rlp.__version__, str) 5 | -------------------------------------------------------------------------------- /tests/core/test_invalid.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from rlp import ( 4 | DecodingError, 5 | decode, 6 | ) 7 | 8 | invalid_rlp = ( 9 | b"", 10 | b"\x00\xab", 11 | b"\x00\x00\xff", 12 | b"\x83dogcat", 13 | b"\x83do", 14 | b"\xc7\xc0\xc1\xc0\xc3\xc0\xc1\xc0\xff", 15 | b"\xc7\xc0\xc1\xc0\xc3\xc0\xc1" b"\x81\x02", 16 | b"\xb8\x00", 17 | b"\xb9\x00\x00", 18 | b"\xba\x00\x02\xff\xff", 19 | b"\x81\x54", 20 | ) 21 | 22 | 23 | def test_invalid_rlp(): 24 | for serial in invalid_rlp: 25 | with pytest.raises(DecodingError): 26 | decode(serial) 27 | -------------------------------------------------------------------------------- /tests/core/test_json.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import json 3 | 4 | from eth_utils import ( 5 | add_0x_prefix, 6 | decode_hex, 7 | encode_hex, 8 | ) 9 | 10 | import rlp 11 | from rlp import ( 12 | DecodingError, 13 | decode, 14 | decode_lazy, 15 | encode, 16 | infer_sedes, 17 | ) 18 | 19 | 20 | def evaluate(ll): 21 | if isinstance(ll, rlp.lazy.LazyList): 22 | return [evaluate(e) for e in ll] 23 | else: 24 | return ll 25 | 26 | 27 | def normalize_input(value): 28 | if isinstance(value, str): 29 | return value.encode("utf8") 30 | elif isinstance(value, list): 31 | return [normalize_input(v) for v in value] 32 | elif isinstance(value, int): 33 | return value 34 | else: 35 | raise ValueError("Unsupported type") 36 | 37 | 38 | def compare_nested(got, expected): 39 | if isinstance(got, bytes): 40 | return got == expected 41 | try: 42 | zipped = zip(got, expected) 43 | except TypeError: 44 | return got == expected 45 | else: 46 | if len(list(zipped)) == len(got) == len(expected): 47 | return all(compare_nested(x, y) for x, y in zipped) 48 | else: 49 | return False 50 | 51 | 52 | with open("tests/core/rlptest.json") as rlptest_file: 53 | test_data = json.load(rlptest_file) 54 | test_pieces = [ 55 | ( 56 | name, 57 | { 58 | "in": normalize_input(in_out["in"]), 59 | "out": add_0x_prefix(in_out["out"]), 60 | }, 61 | ) 62 | for name, in_out in test_data.items() 63 | ] 64 | 65 | 66 | @pytest.mark.parametrize("name, in_out", test_pieces) 67 | def test_encode(name, in_out): 68 | data = in_out["in"] 69 | result = encode_hex(encode(data)).lower() 70 | expected = in_out["out"].lower() 71 | if result != expected: 72 | pytest.fail( 73 | f"Test {name} failed (encoded {data} to {result} instead of {expected})" 74 | ) 75 | 76 | 77 | @pytest.mark.parametrize("name, in_out", test_pieces) 78 | def test_decode(name, in_out): 79 | rlp_string = decode_hex(in_out["out"]) 80 | decoded = decode(rlp_string) 81 | with pytest.raises(DecodingError): 82 | decode(rlp_string + b"\x00") 83 | assert decoded == decode(rlp_string + b"\x00", strict=False) 84 | 85 | assert decoded == evaluate(decode_lazy(rlp_string)) 86 | expected = in_out["in"] 87 | sedes = infer_sedes(expected) 88 | data = sedes.deserialize(decoded) 89 | assert compare_nested(data, decode(rlp_string, sedes)) 90 | 91 | if not compare_nested(data, expected): 92 | pytest.fail( 93 | f"Test {name} failed (decoded {rlp_string} to {decoded} instead of {expected})" # noqa: E501 94 | ) 95 | -------------------------------------------------------------------------------- /tests/core/test_lazy.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from collections.abc import ( 3 | Sequence, 4 | ) 5 | 6 | import rlp 7 | from rlp import ( 8 | DeserializationError, 9 | ) 10 | from rlp.sedes import ( 11 | CountableList, 12 | big_endian_int, 13 | ) 14 | 15 | 16 | def evaluate(lazy_list): 17 | if isinstance(lazy_list, rlp.lazy.LazyList): 18 | return tuple(evaluate(e) for e in lazy_list) 19 | else: 20 | return lazy_list 21 | 22 | 23 | def test_empty_list(): 24 | def dec(): 25 | return rlp.decode_lazy(rlp.encode([])) 26 | 27 | assert isinstance(dec(), Sequence) 28 | with pytest.raises(IndexError): 29 | dec()[0] 30 | with pytest.raises(IndexError): 31 | dec()[1] 32 | assert len(dec()) == 0 33 | assert evaluate(dec()) == () 34 | 35 | 36 | @pytest.mark.parametrize( 37 | "value", 38 | (b"", b"asdf", b"a" * 56, b"b" * 123), 39 | ) 40 | def test_string(value): 41 | def dec(): 42 | return rlp.decode_lazy(rlp.encode(value)) 43 | 44 | assert isinstance(dec(), bytes) 45 | assert len(dec()) == len(value) 46 | assert dec() == value 47 | assert rlp.peek(rlp.encode(value), []) == value 48 | with pytest.raises(IndexError): 49 | rlp.peek(rlp.encode(value), 0) 50 | with pytest.raises(IndexError): 51 | rlp.peek(rlp.encode(value), [0]) 52 | 53 | 54 | def test_list_getitem(): 55 | length = rlp.decode_lazy(rlp.encode([1, 2, 3]), big_endian_int) 56 | assert isinstance(length, rlp.lazy.LazyList) 57 | assert length[0] == 1 58 | assert length[1] == 2 59 | assert length[2] == 3 60 | assert length[-1] == 3 61 | assert length[-2] == 2 62 | assert length[-3] == 1 63 | assert length[0:3] == [1, 2, 3] 64 | assert length[0:2] == [1, 2] 65 | assert length[0:1] == [1] 66 | assert length[1:2] == [2] 67 | assert length[1:] == [2, 3] 68 | assert length[1:-1] == [2] 69 | assert length[-2:] == [2, 3] 70 | assert length[:2] == [1, 2] 71 | 72 | 73 | def test_nested_list(): 74 | length = ((), (b"a"), (b"b", b"c", b"d")) 75 | 76 | def dec(): 77 | return rlp.decode_lazy(rlp.encode(length)) 78 | 79 | assert isinstance(dec(), Sequence) 80 | assert len(dec()) == len(length) 81 | assert evaluate(dec()) == length 82 | with pytest.raises(IndexError): 83 | dec()[0][0] 84 | with pytest.raises(IndexError): 85 | dec()[1][1] 86 | with pytest.raises(IndexError): 87 | dec()[2][3] 88 | with pytest.raises(IndexError): 89 | dec()[3] 90 | 91 | 92 | @pytest.mark.parametrize( 93 | "value", 94 | ( 95 | (), 96 | (1,), 97 | (3, 2, 1), 98 | ), 99 | ) 100 | def test_evaluation_of_lazy_decode_with_simple_value_sedes(value): 101 | assert evaluate(rlp.decode_lazy(rlp.encode(value), big_endian_int)) == value 102 | 103 | 104 | def test_evaluation_of_lazy_decode_with_list_sedes_and_invalid_value(): 105 | sedes = CountableList(big_endian_int) 106 | value = [(), (1, 2), b"asdf", (3)] 107 | invalid_lazy = rlp.decode_lazy(rlp.encode(value), sedes) 108 | assert invalid_lazy[0] == value[0] 109 | assert invalid_lazy[1] == value[1] 110 | with pytest.raises(DeserializationError): 111 | invalid_lazy[2] 112 | 113 | 114 | def test_peek(): 115 | assert rlp.peek(rlp.encode(b""), []) == b"" 116 | nested = rlp.encode([0, 1, [2, 3]]) 117 | assert rlp.peek(nested, [2, 0], big_endian_int) == 2 118 | for index in [3, [3], [0, 0], [2, 2], [2, 1, 0]]: 119 | with pytest.raises(IndexError): 120 | rlp.peek(nested, index) 121 | assert rlp.peek(nested, 2, CountableList(big_endian_int)) == (2, 3) 122 | -------------------------------------------------------------------------------- /tests/core/test_raw_sedes.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from rlp import ( 4 | DecodingError, 5 | SerializationError, 6 | decode, 7 | encode, 8 | ) 9 | from rlp.sedes import ( 10 | raw, 11 | ) 12 | 13 | serializable = ( 14 | b"", 15 | b"asdf", 16 | b"fds89032#$@%", 17 | b"", 18 | b"dfsa", 19 | [b"dfsa", b""], 20 | [], 21 | [b"fdsa", [b"dfs", [b"jfdkl"]]], 22 | ) 23 | 24 | 25 | def test_serializable(): 26 | for s in serializable: 27 | raw.serialize(s) 28 | code = encode(s, raw) 29 | assert s == decode(code, raw) 30 | 31 | 32 | @pytest.mark.parametrize( 33 | "rlp_data", 34 | (0, 32, ["asdf", ["fdsa", [5]]], str), 35 | ) 36 | def test_invalid_serializations(rlp_data): 37 | with pytest.raises(SerializationError): 38 | raw.serialize(rlp_data) 39 | 40 | 41 | @pytest.mark.parametrize( 42 | "rlp_data", 43 | ( 44 | None, 45 | "asdf", 46 | ), 47 | ) 48 | def test_invalid_deserializations(rlp_data): 49 | with pytest.raises(DecodingError): 50 | decode(rlp_data, raw) 51 | -------------------------------------------------------------------------------- /tests/core/test_sedes.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from rlp import ( 4 | DeserializationError, 5 | SerializationError, 6 | infer_sedes, 7 | ) 8 | from rlp.sedes import ( 9 | CountableList, 10 | List, 11 | big_endian_int, 12 | binary, 13 | boolean, 14 | text, 15 | ) 16 | 17 | 18 | @pytest.mark.parametrize( 19 | "value,expected", 20 | ( 21 | (5, big_endian_int), 22 | (0, big_endian_int), 23 | (-1, None), 24 | (True, boolean), 25 | (False, boolean), 26 | (None, None), 27 | (b"", binary), 28 | (b"asdf", binary), 29 | (b"\xe4\xf6\xfc\xea\xe2\xfb", binary), 30 | ("", text), 31 | ("asdf", text), 32 | ("\xe4\xf6\xfc\xea\xe2\xfb", text), 33 | ("你好世界", text), 34 | ("\u4f60\u597d\u4e16\u754c", text), 35 | ([], List()), 36 | ([1, 2, 3], List((big_endian_int,) * 3)), 37 | ([[], b"asdf"], List(([], binary))), 38 | ([1, "asdf"], List((big_endian_int, text))), 39 | ), 40 | ) 41 | def test_inference(value, expected): 42 | if expected is not None: 43 | inferred = infer_sedes(value) 44 | assert inferred == expected 45 | expected.serialize(value) 46 | else: 47 | with pytest.raises(TypeError): 48 | infer_sedes(value) 49 | 50 | 51 | def test_list_sedes(): 52 | l1 = List() 53 | l2 = List((big_endian_int, big_endian_int)) 54 | l3 = List((l1, l2, [[[]]])) 55 | 56 | l1.serialize([]) 57 | l2.serialize((2, 3)) 58 | l3.serialize([[], [5, 6], [[[]]]]) 59 | 60 | with pytest.raises(SerializationError): 61 | l1.serialize([[]]) 62 | with pytest.raises(SerializationError): 63 | l1.serialize([5]) 64 | 65 | for d in ([], [1, 2, 3], [1, [2, 3], 4]): 66 | with pytest.raises(SerializationError): 67 | l2.serialize(d) 68 | for d in ([], [[], [], [[[]]]], [[], [5, 6], [[]]]): 69 | with pytest.raises(SerializationError): 70 | l3.serialize(d) 71 | 72 | c = CountableList(big_endian_int) 73 | assert l1.deserialize(c.serialize([])) == () 74 | for s in (c.serialize(length) for length in [[1], [1, 2, 3], range(30), (4, 3)]): 75 | with pytest.raises(DeserializationError): 76 | l1.deserialize(s) 77 | 78 | valid = [(1, 2), (3, 4), (9, 8)] 79 | for s, v in ((c.serialize(v), v) for v in valid): 80 | assert l2.deserialize(s) == v 81 | invalid = [[], [1], [1, 2, 3]] 82 | for s in (c.serialize(i) for i in invalid): 83 | with pytest.raises(DeserializationError): 84 | l2.deserialize(s) 85 | -------------------------------------------------------------------------------- /tests/core/test_serializable.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from multiprocessing import ( 3 | get_context, 4 | ) 5 | import pickle 6 | import re 7 | 8 | from rlp import ( 9 | SerializationError, 10 | decode, 11 | encode, 12 | infer_sedes, 13 | ) 14 | from rlp.sedes import ( 15 | List, 16 | big_endian_int, 17 | binary, 18 | ) 19 | from rlp.sedes.serializable import ( 20 | Serializable, 21 | ) 22 | 23 | 24 | class RLPType1(Serializable): 25 | fields = [ 26 | ("field1", big_endian_int), 27 | ("field2", binary), 28 | ("field3", List((big_endian_int, binary))), 29 | ] 30 | 31 | 32 | class RLPType2(Serializable): 33 | fields = [ 34 | ("field2_1", RLPType1), 35 | ("field2_2", List((RLPType1, RLPType1))), 36 | ] 37 | 38 | 39 | class RLPType3(Serializable): 40 | fields = [ 41 | ("field1", big_endian_int), 42 | ("field2", big_endian_int), 43 | ("field3", big_endian_int), 44 | ] 45 | 46 | def __init__(self, field2, field1, field3, **kwargs): 47 | super().__init__(field1=field1, field2=field2, field3=field3, **kwargs) 48 | 49 | 50 | class RLPType4(RLPType3): 51 | pass 52 | 53 | 54 | class RLPEmptyFieldsType(Serializable): 55 | fields = () 56 | 57 | 58 | class RLPUndeclaredFieldsType(Serializable): 59 | pass 60 | 61 | 62 | _type_1_a = RLPType1(5, b"a", (0, b"")) 63 | _type_1_b = RLPType1(9, b"b", (2, b"")) 64 | _type_2 = RLPType2(_type_1_a.copy(), [_type_1_a.copy(), _type_1_b.copy()]) 65 | _type_undeclared_fields = RLPUndeclaredFieldsType() 66 | 67 | 68 | @pytest.fixture 69 | def type_1_a(): 70 | return _type_1_a.copy() 71 | 72 | 73 | @pytest.fixture 74 | def type_1_b(): 75 | return _type_1_b.copy() 76 | 77 | 78 | @pytest.fixture 79 | def type_2(): 80 | return _type_2.copy() 81 | 82 | 83 | @pytest.fixture(params=[_type_1_a, _type_1_b, _type_2]) 84 | def rlp_obj(request): 85 | return request.param.copy() 86 | 87 | 88 | def test_serializeable_repr_evaluatable(rlp_obj): 89 | evaluated = eval(repr(rlp_obj)) 90 | assert evaluated == rlp_obj 91 | 92 | 93 | @pytest.mark.parametrize( 94 | "rlptype,args,kwargs,exception_includes", 95 | ( 96 | # missing fields args 97 | (RLPType1, [], {}, ["field1", "field2", "field3"]), 98 | (RLPType1, [8], {}, ["field2", "field3"]), 99 | (RLPType1, [7, 8], {}, ["field3"]), 100 | # missing fields kwargs 101 | (RLPType1, [], {"field1": 7}, ["field2", "field3"]), 102 | (RLPType1, [], {"field1": 7, "field2": 8}, ["field3"]), 103 | (RLPType1, [], {"field2": 7, "field3": (1, b"")}, ["field1"]), 104 | (RLPType1, [], {"field3": (1, b"")}, ["field1", "field2"]), 105 | (RLPType1, [], {"field2": 7}, ["field1", "field3"]), 106 | # missing fields args and kwargs 107 | (RLPType1, [7], {"field2": 8}, ["field3"]), 108 | (RLPType1, [7], {"field3": (1, b"")}, ["field2"]), 109 | # duplicate fields 110 | (RLPType1, [7], {"field1": 8}, ["field1"]), 111 | (RLPType1, [7, 8], {"field1": 8, "field2": 7}, ["field1", "field2"]), 112 | ), 113 | ) 114 | def test_serializable_initialization_validation( 115 | rlptype, args, kwargs, exception_includes 116 | ): 117 | for msg_part in exception_includes: 118 | with pytest.raises(TypeError, match=msg_part): 119 | rlptype(*args, **kwargs) 120 | 121 | 122 | @pytest.mark.parametrize( 123 | "args,kwargs", 124 | ( 125 | ([2, 1, 3], {}), 126 | ([2, 1], {"field3": 3}), 127 | ([2], {"field3": 3, "field1": 1}), 128 | ([], {"field3": 3, "field1": 1, "field2": 2}), 129 | ), 130 | ) 131 | def test_serializable_initialization_args_kwargs_mix(args, kwargs): 132 | obj = RLPType3(*args, **kwargs) 133 | 134 | assert obj.field1 == 1 135 | assert obj.field2 == 2 136 | assert obj.field3 == 3 137 | 138 | 139 | @pytest.mark.parametrize( 140 | "lookup,expected", 141 | ( 142 | (0, 5), 143 | (1, b"a"), 144 | (2, (0, b"")), 145 | (slice(0, 1), (5,)), 146 | (slice(0, 2), (5, b"a")), 147 | (slice(0, 3), (5, b"a", (0, b""))), 148 | (slice(1, 3), (b"a", (0, b""))), 149 | (slice(2, 3), ((0, b""),)), 150 | (slice(None, 3), (5, b"a", (0, b""))), 151 | (slice(None, 2), (5, b"a")), 152 | (slice(None, 1), (5,)), 153 | (slice(None, 0), tuple()), 154 | (slice(0, None), (5, b"a", (0, b""))), 155 | (slice(1, None), (b"a", (0, b""))), 156 | (slice(2, None), ((0, b""),)), 157 | (slice(2, None), ((0, b""),)), 158 | (slice(None, None), (5, b"a", (0, b""))), 159 | (slice(-1, None), ((0, b""),)), 160 | ( 161 | slice(-2, None), 162 | ( 163 | b"a", 164 | (0, b""), 165 | ), 166 | ), 167 | ( 168 | slice(-3, None), 169 | ( 170 | 5, 171 | b"a", 172 | (0, b""), 173 | ), 174 | ), 175 | ), 176 | ) 177 | def test_serializable_getitem_lookups(type_1_a, lookup, expected): 178 | actual = type_1_a[lookup] 179 | assert actual == expected 180 | 181 | 182 | def test_serializable_subclass_retains_field_info_from_parent(): 183 | obj = RLPType4(2, 1, 3) 184 | assert obj.field1 == 1 185 | assert obj.field2 == 2 186 | assert obj.field3 == 3 187 | 188 | 189 | def test_undeclared_fields_serializable_class(): 190 | assert RLPUndeclaredFieldsType.serialize(_type_undeclared_fields) == [] 191 | assert ( 192 | RLPUndeclaredFieldsType.deserialize( 193 | RLPUndeclaredFieldsType.serialize(_type_undeclared_fields) 194 | ) 195 | == _type_undeclared_fields 196 | ) 197 | 198 | 199 | def test_deserialization_for_custom_init_method(): 200 | type_3 = RLPType3(2, 1, 3) 201 | assert type_3.field1 == 1 202 | assert type_3.field2 == 2 203 | assert type_3.field3 == 3 204 | 205 | result = decode(encode(type_3), sedes=RLPType3) 206 | 207 | assert result.field1 == 1 208 | assert result.field2 == 2 209 | assert result.field3 == 3 210 | 211 | 212 | def test_serializable_iterator(): 213 | values = (5, b"a", (1, b"c")) 214 | obj = RLPType1(*values) 215 | assert tuple(obj) == values 216 | 217 | 218 | def test_serializable_equality(type_1_a, type_1_b, type_2): 219 | # equality 220 | assert type_1_a == type_1_a 221 | assert type_1_a == RLPType1(*type_1_a) 222 | assert type_1_b == type_1_b 223 | assert type_1_b == RLPType1(*type_1_b) 224 | 225 | assert type_2 == type_2 226 | assert type_1_a != type_1_b 227 | assert type_1_b != type_2 228 | assert type_2 != type_1_a 229 | 230 | 231 | def test_serializable_pickling_across_processes(type_1_a): 232 | # Ensure the hash is what we expect *and* populate the cache. 233 | assert hash(type_1_a) == hash(tuple(type_1_a)) 234 | 235 | pickled_obj = pickle.dumps(type_1_a) 236 | for method in ["fork", "spawn"]: 237 | ctx = get_context(method) 238 | p = ctx.Process(target=_assert_hash_cache_equal, args=(pickled_obj,)) 239 | p.start() 240 | p.join() 241 | 242 | 243 | def _assert_hash_cache_equal(pickled_obj): 244 | obj = pickle.loads(pickled_obj) 245 | assert hash(obj) == hash(tuple(obj)) 246 | 247 | 248 | def test_serializable_sedes_inference(type_1_a, type_1_b, type_2): 249 | assert infer_sedes(type_1_a) == RLPType1 250 | assert infer_sedes(type_1_b) == RLPType1 251 | assert infer_sedes(type_2) == RLPType2 252 | 253 | 254 | def test_serializable_invalid_serialization_value(type_1_a, type_1_b, type_2): 255 | # Note this is not done in parametrize because the values are pytest.fixtures 256 | value_fixtures = ( 257 | (RLPType2, type_1_a), 258 | (RLPType2, type_1_b), 259 | (RLPType1, type_2), 260 | (RLPEmptyFieldsType, type_1_a), 261 | (RLPEmptyFieldsType, type_1_b), 262 | (RLPEmptyFieldsType, type_2), 263 | ) 264 | for serializer, invalid_value in value_fixtures: 265 | with pytest.raises(SerializationError): 266 | serializer.serialize(invalid_value) 267 | 268 | 269 | def test_serializable_serialization(type_1_a, type_1_b, type_2): 270 | serial_1_a = RLPType1.serialize(type_1_a) 271 | serial_1_b = RLPType1.serialize(type_1_b) 272 | serial_2 = RLPType2.serialize(type_2) 273 | assert serial_1_a == [b"\x05", b"a", [b"", b""]] 274 | assert serial_1_b == [b"\x09", b"b", [b"\x02", b""]] 275 | assert serial_2 == [serial_1_a, [serial_1_a, serial_1_b]] 276 | 277 | 278 | def test_serializable_deserialization(type_1_a, type_1_b, type_2): 279 | serial_1_a = RLPType1.serialize(type_1_a) 280 | serial_1_b = RLPType1.serialize(type_1_b) 281 | serial_2 = RLPType2.serialize(type_2) 282 | 283 | res_type_1_a = RLPType1.deserialize(serial_1_a) 284 | res_type_1_b = RLPType1.deserialize(serial_1_b) 285 | res_type_2 = RLPType2.deserialize(serial_2) 286 | 287 | assert res_type_1_a == type_1_a 288 | assert res_type_1_b == type_1_b 289 | assert res_type_2 == type_2 290 | 291 | 292 | def test_serializable_field_immutability(type_1_a, type_1_b, type_2): 293 | with pytest.raises(AttributeError, match=r"can't set attribute"): 294 | type_1_a.field1 += 1 295 | assert type_1_a.field1 == 5 296 | 297 | with pytest.raises(AttributeError, match=r"can't set attribute"): 298 | type_1_a.field2 = b"x" 299 | assert type_1_a.field2 == b"a" 300 | 301 | with pytest.raises(AttributeError, match=r"can't set attribute"): 302 | type_2.field2_1.field1 += 1 303 | assert type_2.field2_1.field1 == 5 304 | 305 | with pytest.raises( 306 | TypeError, match=r"'tuple' object does not support item assignment" 307 | ): 308 | type_2.field2_2[1] = type_1_a 309 | assert type_2.field2_2[1] == type_1_b 310 | 311 | 312 | def test_serializable_encoding_rlp_caching(rlp_obj): 313 | assert rlp_obj._cached_rlp is None 314 | 315 | # obj should start out without a cache 316 | rlp_code = encode(rlp_obj, cache=False) 317 | assert rlp_obj._cached_rlp is None 318 | 319 | # cache should be populated now. 320 | assert encode(rlp_obj, cache=True) == rlp_code 321 | assert rlp_obj._cached_rlp == rlp_code 322 | 323 | # cache should still be populated and encoding should used cached_rlp value 324 | rlp_obj._cached_rlp = b"test-uses-cache" 325 | assert encode(rlp_obj, cache=True) == b"test-uses-cache" 326 | 327 | obj_decoded = decode(rlp_code, sedes=rlp_obj.__class__) 328 | assert obj_decoded == rlp_obj 329 | assert obj_decoded._cached_rlp == rlp_code 330 | 331 | 332 | def test_list_of_serializable_decoding_rlp_caching(rlp_obj): 333 | rlp_obj_code = encode(rlp_obj, cache=False) 334 | L = [rlp_obj, rlp_obj] 335 | list_code = encode(L, cache=False) 336 | 337 | L2 = decode( 338 | list_code, sedes=List((type(rlp_obj), type(rlp_obj))), recursive_cache=True 339 | ) 340 | assert L2[0]._cached_rlp == rlp_obj_code 341 | assert L2[1]._cached_rlp == rlp_obj_code 342 | 343 | 344 | def test_serializable_basic_copy(type_1_a): 345 | n_type_1_a = type_1_a.copy() 346 | assert n_type_1_a == type_1_a 347 | assert n_type_1_a is not type_1_a 348 | 349 | 350 | def test_serializable_copy_with_nested_serializables(type_2): 351 | n_type_2 = type_2.copy() 352 | assert n_type_2 == type_2 353 | assert n_type_2 is not type_2 354 | 355 | assert n_type_2.field2_1 == type_2.field2_1 356 | assert n_type_2.field2_1 is not type_2.field2_1 357 | 358 | assert n_type_2.field2_2 == type_2.field2_2 359 | assert all(left == right for left, right in zip(n_type_2.field2_2, type_2.field2_2)) 360 | assert all( 361 | left is not right for left, right in zip(n_type_2.field2_2, type_2.field2_2) 362 | ) 363 | 364 | 365 | def test_serializable_build_changeset(type_1_a): 366 | with type_1_a.build_changeset() as changeset: 367 | # make changes to copy 368 | changeset.field1 = 1234 369 | changeset.field2 = b"arst" 370 | 371 | # check that the copy has the new field values 372 | assert changeset.field1 == 1234 373 | assert changeset.field2 == b"arst" 374 | 375 | n_type_1_a = changeset.commit() 376 | 377 | # check that the copy has the new field values 378 | assert n_type_1_a.field1 == 1234 379 | assert n_type_1_a.field2 == b"arst" 380 | 381 | # ensure the base object hasn't changed. 382 | assert type_1_a.field1 == 5 383 | assert type_1_a.field2 == b"a" 384 | 385 | 386 | def test_serializable_build_changeset_changeset_gets_decomissioned(type_1_a): 387 | with type_1_a.build_changeset() as changeset: 388 | changeset.field1 = 54321 389 | n_type_1_a = changeset.commit() 390 | 391 | # ensure that we also can't update the changeset 392 | with pytest.raises(AttributeError): 393 | changeset.field1 = 12345 394 | # ensure that we also can't read values from the changeset 395 | with pytest.raises(AttributeError): 396 | changeset.field1 397 | 398 | assert n_type_1_a.field1 == 54321 399 | 400 | 401 | def test_serializable_build_changeset_with_params(type_1_a): 402 | with type_1_a.build_changeset(1234) as changeset: 403 | assert changeset.field1 == 1234 404 | 405 | n_type_1_a = changeset.commit() 406 | assert n_type_1_a.field1 == 1234 407 | 408 | 409 | def test_serializable_build_changeset_using_open_close_api(type_1_a): 410 | changeset = type_1_a.build_changeset() 411 | changeset.open() 412 | 413 | # make changes to copy 414 | changeset.field1 = 1234 415 | changeset.field2 = b"arst" 416 | 417 | # check that the copy has the new field values 418 | assert changeset.field1 == 1234 419 | assert changeset.field2 == b"arst" 420 | 421 | n_type_1_a = changeset.build_rlp() 422 | 423 | # check that the copy has the new field values 424 | assert n_type_1_a.field1 == 1234 425 | assert n_type_1_a.field2 == b"arst" 426 | 427 | # ensure the base object hasn't changed. 428 | assert type_1_a.field1 == 5 429 | assert type_1_a.field2 == b"a" 430 | 431 | # check we can still access the unclosed changeset 432 | assert changeset.field1 == 1234 433 | 434 | changeset.close() 435 | 436 | with pytest.raises(AttributeError): 437 | assert changeset.field1 == 1234 438 | 439 | 440 | def test_serializable_with_duplicate_field_names_is_error(): 441 | msg1 = "duplicated in the `fields` declaration: field_a" 442 | with pytest.raises(TypeError, match=msg1): 443 | 444 | class ParentA(Serializable): 445 | fields = ( 446 | ("field_a", big_endian_int), 447 | ("field_c", big_endian_int), 448 | ("field_d", big_endian_int), 449 | ("field_a", big_endian_int), 450 | ) 451 | 452 | msg2 = "duplicated in the `fields` declaration: field_a,field_c" 453 | with pytest.raises(TypeError, match=msg2): 454 | 455 | class ParentB(Serializable): 456 | fields = ( 457 | ("field_a", big_endian_int), 458 | ("field_c", big_endian_int), 459 | ("field_d", big_endian_int), 460 | ("field_a", big_endian_int), 461 | ("field_c", big_endian_int), 462 | ) 463 | 464 | 465 | def test_serializable_inheritance_enforces_inclusion_of_parent_fields(): 466 | class Parent(Serializable): 467 | fields = ( 468 | ("field_a", big_endian_int), 469 | ("field_b", big_endian_int), 470 | ("field_c", big_endian_int), 471 | ("field_d", big_endian_int), 472 | ) 473 | 474 | with pytest.raises(TypeError, match="field_a,field_c"): 475 | 476 | class Child(Parent): 477 | fields = ( 478 | ("field_b", big_endian_int), 479 | ("field_d", big_endian_int), 480 | ) 481 | 482 | 483 | def test_serializable_single_inheritance_with_no_fields(): 484 | class Parent(Serializable): 485 | fields = ( 486 | ("field_a", big_endian_int), 487 | ("field_b", big_endian_int), 488 | ) 489 | 490 | class Child(Parent): 491 | pass 492 | 493 | parent = Parent(1, 2) 494 | assert parent.field_a == 1 495 | assert parent.field_b == 2 496 | assert Parent.serialize(parent) == [b"\x01", b"\x02"] 497 | 498 | child = Child(3, 4) 499 | assert child.field_a == 3 500 | assert child.field_b == 4 501 | assert Child.serialize(child) == [b"\x03", b"\x04"] 502 | 503 | 504 | def test_serializable_single_inheritance_with_fields(): 505 | class Parent(Serializable): 506 | fields = ( 507 | ("field_a", big_endian_int), 508 | ("field_b", big_endian_int), 509 | ) 510 | 511 | class Child(Parent): 512 | fields = ( 513 | ("field_a", big_endian_int), 514 | ("field_b", big_endian_int), 515 | ("field_c", big_endian_int), 516 | ) 517 | 518 | parent = Parent(1, 2) 519 | assert parent.field_a == 1 520 | assert parent.field_b == 2 521 | assert Parent.serialize(parent) == [b"\x01", b"\x02"] 522 | 523 | with pytest.raises(TypeError): 524 | # ensure that the fields don't somehow leak into the parent class. 525 | Parent(1, 2, 3) 526 | 527 | child = Child(3, 4, 5) 528 | assert child.field_a == 3 529 | assert child.field_b == 4 530 | assert child.field_c == 5 531 | assert Child.serialize(child) == [b"\x03", b"\x04", b"\x05"] 532 | 533 | 534 | def test_serializable_inheritance_with_sedes_overrides(): 535 | class Parent(Serializable): 536 | fields = ( 537 | ("field_a", big_endian_int), 538 | ("field_b", big_endian_int), 539 | ) 540 | 541 | class Child(Parent): 542 | fields = ( 543 | ("field_a", binary), 544 | ("field_b", binary), 545 | ("field_c", binary), 546 | ) 547 | 548 | parent = Parent(1, 2) 549 | assert parent.field_a == 1 550 | assert parent.field_b == 2 551 | assert Parent.serialize(parent) == [b"\x01", b"\x02"] 552 | 553 | child = Child(b"1", b"2", b"3") 554 | assert child.field_a == b"1" 555 | assert child.field_b == b"2" 556 | assert child.field_c == b"3" 557 | assert Child.serialize(child) == [b"1", b"2", b"3"] 558 | 559 | 560 | def test_serializable_multiple_inheritance_without_fields_declaration_is_error(): 561 | class ParentA(Serializable): 562 | fields = (("field_a", big_endian_int),) 563 | 564 | class ParentB(Serializable): 565 | fields = (("field_b", big_endian_int),) 566 | 567 | with pytest.raises(TypeError, match="explicit `fields` declaration"): 568 | 569 | class Child(ParentA, ParentB): 570 | pass 571 | 572 | 573 | def test_serializable_multiple_inheritance_allowed_with_explicit_fields(): 574 | class ParentA(Serializable): 575 | fields = (("field_a", big_endian_int),) 576 | 577 | class ParentB(Serializable): 578 | fields = (("field_b", big_endian_int),) 579 | 580 | # with same fields 581 | class ChildA(ParentA, ParentB): 582 | fields = ( 583 | ("field_a", big_endian_int), 584 | ("field_b", big_endian_int), 585 | ) 586 | 587 | # with extra fields 588 | class ChildB(ParentA, ParentB): 589 | fields = ( 590 | ("field_a", big_endian_int), 591 | ("field_b", big_endian_int), 592 | ("field_c", big_endian_int), 593 | ) 594 | 595 | 596 | def test_serializable_multiple_inheritance_requires_all_parent_fields(): 597 | class ParentA(Serializable): 598 | fields = (("field_a", big_endian_int),) 599 | 600 | class ParentB(Serializable): 601 | fields = (("field_b", big_endian_int),) 602 | 603 | with pytest.raises(TypeError, match="The following fields are missing: field_b"): 604 | 605 | class ChildA(ParentA, ParentB): 606 | fields = (("field_a", big_endian_int),) 607 | 608 | with pytest.raises(TypeError, match="The following fields are missing: field_a"): 609 | 610 | class ChildB(ParentA, ParentB): 611 | fields = (("field_b", big_endian_int),) 612 | 613 | with pytest.raises( 614 | TypeError, match="The following fields are missing: field_a,field_b" 615 | ): 616 | 617 | class ChildC(ParentA, ParentB): 618 | fields = ( 619 | ("field_c", big_endian_int), 620 | ("field_d", big_endian_int), 621 | ) 622 | 623 | 624 | @pytest.mark.parametrize( 625 | "name", 626 | ( 627 | "0_starts_with_digit", 628 | " starts_with_space", 629 | "$starts_with_dollar", 630 | "has_dollar_$_inside", 631 | "has spaces", 632 | ), 633 | ) 634 | def test_serializable_field_names_must_be_valid_identifiers(name): 635 | with pytest.raises( 636 | TypeError, match=f"not valid python identifiers: `{re.escape(name)}`" 637 | ): 638 | 639 | class Klass(Serializable): 640 | fields = ((name, big_endian_int),) 641 | 642 | 643 | def test_serializable_inheritance_from_base_with_no_fields(): 644 | """ 645 | ensure that we can create base classes of the base `Serializable` without 646 | declaring fields. 647 | """ 648 | 649 | class ExtendedSerializable(Serializable): 650 | pass 651 | 652 | class FurtherExtendedSerializable(ExtendedSerializable): 653 | pass 654 | -------------------------------------------------------------------------------- /tests/core/test_text_sedes.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from hypothesis import ( 4 | given, 5 | strategies as st, 6 | ) 7 | 8 | from rlp import ( 9 | DeserializationError, 10 | SerializationError, 11 | decode, 12 | encode, 13 | ) 14 | from rlp.sedes import ( 15 | Text, 16 | ) 17 | 18 | 19 | @pytest.mark.parametrize( 20 | "value,expected", 21 | ( 22 | ("", b""), 23 | ("asdf", b"asdf"), 24 | ("fdsa", b"fdsa"), 25 | ("�", b"\xef\xbf\xbd"), 26 | ("€", b"\xc2\x80"), 27 | ("ࠀ", b"\xe0\xa0\x80"), 28 | ("𐀀", b"\xf0\x90\x80\x80"), 29 | ("�����", b"\xef\xbf\xbd\xef\xbf\xbd\xef\xbf\xbd\xef\xbf\xbd\xef\xbf\xbd"), 30 | ( 31 | "������", 32 | b"\xef\xbf\xbd\xef\xbf\xbd\xef\xbf\xbd\xef\xbf\xbd\xef\xbf\xbd\xef\xbf\xbd", 33 | ), 34 | ), 35 | ) 36 | def test_simple_text_serialization(value, expected): 37 | sedes = Text() 38 | assert sedes.serialize(value) == expected 39 | 40 | 41 | @pytest.mark.parametrize( 42 | "value", 43 | ( 44 | b"", 45 | b"arst", 46 | 1, 47 | True, 48 | None, 49 | 1.0, 50 | bytearray(), 51 | ), 52 | ) 53 | def test_unserializable_text_sedes_values(value): 54 | sedes = Text() 55 | with pytest.raises(SerializationError): 56 | sedes.serialize(value) 57 | 58 | 59 | @pytest.mark.parametrize( 60 | "length,value,expected", 61 | ( 62 | (0, "", b""), 63 | (1, "a", b"a"), 64 | (1, "a", b"a"), 65 | (1, "�", b"\xef\xbf\xbd"), 66 | (1, "€", b"\xc2\x80"), 67 | (1, "ࠀ", b"\xe0\xa0\x80"), 68 | (1, "𐀀", b"\xf0\x90\x80\x80"), 69 | ), 70 | ) 71 | def test_fixed_length_text_serialization(length, value, expected): 72 | sedes = Text.fixed_length(length) 73 | assert sedes.serialize(value) == expected 74 | 75 | 76 | @pytest.mark.parametrize( 77 | "length,value", 78 | ( 79 | (1, ""), 80 | (0, "a"), 81 | (2, "a"), 82 | (2, "a"), 83 | (2, "�"), 84 | (4, "�"), # actual binary length 85 | (2, "€"), 86 | (4, "€"), # actual binary length 87 | (2, "ࠀ"), 88 | (4, "ࠀ"), # actual binary length 89 | (2, "𐀀"), 90 | (4, "𐀀"), # actual binary length 91 | (4, "�����"), 92 | (15, "�����"), # actual binary length 93 | (5, "������"), 94 | (18, "������"), # actual binary length 95 | ), 96 | ) 97 | def test_fixed_length_text_serialization_with_wrong_length(length, value): 98 | sedes = Text.fixed_length(length) 99 | with pytest.raises(SerializationError): 100 | sedes.serialize(value) 101 | 102 | 103 | @pytest.mark.parametrize( 104 | "min_length,max_length,value,expected", 105 | ( 106 | (0, 4, "", b""), 107 | (0, 4, "arst", b"arst"), 108 | (0, 4, "arst", b"arst"), 109 | (0, 4, "arst", b"arst"), 110 | (0, 1, "�", b"\xef\xbf\xbd"), 111 | (0, 1, "€", b"\xc2\x80"), 112 | (0, 1, "ࠀ", b"\xe0\xa0\x80"), 113 | (0, 1, "𐀀", b"\xf0\x90\x80\x80"), 114 | ( 115 | 0, 116 | 5, 117 | "�����", 118 | b"\xef\xbf\xbd\xef\xbf\xbd\xef\xbf\xbd\xef\xbf\xbd\xef\xbf\xbd", 119 | ), 120 | ( 121 | 0, 122 | 6, 123 | "������", 124 | b"\xef\xbf\xbd\xef\xbf\xbd\xef\xbf\xbd\xef\xbf\xbd\xef\xbf\xbd\xef\xbf\xbd", 125 | ), # noqa: E501 126 | ), 127 | ) 128 | def test_min_max_length_text_serialization(min_length, max_length, value, expected): 129 | sedes = Text(min_length=min_length, max_length=max_length) 130 | assert sedes.serialize(value) == expected 131 | 132 | 133 | @pytest.mark.parametrize( 134 | "min_length,max_length,value", 135 | ( 136 | (1, 4, ""), 137 | (5, 6, ""), 138 | (1, 3, "arst"), 139 | (5, 6, "arst"), 140 | (2, 3, "�"), 141 | (4, 5, "€"), 142 | (6, 7, "ࠀ"), 143 | (8, 9, "𐀀"), 144 | (6, 9, "�����"), 145 | (0, 4, "������"), 146 | ), 147 | ) 148 | def test_min_max_length_text_serialization_wrong_length(min_length, max_length, value): 149 | sedes = Text(min_length=min_length, max_length=max_length) 150 | with pytest.raises(SerializationError): 151 | sedes.serialize(value) 152 | 153 | 154 | @pytest.mark.parametrize( 155 | "serial,expected", 156 | ( 157 | (b"", ""), 158 | (b"asdf", "asdf"), 159 | (b"fdsa", "fdsa"), 160 | (b"\xef\xbf\xbd", "�"), 161 | (b"\xc2\x80", "€"), 162 | (b"\xe0\xa0\x80", "ࠀ"), 163 | (b"\xf0\x90\x80\x80", "𐀀"), 164 | (b"\xef\xbf\xbd\xef\xbf\xbd\xef\xbf\xbd\xef\xbf\xbd\xef\xbf\xbd", "�����"), 165 | ( 166 | b"\xef\xbf\xbd\xef\xbf\xbd\xef\xbf\xbd\xef\xbf\xbd\xef\xbf\xbd\xef\xbf\xbd", 167 | "������", 168 | ), 169 | ), 170 | ) 171 | def test_deserialization_text_sedes(serial, expected): 172 | sedes = Text() 173 | assert sedes.deserialize(serial) == expected 174 | 175 | 176 | def test_allow_empty_bypasses_length_checks(): 177 | sedes = Text.fixed_length(1, allow_empty=True) 178 | assert sedes.serialize("") == b"" 179 | 180 | with pytest.raises(SerializationError): 181 | sedes.serialize(b"12") 182 | 183 | 184 | @given(value=st.text()) 185 | def test_round_trip_text_encoding_and_decoding(value): 186 | sedes = Text() 187 | encoded = encode(value, sedes=sedes) 188 | actual = decode(encoded, sedes=sedes) 189 | assert actual == value 190 | 191 | 192 | def test_desirialization_of_text_encoding_failure(): 193 | sedes = Text() 194 | with pytest.raises(DeserializationError): 195 | sedes.deserialize(b"\xff") 196 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist= 3 | py{38,39,310,311,312,313}-core 4 | py{38,39,310,311,312,313}-lint 5 | py{38,39,310,311,312,313}-wheel 6 | py{38,39,310,311,312}-rust-backend 7 | windows-wheel 8 | docs 9 | 10 | [flake8] 11 | exclude=venv*,.tox,docs,build 12 | extend-ignore=E203 13 | max-line-length=88 14 | per-file-ignores=__init__.py:F401 15 | 16 | [blocklint] 17 | max_issue_threshold=1 18 | 19 | [testenv] 20 | usedevelop=True 21 | commands= 22 | core: pytest {posargs:tests/core} 23 | docs: make check-docs-ci 24 | rust-backend: pytest {posargs:tests/core} 25 | basepython= 26 | docs: python 27 | windows-wheel: python 28 | py38: python3.8 29 | py39: python3.9 30 | py310: python3.10 31 | py311: python3.11 32 | py312: python3.12 33 | py313: python3.13 34 | extras= 35 | test 36 | docs 37 | rust-backend: rust-backend 38 | allowlist_externals=make,pre-commit 39 | 40 | [testenv:py{38,39,310,311,312,313}-lint] 41 | deps=pre-commit 42 | commands= 43 | pre-commit install 44 | pre-commit run --all-files --show-diff-on-failure 45 | 46 | [testenv:py{38,39,310,311,312,313}-wheel] 47 | deps= 48 | wheel 49 | build[virtualenv] 50 | allowlist_externals= 51 | /bin/rm 52 | /bin/bash 53 | commands= 54 | python -m pip install --upgrade pip 55 | /bin/rm -rf build dist 56 | python -m build 57 | /bin/bash -c 'python -m pip install --upgrade "$(ls dist/rlp-*-py3-none-any.whl)" --progress-bar off' 58 | python -c "import rlp" 59 | skip_install=true 60 | 61 | [testenv:windows-wheel] 62 | deps= 63 | wheel 64 | build[virtualenv] 65 | allowlist_externals= 66 | bash.exe 67 | commands= 68 | python --version 69 | python -m pip install --upgrade pip 70 | bash.exe -c "rm -rf build dist" 71 | python -m build 72 | bash.exe -c 'python -m pip install --upgrade "$(ls dist/rlp-*-py3-none-any.whl)" --progress-bar off' 73 | python -c "import rlp" 74 | skip_install=true 75 | --------------------------------------------------------------------------------