├── .coveragerc ├── .editorconfig ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── config.yml │ └── feature_request.md ├── stale.yml └── workflows │ ├── build.yaml │ ├── deploy.yml │ ├── lint.yml │ └── tests.yml ├── .gitignore ├── .pre-commit-config.yaml ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.md ├── SECURITY.md ├── UPGRADE-v1.0.md ├── UPGRADE-v2.0.md ├── bin └── convert_documentation ├── docs ├── Makefile ├── _static │ └── .gitkeep ├── api │ └── index.rst ├── conf.py ├── execution │ ├── dataloader.rst │ ├── execute.rst │ ├── fileuploading.rst │ ├── index.rst │ ├── middleware.rst │ ├── queryvalidation.rst │ └── subscriptions.rst ├── index.rst ├── quickstart.rst ├── relay │ ├── connection.rst │ ├── index.rst │ ├── mutations.rst │ └── nodes.rst ├── requirements.txt ├── testing │ └── index.rst └── types │ ├── enums.rst │ ├── index.rst │ ├── interfaces.rst │ ├── list-and-nonnull.rst │ ├── mutations.rst │ ├── objecttypes.rst │ ├── scalars.rst │ ├── schema.rst │ └── unions.rst ├── examples ├── __init__.py ├── complex_example.py ├── context_example.py ├── simple_example.py ├── starwars │ ├── __init__.py │ ├── data.py │ ├── schema.py │ └── tests │ │ ├── __init__.py │ │ ├── test_query.py │ │ └── test_schema.py └── starwars_relay │ ├── __init__.py │ ├── data.py │ ├── schema.py │ └── tests │ ├── __init__.py │ ├── test_connections.py │ ├── test_mutation.py │ └── test_objectidentification.py ├── graphene ├── __init__.py ├── pyutils │ ├── __init__.py │ └── version.py ├── relay │ ├── __init__.py │ ├── connection.py │ ├── id_type.py │ ├── mutation.py │ ├── node.py │ └── tests │ │ ├── __init__.py │ │ ├── test_connection.py │ │ ├── test_connection_async.py │ │ ├── test_connection_query.py │ │ ├── test_custom_global_id.py │ │ ├── test_global_id.py │ │ ├── test_mutation.py │ │ ├── test_mutation_async.py │ │ ├── test_node.py │ │ └── test_node_custom.py ├── test │ └── __init__.py ├── tests │ ├── __init__.py │ └── issues │ │ ├── __init__.py │ │ ├── test_1293.py │ │ ├── test_1394.py │ │ ├── test_1419.py │ │ ├── test_313.py │ │ ├── test_356.py │ │ ├── test_425.py │ │ ├── test_490.py │ │ ├── test_720.py │ │ ├── test_881.py │ │ └── test_956.py ├── types │ ├── __init__.py │ ├── argument.py │ ├── base.py │ ├── base64.py │ ├── context.py │ ├── datetime.py │ ├── decimal.py │ ├── definitions.py │ ├── dynamic.py │ ├── enum.py │ ├── field.py │ ├── generic.py │ ├── inputfield.py │ ├── inputobjecttype.py │ ├── interface.py │ ├── json.py │ ├── mountedtype.py │ ├── mutation.py │ ├── objecttype.py │ ├── resolver.py │ ├── scalars.py │ ├── schema.py │ ├── structures.py │ ├── tests │ │ ├── __init__.py │ │ ├── conftest.py │ │ ├── test_argument.py │ │ ├── test_base.py │ │ ├── test_base64.py │ │ ├── test_datetime.py │ │ ├── test_decimal.py │ │ ├── test_definition.py │ │ ├── test_dynamic.py │ │ ├── test_enum.py │ │ ├── test_field.py │ │ ├── test_generic.py │ │ ├── test_inputfield.py │ │ ├── test_inputobjecttype.py │ │ ├── test_interface.py │ │ ├── test_json.py │ │ ├── test_mountedtype.py │ │ ├── test_mutation.py │ │ ├── test_objecttype.py │ │ ├── test_query.py │ │ ├── test_resolver.py │ │ ├── test_scalar.py │ │ ├── test_scalars_serialization.py │ │ ├── test_schema.py │ │ ├── test_structures.py │ │ ├── test_subscribe_async.py │ │ ├── test_type_map.py │ │ ├── test_union.py │ │ ├── test_uuid.py │ │ └── utils.py │ ├── union.py │ ├── unmountedtype.py │ ├── utils.py │ └── uuid.py ├── utils │ ├── __init__.py │ ├── crunch.py │ ├── dataloader.py │ ├── deduplicator.py │ ├── deprecated.py │ ├── get_unbound_function.py │ ├── is_introspection_key.py │ ├── module_loading.py │ ├── orderedtype.py │ ├── props.py │ ├── resolve_only_args.py │ ├── str_converters.py │ ├── subclass_with_meta.py │ ├── tests │ │ ├── __init__.py │ │ ├── test_crunch.py │ │ ├── test_dataloader.py │ │ ├── test_deduplicator.py │ │ ├── test_deprecated.py │ │ ├── test_module_loading.py │ │ ├── test_orderedtype.py │ │ ├── test_resolve_only_args.py │ │ ├── test_resolver_from_annotations.py │ │ ├── test_str_converters.py │ │ └── test_trim_docstring.py │ ├── thenables.py │ └── trim_docstring.py └── validation │ ├── __init__.py │ ├── depth_limit.py │ ├── disable_introspection.py │ └── tests │ ├── __init__.py │ ├── test_depth_limit_validator.py │ └── test_disable_introspection.py ├── mypy.ini ├── setup.cfg ├── setup.py └── tox.ini /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit = graphene/pyutils/*,*/tests/* 3 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.{py,rst,ini}] 12 | indent_style = space 13 | indent_size = 4 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: "\U0001F41B bug" 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Note: for support questions, please use stackoverflow**. This repository's issues are reserved for feature requests and bug reports. 11 | 12 | * **What is the current behavior?** 13 | 14 | 15 | 16 | * **If the current behavior is a bug, please provide the steps to reproduce and if possible a minimal demo of the problem** via 17 | a github repo, https://repl.it or similar. 18 | 19 | 20 | 21 | * **What is the expected behavior?** 22 | 23 | 24 | 25 | * **What is the motivation / use case for changing the behavior?** 26 | 27 | 28 | 29 | * **Please tell us about your environment:** 30 | 31 | - Version: 32 | - Platform: 33 | 34 | * **Other information** (e.g. detailed explanation, stacktraces, related issues, suggestions how to fix, links for us to have context, eg. stackoverflow) 35 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: "✨ enhancement" 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: false 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: false 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - pinned 8 | - security 9 | - 🐛 bug 10 | - 📖 documentation 11 | - 🙋 help wanted 12 | - ✨ enhancement 13 | - good first issue 14 | - work in progress 15 | # Label to use when marking an issue as stale 16 | staleLabel: wontfix 17 | # Comment to post when marking an issue as stale. Set to `false` to disable 18 | markComment: false 19 | # markComment: > 20 | # This issue has been automatically marked as stale because it has not had 21 | # recent activity. It will be closed if no further activity occurs. Thank you 22 | # for your contributions. 23 | # Comment to post when closing a stale issue. Set to `false` to disable 24 | closeComment: false 25 | -------------------------------------------------------------------------------- /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: 📦 Build 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v4 10 | - name: Set up Python 3.10 11 | uses: actions/setup-python@v5 12 | with: 13 | python-version: "3.10" 14 | - name: Install dependencies 15 | run: | 16 | python -m pip install --upgrade pip 17 | pip install build twine 18 | - name: Building package 19 | run: python3 -m build 20 | - name: Check package with Twine 21 | run: twine check dist/* 22 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: 🚀 Deploy to PyPI 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v4 14 | - name: Set up Python 3.10 15 | uses: actions/setup-python@v5 16 | with: 17 | python-version: "3.10" 18 | - name: Build wheel and source tarball 19 | run: | 20 | pip install wheel 21 | python setup.py sdist bdist_wheel 22 | - name: Publish a Python distribution to PyPI 23 | uses: pypa/gh-action-pypi-publish@v1.1.0 24 | with: 25 | user: __token__ 26 | password: ${{ secrets.pypi_password }} 27 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: 💅 Lint 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v4 11 | - name: Set up Python 3.10 12 | uses: actions/setup-python@v5 13 | with: 14 | python-version: "3.10" 15 | - name: Install dependencies 16 | run: | 17 | python -m pip install --upgrade pip 18 | pip install tox 19 | - name: Run lint 20 | run: tox 21 | env: 22 | TOXENV: pre-commit 23 | - name: Run mypy 24 | run: tox 25 | env: 26 | TOXENV: mypy 27 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: 📄 Tests 2 | on: 3 | push: 4 | branches: 5 | - master 6 | - '*.x' 7 | paths-ignore: 8 | - 'docs/**' 9 | - '*.md' 10 | - '*.rst' 11 | pull_request: 12 | branches: 13 | - master 14 | - '*.x' 15 | paths-ignore: 16 | - 'docs/**' 17 | - '*.md' 18 | - '*.rst' 19 | jobs: 20 | tests: 21 | # runs the test suite 22 | name: ${{ matrix.name }} 23 | runs-on: ${{ matrix.os }} 24 | strategy: 25 | fail-fast: false 26 | matrix: 27 | include: 28 | - {name: '3.13', python: '3.13', os: ubuntu-latest, tox: py313} 29 | - {name: '3.12', python: '3.12', os: ubuntu-latest, tox: py312} 30 | - {name: '3.11', python: '3.11', os: ubuntu-latest, tox: py311} 31 | - {name: '3.10', python: '3.10', os: ubuntu-latest, tox: py310} 32 | - {name: '3.9', python: '3.9', os: ubuntu-latest, tox: py39} 33 | - {name: '3.8', python: '3.8', os: ubuntu-latest, tox: py38} 34 | steps: 35 | - uses: actions/checkout@v4 36 | - uses: actions/setup-python@v5 37 | with: 38 | python-version: ${{ matrix.python }} 39 | 40 | - name: update pip 41 | run: | 42 | python -m pip install --upgrade pip 43 | pip install --upgrade setuptools wheel 44 | 45 | - name: get pip cache dir 46 | id: pip-cache 47 | run: echo "dir=$(pip cache dir)" >> $GITHUB_OUTPUT 48 | - name: cache pip dependencies 49 | uses: actions/cache@v3 50 | with: 51 | path: ${{ steps.pip-cache.outputs.dir }} 52 | key: pip|${{ runner.os }}|${{ matrix.python }}|${{ hashFiles('setup.py') }} 53 | - run: pip install tox 54 | - run: tox -e ${{ matrix.tox }} 55 | - name: Upload coverage.xml 56 | if: ${{ matrix.python == '3.10' }} 57 | uses: actions/upload-artifact@v4 58 | with: 59 | name: graphene-coverage 60 | path: coverage.xml 61 | if-no-files-found: error 62 | - name: Upload coverage.xml to codecov 63 | if: ${{ matrix.python == '3.10' }} 64 | uses: codecov/codecov-action@v4 65 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io 2 | 3 | ### Python ### 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | .pytest_cache 45 | nosetests.xml 46 | coverage.xml 47 | *.cover 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | 57 | # Sphinx documentation 58 | docs/_build/ 59 | 60 | # PyBuilder 61 | target/ 62 | 63 | # VirtualEnv 64 | .env 65 | .venv 66 | env/ 67 | venv/ 68 | 69 | # Typing 70 | .mypy_cache/ 71 | 72 | /tests/django.sqlite 73 | 74 | /graphene/index.json 75 | /graphene/meta.json 76 | 77 | /meta.json 78 | /index.json 79 | 80 | /docs/playground/graphene-js/pypyjs-release-nojit/ 81 | /docs/static/playground/lib 82 | 83 | /docs/static/playground 84 | 85 | # PyCharm 86 | .idea 87 | *.iml 88 | 89 | # Databases 90 | *.sqlite3 91 | .vscode 92 | .mypy_cache 93 | .ruff_cache 94 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | default_language_version: 2 | python: python3.10 3 | 4 | repos: 5 | - repo: https://github.com/pre-commit/pre-commit-hooks 6 | rev: v4.3.0 7 | hooks: 8 | - id: check-merge-conflict 9 | - id: check-json 10 | - id: check-yaml 11 | - id: debug-statements 12 | - id: end-of-file-fixer 13 | exclude: ^docs/.*$ 14 | - id: pretty-format-json 15 | args: 16 | - --autofix 17 | - id: trailing-whitespace 18 | exclude: README.md 19 | - repo: https://github.com/asottile/pyupgrade 20 | rev: v2.37.3 21 | hooks: 22 | - id: pyupgrade 23 | - repo: https://github.com/astral-sh/ruff-pre-commit 24 | # Ruff version. 25 | rev: v0.5.0 26 | hooks: 27 | - id: ruff 28 | - id: ruff-format 29 | args: [ --check ] 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-Present Syrus Akbary 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 | global-exclude tests/* 2 | recursive-exclude tests * 3 | recursive-exclude tests_py35 * 4 | recursive-exclude examples * 5 | include LICENSE 6 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: help 2 | help: 3 | @echo "Please use \`make ' where is one of" 4 | @grep -E '^\.PHONY: [a-zA-Z_-]+ .*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = "(: |##)"}; {printf "\033[36m%-30s\033[0m %s\n", $$2, $$3}' 5 | 6 | .PHONY: install-dev ## Install development dependencies 7 | install-dev: 8 | pip install -e ".[dev]" 9 | 10 | .PHONY: test ## Run tests 11 | test: 12 | py.test graphene examples 13 | 14 | .PHONY: docs ## Generate docs 15 | docs: install-dev 16 | cd docs && make install && make html 17 | 18 | .PHONY: docs-live ## Generate docs with live reloading 19 | docs-live: install-dev 20 | cd docs && make install && make livehtml 21 | 22 | .PHONY: format 23 | format: 24 | black graphene examples setup.py 25 | 26 | .PHONY: lint 27 | lint: 28 | flake8 graphene examples setup.py 29 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Support for security issues is currently provided for Graphene 3.0 and above. Support on earlier versions cannot be guaranteed by the maintainers of this library, but community PRs may be accepted in critical cases. 6 | The preferred mitigation strategy is via an upgrade to Graphene 3. 7 | 8 | | Version | Supported | 9 | | ------- | ------------------ | 10 | | 3.x | :white_check_mark: | 11 | | <3.x | :x: | 12 | 13 | ## Reporting a Vulnerability 14 | 15 | Please use responsible disclosure by contacting a core maintainer via Discord or E-Mail. 16 | -------------------------------------------------------------------------------- /bin/convert_documentation: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | pandoc README.md --from markdown --to rst -s -o README.rst 4 | -------------------------------------------------------------------------------- /docs/_static/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphql-python/graphene/82903263080b3b7f22c2ad84319584d7a3b1a1f6/docs/_static/.gitkeep -------------------------------------------------------------------------------- /docs/api/index.rst: -------------------------------------------------------------------------------- 1 | API Reference 2 | ============= 3 | 4 | Schema 5 | ------ 6 | 7 | .. autoclass:: graphene.types.schema.Schema 8 | :members: 9 | 10 | .. Uncomment sections / types as API documentation is fleshed out 11 | .. in each class 12 | 13 | Object types 14 | ------------ 15 | 16 | .. autoclass:: graphene.ObjectType 17 | 18 | .. autoclass:: graphene.InputObjectType 19 | 20 | .. autoclass:: graphene.Mutation 21 | :members: 22 | 23 | .. _fields-mounted-types: 24 | 25 | Fields (Mounted Types) 26 | ---------------------- 27 | 28 | .. autoclass:: graphene.Field 29 | 30 | .. autoclass:: graphene.Argument 31 | 32 | .. autoclass:: graphene.InputField 33 | 34 | Fields (Unmounted Types) 35 | ------------------------ 36 | 37 | .. autoclass:: graphene.types.unmountedtype.UnmountedType 38 | 39 | GraphQL Scalars 40 | --------------- 41 | 42 | .. autoclass:: graphene.Int() 43 | 44 | .. autoclass:: graphene.Float() 45 | 46 | .. autoclass:: graphene.String() 47 | 48 | .. autoclass:: graphene.Boolean() 49 | 50 | .. autoclass:: graphene.ID() 51 | 52 | Graphene Scalars 53 | ---------------- 54 | 55 | .. autoclass:: graphene.Date() 56 | 57 | .. autoclass:: graphene.DateTime() 58 | 59 | .. autoclass:: graphene.Time() 60 | 61 | .. autoclass:: graphene.Decimal() 62 | 63 | .. autoclass:: graphene.UUID() 64 | 65 | .. autoclass:: graphene.JSONString() 66 | 67 | .. autoclass:: graphene.Base64() 68 | 69 | Enum 70 | ---- 71 | 72 | .. autoclass:: graphene.Enum() 73 | 74 | Structures 75 | ---------- 76 | 77 | .. autoclass:: graphene.List 78 | 79 | .. autoclass:: graphene.NonNull 80 | 81 | Type Extension 82 | -------------- 83 | 84 | .. autoclass:: graphene.Interface() 85 | 86 | .. autoclass:: graphene.Union() 87 | 88 | Execution Metadata 89 | ------------------ 90 | 91 | .. autoclass:: graphene.ResolveInfo 92 | 93 | .. autoclass:: graphene.Context 94 | 95 | .. autoclass:: graphql.ExecutionResult 96 | 97 | .. Relay 98 | .. ----- 99 | 100 | .. .. autoclass:: graphene.Node 101 | 102 | .. .. autoclass:: graphene.GlobalID 103 | 104 | .. .. autoclass:: graphene.ClientIDMutation 105 | 106 | .. .. autoclass:: graphene.Connection 107 | 108 | .. .. autoclass:: graphene.ConnectionField 109 | 110 | .. .. autoclass:: graphene.PageInfo 111 | -------------------------------------------------------------------------------- /docs/execution/dataloader.rst: -------------------------------------------------------------------------------- 1 | Dataloader 2 | ========== 3 | 4 | DataLoader is a generic utility to be used as part of your application's 5 | data fetching layer to provide a simplified and consistent API over 6 | various remote data sources such as databases or web services via batching 7 | and caching. It is provided by a separate package `aiodataloader `. 8 | 9 | 10 | Batching 11 | -------- 12 | 13 | Batching is not an advanced feature, it's DataLoader's primary feature. 14 | Create loaders by providing a batch loading function. 15 | 16 | .. code:: python 17 | 18 | from aiodataloader import DataLoader 19 | 20 | class UserLoader(DataLoader): 21 | async def batch_load_fn(self, keys): 22 | # Here we call a function to return a user for each key in keys 23 | return [get_user(id=key) for key in keys] 24 | 25 | 26 | A batch loading async function accepts a list of keys, and returns a list of ``values``. 27 | 28 | 29 | ``DataLoader`` will coalesce all individual loads which occur within a 30 | single frame of execution (executed once the wrapping event loop is resolved) 31 | and then call your batch function with all requested keys. 32 | 33 | 34 | .. code:: python 35 | 36 | user_loader = UserLoader() 37 | 38 | user1 = await user_loader.load(1) 39 | user1_best_friend = await user_loader.load(user1.best_friend_id) 40 | 41 | user2 = await user_loader.load(2) 42 | user2_best_friend = await user_loader.load(user2.best_friend_id) 43 | 44 | 45 | A naive application may have issued *four* round-trips to a backend for the 46 | required information, but with ``DataLoader`` this application will make at most *two*. 47 | 48 | Note that loaded values are one-to-one with the keys and must have the same 49 | order. This means that if you load all values from a single query, you must 50 | make sure that you then order the query result for the results to match the keys: 51 | 52 | 53 | .. code:: python 54 | 55 | class UserLoader(DataLoader): 56 | async def batch_load_fn(self, keys): 57 | users = {user.id: user for user in User.objects.filter(id__in=keys)} 58 | return [users.get(user_id) for user_id in keys] 59 | 60 | 61 | ``DataLoader`` allows you to decouple unrelated parts of your application without 62 | sacrificing the performance of batch data-loading. While the loader presents 63 | an API that loads individual values, all concurrent requests will be coalesced 64 | and presented to your batch loading function. This allows your application to 65 | safely distribute data fetching requirements throughout your application and 66 | maintain minimal outgoing data requests. 67 | 68 | 69 | 70 | Using with Graphene 71 | ------------------- 72 | 73 | DataLoader pairs nicely well with Graphene/GraphQL. GraphQL fields are designed 74 | to be stand-alone functions. Without a caching or batching mechanism, it's easy 75 | for a naive GraphQL server to issue new database requests each time a field is resolved. 76 | 77 | Consider the following GraphQL request: 78 | 79 | 80 | .. code:: 81 | 82 | { 83 | me { 84 | name 85 | bestFriend { 86 | name 87 | } 88 | friends(first: 5) { 89 | name 90 | bestFriend { 91 | name 92 | } 93 | } 94 | } 95 | } 96 | 97 | 98 | If ``me``, ``bestFriend`` and ``friends`` each need to send a request to the backend, 99 | there could be at most 13 database requests! 100 | 101 | 102 | When using DataLoader, we could define the User type using our previous example with 103 | leaner code and at most 4 database requests, and possibly fewer if there are cache hits. 104 | 105 | 106 | .. code:: python 107 | 108 | class User(graphene.ObjectType): 109 | name = graphene.String() 110 | best_friend = graphene.Field(lambda: User) 111 | friends = graphene.List(lambda: User) 112 | 113 | async def resolve_best_friend(root, info): 114 | return await user_loader.load(root.best_friend_id) 115 | 116 | async def resolve_friends(root, info): 117 | return await user_loader.load_many(root.friend_ids) 118 | -------------------------------------------------------------------------------- /docs/execution/execute.rst: -------------------------------------------------------------------------------- 1 | .. _SchemaExecute: 2 | 3 | Executing a query 4 | ================= 5 | 6 | For executing a query against a schema, you can directly call the ``execute`` method on it. 7 | 8 | 9 | .. code:: python 10 | 11 | from graphene import Schema 12 | 13 | schema = Schema(...) 14 | result = schema.execute('{ name }') 15 | 16 | ``result`` represents the result of execution. ``result.data`` is the result of executing the query, ``result.errors`` is ``None`` if no errors occurred, and is a non-empty list if an error occurred. 17 | 18 | 19 | .. _SchemaExecuteContext: 20 | 21 | Context 22 | _______ 23 | 24 | You can pass context to a query via ``context``. 25 | 26 | 27 | .. code:: python 28 | 29 | from graphene import ObjectType, String, Schema 30 | 31 | class Query(ObjectType): 32 | name = String() 33 | 34 | def resolve_name(root, info): 35 | return info.context.get('name') 36 | 37 | schema = Schema(Query) 38 | result = schema.execute('{ name }', context={'name': 'Syrus'}) 39 | assert result.data['name'] == 'Syrus' 40 | 41 | 42 | Variables 43 | _________ 44 | 45 | You can pass variables to a query via ``variables``. 46 | 47 | 48 | .. code:: python 49 | 50 | from graphene import ObjectType, Field, ID, Schema 51 | 52 | class Query(ObjectType): 53 | user = Field(User, id=ID(required=True)) 54 | 55 | def resolve_user(root, info, id): 56 | return get_user_by_id(id) 57 | 58 | schema = Schema(Query) 59 | result = schema.execute( 60 | ''' 61 | query getUser($id: ID) { 62 | user(id: $id) { 63 | id 64 | firstName 65 | lastName 66 | } 67 | } 68 | ''', 69 | variables={'id': 12}, 70 | ) 71 | 72 | Root Value 73 | __________ 74 | 75 | Value used for :ref:`ResolverParamParent` in root queries and mutations can be overridden using ``root`` parameter. 76 | 77 | .. code:: python 78 | 79 | from graphene import ObjectType, Field, Schema 80 | 81 | class Query(ObjectType): 82 | me = Field(User) 83 | 84 | def resolve_user(root, info): 85 | return {'id': root.id, 'firstName': root.name} 86 | 87 | schema = Schema(Query) 88 | user_root = User(id=12, name='bob') 89 | result = schema.execute( 90 | ''' 91 | query getUser { 92 | user { 93 | id 94 | firstName 95 | lastName 96 | } 97 | } 98 | ''', 99 | root=user_root 100 | ) 101 | assert result.data['user']['id'] == user_root.id 102 | 103 | Operation Name 104 | ______________ 105 | 106 | If there are multiple operations defined in a query string, ``operation_name`` should be used to indicate which should be executed. 107 | 108 | .. code:: python 109 | 110 | from graphene import ObjectType, Field, Schema 111 | 112 | class Query(ObjectType): 113 | user = Field(User) 114 | 115 | def resolve_user(root, info): 116 | return get_user_by_id(12) 117 | 118 | schema = Schema(Query) 119 | query_string = ''' 120 | query getUserWithFirstName { 121 | user { 122 | id 123 | firstName 124 | lastName 125 | } 126 | } 127 | query getUserWithFullName { 128 | user { 129 | id 130 | fullName 131 | } 132 | } 133 | ''' 134 | result = schema.execute( 135 | query_string, 136 | operation_name='getUserWithFullName' 137 | ) 138 | assert result.data['user']['fullName'] 139 | -------------------------------------------------------------------------------- /docs/execution/fileuploading.rst: -------------------------------------------------------------------------------- 1 | File uploading 2 | ============== 3 | 4 | File uploading is not part of the official GraphQL spec yet and is not natively 5 | implemented in Graphene. 6 | 7 | If your server needs to support file uploading then you can use the library: `graphene-file-upload `_ which enhances Graphene to add file 8 | uploads and conforms to the unoffical GraphQL `multipart request spec `_. 9 | -------------------------------------------------------------------------------- /docs/execution/index.rst: -------------------------------------------------------------------------------- 1 | ========= 2 | Execution 3 | ========= 4 | 5 | .. toctree:: 6 | :maxdepth: 2 7 | 8 | execute 9 | middleware 10 | dataloader 11 | fileuploading 12 | subscriptions 13 | queryvalidation 14 | -------------------------------------------------------------------------------- /docs/execution/middleware.rst: -------------------------------------------------------------------------------- 1 | Middleware 2 | ========== 3 | 4 | You can use ``middleware`` to affect the evaluation of fields in your schema. 5 | 6 | A middleware is any object or function that responds to ``resolve(next_middleware, *args)``. 7 | 8 | Inside that method, it should either: 9 | 10 | - Send ``resolve`` to the next middleware to continue the evaluation; or 11 | - Return a value to end the evaluation early. 12 | 13 | 14 | Resolve arguments 15 | ----------------- 16 | 17 | Middlewares ``resolve`` is invoked with several arguments: 18 | 19 | - ``next`` represents the execution chain. Call ``next`` to continue evaluation. 20 | - ``root`` is the root value object passed throughout the query. 21 | - ``info`` is the resolver info. 22 | - ``args`` is the dict of arguments passed to the field. 23 | 24 | Example 25 | ------- 26 | 27 | This middleware only continues evaluation if the ``field_name`` is not ``'user'`` 28 | 29 | .. code:: python 30 | 31 | class AuthorizationMiddleware(object): 32 | def resolve(self, next, root, info, **args): 33 | if info.field_name == 'user': 34 | return None 35 | return next(root, info, **args) 36 | 37 | 38 | And then execute it with: 39 | 40 | .. code:: python 41 | 42 | result = schema.execute('THE QUERY', middleware=[AuthorizationMiddleware()]) 43 | 44 | If the ``middleware`` argument includes multiple middlewares, 45 | these middlewares will be executed bottom-up, i.e. from last to first. 46 | 47 | Functional example 48 | ------------------ 49 | 50 | Middleware can also be defined as a function. Here we define a middleware that 51 | logs the time it takes to resolve each field: 52 | 53 | .. code:: python 54 | 55 | from time import time as timer 56 | 57 | def timing_middleware(next, root, info, **args): 58 | start = timer() 59 | return_value = next(root, info, **args) 60 | duration = round((timer() - start) * 1000, 2) 61 | parent_type_name = root._meta.name if root and hasattr(root, '_meta') else '' 62 | logger.debug(f"{parent_type_name}.{info.field_name}: {duration} ms") 63 | return return_value 64 | 65 | 66 | And then execute it with: 67 | 68 | .. code:: python 69 | 70 | result = schema.execute('THE QUERY', middleware=[timing_middleware]) 71 | -------------------------------------------------------------------------------- /docs/execution/queryvalidation.rst: -------------------------------------------------------------------------------- 1 | Query Validation 2 | ================ 3 | GraphQL uses query validators to check if Query AST is valid and can be executed. Every GraphQL server implements 4 | standard query validators. For example, there is an validator that tests if queried field exists on queried type, that 5 | makes query fail with "Cannot query field on type" error if it doesn't. 6 | 7 | To help with common use cases, graphene provides a few validation rules out of the box. 8 | 9 | 10 | Depth limit Validator 11 | --------------------- 12 | The depth limit validator helps to prevent execution of malicious 13 | queries. It takes in the following arguments. 14 | 15 | - ``max_depth`` is the maximum allowed depth for any operation in a GraphQL document. 16 | - ``ignore`` Stops recursive depth checking based on a field name. Either a string or regexp to match the name, or a function that returns a boolean 17 | - ``callback`` Called each time validation runs. Receives an Object which is a map of the depths for each operation. 18 | 19 | Usage 20 | ----- 21 | 22 | Here is how you would implement depth-limiting on your schema. 23 | 24 | .. code:: python 25 | 26 | from graphql import validate, parse 27 | from graphene import ObjectType, Schema, String 28 | from graphene.validation import depth_limit_validator 29 | 30 | 31 | class MyQuery(ObjectType): 32 | name = String(required=True) 33 | 34 | 35 | schema = Schema(query=MyQuery) 36 | 37 | # queries which have a depth more than 20 38 | # will not be executed. 39 | 40 | validation_errors = validate( 41 | schema=schema.graphql_schema, 42 | document_ast=parse('THE QUERY'), 43 | rules=( 44 | depth_limit_validator( 45 | max_depth=20 46 | ), 47 | ) 48 | ) 49 | 50 | 51 | Disable Introspection 52 | --------------------- 53 | the disable introspection validation rule ensures that your schema cannot be introspected. 54 | This is a useful security measure in production environments. 55 | 56 | Usage 57 | ----- 58 | 59 | Here is how you would disable introspection for your schema. 60 | 61 | .. code:: python 62 | 63 | from graphql import validate, parse 64 | from graphene import ObjectType, Schema, String 65 | from graphene.validation import DisableIntrospection 66 | 67 | 68 | class MyQuery(ObjectType): 69 | name = String(required=True) 70 | 71 | 72 | schema = Schema(query=MyQuery) 73 | 74 | # introspection queries will not be executed. 75 | 76 | validation_errors = validate( 77 | schema=schema.graphql_schema, 78 | document_ast=parse('THE QUERY'), 79 | rules=( 80 | DisableIntrospection, 81 | ) 82 | ) 83 | 84 | 85 | Implementing custom validators 86 | ------------------------------ 87 | All custom query validators should extend the `ValidationRule `_ 88 | base class importable from the graphql.validation.rules module. Query validators are visitor classes. They are 89 | instantiated at the time of query validation with one required argument (context: ASTValidationContext). In order to 90 | perform validation, your validator class should define one or more of enter_* and leave_* methods. For possible 91 | enter/leave items as well as details on function documentation, please see contents of the visitor module. To make 92 | validation fail, you should call validator's report_error method with the instance of GraphQLError describing failure 93 | reason. Here is an example query validator that visits field definitions in GraphQL query and fails query validation 94 | if any of those fields are blacklisted: 95 | 96 | .. code:: python 97 | 98 | from graphql import GraphQLError 99 | from graphql.language import FieldNode 100 | from graphql.validation import ValidationRule 101 | 102 | 103 | my_blacklist = ( 104 | "disallowed_field", 105 | ) 106 | 107 | 108 | def is_blacklisted_field(field_name: str): 109 | return field_name.lower() in my_blacklist 110 | 111 | 112 | class BlackListRule(ValidationRule): 113 | def enter_field(self, node: FieldNode, *_args): 114 | field_name = node.name.value 115 | if not is_blacklisted_field(field_name): 116 | return 117 | 118 | self.report_error( 119 | GraphQLError( 120 | f"Cannot query '{field_name}': field is blacklisted.", node, 121 | ) 122 | ) 123 | 124 | -------------------------------------------------------------------------------- /docs/execution/subscriptions.rst: -------------------------------------------------------------------------------- 1 | .. _SchemaSubscription: 2 | 3 | Subscriptions 4 | ============= 5 | 6 | To create a subscription, you can directly call the ``subscribe`` method on the 7 | schema. This method is async and must be awaited. 8 | 9 | .. code:: python 10 | 11 | import asyncio 12 | from datetime import datetime 13 | from graphene import ObjectType, String, Schema, Field 14 | 15 | # Every schema requires a query. 16 | class Query(ObjectType): 17 | hello = String() 18 | 19 | def resolve_hello(root, info): 20 | return "Hello, world!" 21 | 22 | class Subscription(ObjectType): 23 | time_of_day = String() 24 | 25 | async def subscribe_time_of_day(root, info): 26 | while True: 27 | yield datetime.now().isoformat() 28 | await asyncio.sleep(1) 29 | 30 | schema = Schema(query=Query, subscription=Subscription) 31 | 32 | async def main(schema): 33 | subscription = 'subscription { timeOfDay }' 34 | result = await schema.subscribe(subscription) 35 | async for item in result: 36 | print(item.data['timeOfDay']) 37 | 38 | asyncio.run(main(schema)) 39 | 40 | The ``result`` is an async iterator which yields items in the same manner as a query. 41 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Graphene 2 | ======== 3 | 4 | Contents: 5 | 6 | .. toctree:: 7 | :maxdepth: 2 8 | 9 | quickstart 10 | types/index 11 | execution/index 12 | relay/index 13 | testing/index 14 | api/index 15 | 16 | .. _Integrations: 17 | 18 | Integrations 19 | ------------ 20 | 21 | * `Graphene-Django `_ (`source `_) 22 | * Flask-Graphql (`source `_) 23 | * `Graphene-SQLAlchemy `_ (`source `_) 24 | * `Graphene-Mongo `_ (`source `_) 25 | * `Starlette `_ (`source `_) 26 | * `FastAPI `_ (`source `_) 27 | -------------------------------------------------------------------------------- /docs/relay/connection.rst: -------------------------------------------------------------------------------- 1 | Connection 2 | ========== 3 | 4 | A connection is a vitaminized version of a List that provides ways of 5 | slicing and paginating through it. The way you create Connection types 6 | in ``graphene`` is using ``relay.Connection`` and ``relay.ConnectionField``. 7 | 8 | Quick example 9 | ------------- 10 | 11 | If we want to create a custom Connection on a given node, we have to subclass the 12 | ``Connection`` class. 13 | 14 | In the following example, ``extra`` will be an extra field in the connection, 15 | and ``other`` an extra field in the Connection Edge. 16 | 17 | .. code:: python 18 | 19 | class ShipConnection(Connection): 20 | extra = String() 21 | 22 | class Meta: 23 | node = Ship 24 | 25 | class Edge: 26 | other = String() 27 | 28 | The ``ShipConnection`` connection class, will have automatically a ``pageInfo`` field, 29 | and a ``edges`` field (which is a list of ``ShipConnection.Edge``). 30 | This ``Edge`` will have a ``node`` field linking to the specified node 31 | (in ``ShipConnection.Meta``) and the field ``other`` that we defined in the class. 32 | 33 | Connection Field 34 | ---------------- 35 | You can create connection fields in any Connection, in case any ObjectType 36 | that implements ``Node`` will have a default Connection. 37 | 38 | .. code:: python 39 | 40 | class Faction(graphene.ObjectType): 41 | name = graphene.String() 42 | ships = relay.ConnectionField(ShipConnection) 43 | 44 | def resolve_ships(root, info): 45 | return [] 46 | -------------------------------------------------------------------------------- /docs/relay/index.rst: -------------------------------------------------------------------------------- 1 | Relay 2 | ===== 3 | 4 | Graphene has complete support for `Relay`_ and offers some utils to make 5 | integration from Python easy. 6 | 7 | 8 | .. toctree:: 9 | :maxdepth: 2 10 | 11 | nodes 12 | connection 13 | mutations 14 | 15 | 16 | Useful links 17 | ------------ 18 | 19 | - `Getting started with Relay`_ 20 | - `Relay Global Identification Specification`_ 21 | - `Relay Cursor Connection Specification`_ 22 | 23 | .. _Relay: https://relay.dev/docs/guides/graphql-server-specification/ 24 | .. _Getting started with Relay: https://relay.dev/docs/getting-started/step-by-step-guide/ 25 | .. _Relay Global Identification Specification: https://relay.dev/graphql/objectidentification.htm 26 | .. _Relay Cursor Connection Specification: https://relay.dev/graphql/connections.htm 27 | -------------------------------------------------------------------------------- /docs/relay/mutations.rst: -------------------------------------------------------------------------------- 1 | Mutations 2 | ========= 3 | 4 | Most APIs don’t just allow you to read data, they also allow you to 5 | write. 6 | 7 | In GraphQL, this is done using mutations. Just like queries, 8 | Relay puts some additional requirements on mutations, but Graphene 9 | nicely manages that for you. All you need to do is make your mutation a 10 | subclass of ``relay.ClientIDMutation``. 11 | 12 | .. code:: python 13 | 14 | class IntroduceShip(relay.ClientIDMutation): 15 | 16 | class Input: 17 | ship_name = graphene.String(required=True) 18 | faction_id = graphene.String(required=True) 19 | 20 | ship = graphene.Field(Ship) 21 | faction = graphene.Field(Faction) 22 | 23 | @classmethod 24 | def mutate_and_get_payload(cls, root, info, **input): 25 | ship_name = input.ship_name 26 | faction_id = input.faction_id 27 | ship = create_ship(ship_name, faction_id) 28 | faction = get_faction(faction_id) 29 | return IntroduceShip(ship=ship, faction=faction) 30 | 31 | 32 | 33 | Accepting Files 34 | --------------- 35 | 36 | Mutations can also accept files, that's how it will work with different integrations: 37 | 38 | .. code:: python 39 | 40 | class UploadFile(graphene.ClientIDMutation): 41 | class Input: 42 | pass 43 | # nothing needed for uploading file 44 | 45 | # your return fields 46 | success = graphene.String() 47 | 48 | @classmethod 49 | def mutate_and_get_payload(cls, root, info, **input): 50 | # When using it in Django, context will be the request 51 | files = info.context.FILES 52 | # Or, if used in Flask, context will be the flask global request 53 | # files = context.files 54 | 55 | # do something with files 56 | 57 | return UploadFile(success=True) 58 | -------------------------------------------------------------------------------- /docs/relay/nodes.rst: -------------------------------------------------------------------------------- 1 | Nodes 2 | ===== 3 | 4 | A ``Node`` is an Interface provided by ``graphene.relay`` that contains 5 | a single field ``id`` (which is a ``ID!``). Any object that inherits 6 | from it has to implement a ``get_node`` method for retrieving a 7 | ``Node`` by an *id*. 8 | 9 | 10 | Quick example 11 | ------------- 12 | 13 | Example usage (taken from the `Starwars Relay example`_): 14 | 15 | .. code:: python 16 | 17 | class Ship(graphene.ObjectType): 18 | '''A ship in the Star Wars saga''' 19 | class Meta: 20 | interfaces = (relay.Node, ) 21 | 22 | name = graphene.String(description='The name of the ship.') 23 | 24 | @classmethod 25 | def get_node(cls, info, id): 26 | return get_ship(id) 27 | 28 | The ``id`` returned by the ``Ship`` type when you query it will be a 29 | scalar which contains enough info for the server to know its type and 30 | its id. 31 | 32 | For example, the instance ``Ship(id=1)`` will return ``U2hpcDox`` as the 33 | id when you query it (which is the base64 encoding of ``Ship:1``), and 34 | which could be useful later if we want to query a node by its id. 35 | 36 | 37 | Custom Nodes 38 | ------------ 39 | 40 | You can use the predefined ``relay.Node`` or you can subclass it, defining 41 | custom ways of how a node id is encoded (using the ``to_global_id`` method in the class) 42 | or how we can retrieve a Node given a encoded id (with the ``get_node_from_global_id`` method). 43 | 44 | Example of a custom node: 45 | 46 | .. code:: python 47 | 48 | class CustomNode(Node): 49 | 50 | class Meta: 51 | name = 'Node' 52 | 53 | @staticmethod 54 | def to_global_id(type_, id): 55 | return f"{type_}:{id}" 56 | 57 | @staticmethod 58 | def get_node_from_global_id(info, global_id, only_type=None): 59 | type_, id = global_id.split(':') 60 | if only_type: 61 | # We assure that the node type that we want to retrieve 62 | # is the same that was indicated in the field type 63 | assert type_ == only_type._meta.name, 'Received not compatible node.' 64 | 65 | if type_ == 'User': 66 | return get_user(id) 67 | elif type_ == 'Photo': 68 | return get_photo(id) 69 | 70 | 71 | The ``get_node_from_global_id`` method will be called when ``CustomNode.Field`` is resolved. 72 | 73 | 74 | Accessing node types 75 | -------------------- 76 | 77 | If we want to retrieve node instances from a ``global_id`` (scalar that identifies an instance by it's type name and id), 78 | we can simply do ``Node.get_node_from_global_id(info, global_id)``. 79 | 80 | In the case we want to restrict the instance retrieval to a specific type, we can do: 81 | ``Node.get_node_from_global_id(info, global_id, only_type=Ship)``. This will raise an error 82 | if the ``global_id`` doesn't correspond to a Ship type. 83 | 84 | 85 | Node Root field 86 | --------------- 87 | 88 | As is required in the `Relay specification`_, the server must implement 89 | a root field called ``node`` that returns a ``Node`` Interface. 90 | 91 | For this reason, ``graphene`` provides the field ``relay.Node.Field``, 92 | which links to any type in the Schema which implements ``Node``. 93 | Example usage: 94 | 95 | .. code:: python 96 | 97 | class Query(graphene.ObjectType): 98 | # Should be CustomNode.Field() if we want to use our custom Node 99 | node = relay.Node.Field() 100 | 101 | .. _Relay specification: https://facebook.github.io/relay/docs/graphql-relay-specification.html 102 | .. _Starwars Relay example: https://github.com/graphql-python/graphene/blob/master/examples/starwars_relay/schema.py 103 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | # Required library 2 | Sphinx==6.1.3 3 | sphinx-autobuild==2021.3.14 4 | # Docs template 5 | http://graphene-python.org/sphinx_graphene_theme.zip 6 | -------------------------------------------------------------------------------- /docs/testing/index.rst: -------------------------------------------------------------------------------- 1 | =================== 2 | Testing in Graphene 3 | =================== 4 | 5 | 6 | Automated testing is an extremely useful bug-killing tool for the modern developer. You can use a collection of tests – a test suite – to solve, or avoid, a number of problems: 7 | 8 | - When you’re writing new code, you can use tests to validate your code works as expected. 9 | - When you’re refactoring or modifying old code, you can use tests to ensure your changes haven’t affected your application’s behavior unexpectedly. 10 | 11 | Testing a GraphQL application is a complex task, because a GraphQL application is made of several layers of logic – schema definition, schema validation, permissions and field resolution. 12 | 13 | With Graphene test-execution framework and assorted utilities, you can simulate GraphQL requests, execute mutations, inspect your application’s output and generally verify your code is doing what it should be doing. 14 | 15 | 16 | Testing tools 17 | ------------- 18 | 19 | Graphene provides a small set of tools that come in handy when writing tests. 20 | 21 | 22 | Test Client 23 | ~~~~~~~~~~~ 24 | 25 | The test client is a Python class that acts as a dummy GraphQL client, allowing you to test your views and interact with your Graphene-powered application programmatically. 26 | 27 | Some of the things you can do with the test client are: 28 | 29 | - Simulate Queries and Mutations and observe the response. 30 | - Test that a given query request is rendered by a given Django template, with a template context that contains certain values. 31 | 32 | 33 | Overview and a quick example 34 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 35 | 36 | To use the test client, instantiate ``graphene.test.Client`` and retrieve GraphQL responses: 37 | 38 | 39 | .. code:: python 40 | 41 | from graphene.test import Client 42 | 43 | def test_hey(): 44 | client = Client(my_schema) 45 | executed = client.execute('''{ hey }''') 46 | assert executed == { 47 | 'data': { 48 | 'hey': 'hello!' 49 | } 50 | } 51 | 52 | 53 | Execute parameters 54 | ~~~~~~~~~~~~~~~~~~ 55 | 56 | You can also add extra keyword arguments to the ``execute`` method, such as 57 | ``context``, ``root``, ``variables``, ...: 58 | 59 | 60 | .. code:: python 61 | 62 | from graphene.test import Client 63 | 64 | def test_hey(): 65 | client = Client(my_schema) 66 | executed = client.execute('''{ hey }''', context={'user': 'Peter'}) 67 | assert executed == { 68 | 'data': { 69 | 'hey': 'hello Peter!' 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /docs/types/enums.rst: -------------------------------------------------------------------------------- 1 | Enums 2 | ===== 3 | 4 | An ``Enum`` is a special ``GraphQL`` type that represents a set of 5 | symbolic names (members) bound to unique, constant values. 6 | 7 | Definition 8 | ---------- 9 | 10 | You can create an ``Enum`` using classes: 11 | 12 | .. code:: python 13 | 14 | import graphene 15 | 16 | class Episode(graphene.Enum): 17 | NEWHOPE = 4 18 | EMPIRE = 5 19 | JEDI = 6 20 | 21 | But also using instances of Enum: 22 | 23 | .. code:: python 24 | 25 | Episode = graphene.Enum('Episode', [('NEWHOPE', 4), ('EMPIRE', 5), ('JEDI', 6)]) 26 | 27 | Value descriptions 28 | ------------------ 29 | 30 | It's possible to add a description to an enum value, for that the enum value 31 | needs to have the ``description`` property on it. 32 | 33 | .. code:: python 34 | 35 | class Episode(graphene.Enum): 36 | NEWHOPE = 4 37 | EMPIRE = 5 38 | JEDI = 6 39 | 40 | @property 41 | def description(self): 42 | if self == Episode.NEWHOPE: 43 | return 'New Hope Episode' 44 | return 'Other episode' 45 | 46 | 47 | Usage with Python Enums 48 | ----------------------- 49 | 50 | In case the Enums are already defined it's possible to reuse them using 51 | the ``Enum.from_enum`` function. 52 | 53 | .. code:: python 54 | 55 | graphene.Enum.from_enum(AlreadyExistingPyEnum) 56 | 57 | ``Enum.from_enum`` supports a ``description`` and ``deprecation_reason`` lambdas as input so 58 | you can add description etc. to your enum without changing the original: 59 | 60 | .. code:: python 61 | 62 | graphene.Enum.from_enum( 63 | AlreadyExistingPyEnum, 64 | description=lambda v: return 'foo' if v == AlreadyExistingPyEnum.Foo else 'bar' 65 | ) 66 | 67 | 68 | Notes 69 | ----- 70 | 71 | ``graphene.Enum`` uses |enum.Enum|_ internally (or a backport if 72 | that's not available) and can be used in a similar way, with the exception of 73 | member getters. 74 | 75 | In the Python ``Enum`` implementation you can access a member by initing the Enum. 76 | 77 | .. code:: python 78 | 79 | from enum import Enum 80 | 81 | class Color(Enum): 82 | RED = 1 83 | GREEN = 2 84 | BLUE = 3 85 | 86 | assert Color(1) == Color.RED 87 | 88 | 89 | However, in Graphene ``Enum`` you need to call `.get` to have the same effect: 90 | 91 | .. code:: python 92 | 93 | from graphene import Enum 94 | 95 | class Color(Enum): 96 | RED = 1 97 | GREEN = 2 98 | BLUE = 3 99 | 100 | assert Color.get(1) == Color.RED 101 | 102 | .. |enum.Enum| replace:: ``enum.Enum`` 103 | .. _enum.Enum: https://docs.python.org/3/library/enum.html 104 | -------------------------------------------------------------------------------- /docs/types/index.rst: -------------------------------------------------------------------------------- 1 | .. _TypesReference: 2 | 3 | =============== 4 | Types Reference 5 | =============== 6 | 7 | .. toctree:: 8 | :maxdepth: 1 9 | 10 | schema 11 | scalars 12 | list-and-nonnull 13 | objecttypes 14 | enums 15 | interfaces 16 | unions 17 | mutations 18 | -------------------------------------------------------------------------------- /docs/types/list-and-nonnull.rst: -------------------------------------------------------------------------------- 1 | Lists and Non-Null 2 | ================== 3 | 4 | Object types, scalars, and enums are the only kinds of types you can 5 | define in Graphene. But when you use the types in other parts of the 6 | schema, or in your query variable declarations, you can apply additional 7 | type modifiers that affect validation of those values. 8 | 9 | NonNull 10 | ------- 11 | 12 | .. code:: python 13 | 14 | import graphene 15 | 16 | class Character(graphene.ObjectType): 17 | name = graphene.NonNull(graphene.String) 18 | 19 | 20 | Here, we're using a ``String`` type and marking it as Non-Null by wrapping 21 | it using the ``NonNull`` class. This means that our server always expects 22 | to return a non-null value for this field, and if it ends up getting a 23 | null value that will actually trigger a GraphQL execution error, 24 | letting the client know that something has gone wrong. 25 | 26 | 27 | The previous ``NonNull`` code snippet is also equivalent to: 28 | 29 | .. code:: python 30 | 31 | import graphene 32 | 33 | class Character(graphene.ObjectType): 34 | name = graphene.String(required=True) 35 | 36 | 37 | List 38 | ---- 39 | 40 | .. code:: python 41 | 42 | import graphene 43 | 44 | class Character(graphene.ObjectType): 45 | appears_in = graphene.List(graphene.String) 46 | 47 | Lists work in a similar way: We can use a type modifier to mark a type as a 48 | ``List``, which indicates that this field will return a list of that type. 49 | It works the same for arguments, where the validation step will expect a list 50 | for that value. 51 | 52 | NonNull Lists 53 | ------------- 54 | 55 | By default items in a list will be considered nullable. To define a list without 56 | any nullable items the type needs to be marked as ``NonNull``. For example: 57 | 58 | .. code:: python 59 | 60 | import graphene 61 | 62 | class Character(graphene.ObjectType): 63 | appears_in = graphene.List(graphene.NonNull(graphene.String)) 64 | 65 | The above results in the type definition: 66 | 67 | .. code:: 68 | 69 | type Character { 70 | appearsIn: [String!] 71 | } 72 | -------------------------------------------------------------------------------- /docs/types/schema.rst: -------------------------------------------------------------------------------- 1 | Schema 2 | ====== 3 | 4 | A GraphQL **Schema** defines the types and relationships between **Fields** in your API. 5 | 6 | A Schema is created by supplying the root :ref:`ObjectType` of each operation, query (mandatory), mutation and subscription. 7 | 8 | Schema will collect all type definitions related to the root operations and then supply them to the validator and executor. 9 | 10 | .. code:: python 11 | 12 | my_schema = Schema( 13 | query=MyRootQuery, 14 | mutation=MyRootMutation, 15 | subscription=MyRootSubscription 16 | ) 17 | 18 | A Root Query is just a special :ref:`ObjectType` that defines the fields that are the entrypoint for your API. Root Mutation and Root Subscription are similar to Root Query, but for different operation types: 19 | 20 | * Query fetches data 21 | * Mutation changes data and retrieves the changes 22 | * Subscription sends changes to clients in real-time 23 | 24 | Review the `GraphQL documentation on Schema`_ for a brief overview of fields, schema and operations. 25 | 26 | .. _GraphQL documentation on Schema: https://graphql.org/learn/schema/ 27 | 28 | 29 | Querying 30 | -------- 31 | 32 | To query a schema, call the ``execute`` method on it. See :ref:`SchemaExecute` for more details. 33 | 34 | 35 | .. code:: python 36 | 37 | query_string = 'query whoIsMyBestFriend { myBestFriend { lastName } }' 38 | my_schema.execute(query_string) 39 | 40 | Types 41 | ----- 42 | 43 | There are some cases where the schema cannot access all of the types that we plan to have. 44 | For example, when a field returns an ``Interface``, the schema doesn't know about any of the 45 | implementations. 46 | 47 | In this case, we need to use the ``types`` argument when creating the Schema: 48 | 49 | 50 | .. code:: python 51 | 52 | my_schema = Schema( 53 | query=MyRootQuery, 54 | types=[SomeExtraObjectType, ] 55 | ) 56 | 57 | .. _SchemaAutoCamelCase: 58 | 59 | Auto camelCase field names 60 | -------------------------- 61 | 62 | By default all field and argument names (that are not 63 | explicitly set with the ``name`` arg) will be converted from 64 | ``snake_case`` to ``camelCase`` (as the API is usually being consumed by a js/mobile client) 65 | 66 | For example with the ObjectType the ``last_name`` field name is converted to ``lastName``: 67 | 68 | .. code:: python 69 | 70 | class Person(graphene.ObjectType): 71 | last_name = graphene.String() 72 | other_name = graphene.String(name='_other_Name') 73 | 74 | In case you don't want to apply this transformation, provide a ``name`` argument to the field constructor. 75 | ``other_name`` converts to ``_other_Name`` (without further transformations). 76 | 77 | Your query should look like: 78 | 79 | .. code:: 80 | 81 | { 82 | lastName 83 | _other_Name 84 | } 85 | 86 | 87 | To disable this behavior, set the ``auto_camelcase`` to ``False`` upon schema instantiation: 88 | 89 | .. code:: python 90 | 91 | my_schema = Schema( 92 | query=MyRootQuery, 93 | auto_camelcase=False, 94 | ) 95 | -------------------------------------------------------------------------------- /docs/types/unions.rst: -------------------------------------------------------------------------------- 1 | Unions 2 | ====== 3 | 4 | Union types are very similar to interfaces, but they don't get 5 | to specify any common fields between the types. 6 | 7 | The basics: 8 | 9 | - Each Union is a Python class that inherits from ``graphene.Union``. 10 | - Unions don't have any fields on it, just links to the possible ObjectTypes. 11 | 12 | Quick example 13 | ------------- 14 | 15 | This example model defines several ObjectTypes with their own fields. 16 | ``SearchResult`` is the implementation of ``Union`` of this object types. 17 | 18 | .. code:: python 19 | 20 | import graphene 21 | 22 | class Human(graphene.ObjectType): 23 | name = graphene.String() 24 | born_in = graphene.String() 25 | 26 | class Droid(graphene.ObjectType): 27 | name = graphene.String() 28 | primary_function = graphene.String() 29 | 30 | class Starship(graphene.ObjectType): 31 | name = graphene.String() 32 | length = graphene.Int() 33 | 34 | class SearchResult(graphene.Union): 35 | class Meta: 36 | types = (Human, Droid, Starship) 37 | 38 | 39 | Wherever we return a SearchResult type in our schema, we might get a Human, a Droid, or a Starship. 40 | Note that members of a union type need to be concrete object types; 41 | you can't create a union type out of interfaces or other unions. 42 | 43 | The above types have the following representation in a schema: 44 | 45 | .. code:: 46 | 47 | type Droid { 48 | name: String 49 | primaryFunction: String 50 | } 51 | 52 | type Human { 53 | name: String 54 | bornIn: String 55 | } 56 | 57 | type Ship { 58 | name: String 59 | length: Int 60 | } 61 | 62 | union SearchResult = Human | Droid | Starship 63 | 64 | -------------------------------------------------------------------------------- /examples/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphql-python/graphene/82903263080b3b7f22c2ad84319584d7a3b1a1f6/examples/__init__.py -------------------------------------------------------------------------------- /examples/complex_example.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | 3 | 4 | class GeoInput(graphene.InputObjectType): 5 | lat = graphene.Float(required=True) 6 | lng = graphene.Float(required=True) 7 | 8 | @property 9 | def latlng(self): 10 | return f"({self.lat},{self.lng})" 11 | 12 | 13 | class Address(graphene.ObjectType): 14 | latlng = graphene.String() 15 | 16 | 17 | class Query(graphene.ObjectType): 18 | address = graphene.Field(Address, geo=GeoInput(required=True)) 19 | 20 | def resolve_address(root, info, geo): 21 | return Address(latlng=geo.latlng) 22 | 23 | 24 | class CreateAddress(graphene.Mutation): 25 | class Arguments: 26 | geo = GeoInput(required=True) 27 | 28 | Output = Address 29 | 30 | def mutate(root, info, geo): 31 | return Address(latlng=geo.latlng) 32 | 33 | 34 | class Mutation(graphene.ObjectType): 35 | create_address = CreateAddress.Field() 36 | 37 | 38 | schema = graphene.Schema(query=Query, mutation=Mutation) 39 | query = """ 40 | query something{ 41 | address(geo: {lat:32.2, lng:12}) { 42 | latlng 43 | } 44 | } 45 | """ 46 | mutation = """ 47 | mutation addAddress{ 48 | createAddress(geo: {lat:32.2, lng:12}) { 49 | latlng 50 | } 51 | } 52 | """ 53 | 54 | 55 | def test_query(): 56 | result = schema.execute(query) 57 | assert not result.errors 58 | assert result.data == {"address": {"latlng": "(32.2,12.0)"}} 59 | 60 | 61 | def test_mutation(): 62 | result = schema.execute(mutation) 63 | assert not result.errors 64 | assert result.data == {"createAddress": {"latlng": "(32.2,12.0)"}} 65 | 66 | 67 | if __name__ == "__main__": 68 | result = schema.execute(query) 69 | print(result.data["address"]["latlng"]) 70 | -------------------------------------------------------------------------------- /examples/context_example.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | 3 | 4 | class User(graphene.ObjectType): 5 | id = graphene.ID() 6 | name = graphene.String() 7 | 8 | 9 | class Query(graphene.ObjectType): 10 | me = graphene.Field(User) 11 | 12 | def resolve_me(root, info): 13 | return info.context["user"] 14 | 15 | 16 | schema = graphene.Schema(query=Query) 17 | query = """ 18 | query something{ 19 | me { 20 | id 21 | name 22 | } 23 | } 24 | """ 25 | 26 | 27 | def test_query(): 28 | result = schema.execute(query, context={"user": User(id="1", name="Syrus")}) 29 | assert not result.errors 30 | assert result.data == {"me": {"id": "1", "name": "Syrus"}} 31 | 32 | 33 | if __name__ == "__main__": 34 | result = schema.execute(query, context={"user": User(id="X", name="Console")}) 35 | print(result.data["me"]) 36 | -------------------------------------------------------------------------------- /examples/simple_example.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | 3 | 4 | class Patron(graphene.ObjectType): 5 | id = graphene.ID() 6 | name = graphene.String() 7 | age = graphene.Int() 8 | 9 | 10 | class Query(graphene.ObjectType): 11 | patron = graphene.Field(Patron) 12 | 13 | def resolve_patron(root, info): 14 | return Patron(id=1, name="Syrus", age=27) 15 | 16 | 17 | schema = graphene.Schema(query=Query) 18 | query = """ 19 | query something{ 20 | patron { 21 | id 22 | name 23 | age 24 | } 25 | } 26 | """ 27 | 28 | 29 | def test_query(): 30 | result = schema.execute(query) 31 | assert not result.errors 32 | assert result.data == {"patron": {"id": "1", "name": "Syrus", "age": 27}} 33 | 34 | 35 | if __name__ == "__main__": 36 | result = schema.execute(query) 37 | print(result.data["patron"]) 38 | -------------------------------------------------------------------------------- /examples/starwars/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphql-python/graphene/82903263080b3b7f22c2ad84319584d7a3b1a1f6/examples/starwars/__init__.py -------------------------------------------------------------------------------- /examples/starwars/data.py: -------------------------------------------------------------------------------- 1 | human_data = {} 2 | droid_data = {} 3 | 4 | 5 | def setup(): 6 | from .schema import Human, Droid 7 | 8 | global human_data, droid_data 9 | luke = Human( 10 | id="1000", 11 | name="Luke Skywalker", 12 | friends=["1002", "1003", "2000", "2001"], 13 | appears_in=[4, 5, 6], 14 | home_planet="Tatooine", 15 | ) 16 | 17 | vader = Human( 18 | id="1001", 19 | name="Darth Vader", 20 | friends=["1004"], 21 | appears_in=[4, 5, 6], 22 | home_planet="Tatooine", 23 | ) 24 | 25 | han = Human( 26 | id="1002", 27 | name="Han Solo", 28 | friends=["1000", "1003", "2001"], 29 | appears_in=[4, 5, 6], 30 | home_planet=None, 31 | ) 32 | 33 | leia = Human( 34 | id="1003", 35 | name="Leia Organa", 36 | friends=["1000", "1002", "2000", "2001"], 37 | appears_in=[4, 5, 6], 38 | home_planet="Alderaan", 39 | ) 40 | 41 | tarkin = Human( 42 | id="1004", 43 | name="Wilhuff Tarkin", 44 | friends=["1001"], 45 | appears_in=[4], 46 | home_planet=None, 47 | ) 48 | 49 | human_data = { 50 | "1000": luke, 51 | "1001": vader, 52 | "1002": han, 53 | "1003": leia, 54 | "1004": tarkin, 55 | } 56 | 57 | c3po = Droid( 58 | id="2000", 59 | name="C-3PO", 60 | friends=["1000", "1002", "1003", "2001"], 61 | appears_in=[4, 5, 6], 62 | primary_function="Protocol", 63 | ) 64 | 65 | r2d2 = Droid( 66 | id="2001", 67 | name="R2-D2", 68 | friends=["1000", "1002", "1003"], 69 | appears_in=[4, 5, 6], 70 | primary_function="Astromech", 71 | ) 72 | 73 | droid_data = {"2000": c3po, "2001": r2d2} 74 | 75 | 76 | def get_character(id): 77 | return human_data.get(id) or droid_data.get(id) 78 | 79 | 80 | def get_friends(character): 81 | return map(get_character, character.friends) 82 | 83 | 84 | def get_hero(episode): 85 | if episode == 5: 86 | return human_data["1000"] 87 | return droid_data["2001"] 88 | 89 | 90 | def get_human(id): 91 | return human_data.get(id) 92 | 93 | 94 | def get_droid(id): 95 | return droid_data.get(id) 96 | -------------------------------------------------------------------------------- /examples/starwars/schema.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | 3 | from .data import get_character, get_droid, get_hero, get_human 4 | 5 | 6 | class Episode(graphene.Enum): 7 | NEWHOPE = 4 8 | EMPIRE = 5 9 | JEDI = 6 10 | 11 | 12 | class Character(graphene.Interface): 13 | id = graphene.ID() 14 | name = graphene.String() 15 | friends = graphene.List(lambda: Character) 16 | appears_in = graphene.List(Episode) 17 | 18 | def resolve_friends(self, info): 19 | # The character friends is a list of strings 20 | return [get_character(f) for f in self.friends] 21 | 22 | 23 | class Human(graphene.ObjectType): 24 | class Meta: 25 | interfaces = (Character,) 26 | 27 | home_planet = graphene.String() 28 | 29 | 30 | class Droid(graphene.ObjectType): 31 | class Meta: 32 | interfaces = (Character,) 33 | 34 | primary_function = graphene.String() 35 | 36 | 37 | class Query(graphene.ObjectType): 38 | hero = graphene.Field(Character, episode=Episode()) 39 | human = graphene.Field(Human, id=graphene.String()) 40 | droid = graphene.Field(Droid, id=graphene.String()) 41 | 42 | def resolve_hero(root, info, episode=None): 43 | return get_hero(episode) 44 | 45 | def resolve_human(root, info, id): 46 | return get_human(id) 47 | 48 | def resolve_droid(root, info, id): 49 | return get_droid(id) 50 | 51 | 52 | schema = graphene.Schema(query=Query) 53 | -------------------------------------------------------------------------------- /examples/starwars/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphql-python/graphene/82903263080b3b7f22c2ad84319584d7a3b1a1f6/examples/starwars/tests/__init__.py -------------------------------------------------------------------------------- /examples/starwars/tests/test_schema.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphql-python/graphene/82903263080b3b7f22c2ad84319584d7a3b1a1f6/examples/starwars/tests/test_schema.py -------------------------------------------------------------------------------- /examples/starwars_relay/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphql-python/graphene/82903263080b3b7f22c2ad84319584d7a3b1a1f6/examples/starwars_relay/__init__.py -------------------------------------------------------------------------------- /examples/starwars_relay/data.py: -------------------------------------------------------------------------------- 1 | data = {} 2 | 3 | 4 | def setup(): 5 | global data 6 | 7 | from .schema import Ship, Faction 8 | 9 | xwing = Ship(id="1", name="X-Wing") 10 | 11 | ywing = Ship(id="2", name="Y-Wing") 12 | 13 | awing = Ship(id="3", name="A-Wing") 14 | 15 | # Yeah, technically it's Corellian. But it flew in the service of the rebels, 16 | # so for the purposes of this demo it's a rebel ship. 17 | falcon = Ship(id="4", name="Millennium Falcon") 18 | 19 | homeOne = Ship(id="5", name="Home One") 20 | 21 | tieFighter = Ship(id="6", name="TIE Fighter") 22 | 23 | tieInterceptor = Ship(id="7", name="TIE Interceptor") 24 | 25 | executor = Ship(id="8", name="Executor") 26 | 27 | rebels = Faction( 28 | id="1", name="Alliance to Restore the Republic", ships=["1", "2", "3", "4", "5"] 29 | ) 30 | 31 | empire = Faction(id="2", name="Galactic Empire", ships=["6", "7", "8"]) 32 | 33 | data = { 34 | "Faction": {"1": rebels, "2": empire}, 35 | "Ship": { 36 | "1": xwing, 37 | "2": ywing, 38 | "3": awing, 39 | "4": falcon, 40 | "5": homeOne, 41 | "6": tieFighter, 42 | "7": tieInterceptor, 43 | "8": executor, 44 | }, 45 | } 46 | 47 | 48 | def create_ship(ship_name, faction_id): 49 | from .schema import Ship 50 | 51 | next_ship = len(data["Ship"].keys()) + 1 52 | new_ship = Ship(id=str(next_ship), name=ship_name) 53 | data["Ship"][new_ship.id] = new_ship 54 | data["Faction"][faction_id].ships.append(new_ship.id) 55 | return new_ship 56 | 57 | 58 | def get_ship(_id): 59 | return data["Ship"][_id] 60 | 61 | 62 | def get_faction(_id): 63 | return data["Faction"][_id] 64 | 65 | 66 | def get_rebels(): 67 | return get_faction("1") 68 | 69 | 70 | def get_empire(): 71 | return get_faction("2") 72 | -------------------------------------------------------------------------------- /examples/starwars_relay/schema.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | from graphene import relay 3 | 4 | from .data import create_ship, get_empire, get_faction, get_rebels, get_ship 5 | 6 | 7 | class Ship(graphene.ObjectType): 8 | """A ship in the Star Wars saga""" 9 | 10 | class Meta: 11 | interfaces = (relay.Node,) 12 | 13 | name = graphene.String(description="The name of the ship.") 14 | 15 | @classmethod 16 | def get_node(cls, info, id): 17 | return get_ship(id) 18 | 19 | 20 | class ShipConnection(relay.Connection): 21 | class Meta: 22 | node = Ship 23 | 24 | 25 | class Faction(graphene.ObjectType): 26 | """A faction in the Star Wars saga""" 27 | 28 | class Meta: 29 | interfaces = (relay.Node,) 30 | 31 | name = graphene.String(description="The name of the faction.") 32 | ships = relay.ConnectionField( 33 | ShipConnection, description="The ships used by the faction." 34 | ) 35 | 36 | def resolve_ships(self, info, **args): 37 | # Transform the instance ship_ids into real instances 38 | return [get_ship(ship_id) for ship_id in self.ships] 39 | 40 | @classmethod 41 | def get_node(cls, info, id): 42 | return get_faction(id) 43 | 44 | 45 | class IntroduceShip(relay.ClientIDMutation): 46 | class Input: 47 | ship_name = graphene.String(required=True) 48 | faction_id = graphene.String(required=True) 49 | 50 | ship = graphene.Field(Ship) 51 | faction = graphene.Field(Faction) 52 | 53 | @classmethod 54 | def mutate_and_get_payload( 55 | cls, root, info, ship_name, faction_id, client_mutation_id=None 56 | ): 57 | ship = create_ship(ship_name, faction_id) 58 | faction = get_faction(faction_id) 59 | return IntroduceShip(ship=ship, faction=faction) 60 | 61 | 62 | class Query(graphene.ObjectType): 63 | rebels = graphene.Field(Faction) 64 | empire = graphene.Field(Faction) 65 | node = relay.Node.Field() 66 | 67 | def resolve_rebels(root, info): 68 | return get_rebels() 69 | 70 | def resolve_empire(root, info): 71 | return get_empire() 72 | 73 | 74 | class Mutation(graphene.ObjectType): 75 | introduce_ship = IntroduceShip.Field() 76 | 77 | 78 | schema = graphene.Schema(query=Query, mutation=Mutation) 79 | -------------------------------------------------------------------------------- /examples/starwars_relay/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphql-python/graphene/82903263080b3b7f22c2ad84319584d7a3b1a1f6/examples/starwars_relay/tests/__init__.py -------------------------------------------------------------------------------- /examples/starwars_relay/tests/test_connections.py: -------------------------------------------------------------------------------- 1 | from graphene.test import Client 2 | 3 | from ..data import setup 4 | from ..schema import schema 5 | 6 | setup() 7 | 8 | client = Client(schema) 9 | 10 | 11 | def test_correct_fetch_first_ship_rebels(): 12 | result = client.execute(""" 13 | query RebelsShipsQuery { 14 | rebels { 15 | name, 16 | ships(first: 1) { 17 | pageInfo { 18 | startCursor 19 | endCursor 20 | hasNextPage 21 | hasPreviousPage 22 | } 23 | edges { 24 | cursor 25 | node { 26 | name 27 | } 28 | } 29 | } 30 | } 31 | } 32 | """) 33 | assert result == { 34 | "data": { 35 | "rebels": { 36 | "name": "Alliance to Restore the Republic", 37 | "ships": { 38 | "pageInfo": { 39 | "startCursor": "YXJyYXljb25uZWN0aW9uOjA=", 40 | "endCursor": "YXJyYXljb25uZWN0aW9uOjA=", 41 | "hasNextPage": True, 42 | "hasPreviousPage": False, 43 | }, 44 | "edges": [ 45 | { 46 | "cursor": "YXJyYXljb25uZWN0aW9uOjA=", 47 | "node": {"name": "X-Wing"}, 48 | } 49 | ], 50 | }, 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /examples/starwars_relay/tests/test_mutation.py: -------------------------------------------------------------------------------- 1 | from graphene.test import Client 2 | 3 | from ..data import setup 4 | from ..schema import schema 5 | 6 | setup() 7 | 8 | client = Client(schema) 9 | 10 | 11 | def test_mutations(): 12 | result = client.execute(""" 13 | mutation MyMutation { 14 | introduceShip(input:{clientMutationId:"abc", shipName: "Peter", factionId: "1"}) { 15 | ship { 16 | id 17 | name 18 | } 19 | faction { 20 | name 21 | ships { 22 | edges { 23 | node { 24 | id 25 | name 26 | } 27 | } 28 | } 29 | } 30 | } 31 | } 32 | """) 33 | assert result == { 34 | "data": { 35 | "introduceShip": { 36 | "ship": {"id": "U2hpcDo5", "name": "Peter"}, 37 | "faction": { 38 | "name": "Alliance to Restore the Republic", 39 | "ships": { 40 | "edges": [ 41 | {"node": {"id": "U2hpcDox", "name": "X-Wing"}}, 42 | {"node": {"id": "U2hpcDoy", "name": "Y-Wing"}}, 43 | {"node": {"id": "U2hpcDoz", "name": "A-Wing"}}, 44 | {"node": {"id": "U2hpcDo0", "name": "Millennium Falcon"}}, 45 | {"node": {"id": "U2hpcDo1", "name": "Home One"}}, 46 | {"node": {"id": "U2hpcDo5", "name": "Peter"}}, 47 | ] 48 | }, 49 | }, 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /graphene/__init__.py: -------------------------------------------------------------------------------- 1 | from .pyutils.version import get_version 2 | from .relay import ( 3 | BaseGlobalIDType, 4 | ClientIDMutation, 5 | Connection, 6 | ConnectionField, 7 | DefaultGlobalIDType, 8 | GlobalID, 9 | Node, 10 | PageInfo, 11 | SimpleGlobalIDType, 12 | UUIDGlobalIDType, 13 | is_node, 14 | ) 15 | from .types import ( 16 | ID, 17 | UUID, 18 | Argument, 19 | Base64, 20 | BigInt, 21 | Boolean, 22 | Context, 23 | Date, 24 | DateTime, 25 | Decimal, 26 | Dynamic, 27 | Enum, 28 | Field, 29 | Float, 30 | InputField, 31 | InputObjectType, 32 | Int, 33 | Interface, 34 | JSONString, 35 | List, 36 | Mutation, 37 | NonNull, 38 | ObjectType, 39 | ResolveInfo, 40 | Scalar, 41 | Schema, 42 | String, 43 | Time, 44 | Union, 45 | ) 46 | from .utils.module_loading import lazy_import 47 | from .utils.resolve_only_args import resolve_only_args 48 | 49 | VERSION = (3, 4, 3, "final", 0) 50 | 51 | 52 | __version__ = get_version(VERSION) 53 | 54 | __all__ = [ 55 | "__version__", 56 | "Argument", 57 | "Base64", 58 | "BigInt", 59 | "BaseGlobalIDType", 60 | "Boolean", 61 | "ClientIDMutation", 62 | "Connection", 63 | "ConnectionField", 64 | "Context", 65 | "Date", 66 | "DateTime", 67 | "Decimal", 68 | "DefaultGlobalIDType", 69 | "Dynamic", 70 | "Enum", 71 | "Field", 72 | "Float", 73 | "GlobalID", 74 | "ID", 75 | "InputField", 76 | "InputObjectType", 77 | "Int", 78 | "Interface", 79 | "JSONString", 80 | "List", 81 | "Mutation", 82 | "Node", 83 | "NonNull", 84 | "ObjectType", 85 | "PageInfo", 86 | "ResolveInfo", 87 | "Scalar", 88 | "Schema", 89 | "SimpleGlobalIDType", 90 | "String", 91 | "Time", 92 | "Union", 93 | "UUID", 94 | "UUIDGlobalIDType", 95 | "is_node", 96 | "lazy_import", 97 | "resolve_only_args", 98 | ] 99 | -------------------------------------------------------------------------------- /graphene/pyutils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphql-python/graphene/82903263080b3b7f22c2ad84319584d7a3b1a1f6/graphene/pyutils/__init__.py -------------------------------------------------------------------------------- /graphene/pyutils/version.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import os 3 | import subprocess 4 | 5 | 6 | def get_version(version=None): 7 | "Returns a PEP 440-compliant version number from VERSION." 8 | version = get_complete_version(version) 9 | 10 | # Now build the two parts of the version number: 11 | # main = X.Y[.Z] 12 | # sub = .devN - for pre-alpha releases 13 | # | {a|b|rc}N - for alpha, beta, and rc releases 14 | 15 | main = get_main_version(version) 16 | 17 | sub = "" 18 | if version[3] == "alpha" and version[4] == 0: 19 | git_changeset = get_git_changeset() 20 | sub = ".dev%s" % git_changeset if git_changeset else ".dev" 21 | elif version[3] != "final": 22 | mapping = {"alpha": "a", "beta": "b", "rc": "rc"} 23 | sub = mapping[version[3]] + str(version[4]) 24 | 25 | return str(main + sub) 26 | 27 | 28 | def get_main_version(version=None): 29 | "Returns main version (X.Y[.Z]) from VERSION." 30 | version = get_complete_version(version) 31 | parts = 2 if version[2] == 0 else 3 32 | return ".".join(str(x) for x in version[:parts]) 33 | 34 | 35 | def get_complete_version(version=None): 36 | """Returns a tuple of the graphene version. If version argument is non-empty, 37 | then checks for correctness of the tuple provided. 38 | """ 39 | if version is None: 40 | from graphene import VERSION as version 41 | else: 42 | assert len(version) == 5 43 | assert version[3] in ("alpha", "beta", "rc", "final") 44 | 45 | return version 46 | 47 | 48 | def get_docs_version(version=None): 49 | version = get_complete_version(version) 50 | if version[3] != "final": 51 | return "dev" 52 | else: 53 | return "%d.%d" % version[:2] 54 | 55 | 56 | def get_git_changeset(): 57 | """Returns a numeric identifier of the latest git changeset. 58 | The result is the UTC timestamp of the changeset in YYYYMMDDHHMMSS format. 59 | This value isn't guaranteed to be unique, but collisions are very unlikely, 60 | so it's sufficient for generating the development version numbers. 61 | """ 62 | repo_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 63 | try: 64 | git_log = subprocess.Popen( 65 | "git log --pretty=format:%ct --quiet -1 HEAD", 66 | stdout=subprocess.PIPE, 67 | stderr=subprocess.PIPE, 68 | shell=True, 69 | cwd=repo_dir, 70 | universal_newlines=True, 71 | ) 72 | timestamp = git_log.communicate()[0] 73 | timestamp = datetime.datetime.utcfromtimestamp(int(timestamp)) 74 | except Exception: 75 | return None 76 | return timestamp.strftime("%Y%m%d%H%M%S") 77 | -------------------------------------------------------------------------------- /graphene/relay/__init__.py: -------------------------------------------------------------------------------- 1 | from .node import Node, is_node, GlobalID 2 | from .mutation import ClientIDMutation 3 | from .connection import Connection, ConnectionField, PageInfo 4 | from .id_type import ( 5 | BaseGlobalIDType, 6 | DefaultGlobalIDType, 7 | SimpleGlobalIDType, 8 | UUIDGlobalIDType, 9 | ) 10 | 11 | __all__ = [ 12 | "BaseGlobalIDType", 13 | "ClientIDMutation", 14 | "Connection", 15 | "ConnectionField", 16 | "DefaultGlobalIDType", 17 | "GlobalID", 18 | "Node", 19 | "PageInfo", 20 | "SimpleGlobalIDType", 21 | "UUIDGlobalIDType", 22 | "is_node", 23 | ] 24 | -------------------------------------------------------------------------------- /graphene/relay/id_type.py: -------------------------------------------------------------------------------- 1 | from graphql_relay import from_global_id, to_global_id 2 | 3 | from ..types import ID, UUID 4 | from ..types.base import BaseType 5 | 6 | from typing import Type 7 | 8 | 9 | class BaseGlobalIDType: 10 | """ 11 | Base class that define the required attributes/method for a type. 12 | """ 13 | 14 | graphene_type: Type[BaseType] = ID 15 | 16 | @classmethod 17 | def resolve_global_id(cls, info, global_id): 18 | # return _type, _id 19 | raise NotImplementedError 20 | 21 | @classmethod 22 | def to_global_id(cls, _type, _id): 23 | # return _id 24 | raise NotImplementedError 25 | 26 | 27 | class DefaultGlobalIDType(BaseGlobalIDType): 28 | """ 29 | Default global ID type: base64 encoded version of ": ". 30 | """ 31 | 32 | graphene_type = ID 33 | 34 | @classmethod 35 | def resolve_global_id(cls, info, global_id): 36 | try: 37 | _type, _id = from_global_id(global_id) 38 | if not _type: 39 | raise ValueError("Invalid Global ID") 40 | return _type, _id 41 | except Exception as e: 42 | raise Exception( 43 | f'Unable to parse global ID "{global_id}". ' 44 | 'Make sure it is a base64 encoded string in the format: "TypeName:id". ' 45 | f"Exception message: {e}" 46 | ) 47 | 48 | @classmethod 49 | def to_global_id(cls, _type, _id): 50 | return to_global_id(_type, _id) 51 | 52 | 53 | class SimpleGlobalIDType(BaseGlobalIDType): 54 | """ 55 | Simple global ID type: simply the id of the object. 56 | To be used carefully as the user is responsible for ensuring that the IDs are indeed global 57 | (otherwise it could cause request caching issues). 58 | """ 59 | 60 | graphene_type = ID 61 | 62 | @classmethod 63 | def resolve_global_id(cls, info, global_id): 64 | _type = info.return_type.graphene_type._meta.name 65 | return _type, global_id 66 | 67 | @classmethod 68 | def to_global_id(cls, _type, _id): 69 | return _id 70 | 71 | 72 | class UUIDGlobalIDType(BaseGlobalIDType): 73 | """ 74 | UUID global ID type. 75 | By definition UUID are global so they are used as they are. 76 | """ 77 | 78 | graphene_type = UUID 79 | 80 | @classmethod 81 | def resolve_global_id(cls, info, global_id): 82 | _type = info.return_type.graphene_type._meta.name 83 | return _type, global_id 84 | 85 | @classmethod 86 | def to_global_id(cls, _type, _id): 87 | return _id 88 | -------------------------------------------------------------------------------- /graphene/relay/mutation.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from ..types import Field, InputObjectType, String 4 | from ..types.mutation import Mutation 5 | from ..utils.thenables import maybe_thenable 6 | 7 | 8 | class ClientIDMutation(Mutation): 9 | class Meta: 10 | abstract = True 11 | 12 | @classmethod 13 | def __init_subclass_with_meta__( 14 | cls, output=None, input_fields=None, arguments=None, name=None, **options 15 | ): 16 | input_class = getattr(cls, "Input", None) 17 | base_name = re.sub("Payload$", "", name or cls.__name__) 18 | 19 | assert not output, "Can't specify any output" 20 | assert not arguments, "Can't specify any arguments" 21 | 22 | bases = (InputObjectType,) 23 | if input_class: 24 | bases += (input_class,) 25 | 26 | if not input_fields: 27 | input_fields = {} 28 | 29 | cls.Input = type( 30 | f"{base_name}Input", 31 | bases, 32 | dict(input_fields, client_mutation_id=String(name="clientMutationId")), 33 | ) 34 | 35 | arguments = dict( 36 | input=cls.Input(required=True) 37 | # 'client_mutation_id': String(name='clientMutationId') 38 | ) 39 | mutate_and_get_payload = getattr(cls, "mutate_and_get_payload", None) 40 | if cls.mutate and cls.mutate.__func__ == ClientIDMutation.mutate.__func__: 41 | assert mutate_and_get_payload, ( 42 | f"{name or cls.__name__}.mutate_and_get_payload method is required" 43 | " in a ClientIDMutation." 44 | ) 45 | 46 | if not name: 47 | name = f"{base_name}Payload" 48 | 49 | super(ClientIDMutation, cls).__init_subclass_with_meta__( 50 | output=None, arguments=arguments, name=name, **options 51 | ) 52 | cls._meta.fields["client_mutation_id"] = Field(String, name="clientMutationId") 53 | 54 | @classmethod 55 | def mutate(cls, root, info, input): 56 | def on_resolve(payload): 57 | try: 58 | payload.client_mutation_id = input.get("client_mutation_id") 59 | except Exception: 60 | raise Exception( 61 | f"Cannot set client_mutation_id in the payload object {repr(payload)}" 62 | ) 63 | return payload 64 | 65 | result = cls.mutate_and_get_payload(root, info, **input) 66 | return maybe_thenable(result, on_resolve) 67 | -------------------------------------------------------------------------------- /graphene/relay/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphql-python/graphene/82903263080b3b7f22c2ad84319584d7a3b1a1f6/graphene/relay/tests/__init__.py -------------------------------------------------------------------------------- /graphene/relay/tests/test_connection_async.py: -------------------------------------------------------------------------------- 1 | from pytest import mark 2 | 3 | from graphql_relay.utils import base64 4 | 5 | from graphene.types import ObjectType, Schema, String 6 | from graphene.relay.connection import Connection, ConnectionField, PageInfo 7 | from graphene.relay.node import Node 8 | 9 | letter_chars = ["A", "B", "C", "D", "E"] 10 | 11 | 12 | class Letter(ObjectType): 13 | class Meta: 14 | interfaces = (Node,) 15 | 16 | letter = String() 17 | 18 | 19 | class LetterConnection(Connection): 20 | class Meta: 21 | node = Letter 22 | 23 | 24 | class Query(ObjectType): 25 | letters = ConnectionField(LetterConnection) 26 | connection_letters = ConnectionField(LetterConnection) 27 | async_letters = ConnectionField(LetterConnection) 28 | 29 | node = Node.Field() 30 | 31 | def resolve_letters(self, info, **args): 32 | return list(letters.values()) 33 | 34 | async def resolve_async_letters(self, info, **args): 35 | return list(letters.values()) 36 | 37 | def resolve_connection_letters(self, info, **args): 38 | return LetterConnection( 39 | page_info=PageInfo(has_next_page=True, has_previous_page=False), 40 | edges=[ 41 | LetterConnection.Edge(node=Letter(id=0, letter="A"), cursor="a-cursor") 42 | ], 43 | ) 44 | 45 | 46 | schema = Schema(Query) 47 | 48 | letters = {letter: Letter(id=i, letter=letter) for i, letter in enumerate(letter_chars)} 49 | 50 | 51 | def edges(selected_letters): 52 | return [ 53 | { 54 | "node": {"id": base64("Letter:%s" % letter.id), "letter": letter.letter}, 55 | "cursor": base64("arrayconnection:%s" % letter.id), 56 | } 57 | for letter in [letters[i] for i in selected_letters] 58 | ] 59 | 60 | 61 | def cursor_for(ltr): 62 | letter = letters[ltr] 63 | return base64("arrayconnection:%s" % letter.id) 64 | 65 | 66 | def execute(args=""): 67 | if args: 68 | args = "(" + args + ")" 69 | 70 | return schema.execute( 71 | """ 72 | { 73 | letters%s { 74 | edges { 75 | node { 76 | id 77 | letter 78 | } 79 | cursor 80 | } 81 | pageInfo { 82 | hasPreviousPage 83 | hasNextPage 84 | startCursor 85 | endCursor 86 | } 87 | } 88 | } 89 | """ 90 | % args 91 | ) 92 | 93 | 94 | @mark.asyncio 95 | async def test_connection_async(): 96 | result = await schema.execute_async( 97 | """ 98 | { 99 | asyncLetters(first:1) { 100 | edges { 101 | node { 102 | id 103 | letter 104 | } 105 | } 106 | pageInfo { 107 | hasPreviousPage 108 | hasNextPage 109 | } 110 | } 111 | } 112 | """ 113 | ) 114 | 115 | assert not result.errors 116 | assert result.data == { 117 | "asyncLetters": { 118 | "edges": [{"node": {"id": "TGV0dGVyOjA=", "letter": "A"}}], 119 | "pageInfo": {"hasPreviousPage": False, "hasNextPage": True}, 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /graphene/relay/tests/test_global_id.py: -------------------------------------------------------------------------------- 1 | from graphql_relay import to_global_id 2 | 3 | from ...types import ID, NonNull, ObjectType, String 4 | from ...types.definitions import GrapheneObjectType 5 | from ..node import GlobalID, Node 6 | 7 | 8 | class CustomNode(Node): 9 | class Meta: 10 | name = "Node" 11 | 12 | 13 | class User(ObjectType): 14 | class Meta: 15 | interfaces = [CustomNode] 16 | 17 | name = String() 18 | 19 | 20 | class Info: 21 | def __init__(self, parent_type): 22 | self.parent_type = GrapheneObjectType( 23 | graphene_type=parent_type, 24 | name=parent_type._meta.name, 25 | description=parent_type._meta.description, 26 | fields=None, 27 | is_type_of=parent_type.is_type_of, 28 | interfaces=None, 29 | ) 30 | 31 | 32 | def test_global_id_defaults_to_required_and_node(): 33 | gid = GlobalID() 34 | assert isinstance(gid.type, NonNull) 35 | assert gid.type.of_type == ID 36 | assert gid.node == Node 37 | 38 | 39 | def test_global_id_allows_overriding_of_node_and_required(): 40 | gid = GlobalID(node=CustomNode, required=False) 41 | assert gid.type == ID 42 | assert gid.node == CustomNode 43 | 44 | 45 | def test_global_id_defaults_to_info_parent_type(): 46 | my_id = "1" 47 | gid = GlobalID() 48 | id_resolver = gid.wrap_resolve(lambda *_: my_id) 49 | my_global_id = id_resolver(None, Info(User)) 50 | assert my_global_id == to_global_id(User._meta.name, my_id) 51 | 52 | 53 | def test_global_id_allows_setting_customer_parent_type(): 54 | my_id = "1" 55 | gid = GlobalID(parent_type=User) 56 | id_resolver = gid.wrap_resolve(lambda *_: my_id) 57 | my_global_id = id_resolver(None, None) 58 | assert my_global_id == to_global_id(User._meta.name, my_id) 59 | -------------------------------------------------------------------------------- /graphene/relay/tests/test_mutation_async.py: -------------------------------------------------------------------------------- 1 | from pytest import mark 2 | 3 | from graphene.types import ID, Field, ObjectType, Schema 4 | from graphene.types.scalars import String 5 | from graphene.relay.mutation import ClientIDMutation 6 | from graphene.test import Client 7 | 8 | 9 | class SharedFields(object): 10 | shared = String() 11 | 12 | 13 | class MyNode(ObjectType): 14 | # class Meta: 15 | # interfaces = (Node, ) 16 | id = ID() 17 | name = String() 18 | 19 | 20 | class SaySomethingAsync(ClientIDMutation): 21 | class Input: 22 | what = String() 23 | 24 | phrase = String() 25 | 26 | @staticmethod 27 | async def mutate_and_get_payload(self, info, what, client_mutation_id=None): 28 | return SaySomethingAsync(phrase=str(what)) 29 | 30 | 31 | # MyEdge = MyNode.Connection.Edge 32 | class MyEdge(ObjectType): 33 | node = Field(MyNode) 34 | cursor = String() 35 | 36 | 37 | class OtherMutation(ClientIDMutation): 38 | class Input(SharedFields): 39 | additional_field = String() 40 | 41 | name = String() 42 | my_node_edge = Field(MyEdge) 43 | 44 | @staticmethod 45 | def mutate_and_get_payload( 46 | self, info, shared="", additional_field="", client_mutation_id=None 47 | ): 48 | edge_type = MyEdge 49 | return OtherMutation( 50 | name=shared + additional_field, 51 | my_node_edge=edge_type(cursor="1", node=MyNode(name="name")), 52 | ) 53 | 54 | 55 | class RootQuery(ObjectType): 56 | something = String() 57 | 58 | 59 | class Mutation(ObjectType): 60 | say_promise = SaySomethingAsync.Field() 61 | other = OtherMutation.Field() 62 | 63 | 64 | schema = Schema(query=RootQuery, mutation=Mutation) 65 | client = Client(schema) 66 | 67 | 68 | @mark.asyncio 69 | async def test_node_query_promise(): 70 | executed = await client.execute_async( 71 | 'mutation a { sayPromise(input: {what:"hello", clientMutationId:"1"}) { phrase } }' 72 | ) 73 | assert isinstance(executed, dict) 74 | assert "errors" not in executed 75 | assert executed["data"] == {"sayPromise": {"phrase": "hello"}} 76 | 77 | 78 | @mark.asyncio 79 | async def test_edge_query(): 80 | executed = await client.execute_async( 81 | 'mutation a { other(input: {clientMutationId:"1"}) { clientMutationId, myNodeEdge { cursor node { name }} } }' 82 | ) 83 | assert isinstance(executed, dict) 84 | assert "errors" not in executed 85 | assert executed["data"] == { 86 | "other": { 87 | "clientMutationId": "1", 88 | "myNodeEdge": {"cursor": "1", "node": {"name": "name"}}, 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /graphene/test/__init__.py: -------------------------------------------------------------------------------- 1 | from graphql.error import GraphQLError 2 | 3 | from graphene.types.schema import Schema 4 | 5 | 6 | def default_format_error(error): 7 | if isinstance(error, GraphQLError): 8 | return error.formatted 9 | return {"message": str(error)} 10 | 11 | 12 | def format_execution_result(execution_result, format_error): 13 | if execution_result: 14 | response = {} 15 | if execution_result.errors: 16 | response["errors"] = [format_error(e) for e in execution_result.errors] 17 | response["data"] = execution_result.data 18 | return response 19 | 20 | 21 | class Client: 22 | def __init__(self, schema, format_error=None, **execute_options): 23 | assert isinstance(schema, Schema) 24 | self.schema = schema 25 | self.execute_options = execute_options 26 | self.format_error = format_error or default_format_error 27 | 28 | def format_result(self, result): 29 | return format_execution_result(result, self.format_error) 30 | 31 | def execute(self, *args, **kwargs): 32 | executed = self.schema.execute(*args, **dict(self.execute_options, **kwargs)) 33 | return self.format_result(executed) 34 | 35 | async def execute_async(self, *args, **kwargs): 36 | executed = await self.schema.execute_async( 37 | *args, **dict(self.execute_options, **kwargs) 38 | ) 39 | return self.format_result(executed) 40 | -------------------------------------------------------------------------------- /graphene/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphql-python/graphene/82903263080b3b7f22c2ad84319584d7a3b1a1f6/graphene/tests/__init__.py -------------------------------------------------------------------------------- /graphene/tests/issues/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphql-python/graphene/82903263080b3b7f22c2ad84319584d7a3b1a1f6/graphene/tests/issues/__init__.py -------------------------------------------------------------------------------- /graphene/tests/issues/test_1293.py: -------------------------------------------------------------------------------- 1 | # https://github.com/graphql-python/graphene/issues/1293 2 | 3 | from datetime import datetime, timezone 4 | 5 | import graphene 6 | from graphql.utilities import print_schema 7 | 8 | 9 | class Filters(graphene.InputObjectType): 10 | datetime_after = graphene.DateTime( 11 | required=False, 12 | default_value=datetime.fromtimestamp(1434549820.776, timezone.utc), 13 | ) 14 | datetime_before = graphene.DateTime( 15 | required=False, 16 | default_value=datetime.fromtimestamp(1444549820.776, timezone.utc), 17 | ) 18 | 19 | 20 | class SetDatetime(graphene.Mutation): 21 | class Arguments: 22 | filters = Filters(required=True) 23 | 24 | ok = graphene.Boolean() 25 | 26 | def mutate(root, info, filters): 27 | return SetDatetime(ok=True) 28 | 29 | 30 | class Query(graphene.ObjectType): 31 | goodbye = graphene.String() 32 | 33 | 34 | class Mutations(graphene.ObjectType): 35 | set_datetime = SetDatetime.Field() 36 | 37 | 38 | def test_schema_printable_with_default_datetime_value(): 39 | schema = graphene.Schema(query=Query, mutation=Mutations) 40 | schema_str = print_schema(schema.graphql_schema) 41 | assert schema_str, "empty schema printed" 42 | -------------------------------------------------------------------------------- /graphene/tests/issues/test_1394.py: -------------------------------------------------------------------------------- 1 | from ...types import ObjectType, Schema, String, NonNull 2 | 3 | 4 | class Query(ObjectType): 5 | hello = String(input=NonNull(String)) 6 | 7 | def resolve_hello(self, info, input): 8 | if input == "nothing": 9 | return None 10 | return f"Hello {input}!" 11 | 12 | 13 | schema = Schema(query=Query) 14 | 15 | 16 | def test_required_input_provided(): 17 | """ 18 | Test that a required argument works when provided. 19 | """ 20 | input_value = "Potato" 21 | result = schema.execute('{ hello(input: "%s") }' % input_value) 22 | assert not result.errors 23 | assert result.data == {"hello": "Hello Potato!"} 24 | 25 | 26 | def test_required_input_missing(): 27 | """ 28 | Test that a required argument raised an error if not provided. 29 | """ 30 | result = schema.execute("{ hello }") 31 | assert result.errors 32 | assert len(result.errors) == 1 33 | assert ( 34 | result.errors[0].message 35 | == "Field 'hello' argument 'input' of type 'String!' is required, but it was not provided." 36 | ) 37 | -------------------------------------------------------------------------------- /graphene/tests/issues/test_1419.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from ...types.base64 import Base64 4 | from ...types.datetime import Date, DateTime 5 | from ...types.decimal import Decimal 6 | from ...types.generic import GenericScalar 7 | from ...types.json import JSONString 8 | from ...types.objecttype import ObjectType 9 | from ...types.scalars import ID, BigInt, Boolean, Float, Int, String 10 | from ...types.schema import Schema 11 | from ...types.uuid import UUID 12 | 13 | 14 | @pytest.mark.parametrize( 15 | "input_type,input_value", 16 | [ 17 | (Date, '"2022-02-02"'), 18 | (GenericScalar, '"foo"'), 19 | (Int, "1"), 20 | (BigInt, "12345678901234567890"), 21 | (Float, "1.1"), 22 | (String, '"foo"'), 23 | (Boolean, "true"), 24 | (ID, "1"), 25 | (DateTime, '"2022-02-02T11:11:11"'), 26 | (UUID, '"cbebbc62-758e-4f75-a890-bc73b5017d81"'), 27 | (Decimal, '"1.1"'), 28 | (JSONString, '"{\\"key\\":\\"foo\\",\\"value\\":\\"bar\\"}"'), 29 | (Base64, '"Q2hlbG8gd29ycmxkCg=="'), 30 | ], 31 | ) 32 | def test_parse_literal_with_variables(input_type, input_value): 33 | # input_b needs to be evaluated as literal while the variable dict for 34 | # input_a is passed along. 35 | 36 | class Query(ObjectType): 37 | generic = GenericScalar(input_a=GenericScalar(), input_b=input_type()) 38 | 39 | def resolve_generic(self, info, input_a=None, input_b=None): 40 | return input 41 | 42 | schema = Schema(query=Query) 43 | 44 | query = f""" 45 | query Test($a: GenericScalar){{ 46 | generic(inputA: $a, inputB: {input_value}) 47 | }} 48 | """ 49 | result = schema.execute( 50 | query, 51 | variables={"a": "bar"}, 52 | ) 53 | assert not result.errors 54 | -------------------------------------------------------------------------------- /graphene/tests/issues/test_313.py: -------------------------------------------------------------------------------- 1 | # https://github.com/graphql-python/graphene/issues/313 2 | 3 | import graphene 4 | 5 | 6 | class Query(graphene.ObjectType): 7 | rand = graphene.String() 8 | 9 | 10 | class Success(graphene.ObjectType): 11 | yeah = graphene.String() 12 | 13 | 14 | class Error(graphene.ObjectType): 15 | message = graphene.String() 16 | 17 | 18 | class CreatePostResult(graphene.Union): 19 | class Meta: 20 | types = [Success, Error] 21 | 22 | 23 | class CreatePost(graphene.Mutation): 24 | class Arguments: 25 | text = graphene.String(required=True) 26 | 27 | result = graphene.Field(CreatePostResult) 28 | 29 | def mutate(self, info, text): 30 | result = Success(yeah="yeah") 31 | 32 | return CreatePost(result=result) 33 | 34 | 35 | class Mutations(graphene.ObjectType): 36 | create_post = CreatePost.Field() 37 | 38 | 39 | # tests.py 40 | 41 | 42 | def test_create_post(): 43 | query_string = """ 44 | mutation { 45 | createPost(text: "Try this out") { 46 | result { 47 | __typename 48 | } 49 | } 50 | } 51 | """ 52 | 53 | schema = graphene.Schema(query=Query, mutation=Mutations) 54 | result = schema.execute(query_string) 55 | 56 | assert not result.errors 57 | assert result.data["createPost"]["result"]["__typename"] == "Success" 58 | -------------------------------------------------------------------------------- /graphene/tests/issues/test_356.py: -------------------------------------------------------------------------------- 1 | # https://github.com/graphql-python/graphene/issues/356 2 | 3 | from pytest import raises 4 | 5 | import graphene 6 | from graphene import relay 7 | 8 | 9 | class SomeTypeOne(graphene.ObjectType): 10 | pass 11 | 12 | 13 | class SomeTypeTwo(graphene.ObjectType): 14 | pass 15 | 16 | 17 | class MyUnion(graphene.Union): 18 | class Meta: 19 | types = (SomeTypeOne, SomeTypeTwo) 20 | 21 | 22 | def test_issue(): 23 | class Query(graphene.ObjectType): 24 | things = relay.ConnectionField(MyUnion) 25 | 26 | with raises(Exception) as exc_info: 27 | graphene.Schema(query=Query) 28 | 29 | assert str(exc_info.value) == ( 30 | "Query fields cannot be resolved." 31 | " IterableConnectionField type has to be a subclass of Connection." 32 | ' Received "MyUnion".' 33 | ) 34 | -------------------------------------------------------------------------------- /graphene/tests/issues/test_425.py: -------------------------------------------------------------------------------- 1 | # https://github.com/graphql-python/graphene/issues/425 2 | # Adapted for Graphene 2.0 3 | 4 | from graphene.types.enum import Enum, EnumOptions 5 | from graphene.types.inputobjecttype import InputObjectType 6 | from graphene.types.objecttype import ObjectType, ObjectTypeOptions 7 | 8 | 9 | # ObjectType 10 | class SpecialOptions(ObjectTypeOptions): 11 | other_attr = None 12 | 13 | 14 | class SpecialObjectType(ObjectType): 15 | @classmethod 16 | def __init_subclass_with_meta__(cls, other_attr="default", **options): 17 | _meta = SpecialOptions(cls) 18 | _meta.other_attr = other_attr 19 | super(SpecialObjectType, cls).__init_subclass_with_meta__( 20 | _meta=_meta, **options 21 | ) 22 | 23 | 24 | def test_special_objecttype_could_be_subclassed(): 25 | class MyType(SpecialObjectType): 26 | class Meta: 27 | other_attr = "yeah!" 28 | 29 | assert MyType._meta.other_attr == "yeah!" 30 | 31 | 32 | def test_special_objecttype_could_be_subclassed_default(): 33 | class MyType(SpecialObjectType): 34 | pass 35 | 36 | assert MyType._meta.other_attr == "default" 37 | 38 | 39 | def test_special_objecttype_inherit_meta_options(): 40 | class MyType(SpecialObjectType): 41 | pass 42 | 43 | assert MyType._meta.name == "MyType" 44 | assert MyType._meta.default_resolver is None 45 | assert MyType._meta.interfaces == () 46 | 47 | 48 | # InputObjectType 49 | class SpecialInputObjectTypeOptions(ObjectTypeOptions): 50 | other_attr = None 51 | 52 | 53 | class SpecialInputObjectType(InputObjectType): 54 | @classmethod 55 | def __init_subclass_with_meta__(cls, other_attr="default", **options): 56 | _meta = SpecialInputObjectTypeOptions(cls) 57 | _meta.other_attr = other_attr 58 | super(SpecialInputObjectType, cls).__init_subclass_with_meta__( 59 | _meta=_meta, **options 60 | ) 61 | 62 | 63 | def test_special_inputobjecttype_could_be_subclassed(): 64 | class MyInputObjectType(SpecialInputObjectType): 65 | class Meta: 66 | other_attr = "yeah!" 67 | 68 | assert MyInputObjectType._meta.other_attr == "yeah!" 69 | 70 | 71 | def test_special_inputobjecttype_could_be_subclassed_default(): 72 | class MyInputObjectType(SpecialInputObjectType): 73 | pass 74 | 75 | assert MyInputObjectType._meta.other_attr == "default" 76 | 77 | 78 | def test_special_inputobjecttype_inherit_meta_options(): 79 | class MyInputObjectType(SpecialInputObjectType): 80 | pass 81 | 82 | assert MyInputObjectType._meta.name == "MyInputObjectType" 83 | 84 | 85 | # Enum 86 | class SpecialEnumOptions(EnumOptions): 87 | other_attr = None 88 | 89 | 90 | class SpecialEnum(Enum): 91 | @classmethod 92 | def __init_subclass_with_meta__(cls, other_attr="default", **options): 93 | _meta = SpecialEnumOptions(cls) 94 | _meta.other_attr = other_attr 95 | super(SpecialEnum, cls).__init_subclass_with_meta__(_meta=_meta, **options) 96 | 97 | 98 | def test_special_enum_could_be_subclassed(): 99 | class MyEnum(SpecialEnum): 100 | class Meta: 101 | other_attr = "yeah!" 102 | 103 | assert MyEnum._meta.other_attr == "yeah!" 104 | 105 | 106 | def test_special_enum_could_be_subclassed_default(): 107 | class MyEnum(SpecialEnum): 108 | pass 109 | 110 | assert MyEnum._meta.other_attr == "default" 111 | 112 | 113 | def test_special_enum_inherit_meta_options(): 114 | class MyEnum(SpecialEnum): 115 | pass 116 | 117 | assert MyEnum._meta.name == "MyEnum" 118 | -------------------------------------------------------------------------------- /graphene/tests/issues/test_490.py: -------------------------------------------------------------------------------- 1 | # https://github.com/graphql-python/graphene/issues/313 2 | 3 | import graphene 4 | 5 | 6 | class Query(graphene.ObjectType): 7 | some_field = graphene.String(from_=graphene.String(name="from")) 8 | 9 | def resolve_some_field(self, info, from_=None): 10 | return from_ 11 | 12 | 13 | def test_issue(): 14 | query_string = """ 15 | query myQuery { 16 | someField(from: "Oh") 17 | } 18 | """ 19 | 20 | schema = graphene.Schema(query=Query) 21 | result = schema.execute(query_string) 22 | 23 | assert not result.errors 24 | assert result.data["someField"] == "Oh" 25 | -------------------------------------------------------------------------------- /graphene/tests/issues/test_720.py: -------------------------------------------------------------------------------- 1 | # https://github.com/graphql-python/graphene/issues/720 2 | # InputObjectTypes overwrite the "fields" attribute of the provided 3 | # _meta object, so even if dynamic fields are provided with a standard 4 | # InputObjectTypeOptions, they are ignored. 5 | 6 | import graphene 7 | 8 | 9 | class MyInputClass(graphene.InputObjectType): 10 | @classmethod 11 | def __init_subclass_with_meta__( 12 | cls, container=None, _meta=None, fields=None, **options 13 | ): 14 | if _meta is None: 15 | _meta = graphene.types.inputobjecttype.InputObjectTypeOptions(cls) 16 | _meta.fields = fields 17 | super(MyInputClass, cls).__init_subclass_with_meta__( 18 | container=container, _meta=_meta, **options 19 | ) 20 | 21 | 22 | class MyInput(MyInputClass): 23 | class Meta: 24 | fields = dict(x=graphene.Field(graphene.Int)) 25 | 26 | 27 | class Query(graphene.ObjectType): 28 | myField = graphene.Field(graphene.String, input=graphene.Argument(MyInput)) 29 | 30 | def resolve_myField(parent, info, input): 31 | return "ok" 32 | 33 | 34 | def test_issue(): 35 | query_string = """ 36 | query myQuery { 37 | myField(input: {x: 1}) 38 | } 39 | """ 40 | 41 | schema = graphene.Schema(query=Query) 42 | result = schema.execute(query_string) 43 | 44 | assert not result.errors 45 | -------------------------------------------------------------------------------- /graphene/tests/issues/test_881.py: -------------------------------------------------------------------------------- 1 | import pickle 2 | 3 | from ...types.enum import Enum 4 | 5 | 6 | class PickleEnum(Enum): 7 | # is defined outside of test because pickle unable to dump class inside ot pytest function 8 | A = "a" 9 | B = 1 10 | 11 | 12 | def test_enums_pickling(): 13 | a = PickleEnum.A 14 | pickled = pickle.dumps(a) 15 | restored = pickle.loads(pickled) 16 | assert type(a) is type(restored) 17 | assert a == restored 18 | assert a.value == restored.value 19 | assert a.name == restored.name 20 | 21 | b = PickleEnum.B 22 | pickled = pickle.dumps(b) 23 | restored = pickle.loads(pickled) 24 | assert type(a) is type(restored) 25 | assert b == restored 26 | assert b.value == restored.value 27 | assert b.name == restored.name 28 | -------------------------------------------------------------------------------- /graphene/tests/issues/test_956.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | 3 | 4 | def test_issue(): 5 | options = {"description": "This my enum", "deprecation_reason": "For the funs"} 6 | new_enum = graphene.Enum("MyEnum", [("some", "data")], **options) 7 | assert new_enum._meta.description == options["description"] 8 | assert new_enum._meta.deprecation_reason == options["deprecation_reason"] 9 | -------------------------------------------------------------------------------- /graphene/types/__init__.py: -------------------------------------------------------------------------------- 1 | from graphql import GraphQLResolveInfo as ResolveInfo 2 | 3 | from .argument import Argument 4 | from .base64 import Base64 5 | from .context import Context 6 | from .datetime import Date, DateTime, Time 7 | from .decimal import Decimal 8 | from .dynamic import Dynamic 9 | from .enum import Enum 10 | from .field import Field 11 | from .inputfield import InputField 12 | from .inputobjecttype import InputObjectType 13 | from .interface import Interface 14 | from .json import JSONString 15 | from .mutation import Mutation 16 | from .objecttype import ObjectType 17 | from .scalars import ID, BigInt, Boolean, Float, Int, Scalar, String 18 | from .schema import Schema 19 | from .structures import List, NonNull 20 | from .union import Union 21 | from .uuid import UUID 22 | 23 | __all__ = [ 24 | "Argument", 25 | "Base64", 26 | "BigInt", 27 | "Boolean", 28 | "Context", 29 | "Date", 30 | "DateTime", 31 | "Decimal", 32 | "Dynamic", 33 | "Enum", 34 | "Field", 35 | "Float", 36 | "ID", 37 | "InputField", 38 | "InputObjectType", 39 | "Int", 40 | "Interface", 41 | "JSONString", 42 | "List", 43 | "Mutation", 44 | "NonNull", 45 | "ObjectType", 46 | "ResolveInfo", 47 | "Scalar", 48 | "Schema", 49 | "String", 50 | "Time", 51 | "UUID", 52 | "Union", 53 | ] 54 | -------------------------------------------------------------------------------- /graphene/types/argument.py: -------------------------------------------------------------------------------- 1 | from itertools import chain 2 | from graphql import Undefined 3 | 4 | from .dynamic import Dynamic 5 | from .mountedtype import MountedType 6 | from .structures import NonNull 7 | from .utils import get_type 8 | 9 | 10 | class Argument(MountedType): 11 | """ 12 | Makes an Argument available on a Field in the GraphQL schema. 13 | 14 | Arguments will be parsed and provided to resolver methods for fields as keyword arguments. 15 | 16 | All ``arg`` and ``**extra_args`` for a ``graphene.Field`` are implicitly mounted as Argument 17 | using the below parameters. 18 | 19 | .. code:: python 20 | 21 | from graphene import String, Boolean, Argument 22 | 23 | age = String( 24 | # Boolean implicitly mounted as Argument 25 | dog_years=Boolean(description="convert to dog years"), 26 | # Boolean explicitly mounted as Argument 27 | decades=Argument(Boolean, default_value=False), 28 | ) 29 | 30 | args: 31 | type (class for a graphene.UnmountedType): must be a class (not an instance) of an 32 | unmounted graphene type (ex. scalar or object) which is used for the type of this 33 | argument in the GraphQL schema. 34 | required (optional, bool): indicates this argument as not null in the graphql schema. Same behavior 35 | as graphene.NonNull. Default False. 36 | name (optional, str): the name of the GraphQL argument. Defaults to parameter name. 37 | description (optional, str): the description of the GraphQL argument in the schema. 38 | default_value (optional, Any): The value to be provided if the user does not set this argument in 39 | the operation. 40 | deprecation_reason (optional, str): Setting this value indicates that the argument is 41 | depreciated and may provide instruction or reason on how for clients to proceed. Cannot be 42 | set if the argument is required (see spec). 43 | """ 44 | 45 | def __init__( 46 | self, 47 | type_, 48 | default_value=Undefined, 49 | deprecation_reason=None, 50 | description=None, 51 | name=None, 52 | required=False, 53 | _creation_counter=None, 54 | ): 55 | super(Argument, self).__init__(_creation_counter=_creation_counter) 56 | 57 | if required: 58 | assert ( 59 | deprecation_reason is None 60 | ), f"Argument {name} is required, cannot deprecate it." 61 | type_ = NonNull(type_) 62 | 63 | self.name = name 64 | self._type = type_ 65 | self.default_value = default_value 66 | self.description = description 67 | self.deprecation_reason = deprecation_reason 68 | 69 | @property 70 | def type(self): 71 | return get_type(self._type) 72 | 73 | def __eq__(self, other): 74 | return isinstance(other, Argument) and ( 75 | self.name == other.name 76 | and self.type == other.type 77 | and self.default_value == other.default_value 78 | and self.description == other.description 79 | and self.deprecation_reason == other.deprecation_reason 80 | ) 81 | 82 | 83 | def to_arguments(args, extra_args=None): 84 | from .unmountedtype import UnmountedType 85 | from .field import Field 86 | from .inputfield import InputField 87 | 88 | if extra_args: 89 | extra_args = sorted(extra_args.items(), key=lambda f: f[1]) 90 | else: 91 | extra_args = [] 92 | iter_arguments = chain(args.items(), extra_args) 93 | arguments = {} 94 | for default_name, arg in iter_arguments: 95 | if isinstance(arg, Dynamic): 96 | arg = arg.get_type() 97 | if arg is None: 98 | # If the Dynamic type returned None 99 | # then we skip the Argument 100 | continue 101 | 102 | if isinstance(arg, UnmountedType): 103 | arg = Argument.mounted(arg) 104 | 105 | if isinstance(arg, (InputField, Field)): 106 | raise ValueError( 107 | f"Expected {default_name} to be Argument, " 108 | f"but received {type(arg).__name__}. Try using Argument({arg.type})." 109 | ) 110 | 111 | if not isinstance(arg, Argument): 112 | raise ValueError(f'Unknown argument "{default_name}".') 113 | 114 | arg_name = default_name or arg.name 115 | assert ( 116 | arg_name not in arguments 117 | ), f'More than one Argument have same name "{arg_name}".' 118 | arguments[arg_name] = arg 119 | 120 | return arguments 121 | -------------------------------------------------------------------------------- /graphene/types/base.py: -------------------------------------------------------------------------------- 1 | from typing import Type, Optional 2 | 3 | from ..utils.subclass_with_meta import SubclassWithMeta, SubclassWithMeta_Meta 4 | from ..utils.trim_docstring import trim_docstring 5 | 6 | 7 | class BaseOptions: 8 | name: Optional[str] = None 9 | description: Optional[str] = None 10 | 11 | _frozen: bool = False 12 | 13 | def __init__(self, class_type: Type): 14 | self.class_type: Type = class_type 15 | 16 | def freeze(self): 17 | self._frozen = True 18 | 19 | def __setattr__(self, name, value): 20 | if not self._frozen: 21 | super(BaseOptions, self).__setattr__(name, value) 22 | else: 23 | raise Exception(f"Can't modify frozen Options {self}") 24 | 25 | def __repr__(self): 26 | return f"<{self.__class__.__name__} name={repr(self.name)}>" 27 | 28 | 29 | BaseTypeMeta = SubclassWithMeta_Meta 30 | 31 | 32 | class BaseType(SubclassWithMeta): 33 | @classmethod 34 | def create_type(cls, class_name, **options): 35 | return type(class_name, (cls,), {"Meta": options}) 36 | 37 | @classmethod 38 | def __init_subclass_with_meta__( 39 | cls, name=None, description=None, _meta=None, **_kwargs 40 | ): 41 | assert "_meta" not in cls.__dict__, "Can't assign meta directly" 42 | if not _meta: 43 | return 44 | _meta.name = name or cls.__name__ 45 | _meta.description = description or trim_docstring(cls.__doc__) 46 | _meta.freeze() 47 | cls._meta = _meta 48 | super(BaseType, cls).__init_subclass_with_meta__() 49 | -------------------------------------------------------------------------------- /graphene/types/base64.py: -------------------------------------------------------------------------------- 1 | from binascii import Error as _Error 2 | from base64 import b64decode, b64encode 3 | 4 | from graphql.error import GraphQLError 5 | from graphql.language import StringValueNode, print_ast 6 | 7 | from .scalars import Scalar 8 | 9 | 10 | class Base64(Scalar): 11 | """ 12 | The `Base64` scalar type represents a base64-encoded String. 13 | """ 14 | 15 | @staticmethod 16 | def serialize(value): 17 | if not isinstance(value, bytes): 18 | if isinstance(value, str): 19 | value = value.encode("utf-8") 20 | else: 21 | value = str(value).encode("utf-8") 22 | return b64encode(value).decode("utf-8") 23 | 24 | @classmethod 25 | def parse_literal(cls, node, _variables=None): 26 | if not isinstance(node, StringValueNode): 27 | raise GraphQLError( 28 | f"Base64 cannot represent non-string value: {print_ast(node)}" 29 | ) 30 | return cls.parse_value(node.value) 31 | 32 | @staticmethod 33 | def parse_value(value): 34 | if not isinstance(value, bytes): 35 | if not isinstance(value, str): 36 | raise GraphQLError( 37 | f"Base64 cannot represent non-string value: {repr(value)}" 38 | ) 39 | value = value.encode("utf-8") 40 | try: 41 | return b64decode(value, validate=True).decode("utf-8") 42 | except _Error: 43 | raise GraphQLError(f"Base64 cannot decode value: {repr(value)}") 44 | -------------------------------------------------------------------------------- /graphene/types/context.py: -------------------------------------------------------------------------------- 1 | class Context: 2 | """ 3 | Context can be used to make a convenient container for attributes to provide 4 | for execution for resolvers of a GraphQL operation like a query. 5 | 6 | .. code:: python 7 | 8 | from graphene import Context 9 | 10 | context = Context(loaders=build_dataloaders(), request=my_web_request) 11 | schema.execute('{ hello(name: "world") }', context=context) 12 | 13 | def resolve_hello(parent, info, name): 14 | info.context.request # value set in Context 15 | info.context.loaders # value set in Context 16 | # ... 17 | 18 | args: 19 | **params (Dict[str, Any]): values to make available on Context instance as attributes. 20 | 21 | """ 22 | 23 | def __init__(self, **params): 24 | for key, value in params.items(): 25 | setattr(self, key, value) 26 | -------------------------------------------------------------------------------- /graphene/types/datetime.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from dateutil.parser import isoparse 4 | 5 | from graphql.error import GraphQLError 6 | from graphql.language import StringValueNode, print_ast 7 | 8 | from .scalars import Scalar 9 | 10 | 11 | class Date(Scalar): 12 | """ 13 | The `Date` scalar type represents a Date 14 | value as specified by 15 | [iso8601](https://en.wikipedia.org/wiki/ISO_8601). 16 | """ 17 | 18 | @staticmethod 19 | def serialize(date): 20 | if isinstance(date, datetime.datetime): 21 | date = date.date() 22 | if not isinstance(date, datetime.date): 23 | raise GraphQLError(f"Date cannot represent value: {repr(date)}") 24 | return date.isoformat() 25 | 26 | @classmethod 27 | def parse_literal(cls, node, _variables=None): 28 | if not isinstance(node, StringValueNode): 29 | raise GraphQLError( 30 | f"Date cannot represent non-string value: {print_ast(node)}" 31 | ) 32 | return cls.parse_value(node.value) 33 | 34 | @staticmethod 35 | def parse_value(value): 36 | if isinstance(value, datetime.date): 37 | return value 38 | if not isinstance(value, str): 39 | raise GraphQLError(f"Date cannot represent non-string value: {repr(value)}") 40 | try: 41 | return datetime.date.fromisoformat(value) 42 | except ValueError: 43 | raise GraphQLError(f"Date cannot represent value: {repr(value)}") 44 | 45 | 46 | class DateTime(Scalar): 47 | """ 48 | The `DateTime` scalar type represents a DateTime 49 | value as specified by 50 | [iso8601](https://en.wikipedia.org/wiki/ISO_8601). 51 | """ 52 | 53 | @staticmethod 54 | def serialize(dt): 55 | if not isinstance(dt, (datetime.datetime, datetime.date)): 56 | raise GraphQLError(f"DateTime cannot represent value: {repr(dt)}") 57 | return dt.isoformat() 58 | 59 | @classmethod 60 | def parse_literal(cls, node, _variables=None): 61 | if not isinstance(node, StringValueNode): 62 | raise GraphQLError( 63 | f"DateTime cannot represent non-string value: {print_ast(node)}" 64 | ) 65 | return cls.parse_value(node.value) 66 | 67 | @staticmethod 68 | def parse_value(value): 69 | if isinstance(value, datetime.datetime): 70 | return value 71 | if not isinstance(value, str): 72 | raise GraphQLError( 73 | f"DateTime cannot represent non-string value: {repr(value)}" 74 | ) 75 | try: 76 | return isoparse(value) 77 | except ValueError: 78 | raise GraphQLError(f"DateTime cannot represent value: {repr(value)}") 79 | 80 | 81 | class Time(Scalar): 82 | """ 83 | The `Time` scalar type represents a Time value as 84 | specified by 85 | [iso8601](https://en.wikipedia.org/wiki/ISO_8601). 86 | """ 87 | 88 | @staticmethod 89 | def serialize(time): 90 | if not isinstance(time, datetime.time): 91 | raise GraphQLError(f"Time cannot represent value: {repr(time)}") 92 | return time.isoformat() 93 | 94 | @classmethod 95 | def parse_literal(cls, node, _variables=None): 96 | if not isinstance(node, StringValueNode): 97 | raise GraphQLError( 98 | f"Time cannot represent non-string value: {print_ast(node)}" 99 | ) 100 | return cls.parse_value(node.value) 101 | 102 | @classmethod 103 | def parse_value(cls, value): 104 | if isinstance(value, datetime.time): 105 | return value 106 | if not isinstance(value, str): 107 | raise GraphQLError(f"Time cannot represent non-string value: {repr(value)}") 108 | try: 109 | return datetime.time.fromisoformat(value) 110 | except ValueError: 111 | raise GraphQLError(f"Time cannot represent value: {repr(value)}") 112 | -------------------------------------------------------------------------------- /graphene/types/decimal.py: -------------------------------------------------------------------------------- 1 | from decimal import Decimal as _Decimal 2 | 3 | from graphql import Undefined 4 | from graphql.language.ast import StringValueNode, IntValueNode 5 | 6 | from .scalars import Scalar 7 | 8 | 9 | class Decimal(Scalar): 10 | """ 11 | The `Decimal` scalar type represents a python Decimal. 12 | """ 13 | 14 | @staticmethod 15 | def serialize(dec): 16 | if isinstance(dec, str): 17 | dec = _Decimal(dec) 18 | assert isinstance( 19 | dec, _Decimal 20 | ), f'Received not compatible Decimal "{repr(dec)}"' 21 | return str(dec) 22 | 23 | @classmethod 24 | def parse_literal(cls, node, _variables=None): 25 | if isinstance(node, (StringValueNode, IntValueNode)): 26 | return cls.parse_value(node.value) 27 | return Undefined 28 | 29 | @staticmethod 30 | def parse_value(value): 31 | try: 32 | return _Decimal(value) 33 | except Exception: 34 | return Undefined 35 | -------------------------------------------------------------------------------- /graphene/types/definitions.py: -------------------------------------------------------------------------------- 1 | from enum import Enum as PyEnum 2 | 3 | from graphql import ( 4 | GraphQLEnumType, 5 | GraphQLInputObjectType, 6 | GraphQLInterfaceType, 7 | GraphQLObjectType, 8 | GraphQLScalarType, 9 | GraphQLUnionType, 10 | ) 11 | 12 | 13 | class GrapheneGraphQLType: 14 | """ 15 | A class for extending the base GraphQLType with the related 16 | graphene_type 17 | """ 18 | 19 | def __init__(self, *args, **kwargs): 20 | self.graphene_type = kwargs.pop("graphene_type") 21 | super(GrapheneGraphQLType, self).__init__(*args, **kwargs) 22 | 23 | def __copy__(self): 24 | result = GrapheneGraphQLType(graphene_type=self.graphene_type) 25 | result.__dict__.update(self.__dict__) 26 | return result 27 | 28 | 29 | class GrapheneInterfaceType(GrapheneGraphQLType, GraphQLInterfaceType): 30 | pass 31 | 32 | 33 | class GrapheneUnionType(GrapheneGraphQLType, GraphQLUnionType): 34 | pass 35 | 36 | 37 | class GrapheneObjectType(GrapheneGraphQLType, GraphQLObjectType): 38 | pass 39 | 40 | 41 | class GrapheneScalarType(GrapheneGraphQLType, GraphQLScalarType): 42 | pass 43 | 44 | 45 | class GrapheneEnumType(GrapheneGraphQLType, GraphQLEnumType): 46 | def serialize(self, value): 47 | if not isinstance(value, PyEnum): 48 | enum = self.graphene_type._meta.enum 49 | try: 50 | # Try and get enum by value 51 | value = enum(value) 52 | except ValueError: 53 | # Try and get enum by name 54 | try: 55 | value = enum[value] 56 | except KeyError: 57 | pass 58 | return super(GrapheneEnumType, self).serialize(value) 59 | 60 | 61 | class GrapheneInputObjectType(GrapheneGraphQLType, GraphQLInputObjectType): 62 | pass 63 | -------------------------------------------------------------------------------- /graphene/types/dynamic.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | from functools import partial 3 | 4 | from .mountedtype import MountedType 5 | 6 | 7 | class Dynamic(MountedType): 8 | """ 9 | A Dynamic Type let us get the type in runtime when we generate 10 | the schema. So we can have lazy fields. 11 | """ 12 | 13 | def __init__(self, type_, with_schema=False, _creation_counter=None): 14 | super(Dynamic, self).__init__(_creation_counter=_creation_counter) 15 | assert inspect.isfunction(type_) or isinstance(type_, partial) 16 | self.type = type_ 17 | self.with_schema = with_schema 18 | 19 | def get_type(self, schema=None): 20 | if schema and self.with_schema: 21 | return self.type(schema=schema) 22 | return self.type() 23 | -------------------------------------------------------------------------------- /graphene/types/enum.py: -------------------------------------------------------------------------------- 1 | from enum import Enum as PyEnum 2 | 3 | from graphene.utils.subclass_with_meta import SubclassWithMeta_Meta 4 | 5 | from .base import BaseOptions, BaseType 6 | from .unmountedtype import UnmountedType 7 | 8 | 9 | def eq_enum(self, other): 10 | if isinstance(other, self.__class__): 11 | return self is other 12 | return self.value is other 13 | 14 | 15 | def hash_enum(self): 16 | return hash(self.name) 17 | 18 | 19 | EnumType = type(PyEnum) 20 | 21 | 22 | class EnumOptions(BaseOptions): 23 | enum = None # type: Enum 24 | deprecation_reason = None 25 | 26 | 27 | class EnumMeta(SubclassWithMeta_Meta): 28 | def __new__(cls, name_, bases, classdict, **options): 29 | enum_members = dict(classdict, __eq__=eq_enum, __hash__=hash_enum) 30 | # We remove the Meta attribute from the class to not collide 31 | # with the enum values. 32 | enum_members.pop("Meta", None) 33 | enum = PyEnum(cls.__name__, enum_members) 34 | obj = SubclassWithMeta_Meta.__new__( 35 | cls, name_, bases, dict(classdict, __enum__=enum), **options 36 | ) 37 | globals()[name_] = obj.__enum__ 38 | return obj 39 | 40 | def get(cls, value): 41 | return cls._meta.enum(value) 42 | 43 | def __getitem__(cls, value): 44 | return cls._meta.enum[value] 45 | 46 | def __prepare__(name, bases, **kwargs): # noqa: N805 47 | return {} 48 | 49 | def __call__(cls, *args, **kwargs): # noqa: N805 50 | if cls is Enum: 51 | description = kwargs.pop("description", None) 52 | deprecation_reason = kwargs.pop("deprecation_reason", None) 53 | return cls.from_enum( 54 | PyEnum(*args, **kwargs), 55 | description=description, 56 | deprecation_reason=deprecation_reason, 57 | ) 58 | return super(EnumMeta, cls).__call__(*args, **kwargs) 59 | # return cls._meta.enum(*args, **kwargs) 60 | 61 | def __iter__(cls): 62 | return cls._meta.enum.__iter__() 63 | 64 | def from_enum(cls, enum, name=None, description=None, deprecation_reason=None): # noqa: N805 65 | name = name or enum.__name__ 66 | description = description or enum.__doc__ or "An enumeration." 67 | meta_dict = { 68 | "enum": enum, 69 | "description": description, 70 | "deprecation_reason": deprecation_reason, 71 | } 72 | meta_class = type("Meta", (object,), meta_dict) 73 | return type(name, (Enum,), {"Meta": meta_class}) 74 | 75 | 76 | class Enum(UnmountedType, BaseType, metaclass=EnumMeta): 77 | """ 78 | Enum type definition 79 | 80 | Defines a static set of values that can be provided as a Field, Argument or InputField. 81 | 82 | .. code:: python 83 | 84 | from graphene import Enum 85 | 86 | class NameFormat(Enum): 87 | FIRST_LAST = "first_last" 88 | LAST_FIRST = "last_first" 89 | 90 | Meta: 91 | enum (optional, Enum): Python enum to use as a base for GraphQL Enum. 92 | 93 | name (optional, str): Name of the GraphQL type (must be unique in schema). Defaults to class 94 | name. 95 | description (optional, str): Description of the GraphQL type in the schema. Defaults to class 96 | docstring. 97 | deprecation_reason (optional, str): Setting this value indicates that the enum is 98 | depreciated and may provide instruction or reason on how for clients to proceed. 99 | """ 100 | 101 | @classmethod 102 | def __init_subclass_with_meta__(cls, enum=None, _meta=None, **options): 103 | if not _meta: 104 | _meta = EnumOptions(cls) 105 | _meta.enum = enum or cls.__enum__ 106 | _meta.deprecation_reason = options.pop("deprecation_reason", None) 107 | for key, value in _meta.enum.__members__.items(): 108 | setattr(cls, key, value) 109 | 110 | super(Enum, cls).__init_subclass_with_meta__(_meta=_meta, **options) 111 | 112 | @classmethod 113 | def get_type(cls): 114 | """ 115 | This function is called when the unmounted type (Enum instance) 116 | is mounted (as a Field, InputField or Argument) 117 | """ 118 | return cls 119 | -------------------------------------------------------------------------------- /graphene/types/generic.py: -------------------------------------------------------------------------------- 1 | from graphql.language.ast import ( 2 | BooleanValueNode, 3 | FloatValueNode, 4 | IntValueNode, 5 | ListValueNode, 6 | ObjectValueNode, 7 | StringValueNode, 8 | ) 9 | 10 | from graphene.types.scalars import MAX_INT, MIN_INT 11 | 12 | from .scalars import Scalar 13 | 14 | 15 | class GenericScalar(Scalar): 16 | """ 17 | The `GenericScalar` scalar type represents a generic 18 | GraphQL scalar value that could be: 19 | String, Boolean, Int, Float, List or Object. 20 | """ 21 | 22 | @staticmethod 23 | def identity(value): 24 | return value 25 | 26 | serialize = identity 27 | parse_value = identity 28 | 29 | @staticmethod 30 | def parse_literal(ast, _variables=None): 31 | if isinstance(ast, (StringValueNode, BooleanValueNode)): 32 | return ast.value 33 | elif isinstance(ast, IntValueNode): 34 | num = int(ast.value) 35 | if MIN_INT <= num <= MAX_INT: 36 | return num 37 | elif isinstance(ast, FloatValueNode): 38 | return float(ast.value) 39 | elif isinstance(ast, ListValueNode): 40 | return [GenericScalar.parse_literal(value) for value in ast.values] 41 | elif isinstance(ast, ObjectValueNode): 42 | return { 43 | field.name.value: GenericScalar.parse_literal(field.value) 44 | for field in ast.fields 45 | } 46 | else: 47 | return None 48 | -------------------------------------------------------------------------------- /graphene/types/inputfield.py: -------------------------------------------------------------------------------- 1 | from graphql import Undefined 2 | 3 | from .mountedtype import MountedType 4 | from .structures import NonNull 5 | from .utils import get_type 6 | 7 | 8 | class InputField(MountedType): 9 | """ 10 | Makes a field available on an ObjectType in the GraphQL schema. Any type can be mounted as a 11 | Input Field except Interface and Union: 12 | 13 | - Object Type 14 | - Scalar Type 15 | - Enum 16 | 17 | Input object types also can't have arguments on their input fields, unlike regular ``graphene.Field``. 18 | 19 | All class attributes of ``graphene.InputObjectType`` are implicitly mounted as InputField 20 | using the below arguments. 21 | 22 | .. code:: python 23 | 24 | from graphene import InputObjectType, String, InputField 25 | 26 | class Person(InputObjectType): 27 | # implicitly mounted as Input Field 28 | first_name = String(required=True) 29 | # explicitly mounted as Input Field 30 | last_name = InputField(String, description="Surname") 31 | 32 | args: 33 | type (class for a graphene.UnmountedType): Must be a class (not an instance) of an 34 | unmounted graphene type (ex. scalar or object) which is used for the type of this 35 | field in the GraphQL schema. 36 | name (optional, str): Name of the GraphQL input field (must be unique in a type). 37 | Defaults to attribute name. 38 | default_value (optional, Any): Default value to use as input if none set in user operation ( 39 | query, mutation, etc.). 40 | deprecation_reason (optional, str): Setting this value indicates that the field is 41 | depreciated and may provide instruction or reason on how for clients to proceed. 42 | description (optional, str): Description of the GraphQL field in the schema. 43 | required (optional, bool): Indicates this input field as not null in the graphql schema. 44 | Raises a validation error if argument not provided. Same behavior as graphene.NonNull. 45 | Default False. 46 | **extra_args (optional, Dict): Not used. 47 | """ 48 | 49 | def __init__( 50 | self, 51 | type_, 52 | name=None, 53 | default_value=Undefined, 54 | deprecation_reason=None, 55 | description=None, 56 | required=False, 57 | _creation_counter=None, 58 | **extra_args, 59 | ): 60 | super(InputField, self).__init__(_creation_counter=_creation_counter) 61 | self.name = name 62 | if required: 63 | assert ( 64 | deprecation_reason is None 65 | ), f"InputField {name} is required, cannot deprecate it." 66 | type_ = NonNull(type_) 67 | self._type = type_ 68 | self.deprecation_reason = deprecation_reason 69 | self.default_value = default_value 70 | self.description = description 71 | 72 | @property 73 | def type(self): 74 | return get_type(self._type) 75 | -------------------------------------------------------------------------------- /graphene/types/interface.py: -------------------------------------------------------------------------------- 1 | from typing import TYPE_CHECKING 2 | 3 | from .base import BaseOptions, BaseType 4 | from .field import Field 5 | from .utils import yank_fields_from_attrs 6 | 7 | # For static type checking with type checker 8 | if TYPE_CHECKING: 9 | from typing import Dict, Iterable, Type # NOQA 10 | 11 | 12 | class InterfaceOptions(BaseOptions): 13 | fields = None # type: Dict[str, Field] 14 | interfaces = () # type: Iterable[Type[Interface]] 15 | 16 | 17 | class Interface(BaseType): 18 | """ 19 | Interface Type Definition 20 | 21 | When a field can return one of a heterogeneous set of types, a Interface type 22 | is used to describe what types are possible, what fields are in common across 23 | all types, as well as a function to determine which type is actually used 24 | when the field is resolved. 25 | 26 | .. code:: python 27 | 28 | from graphene import Interface, String 29 | 30 | class HasAddress(Interface): 31 | class Meta: 32 | description = "Address fields" 33 | 34 | address1 = String() 35 | address2 = String() 36 | 37 | If a field returns an Interface Type, the ambiguous type of the object can be determined using 38 | ``resolve_type`` on Interface and an ObjectType with ``Meta.possible_types`` or ``is_type_of``. 39 | 40 | Meta: 41 | name (str): Name of the GraphQL type (must be unique in schema). Defaults to class 42 | name. 43 | description (str): Description of the GraphQL type in the schema. Defaults to class 44 | docstring. 45 | fields (Dict[str, graphene.Field]): Dictionary of field name to Field. Not recommended to 46 | use (prefer class attributes). 47 | """ 48 | 49 | @classmethod 50 | def __init_subclass_with_meta__(cls, _meta=None, interfaces=(), **options): 51 | if not _meta: 52 | _meta = InterfaceOptions(cls) 53 | 54 | fields = {} 55 | for base in reversed(cls.__mro__): 56 | fields.update(yank_fields_from_attrs(base.__dict__, _as=Field)) 57 | 58 | if _meta.fields: 59 | _meta.fields.update(fields) 60 | else: 61 | _meta.fields = fields 62 | 63 | if not _meta.interfaces: 64 | _meta.interfaces = interfaces 65 | 66 | super(Interface, cls).__init_subclass_with_meta__(_meta=_meta, **options) 67 | 68 | @classmethod 69 | def resolve_type(cls, instance, info): 70 | from .objecttype import ObjectType 71 | 72 | if isinstance(instance, ObjectType): 73 | return type(instance) 74 | 75 | def __init__(self, *args, **kwargs): 76 | raise Exception("An Interface cannot be initialized") 77 | -------------------------------------------------------------------------------- /graphene/types/json.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from graphql import Undefined 4 | from graphql.language.ast import StringValueNode 5 | 6 | from .scalars import Scalar 7 | 8 | 9 | class JSONString(Scalar): 10 | """ 11 | Allows use of a JSON String for input / output from the GraphQL schema. 12 | 13 | Use of this type is *not recommended* as you lose the benefits of having a defined, static 14 | schema (one of the key benefits of GraphQL). 15 | """ 16 | 17 | @staticmethod 18 | def serialize(dt): 19 | return json.dumps(dt) 20 | 21 | @staticmethod 22 | def parse_literal(node, _variables=None): 23 | if isinstance(node, StringValueNode): 24 | try: 25 | return json.loads(node.value) 26 | except Exception as error: 27 | raise ValueError(f"Badly formed JSONString: {str(error)}") 28 | return Undefined 29 | 30 | @staticmethod 31 | def parse_value(value): 32 | return json.loads(value) 33 | -------------------------------------------------------------------------------- /graphene/types/mountedtype.py: -------------------------------------------------------------------------------- 1 | from ..utils.orderedtype import OrderedType 2 | from .unmountedtype import UnmountedType 3 | 4 | 5 | class MountedType(OrderedType): 6 | @classmethod 7 | def mounted(cls, unmounted): # noqa: N802 8 | """ 9 | Mount the UnmountedType instance 10 | """ 11 | assert isinstance( 12 | unmounted, UnmountedType 13 | ), f"{cls.__name__} can't mount {repr(unmounted)}" 14 | 15 | return cls( 16 | unmounted.get_type(), 17 | *unmounted.args, 18 | _creation_counter=unmounted.creation_counter, 19 | **unmounted.kwargs, 20 | ) 21 | -------------------------------------------------------------------------------- /graphene/types/resolver.py: -------------------------------------------------------------------------------- 1 | def attr_resolver(attname, default_value, root, info, **args): 2 | return getattr(root, attname, default_value) 3 | 4 | 5 | def dict_resolver(attname, default_value, root, info, **args): 6 | return root.get(attname, default_value) 7 | 8 | 9 | def dict_or_attr_resolver(attname, default_value, root, info, **args): 10 | resolver = dict_resolver if isinstance(root, dict) else attr_resolver 11 | return resolver(attname, default_value, root, info, **args) 12 | 13 | 14 | default_resolver = dict_or_attr_resolver 15 | 16 | 17 | def set_default_resolver(resolver): 18 | global default_resolver 19 | assert callable(resolver), "Received non-callable resolver." 20 | default_resolver = resolver 21 | 22 | 23 | def get_default_resolver(): 24 | return default_resolver 25 | -------------------------------------------------------------------------------- /graphene/types/structures.py: -------------------------------------------------------------------------------- 1 | from .unmountedtype import UnmountedType 2 | from .utils import get_type 3 | 4 | 5 | class Structure(UnmountedType): 6 | """ 7 | A structure is a GraphQL type instance that 8 | wraps a main type with certain structure. 9 | """ 10 | 11 | def __init__(self, of_type, *args, **kwargs): 12 | super(Structure, self).__init__(*args, **kwargs) 13 | if not isinstance(of_type, Structure) and isinstance(of_type, UnmountedType): 14 | cls_name = type(self).__name__ 15 | of_type_name = type(of_type).__name__ 16 | raise Exception( 17 | f"{cls_name} could not have a mounted {of_type_name}()" 18 | f" as inner type. Try with {cls_name}({of_type_name})." 19 | ) 20 | self._of_type = of_type 21 | 22 | @property 23 | def of_type(self): 24 | return get_type(self._of_type) 25 | 26 | def get_type(self): 27 | """ 28 | This function is called when the unmounted type (List or NonNull instance) 29 | is mounted (as a Field, InputField or Argument) 30 | """ 31 | return self 32 | 33 | 34 | class List(Structure): 35 | """ 36 | List Modifier 37 | 38 | A list is a kind of type marker, a wrapping type which points to another 39 | type. Lists are often created within the context of defining the fields of 40 | an object type. 41 | 42 | List indicates that many values will be returned (or input) for this field. 43 | 44 | .. code:: python 45 | 46 | from graphene import List, String 47 | 48 | field_name = List(String, description="There will be many values") 49 | """ 50 | 51 | def __str__(self): 52 | return f"[{self.of_type}]" 53 | 54 | def __eq__(self, other): 55 | return isinstance(other, List) and ( 56 | self.of_type == other.of_type 57 | and self.args == other.args 58 | and self.kwargs == other.kwargs 59 | ) 60 | 61 | 62 | class NonNull(Structure): 63 | """ 64 | Non-Null Modifier 65 | 66 | A non-null is a kind of type marker, a wrapping type which points to another 67 | type. Non-null types enforce that their values are never null and can ensure 68 | an error is raised if this ever occurs during a request. It is useful for 69 | fields which you can make a strong guarantee on non-nullability, for example 70 | usually the id field of a database row will never be null. 71 | 72 | Note: the enforcement of non-nullability occurs within the executor. 73 | 74 | NonNull can also be indicated on all Mounted types with the keyword argument ``required``. 75 | 76 | .. code:: python 77 | 78 | from graphene import NonNull, String 79 | 80 | field_name = NonNull(String, description='This field will not be null') 81 | another_field = String(required=True, description='This is equivalent to the above') 82 | 83 | """ 84 | 85 | def __init__(self, *args, **kwargs): 86 | super(NonNull, self).__init__(*args, **kwargs) 87 | assert not isinstance( 88 | self._of_type, NonNull 89 | ), f"Can only create NonNull of a Nullable GraphQLType but got: {self._of_type}." 90 | 91 | def __str__(self): 92 | return f"{self.of_type}!" 93 | 94 | def __eq__(self, other): 95 | return isinstance(other, NonNull) and ( 96 | self.of_type == other.of_type 97 | and self.args == other.args 98 | and self.kwargs == other.kwargs 99 | ) 100 | -------------------------------------------------------------------------------- /graphene/types/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphql-python/graphene/82903263080b3b7f22c2ad84319584d7a3b1a1f6/graphene/types/tests/__init__.py -------------------------------------------------------------------------------- /graphene/types/tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from graphql import Undefined 3 | 4 | from graphene.types.inputobjecttype import set_input_object_type_default_value 5 | 6 | 7 | @pytest.fixture() 8 | def set_default_input_object_type_to_undefined(): 9 | """This fixture is used to change the default value of optional inputs in InputObjectTypes for specific tests""" 10 | set_input_object_type_default_value(Undefined) 11 | yield 12 | set_input_object_type_default_value(None) 13 | -------------------------------------------------------------------------------- /graphene/types/tests/test_argument.py: -------------------------------------------------------------------------------- 1 | from functools import partial 2 | 3 | from pytest import raises 4 | 5 | from ..argument import Argument, to_arguments 6 | from ..field import Field 7 | from ..inputfield import InputField 8 | from ..scalars import String 9 | from ..structures import NonNull 10 | 11 | 12 | def test_argument(): 13 | arg = Argument(String, default_value="a", description="desc", name="b") 14 | assert arg.type == String 15 | assert arg.default_value == "a" 16 | assert arg.description == "desc" 17 | assert arg.name == "b" 18 | 19 | 20 | def test_argument_comparasion(): 21 | arg1 = Argument( 22 | String, 23 | name="Hey", 24 | description="Desc", 25 | default_value="default", 26 | deprecation_reason="deprecated", 27 | ) 28 | arg2 = Argument( 29 | String, 30 | name="Hey", 31 | description="Desc", 32 | default_value="default", 33 | deprecation_reason="deprecated", 34 | ) 35 | 36 | assert arg1 == arg2 37 | assert arg1 != String() 38 | 39 | 40 | def test_argument_required(): 41 | arg = Argument(String, required=True) 42 | assert arg.type == NonNull(String) 43 | 44 | 45 | def test_to_arguments(): 46 | args = {"arg_string": Argument(String), "unmounted_arg": String(required=True)} 47 | 48 | my_args = to_arguments(args) 49 | assert my_args == { 50 | "arg_string": Argument(String), 51 | "unmounted_arg": Argument(String, required=True), 52 | } 53 | 54 | 55 | def test_to_arguments_deprecated(): 56 | args = {"unmounted_arg": String(required=False, deprecation_reason="deprecated")} 57 | 58 | my_args = to_arguments(args) 59 | assert my_args == { 60 | "unmounted_arg": Argument( 61 | String, required=False, deprecation_reason="deprecated" 62 | ), 63 | } 64 | 65 | 66 | def test_to_arguments_required_deprecated(): 67 | args = { 68 | "unmounted_arg": String( 69 | required=True, name="arg", deprecation_reason="deprecated" 70 | ) 71 | } 72 | 73 | with raises(AssertionError) as exc_info: 74 | to_arguments(args) 75 | 76 | assert str(exc_info.value) == "Argument arg is required, cannot deprecate it." 77 | 78 | 79 | def test_to_arguments_raises_if_field(): 80 | args = {"arg_string": Field(String)} 81 | 82 | with raises(ValueError) as exc_info: 83 | to_arguments(args) 84 | 85 | assert str(exc_info.value) == ( 86 | "Expected arg_string to be Argument, but received Field. Try using " 87 | "Argument(String)." 88 | ) 89 | 90 | 91 | def test_to_arguments_raises_if_inputfield(): 92 | args = {"arg_string": InputField(String)} 93 | 94 | with raises(ValueError) as exc_info: 95 | to_arguments(args) 96 | 97 | assert str(exc_info.value) == ( 98 | "Expected arg_string to be Argument, but received InputField. Try " 99 | "using Argument(String)." 100 | ) 101 | 102 | 103 | def test_argument_with_lazy_type(): 104 | MyType = object() 105 | arg = Argument(lambda: MyType) 106 | assert arg.type == MyType 107 | 108 | 109 | def test_argument_with_lazy_partial_type(): 110 | MyType = object() 111 | arg = Argument(partial(lambda: MyType)) 112 | assert arg.type == MyType 113 | -------------------------------------------------------------------------------- /graphene/types/tests/test_base.py: -------------------------------------------------------------------------------- 1 | from ..base import BaseOptions, BaseType 2 | 3 | 4 | class CustomOptions(BaseOptions): 5 | pass 6 | 7 | 8 | class CustomType(BaseType): 9 | @classmethod 10 | def __init_subclass_with_meta__(cls, **options): 11 | _meta = CustomOptions(cls) 12 | super(CustomType, cls).__init_subclass_with_meta__(_meta=_meta, **options) 13 | 14 | 15 | def test_basetype(): 16 | class MyBaseType(CustomType): 17 | pass 18 | 19 | assert isinstance(MyBaseType._meta, CustomOptions) 20 | assert MyBaseType._meta.name == "MyBaseType" 21 | assert MyBaseType._meta.description is None 22 | 23 | 24 | def test_basetype_nones(): 25 | class MyBaseType(CustomType): 26 | """Documentation""" 27 | 28 | class Meta: 29 | name = None 30 | description = None 31 | 32 | assert isinstance(MyBaseType._meta, CustomOptions) 33 | assert MyBaseType._meta.name == "MyBaseType" 34 | assert MyBaseType._meta.description == "Documentation" 35 | 36 | 37 | def test_basetype_custom(): 38 | class MyBaseType(CustomType): 39 | """Documentation""" 40 | 41 | class Meta: 42 | name = "Base" 43 | description = "Desc" 44 | 45 | assert isinstance(MyBaseType._meta, CustomOptions) 46 | assert MyBaseType._meta.name == "Base" 47 | assert MyBaseType._meta.description == "Desc" 48 | 49 | 50 | def test_basetype_create(): 51 | MyBaseType = CustomType.create_type("MyBaseType") 52 | 53 | assert isinstance(MyBaseType._meta, CustomOptions) 54 | assert MyBaseType._meta.name == "MyBaseType" 55 | assert MyBaseType._meta.description is None 56 | 57 | 58 | def test_basetype_create_extra(): 59 | MyBaseType = CustomType.create_type("MyBaseType", name="Base", description="Desc") 60 | 61 | assert isinstance(MyBaseType._meta, CustomOptions) 62 | assert MyBaseType._meta.name == "Base" 63 | assert MyBaseType._meta.description == "Desc" 64 | -------------------------------------------------------------------------------- /graphene/types/tests/test_base64.py: -------------------------------------------------------------------------------- 1 | import base64 2 | 3 | from graphql import GraphQLError 4 | 5 | from ..objecttype import ObjectType 6 | from ..scalars import String 7 | from ..schema import Schema 8 | from ..base64 import Base64 9 | 10 | 11 | class Query(ObjectType): 12 | base64 = Base64(_in=Base64(name="input"), _match=String(name="match")) 13 | bytes_as_base64 = Base64() 14 | string_as_base64 = Base64() 15 | number_as_base64 = Base64() 16 | 17 | def resolve_base64(self, info, _in=None, _match=None): 18 | if _match: 19 | assert _in == _match 20 | return _in 21 | 22 | def resolve_bytes_as_base64(self, info): 23 | return b"Hello world" 24 | 25 | def resolve_string_as_base64(self, info): 26 | return "Spam and eggs" 27 | 28 | def resolve_number_as_base64(self, info): 29 | return 42 30 | 31 | 32 | schema = Schema(query=Query) 33 | 34 | 35 | def test_base64_query(): 36 | base64_value = base64.b64encode(b"Random string").decode("utf-8") 37 | result = schema.execute( 38 | """{{ base64(input: "{}", match: "Random string") }}""".format(base64_value) 39 | ) 40 | assert not result.errors 41 | assert result.data == {"base64": base64_value} 42 | 43 | 44 | def test_base64_query_with_variable(): 45 | base64_value = base64.b64encode(b"Another string").decode("utf-8") 46 | 47 | # test datetime variable in string representation 48 | result = schema.execute( 49 | """ 50 | query GetBase64($base64: Base64) { 51 | base64(input: $base64, match: "Another string") 52 | } 53 | """, 54 | variables={"base64": base64_value}, 55 | ) 56 | assert not result.errors 57 | assert result.data == {"base64": base64_value} 58 | 59 | 60 | def test_base64_query_none(): 61 | result = schema.execute("""{ base64 }""") 62 | assert not result.errors 63 | assert result.data == {"base64": None} 64 | 65 | 66 | def test_base64_query_invalid(): 67 | bad_inputs = [dict(), 123, "This is not valid base64"] 68 | 69 | for input_ in bad_inputs: 70 | result = schema.execute( 71 | """{ base64(input: $input) }""", variables={"input": input_} 72 | ) 73 | assert isinstance(result.errors, list) 74 | assert len(result.errors) == 1 75 | assert isinstance(result.errors[0], GraphQLError) 76 | assert result.data is None 77 | 78 | 79 | def test_base64_from_bytes(): 80 | base64_value = base64.b64encode(b"Hello world").decode("utf-8") 81 | result = schema.execute("""{ bytesAsBase64 }""") 82 | assert not result.errors 83 | assert result.data == {"bytesAsBase64": base64_value} 84 | 85 | 86 | def test_base64_from_string(): 87 | base64_value = base64.b64encode(b"Spam and eggs").decode("utf-8") 88 | result = schema.execute("""{ stringAsBase64 }""") 89 | assert not result.errors 90 | assert result.data == {"stringAsBase64": base64_value} 91 | 92 | 93 | def test_base64_from_number(): 94 | base64_value = base64.b64encode(b"42").decode("utf-8") 95 | result = schema.execute("""{ numberAsBase64 }""") 96 | assert not result.errors 97 | assert result.data == {"numberAsBase64": base64_value} 98 | -------------------------------------------------------------------------------- /graphene/types/tests/test_decimal.py: -------------------------------------------------------------------------------- 1 | import decimal 2 | 3 | from ..decimal import Decimal 4 | from ..objecttype import ObjectType 5 | from ..schema import Schema 6 | 7 | 8 | class Query(ObjectType): 9 | decimal = Decimal(input=Decimal()) 10 | 11 | def resolve_decimal(self, info, input): 12 | return input 13 | 14 | 15 | schema = Schema(query=Query) 16 | 17 | 18 | def test_decimal_string_query(): 19 | decimal_value = decimal.Decimal("1969.1974") 20 | result = schema.execute("""{ decimal(input: "%s") }""" % decimal_value) 21 | assert not result.errors 22 | assert result.data == {"decimal": str(decimal_value)} 23 | assert decimal.Decimal(result.data["decimal"]) == decimal_value 24 | 25 | 26 | def test_decimal_string_query_variable(): 27 | decimal_value = decimal.Decimal("1969.1974") 28 | 29 | result = schema.execute( 30 | """query Test($decimal: Decimal){ decimal(input: $decimal) }""", 31 | variables={"decimal": decimal_value}, 32 | ) 33 | assert not result.errors 34 | assert result.data == {"decimal": str(decimal_value)} 35 | assert decimal.Decimal(result.data["decimal"]) == decimal_value 36 | 37 | 38 | def test_bad_decimal_query(): 39 | not_a_decimal = "Nobody expects the Spanish Inquisition!" 40 | 41 | result = schema.execute("""{ decimal(input: "%s") }""" % not_a_decimal) 42 | assert result.errors 43 | assert len(result.errors) == 1 44 | assert result.data is None 45 | assert ( 46 | result.errors[0].message 47 | == "Expected value of type 'Decimal', found \"Nobody expects the Spanish Inquisition!\"." 48 | ) 49 | 50 | result = schema.execute("{ decimal(input: true) }") 51 | assert result.errors 52 | assert len(result.errors) == 1 53 | assert result.data is None 54 | assert result.errors[0].message == "Expected value of type 'Decimal', found true." 55 | 56 | result = schema.execute("{ decimal(input: 1.2) }") 57 | assert result.errors 58 | assert len(result.errors) == 1 59 | assert result.data is None 60 | assert result.errors[0].message == "Expected value of type 'Decimal', found 1.2." 61 | 62 | 63 | def test_decimal_string_query_integer(): 64 | decimal_value = 1 65 | result = schema.execute("""{ decimal(input: %s) }""" % decimal_value) 66 | assert not result.errors 67 | assert result.data == {"decimal": str(decimal_value)} 68 | assert decimal.Decimal(result.data["decimal"]) == decimal_value 69 | -------------------------------------------------------------------------------- /graphene/types/tests/test_dynamic.py: -------------------------------------------------------------------------------- 1 | from functools import partial 2 | 3 | from ..dynamic import Dynamic 4 | from ..scalars import String 5 | from ..structures import List, NonNull 6 | 7 | 8 | def test_dynamic(): 9 | dynamic = Dynamic(lambda: String) 10 | assert dynamic.get_type() == String 11 | assert str(dynamic.get_type()) == "String" 12 | 13 | 14 | def test_nonnull(): 15 | dynamic = Dynamic(lambda: NonNull(String)) 16 | assert dynamic.get_type().of_type == String 17 | assert str(dynamic.get_type()) == "String!" 18 | 19 | 20 | def test_list(): 21 | dynamic = Dynamic(lambda: List(String)) 22 | assert dynamic.get_type().of_type == String 23 | assert str(dynamic.get_type()) == "[String]" 24 | 25 | 26 | def test_list_non_null(): 27 | dynamic = Dynamic(lambda: List(NonNull(String))) 28 | assert dynamic.get_type().of_type.of_type == String 29 | assert str(dynamic.get_type()) == "[String!]" 30 | 31 | 32 | def test_partial(): 33 | def __type(_type): 34 | return _type 35 | 36 | dynamic = Dynamic(partial(__type, String)) 37 | assert dynamic.get_type() == String 38 | assert str(dynamic.get_type()) == "String" 39 | -------------------------------------------------------------------------------- /graphene/types/tests/test_field.py: -------------------------------------------------------------------------------- 1 | from functools import partial 2 | 3 | from pytest import raises 4 | 5 | from ..argument import Argument 6 | from ..field import Field 7 | from ..scalars import String 8 | from ..structures import NonNull 9 | from .utils import MyLazyType 10 | 11 | 12 | class MyInstance: 13 | value = "value" 14 | value_func = staticmethod(lambda: "value_func") 15 | 16 | def value_method(self): 17 | return "value_method" 18 | 19 | 20 | def test_field_basic(): 21 | MyType = object() 22 | args = {"my arg": Argument(True)} 23 | 24 | def resolver(): 25 | return None 26 | 27 | deprecation_reason = "Deprecated now" 28 | description = "My Field" 29 | my_default = "something" 30 | field = Field( 31 | MyType, 32 | name="name", 33 | args=args, 34 | resolver=resolver, 35 | description=description, 36 | deprecation_reason=deprecation_reason, 37 | default_value=my_default, 38 | ) 39 | assert field.name == "name" 40 | assert field.args == args 41 | assert field.resolver == resolver 42 | assert field.deprecation_reason == deprecation_reason 43 | assert field.description == description 44 | assert field.default_value == my_default 45 | 46 | 47 | def test_field_required(): 48 | MyType = object() 49 | field = Field(MyType, required=True) 50 | assert isinstance(field.type, NonNull) 51 | assert field.type.of_type == MyType 52 | 53 | 54 | def test_field_default_value_not_callable(): 55 | MyType = object() 56 | try: 57 | Field(MyType, default_value=lambda: True) 58 | except AssertionError as e: 59 | # substring comparison for py 2/3 compatibility 60 | assert "The default value can not be a function but received" in str(e) 61 | 62 | 63 | def test_field_source(): 64 | MyType = object() 65 | field = Field(MyType, source="value") 66 | assert field.resolver(MyInstance(), None) == MyInstance.value 67 | 68 | 69 | def test_field_source_dict_or_attr(): 70 | MyType = object() 71 | field = Field(MyType, source="value") 72 | assert field.resolver(MyInstance(), None) == MyInstance.value 73 | assert field.resolver({"value": MyInstance.value}, None) == MyInstance.value 74 | 75 | 76 | def test_field_with_lazy_type(): 77 | MyType = object() 78 | field = Field(lambda: MyType) 79 | assert field.type == MyType 80 | 81 | 82 | def test_field_with_lazy_partial_type(): 83 | MyType = object() 84 | field = Field(partial(lambda: MyType)) 85 | assert field.type == MyType 86 | 87 | 88 | def test_field_with_string_type(): 89 | field = Field("graphene.types.tests.utils.MyLazyType") 90 | assert field.type == MyLazyType 91 | 92 | 93 | def test_field_not_source_and_resolver(): 94 | MyType = object() 95 | with raises(Exception) as exc_info: 96 | Field(MyType, source="value", resolver=lambda: None) 97 | assert ( 98 | str(exc_info.value) 99 | == "A Field cannot have a source and a resolver in at the same time." 100 | ) 101 | 102 | 103 | def test_field_source_func(): 104 | MyType = object() 105 | field = Field(MyType, source="value_func") 106 | assert field.resolver(MyInstance(), None) == MyInstance.value_func() 107 | 108 | 109 | def test_field_source_method(): 110 | MyType = object() 111 | field = Field(MyType, source="value_method") 112 | assert field.resolver(MyInstance(), None) == MyInstance().value_method() 113 | 114 | 115 | def test_field_source_as_argument(): 116 | MyType = object() 117 | field = Field(MyType, source=String()) 118 | assert "source" in field.args 119 | assert field.args["source"].type == String 120 | 121 | 122 | def test_field_name_as_argument(): 123 | MyType = object() 124 | field = Field(MyType, name=String()) 125 | assert "name" in field.args 126 | assert field.args["name"].type == String 127 | 128 | 129 | def test_field_source_argument_as_kw(): 130 | MyType = object() 131 | deprecation_reason = "deprecated" 132 | field = Field( 133 | MyType, 134 | b=NonNull(True), 135 | c=Argument(None, deprecation_reason=deprecation_reason), 136 | a=NonNull(False), 137 | ) 138 | assert list(field.args) == ["b", "c", "a"] 139 | assert isinstance(field.args["b"], Argument) 140 | assert isinstance(field.args["b"].type, NonNull) 141 | assert field.args["b"].type.of_type is True 142 | assert isinstance(field.args["c"], Argument) 143 | assert field.args["c"].type is None 144 | assert field.args["c"].deprecation_reason == deprecation_reason 145 | assert isinstance(field.args["a"], Argument) 146 | assert isinstance(field.args["a"].type, NonNull) 147 | assert field.args["a"].type.of_type is False 148 | -------------------------------------------------------------------------------- /graphene/types/tests/test_generic.py: -------------------------------------------------------------------------------- 1 | from ..generic import GenericScalar 2 | from ..objecttype import ObjectType 3 | from ..schema import Schema 4 | 5 | 6 | class Query(ObjectType): 7 | generic = GenericScalar(input=GenericScalar()) 8 | 9 | def resolve_generic(self, info, input=None): 10 | return input 11 | 12 | 13 | schema = Schema(query=Query) 14 | 15 | 16 | def test_generic_query_variable(): 17 | for generic_value in [ 18 | 1, 19 | 1.1, 20 | True, 21 | "str", 22 | [1, 2, 3], 23 | [1.1, 2.2, 3.3], 24 | [True, False], 25 | ["str1", "str2"], 26 | {"key_a": "a", "key_b": "b"}, 27 | { 28 | "int": 1, 29 | "float": 1.1, 30 | "boolean": True, 31 | "string": "str", 32 | "int_list": [1, 2, 3], 33 | "float_list": [1.1, 2.2, 3.3], 34 | "boolean_list": [True, False], 35 | "string_list": ["str1", "str2"], 36 | "nested_dict": {"key_a": "a", "key_b": "b"}, 37 | }, 38 | None, 39 | ]: 40 | result = schema.execute( 41 | """query Test($generic: GenericScalar){ generic(input: $generic) }""", 42 | variables={"generic": generic_value}, 43 | ) 44 | assert not result.errors 45 | assert result.data == {"generic": generic_value} 46 | 47 | 48 | def test_generic_parse_literal_query(): 49 | result = schema.execute( 50 | """ 51 | query { 52 | generic(input: { 53 | int: 1, 54 | float: 1.1 55 | boolean: true, 56 | string: "str", 57 | int_list: [1, 2, 3], 58 | float_list: [1.1, 2.2, 3.3], 59 | boolean_list: [true, false] 60 | string_list: ["str1", "str2"], 61 | nested_dict: { 62 | key_a: "a", 63 | key_b: "b" 64 | }, 65 | empty_key: undefined 66 | }) 67 | } 68 | """ 69 | ) 70 | assert not result.errors 71 | assert result.data == { 72 | "generic": { 73 | "int": 1, 74 | "float": 1.1, 75 | "boolean": True, 76 | "string": "str", 77 | "int_list": [1, 2, 3], 78 | "float_list": [1.1, 2.2, 3.3], 79 | "boolean_list": [True, False], 80 | "string_list": ["str1", "str2"], 81 | "nested_dict": {"key_a": "a", "key_b": "b"}, 82 | "empty_key": None, 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /graphene/types/tests/test_inputfield.py: -------------------------------------------------------------------------------- 1 | from functools import partial 2 | 3 | from pytest import raises 4 | 5 | from ..inputfield import InputField 6 | from ..structures import NonNull 7 | from .utils import MyLazyType 8 | 9 | 10 | def test_inputfield_required(): 11 | MyType = object() 12 | field = InputField(MyType, required=True) 13 | assert isinstance(field.type, NonNull) 14 | assert field.type.of_type == MyType 15 | 16 | 17 | def test_inputfield_deprecated(): 18 | MyType = object() 19 | deprecation_reason = "deprecated" 20 | field = InputField(MyType, required=False, deprecation_reason=deprecation_reason) 21 | assert isinstance(field.type, type(MyType)) 22 | assert field.deprecation_reason == deprecation_reason 23 | 24 | 25 | def test_inputfield_required_deprecated(): 26 | MyType = object() 27 | with raises(AssertionError) as exc_info: 28 | InputField(MyType, name="input", required=True, deprecation_reason="deprecated") 29 | 30 | assert str(exc_info.value) == "InputField input is required, cannot deprecate it." 31 | 32 | 33 | def test_inputfield_with_lazy_type(): 34 | MyType = object() 35 | field = InputField(lambda: MyType) 36 | assert field.type == MyType 37 | 38 | 39 | def test_inputfield_with_lazy_partial_type(): 40 | MyType = object() 41 | field = InputField(partial(lambda: MyType)) 42 | assert field.type == MyType 43 | 44 | 45 | def test_inputfield_with_string_type(): 46 | field = InputField("graphene.types.tests.utils.MyLazyType") 47 | assert field.type == MyLazyType 48 | -------------------------------------------------------------------------------- /graphene/types/tests/test_json.py: -------------------------------------------------------------------------------- 1 | from ..json import JSONString 2 | from ..objecttype import ObjectType 3 | from ..schema import Schema 4 | 5 | 6 | class Query(ObjectType): 7 | json = JSONString(input=JSONString()) 8 | 9 | def resolve_json(self, info, input): 10 | return input 11 | 12 | 13 | schema = Schema(query=Query) 14 | 15 | 16 | def test_jsonstring_query(): 17 | json_value = '{"key": "value"}' 18 | 19 | json_value_quoted = json_value.replace('"', '\\"') 20 | result = schema.execute("""{ json(input: "%s") }""" % json_value_quoted) 21 | assert not result.errors 22 | assert result.data == {"json": json_value} 23 | 24 | result = schema.execute("""{ json(input: "{}") }""") 25 | assert not result.errors 26 | assert result.data == {"json": "{}"} 27 | 28 | 29 | def test_jsonstring_query_variable(): 30 | json_value = '{"key": "value"}' 31 | 32 | result = schema.execute( 33 | """query Test($json: JSONString){ json(input: $json) }""", 34 | variables={"json": json_value}, 35 | ) 36 | assert not result.errors 37 | assert result.data == {"json": json_value} 38 | 39 | 40 | def test_jsonstring_optional_uuid_input(): 41 | """ 42 | Test that we can provide a null value to an optional input 43 | """ 44 | result = schema.execute("{ json(input: null) }") 45 | assert not result.errors 46 | assert result.data == {"json": None} 47 | 48 | 49 | def test_jsonstring_invalid_query(): 50 | """ 51 | Test that if an invalid type is provided we get an error 52 | """ 53 | result = schema.execute("{ json(input: 1) }") 54 | assert result.errors == [ 55 | {"message": "Expected value of type 'JSONString', found 1."}, 56 | ] 57 | 58 | result = schema.execute("{ json(input: {}) }") 59 | assert result.errors == [ 60 | {"message": "Expected value of type 'JSONString', found {}."}, 61 | ] 62 | 63 | result = schema.execute('{ json(input: "a") }') 64 | assert result.errors == [ 65 | { 66 | "message": "Expected value of type 'JSONString', found \"a\"; " 67 | "Badly formed JSONString: Expecting value: line 1 column 1 (char 0)", 68 | }, 69 | ] 70 | 71 | result = schema.execute("""{ json(input: "{\\'key\\': 0}") }""") 72 | assert result.errors == [ 73 | {"message": "Syntax Error: Invalid character escape sequence: '\\''."}, 74 | ] 75 | 76 | result = schema.execute("""{ json(input: "{\\"key\\": 0,}") }""") 77 | assert len(result.errors) == 1 78 | assert result.errors[0].message.startswith( 79 | 'Expected value of type \'JSONString\', found "{\\"key\\": 0,}"; Badly formed JSONString:' 80 | ) 81 | -------------------------------------------------------------------------------- /graphene/types/tests/test_mountedtype.py: -------------------------------------------------------------------------------- 1 | from ..field import Field 2 | from ..scalars import String 3 | 4 | 5 | class CustomField(Field): 6 | def __init__(self, *args, **kwargs): 7 | self.metadata = kwargs.pop("metadata", None) 8 | super(CustomField, self).__init__(*args, **kwargs) 9 | 10 | 11 | def test_mounted_type(): 12 | unmounted = String() 13 | mounted = Field.mounted(unmounted) 14 | assert isinstance(mounted, Field) 15 | assert mounted.type == String 16 | 17 | 18 | def test_mounted_type_custom(): 19 | unmounted = String(metadata={"hey": "yo!"}) 20 | mounted = CustomField.mounted(unmounted) 21 | assert isinstance(mounted, CustomField) 22 | assert mounted.type == String 23 | assert mounted.metadata == {"hey": "yo!"} 24 | -------------------------------------------------------------------------------- /graphene/types/tests/test_resolver.py: -------------------------------------------------------------------------------- 1 | from ..resolver import ( 2 | attr_resolver, 3 | dict_resolver, 4 | dict_or_attr_resolver, 5 | get_default_resolver, 6 | set_default_resolver, 7 | ) 8 | 9 | args = {} 10 | context = None 11 | info = None 12 | 13 | demo_dict = {"attr": "value"} 14 | 15 | 16 | class demo_obj: 17 | attr = "value" 18 | 19 | 20 | def test_attr_resolver(): 21 | resolved = attr_resolver("attr", None, demo_obj, info, **args) 22 | assert resolved == "value" 23 | 24 | 25 | def test_attr_resolver_default_value(): 26 | resolved = attr_resolver("attr2", "default", demo_obj, info, **args) 27 | assert resolved == "default" 28 | 29 | 30 | def test_dict_resolver(): 31 | resolved = dict_resolver("attr", None, demo_dict, info, **args) 32 | assert resolved == "value" 33 | 34 | 35 | def test_dict_resolver_default_value(): 36 | resolved = dict_resolver("attr2", "default", demo_dict, info, **args) 37 | assert resolved == "default" 38 | 39 | 40 | def test_dict_or_attr_resolver(): 41 | resolved = dict_or_attr_resolver("attr", None, demo_dict, info, **args) 42 | assert resolved == "value" 43 | 44 | resolved = dict_or_attr_resolver("attr", None, demo_obj, info, **args) 45 | assert resolved == "value" 46 | 47 | 48 | def test_get_default_resolver_is_attr_resolver(): 49 | assert get_default_resolver() == dict_or_attr_resolver 50 | 51 | 52 | def test_set_default_resolver_workd(): 53 | default_resolver = get_default_resolver() 54 | 55 | set_default_resolver(dict_resolver) 56 | assert get_default_resolver() == dict_resolver 57 | 58 | set_default_resolver(default_resolver) 59 | -------------------------------------------------------------------------------- /graphene/types/tests/test_scalars_serialization.py: -------------------------------------------------------------------------------- 1 | from graphql import Undefined 2 | from ..scalars import Boolean, Float, Int, String 3 | 4 | 5 | def test_serializes_output_int(): 6 | assert Int.serialize(1) == 1 7 | assert Int.serialize(0) == 0 8 | assert Int.serialize(-1) == -1 9 | assert Int.serialize(0.1) == 0 10 | assert Int.serialize(1.1) == 1 11 | assert Int.serialize(-1.1) == -1 12 | assert Int.serialize(1e5) == 100000 13 | assert Int.serialize(9876504321) is Undefined 14 | assert Int.serialize(-9876504321) is Undefined 15 | assert Int.serialize(1e100) is Undefined 16 | assert Int.serialize(-1e100) is Undefined 17 | assert Int.serialize("-1.1") == -1 18 | assert Int.serialize("one") is Undefined 19 | assert Int.serialize(False) == 0 20 | assert Int.serialize(True) == 1 21 | 22 | 23 | def test_serializes_output_float(): 24 | assert Float.serialize(1) == 1.0 25 | assert Float.serialize(0) == 0.0 26 | assert Float.serialize(-1) == -1.0 27 | assert Float.serialize(0.1) == 0.1 28 | assert Float.serialize(1.1) == 1.1 29 | assert Float.serialize(-1.1) == -1.1 30 | assert Float.serialize("-1.1") == -1.1 31 | assert Float.serialize("one") is Undefined 32 | assert Float.serialize(False) == 0 33 | assert Float.serialize(True) == 1 34 | 35 | 36 | def test_serializes_output_string(): 37 | assert String.serialize("string") == "string" 38 | assert String.serialize(1) == "1" 39 | assert String.serialize(-1.1) == "-1.1" 40 | assert String.serialize(True) == "true" 41 | assert String.serialize(False) == "false" 42 | assert String.serialize("\U0001f601") == "\U0001f601" 43 | 44 | 45 | def test_serializes_output_boolean(): 46 | assert Boolean.serialize("string") is True 47 | assert Boolean.serialize("") is False 48 | assert Boolean.serialize(1) is True 49 | assert Boolean.serialize(0) is False 50 | assert Boolean.serialize(True) is True 51 | assert Boolean.serialize(False) is False 52 | -------------------------------------------------------------------------------- /graphene/types/tests/test_schema.py: -------------------------------------------------------------------------------- 1 | from textwrap import dedent 2 | 3 | from pytest import raises 4 | 5 | from graphql.type import GraphQLObjectType, GraphQLSchema 6 | 7 | from ..field import Field 8 | from ..objecttype import ObjectType 9 | from ..scalars import String 10 | from ..schema import Schema 11 | 12 | 13 | class MyOtherType(ObjectType): 14 | field = String() 15 | 16 | 17 | class Query(ObjectType): 18 | inner = Field(MyOtherType) 19 | 20 | 21 | def test_schema(): 22 | schema = Schema(Query) 23 | graphql_schema = schema.graphql_schema 24 | assert isinstance(graphql_schema, GraphQLSchema) 25 | query_type = graphql_schema.query_type 26 | assert isinstance(query_type, GraphQLObjectType) 27 | assert query_type.name == "Query" 28 | assert query_type.graphene_type is Query 29 | 30 | 31 | def test_schema_get_type(): 32 | schema = Schema(Query) 33 | assert schema.Query == Query 34 | assert schema.MyOtherType == MyOtherType 35 | 36 | 37 | def test_schema_get_type_error(): 38 | schema = Schema(Query) 39 | with raises(AttributeError) as exc_info: 40 | schema.X 41 | 42 | assert str(exc_info.value) == 'Type "X" not found in the Schema' 43 | 44 | 45 | def test_schema_str(): 46 | schema = Schema(Query) 47 | assert ( 48 | str(schema).strip() 49 | == dedent( 50 | """ 51 | type Query { 52 | inner: MyOtherType 53 | } 54 | 55 | type MyOtherType { 56 | field: String 57 | } 58 | """ 59 | ).strip() 60 | ) 61 | 62 | 63 | def test_schema_introspect(): 64 | schema = Schema(Query) 65 | assert "__schema" in schema.introspect() 66 | 67 | 68 | def test_schema_requires_query_type(): 69 | schema = Schema() 70 | result = schema.execute("query {}") 71 | 72 | assert len(result.errors) == 1 73 | error = result.errors[0] 74 | assert error.message == "Query root type must be provided." 75 | -------------------------------------------------------------------------------- /graphene/types/tests/test_structures.py: -------------------------------------------------------------------------------- 1 | from functools import partial 2 | 3 | from pytest import raises 4 | 5 | from ..scalars import String 6 | from ..structures import List, NonNull 7 | from .utils import MyLazyType 8 | 9 | 10 | def test_list(): 11 | _list = List(String) 12 | assert _list.of_type == String 13 | assert str(_list) == "[String]" 14 | 15 | 16 | def test_list_with_unmounted_type(): 17 | with raises(Exception) as exc_info: 18 | List(String()) 19 | 20 | assert ( 21 | str(exc_info.value) 22 | == "List could not have a mounted String() as inner type. Try with List(String)." 23 | ) 24 | 25 | 26 | def test_list_with_lazy_type(): 27 | MyType = object() 28 | field = List(lambda: MyType) 29 | assert field.of_type == MyType 30 | 31 | 32 | def test_list_with_lazy_partial_type(): 33 | MyType = object() 34 | field = List(partial(lambda: MyType)) 35 | assert field.of_type == MyType 36 | 37 | 38 | def test_list_with_string_type(): 39 | field = List("graphene.types.tests.utils.MyLazyType") 40 | assert field.of_type == MyLazyType 41 | 42 | 43 | def test_list_inherited_works_list(): 44 | _list = List(List(String)) 45 | assert isinstance(_list.of_type, List) 46 | assert _list.of_type.of_type == String 47 | 48 | 49 | def test_list_inherited_works_nonnull(): 50 | _list = List(NonNull(String)) 51 | assert isinstance(_list.of_type, NonNull) 52 | assert _list.of_type.of_type == String 53 | 54 | 55 | def test_nonnull(): 56 | nonnull = NonNull(String) 57 | assert nonnull.of_type == String 58 | assert str(nonnull) == "String!" 59 | 60 | 61 | def test_nonnull_with_lazy_type(): 62 | MyType = object() 63 | field = NonNull(lambda: MyType) 64 | assert field.of_type == MyType 65 | 66 | 67 | def test_nonnull_with_lazy_partial_type(): 68 | MyType = object() 69 | field = NonNull(partial(lambda: MyType)) 70 | assert field.of_type == MyType 71 | 72 | 73 | def test_nonnull_with_string_type(): 74 | field = NonNull("graphene.types.tests.utils.MyLazyType") 75 | assert field.of_type == MyLazyType 76 | 77 | 78 | def test_nonnull_inherited_works_list(): 79 | _list = NonNull(List(String)) 80 | assert isinstance(_list.of_type, List) 81 | assert _list.of_type.of_type == String 82 | 83 | 84 | def test_nonnull_inherited_dont_work_nonnull(): 85 | with raises(Exception) as exc_info: 86 | NonNull(NonNull(String)) 87 | 88 | assert ( 89 | str(exc_info.value) 90 | == "Can only create NonNull of a Nullable GraphQLType but got: String!." 91 | ) 92 | 93 | 94 | def test_nonnull_with_unmounted_type(): 95 | with raises(Exception) as exc_info: 96 | NonNull(String()) 97 | 98 | assert ( 99 | str(exc_info.value) 100 | == "NonNull could not have a mounted String() as inner type. Try with NonNull(String)." 101 | ) 102 | 103 | 104 | def test_list_comparasion(): 105 | list1 = List(String) 106 | list2 = List(String) 107 | list3 = List(None) 108 | 109 | list1_argskwargs = List(String, None, b=True) 110 | list2_argskwargs = List(String, None, b=True) 111 | 112 | assert list1 == list2 113 | assert list1 != list3 114 | assert list1_argskwargs == list2_argskwargs 115 | assert list1 != list1_argskwargs 116 | 117 | 118 | def test_nonnull_comparasion(): 119 | nonnull1 = NonNull(String) 120 | nonnull2 = NonNull(String) 121 | nonnull3 = NonNull(None) 122 | 123 | nonnull1_argskwargs = NonNull(String, None, b=True) 124 | nonnull2_argskwargs = NonNull(String, None, b=True) 125 | 126 | assert nonnull1 == nonnull2 127 | assert nonnull1 != nonnull3 128 | assert nonnull1_argskwargs == nonnull2_argskwargs 129 | assert nonnull1 != nonnull1_argskwargs 130 | -------------------------------------------------------------------------------- /graphene/types/tests/test_subscribe_async.py: -------------------------------------------------------------------------------- 1 | from pytest import mark 2 | 3 | from graphene import ObjectType, Int, String, Schema, Field 4 | 5 | 6 | class Query(ObjectType): 7 | hello = String() 8 | 9 | def resolve_hello(root, info): 10 | return "Hello, world!" 11 | 12 | 13 | class Subscription(ObjectType): 14 | count_to_ten = Field(Int) 15 | 16 | async def subscribe_count_to_ten(root, info): 17 | for count in range(1, 11): 18 | yield count 19 | 20 | 21 | schema = Schema(query=Query, subscription=Subscription) 22 | 23 | 24 | @mark.asyncio 25 | async def test_subscription(): 26 | subscription = "subscription { countToTen }" 27 | result = await schema.subscribe(subscription) 28 | count = 0 29 | async for item in result: 30 | count = item.data["countToTen"] 31 | assert count == 10 32 | 33 | 34 | @mark.asyncio 35 | async def test_subscription_fails_with_invalid_query(): 36 | # It fails if the provided query is invalid 37 | subscription = "subscription { " 38 | result = await schema.subscribe(subscription) 39 | assert not result.data 40 | assert result.errors 41 | assert "Syntax Error: Expected Name, found " in str(result.errors[0]) 42 | 43 | 44 | @mark.asyncio 45 | async def test_subscription_fails_when_query_is_not_valid(): 46 | # It can't subscribe to two fields at the same time, triggering a 47 | # validation error. 48 | subscription = "subscription { countToTen, b: countToTen }" 49 | result = await schema.subscribe(subscription) 50 | assert not result.data 51 | assert result.errors 52 | assert "Anonymous Subscription must select only one top level field." in str( 53 | result.errors[0] 54 | ) 55 | 56 | 57 | @mark.asyncio 58 | async def test_subscription_with_args(): 59 | class Query(ObjectType): 60 | hello = String() 61 | 62 | class Subscription(ObjectType): 63 | count_upwards = Field(Int, limit=Int(required=True)) 64 | 65 | async def subscribe_count_upwards(root, info, limit): 66 | count = 0 67 | while count < limit: 68 | count += 1 69 | yield count 70 | 71 | schema = Schema(query=Query, subscription=Subscription) 72 | 73 | subscription = "subscription { countUpwards(limit: 5) }" 74 | result = await schema.subscribe(subscription) 75 | count = 0 76 | async for item in result: 77 | count = item.data["countUpwards"] 78 | assert count == 5 79 | -------------------------------------------------------------------------------- /graphene/types/tests/test_union.py: -------------------------------------------------------------------------------- 1 | from pytest import raises 2 | 3 | from ..field import Field 4 | from ..objecttype import ObjectType 5 | from ..union import Union 6 | from ..unmountedtype import UnmountedType 7 | 8 | 9 | class MyObjectType1(ObjectType): 10 | pass 11 | 12 | 13 | class MyObjectType2(ObjectType): 14 | pass 15 | 16 | 17 | def test_generate_union(): 18 | class MyUnion(Union): 19 | """Documentation""" 20 | 21 | class Meta: 22 | types = (MyObjectType1, MyObjectType2) 23 | 24 | assert MyUnion._meta.name == "MyUnion" 25 | assert MyUnion._meta.description == "Documentation" 26 | assert MyUnion._meta.types == (MyObjectType1, MyObjectType2) 27 | 28 | 29 | def test_generate_union_with_meta(): 30 | class MyUnion(Union): 31 | class Meta: 32 | name = "MyOtherUnion" 33 | description = "Documentation" 34 | types = (MyObjectType1, MyObjectType2) 35 | 36 | assert MyUnion._meta.name == "MyOtherUnion" 37 | assert MyUnion._meta.description == "Documentation" 38 | 39 | 40 | def test_generate_union_with_no_types(): 41 | with raises(Exception) as exc_info: 42 | 43 | class MyUnion(Union): 44 | pass 45 | 46 | assert str(exc_info.value) == "Must provide types for Union MyUnion." 47 | 48 | 49 | def test_union_can_be_mounted(): 50 | class MyUnion(Union): 51 | class Meta: 52 | types = (MyObjectType1, MyObjectType2) 53 | 54 | my_union_instance = MyUnion() 55 | assert isinstance(my_union_instance, UnmountedType) 56 | my_union_field = my_union_instance.mount_as(Field) 57 | assert isinstance(my_union_field, Field) 58 | assert my_union_field.type == MyUnion 59 | -------------------------------------------------------------------------------- /graphene/types/tests/test_uuid.py: -------------------------------------------------------------------------------- 1 | from ..objecttype import ObjectType 2 | from ..schema import Schema 3 | from ..uuid import UUID 4 | from ..structures import NonNull 5 | 6 | 7 | class Query(ObjectType): 8 | uuid = UUID(input=UUID()) 9 | required_uuid = UUID(input=NonNull(UUID), required=True) 10 | 11 | def resolve_uuid(self, info, input): 12 | return input 13 | 14 | def resolve_required_uuid(self, info, input): 15 | return input 16 | 17 | 18 | schema = Schema(query=Query) 19 | 20 | 21 | def test_uuidstring_query(): 22 | uuid_value = "dfeb3bcf-70fd-11e7-a61a-6003088f8204" 23 | result = schema.execute("""{ uuid(input: "%s") }""" % uuid_value) 24 | assert not result.errors 25 | assert result.data == {"uuid": uuid_value} 26 | 27 | 28 | def test_uuidstring_query_variable(): 29 | uuid_value = "dfeb3bcf-70fd-11e7-a61a-6003088f8204" 30 | 31 | result = schema.execute( 32 | """query Test($uuid: UUID){ uuid(input: $uuid) }""", 33 | variables={"uuid": uuid_value}, 34 | ) 35 | assert not result.errors 36 | assert result.data == {"uuid": uuid_value} 37 | 38 | 39 | def test_uuidstring_invalid_argument(): 40 | uuid_value = {"not": "a string"} 41 | 42 | result = schema.execute( 43 | """query Test($uuid: UUID){ uuid(input: $uuid) }""", 44 | variables={"uuid": uuid_value}, 45 | ) 46 | assert result.errors 47 | assert len(result.errors) == 1 48 | assert ( 49 | result.errors[0].message 50 | == "Variable '$uuid' got invalid value {'not': 'a string'}; UUID cannot represent value: {'not': 'a string'}" 51 | ) 52 | 53 | 54 | def test_uuidstring_optional_uuid_input(): 55 | """ 56 | Test that we can provide a null value to an optional input 57 | """ 58 | result = schema.execute("{ uuid(input: null) }") 59 | assert not result.errors 60 | assert result.data == {"uuid": None} 61 | 62 | 63 | def test_uuidstring_invalid_query(): 64 | """ 65 | Test that if an invalid type is provided we get an error 66 | """ 67 | result = schema.execute("{ uuid(input: 1) }") 68 | assert result.errors 69 | assert len(result.errors) == 1 70 | assert result.errors[0].message == "Expected value of type 'UUID', found 1." 71 | 72 | result = schema.execute('{ uuid(input: "a") }') 73 | assert result.errors 74 | assert len(result.errors) == 1 75 | assert ( 76 | result.errors[0].message 77 | == "Expected value of type 'UUID', found \"a\"; badly formed hexadecimal UUID string" 78 | ) 79 | 80 | result = schema.execute("{ requiredUuid(input: null) }") 81 | assert result.errors 82 | assert len(result.errors) == 1 83 | assert result.errors[0].message == "Expected value of type 'UUID!', found null." 84 | -------------------------------------------------------------------------------- /graphene/types/tests/utils.py: -------------------------------------------------------------------------------- 1 | MyLazyType = object() 2 | -------------------------------------------------------------------------------- /graphene/types/union.py: -------------------------------------------------------------------------------- 1 | from typing import TYPE_CHECKING 2 | 3 | from .base import BaseOptions, BaseType 4 | from .unmountedtype import UnmountedType 5 | 6 | # For static type checking with type checker 7 | if TYPE_CHECKING: 8 | from .objecttype import ObjectType # NOQA 9 | from typing import Iterable, Type # NOQA 10 | 11 | 12 | class UnionOptions(BaseOptions): 13 | types = () # type: Iterable[Type[ObjectType]] 14 | 15 | 16 | class Union(UnmountedType, BaseType): 17 | """ 18 | Union Type Definition 19 | 20 | When a field can return one of a heterogeneous set of types, a Union type 21 | is used to describe what types are possible as well as providing a function 22 | to determine which type is actually used when the field is resolved. 23 | 24 | The schema in this example can take a search text and return any of the GraphQL object types 25 | indicated: Human, Droid or Starship. 26 | 27 | Ambiguous return types can be resolved on each ObjectType through ``Meta.possible_types`` 28 | attribute or ``is_type_of`` method. Or by implementing ``resolve_type`` class method on the 29 | Union. 30 | 31 | .. code:: python 32 | 33 | from graphene import Union, ObjectType, List 34 | 35 | class SearchResult(Union): 36 | class Meta: 37 | types = (Human, Droid, Starship) 38 | 39 | class Query(ObjectType): 40 | search = List(SearchResult.Field( 41 | search_text=String(description='Value to search for')) 42 | ) 43 | 44 | Meta: 45 | types (Iterable[graphene.ObjectType]): Required. Collection of types that may be returned 46 | by this Union for the graphQL schema. 47 | name (optional, str): the name of the GraphQL type (must be unique in schema). Defaults to class 48 | name. 49 | description (optional, str): the description of the GraphQL type in the schema. Defaults to class 50 | docstring. 51 | """ 52 | 53 | @classmethod 54 | def __init_subclass_with_meta__(cls, types=None, _meta=None, **options): 55 | assert ( 56 | isinstance(types, (list, tuple)) and len(types) > 0 57 | ), f"Must provide types for Union {cls.__name__}." 58 | 59 | if not _meta: 60 | _meta = UnionOptions(cls) 61 | 62 | _meta.types = types 63 | super(Union, cls).__init_subclass_with_meta__(_meta=_meta, **options) 64 | 65 | @classmethod 66 | def get_type(cls): 67 | """ 68 | This function is called when the unmounted type (Union instance) 69 | is mounted (as a Field, InputField or Argument) 70 | """ 71 | return cls 72 | 73 | @classmethod 74 | def resolve_type(cls, instance, info): 75 | from .objecttype import ObjectType # NOQA 76 | 77 | if isinstance(instance, ObjectType): 78 | return type(instance) 79 | -------------------------------------------------------------------------------- /graphene/types/unmountedtype.py: -------------------------------------------------------------------------------- 1 | from ..utils.orderedtype import OrderedType 2 | 3 | 4 | class UnmountedType(OrderedType): 5 | """ 6 | This class acts a proxy for a Graphene Type, so it can be mounted 7 | dynamically as Field, InputField or Argument. 8 | 9 | Instead of writing: 10 | 11 | .. code:: python 12 | 13 | from graphene import ObjectType, Field, String 14 | 15 | class MyObjectType(ObjectType): 16 | my_field = Field(String, description='Description here') 17 | 18 | It lets you write: 19 | 20 | .. code:: python 21 | 22 | from graphene import ObjectType, String 23 | 24 | class MyObjectType(ObjectType): 25 | my_field = String(description='Description here') 26 | 27 | It is not used directly, but is inherited by other types and streamlines their use in 28 | different context: 29 | 30 | - Object Type 31 | - Scalar Type 32 | - Enum 33 | - Interface 34 | - Union 35 | 36 | An unmounted type will accept arguments based upon its context (ObjectType, Field or 37 | InputObjectType) and pass it on to the appropriate MountedType (Field, Argument or InputField). 38 | 39 | See each Mounted type reference for more information about valid parameters. 40 | """ 41 | 42 | def __init__(self, *args, **kwargs): 43 | super(UnmountedType, self).__init__() 44 | self.args = args 45 | self.kwargs = kwargs 46 | 47 | def get_type(self): 48 | """ 49 | This function is called when the UnmountedType instance 50 | is mounted (as a Field, InputField or Argument) 51 | """ 52 | raise NotImplementedError(f"get_type not implemented in {self}") 53 | 54 | def mount_as(self, _as): 55 | return _as.mounted(self) 56 | 57 | def Field(self): # noqa: N802 58 | """ 59 | Mount the UnmountedType as Field 60 | """ 61 | from .field import Field 62 | 63 | return self.mount_as(Field) 64 | 65 | def InputField(self): # noqa: N802 66 | """ 67 | Mount the UnmountedType as InputField 68 | """ 69 | from .inputfield import InputField 70 | 71 | return self.mount_as(InputField) 72 | 73 | def Argument(self): # noqa: N802 74 | """ 75 | Mount the UnmountedType as Argument 76 | """ 77 | from .argument import Argument 78 | 79 | return self.mount_as(Argument) 80 | 81 | def __eq__(self, other): 82 | return self is other or ( 83 | isinstance(other, UnmountedType) 84 | and self.get_type() == other.get_type() 85 | and self.args == other.args 86 | and self.kwargs == other.kwargs 87 | ) 88 | -------------------------------------------------------------------------------- /graphene/types/utils.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | from functools import partial 3 | 4 | from ..utils.module_loading import import_string 5 | from .mountedtype import MountedType 6 | from .unmountedtype import UnmountedType 7 | 8 | 9 | def get_field_as(value, _as=None): 10 | """ 11 | Get type mounted 12 | """ 13 | if isinstance(value, MountedType): 14 | return value 15 | elif isinstance(value, UnmountedType): 16 | if _as is None: 17 | return value 18 | return _as.mounted(value) 19 | 20 | 21 | def yank_fields_from_attrs(attrs, _as=None, sort=True): 22 | """ 23 | Extract all the fields in given attributes (dict) 24 | and return them ordered 25 | """ 26 | fields_with_names = [] 27 | for attname, value in list(attrs.items()): 28 | field = get_field_as(value, _as) 29 | if not field: 30 | continue 31 | fields_with_names.append((attname, field)) 32 | 33 | if sort: 34 | fields_with_names = sorted(fields_with_names, key=lambda f: f[1]) 35 | return dict(fields_with_names) 36 | 37 | 38 | def get_type(_type): 39 | if isinstance(_type, str): 40 | return import_string(_type) 41 | if inspect.isfunction(_type) or isinstance(_type, partial): 42 | return _type() 43 | return _type 44 | 45 | 46 | def get_underlying_type(_type): 47 | """Get the underlying type even if it is wrapped in structures like NonNull""" 48 | while hasattr(_type, "of_type"): 49 | _type = _type.of_type 50 | return _type 51 | -------------------------------------------------------------------------------- /graphene/types/uuid.py: -------------------------------------------------------------------------------- 1 | from uuid import UUID as _UUID 2 | 3 | from graphql.error import GraphQLError 4 | from graphql.language.ast import StringValueNode 5 | from graphql import Undefined 6 | 7 | from .scalars import Scalar 8 | 9 | 10 | class UUID(Scalar): 11 | """ 12 | Leverages the internal Python implementation of UUID (uuid.UUID) to provide native UUID objects 13 | in fields, resolvers and input. 14 | """ 15 | 16 | @staticmethod 17 | def serialize(uuid): 18 | if isinstance(uuid, str): 19 | uuid = _UUID(uuid) 20 | 21 | assert isinstance(uuid, _UUID), f"Expected UUID instance, received {uuid}" 22 | return str(uuid) 23 | 24 | @staticmethod 25 | def parse_literal(node, _variables=None): 26 | if isinstance(node, StringValueNode): 27 | return _UUID(node.value) 28 | return Undefined 29 | 30 | @staticmethod 31 | def parse_value(value): 32 | if isinstance(value, _UUID): 33 | return value 34 | try: 35 | return _UUID(value) 36 | except (ValueError, AttributeError): 37 | raise GraphQLError(f"UUID cannot represent value: {repr(value)}") 38 | -------------------------------------------------------------------------------- /graphene/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphql-python/graphene/82903263080b3b7f22c2ad84319584d7a3b1a1f6/graphene/utils/__init__.py -------------------------------------------------------------------------------- /graphene/utils/crunch.py: -------------------------------------------------------------------------------- 1 | import json 2 | from collections.abc import Mapping 3 | 4 | 5 | def to_key(value): 6 | return json.dumps(value) 7 | 8 | 9 | def insert(value, index, values): 10 | key = to_key(value) 11 | 12 | if key not in index: 13 | index[key] = len(values) 14 | values.append(value) 15 | return len(values) - 1 16 | 17 | return index.get(key) 18 | 19 | 20 | def flatten(data, index, values): 21 | if isinstance(data, (list, tuple)): 22 | flattened = [flatten(child, index, values) for child in data] 23 | elif isinstance(data, Mapping): 24 | flattened = {key: flatten(child, index, values) for key, child in data.items()} 25 | else: 26 | flattened = data 27 | return insert(flattened, index, values) 28 | 29 | 30 | def crunch(data): 31 | index = {} 32 | values = [] 33 | 34 | flatten(data, index, values) 35 | return values 36 | -------------------------------------------------------------------------------- /graphene/utils/deduplicator.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Mapping 2 | 3 | 4 | def deflate(node, index=None, path=None): 5 | if index is None: 6 | index = {} 7 | if path is None: 8 | path = [] 9 | 10 | if node and "id" in node and "__typename" in node: 11 | route = ",".join(path) 12 | cache_key = ":".join([route, str(node["__typename"]), str(node["id"])]) 13 | 14 | if index.get(cache_key) is True: 15 | return {"__typename": node["__typename"], "id": node["id"]} 16 | else: 17 | index[cache_key] = True 18 | 19 | result = {} 20 | 21 | for field_name in node: 22 | value = node[field_name] 23 | 24 | new_path = path + [field_name] 25 | if isinstance(value, (list, tuple)): 26 | result[field_name] = [deflate(child, index, new_path) for child in value] 27 | elif isinstance(value, Mapping): 28 | result[field_name] = deflate(value, index, new_path) 29 | else: 30 | result[field_name] = value 31 | 32 | return result 33 | -------------------------------------------------------------------------------- /graphene/utils/deprecated.py: -------------------------------------------------------------------------------- 1 | from warnings import warn 2 | 3 | 4 | def warn_deprecation(text: str): 5 | warn(text, category=DeprecationWarning, stacklevel=2) 6 | -------------------------------------------------------------------------------- /graphene/utils/get_unbound_function.py: -------------------------------------------------------------------------------- 1 | def get_unbound_function(func): 2 | if not getattr(func, "__self__", True): 3 | return func.__func__ 4 | return func 5 | -------------------------------------------------------------------------------- /graphene/utils/is_introspection_key.py: -------------------------------------------------------------------------------- 1 | def is_introspection_key(key): 2 | # from: https://spec.graphql.org/June2018/#sec-Schema 3 | # > All types and directives defined within a schema must not have a name which 4 | # > begins with "__" (two underscores), as this is used exclusively 5 | # > by GraphQL’s introspection system. 6 | return str(key).startswith("__") 7 | -------------------------------------------------------------------------------- /graphene/utils/module_loading.py: -------------------------------------------------------------------------------- 1 | from functools import partial 2 | from importlib import import_module 3 | 4 | 5 | def import_string(dotted_path, dotted_attributes=None): 6 | """ 7 | Import a dotted module path and return the attribute/class designated by the 8 | last name in the path. When a dotted attribute path is also provided, the 9 | dotted attribute path would be applied to the attribute/class retrieved from 10 | the first step, and return the corresponding value designated by the 11 | attribute path. Raise ImportError if the import failed. 12 | """ 13 | try: 14 | module_path, class_name = dotted_path.rsplit(".", 1) 15 | except ValueError: 16 | raise ImportError("%s doesn't look like a module path" % dotted_path) 17 | 18 | module = import_module(module_path) 19 | 20 | try: 21 | result = getattr(module, class_name) 22 | except AttributeError: 23 | raise ImportError( 24 | 'Module "%s" does not define a "%s" attribute/class' 25 | % (module_path, class_name) 26 | ) 27 | 28 | if not dotted_attributes: 29 | return result 30 | attributes = dotted_attributes.split(".") 31 | traveled_attributes = [] 32 | try: 33 | for attribute in attributes: 34 | traveled_attributes.append(attribute) 35 | result = getattr(result, attribute) 36 | return result 37 | except AttributeError: 38 | raise ImportError( 39 | 'Module "%s" does not define a "%s" attribute inside attribute/class "%s"' 40 | % (module_path, ".".join(traveled_attributes), class_name) 41 | ) 42 | 43 | 44 | def lazy_import(dotted_path, dotted_attributes=None): 45 | return partial(import_string, dotted_path, dotted_attributes) 46 | -------------------------------------------------------------------------------- /graphene/utils/orderedtype.py: -------------------------------------------------------------------------------- 1 | from functools import total_ordering 2 | 3 | 4 | @total_ordering 5 | class OrderedType: 6 | creation_counter = 1 7 | 8 | def __init__(self, _creation_counter=None): 9 | self.creation_counter = _creation_counter or self.gen_counter() 10 | 11 | @staticmethod 12 | def gen_counter(): 13 | counter = OrderedType.creation_counter 14 | OrderedType.creation_counter += 1 15 | return counter 16 | 17 | def reset_counter(self): 18 | self.creation_counter = self.gen_counter() 19 | 20 | def __eq__(self, other): 21 | # Needed for @total_ordering 22 | if isinstance(self, type(other)): 23 | return self.creation_counter == other.creation_counter 24 | return NotImplemented 25 | 26 | def __lt__(self, other): 27 | # This is needed because bisect does not take a comparison function. 28 | if isinstance(other, OrderedType): 29 | return self.creation_counter < other.creation_counter 30 | return NotImplemented 31 | 32 | def __gt__(self, other): 33 | # This is needed because bisect does not take a comparison function. 34 | if isinstance(other, OrderedType): 35 | return self.creation_counter > other.creation_counter 36 | return NotImplemented 37 | 38 | def __hash__(self): 39 | return hash(self.creation_counter) 40 | -------------------------------------------------------------------------------- /graphene/utils/props.py: -------------------------------------------------------------------------------- 1 | class _OldClass: 2 | pass 3 | 4 | 5 | class _NewClass: 6 | pass 7 | 8 | 9 | _all_vars = set(dir(_OldClass) + dir(_NewClass)) 10 | 11 | 12 | def props(x): 13 | return { 14 | key: vars(x).get(key, getattr(x, key)) for key in dir(x) if key not in _all_vars 15 | } 16 | -------------------------------------------------------------------------------- /graphene/utils/resolve_only_args.py: -------------------------------------------------------------------------------- 1 | from functools import wraps 2 | from typing_extensions import deprecated 3 | 4 | 5 | @deprecated("This function is deprecated") 6 | def resolve_only_args(func): 7 | @wraps(func) 8 | def wrapped_func(root, info, **args): 9 | return func(root, **args) 10 | 11 | return wrapped_func 12 | -------------------------------------------------------------------------------- /graphene/utils/str_converters.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | 4 | # Adapted from this response in Stackoverflow 5 | # http://stackoverflow.com/a/19053800/1072990 6 | def to_camel_case(snake_str): 7 | components = snake_str.split("_") 8 | # We capitalize the first letter of each component except the first one 9 | # with the 'capitalize' method and join them together. 10 | return components[0] + "".join(x.capitalize() if x else "_" for x in components[1:]) 11 | 12 | 13 | # From this response in Stackoverflow 14 | # http://stackoverflow.com/a/1176023/1072990 15 | def to_snake_case(name): 16 | s1 = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", name) 17 | return re.sub("([a-z0-9])([A-Z])", r"\1_\2", s1).lower() 18 | -------------------------------------------------------------------------------- /graphene/utils/subclass_with_meta.py: -------------------------------------------------------------------------------- 1 | from inspect import isclass 2 | 3 | from .props import props 4 | 5 | 6 | class SubclassWithMeta_Meta(type): 7 | _meta = None 8 | 9 | def __str__(cls): 10 | if cls._meta: 11 | return cls._meta.name 12 | return cls.__name__ 13 | 14 | def __repr__(cls): 15 | return f"<{cls.__name__} meta={repr(cls._meta)}>" 16 | 17 | 18 | class SubclassWithMeta(metaclass=SubclassWithMeta_Meta): 19 | """This class improves __init_subclass__ to receive automatically the options from meta""" 20 | 21 | def __init_subclass__(cls, **meta_options): 22 | """This method just terminates the super() chain""" 23 | _Meta = getattr(cls, "Meta", None) 24 | _meta_props = {} 25 | if _Meta: 26 | if isinstance(_Meta, dict): 27 | _meta_props = _Meta 28 | elif isclass(_Meta): 29 | _meta_props = props(_Meta) 30 | else: 31 | raise Exception( 32 | f"Meta have to be either a class or a dict. Received {_Meta}" 33 | ) 34 | delattr(cls, "Meta") 35 | options = dict(meta_options, **_meta_props) 36 | 37 | abstract = options.pop("abstract", False) 38 | if abstract: 39 | assert not options, ( 40 | "Abstract types can only contain the abstract attribute. " 41 | f"Received: abstract, {', '.join(options)}" 42 | ) 43 | else: 44 | super_class = super(cls, cls) 45 | if hasattr(super_class, "__init_subclass_with_meta__"): 46 | super_class.__init_subclass_with_meta__(**options) 47 | 48 | @classmethod 49 | def __init_subclass_with_meta__(cls, **meta_options): 50 | """This method just terminates the super() chain""" 51 | -------------------------------------------------------------------------------- /graphene/utils/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphql-python/graphene/82903263080b3b7f22c2ad84319584d7a3b1a1f6/graphene/utils/tests/__init__.py -------------------------------------------------------------------------------- /graphene/utils/tests/test_crunch.py: -------------------------------------------------------------------------------- 1 | from pytest import mark 2 | 3 | from ..crunch import crunch 4 | 5 | 6 | @mark.parametrize( 7 | "description,uncrunched,crunched", 8 | [ 9 | ["number primitive", 0, [0]], 10 | ["boolean primitive", True, [True]], 11 | ["string primitive", "string", ["string"]], 12 | ["empty array", [], [[]]], 13 | ["single-item array", [None], [None, [0]]], 14 | [ 15 | "multi-primitive all distinct array", 16 | [None, 0, True, "string"], 17 | [None, 0, True, "string", [0, 1, 2, 3]], 18 | ], 19 | [ 20 | "multi-primitive repeated array", 21 | [True, True, True, True], 22 | [True, [0, 0, 0, 0]], 23 | ], 24 | ["one-level nested array", [[1, 2, 3]], [1, 2, 3, [0, 1, 2], [3]]], 25 | ["two-level nested array", [[[1, 2, 3]]], [1, 2, 3, [0, 1, 2], [3], [4]]], 26 | ["empty object", {}, [{}]], 27 | ["single-item object", {"a": None}, [None, {"a": 0}]], 28 | [ 29 | "multi-item all distinct object", 30 | {"a": None, "b": 0, "c": True, "d": "string"}, 31 | [None, 0, True, "string", {"a": 0, "b": 1, "c": 2, "d": 3}], 32 | ], 33 | [ 34 | "multi-item repeated object", 35 | {"a": True, "b": True, "c": True, "d": True}, 36 | [True, {"a": 0, "b": 0, "c": 0, "d": 0}], 37 | ], 38 | [ 39 | "complex array", 40 | [{"a": True, "b": [1, 2, 3]}, [1, 2, 3]], 41 | [True, 1, 2, 3, [1, 2, 3], {"a": 0, "b": 4}, [5, 4]], 42 | ], 43 | [ 44 | "complex object", 45 | {"a": True, "b": [1, 2, 3], "c": {"a": True, "b": [1, 2, 3]}}, 46 | [True, 1, 2, 3, [1, 2, 3], {"a": 0, "b": 4}, {"a": 0, "b": 4, "c": 5}], 47 | ], 48 | ], 49 | ) 50 | def test_crunch(description, uncrunched, crunched): 51 | assert crunch(uncrunched) == crunched 52 | -------------------------------------------------------------------------------- /graphene/utils/tests/test_deprecated.py: -------------------------------------------------------------------------------- 1 | from .. import deprecated 2 | from ..deprecated import warn_deprecation 3 | 4 | 5 | def test_warn_deprecation(mocker): 6 | mocker.patch.object(deprecated, "warn") 7 | 8 | warn_deprecation("OH!") 9 | deprecated.warn.assert_called_with("OH!", stacklevel=2, category=DeprecationWarning) 10 | -------------------------------------------------------------------------------- /graphene/utils/tests/test_module_loading.py: -------------------------------------------------------------------------------- 1 | from pytest import raises 2 | 3 | from graphene import ObjectType, String 4 | 5 | from ..module_loading import import_string, lazy_import 6 | 7 | 8 | def test_import_string(): 9 | MyString = import_string("graphene.String") 10 | assert MyString == String 11 | 12 | MyObjectTypeMeta = import_string("graphene.ObjectType", "__doc__") 13 | assert MyObjectTypeMeta == ObjectType.__doc__ 14 | 15 | 16 | def test_import_string_module(): 17 | with raises(Exception) as exc_info: 18 | import_string("graphenea") 19 | 20 | assert str(exc_info.value) == "graphenea doesn't look like a module path" 21 | 22 | 23 | def test_import_string_class(): 24 | with raises(Exception) as exc_info: 25 | import_string("graphene.Stringa") 26 | 27 | assert ( 28 | str(exc_info.value) 29 | == 'Module "graphene" does not define a "Stringa" attribute/class' 30 | ) 31 | 32 | 33 | def test_import_string_attributes(): 34 | with raises(Exception) as exc_info: 35 | import_string("graphene.String", "length") 36 | 37 | assert ( 38 | str(exc_info.value) 39 | == 'Module "graphene" does not define a "length" attribute inside attribute/class ' 40 | '"String"' 41 | ) 42 | 43 | with raises(Exception) as exc_info: 44 | import_string("graphene.ObjectType", "__class__.length") 45 | 46 | assert ( 47 | str(exc_info.value) 48 | == 'Module "graphene" does not define a "__class__.length" attribute inside ' 49 | 'attribute/class "ObjectType"' 50 | ) 51 | 52 | with raises(Exception) as exc_info: 53 | import_string("graphene.ObjectType", "__classa__.__base__") 54 | 55 | assert ( 56 | str(exc_info.value) 57 | == 'Module "graphene" does not define a "__classa__" attribute inside attribute/class ' 58 | '"ObjectType"' 59 | ) 60 | 61 | 62 | def test_lazy_import(): 63 | f = lazy_import("graphene.String") 64 | MyString = f() 65 | assert MyString == String 66 | 67 | f = lazy_import("graphene.ObjectType", "__doc__") 68 | MyObjectTypeMeta = f() 69 | assert MyObjectTypeMeta == ObjectType.__doc__ 70 | -------------------------------------------------------------------------------- /graphene/utils/tests/test_orderedtype.py: -------------------------------------------------------------------------------- 1 | from ..orderedtype import OrderedType 2 | 3 | 4 | def test_orderedtype(): 5 | one = OrderedType() 6 | two = OrderedType() 7 | three = OrderedType() 8 | 9 | assert one < two < three 10 | 11 | 12 | def test_orderedtype_eq(): 13 | one = OrderedType() 14 | two = OrderedType() 15 | 16 | assert one == one 17 | assert one != two 18 | 19 | 20 | def test_orderedtype_hash(): 21 | one = OrderedType() 22 | two = OrderedType() 23 | 24 | assert hash(one) == hash(one) 25 | assert hash(one) != hash(two) 26 | 27 | 28 | def test_orderedtype_resetcounter(): 29 | one = OrderedType() 30 | two = OrderedType() 31 | one.reset_counter() 32 | 33 | assert one > two 34 | 35 | 36 | def test_orderedtype_non_orderabletypes(): 37 | one = OrderedType() 38 | 39 | assert one.__lt__(1) == NotImplemented 40 | assert one.__gt__(1) == NotImplemented 41 | assert one != 1 42 | -------------------------------------------------------------------------------- /graphene/utils/tests/test_resolve_only_args.py: -------------------------------------------------------------------------------- 1 | from .. import deprecated 2 | from ..resolve_only_args import resolve_only_args 3 | 4 | 5 | def test_resolve_only_args(mocker): 6 | mocker.patch.object(deprecated, "warn_deprecation") 7 | 8 | def resolver(root, **args): 9 | return root, args 10 | 11 | wrapped_resolver = resolve_only_args(resolver) 12 | result = wrapped_resolver(1, 2, a=3) 13 | assert result == (1, {"a": 3}) 14 | -------------------------------------------------------------------------------- /graphene/utils/tests/test_resolver_from_annotations.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphql-python/graphene/82903263080b3b7f22c2ad84319584d7a3b1a1f6/graphene/utils/tests/test_resolver_from_annotations.py -------------------------------------------------------------------------------- /graphene/utils/tests/test_str_converters.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from ..str_converters import to_camel_case, to_snake_case 3 | 4 | 5 | def test_snake_case(): 6 | assert to_snake_case("snakesOnAPlane") == "snakes_on_a_plane" 7 | assert to_snake_case("SnakesOnAPlane") == "snakes_on_a_plane" 8 | assert to_snake_case("SnakesOnA_Plane") == "snakes_on_a__plane" 9 | assert to_snake_case("snakes_on_a_plane") == "snakes_on_a_plane" 10 | assert to_snake_case("snakes_on_a__plane") == "snakes_on_a__plane" 11 | assert to_snake_case("IPhoneHysteria") == "i_phone_hysteria" 12 | assert to_snake_case("iPhoneHysteria") == "i_phone_hysteria" 13 | 14 | 15 | def test_camel_case(): 16 | assert to_camel_case("snakes_on_a_plane") == "snakesOnAPlane" 17 | assert to_camel_case("snakes_on_a__plane") == "snakesOnA_Plane" 18 | assert to_camel_case("i_phone_hysteria") == "iPhoneHysteria" 19 | assert to_camel_case("field_i18n") == "fieldI18n" 20 | -------------------------------------------------------------------------------- /graphene/utils/tests/test_trim_docstring.py: -------------------------------------------------------------------------------- 1 | from ..trim_docstring import trim_docstring 2 | 3 | 4 | def test_trim_docstring(): 5 | class WellDocumentedObject: 6 | """ 7 | This object is very well-documented. It has multiple lines in its 8 | description. 9 | 10 | Multiple paragraphs too 11 | """ 12 | 13 | assert ( 14 | trim_docstring(WellDocumentedObject.__doc__) 15 | == "This object is very well-documented. It has multiple lines in its\n" 16 | "description.\n\nMultiple paragraphs too" 17 | ) 18 | 19 | class UndocumentedObject: 20 | pass 21 | 22 | assert trim_docstring(UndocumentedObject.__doc__) is None 23 | -------------------------------------------------------------------------------- /graphene/utils/thenables.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file is used mainly as a bridge for thenable abstractions. 3 | """ 4 | 5 | from inspect import isawaitable 6 | 7 | 8 | def await_and_execute(obj, on_resolve): 9 | async def build_resolve_async(): 10 | return on_resolve(await obj) 11 | 12 | return build_resolve_async() 13 | 14 | 15 | def maybe_thenable(obj, on_resolve): 16 | """ 17 | Execute a on_resolve function once the thenable is resolved, 18 | returning the same type of object inputed. 19 | If the object is not thenable, it should return on_resolve(obj) 20 | """ 21 | if isawaitable(obj): 22 | return await_and_execute(obj, on_resolve) 23 | 24 | # If it's not awaitable, return the function executed over the object 25 | return on_resolve(obj) 26 | -------------------------------------------------------------------------------- /graphene/utils/trim_docstring.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | 3 | 4 | def trim_docstring(docstring): 5 | # Cleans up whitespaces from an indented docstring 6 | # 7 | # See https://www.python.org/dev/peps/pep-0257/ 8 | # and https://docs.python.org/2/library/inspect.html#inspect.cleandoc 9 | return inspect.cleandoc(docstring) if docstring else None 10 | -------------------------------------------------------------------------------- /graphene/validation/__init__.py: -------------------------------------------------------------------------------- 1 | from .depth_limit import depth_limit_validator 2 | from .disable_introspection import DisableIntrospection 3 | 4 | 5 | __all__ = ["DisableIntrospection", "depth_limit_validator"] 6 | -------------------------------------------------------------------------------- /graphene/validation/disable_introspection.py: -------------------------------------------------------------------------------- 1 | from graphql import GraphQLError 2 | from graphql.language import FieldNode 3 | from graphql.validation import ValidationRule 4 | 5 | from ..utils.is_introspection_key import is_introspection_key 6 | 7 | 8 | class DisableIntrospection(ValidationRule): 9 | def enter_field(self, node: FieldNode, *_args): 10 | field_name = node.name.value 11 | if is_introspection_key(field_name): 12 | self.report_error( 13 | GraphQLError( 14 | f"Cannot query '{field_name}': introspection is disabled.", node 15 | ) 16 | ) 17 | -------------------------------------------------------------------------------- /graphene/validation/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphql-python/graphene/82903263080b3b7f22c2ad84319584d7a3b1a1f6/graphene/validation/tests/__init__.py -------------------------------------------------------------------------------- /graphene/validation/tests/test_disable_introspection.py: -------------------------------------------------------------------------------- 1 | from graphql import parse, validate 2 | 3 | from ...types import Schema, ObjectType, String 4 | from ..disable_introspection import DisableIntrospection 5 | 6 | 7 | class Query(ObjectType): 8 | name = String(required=True) 9 | 10 | @staticmethod 11 | def resolve_name(root, info): 12 | return "Hello world!" 13 | 14 | 15 | schema = Schema(query=Query) 16 | 17 | 18 | def run_query(query: str): 19 | document = parse(query) 20 | 21 | return validate( 22 | schema=schema.graphql_schema, 23 | document_ast=document, 24 | rules=(DisableIntrospection,), 25 | ) 26 | 27 | 28 | def test_disallows_introspection_queries(): 29 | errors = run_query("{ __schema { queryType { name } } }") 30 | 31 | assert len(errors) == 1 32 | assert errors[0].message == "Cannot query '__schema': introspection is disabled." 33 | 34 | 35 | def test_allows_non_introspection_queries(): 36 | errors = run_query("{ name }") 37 | assert len(errors) == 0 38 | -------------------------------------------------------------------------------- /mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | ignore_missing_imports = True 3 | 4 | [mypy-graphene.pyutils.*] 5 | ignore_errors = True 6 | 7 | [mypy-graphene.types.scalars] 8 | ignore_errors = True 9 | 10 | [mypy-graphene.types.generic] 11 | ignore_errors = True 12 | 13 | [mypy-graphene.types.tests.*] 14 | ignore_errors = True 15 | 16 | [mypy-graphene.relay.tests.*] 17 | ignore_errors = True 18 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [coverage:run] 2 | omit = graphene/pyutils/*,*/tests/*,graphene/types/scalars.py 3 | 4 | [bdist_wheel] 5 | universal=1 6 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import ast 2 | import codecs 3 | import re 4 | import sys 5 | 6 | from setuptools import find_packages, setup 7 | from setuptools.command.test import test as TestCommand 8 | 9 | _version_re = re.compile(r"VERSION\s+=\s+(.*)") 10 | 11 | with open("graphene/__init__.py", "rb") as f: 12 | version = ast.literal_eval(_version_re.search(f.read().decode("utf-8")).group(1)) 13 | 14 | path_copy = sys.path[:] 15 | 16 | sys.path.append("graphene") 17 | try: 18 | from pyutils.version import get_version 19 | 20 | version = get_version(version) 21 | except Exception: 22 | version = ".".join([str(v) for v in version]) 23 | 24 | sys.path[:] = path_copy 25 | 26 | 27 | class PyTest(TestCommand): 28 | user_options = [("pytest-args=", "a", "Arguments to pass to py.test")] 29 | 30 | def initialize_options(self): 31 | TestCommand.initialize_options(self) 32 | self.pytest_args = [] 33 | 34 | def finalize_options(self): 35 | TestCommand.finalize_options(self) 36 | self.test_args = [] 37 | self.test_suite = True 38 | 39 | def run_tests(self): 40 | # import here, cause outside the eggs aren't loaded 41 | import pytest 42 | 43 | errno = pytest.main(self.pytest_args) 44 | sys.exit(errno) 45 | 46 | 47 | tests_require = [ 48 | "pytest>=8,<9", 49 | "pytest-benchmark>=4,<5", 50 | "pytest-cov>=5,<6", 51 | "pytest-mock>=3,<4", 52 | "pytest-asyncio>=0.16,<2", 53 | "coveralls>=3.3,<5", 54 | ] 55 | 56 | dev_requires = [ 57 | "ruff==0.5.0", 58 | "types-python-dateutil>=2.8.1,<3", 59 | "mypy>=1.10,<2", 60 | ] + tests_require 61 | 62 | setup( 63 | name="graphene", 64 | version=version, 65 | description="GraphQL Framework for Python", 66 | long_description=codecs.open( 67 | "README.md", "r", encoding="ascii", errors="replace" 68 | ).read(), 69 | long_description_content_type="text/markdown", 70 | url="https://github.com/graphql-python/graphene", 71 | author="Syrus Akbary", 72 | author_email="me@syrusakbary.com", 73 | license="MIT", 74 | classifiers=[ 75 | "Development Status :: 5 - Production/Stable", 76 | "Intended Audience :: Developers", 77 | "Topic :: Software Development :: Libraries", 78 | "Programming Language :: Python :: 3.8", 79 | "Programming Language :: Python :: 3.9", 80 | "Programming Language :: Python :: 3.10", 81 | "Programming Language :: Python :: 3.11", 82 | "Programming Language :: Python :: 3.12", 83 | "Programming Language :: Python :: 3.13", 84 | ], 85 | keywords="api graphql protocol rest relay graphene", 86 | packages=find_packages(exclude=["examples*"]), 87 | install_requires=[ 88 | "graphql-core>=3.1,<3.3", 89 | "graphql-relay>=3.1,<3.3", 90 | "python-dateutil>=2.7.0,<3", 91 | "typing-extensions>=4.7.1,<5", 92 | ], 93 | tests_require=tests_require, 94 | extras_require={"test": tests_require, "dev": dev_requires}, 95 | cmdclass={"test": PyTest}, 96 | ) 97 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py3{8,9,10,11,12,13}, mypy, pre-commit 3 | skipsdist = true 4 | 5 | [testenv] 6 | deps = 7 | .[test] 8 | commands = 9 | pytest --cov=graphene graphene --cov-report=term --cov-report=xml examples {posargs} 10 | 11 | [testenv:pre-commit] 12 | basepython = python3.10 13 | deps = 14 | pre-commit>=3.7,<4 15 | setenv = 16 | LC_CTYPE=en_US.UTF-8 17 | commands = 18 | pre-commit run --all-files --show-diff-on-failure 19 | 20 | [testenv:mypy] 21 | basepython = python3.10 22 | deps = 23 | .[dev] 24 | commands = 25 | mypy graphene 26 | 27 | [pytest] 28 | --------------------------------------------------------------------------------