├── CODEOWNERS ├── .pyup.yml ├── tests ├── __init__.py ├── error │ ├── __init__.py │ ├── test_located_error.py │ └── test_print_location.py ├── type │ └── __init__.py ├── language │ ├── __init__.py │ ├── test_location.py │ ├── test_block_string_fuzz.py │ └── test_print_string.py ├── pyutils │ ├── __init__.py │ ├── test_frozen_error.py │ ├── test_print_path_list.py │ ├── test_identity_func.py │ ├── test_cached_property.py │ ├── test_did_you_mean.py │ ├── test_path.py │ ├── test_undefined.py │ ├── test_natural_compare.py │ ├── test_convert_case.py │ ├── test_suggestion_list.py │ └── test_frozen_dict.py ├── execution │ ├── __init__.py │ └── test_customize.py ├── utilities │ ├── __init__.py │ ├── test_concat_ast.py │ ├── test_assert_valid_name.py │ ├── test_type_from_ast.py │ ├── test_strip_ignored_characters_fuzz.py │ ├── test_introspection_from_schema.py │ ├── test_get_operation_ast.py │ ├── test_value_from_ast_untyped.py │ └── test_get_introspection_query.py ├── subscription │ └── __init__.py ├── validation │ ├── __init__.py │ ├── test_variables_are_input_types.py │ ├── test_unique_variable_names.py │ ├── test_known_fragment_names.py │ ├── test_executable_definitions.py │ ├── test_unique_input_field_names.py │ ├── test_unique_fragment_names.py │ ├── test_lone_anonymous_operation.py │ └── test_unique_directive_names.py ├── utils │ ├── __init__.py │ ├── dedent.py │ ├── gen_fuzz_strings.py │ ├── test_gen_fuzz_strings.py │ └── test_dedent.py ├── benchmarks │ ├── test_parser.py │ ├── __init__.py │ ├── test_validate_sdl.py │ ├── test_build_ast_schema.py │ ├── test_validate_gql.py │ ├── test_build_client_schema.py │ ├── test_introspection_from_schema.py │ ├── test_execution_sync.py │ ├── test_validate_invalid_gql.py │ └── test_execution_async.py ├── conftest.py └── fixtures │ ├── __init__.py │ └── kitchen_sink.graphql ├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ └── open-a-graphql-core-issue.md └── workflows │ ├── lint.yml │ ├── publish.yml │ └── test.yml ├── docs ├── requirements.txt ├── index.rst ├── modules │ ├── subscription.rst │ ├── error.rst │ ├── graphql.rst │ ├── execution.rst │ ├── pyutils.rst │ └── utilities.rst ├── usage │ ├── index.rst │ ├── other.rst │ ├── extension.rst │ ├── validator.rst │ ├── methods.rst │ ├── introspection.rst │ ├── sdl.rst │ └── parser.rst ├── Makefile └── make.bat ├── src └── graphql │ ├── py.typed │ ├── validation │ └── rules │ │ ├── custom │ │ ├── __init__.py │ │ └── no_schema_introspection.py │ │ ├── known_fragment_names.py │ │ ├── __init__.py │ │ ├── variables_are_input_types.py │ │ ├── lone_anonymous_operation.py │ │ ├── unique_variable_names.py │ │ ├── unique_argument_names.py │ │ ├── unique_fragment_names.py │ │ ├── lone_schema_definition.py │ │ ├── unique_input_field_names.py │ │ ├── unique_operation_names.py │ │ ├── scalar_leafs.py │ │ ├── executable_definitions.py │ │ ├── unique_directive_names.py │ │ ├── no_undefined_variables.py │ │ ├── no_unused_fragments.py │ │ ├── unique_type_names.py │ │ ├── no_unused_variables.py │ │ ├── fragments_on_composite_types.py │ │ ├── unique_enum_value_names.py │ │ ├── unique_operation_types.py │ │ ├── possible_fragment_spreads.py │ │ ├── unique_field_definition_names.py │ │ ├── known_type_names.py │ │ └── no_fragment_cycles.py │ ├── pyutils │ ├── frozen_error.py │ ├── awaitable_or_value.py │ ├── print_path_list.py │ ├── identity_func.py │ ├── natural_compare.py │ ├── convert_case.py │ ├── is_awaitable.py │ ├── did_you_mean.py │ ├── is_iterable.py │ ├── undefined.py │ ├── path.py │ ├── cached_property.py │ ├── frozen_dict.py │ ├── frozen_list.py │ ├── __init__.py │ ├── description.py │ └── simple_pub_sub.py │ ├── subscription │ └── __init__.py │ ├── error │ ├── __init__.py │ ├── syntax_error.py │ └── located_error.py │ ├── utilities │ ├── concat_ast.py │ ├── assert_valid_name.py │ ├── get_operation_ast.py │ ├── get_operation_root_type.py │ ├── introspection_from_schema.py │ └── type_from_ast.py │ ├── language │ ├── token_kind.py │ ├── directive_locations.py │ ├── location.py │ ├── print_string.py │ ├── source.py │ ├── predicates.py │ └── print_location.py │ ├── execution │ ├── __init__.py │ └── middleware.py │ └── version.py ├── .flake8 ├── .gitignore ├── .editorconfig ├── setup.cfg ├── .coveragerc ├── MANIFEST.in ├── .mypy.ini ├── .bumpversion.cfg ├── LICENSE ├── tox.ini ├── setup.py └── pyproject.toml /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @Cito -------------------------------------------------------------------------------- /.pyup.yml: -------------------------------------------------------------------------------- 1 | branch: main -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | """Tests for graphql""" 2 | -------------------------------------------------------------------------------- /tests/error/__init__.py: -------------------------------------------------------------------------------- 1 | """Tests for graphql.error""" 2 | -------------------------------------------------------------------------------- /tests/type/__init__.py: -------------------------------------------------------------------------------- 1 | """Tests for graphql.type""" 2 | -------------------------------------------------------------------------------- /tests/language/__init__.py: -------------------------------------------------------------------------------- 1 | """Tests for graphql.language""" 2 | -------------------------------------------------------------------------------- /tests/pyutils/__init__.py: -------------------------------------------------------------------------------- 1 | """Tests for graphql.pyutils""" 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | -------------------------------------------------------------------------------- /tests/execution/__init__.py: -------------------------------------------------------------------------------- 1 | """Tests for graphql.execution""" 2 | -------------------------------------------------------------------------------- /tests/utilities/__init__.py: -------------------------------------------------------------------------------- 1 | """Tests for graphql.utilities""" 2 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx>=3.5,<4 2 | sphinx_rtd_theme>=0.5,<1 3 | -------------------------------------------------------------------------------- /tests/subscription/__init__.py: -------------------------------------------------------------------------------- 1 | """Tests for graphql.subscription""" 2 | -------------------------------------------------------------------------------- /src/graphql/py.typed: -------------------------------------------------------------------------------- 1 | # Marker file for PEP 561. The graphql package uses inline types. 2 | -------------------------------------------------------------------------------- /src/graphql/validation/rules/custom/__init__.py: -------------------------------------------------------------------------------- 1 | """graphql.validation.rules.custom package""" 2 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = E203,W503 3 | exclude = .git,.mypy_cache,.pytest_cache,.tox,.venv,__pycache__,build,dist,docs 4 | max-line-length = 88 5 | -------------------------------------------------------------------------------- /tests/validation/__init__.py: -------------------------------------------------------------------------------- 1 | """Tests for graphql.validation""" 2 | 3 | from pytest import register_assert_rewrite 4 | 5 | register_assert_rewrite("tests.validation.harness") 6 | -------------------------------------------------------------------------------- /src/graphql/pyutils/frozen_error.py: -------------------------------------------------------------------------------- 1 | __all__ = ["FrozenError"] 2 | 3 | 4 | class FrozenError(TypeError): 5 | """Error when trying to change a frozen (read only) collection.""" 6 | -------------------------------------------------------------------------------- /tests/utils/__init__.py: -------------------------------------------------------------------------------- 1 | """Test utilities""" 2 | 3 | from .dedent import dedent 4 | from .gen_fuzz_strings import gen_fuzz_strings 5 | 6 | __all__ = ["dedent", "gen_fuzz_strings"] 7 | -------------------------------------------------------------------------------- /src/graphql/pyutils/awaitable_or_value.py: -------------------------------------------------------------------------------- 1 | from typing import Awaitable, TypeVar, Union 2 | 3 | __all__ = ["AwaitableOrValue"] 4 | 5 | 6 | T = TypeVar("T") 7 | 8 | AwaitableOrValue = Union[Awaitable[T], T] 9 | -------------------------------------------------------------------------------- /tests/pyutils/test_frozen_error.py: -------------------------------------------------------------------------------- 1 | from graphql.pyutils import FrozenError 2 | 3 | 4 | def describe_frozen_error(): 5 | def frozen_error_is_type_error(): 6 | assert issubclass(FrozenError, TypeError) 7 | -------------------------------------------------------------------------------- /tests/utils/dedent.py: -------------------------------------------------------------------------------- 1 | from textwrap import dedent as _dedent 2 | 3 | __all__ = ["dedent"] 4 | 5 | 6 | def dedent(text: str) -> str: 7 | """Fix indentation and also trim given text string.""" 8 | return _dedent(text.lstrip("\n").rstrip(" \t\n")) 9 | -------------------------------------------------------------------------------- /src/graphql/pyutils/print_path_list.py: -------------------------------------------------------------------------------- 1 | from typing import Collection, Union 2 | 3 | 4 | def print_path_list(path: Collection[Union[str, int]]) -> str: 5 | """Build a string describing the path.""" 6 | return "".join(f"[{key}]" if isinstance(key, int) else f".{key}" for key in path) 7 | -------------------------------------------------------------------------------- /tests/benchmarks/test_parser.py: -------------------------------------------------------------------------------- 1 | from graphql import parse, DocumentNode 2 | 3 | from ..fixtures import kitchen_sink_query # noqa: F401 4 | 5 | 6 | def test_parse_kitchen_sink(benchmark, kitchen_sink_query): # noqa: F811 7 | query = benchmark(lambda: parse(kitchen_sink_query)) 8 | assert isinstance(query, DocumentNode) 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | .cache/ 3 | .coverage 4 | .env*/ 5 | .idea/ 6 | .mypy_cache/ 7 | .pytest_cache/ 8 | .tox/ 9 | .venv*/ 10 | .vs/ 11 | 12 | build/ 13 | dist/ 14 | docs/_build/ 15 | htmlcov/ 16 | pip-wheel-metadata/ 17 | wheels/ 18 | 19 | play/ 20 | 21 | __pycache__/ 22 | 23 | *.cover 24 | *.egg 25 | *.egg-info 26 | *.log 27 | *.py[cod] 28 | -------------------------------------------------------------------------------- /src/graphql/pyutils/identity_func.py: -------------------------------------------------------------------------------- 1 | from typing import cast, Any, TypeVar 2 | 3 | from .undefined import Undefined 4 | 5 | __all__ = ["identity_func"] 6 | 7 | 8 | T = TypeVar("T") 9 | 10 | 11 | def identity_func(x: T = cast(Any, Undefined), *_args: Any) -> T: 12 | """Return the first received argument.""" 13 | return x 14 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to GraphQL-core 3 2 | ========================= 3 | 4 | Contents 5 | -------- 6 | 7 | .. toctree:: 8 | :maxdepth: 2 9 | 10 | intro 11 | usage/index 12 | diffs 13 | modules/graphql 14 | 15 | 16 | Indices and tables 17 | ------------------ 18 | 19 | * :ref:`genindex` 20 | * :ref:`modindex` 21 | * :ref:`search` 22 | 23 | -------------------------------------------------------------------------------- /tests/benchmarks/__init__.py: -------------------------------------------------------------------------------- 1 | """Benchmarks for graphql 2 | 3 | Benchmarks are disabled (only executed as tests) by default in setup.cfg. 4 | You can enable them with --benchmark-enable if your want to execute them. 5 | 6 | E.g. in order to execute all the benchmarks with tox using Python 3.7:: 7 | 8 | tox -e py37 -- -k benchmarks --benchmark-enable 9 | """ 10 | -------------------------------------------------------------------------------- /docs/modules/subscription.rst: -------------------------------------------------------------------------------- 1 | Subscription 2 | ============ 3 | 4 | .. currentmodule:: graphql.subscription 5 | 6 | .. automodule:: graphql.subscription 7 | :no-members: 8 | :no-inherited-members: 9 | 10 | .. autofunction:: subscribe 11 | 12 | Helpers 13 | ------- 14 | 15 | .. autofunction:: create_source_event_stream 16 | 17 | .. autoclass:: MapAsyncIterator 18 | -------------------------------------------------------------------------------- /docs/usage/index.rst: -------------------------------------------------------------------------------- 1 | Usage 2 | ===== 3 | 4 | GraphQL-core provides two important capabilities: building a type schema, and serving 5 | queries against that type schema. 6 | 7 | .. toctree:: 8 | :maxdepth: 2 9 | 10 | schema 11 | resolvers 12 | queries 13 | sdl 14 | methods 15 | introspection 16 | parser 17 | extension 18 | validator 19 | other 20 | -------------------------------------------------------------------------------- /src/graphql/subscription/__init__.py: -------------------------------------------------------------------------------- 1 | """GraphQL Subscription 2 | 3 | The :mod:`graphql.subscription` package is responsible for subscribing to updates 4 | on specific data. 5 | """ 6 | 7 | from .subscribe import subscribe, create_source_event_stream 8 | from .map_async_iterator import MapAsyncIterator 9 | 10 | __all__ = ["subscribe", "create_source_event_stream", "MapAsyncIterator"] 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 4 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | charset = utf-8 11 | end_of_line = lf 12 | 13 | [*.bat] 14 | indent_style = tab 15 | end_of_line = crlf 16 | 17 | [LICENSE] 18 | insert_final_newline = false 19 | 20 | [Makefile] 21 | indent_style = tab 22 | -------------------------------------------------------------------------------- /tests/benchmarks/test_validate_sdl.py: -------------------------------------------------------------------------------- 1 | from graphql import parse 2 | from graphql.validation.validate import validate_sdl 3 | 4 | from ..fixtures import big_schema_sdl # noqa: F401 5 | 6 | 7 | def test_validate_sdl_document(benchmark, big_schema_sdl): # noqa: F811 8 | sdl_ast = parse(big_schema_sdl) 9 | result = benchmark(lambda: validate_sdl(sdl_ast)) 10 | assert result == [] 11 | -------------------------------------------------------------------------------- /docs/modules/error.rst: -------------------------------------------------------------------------------- 1 | Error 2 | ===== 3 | 4 | .. currentmodule:: graphql.error 5 | 6 | .. automodule:: graphql.error 7 | :no-members: 8 | :no-inherited-members: 9 | 10 | .. autoclass:: GraphQLError 11 | 12 | .. autoclass:: GraphQLSyntaxError 13 | 14 | .. autoclass:: GraphQLFormattedError 15 | :no-inherited-members: 16 | :no-special-members: 17 | 18 | .. autofunction:: located_error 19 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | python-tag = py3 3 | 4 | [aliases] 5 | test = pytest 6 | 7 | [check-manifest] 8 | ignore = 9 | .pyup.yml 10 | 11 | [tool:pytest] 12 | # Only run benchmarks as tests. 13 | # To actually run the benchmarks, use --benchmark-enable on the command line. 14 | # To run the slow tests (fuzzing), add --run-slow on the command line. 15 | addopts = --benchmark-disable 16 | timeout = 100 17 | -------------------------------------------------------------------------------- /tests/utils/gen_fuzz_strings.py: -------------------------------------------------------------------------------- 1 | from itertools import product 2 | from typing import Generator 3 | 4 | __all__ = ["gen_fuzz_strings"] 5 | 6 | 7 | def gen_fuzz_strings(allowed_chars: str, max_length: int) -> Generator[str, None, None]: 8 | """Generator that produces all possible combinations of allowed characters.""" 9 | for length in range(max_length + 1): 10 | yield from map("".join, product(allowed_chars, repeat=length)) 11 | -------------------------------------------------------------------------------- /tests/benchmarks/test_build_ast_schema.py: -------------------------------------------------------------------------------- 1 | from graphql import parse, build_ast_schema, GraphQLSchema 2 | 3 | from ..fixtures import big_schema_sdl # noqa: F401 4 | 5 | 6 | def test_build_schema_from_ast(benchmark, big_schema_sdl): # noqa: F811 7 | schema_ast = parse(big_schema_sdl) 8 | schema: GraphQLSchema = benchmark( 9 | lambda: build_ast_schema(schema_ast, assume_valid=True) 10 | ) 11 | assert schema.query_type is not None 12 | -------------------------------------------------------------------------------- /src/graphql/error/__init__.py: -------------------------------------------------------------------------------- 1 | """GraphQL Errors 2 | 3 | The :mod:`graphql.error` package is responsible for creating and formatting GraphQL 4 | errors. 5 | """ 6 | 7 | from .graphql_error import GraphQLError, GraphQLFormattedError 8 | 9 | from .syntax_error import GraphQLSyntaxError 10 | 11 | from .located_error import located_error 12 | 13 | __all__ = [ 14 | "GraphQLError", 15 | "GraphQLFormattedError", 16 | "GraphQLSyntaxError", 17 | "located_error", 18 | ] 19 | -------------------------------------------------------------------------------- /tests/benchmarks/test_validate_gql.py: -------------------------------------------------------------------------------- 1 | from graphql import build_schema, parse, validate 2 | from graphql.utilities import get_introspection_query 3 | 4 | from ..fixtures import big_schema_sdl # noqa: F401 5 | 6 | 7 | def test_validate_introspection_query(benchmark, big_schema_sdl): # noqa: F811 8 | schema = build_schema(big_schema_sdl, assume_valid=True) 9 | query = parse(get_introspection_query()) 10 | result = benchmark(lambda: validate(schema, query)) 11 | assert result == [] 12 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = True 3 | source = src 4 | omit = 5 | */conftest.py 6 | */cached_property.py 7 | */is_iterable.py 8 | */test_*_fuzz.py 9 | 10 | [report] 11 | exclude_lines = 12 | pragma: no cover 13 | except ImportError: 14 | \# Python < 15 | raise NotImplementedError 16 | raise TypeError\(f?"Unexpected 17 | assert False, 18 | \s+next\($ 19 | if MYPY: 20 | if TYPE_CHECKING: 21 | ^\s+\.\.\.$ 22 | ^\s+pass$ 23 | ignore_errors = True 24 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include MANIFEST.in 2 | 3 | include CODEOWNERS 4 | include LICENSE 5 | include README.md 6 | 7 | include .bumpversion.cfg 8 | include .coveragerc 9 | include .editorconfig 10 | include .flake8 11 | include .mypy.ini 12 | 13 | include tox.ini 14 | 15 | include poetry.lock 16 | include pyproject.toml 17 | 18 | graft src/graphql 19 | graft tests 20 | recursive-include docs *.txt *.rst conf.py Makefile make.bat *.jpg *.png *.gif 21 | prune docs/_build 22 | 23 | global-exclude *.py[co] __pycache__ 24 | -------------------------------------------------------------------------------- /tests/pyutils/test_print_path_list.py: -------------------------------------------------------------------------------- 1 | from graphql.pyutils import print_path_list 2 | 3 | 4 | def describe_print_path_as_list(): 5 | def without_key(): 6 | assert print_path_list([]) == "" 7 | 8 | def with_one_key(): 9 | assert print_path_list(["one"]) == ".one" 10 | assert print_path_list([1]) == "[1]" 11 | 12 | def with_three_keys(): 13 | assert print_path_list([0, "one", 2]) == "[0].one[2]" 14 | assert print_path_list(["one", 2, "three"]) == ".one[2].three" 15 | -------------------------------------------------------------------------------- /.mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | python_version = 3.9 3 | check_untyped_defs = True 4 | no_implicit_optional = True 5 | strict_optional = True 6 | warn_redundant_casts = True 7 | warn_unused_ignores = True 8 | disallow_untyped_defs = True 9 | 10 | [mypy-graphql.pyutils.frozen_dict] 11 | disallow_untyped_defs = False 12 | 13 | [mypy-graphql.pyutils.frozen_list] 14 | disallow_untyped_defs = False 15 | 16 | [mypy-graphql.type.introspection] 17 | disallow_untyped_defs = False 18 | 19 | [mypy-tests.*] 20 | disallow_untyped_defs = False 21 | -------------------------------------------------------------------------------- /tests/benchmarks/test_build_client_schema.py: -------------------------------------------------------------------------------- 1 | from graphql import build_client_schema, GraphQLSchema 2 | 3 | from ..fixtures import big_schema_introspection_result # noqa: F401 4 | 5 | 6 | def test_build_schema_from_introspection( 7 | benchmark, big_schema_introspection_result # noqa: F811 8 | ): 9 | schema: GraphQLSchema = benchmark( 10 | lambda: build_client_schema( 11 | big_schema_introspection_result["data"], assume_valid=True 12 | ) 13 | ) 14 | assert schema.query_type is not None 15 | -------------------------------------------------------------------------------- /tests/benchmarks/test_introspection_from_schema.py: -------------------------------------------------------------------------------- 1 | from graphql import build_schema, parse, execute_sync 2 | from graphql.utilities import get_introspection_query 3 | 4 | from ..fixtures import big_schema_sdl # noqa: F401 5 | 6 | 7 | def test_execute_introspection_query(benchmark, big_schema_sdl): # noqa: F811 8 | schema = build_schema(big_schema_sdl, assume_valid=True) 9 | document = parse(get_introspection_query()) 10 | result = benchmark(lambda: execute_sync(schema=schema, document=document)) 11 | assert result.errors is None 12 | -------------------------------------------------------------------------------- /docs/modules/graphql.rst: -------------------------------------------------------------------------------- 1 | Reference 2 | ========= 3 | 4 | .. currentmodule:: graphql 5 | 6 | .. automodule:: graphql 7 | :no-members: 8 | :no-inherited-members: 9 | 10 | .. _top-level-functions: 11 | 12 | Top-Level Functions 13 | ------------------- 14 | 15 | .. autofunction:: graphql 16 | .. autofunction:: graphql_sync 17 | 18 | .. _sub-packages: 19 | 20 | Sub-Packages 21 | ------------ 22 | 23 | .. toctree:: 24 | :maxdepth: 2 25 | 26 | error 27 | execution 28 | language 29 | pyutils 30 | subscription 31 | type 32 | utilities 33 | validation 34 | -------------------------------------------------------------------------------- /src/graphql/pyutils/natural_compare.py: -------------------------------------------------------------------------------- 1 | import re 2 | from typing import Tuple 3 | 4 | from itertools import cycle 5 | 6 | __all__ = ["natural_comparison_key"] 7 | 8 | _re_digits = re.compile(r"(\d+)") 9 | 10 | 11 | def natural_comparison_key(key: str) -> Tuple: 12 | """Comparison key function for sorting strings by natural sort order. 13 | 14 | See: https://en.wikipedia.org/wiki/Natural_sort_order 15 | """ 16 | return tuple( 17 | (int(part), part) if is_digit else part 18 | for part, is_digit in zip(_re_digits.split(key), cycle((False, True))) 19 | ) 20 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Code quality 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v2 11 | 12 | - name: Set up Python 3.9 13 | uses: actions/setup-python@v2 14 | with: 15 | python-version: 3.9 16 | 17 | - name: Install dependencies 18 | run: | 19 | python -m pip install --upgrade pip 20 | pip install tox 21 | 22 | - name: Run code quality tests with tox 23 | run: tox 24 | env: 25 | TOXENV: black,flake8,mypy,docs,manifest 26 | -------------------------------------------------------------------------------- /src/graphql/error/syntax_error.py: -------------------------------------------------------------------------------- 1 | from typing import TYPE_CHECKING 2 | 3 | from .graphql_error import GraphQLError 4 | 5 | if TYPE_CHECKING: 6 | from ..language.source import Source # noqa: F401 7 | 8 | __all__ = ["GraphQLSyntaxError"] 9 | 10 | 11 | class GraphQLSyntaxError(GraphQLError): 12 | """A GraphQLError representing a syntax error.""" 13 | 14 | def __init__(self, source: "Source", position: int, description: str) -> None: 15 | super().__init__( 16 | f"Syntax Error: {description}", source=source, positions=[position] 17 | ) 18 | self.description = description 19 | -------------------------------------------------------------------------------- /.bumpversion.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 3.2.0rc2 3 | commit = False 4 | tag = False 5 | 6 | [bumpversion:file:src/graphql/version.py] 7 | search = version = "{current_version}" 8 | replace = version = "{new_version}" 9 | 10 | [bumpversion:file:docs/conf.py] 11 | search = version = release = '{current_version}' 12 | replace = version = release = '{new_version}' 13 | 14 | [bumpversion:file:README.md] 15 | search = The current version {current_version} 16 | replace = The current version {new_version} 17 | 18 | [bumpversion:file:pyproject.toml] 19 | search = version = "{current_version}" 20 | replace = version = "{new_version}" 21 | -------------------------------------------------------------------------------- /docs/modules/execution.rst: -------------------------------------------------------------------------------- 1 | Execution 2 | ========= 3 | 4 | .. currentmodule:: graphql.execution 5 | 6 | .. automodule:: graphql.execution 7 | :no-members: 8 | :no-inherited-members: 9 | 10 | .. autofunction:: execute 11 | 12 | .. autofunction:: execute_sync 13 | 14 | .. autofunction:: default_field_resolver 15 | 16 | .. autofunction:: default_type_resolver 17 | 18 | .. autoclass:: ExecutionContext 19 | 20 | .. autoclass:: ExecutionResult 21 | 22 | .. autoclass:: FormattedExecutionResult 23 | :no-inherited-members: 24 | 25 | .. autoclass:: Middleware 26 | 27 | .. autoclass:: MiddlewareManager 28 | 29 | .. autofunction:: get_directive_values 30 | -------------------------------------------------------------------------------- /src/graphql/utilities/concat_ast.py: -------------------------------------------------------------------------------- 1 | from itertools import chain 2 | from typing import Collection 3 | 4 | from ..language.ast import DocumentNode 5 | 6 | __all__ = ["concat_ast"] 7 | 8 | 9 | def concat_ast(asts: Collection[DocumentNode]) -> DocumentNode: 10 | """Concat ASTs. 11 | 12 | Provided a collection of ASTs, presumably each from different files, concatenate 13 | the ASTs together into batched AST, useful for validating many GraphQL source files 14 | which together represent one conceptual application. 15 | """ 16 | return DocumentNode( 17 | definitions=list(chain.from_iterable(document.definitions for document in asts)) 18 | ) 19 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 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@v2 14 | 15 | - name: Set up Python 3.9 16 | uses: actions/setup-python@v2 17 | with: 18 | python-version: 3.9 19 | 20 | - name: Build wheel and source tarball 21 | run: | 22 | pip install wheel 23 | python setup.py sdist bdist_wheel 24 | 25 | - name: Publish a Python distribution to PyPI 26 | uses: pypa/gh-action-pypi-publish@release/v1 27 | with: 28 | user: __token__ 29 | password: ${{ secrets.PYPI_TOKEN }} 30 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | strategy: 10 | matrix: 11 | python: [3.6, 3.7, 3.8, 3.9, 3.10, pypy3] 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | 16 | - name: Set up Python ${{ matrix.python-version }} 17 | uses: actions/setup-python@v2 18 | with: 19 | python-version: ${{ matrix.python-version }} 20 | 21 | - name: Install dependencies 22 | run: | 23 | python -m pip install --upgrade pip 24 | pip install tox tox-gh-actions 25 | 26 | - name: Run unit tests with tox 27 | run: tox 28 | -------------------------------------------------------------------------------- /src/graphql/language/token_kind.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | __all__ = ["TokenKind"] 4 | 5 | 6 | class TokenKind(Enum): 7 | """The different kinds of tokens that the lexer emits""" 8 | 9 | SOF = "" 10 | EOF = "" 11 | BANG = "!" 12 | DOLLAR = "$" 13 | AMP = "&" 14 | PAREN_L = "(" 15 | PAREN_R = ")" 16 | SPREAD = "..." 17 | COLON = ":" 18 | EQUALS = "=" 19 | AT = "@" 20 | BRACKET_L = "[" 21 | BRACKET_R = "]" 22 | BRACE_L = "{" 23 | PIPE = "|" 24 | BRACE_R = "}" 25 | NAME = "Name" 26 | INT = "Int" 27 | FLOAT = "Float" 28 | STRING = "String" 29 | BLOCK_STRING = "BlockString" 30 | COMMENT = "Comment" 31 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | # pytest configuration 2 | 3 | import pytest 4 | 5 | 6 | def pytest_addoption(parser): 7 | parser.addoption( 8 | "--run-slow", action="store_true", default=False, help="run slow tests" 9 | ) 10 | 11 | 12 | def pytest_configure(config): 13 | config.addinivalue_line("markers", "slow: mark test as slow to run") 14 | 15 | 16 | def pytest_collection_modifyitems(config, items): 17 | if not config.getoption("--run-slow"): 18 | # without --run-slow option: skip all slow tests 19 | skip_slow = pytest.mark.skip(reason="need --run-slow option to run") 20 | for item in items: 21 | if "slow" in item.keywords: 22 | item.add_marker(skip_slow) 23 | -------------------------------------------------------------------------------- /tests/pyutils/test_identity_func.py: -------------------------------------------------------------------------------- 1 | from graphql.pyutils import identity_func, Undefined 2 | 3 | 4 | def describe_identity_func(): 5 | def returns_the_first_argument_it_receives(): 6 | assert identity_func() is Undefined 7 | assert identity_func(Undefined) is Undefined 8 | assert identity_func(None) is None 9 | obj = object() 10 | assert identity_func(obj) is obj 11 | 12 | assert identity_func(Undefined, None) is Undefined 13 | assert identity_func(None, Undefined) is None 14 | 15 | assert identity_func(None, Undefined, obj) is None 16 | assert identity_func(Undefined, None, obj) is Undefined 17 | assert identity_func(obj, None, Undefined) is obj 18 | -------------------------------------------------------------------------------- /tests/pyutils/test_cached_property.py: -------------------------------------------------------------------------------- 1 | from graphql.pyutils import cached_property 2 | 3 | 4 | def describe_cached_property(): 5 | def works_like_a_normal_property(): 6 | class TestClass: 7 | @cached_property 8 | def value(self): 9 | return 42 10 | 11 | assert TestClass().value == 42 12 | 13 | def caches_the_value(): 14 | class TestClass: 15 | evaluations = 0 16 | 17 | @cached_property 18 | def value(self): 19 | self.__class__.evaluations += 1 20 | return 42 21 | 22 | obj = TestClass() 23 | assert TestClass.evaluations == 0 24 | assert obj.value == 42 25 | assert TestClass.evaluations == 1 26 | assert obj.value == 42 27 | assert TestClass.evaluations == 1 28 | -------------------------------------------------------------------------------- /src/graphql/pyutils/convert_case.py: -------------------------------------------------------------------------------- 1 | # uses code from https://github.com/daveoncode/python-string-utils 2 | 3 | import re 4 | 5 | __all__ = ["camel_to_snake", "snake_to_camel"] 6 | 7 | _re_camel_to_snake = re.compile(r"([a-z]|[A-Z0-9]+)(?=[A-Z])") 8 | _re_snake_to_camel = re.compile(r"(_)([a-z\d])") 9 | 10 | 11 | def camel_to_snake(s: str) -> str: 12 | """Convert from CamelCase to snake_case""" 13 | return _re_camel_to_snake.sub(r"\1_", s).lower() 14 | 15 | 16 | def snake_to_camel(s: str, upper: bool = True) -> str: 17 | """Convert from snake_case to CamelCase 18 | 19 | If upper is set, then convert to upper CamelCase, otherwise the first character 20 | keeps its case. 21 | """ 22 | s = _re_snake_to_camel.sub(lambda m: m.group(2).upper(), s) 23 | if upper: 24 | s = s[:1].upper() + s[1:] 25 | return s 26 | -------------------------------------------------------------------------------- /src/graphql/execution/__init__.py: -------------------------------------------------------------------------------- 1 | """GraphQL Execution 2 | 3 | The :mod:`graphql.execution` package is responsible for the execution phase of 4 | fulfilling a GraphQL request. 5 | """ 6 | 7 | from .execute import ( 8 | execute, 9 | execute_sync, 10 | default_field_resolver, 11 | default_type_resolver, 12 | ExecutionContext, 13 | ExecutionResult, 14 | FormattedExecutionResult, 15 | Middleware, 16 | ) 17 | 18 | from .middleware import MiddlewareManager 19 | 20 | from .values import get_directive_values 21 | 22 | __all__ = [ 23 | "execute", 24 | "execute_sync", 25 | "default_field_resolver", 26 | "default_type_resolver", 27 | "ExecutionContext", 28 | "ExecutionResult", 29 | "FormattedExecutionResult", 30 | "Middleware", 31 | "MiddlewareManager", 32 | "get_directive_values", 33 | ] 34 | -------------------------------------------------------------------------------- /src/graphql/validation/rules/known_fragment_names.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from ...error import GraphQLError 4 | from ...language import FragmentSpreadNode 5 | from . import ValidationRule 6 | 7 | __all__ = ["KnownFragmentNamesRule"] 8 | 9 | 10 | class KnownFragmentNamesRule(ValidationRule): 11 | """Known fragment names 12 | 13 | A GraphQL document is only valid if all ``...Fragment`` fragment spreads refer to 14 | fragments defined in the same document. 15 | """ 16 | 17 | def enter_fragment_spread(self, node: FragmentSpreadNode, *_args: Any) -> None: 18 | fragment_name = node.name.value 19 | fragment = self.context.get_fragment(fragment_name) 20 | if not fragment: 21 | self.report_error( 22 | GraphQLError(f"Unknown fragment '{fragment_name}'.", node.name) 23 | ) 24 | -------------------------------------------------------------------------------- /tests/pyutils/test_did_you_mean.py: -------------------------------------------------------------------------------- 1 | from graphql.pyutils import did_you_mean 2 | 3 | 4 | def describe_did_you_mean(): 5 | def does_accept_an_empty_list(): 6 | assert did_you_mean([]) == "" 7 | 8 | def handles_single_suggestion(): 9 | assert did_you_mean(["A"]) == " Did you mean 'A'?" 10 | 11 | def handles_two_suggestions(): 12 | assert did_you_mean(["A", "B"]) == " Did you mean 'A' or 'B'?" 13 | 14 | def handles_multiple_suggestions(): 15 | assert did_you_mean(["A", "B", "C"]) == " Did you mean 'A', 'B', or 'C'?" 16 | 17 | def limits_to_five_suggestions(): 18 | assert ( 19 | did_you_mean(["A", "B", "C", "D", "E", "F"]) 20 | == " Did you mean 'A', 'B', 'C', 'D', or 'E'?" 21 | ) 22 | 23 | def adds_sub_message(): 24 | assert did_you_mean(["A"], "the letter") == " Did you mean the letter 'A'?" 25 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /src/graphql/pyutils/is_awaitable.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | from typing import Any 3 | from types import CoroutineType, GeneratorType 4 | 5 | __all__ = ["is_awaitable"] 6 | 7 | CO_ITERABLE_COROUTINE = inspect.CO_ITERABLE_COROUTINE 8 | 9 | 10 | def is_awaitable(value: Any) -> bool: 11 | """Return true if object can be passed to an ``await`` expression. 12 | 13 | Instead of testing if the object is an instance of abc.Awaitable, it checks 14 | the existence of an `__await__` attribute. This is much faster. 15 | """ 16 | return ( 17 | # check for coroutine objects 18 | isinstance(value, CoroutineType) 19 | # check for old-style generator based coroutine objects 20 | or isinstance(value, GeneratorType) 21 | and bool(value.gi_code.co_flags & CO_ITERABLE_COROUTINE) 22 | # check for other awaitables (e.g. futures) 23 | or hasattr(value, "__await__") 24 | ) 25 | -------------------------------------------------------------------------------- /tests/pyutils/test_path.py: -------------------------------------------------------------------------------- 1 | from graphql.pyutils import Path 2 | 3 | 4 | def describe_path(): 5 | def add_path(): 6 | path = Path(None, 0, None) 7 | assert path.prev is None 8 | assert path.key == 0 9 | prev, path = path, Path(path, 1, None) 10 | assert path.prev is prev 11 | assert path.key == 1 12 | prev, path = path, Path(path, "two", None) 13 | assert path.prev is prev 14 | assert path.key == "two" 15 | 16 | def add_key(): 17 | prev = Path(None, 0, None) 18 | path = prev.add_key("one") 19 | assert path.prev is prev 20 | assert path.key == "one" 21 | 22 | def as_list(): 23 | path = Path(None, 1, None) 24 | assert path.as_list() == [1] 25 | path = path.add_key("two") 26 | assert path.as_list() == [1, "two"] 27 | path = path.add_key(3) 28 | assert path.as_list() == [1, "two", 3] 29 | -------------------------------------------------------------------------------- /src/graphql/pyutils/did_you_mean.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, Sequence 2 | 3 | __all__ = ["did_you_mean"] 4 | 5 | MAX_LENGTH = 5 6 | 7 | 8 | def did_you_mean(suggestions: Sequence[str], sub_message: Optional[str] = None) -> str: 9 | """Given [ A, B, C ] return ' Did you mean A, B, or C?'""" 10 | if not suggestions or not MAX_LENGTH: 11 | return "" 12 | parts = [" Did you mean "] 13 | if sub_message: 14 | parts.extend([sub_message, " "]) 15 | suggestions = suggestions[:MAX_LENGTH] 16 | n = len(suggestions) 17 | if n == 1: 18 | parts.append(f"'{suggestions[0]}'?") 19 | elif n == 2: 20 | parts.append(f"'{suggestions[0]}' or '{suggestions[1]}'?") 21 | else: 22 | parts.extend( 23 | [ 24 | ", ".join(f"'{s}'" for s in suggestions[:-1]), 25 | f", or '{suggestions[-1]}'?", 26 | ] 27 | ) 28 | return "".join(parts) 29 | -------------------------------------------------------------------------------- /tests/pyutils/test_undefined.py: -------------------------------------------------------------------------------- 1 | from graphql.pyutils import Undefined 2 | 3 | 4 | def describe_invalid(): 5 | def has_repr(): 6 | assert repr(Undefined) == "Undefined" 7 | 8 | def has_str(): 9 | assert str(Undefined) == "Undefined" 10 | 11 | def is_hashable(): 12 | assert hash(Undefined) == hash(Undefined) 13 | assert hash(Undefined) != hash(None) 14 | assert hash(Undefined) != hash(False) 15 | assert hash(Undefined) != hash(True) 16 | 17 | def as_bool_is_false(): 18 | assert bool(Undefined) is False 19 | 20 | def only_equal_to_itself(): 21 | assert Undefined == Undefined 22 | assert not Undefined != Undefined 23 | none_object = None 24 | assert Undefined != none_object 25 | assert not Undefined == none_object 26 | false_object = False 27 | assert Undefined != false_object 28 | assert not Undefined == false_object 29 | -------------------------------------------------------------------------------- /tests/utilities/test_concat_ast.py: -------------------------------------------------------------------------------- 1 | from graphql.language import parse, print_ast, Source 2 | from graphql.utilities import concat_ast 3 | 4 | from ..utils import dedent 5 | 6 | 7 | def describe_concat_ast(): 8 | def concatenates_two_asts_together(): 9 | source_a = Source( 10 | """ 11 | { a, b, ... Frag } 12 | """ 13 | ) 14 | 15 | source_b = Source( 16 | """ 17 | fragment Frag on T { 18 | c 19 | } 20 | """ 21 | ) 22 | 23 | ast_a = parse(source_a) 24 | ast_b = parse(source_b) 25 | ast_c = concat_ast([ast_a, ast_b]) 26 | 27 | assert print_ast(ast_c) == dedent( 28 | """ 29 | { 30 | a 31 | b 32 | ...Frag 33 | } 34 | 35 | fragment Frag on T { 36 | c 37 | } 38 | """ 39 | ) 40 | -------------------------------------------------------------------------------- /src/graphql/pyutils/is_iterable.py: -------------------------------------------------------------------------------- 1 | from typing import ( 2 | Any, 3 | ByteString, 4 | Collection, 5 | Iterable, 6 | Mapping, 7 | Text, 8 | ValuesView, 9 | ) 10 | 11 | __all__ = ["is_collection", "is_iterable"] 12 | 13 | collection_types: Any = Collection 14 | if not isinstance({}.values(), Collection): # Python < 3.7.2 15 | collection_types = (Collection, ValuesView) 16 | iterable_types: Any = Iterable 17 | not_iterable_types: Any = (ByteString, Mapping, Text) 18 | 19 | 20 | def is_collection(value: Any) -> bool: 21 | """Check if value is a collection, but not a string or a mapping.""" 22 | return isinstance(value, collection_types) and not isinstance( 23 | value, not_iterable_types 24 | ) 25 | 26 | 27 | def is_iterable(value: Any) -> bool: 28 | """Check if value is an iterable, but not a string or a mapping.""" 29 | return isinstance(value, iterable_types) and not isinstance( 30 | value, not_iterable_types 31 | ) 32 | -------------------------------------------------------------------------------- /src/graphql/language/directive_locations.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | __all__ = ["DirectiveLocation"] 4 | 5 | 6 | class DirectiveLocation(Enum): 7 | """The enum type representing the directive location values.""" 8 | 9 | # Request Definitions 10 | QUERY = "query" 11 | MUTATION = "mutation" 12 | SUBSCRIPTION = "subscription" 13 | FIELD = "field" 14 | FRAGMENT_DEFINITION = "fragment definition" 15 | FRAGMENT_SPREAD = "fragment spread" 16 | VARIABLE_DEFINITION = "variable definition" 17 | INLINE_FRAGMENT = "inline fragment" 18 | 19 | # Type System Definitions 20 | SCHEMA = "schema" 21 | SCALAR = "scalar" 22 | OBJECT = "object" 23 | FIELD_DEFINITION = "field definition" 24 | ARGUMENT_DEFINITION = "argument definition" 25 | INTERFACE = "interface" 26 | UNION = "union" 27 | ENUM = "enum" 28 | ENUM_VALUE = "enum value" 29 | INPUT_OBJECT = "input object" 30 | INPUT_FIELD_DEFINITION = "input field definition" 31 | -------------------------------------------------------------------------------- /src/graphql/pyutils/undefined.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | __all__ = ["Undefined", "UndefinedType"] 4 | 5 | 6 | class UndefinedType(ValueError): 7 | """Auxiliary class for creating the Undefined singleton.""" 8 | 9 | def __repr__(self) -> str: 10 | return "Undefined" 11 | 12 | __str__ = __repr__ 13 | 14 | def __hash__(self) -> int: 15 | return hash(UndefinedType) 16 | 17 | def __bool__(self) -> bool: 18 | return False 19 | 20 | def __eq__(self, other: Any) -> bool: 21 | return other is Undefined 22 | 23 | def __ne__(self, other: Any) -> bool: 24 | return not self == other 25 | 26 | 27 | # Used to indicate undefined or invalid values (like "undefined" in JavaScript): 28 | Undefined = UndefinedType() 29 | 30 | Undefined.__doc__ = """Symbol for undefined values 31 | 32 | This singleton object is used to describe undefined or invalid values. 33 | It can be used in places where you would use ``undefined`` in GraphQL.js. 34 | """ 35 | -------------------------------------------------------------------------------- /tests/pyutils/test_natural_compare.py: -------------------------------------------------------------------------------- 1 | from graphql.pyutils import natural_comparison_key 2 | 3 | key = natural_comparison_key 4 | 5 | 6 | def describe_natural_compare(): 7 | def handles_empty_strings(): 8 | assert key("") < key("a") 9 | assert key("") < key("1") 10 | 11 | def handles_strings_of_different_length(): 12 | assert key("A") < key("AA") 13 | assert key("A1") < key("A1A") 14 | 15 | def handles_numbers(): 16 | assert key("1") < key("2") 17 | assert key("2") < key("11") 18 | 19 | def handles_numbers_with_leading_zeros(): 20 | assert key("0") < key("00") 21 | assert key("02") < key("11") 22 | assert key("011") < key("200") 23 | 24 | def handles_numbers_embedded_into_names(): 25 | assert key("a0a") < key("a9a") 26 | assert key("a00a") < key("a09a") 27 | assert key("a0a1") < key("a0a9") 28 | assert key("a10a11a") < key("a10a19a") 29 | assert key("a10a11a") < key("a10a11b") 30 | -------------------------------------------------------------------------------- /tests/error/test_located_error.py: -------------------------------------------------------------------------------- 1 | from typing import cast, Any 2 | 3 | from graphql.error import GraphQLError, located_error 4 | 5 | 6 | def describe_located_error(): 7 | def throws_without_an_original_error(): 8 | e = located_error([], [], []).original_error # type: ignore 9 | assert isinstance(e, TypeError) 10 | assert str(e) == "Unexpected error value: []" 11 | 12 | def passes_graphql_error_through(): 13 | path = ["path", 3, "to", "field"] 14 | e = GraphQLError("msg", None, None, None, cast(Any, path)) 15 | assert located_error(e, [], []) == e 16 | 17 | def passes_graphql_error_ish_through(): 18 | e = GraphQLError("I am a located GraphQL error") 19 | e.path = [] 20 | assert located_error(e, [], []) is e 21 | 22 | def does_not_pass_through_elasticsearch_like_errors(): 23 | e = Exception("I am from elasticsearch") 24 | cast(Any, e).path = "/something/feed/_search" 25 | assert located_error(e, [], []) is not e 26 | -------------------------------------------------------------------------------- /src/graphql/pyutils/path.py: -------------------------------------------------------------------------------- 1 | from typing import Any, List, NamedTuple, Optional, Union 2 | 3 | __all__ = ["Path"] 4 | 5 | 6 | class Path(NamedTuple): 7 | """A generic path of string or integer indices""" 8 | 9 | prev: Any # Optional['Path'] (python/mypy/issues/731) 10 | """path with the previous indices""" 11 | key: Union[str, int] 12 | """current index in the path (string or integer)""" 13 | typename: Optional[str] 14 | """name of the parent type to avoid path ambiguity""" 15 | 16 | def add_key(self, key: Union[str, int], typename: Optional[str] = None) -> "Path": 17 | """Return a new Path containing the given key.""" 18 | return Path(self, key, typename) 19 | 20 | def as_list(self) -> List[Union[str, int]]: 21 | """Return a list of the path keys.""" 22 | flattened: List[Union[str, int]] = [] 23 | append = flattened.append 24 | curr: Path = self 25 | while curr: 26 | append(curr.key) 27 | curr = curr.prev 28 | return flattened[::-1] 29 | -------------------------------------------------------------------------------- /tests/fixtures/__init__.py: -------------------------------------------------------------------------------- 1 | """Fixtures for graphql tests""" 2 | import json 3 | from os.path import dirname, join 4 | 5 | from pytest import fixture 6 | 7 | __all__ = [ 8 | "kitchen_sink_query", 9 | "kitchen_sink_sdl", 10 | "big_schema_sdl", 11 | "big_schema_introspection_result", 12 | ] 13 | 14 | 15 | def read_graphql(name): 16 | path = join(dirname(__file__), name + ".graphql") 17 | return open(path, encoding="utf-8").read() 18 | 19 | 20 | def read_json(name): 21 | path = join(dirname(__file__), name + ".json") 22 | return json.load(open(path, encoding="utf-8")) 23 | 24 | 25 | @fixture(scope="module") 26 | def kitchen_sink_query(): 27 | return read_graphql("kitchen_sink") 28 | 29 | 30 | @fixture(scope="module") 31 | def kitchen_sink_sdl(): 32 | return read_graphql("schema_kitchen_sink") 33 | 34 | 35 | @fixture(scope="module") 36 | def big_schema_sdl(): 37 | return read_graphql("github_schema") 38 | 39 | 40 | @fixture(scope="module") 41 | def big_schema_introspection_result(): 42 | return read_json("github_schema") 43 | -------------------------------------------------------------------------------- /src/graphql/utilities/assert_valid_name.py: -------------------------------------------------------------------------------- 1 | import re 2 | from typing import Optional 3 | 4 | from ..error import GraphQLError 5 | 6 | __all__ = ["assert_valid_name", "is_valid_name_error"] 7 | 8 | 9 | re_name = re.compile("^[_a-zA-Z][_a-zA-Z0-9]*$") 10 | 11 | 12 | def assert_valid_name(name: str) -> str: 13 | """Uphold the spec rules about naming.""" 14 | error = is_valid_name_error(name) 15 | if error: 16 | raise error 17 | return name 18 | 19 | 20 | def is_valid_name_error(name: str) -> Optional[GraphQLError]: 21 | """Return an Error if a name is invalid.""" 22 | if not isinstance(name, str): 23 | raise TypeError("Expected name to be a string.") 24 | if name.startswith("__"): 25 | return GraphQLError( 26 | f"Name {name!r} must not begin with '__'," 27 | " which is reserved by GraphQL introspection." 28 | ) 29 | if not re_name.match(name): 30 | return GraphQLError( 31 | f"Names must match /^[_a-zA-Z][_a-zA-Z0-9]*$/ but {name!r} does not." 32 | ) 33 | return None 34 | -------------------------------------------------------------------------------- /tests/benchmarks/test_execution_sync.py: -------------------------------------------------------------------------------- 1 | from graphql import ( 2 | GraphQLSchema, 3 | GraphQLObjectType, 4 | GraphQLField, 5 | GraphQLString, 6 | graphql_sync, 7 | ) 8 | 9 | 10 | user = GraphQLObjectType( 11 | name="User", 12 | fields={ 13 | "id": GraphQLField(GraphQLString), 14 | "name": GraphQLField(GraphQLString), 15 | }, 16 | ) 17 | 18 | 19 | def resolve_user(obj, info): 20 | return { 21 | "id": "1", 22 | "name": "Sarah", 23 | } 24 | 25 | 26 | schema = GraphQLSchema( 27 | query=GraphQLObjectType( 28 | name="Query", 29 | fields={ 30 | "user": GraphQLField( 31 | user, 32 | resolve=resolve_user, 33 | ) 34 | }, 35 | ) 36 | ) 37 | 38 | 39 | def test_execute_basic_sync(benchmark): 40 | result = benchmark(lambda: graphql_sync(schema, "query { user { id, name }}")) 41 | assert not result.errors 42 | assert result.data == { 43 | "user": { 44 | "id": "1", 45 | "name": "Sarah", 46 | }, 47 | } 48 | -------------------------------------------------------------------------------- /tests/utilities/test_assert_valid_name.py: -------------------------------------------------------------------------------- 1 | from pytest import raises 2 | 3 | from graphql.error import GraphQLError 4 | from graphql.utilities import assert_valid_name 5 | 6 | 7 | def describe_assert_valid_name(): 8 | def pass_through_valid_name(): 9 | assert assert_valid_name("_ValidName123") == "_ValidName123" 10 | 11 | def throws_for_use_of_leading_double_underscore(): 12 | with raises(GraphQLError) as exc_info: 13 | assert assert_valid_name("__bad") 14 | msg = exc_info.value.message 15 | assert msg == ( 16 | "Name '__bad' must not begin with '__'," 17 | " which is reserved by GraphQL introspection." 18 | ) 19 | 20 | def throws_for_non_strings(): 21 | with raises(TypeError) as exc_info: 22 | # noinspection PyTypeChecker 23 | assert_valid_name({}) # type: ignore 24 | msg = str(exc_info.value) 25 | assert msg == "Expected name to be a string." 26 | 27 | def throws_for_names_with_invalid_characters(): 28 | with raises(GraphQLError, match="Names must match"): 29 | assert_valid_name(">--()-->") 30 | -------------------------------------------------------------------------------- /docs/modules/pyutils.rst: -------------------------------------------------------------------------------- 1 | PyUtils 2 | ======= 3 | 4 | .. currentmodule:: graphql.pyutils 5 | 6 | .. automodule:: graphql.pyutils 7 | :no-members: 8 | :no-inherited-members: 9 | 10 | .. autofunction:: camel_to_snake 11 | .. autofunction:: snake_to_camel 12 | .. autofunction:: cached_property 13 | .. autofunction:: register_description 14 | .. autofunction:: unregister_description 15 | .. autofunction:: did_you_mean 16 | .. autofunction:: identity_func 17 | .. autofunction:: inspect 18 | .. autofunction:: is_awaitable 19 | .. autofunction:: is_collection 20 | .. autofunction:: is_iterable 21 | .. autofunction:: natural_comparison_key 22 | .. autoclass:: AwaitableOrValue 23 | .. autofunction:: suggestion_list 24 | .. autoclass:: FrozenError 25 | :no-members: 26 | :no-inherited-members: 27 | :no-special-members: 28 | .. autoclass:: FrozenList 29 | :no-members: 30 | :no-inherited-members: 31 | :no-special-members: 32 | .. autoclass:: FrozenDict 33 | :no-members: 34 | :no-inherited-members: 35 | :no-special-members: 36 | .. autoclass:: Path 37 | .. autofunction:: print_path_list 38 | .. autoclass:: SimplePubSub 39 | .. autoclass:: SimplePubSubIterator 40 | .. autodata:: Undefined 41 | -------------------------------------------------------------------------------- /src/graphql/pyutils/cached_property.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Callable, TYPE_CHECKING 2 | 3 | if TYPE_CHECKING: 4 | standard_cached_property = None 5 | else: 6 | try: 7 | from functools import cached_property as standard_cached_property 8 | except ImportError: # Python < 3.8 9 | standard_cached_property = None 10 | 11 | if standard_cached_property: 12 | cached_property = standard_cached_property 13 | else: 14 | # Code taken from https://github.com/bottlepy/bottle 15 | 16 | class CachedProperty: 17 | """A cached property. 18 | 19 | A property that is only computed once per instance and then replaces itself with 20 | an ordinary attribute. Deleting the attribute resets the property. 21 | """ 22 | 23 | def __init__(self, func: Callable) -> None: 24 | self.__doc__ = getattr(func, "__doc__") 25 | self.func = func 26 | 27 | def __get__(self, obj: object, cls: type) -> Any: 28 | if obj is None: 29 | return self 30 | value = obj.__dict__[self.func.__name__] = self.func(obj) 31 | return value 32 | 33 | cached_property = CachedProperty 34 | 35 | __all__ = ["cached_property"] 36 | -------------------------------------------------------------------------------- /src/graphql/utilities/get_operation_ast.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from ..language import DocumentNode, OperationDefinitionNode 4 | 5 | __all__ = ["get_operation_ast"] 6 | 7 | 8 | def get_operation_ast( 9 | document_ast: DocumentNode, operation_name: Optional[str] = None 10 | ) -> Optional[OperationDefinitionNode]: 11 | """Get operation AST node. 12 | 13 | Returns an operation AST given a document AST and optionally an operation 14 | name. If a name is not provided, an operation is only returned if only one 15 | is provided in the document. 16 | """ 17 | operation = None 18 | for definition in document_ast.definitions: 19 | if isinstance(definition, OperationDefinitionNode): 20 | if operation_name is None: 21 | # If no operation name was provided, only return an Operation if there 22 | # is one defined in the document. 23 | # Upon encountering the second, return None. 24 | if operation: 25 | return None 26 | operation = definition 27 | elif definition.name and definition.name.value == operation_name: 28 | return definition 29 | return operation 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) GraphQL Contributors (GraphQL.js) 4 | Copyright (c) Syrus Akbary (GraphQL-core 2) 5 | Copyright (c) Christoph Zwerschke (GraphQL-core 3) 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | -------------------------------------------------------------------------------- /src/graphql/validation/rules/__init__.py: -------------------------------------------------------------------------------- 1 | """graphql.validation.rules package""" 2 | 3 | from ...error import GraphQLError 4 | from ...language.visitor import Visitor 5 | from ..validation_context import ( 6 | ASTValidationContext, 7 | SDLValidationContext, 8 | ValidationContext, 9 | ) 10 | 11 | __all__ = ["ASTValidationRule", "SDLValidationRule", "ValidationRule"] 12 | 13 | 14 | class ASTValidationRule(Visitor): 15 | """Visitor for validation of an AST.""" 16 | 17 | context: ASTValidationContext 18 | 19 | def __init__(self, context: ASTValidationContext): 20 | self.context = context 21 | 22 | def report_error(self, error: GraphQLError) -> None: 23 | self.context.report_error(error) 24 | 25 | 26 | class SDLValidationRule(ASTValidationRule): 27 | """Visitor for validation of an SDL AST.""" 28 | 29 | context: SDLValidationContext 30 | 31 | def __init__(self, context: SDLValidationContext) -> None: 32 | super().__init__(context) 33 | 34 | 35 | class ValidationRule(ASTValidationRule): 36 | """Visitor for validation using a GraphQL schema.""" 37 | 38 | context: ValidationContext 39 | 40 | def __init__(self, context: ValidationContext) -> None: 41 | super().__init__(context) 42 | -------------------------------------------------------------------------------- /tests/benchmarks/test_validate_invalid_gql.py: -------------------------------------------------------------------------------- 1 | from graphql import build_schema, parse, validate 2 | 3 | from ..fixtures import big_schema_sdl # noqa: F401 4 | 5 | 6 | def test_validate_invalid_query(benchmark, big_schema_sdl): # noqa: F811 7 | schema = build_schema(big_schema_sdl, assume_valid=True) 8 | query_ast = parse( 9 | """ 10 | { 11 | unknownField 12 | ... on unknownType { 13 | anotherUnknownField 14 | ...unknownFragment 15 | } 16 | } 17 | 18 | fragment TestFragment on anotherUnknownType { 19 | yetAnotherUnknownField 20 | } 21 | """ 22 | ) 23 | result = benchmark(lambda: validate(schema, query_ast)) 24 | assert result == [ 25 | { 26 | "message": "Cannot query field 'unknownField' on type 'Query'.", 27 | "locations": [(3, 11)], 28 | }, 29 | {"message": "Unknown type 'unknownType'.", "locations": [(4, 18)]}, 30 | {"message": "Unknown fragment 'unknownFragment'.", "locations": [(6, 16)]}, 31 | {"message": "Unknown type 'anotherUnknownType'.", "locations": [(10, 34)]}, 32 | {"message": "Fragment 'TestFragment' is never used.", "locations": [(10, 9)]}, 33 | ] 34 | -------------------------------------------------------------------------------- /src/graphql/pyutils/frozen_dict.py: -------------------------------------------------------------------------------- 1 | from copy import deepcopy 2 | from typing import Dict, TypeVar 3 | 4 | from .frozen_error import FrozenError 5 | 6 | __all__ = ["FrozenDict"] 7 | 8 | KT = TypeVar("KT") 9 | VT = TypeVar("VT", covariant=True) 10 | 11 | 12 | class FrozenDict(Dict[KT, VT]): 13 | """Dictionary that can only be read, but not changed.""" 14 | 15 | def __delitem__(self, key): 16 | raise FrozenError 17 | 18 | def __setitem__(self, key, value): 19 | raise FrozenError 20 | 21 | def __iadd__(self, value): 22 | raise FrozenError 23 | 24 | def __hash__(self): 25 | return hash(tuple(self.items())) 26 | 27 | def __copy__(self) -> "FrozenDict": 28 | return FrozenDict(self) 29 | 30 | copy = __copy__ 31 | 32 | def __deepcopy__(self, memo: Dict) -> "FrozenDict": 33 | return FrozenDict({k: deepcopy(v, memo) for k, v in self.items()}) 34 | 35 | def clear(self): 36 | raise FrozenError 37 | 38 | def pop(self, key, default=None): 39 | raise FrozenError 40 | 41 | def popitem(self): 42 | raise FrozenError 43 | 44 | def setdefault(self, key, default=None): 45 | raise FrozenError 46 | 47 | def update(self, other=None): 48 | raise FrozenError 49 | -------------------------------------------------------------------------------- /tests/fixtures/kitchen_sink.graphql: -------------------------------------------------------------------------------- 1 | query queryName($foo: ComplexType, $site: Site = MOBILE) @onQuery { 2 | whoever123is: node(id: [123, 456]) { 3 | id , 4 | ... on User @onInlineFragment { 5 | field2 { 6 | id , 7 | alias: field1(first:10, after:$foo,) @include(if: $foo) { 8 | id, 9 | ...frag @onFragmentSpread 10 | } 11 | } 12 | } 13 | ... @skip(unless: $foo) { 14 | id 15 | } 16 | ... { 17 | id 18 | } 19 | } 20 | } 21 | 22 | mutation likeStory @onMutation { 23 | like(story: 123) @onField { 24 | story { 25 | id @onField 26 | } 27 | } 28 | } 29 | 30 | subscription StoryLikeSubscription( 31 | $input: StoryLikeSubscribeInput @onVariableDefinition 32 | ) @onSubscription { 33 | storyLikeSubscribe(input: $input) { 34 | story { 35 | likers { 36 | count 37 | } 38 | likeSentence { 39 | text 40 | } 41 | } 42 | } 43 | } 44 | 45 | fragment frag on Friend @onFragmentDefinition { 46 | foo(size: $size, bar: $b, obj: {key: "value", block: """ 47 | 48 | block string uses \""" 49 | 50 | """}) 51 | } 52 | 53 | { 54 | unnamed(truthy: true, falsy: false, nullish: null), 55 | query 56 | } 57 | 58 | query { __typename } 59 | -------------------------------------------------------------------------------- /src/graphql/validation/rules/variables_are_input_types.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from ...error import GraphQLError 4 | from ...language import VariableDefinitionNode, print_ast 5 | from ...type import is_input_type 6 | from ...utilities import type_from_ast 7 | from . import ValidationRule 8 | 9 | __all__ = ["VariablesAreInputTypesRule"] 10 | 11 | 12 | class VariablesAreInputTypesRule(ValidationRule): 13 | """Variables are input types 14 | 15 | A GraphQL operation is only valid if all the variables it defines are of input types 16 | (scalar, enum, or input object). 17 | """ 18 | 19 | def enter_variable_definition( 20 | self, node: VariableDefinitionNode, *_args: Any 21 | ) -> None: 22 | type_ = type_from_ast(self.context.schema, node.type) 23 | 24 | # If the variable type is not an input type, return an error. 25 | if type_ and not is_input_type(type_): 26 | variable_name = node.variable.name.value 27 | type_name = print_ast(node.type) 28 | self.report_error( 29 | GraphQLError( 30 | f"Variable '${variable_name}'" 31 | f" cannot be non-input type '{type_name}'.", 32 | node.type, 33 | ) 34 | ) 35 | -------------------------------------------------------------------------------- /src/graphql/validation/rules/custom/no_schema_introspection.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from ....error import GraphQLError 4 | from ....language import FieldNode 5 | from ....type import get_named_type, is_introspection_type 6 | from .. import ValidationRule 7 | 8 | __all__ = ["NoSchemaIntrospectionCustomRule"] 9 | 10 | 11 | class NoSchemaIntrospectionCustomRule(ValidationRule): 12 | """Prohibit introspection queries 13 | 14 | A GraphQL document is only valid if all fields selected are not fields that 15 | return an introspection type. 16 | 17 | Note: This rule is optional and is not part of the Validation section of the 18 | GraphQL Specification. This rule effectively disables introspection, which 19 | does not reflect best practices and should only be done if absolutely necessary. 20 | """ 21 | 22 | def enter_field(self, node: FieldNode, *_args: Any) -> None: 23 | type_ = get_named_type(self.context.get_type()) 24 | if type_ and is_introspection_type(type_): 25 | self.report_error( 26 | GraphQLError( 27 | "GraphQL introspection has been disabled, but the requested query" 28 | f" contained the field '{node.name.value}'.", 29 | node, 30 | ) 31 | ) 32 | -------------------------------------------------------------------------------- /tests/benchmarks/test_execution_async.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from graphql import ( 3 | GraphQLSchema, 4 | GraphQLObjectType, 5 | GraphQLField, 6 | GraphQLString, 7 | graphql, 8 | ) 9 | 10 | 11 | user = GraphQLObjectType( 12 | name="User", 13 | fields={ 14 | "id": GraphQLField(GraphQLString), 15 | "name": GraphQLField(GraphQLString), 16 | }, 17 | ) 18 | 19 | 20 | async def resolve_user(obj, info): 21 | return { 22 | "id": "1", 23 | "name": "Sarah", 24 | } 25 | 26 | 27 | schema = GraphQLSchema( 28 | query=GraphQLObjectType( 29 | name="Query", 30 | fields={ 31 | "user": GraphQLField( 32 | user, 33 | resolve=resolve_user, 34 | ) 35 | }, 36 | ) 37 | ) 38 | 39 | 40 | def test_execute_basic_async(benchmark): 41 | try: 42 | run = asyncio.run 43 | except AttributeError: # Python < 3.7 44 | loop = asyncio.get_event_loop() 45 | run = loop.run_until_complete # type: ignore 46 | result = benchmark(lambda: run(graphql(schema, "query { user { id, name }}"))) 47 | assert not result.errors 48 | assert result.data == { 49 | "user": { 50 | "id": "1", 51 | "name": "Sarah", 52 | }, 53 | } 54 | -------------------------------------------------------------------------------- /src/graphql/validation/rules/lone_anonymous_operation.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from ...error import GraphQLError 4 | from ...language import DocumentNode, OperationDefinitionNode 5 | from . import ASTValidationContext, ASTValidationRule 6 | 7 | __all__ = ["LoneAnonymousOperationRule"] 8 | 9 | 10 | class LoneAnonymousOperationRule(ASTValidationRule): 11 | """Lone anonymous operation 12 | 13 | A GraphQL document is only valid if when it contains an anonymous operation 14 | (the query short-hand) that it contains only that one operation definition. 15 | """ 16 | 17 | def __init__(self, context: ASTValidationContext): 18 | super().__init__(context) 19 | self.operation_count = 0 20 | 21 | def enter_document(self, node: DocumentNode, *_args: Any) -> None: 22 | self.operation_count = sum( 23 | isinstance(definition, OperationDefinitionNode) 24 | for definition in node.definitions 25 | ) 26 | 27 | def enter_operation_definition( 28 | self, node: OperationDefinitionNode, *_args: Any 29 | ) -> None: 30 | if not node.name and self.operation_count > 1: 31 | self.report_error( 32 | GraphQLError( 33 | "This anonymous operation must be the only defined operation.", node 34 | ) 35 | ) 36 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py3{6,7,8,9,10}, black, flake8, mypy, docs, manifest 3 | isolated_build = true 4 | 5 | [gh-actions] 6 | python = 7 | 3.6: py36 8 | 3.7: py37 9 | 3.8: py38 10 | 3.9: py39 11 | 3.10: py310 12 | 13 | [testenv:black] 14 | basepython = python3.9 15 | deps = black==21.11b1 16 | commands = 17 | black src tests setup.py -t py39 --check 18 | 19 | [testenv:flake8] 20 | basepython = python3.9 21 | deps = flake8>=4,<5 22 | commands = 23 | flake8 src tests setup.py 24 | 25 | [testenv:mypy] 26 | basepython = python3.9 27 | deps = 28 | mypy==0.910 29 | pytest>=6.2,<7 30 | commands = 31 | mypy src tests 32 | 33 | [testenv:docs] 34 | basepython = python3.9 35 | deps = 36 | sphinx>=4.3,<5 37 | sphinx_rtd_theme>=1,<2 38 | commands = 39 | sphinx-build -b html -nEW docs docs/_build/html 40 | 41 | [testenv:manifest] 42 | basepython = python3.9 43 | deps = check-manifest>=0.46,<1 44 | commands = 45 | check-manifest -v 46 | 47 | [testenv] 48 | deps = 49 | pytest>=6.2,<7 50 | pytest-asyncio>=0.16,<1 51 | pytest-benchmark>=3.4,<4 52 | pytest-cov>=3,<4 53 | pytest-describe>=2,<3 54 | pytest-timeout>=1.4,<2 55 | py36,py37: typing-extensions>=3.10,<4 56 | commands = 57 | pytest tests {posargs: --cov-report=term-missing --cov=graphql --cov=tests --cov-fail-under=100} 58 | -------------------------------------------------------------------------------- /docs/usage/other.rst: -------------------------------------------------------------------------------- 1 | Subscriptions 2 | ------------- 3 | 4 | .. currentmodule:: graphql.subscription 5 | 6 | Sometimes you need to not only query data from a server, but you also want to push data 7 | from the server to the client. GraphQL-core 3 has you also covered here, because it 8 | implements the "Subscribe" algorithm described in the GraphQL spec. To execute a GraphQL 9 | subscription, you must use the :func:`subscribe` method from the 10 | :mod:`graphql.subscription` module. Instead of a single 11 | :class:`~graphql.execution.ExecutionResult`, this function returns an asynchronous 12 | iterator yielding a stream of those, unless there was an immediate error. 13 | Of course you will then also need to maintain a persistent channel to the client 14 | (often realized via WebSockets) to push these results back. 15 | 16 | 17 | Other Usages 18 | ------------ 19 | 20 | .. currentmodule:: graphql.utilities 21 | 22 | GraphQL-core 3 provides many more low-level functions that can be used to work with 23 | GraphQL schemas and queries. We encourage you to explore the contents of the various 24 | :ref:`sub-packages`, particularly :mod:`graphql.utilities`, and to look into the source 25 | code and tests of `GraphQL-core 3`_ in order to find all the functionality that is 26 | provided and understand it in detail. 27 | 28 | .. _GraphQL-core 3: https://github.com/graphql-python/graphql-core 29 | -------------------------------------------------------------------------------- /src/graphql/language/location.py: -------------------------------------------------------------------------------- 1 | from typing import Any, NamedTuple, TYPE_CHECKING 2 | 3 | try: 4 | from typing import TypedDict 5 | except ImportError: # Python < 3.8 6 | from typing_extensions import TypedDict 7 | 8 | if TYPE_CHECKING: 9 | from .source import Source # noqa: F401 10 | 11 | __all__ = ["get_location", "SourceLocation", "FormattedSourceLocation"] 12 | 13 | 14 | class FormattedSourceLocation(TypedDict): 15 | """Formatted source location""" 16 | 17 | line: int 18 | column: int 19 | 20 | 21 | class SourceLocation(NamedTuple): 22 | """Represents a location in a Source.""" 23 | 24 | line: int 25 | column: int 26 | 27 | @property 28 | def formatted(self) -> FormattedSourceLocation: 29 | return dict(line=self.line, column=self.column) 30 | 31 | def __eq__(self, other: Any) -> bool: 32 | if isinstance(other, dict): 33 | return self.formatted == other 34 | return tuple(self) == other 35 | 36 | def __ne__(self, other: Any) -> bool: 37 | return not self == other 38 | 39 | 40 | def get_location(source: "Source", position: int) -> SourceLocation: 41 | """Get the line and column for a character position in the source. 42 | 43 | Takes a Source and a UTF-8 character offset, and returns the corresponding line and 44 | column as a SourceLocation. 45 | """ 46 | return source.get_location(position) 47 | -------------------------------------------------------------------------------- /src/graphql/validation/rules/unique_variable_names.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict 2 | 3 | from ...error import GraphQLError 4 | from ...language import NameNode, VariableDefinitionNode 5 | from . import ASTValidationContext, ASTValidationRule 6 | 7 | __all__ = ["UniqueVariableNamesRule"] 8 | 9 | 10 | class UniqueVariableNamesRule(ASTValidationRule): 11 | """Unique variable names 12 | 13 | A GraphQL operation is only valid if all its variables are uniquely named. 14 | """ 15 | 16 | def __init__(self, context: ASTValidationContext): 17 | super().__init__(context) 18 | self.known_variable_names: Dict[str, NameNode] = {} 19 | 20 | def enter_operation_definition(self, *_args: Any) -> None: 21 | self.known_variable_names.clear() 22 | 23 | def enter_variable_definition( 24 | self, node: VariableDefinitionNode, *_args: Any 25 | ) -> None: 26 | known_variable_names = self.known_variable_names 27 | variable_name = node.variable.name.value 28 | if variable_name in known_variable_names: 29 | self.report_error( 30 | GraphQLError( 31 | f"There can be only one variable named '${variable_name}'.", 32 | [known_variable_names[variable_name], node.variable.name], 33 | ) 34 | ) 35 | else: 36 | known_variable_names[variable_name] = node.variable.name 37 | -------------------------------------------------------------------------------- /src/graphql/validation/rules/unique_argument_names.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict 2 | 3 | from ...error import GraphQLError 4 | from ...language import ArgumentNode, NameNode, VisitorAction, SKIP 5 | from . import ASTValidationContext, ASTValidationRule 6 | 7 | __all__ = ["UniqueArgumentNamesRule"] 8 | 9 | 10 | class UniqueArgumentNamesRule(ASTValidationRule): 11 | """Unique argument names 12 | 13 | A GraphQL field or directive is only valid if all supplied arguments are uniquely 14 | named. 15 | """ 16 | 17 | def __init__(self, context: ASTValidationContext): 18 | super().__init__(context) 19 | self.known_arg_names: Dict[str, NameNode] = {} 20 | 21 | def enter_field(self, *_args: Any) -> None: 22 | self.known_arg_names.clear() 23 | 24 | def enter_directive(self, *_args: Any) -> None: 25 | self.known_arg_names.clear() 26 | 27 | def enter_argument(self, node: ArgumentNode, *_args: Any) -> VisitorAction: 28 | known_arg_names = self.known_arg_names 29 | arg_name = node.name.value 30 | if arg_name in known_arg_names: 31 | self.report_error( 32 | GraphQLError( 33 | f"There can be only one argument named '{arg_name}'.", 34 | [known_arg_names[arg_name], node.name], 35 | ) 36 | ) 37 | else: 38 | known_arg_names[arg_name] = node.name 39 | return SKIP 40 | -------------------------------------------------------------------------------- /src/graphql/validation/rules/unique_fragment_names.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict 2 | 3 | from ...error import GraphQLError 4 | from ...language import NameNode, FragmentDefinitionNode, VisitorAction, SKIP 5 | from . import ASTValidationContext, ASTValidationRule 6 | 7 | __all__ = ["UniqueFragmentNamesRule"] 8 | 9 | 10 | class UniqueFragmentNamesRule(ASTValidationRule): 11 | """Unique fragment names 12 | 13 | A GraphQL document is only valid if all defined fragments have unique names. 14 | """ 15 | 16 | def __init__(self, context: ASTValidationContext): 17 | super().__init__(context) 18 | self.known_fragment_names: Dict[str, NameNode] = {} 19 | 20 | @staticmethod 21 | def enter_operation_definition(*_args: Any) -> VisitorAction: 22 | return SKIP 23 | 24 | def enter_fragment_definition( 25 | self, node: FragmentDefinitionNode, *_args: Any 26 | ) -> VisitorAction: 27 | known_fragment_names = self.known_fragment_names 28 | fragment_name = node.name.value 29 | if fragment_name in known_fragment_names: 30 | self.report_error( 31 | GraphQLError( 32 | f"There can be only one fragment named '{fragment_name}'.", 33 | [known_fragment_names[fragment_name], node.name], 34 | ) 35 | ) 36 | else: 37 | known_fragment_names[fragment_name] = node.name 38 | return SKIP 39 | -------------------------------------------------------------------------------- /src/graphql/validation/rules/lone_schema_definition.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from ...error import GraphQLError 4 | from ...language import SchemaDefinitionNode 5 | from . import SDLValidationRule, SDLValidationContext 6 | 7 | __all__ = ["LoneSchemaDefinitionRule"] 8 | 9 | 10 | class LoneSchemaDefinitionRule(SDLValidationRule): 11 | """Lone Schema definition 12 | 13 | A GraphQL document is only valid if it contains only one schema definition. 14 | """ 15 | 16 | def __init__(self, context: SDLValidationContext): 17 | super().__init__(context) 18 | old_schema = context.schema 19 | self.already_defined = old_schema and ( 20 | old_schema.ast_node 21 | or old_schema.query_type 22 | or old_schema.mutation_type 23 | or old_schema.subscription_type 24 | ) 25 | self.schema_definitions_count = 0 26 | 27 | def enter_schema_definition(self, node: SchemaDefinitionNode, *_args: Any) -> None: 28 | if self.already_defined: 29 | self.report_error( 30 | GraphQLError( 31 | "Cannot define a new schema within a schema extension.", node 32 | ) 33 | ) 34 | else: 35 | if self.schema_definitions_count: 36 | self.report_error( 37 | GraphQLError("Must provide only one schema definition.", node) 38 | ) 39 | self.schema_definitions_count += 1 40 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/open-a-graphql-core-issue.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Open a GraphQL-core issue 3 | about: General template for all GraphQL-core issues 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | # Questions regarding how to use GraphQL 11 | 12 | If you have a question on how to use GraphQL, please [post it to Stack Overflow](https://stackoverflow.com/questions/ask?tags=graphql) with the tag [#graphql](https://stackoverflow.com/questions/tagged/graphql). 13 | 14 | # Reporting issues with GraphQL-core 3 15 | 16 | Before filing a new issue, make sure an issue for your problem doesn't already exist and that this is not an issue that should be filed against a different repository (see below). 17 | 18 | The best way to get a bug fixed is to provide a _pull request_ with a simplified failing test case (or better yet, include a fix). 19 | 20 | # Reporting issues with GraphQL-core 2 21 | 22 | Please use the issue tracker of the [legacy repository](https://github.com/graphql-python/graphql-core-legacy) for issues with legacy versions of GraphQL-core. 23 | 24 | # Feature requests 25 | 26 | GraphQL-core is a Python port of the [GraphQL.js](https://github.com/graphql/graphql-js) reference implementation of the [GraphQL specification](https://github.com/graphql/graphql-spec). To discuss new features which are not Python specific, please open an issue against the GraphQL.js project. To discuss features that fundamentally change the way GraphQL works, open an issue against the specification. 27 | -------------------------------------------------------------------------------- /tests/validation/test_variables_are_input_types.py: -------------------------------------------------------------------------------- 1 | from functools import partial 2 | 3 | from graphql.validation import VariablesAreInputTypesRule 4 | 5 | from .harness import assert_validation_errors 6 | 7 | assert_errors = partial(assert_validation_errors, VariablesAreInputTypesRule) 8 | 9 | assert_valid = partial(assert_errors, errors=[]) 10 | 11 | 12 | def describe_validate_variables_are_input_types(): 13 | def input_types_are_valid(): 14 | assert_valid( 15 | """ 16 | query Foo($a: String, $b: [Boolean!]!, $c: ComplexInput) { 17 | field(a: $a, b: $b, c: $c) 18 | } 19 | """ 20 | ) 21 | 22 | def output_types_are_invalid(): 23 | assert_errors( 24 | """ 25 | query Foo($a: Dog, $b: [[CatOrDog!]]!, $c: Pet) { 26 | field(a: $a, b: $b, c: $c) 27 | } 28 | """, 29 | [ 30 | { 31 | "locations": [(2, 27)], 32 | "message": "Variable '$a' cannot be non-input type 'Dog'.", 33 | }, 34 | { 35 | "locations": [(2, 36)], 36 | "message": "Variable '$b' cannot be" 37 | " non-input type '[[CatOrDog!]]!'.", 38 | }, 39 | { 40 | "locations": [(2, 56)], 41 | "message": "Variable '$c' cannot be non-input type 'Pet'.", 42 | }, 43 | ], 44 | ) 45 | -------------------------------------------------------------------------------- /src/graphql/version.py: -------------------------------------------------------------------------------- 1 | import re 2 | from typing import NamedTuple 3 | 4 | __all__ = ["version", "version_info", "version_js", "version_info_js"] 5 | 6 | 7 | version = "3.2.0rc2" 8 | 9 | version_js = "16.0.0rc2" 10 | 11 | 12 | _re_version = re.compile(r"(\d+)\.(\d+)\.(\d+)(\D*)(\d*)") 13 | 14 | 15 | class VersionInfo(NamedTuple): 16 | major: int 17 | minor: int 18 | micro: int 19 | releaselevel: str 20 | serial: int 21 | 22 | @classmethod 23 | def from_str(cls, v: str) -> "VersionInfo": 24 | groups = _re_version.match(v).groups() # type: ignore 25 | major, minor, micro = map(int, groups[:3]) 26 | level = (groups[3] or "")[:1] 27 | if level == "a": 28 | level = "alpha" 29 | elif level == "b": 30 | level = "beta" 31 | elif level in ("c", "r"): 32 | level = "candidate" 33 | else: 34 | level = "final" 35 | serial = groups[4] 36 | serial = int(serial) if serial else 0 37 | return cls(major, minor, micro, level, serial) 38 | 39 | def __str__(self) -> str: 40 | v = f"{self.major}.{self.minor}.{self.micro}" 41 | level = self.releaselevel 42 | if level and level != "final": 43 | level = level[:1] 44 | if level == "c": 45 | level = "rc" 46 | v = f"{v}{level}{self.serial}" 47 | return v 48 | 49 | 50 | version_info = VersionInfo.from_str(version) 51 | 52 | version_info_js = VersionInfo.from_str(version_js) 53 | -------------------------------------------------------------------------------- /tests/utilities/test_type_from_ast.py: -------------------------------------------------------------------------------- 1 | from pytest import raises 2 | 3 | from graphql.language import parse_type, TypeNode 4 | from graphql.type import GraphQLList, GraphQLNonNull, GraphQLObjectType 5 | from graphql.utilities import type_from_ast 6 | 7 | from ..validation.harness import test_schema 8 | 9 | 10 | def describe_type_from_ast(): 11 | def for_named_type_node(): 12 | node = parse_type("Cat") 13 | type_for_node = type_from_ast(test_schema, node) 14 | assert isinstance(type_for_node, GraphQLObjectType) 15 | assert type_for_node.name == "Cat" 16 | 17 | def for_list_type_node(): 18 | node = parse_type("[Cat]") 19 | type_for_node = type_from_ast(test_schema, node) 20 | assert isinstance(type_for_node, GraphQLList) 21 | of_type = type_for_node.of_type 22 | assert isinstance(of_type, GraphQLObjectType) 23 | assert of_type.name == "Cat" 24 | 25 | def for_non_null_type_node(): 26 | node = parse_type("Cat!") 27 | type_for_node = type_from_ast(test_schema, node) 28 | assert isinstance(type_for_node, GraphQLNonNull) 29 | of_type = type_for_node.of_type 30 | assert isinstance(of_type, GraphQLObjectType) 31 | assert of_type.name == "Cat" 32 | 33 | def for_unspecified_type_node(): 34 | node = TypeNode() 35 | with raises(TypeError) as exc_info: 36 | type_from_ast(test_schema, node) 37 | msg = str(exc_info.value) 38 | assert msg == "Unexpected type node: ." 39 | -------------------------------------------------------------------------------- /src/graphql/validation/rules/unique_input_field_names.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict, List 2 | 3 | from ...error import GraphQLError 4 | from ...language import NameNode, ObjectFieldNode 5 | from . import ASTValidationContext, ASTValidationRule 6 | 7 | __all__ = ["UniqueInputFieldNamesRule"] 8 | 9 | 10 | class UniqueInputFieldNamesRule(ASTValidationRule): 11 | """Unique input field names 12 | 13 | A GraphQL input object value is only valid if all supplied fields are uniquely 14 | named. 15 | """ 16 | 17 | def __init__(self, context: ASTValidationContext): 18 | super().__init__(context) 19 | self.known_names_stack: List[Dict[str, NameNode]] = [] 20 | self.known_names: Dict[str, NameNode] = {} 21 | 22 | def enter_object_value(self, *_args: Any) -> None: 23 | self.known_names_stack.append(self.known_names) 24 | self.known_names = {} 25 | 26 | def leave_object_value(self, *_args: Any) -> None: 27 | self.known_names = self.known_names_stack.pop() 28 | 29 | def enter_object_field(self, node: ObjectFieldNode, *_args: Any) -> None: 30 | known_names = self.known_names 31 | field_name = node.name.value 32 | if field_name in known_names: 33 | self.report_error( 34 | GraphQLError( 35 | f"There can be only one input field named '{field_name}'.", 36 | [known_names[field_name], node.name], 37 | ) 38 | ) 39 | else: 40 | known_names[field_name] = node.name 41 | -------------------------------------------------------------------------------- /tests/execution/test_customize.py: -------------------------------------------------------------------------------- 1 | from graphql.execution import execute, ExecutionContext 2 | from graphql.language import parse 3 | from graphql.type import GraphQLSchema, GraphQLObjectType, GraphQLString, GraphQLField 4 | 5 | 6 | def describe_customize_execution(): 7 | def uses_a_custom_field_resolver(): 8 | query = parse("{ foo }") 9 | 10 | schema = GraphQLSchema( 11 | GraphQLObjectType("Query", {"foo": GraphQLField(GraphQLString)}) 12 | ) 13 | 14 | # For the purposes of test, just return the name of the field! 15 | def custom_resolver(_source, info, **_args): 16 | return info.field_name 17 | 18 | assert execute(schema, query, field_resolver=custom_resolver) == ( 19 | {"foo": "foo"}, 20 | None, 21 | ) 22 | 23 | def uses_a_custom_execution_context_class(): 24 | query = parse("{ foo }") 25 | 26 | schema = GraphQLSchema( 27 | GraphQLObjectType( 28 | "Query", 29 | {"foo": GraphQLField(GraphQLString, resolve=lambda *_args: "bar")}, 30 | ) 31 | ) 32 | 33 | class TestExecutionContext(ExecutionContext): 34 | def execute_field(self, parent_type, source, field_nodes, path): 35 | result = super().execute_field(parent_type, source, field_nodes, path) 36 | return result * 2 # type: ignore 37 | 38 | assert execute(schema, query, execution_context_class=TestExecutionContext) == ( 39 | {"foo": "barbar"}, 40 | None, 41 | ) 42 | -------------------------------------------------------------------------------- /src/graphql/validation/rules/unique_operation_names.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict 2 | 3 | from ...error import GraphQLError 4 | from ...language import NameNode, OperationDefinitionNode, VisitorAction, SKIP 5 | from . import ASTValidationContext, ASTValidationRule 6 | 7 | __all__ = ["UniqueOperationNamesRule"] 8 | 9 | 10 | class UniqueOperationNamesRule(ASTValidationRule): 11 | """Unique operation names 12 | 13 | A GraphQL document is only valid if all defined operations have unique names. 14 | """ 15 | 16 | def __init__(self, context: ASTValidationContext): 17 | super().__init__(context) 18 | self.known_operation_names: Dict[str, NameNode] = {} 19 | 20 | def enter_operation_definition( 21 | self, node: OperationDefinitionNode, *_args: Any 22 | ) -> VisitorAction: 23 | operation_name = node.name 24 | if operation_name: 25 | known_operation_names = self.known_operation_names 26 | if operation_name.value in known_operation_names: 27 | self.report_error( 28 | GraphQLError( 29 | "There can be only one operation" 30 | f" named '{operation_name.value}'.", 31 | [known_operation_names[operation_name.value], operation_name], 32 | ) 33 | ) 34 | else: 35 | known_operation_names[operation_name.value] = operation_name 36 | return SKIP 37 | 38 | @staticmethod 39 | def enter_fragment_definition(*_args: Any) -> VisitorAction: 40 | return SKIP 41 | -------------------------------------------------------------------------------- /tests/utilities/test_strip_ignored_characters_fuzz.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from pytest import mark 4 | 5 | from graphql.error import GraphQLSyntaxError 6 | from graphql.language import Lexer, Source, TokenKind 7 | from graphql.utilities import strip_ignored_characters 8 | 9 | from ..utils import dedent, gen_fuzz_strings 10 | 11 | 12 | def lex_value(s: str) -> Optional[str]: 13 | lexer = Lexer(Source(s)) 14 | value = lexer.advance().value 15 | assert lexer.advance().kind == TokenKind.EOF, "Expected EOF" 16 | return value 17 | 18 | 19 | def describe_strip_ignored_characters(): 20 | @mark.slow 21 | @mark.timeout(20) 22 | def strips_ignored_characters_inside_random_block_strings(): 23 | # Testing with length >7 is taking exponentially more time. However it is 24 | # highly recommended to test with increased limit if you make any change. 25 | for fuzz_str in gen_fuzz_strings(allowed_chars='\n\t "a\\', max_length=7): 26 | test_str = f'"""{fuzz_str}"""' 27 | 28 | try: 29 | test_value = lex_value(test_str) 30 | except (AssertionError, GraphQLSyntaxError): 31 | continue # skip invalid values 32 | 33 | stripped_value = lex_value(strip_ignored_characters(test_str)) 34 | 35 | assert test_value == stripped_value, dedent( 36 | f""" 37 | Expected lexValue(stripIgnoredCharacters({test_str!r}) 38 | to equal {test_value!r} 39 | but got {stripped_value!r} 40 | """ 41 | ) 42 | -------------------------------------------------------------------------------- /src/graphql/utilities/get_operation_root_type.py: -------------------------------------------------------------------------------- 1 | from typing import Union 2 | 3 | from ..error import GraphQLError 4 | from ..language import ( 5 | OperationType, 6 | OperationDefinitionNode, 7 | OperationTypeDefinitionNode, 8 | ) 9 | from ..type import GraphQLObjectType, GraphQLSchema 10 | 11 | __all__ = ["get_operation_root_type"] 12 | 13 | 14 | def get_operation_root_type( 15 | schema: GraphQLSchema, 16 | operation: Union[OperationDefinitionNode, OperationTypeDefinitionNode], 17 | ) -> GraphQLObjectType: 18 | """Extract the root type of the operation from the schema.""" 19 | operation_type = operation.operation 20 | if operation_type == OperationType.QUERY: 21 | query_type = schema.query_type 22 | if not query_type: 23 | raise GraphQLError( 24 | "Schema does not define the required query root type.", operation 25 | ) 26 | return query_type 27 | 28 | if operation_type == OperationType.MUTATION: 29 | mutation_type = schema.mutation_type 30 | if not mutation_type: 31 | raise GraphQLError("Schema is not configured for mutations.", operation) 32 | return mutation_type 33 | 34 | if operation_type == OperationType.SUBSCRIPTION: 35 | subscription_type = schema.subscription_type 36 | if not subscription_type: 37 | raise GraphQLError("Schema is not configured for subscriptions.", operation) 38 | return subscription_type 39 | 40 | raise GraphQLError( 41 | "Can only have query, mutation and subscription operations.", operation 42 | ) 43 | -------------------------------------------------------------------------------- /src/graphql/validation/rules/scalar_leafs.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from ...error import GraphQLError 4 | from ...language import FieldNode 5 | from ...type import get_named_type, is_leaf_type 6 | from . import ValidationRule 7 | 8 | __all__ = ["ScalarLeafsRule"] 9 | 10 | 11 | class ScalarLeafsRule(ValidationRule): 12 | """Scalar leafs 13 | 14 | A GraphQL document is valid only if all leaf fields (fields without sub selections) 15 | are of scalar or enum types. 16 | """ 17 | 18 | def enter_field(self, node: FieldNode, *_args: Any) -> None: 19 | type_ = self.context.get_type() 20 | if type_: 21 | selection_set = node.selection_set 22 | if is_leaf_type(get_named_type(type_)): 23 | if selection_set: 24 | field_name = node.name.value 25 | self.report_error( 26 | GraphQLError( 27 | f"Field '{field_name}' must not have a selection" 28 | f" since type '{type_}' has no subfields.", 29 | selection_set, 30 | ) 31 | ) 32 | elif not selection_set: 33 | field_name = node.name.value 34 | self.report_error( 35 | GraphQLError( 36 | f"Field '{field_name}' of type '{type_}'" 37 | " must have a selection of subfields." 38 | f" Did you mean '{field_name} {{ ... }}'?", 39 | node, 40 | ) 41 | ) 42 | -------------------------------------------------------------------------------- /docs/usage/extension.rst: -------------------------------------------------------------------------------- 1 | Extending a Schema 2 | ------------------ 3 | 4 | .. currentmodule:: graphql.utilities 5 | 6 | With GraphQL-core 3 you can also extend a given schema using type extensions. For 7 | example, we might want to add a ``lastName`` property to our ``Human`` data type to 8 | retrieve only the last name of the person. 9 | 10 | This can be achieved with the :func:`extend_schema` function as follows:: 11 | 12 | from graphql import extend_schema, parse 13 | 14 | schema = extend_schema(schema, parse(""" 15 | extend type Human { 16 | lastName: String 17 | } 18 | """)) 19 | 20 | Note that this function expects the extensions as an AST, which we can get using the 21 | :func:`~graphql.language.parse` function. Also note that the :func:`extend_schema` 22 | function does not alter the original schema, but returns a new schema object. 23 | 24 | We also need to attach a resolver function to the new field:: 25 | 26 | def get_last_name(human, info): 27 | return human['name'].rsplit(None, 1)[-1] 28 | 29 | schema.get_type('Human').fields['lastName'].resolve = get_last_name 30 | 31 | Now we can query only the last name of a human:: 32 | 33 | from graphql import graphql_sync 34 | 35 | result = graphql_sync(schema, """ 36 | { 37 | human(id: "1000") { 38 | lastName 39 | homePlanet 40 | } 41 | } 42 | """) 43 | print(result) 44 | 45 | This query will give the following result:: 46 | 47 | ExecutionResult( 48 | data={'human': {'lastName': 'Skywalker', 'homePlanet': 'Tatooine'}}, 49 | errors=None) 50 | 51 | -------------------------------------------------------------------------------- /src/graphql/validation/rules/executable_definitions.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Union, cast 2 | 3 | from ...error import GraphQLError 4 | from ...language import ( 5 | DirectiveDefinitionNode, 6 | DocumentNode, 7 | ExecutableDefinitionNode, 8 | SchemaDefinitionNode, 9 | SchemaExtensionNode, 10 | TypeDefinitionNode, 11 | VisitorAction, 12 | SKIP, 13 | ) 14 | from . import ASTValidationRule 15 | 16 | __all__ = ["ExecutableDefinitionsRule"] 17 | 18 | 19 | class ExecutableDefinitionsRule(ASTValidationRule): 20 | """Executable definitions 21 | 22 | A GraphQL document is only valid for execution if all definitions are either 23 | operation or fragment definitions. 24 | """ 25 | 26 | def enter_document(self, node: DocumentNode, *_args: Any) -> VisitorAction: 27 | for definition in node.definitions: 28 | if not isinstance(definition, ExecutableDefinitionNode): 29 | def_name = ( 30 | "schema" 31 | if isinstance( 32 | definition, (SchemaDefinitionNode, SchemaExtensionNode) 33 | ) 34 | else "'{}'".format( 35 | cast( 36 | Union[DirectiveDefinitionNode, TypeDefinitionNode], 37 | definition, 38 | ).name.value 39 | ) 40 | ) 41 | self.report_error( 42 | GraphQLError( 43 | f"The {def_name} definition is not executable.", 44 | definition, 45 | ) 46 | ) 47 | return SKIP 48 | -------------------------------------------------------------------------------- /src/graphql/pyutils/frozen_list.py: -------------------------------------------------------------------------------- 1 | from copy import deepcopy 2 | from typing import Dict, List, TypeVar 3 | 4 | from .frozen_error import FrozenError 5 | 6 | __all__ = ["FrozenList"] 7 | 8 | 9 | T = TypeVar("T", covariant=True) 10 | 11 | 12 | class FrozenList(List[T]): 13 | """List that can only be read, but not changed.""" 14 | 15 | def __delitem__(self, key): 16 | raise FrozenError 17 | 18 | def __setitem__(self, key, value): 19 | raise FrozenError 20 | 21 | def __add__(self, value): 22 | if isinstance(value, tuple): 23 | value = list(value) 24 | return list.__add__(self, value) 25 | 26 | def __iadd__(self, value): 27 | raise FrozenError 28 | 29 | def __mul__(self, value): 30 | return list.__mul__(self, value) 31 | 32 | def __imul__(self, value): 33 | raise FrozenError 34 | 35 | def __hash__(self): 36 | return hash(tuple(self)) 37 | 38 | def __copy__(self) -> "FrozenList": 39 | return FrozenList(self) 40 | 41 | def __deepcopy__(self, memo: Dict) -> "FrozenList": 42 | return FrozenList(deepcopy(value, memo) for value in self) 43 | 44 | def append(self, x): 45 | raise FrozenError 46 | 47 | def extend(self, iterable): 48 | raise FrozenError 49 | 50 | def insert(self, i, x): 51 | raise FrozenError 52 | 53 | def remove(self, x): 54 | raise FrozenError 55 | 56 | def pop(self, i=None): 57 | raise FrozenError 58 | 59 | def clear(self): 60 | raise FrozenError 61 | 62 | def sort(self, *, key=None, reverse=False): 63 | raise FrozenError 64 | 65 | def reverse(self): 66 | raise FrozenError 67 | -------------------------------------------------------------------------------- /tests/validation/test_unique_variable_names.py: -------------------------------------------------------------------------------- 1 | from functools import partial 2 | 3 | from graphql.validation import UniqueVariableNamesRule 4 | 5 | from .harness import assert_validation_errors 6 | 7 | assert_errors = partial(assert_validation_errors, UniqueVariableNamesRule) 8 | 9 | assert_valid = partial(assert_errors, errors=[]) 10 | 11 | 12 | def describe_validate_unique_variable_names(): 13 | def unique_variable_names(): 14 | assert_valid( 15 | """ 16 | query A($x: Int, $y: String) { __typename } 17 | query B($x: String, $y: Int) { __typename } 18 | """ 19 | ) 20 | 21 | def duplicate_variable_names(): 22 | assert_errors( 23 | """ 24 | query A($x: Int, $x: Int, $x: String) { __typename } 25 | query B($x: String, $x: Int) { __typename } 26 | query C($x: Int, $x: Int) { __typename } 27 | """, 28 | [ 29 | { 30 | "message": "There can be only one variable named '$x'.", 31 | "locations": [(2, 22), (2, 31)], 32 | }, 33 | { 34 | "message": "There can be only one variable named '$x'.", 35 | "locations": [(2, 22), (2, 40)], 36 | }, 37 | { 38 | "message": "There can be only one variable named '$x'.", 39 | "locations": [(3, 22), (3, 34)], 40 | }, 41 | { 42 | "message": "There can be only one variable named '$x'.", 43 | "locations": [(4, 22), (4, 31)], 44 | }, 45 | ], 46 | ) 47 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from re import search 2 | from setuptools import setup, find_packages 3 | 4 | with open("src/graphql/version.py") as version_file: 5 | version = search('version = "(.*)"', version_file.read()).group(1) 6 | 7 | with open("README.md") as readme_file: 8 | readme = readme_file.read() 9 | 10 | setup( 11 | name="graphql-core", 12 | version=version, 13 | description="GraphQL implementation for Python, a port of GraphQL.js," 14 | " the JavaScript reference implementation for GraphQL.", 15 | long_description=readme, 16 | long_description_content_type="text/markdown", 17 | keywords="graphql", 18 | url="https://github.com/graphql-python/graphql-core", 19 | author="Christoph Zwerschke", 20 | author_email="cito@online.de", 21 | license="MIT license", 22 | classifiers=[ 23 | "Development Status :: 5 - Production/Stable", 24 | "Intended Audience :: Developers", 25 | "Topic :: Software Development :: Libraries", 26 | "License :: OSI Approved :: MIT License", 27 | "Programming Language :: Python :: 3", 28 | "Programming Language :: Python :: 3 :: Only", 29 | "Programming Language :: Python :: 3.6", 30 | "Programming Language :: Python :: 3.7", 31 | "Programming Language :: Python :: 3.8", 32 | "Programming Language :: Python :: 3.9", 33 | "Programming Language :: Python :: 3.10", 34 | ], 35 | install_requires=[], 36 | python_requires=">=3.6,<4", 37 | packages=find_packages("src"), 38 | package_dir={"": "src"}, 39 | # PEP-561: https://www.python.org/dev/peps/pep-0561/ 40 | package_data={"graphql": ["py.typed"]}, 41 | include_package_data=True, 42 | zip_safe=False, 43 | ) 44 | -------------------------------------------------------------------------------- /src/graphql/validation/rules/unique_directive_names.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict 2 | 3 | from ...error import GraphQLError 4 | from ...language import DirectiveDefinitionNode, NameNode, VisitorAction, SKIP 5 | from . import SDLValidationContext, SDLValidationRule 6 | 7 | __all__ = ["UniqueDirectiveNamesRule"] 8 | 9 | 10 | class UniqueDirectiveNamesRule(SDLValidationRule): 11 | """Unique directive names 12 | 13 | A GraphQL document is only valid if all defined directives have unique names. 14 | """ 15 | 16 | def __init__(self, context: SDLValidationContext): 17 | super().__init__(context) 18 | self.known_directive_names: Dict[str, NameNode] = {} 19 | self.schema = context.schema 20 | 21 | def enter_directive_definition( 22 | self, node: DirectiveDefinitionNode, *_args: Any 23 | ) -> VisitorAction: 24 | directive_name = node.name.value 25 | 26 | if self.schema and self.schema.get_directive(directive_name): 27 | self.report_error( 28 | GraphQLError( 29 | f"Directive '@{directive_name}' already exists in the schema." 30 | " It cannot be redefined.", 31 | node.name, 32 | ) 33 | ) 34 | else: 35 | if directive_name in self.known_directive_names: 36 | self.report_error( 37 | GraphQLError( 38 | f"There can be only one directive named '@{directive_name}'.", 39 | [self.known_directive_names[directive_name], node.name], 40 | ) 41 | ) 42 | else: 43 | self.known_directive_names[directive_name] = node.name 44 | return SKIP 45 | 46 | return None 47 | -------------------------------------------------------------------------------- /src/graphql/utilities/introspection_from_schema.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict 2 | 3 | from ..error import GraphQLError 4 | from ..language import parse 5 | from ..type import GraphQLSchema 6 | from .get_introspection_query import get_introspection_query 7 | 8 | __all__ = ["introspection_from_schema"] 9 | 10 | 11 | IntrospectionSchema = Dict[str, Any] 12 | 13 | 14 | def introspection_from_schema( 15 | schema: GraphQLSchema, 16 | descriptions: bool = True, 17 | specified_by_url: bool = True, 18 | directive_is_repeatable: bool = True, 19 | schema_description: bool = True, 20 | input_value_deprecation: bool = True, 21 | ) -> IntrospectionSchema: 22 | """Build an IntrospectionQuery from a GraphQLSchema 23 | 24 | IntrospectionQuery is useful for utilities that care about type and field 25 | relationships, but do not need to traverse through those relationships. 26 | 27 | This is the inverse of build_client_schema. The primary use case is outside of the 28 | server context, for instance when doing schema comparisons. 29 | """ 30 | document = parse( 31 | get_introspection_query( 32 | descriptions, 33 | specified_by_url, 34 | directive_is_repeatable, 35 | schema_description, 36 | input_value_deprecation, 37 | ) 38 | ) 39 | 40 | from ..execution.execute import execute_sync, ExecutionResult 41 | 42 | result = execute_sync(schema, document) 43 | if not isinstance(result, ExecutionResult): # pragma: no cover 44 | raise RuntimeError("Introspection cannot be executed") 45 | if result.errors: # pragma: no cover 46 | raise result.errors[0] 47 | if not result.data: # pragma: no cover 48 | raise GraphQLError("Introspection did not return a result") 49 | return result.data 50 | -------------------------------------------------------------------------------- /tests/utils/test_gen_fuzz_strings.py: -------------------------------------------------------------------------------- 1 | from . import gen_fuzz_strings 2 | 3 | 4 | def describe_gen_fuzz_strings(): 5 | def always_provide_empty_string(): 6 | assert list(gen_fuzz_strings(allowed_chars="", max_length=0)) == [""] 7 | assert list(gen_fuzz_strings(allowed_chars="", max_length=1)) == [""] 8 | assert list(gen_fuzz_strings(allowed_chars="a", max_length=0)) == [""] 9 | 10 | def generate_strings_with_single_character(): 11 | assert list(gen_fuzz_strings(allowed_chars="a", max_length=1)) == ["", "a"] 12 | assert list(gen_fuzz_strings(allowed_chars="abc", max_length=1)) == [ 13 | "", 14 | "a", 15 | "b", 16 | "c", 17 | ] 18 | 19 | def generate_strings_with_multiple_character(): 20 | assert list(gen_fuzz_strings(allowed_chars="a", max_length=2)) == [ 21 | "", 22 | "a", 23 | "aa", 24 | ] 25 | 26 | assert list(gen_fuzz_strings(allowed_chars="abc", max_length=2)) == [ 27 | "", 28 | "a", 29 | "b", 30 | "c", 31 | "aa", 32 | "ab", 33 | "ac", 34 | "ba", 35 | "bb", 36 | "bc", 37 | "ca", 38 | "cb", 39 | "cc", 40 | ] 41 | 42 | def generate_strings_longer_than_possible_number_of_characters(): 43 | assert list(gen_fuzz_strings(allowed_chars="ab", max_length=3)) == [ 44 | "", 45 | "a", 46 | "b", 47 | "aa", 48 | "ab", 49 | "ba", 50 | "bb", 51 | "aaa", 52 | "aab", 53 | "aba", 54 | "abb", 55 | "baa", 56 | "bab", 57 | "bba", 58 | "bbb", 59 | ] 60 | -------------------------------------------------------------------------------- /src/graphql/validation/rules/no_undefined_variables.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Set 2 | 3 | from ...error import GraphQLError 4 | from ...language import OperationDefinitionNode, VariableDefinitionNode 5 | from . import ValidationContext, ValidationRule 6 | 7 | __all__ = ["NoUndefinedVariablesRule"] 8 | 9 | 10 | class NoUndefinedVariablesRule(ValidationRule): 11 | """No undefined variables 12 | 13 | A GraphQL operation is only valid if all variables encountered, both directly and 14 | via fragment spreads, are defined by that operation. 15 | """ 16 | 17 | def __init__(self, context: ValidationContext): 18 | super().__init__(context) 19 | self.defined_variable_names: Set[str] = set() 20 | 21 | def enter_operation_definition(self, *_args: Any) -> None: 22 | self.defined_variable_names.clear() 23 | 24 | def leave_operation_definition( 25 | self, operation: OperationDefinitionNode, *_args: Any 26 | ) -> None: 27 | usages = self.context.get_recursive_variable_usages(operation) 28 | defined_variables = self.defined_variable_names 29 | for usage in usages: 30 | node = usage.node 31 | var_name = node.name.value 32 | if var_name not in defined_variables: 33 | self.report_error( 34 | GraphQLError( 35 | f"Variable '${var_name}' is not defined" 36 | f" by operation '{operation.name.value}'." 37 | if operation.name 38 | else f"Variable '${var_name}' is not defined.", 39 | [node, operation], 40 | ) 41 | ) 42 | 43 | def enter_variable_definition( 44 | self, node: VariableDefinitionNode, *_args: Any 45 | ) -> None: 46 | self.defined_variable_names.add(node.variable.name.value) 47 | -------------------------------------------------------------------------------- /docs/usage/validator.rst: -------------------------------------------------------------------------------- 1 | Validating GraphQL Queries 2 | -------------------------- 3 | 4 | .. currentmodule:: graphql.validation 5 | 6 | When executing GraphQL queries, the second step that happens under the hood after 7 | parsing the source code is a validation against the given schema using the rules of the 8 | GraphQL specification. You can also run the validation step manually by calling the 9 | :func:`validate` function, passing the schema and the AST document:: 10 | 11 | from graphql import parse, validate 12 | 13 | errors = validate(schema, parse(""" 14 | { 15 | human(id: NEWHOPE) { 16 | name 17 | homePlace 18 | friends 19 | } 20 | } 21 | """)) 22 | 23 | As a result, you will get a complete list of all errors that the validators has found. 24 | In this case, we will get the following three validation errors:: 25 | 26 | [GraphQLError( 27 | 'String cannot represent a non string value: NEWHOPE', 28 | locations=[SourceLocation(line=3, column=17)]), 29 | GraphQLError( 30 | "Cannot query field 'homePlace' on type 'Human'." 31 | " Did you mean 'homePlanet'?", 32 | locations=[SourceLocation(line=5, column=9)]), 33 | GraphQLError( 34 | "Field 'friends' of type '[Character]' must have a selection of subfields." 35 | " Did you mean 'friends { ... }'?", 36 | locations=[SourceLocation(line=6, column=9)])] 37 | 38 | These rules are available in the :data:`specified_rules` list and implemented in the 39 | :mod:`graphql.validation.rules` subpackage. Instead of the default rules, you can also 40 | use a subset or create custom rules. The rules are based on the :class:`ValidationRule` 41 | class which is based on the :class:`~graphql.language.Visitor` class which provides a 42 | way of walking through an AST document using the visitor pattern. 43 | -------------------------------------------------------------------------------- /tests/language/test_location.py: -------------------------------------------------------------------------------- 1 | from graphql import SourceLocation 2 | 3 | 4 | def describe_source_location(): 5 | def can_be_formatted(): 6 | location = SourceLocation(1, 2) 7 | assert location.formatted == {"line": 1, "column": 2} 8 | 9 | def can_compare_with_other_source_location(): 10 | location = SourceLocation(1, 2) 11 | same_location = SourceLocation(1, 2) 12 | assert location == same_location 13 | assert not location != same_location 14 | different_location = SourceLocation(1, 1) 15 | assert not location == different_location 16 | assert location != different_location 17 | different_location = SourceLocation(2, 2) 18 | assert not location == different_location 19 | assert location != different_location 20 | 21 | def can_compare_with_location_tuple(): 22 | location = SourceLocation(1, 2) 23 | same_location = (1, 2) 24 | assert location == same_location 25 | assert not location != same_location 26 | different_location = (1, 1) 27 | assert not location == different_location 28 | assert location != different_location 29 | different_location = (2, 2) 30 | assert not location == different_location 31 | assert location != different_location 32 | 33 | def can_compare_with_formatted_location(): 34 | location = SourceLocation(1, 2) 35 | same_location = location.formatted 36 | assert location == same_location 37 | assert not location != same_location 38 | different_location = SourceLocation(1, 1).formatted 39 | assert not location == different_location 40 | assert location != different_location 41 | different_location = SourceLocation(2, 2).formatted 42 | assert not location == different_location 43 | assert location != different_location 44 | -------------------------------------------------------------------------------- /tests/utilities/test_introspection_from_schema.py: -------------------------------------------------------------------------------- 1 | from typing import Dict 2 | 3 | from graphql.type import GraphQLSchema, GraphQLObjectType, GraphQLField, GraphQLString 4 | from graphql.utilities import ( 5 | build_client_schema, 6 | print_schema, 7 | introspection_from_schema, 8 | ) 9 | 10 | from ..utils import dedent 11 | 12 | 13 | def introspection_to_sdl(introspection: Dict) -> str: 14 | return print_schema(build_client_schema(introspection)) 15 | 16 | 17 | def describe_introspection_from_schema(): 18 | 19 | schema = GraphQLSchema( 20 | GraphQLObjectType( 21 | "Simple", 22 | { 23 | "string": GraphQLField( 24 | GraphQLString, description="This is a string field" 25 | ) 26 | }, 27 | description="This is a simple type", 28 | ), 29 | description="This is a simple schema", 30 | ) 31 | 32 | def converts_a_simple_schema(): 33 | introspection = introspection_from_schema(schema) 34 | 35 | assert introspection_to_sdl(introspection) == dedent( 36 | ''' 37 | """This is a simple schema""" 38 | schema { 39 | query: Simple 40 | } 41 | 42 | """This is a simple type""" 43 | type Simple { 44 | """This is a string field""" 45 | string: String 46 | } 47 | ''' 48 | ) 49 | 50 | def converts_a_simple_schema_without_description(): 51 | introspection = introspection_from_schema(schema, descriptions=False) 52 | 53 | assert introspection_to_sdl(introspection) == dedent( 54 | """ 55 | schema { 56 | query: Simple 57 | } 58 | 59 | type Simple { 60 | string: String 61 | } 62 | """ 63 | ) 64 | -------------------------------------------------------------------------------- /src/graphql/validation/rules/no_unused_fragments.py: -------------------------------------------------------------------------------- 1 | from typing import Any, List 2 | 3 | from ...error import GraphQLError 4 | from ...language import ( 5 | FragmentDefinitionNode, 6 | OperationDefinitionNode, 7 | VisitorAction, 8 | SKIP, 9 | ) 10 | from . import ASTValidationContext, ASTValidationRule 11 | 12 | __all__ = ["NoUnusedFragmentsRule"] 13 | 14 | 15 | class NoUnusedFragmentsRule(ASTValidationRule): 16 | """No unused fragments 17 | 18 | A GraphQL document is only valid if all fragment definitions are spread within 19 | operations, or spread within other fragments spread within operations. 20 | """ 21 | 22 | def __init__(self, context: ASTValidationContext): 23 | super().__init__(context) 24 | self.operation_defs: List[OperationDefinitionNode] = [] 25 | self.fragment_defs: List[FragmentDefinitionNode] = [] 26 | 27 | def enter_operation_definition( 28 | self, node: OperationDefinitionNode, *_args: Any 29 | ) -> VisitorAction: 30 | self.operation_defs.append(node) 31 | return SKIP 32 | 33 | def enter_fragment_definition( 34 | self, node: FragmentDefinitionNode, *_args: Any 35 | ) -> VisitorAction: 36 | self.fragment_defs.append(node) 37 | return SKIP 38 | 39 | def leave_document(self, *_args: Any) -> None: 40 | fragment_names_used = set() 41 | get_fragments = self.context.get_recursively_referenced_fragments 42 | for operation in self.operation_defs: 43 | for fragment in get_fragments(operation): 44 | fragment_names_used.add(fragment.name.value) 45 | 46 | for fragment_def in self.fragment_defs: 47 | frag_name = fragment_def.name.value 48 | if frag_name not in fragment_names_used: 49 | self.report_error( 50 | GraphQLError(f"Fragment '{frag_name}' is never used.", fragment_def) 51 | ) 52 | -------------------------------------------------------------------------------- /src/graphql/pyutils/__init__.py: -------------------------------------------------------------------------------- 1 | """Python Utils 2 | 3 | This package contains dependency-free Python utility functions used throughout the 4 | codebase. 5 | 6 | Each utility should belong in its own file and be the default export. 7 | 8 | These functions are not part of the module interface and are subject to change. 9 | """ 10 | 11 | from .convert_case import camel_to_snake, snake_to_camel 12 | from .cached_property import cached_property 13 | from .description import ( 14 | Description, 15 | is_description, 16 | register_description, 17 | unregister_description, 18 | ) 19 | from .did_you_mean import did_you_mean 20 | from .identity_func import identity_func 21 | from .inspect import inspect 22 | from .is_awaitable import is_awaitable 23 | from .is_iterable import is_collection, is_iterable 24 | from .natural_compare import natural_comparison_key 25 | from .awaitable_or_value import AwaitableOrValue 26 | from .suggestion_list import suggestion_list 27 | from .frozen_error import FrozenError 28 | from .frozen_list import FrozenList 29 | from .frozen_dict import FrozenDict 30 | from .path import Path 31 | from .print_path_list import print_path_list 32 | from .simple_pub_sub import SimplePubSub, SimplePubSubIterator 33 | from .undefined import Undefined, UndefinedType 34 | 35 | __all__ = [ 36 | "camel_to_snake", 37 | "snake_to_camel", 38 | "cached_property", 39 | "did_you_mean", 40 | "Description", 41 | "is_description", 42 | "register_description", 43 | "unregister_description", 44 | "identity_func", 45 | "inspect", 46 | "is_awaitable", 47 | "is_collection", 48 | "is_iterable", 49 | "natural_comparison_key", 50 | "AwaitableOrValue", 51 | "suggestion_list", 52 | "FrozenError", 53 | "FrozenList", 54 | "FrozenDict", 55 | "Path", 56 | "print_path_list", 57 | "SimplePubSub", 58 | "SimplePubSubIterator", 59 | "Undefined", 60 | "UndefinedType", 61 | ] 62 | -------------------------------------------------------------------------------- /src/graphql/validation/rules/unique_type_names.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict 2 | 3 | from ...error import GraphQLError 4 | from ...language import NameNode, TypeDefinitionNode, VisitorAction, SKIP 5 | from . import SDLValidationContext, SDLValidationRule 6 | 7 | __all__ = ["UniqueTypeNamesRule"] 8 | 9 | 10 | class UniqueTypeNamesRule(SDLValidationRule): 11 | """Unique type names 12 | 13 | A GraphQL document is only valid if all defined types have unique names. 14 | """ 15 | 16 | def __init__(self, context: SDLValidationContext): 17 | super().__init__(context) 18 | self.known_type_names: Dict[str, NameNode] = {} 19 | self.schema = context.schema 20 | 21 | def check_type_name(self, node: TypeDefinitionNode, *_args: Any) -> VisitorAction: 22 | type_name = node.name.value 23 | 24 | if self.schema and self.schema.get_type(type_name): 25 | self.report_error( 26 | GraphQLError( 27 | f"Type '{type_name}' already exists in the schema." 28 | " It cannot also be defined in this type definition.", 29 | node.name, 30 | ) 31 | ) 32 | else: 33 | if type_name in self.known_type_names: 34 | self.report_error( 35 | GraphQLError( 36 | f"There can be only one type named '{type_name}'.", 37 | [self.known_type_names[type_name], node.name], 38 | ) 39 | ) 40 | else: 41 | self.known_type_names[type_name] = node.name 42 | return SKIP 43 | 44 | return None 45 | 46 | enter_scalar_type_definition = enter_object_type_definition = check_type_name 47 | enter_interface_type_definition = enter_union_type_definition = check_type_name 48 | enter_enum_type_definition = enter_input_object_type_definition = check_type_name 49 | -------------------------------------------------------------------------------- /src/graphql/validation/rules/no_unused_variables.py: -------------------------------------------------------------------------------- 1 | from typing import Any, List, Set 2 | 3 | from ...error import GraphQLError 4 | from ...language import OperationDefinitionNode, VariableDefinitionNode 5 | from . import ValidationContext, ValidationRule 6 | 7 | __all__ = ["NoUnusedVariablesRule"] 8 | 9 | 10 | class NoUnusedVariablesRule(ValidationRule): 11 | """No unused variables 12 | 13 | A GraphQL operation is only valid if all variables defined by an operation are used, 14 | either directly or within a spread fragment. 15 | """ 16 | 17 | def __init__(self, context: ValidationContext): 18 | super().__init__(context) 19 | self.variable_defs: List[VariableDefinitionNode] = [] 20 | 21 | def enter_operation_definition(self, *_args: Any) -> None: 22 | self.variable_defs.clear() 23 | 24 | def leave_operation_definition( 25 | self, operation: OperationDefinitionNode, *_args: Any 26 | ) -> None: 27 | variable_name_used: Set[str] = set() 28 | usages = self.context.get_recursive_variable_usages(operation) 29 | 30 | for usage in usages: 31 | variable_name_used.add(usage.node.name.value) 32 | 33 | for variable_def in self.variable_defs: 34 | variable_name = variable_def.variable.name.value 35 | if variable_name not in variable_name_used: 36 | self.report_error( 37 | GraphQLError( 38 | f"Variable '${variable_name}' is never used" 39 | f" in operation '{operation.name.value}'." 40 | if operation.name 41 | else f"Variable '${variable_name}' is never used.", 42 | variable_def, 43 | ) 44 | ) 45 | 46 | def enter_variable_definition( 47 | self, definition: VariableDefinitionNode, *_args: Any 48 | ) -> None: 49 | self.variable_defs.append(definition) 50 | -------------------------------------------------------------------------------- /tests/language/test_block_string_fuzz.py: -------------------------------------------------------------------------------- 1 | from pytest import mark 2 | 3 | from graphql.error import GraphQLSyntaxError 4 | from graphql.language import Source, Lexer, TokenKind 5 | from graphql.language.block_string import print_block_string 6 | 7 | from ..utils import dedent, gen_fuzz_strings 8 | 9 | 10 | def lex_value(s: str) -> str: 11 | lexer = Lexer(Source(s)) 12 | value = lexer.advance().value 13 | assert isinstance(value, str) 14 | assert lexer.advance().kind == TokenKind.EOF, "Expected EOF" 15 | return value 16 | 17 | 18 | def describe_print_block_string(): 19 | @mark.slow 20 | @mark.timeout(20) 21 | def correctly_print_random_strings(): 22 | # Testing with length >7 is taking exponentially more time. However it is 23 | # highly recommended to test with increased limit if you make any change. 24 | for fuzz_str in gen_fuzz_strings(allowed_chars='\n\t "a\\', max_length=7): 25 | test_str = f'"""{fuzz_str}"""' 26 | 27 | try: 28 | test_value = lex_value(test_str) 29 | except (AssertionError, GraphQLSyntaxError): 30 | continue # skip invalid values 31 | assert isinstance(test_value, str) 32 | 33 | printed_value = lex_value(print_block_string(test_value)) 34 | 35 | assert test_value == printed_value, dedent( 36 | f""" 37 | Expected lex_value(print_block_string({test_value!r}) 38 | to equal {test_value!r} 39 | but got {printed_value!r} 40 | """ 41 | ) 42 | 43 | printed_multiline_string = lex_value(print_block_string(test_value, True)) 44 | 45 | assert test_value == printed_multiline_string, dedent( 46 | f""" 47 | Expected lex_value(print_block_string({test_value!r}, True) 48 | to equal {test_value!r} 49 | but got {printed_multiline_string!r} 50 | """ 51 | ) 52 | -------------------------------------------------------------------------------- /src/graphql/validation/rules/fragments_on_composite_types.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from ...error import GraphQLError 4 | from ...language import ( 5 | FragmentDefinitionNode, 6 | InlineFragmentNode, 7 | print_ast, 8 | ) 9 | from ...type import is_composite_type 10 | from ...utilities import type_from_ast 11 | from . import ValidationRule 12 | 13 | __all__ = ["FragmentsOnCompositeTypesRule"] 14 | 15 | 16 | class FragmentsOnCompositeTypesRule(ValidationRule): 17 | """Fragments on composite type 18 | 19 | Fragments use a type condition to determine if they apply, since fragments can only 20 | be spread into a composite type (object, interface, or union), the type condition 21 | must also be a composite type. 22 | """ 23 | 24 | def enter_inline_fragment(self, node: InlineFragmentNode, *_args: Any) -> None: 25 | type_condition = node.type_condition 26 | if type_condition: 27 | type_ = type_from_ast(self.context.schema, type_condition) 28 | if type_ and not is_composite_type(type_): 29 | type_str = print_ast(type_condition) 30 | self.report_error( 31 | GraphQLError( 32 | "Fragment cannot condition" 33 | f" on non composite type '{type_str}'.", 34 | type_condition, 35 | ) 36 | ) 37 | 38 | def enter_fragment_definition( 39 | self, node: FragmentDefinitionNode, *_args: Any 40 | ) -> None: 41 | type_condition = node.type_condition 42 | type_ = type_from_ast(self.context.schema, type_condition) 43 | if type_ and not is_composite_type(type_): 44 | type_str = print_ast(type_condition) 45 | self.report_error( 46 | GraphQLError( 47 | f"Fragment '{node.name.value}' cannot condition" 48 | f" on non composite type '{type_str}'.", 49 | type_condition, 50 | ) 51 | ) 52 | -------------------------------------------------------------------------------- /src/graphql/error/located_error.py: -------------------------------------------------------------------------------- 1 | from typing import TYPE_CHECKING, Collection, Optional, Union 2 | 3 | from ..pyutils import inspect 4 | from .graphql_error import GraphQLError 5 | 6 | if TYPE_CHECKING: 7 | from ..language.ast import Node # noqa: F401 8 | 9 | __all__ = ["located_error"] 10 | 11 | 12 | def located_error( 13 | original_error: Exception, 14 | nodes: Optional[Union["None", Collection["Node"]]], 15 | path: Optional[Collection[Union[str, int]]] = None, 16 | ) -> GraphQLError: 17 | """Located GraphQL Error 18 | 19 | Given an arbitrary Exception, presumably thrown while attempting to execute a 20 | GraphQL operation, produce a new GraphQLError aware of the location in the document 21 | responsible for the original Exception. 22 | """ 23 | # Sometimes a non-error is thrown, wrap it as a TypeError to ensure consistency. 24 | if not isinstance(original_error, Exception): 25 | original_error = TypeError(f"Unexpected error value: {inspect(original_error)}") 26 | # Note: this uses a brand-check to support GraphQL errors originating from 27 | # other contexts. 28 | if isinstance(original_error, GraphQLError) and original_error.path is not None: 29 | return original_error 30 | try: 31 | # noinspection PyUnresolvedReferences 32 | message = original_error.message # type: ignore 33 | except AttributeError: 34 | message = str(original_error) 35 | try: 36 | # noinspection PyUnresolvedReferences 37 | source = original_error.source # type: ignore 38 | except AttributeError: 39 | source = None 40 | try: 41 | # noinspection PyUnresolvedReferences 42 | positions = original_error.positions # type: ignore 43 | except AttributeError: 44 | positions = None 45 | try: 46 | # noinspection PyUnresolvedReferences 47 | nodes = original_error.nodes or nodes # type: ignore 48 | except AttributeError: 49 | pass 50 | return GraphQLError(message, nodes, source, positions, path, original_error) 51 | -------------------------------------------------------------------------------- /src/graphql/language/print_string.py: -------------------------------------------------------------------------------- 1 | __all__ = ["print_string"] 2 | 3 | 4 | def print_string(s: str) -> str: 5 | """ "Print a string as a GraphQL StringValue literal. 6 | 7 | Replaces control characters and excluded characters (" U+0022 and \\ U+005C) 8 | with escape sequences. 9 | """ 10 | return f'"{s.translate(escape_sequences)}"' 11 | 12 | 13 | escape_sequences = { 14 | 0x00: "\\u0000", 15 | 0x01: "\\u0001", 16 | 0x02: "\\u0002", 17 | 0x03: "\\u0003", 18 | 0x04: "\\u0004", 19 | 0x05: "\\u0005", 20 | 0x06: "\\u0006", 21 | 0x07: "\\u0007", 22 | 0x08: "\\b", 23 | 0x09: "\\t", 24 | 0x0A: "\\n", 25 | 0x0B: "\\u000B", 26 | 0x0C: "\\f", 27 | 0x0D: "\\r", 28 | 0x0E: "\\u000E", 29 | 0x0F: "\\u000F", 30 | 0x10: "\\u0010", 31 | 0x11: "\\u0011", 32 | 0x12: "\\u0012", 33 | 0x13: "\\u0013", 34 | 0x14: "\\u0014", 35 | 0x15: "\\u0015", 36 | 0x16: "\\u0016", 37 | 0x17: "\\u0017", 38 | 0x18: "\\u0018", 39 | 0x19: "\\u0019", 40 | 0x1A: "\\u001A", 41 | 0x1B: "\\u001B", 42 | 0x1C: "\\u001C", 43 | 0x1D: "\\u001D", 44 | 0x1E: "\\u001E", 45 | 0x1F: "\\u001F", 46 | 0x22: '\\"', 47 | 0x5C: "\\\\", 48 | 0x7F: "\\u007F", 49 | 0x80: "\\u0080", 50 | 0x81: "\\u0081", 51 | 0x82: "\\u0082", 52 | 0x83: "\\u0083", 53 | 0x84: "\\u0084", 54 | 0x85: "\\u0085", 55 | 0x86: "\\u0086", 56 | 0x87: "\\u0087", 57 | 0x88: "\\u0088", 58 | 0x89: "\\u0089", 59 | 0x8A: "\\u008A", 60 | 0x8B: "\\u008B", 61 | 0x8C: "\\u008C", 62 | 0x8D: "\\u008D", 63 | 0x8E: "\\u008E", 64 | 0x8F: "\\u008F", 65 | 0x90: "\\u0090", 66 | 0x91: "\\u0091", 67 | 0x92: "\\u0092", 68 | 0x93: "\\u0093", 69 | 0x94: "\\u0094", 70 | 0x95: "\\u0095", 71 | 0x96: "\\u0096", 72 | 0x97: "\\u0097", 73 | 0x98: "\\u0098", 74 | 0x99: "\\u0099", 75 | 0x9A: "\\u009A", 76 | 0x9B: "\\u009B", 77 | 0x9C: "\\u009C", 78 | 0x9D: "\\u009D", 79 | 0x9E: "\\u009E", 80 | 0x9F: "\\u009F", 81 | } 82 | -------------------------------------------------------------------------------- /tests/validation/test_known_fragment_names.py: -------------------------------------------------------------------------------- 1 | from functools import partial 2 | 3 | from graphql.validation import KnownFragmentNamesRule 4 | 5 | from .harness import assert_validation_errors 6 | 7 | assert_errors = partial(assert_validation_errors, KnownFragmentNamesRule) 8 | 9 | assert_valid = partial(assert_errors, errors=[]) 10 | 11 | 12 | def describe_validate_known_fragment_names(): 13 | def known_fragment_names_are_valid(): 14 | assert_valid( 15 | """ 16 | { 17 | human(id: 4) { 18 | ...HumanFields1 19 | ... on Human { 20 | ...HumanFields2 21 | } 22 | ... { 23 | name 24 | } 25 | } 26 | } 27 | fragment HumanFields1 on Human { 28 | name 29 | ...HumanFields3 30 | } 31 | fragment HumanFields2 on Human { 32 | name 33 | } 34 | fragment HumanFields3 on Human { 35 | name 36 | } 37 | """ 38 | ) 39 | 40 | def unknown_fragment_names_are_invalid(): 41 | assert_errors( 42 | """ 43 | { 44 | human(id: 4) { 45 | ...UnknownFragment1 46 | ... on Human { 47 | ...UnknownFragment2 48 | } 49 | } 50 | } 51 | fragment HumanFields on Human { 52 | name 53 | ...UnknownFragment3 54 | } 55 | """, 56 | [ 57 | { 58 | "message": "Unknown fragment 'UnknownFragment1'.", 59 | "locations": [(4, 20)], 60 | }, 61 | { 62 | "message": "Unknown fragment 'UnknownFragment2'.", 63 | "locations": [(6, 22)], 64 | }, 65 | { 66 | "message": "Unknown fragment 'UnknownFragment3'.", 67 | "locations": [(12, 18)], 68 | }, 69 | ], 70 | ) 71 | -------------------------------------------------------------------------------- /src/graphql/pyutils/description.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Tuple, Union 2 | 3 | __all__ = [ 4 | "Description", 5 | "is_description", 6 | "register_description", 7 | "unregister_description", 8 | ] 9 | 10 | 11 | class Description: 12 | """Type checker for human readable descriptions. 13 | 14 | By default, only ordinary strings are accepted as descriptions, 15 | but you can register() other classes that will also be allowed, 16 | e.g. to support lazy string objects that are evaluated only at runtime. 17 | If you register(object), any object will be allowed as description. 18 | """ 19 | 20 | bases: Union[type, Tuple[type, ...]] = str 21 | 22 | @classmethod 23 | def isinstance(cls, obj: Any) -> bool: 24 | return isinstance(obj, cls.bases) 25 | 26 | @classmethod 27 | def register(cls, base: type) -> None: 28 | """Register a class that shall be accepted as a description.""" 29 | if not isinstance(base, type): 30 | raise TypeError("Only types can be registered.") 31 | if base is object: 32 | cls.bases = object 33 | elif cls.bases is object: 34 | cls.bases = base 35 | elif not isinstance(cls.bases, tuple): 36 | if base is not cls.bases: 37 | cls.bases = (cls.bases, base) 38 | elif base not in cls.bases: 39 | cls.bases += (base,) 40 | 41 | @classmethod 42 | def unregister(cls, base: type) -> None: 43 | """Unregister a class that shall no more be accepted as a description.""" 44 | if not isinstance(base, type): 45 | raise TypeError("Only types can be unregistered.") 46 | if isinstance(cls.bases, tuple): 47 | if base in cls.bases: 48 | cls.bases = tuple(b for b in cls.bases if b is not base) 49 | if not cls.bases: 50 | cls.bases = object 51 | elif len(cls.bases) == 1: 52 | cls.bases = cls.bases[0] 53 | elif cls.bases is base: 54 | cls.bases = object 55 | 56 | 57 | is_description = Description.isinstance 58 | register_description = Description.register 59 | unregister_description = Description.unregister 60 | -------------------------------------------------------------------------------- /tests/pyutils/test_convert_case.py: -------------------------------------------------------------------------------- 1 | from graphql.pyutils import camel_to_snake, snake_to_camel 2 | 3 | 4 | def describe_camel_to_snake(): 5 | def converts_typical_names(): 6 | assert camel_to_snake("CamelCase") == "camel_case" 7 | assert ( 8 | camel_to_snake("InputObjectTypeExtensionNode") 9 | == "input_object_type_extension_node" 10 | ) 11 | assert camel_to_snake("CamelToSnake") == "camel_to_snake" 12 | 13 | def may_start_with_lowercase(): 14 | assert camel_to_snake("camelCase") == "camel_case" 15 | 16 | def works_with_acronyms(): 17 | assert camel_to_snake("SlowXMLParser") == "slow_xml_parser" 18 | assert camel_to_snake("FastGraphQLParser") == "fast_graph_ql_parser" 19 | 20 | def works_with_numbers(): 21 | assert camel_to_snake("Python3Script") == "python3_script" 22 | assert camel_to_snake("camel2snake") == "camel2snake" 23 | 24 | def keeps_already_snake(): 25 | assert camel_to_snake("snake_case") == "snake_case" 26 | 27 | 28 | def describe_snake_to_camel(): 29 | def converts_typical_names(): 30 | assert snake_to_camel("snake_case") == "SnakeCase" 31 | assert ( 32 | snake_to_camel("input_object_type_extension_node") 33 | == "InputObjectTypeExtensionNode" 34 | ) 35 | assert snake_to_camel("snake_to_camel") == "SnakeToCamel" 36 | 37 | def may_start_with_uppercase(): 38 | assert snake_to_camel("Snake_case") == "SnakeCase" 39 | 40 | def works_with_acronyms(): 41 | assert snake_to_camel("slow_xml_parser") == "SlowXmlParser" 42 | assert snake_to_camel("fast_graph_ql_parser") == "FastGraphQlParser" 43 | 44 | def works_with_numbers(): 45 | assert snake_to_camel("python3_script") == "Python3Script" 46 | assert snake_to_camel("snake2camel") == "Snake2camel" 47 | 48 | def keeps_already_camel(): 49 | assert snake_to_camel("CamelCase") == "CamelCase" 50 | 51 | def can_produce_lower_camel_case(): 52 | assert snake_to_camel("snake_case", upper=False) == "snakeCase" 53 | assert ( 54 | snake_to_camel("input_object_type_extension_node", False) 55 | == "inputObjectTypeExtensionNode" 56 | ) 57 | -------------------------------------------------------------------------------- /tests/utilities/test_get_operation_ast.py: -------------------------------------------------------------------------------- 1 | from graphql.language import parse 2 | from graphql.utilities import get_operation_ast 3 | 4 | 5 | def describe_get_operation_ast(): 6 | def gets_an_operation_from_a_simple_document(): 7 | doc = parse("{ field }") 8 | assert get_operation_ast(doc) == doc.definitions[0] 9 | 10 | def gets_an_operation_from_a_document_with_named_op_mutation(): 11 | doc = parse("mutation Test { field }") 12 | assert get_operation_ast(doc) == doc.definitions[0] 13 | 14 | def gets_an_operation_from_a_document_with_named_op_subscription(): 15 | doc = parse("subscription Test { field }") 16 | assert get_operation_ast(doc) == doc.definitions[0] 17 | 18 | def does_not_get_missing_operation(): 19 | doc = parse("type Foo { field: String }") 20 | assert get_operation_ast(doc) is None 21 | 22 | def does_not_get_ambiguous_unnamed_operation(): 23 | doc = parse( 24 | """ 25 | { field } 26 | mutation Test { field } 27 | subscription TestSub { field } 28 | """ 29 | ) 30 | assert get_operation_ast(doc) is None 31 | 32 | def does_not_get_ambiguous_named_operation(): 33 | doc = parse( 34 | """ 35 | query TestQ { field } 36 | mutation TestM { field } 37 | subscription TestS { field } 38 | """ 39 | ) 40 | assert get_operation_ast(doc) is None 41 | 42 | def does_not_get_misnamed_operation(): 43 | doc = parse( 44 | """ 45 | query TestQ { field } 46 | mutation TestM { field } 47 | subscription TestS { field } 48 | """ 49 | ) 50 | assert get_operation_ast(doc, "Unknown") is None 51 | 52 | def gets_named_operation(): 53 | doc = parse( 54 | """ 55 | query TestQ { field } 56 | mutation TestM { field } 57 | subscription TestS { field } 58 | """ 59 | ) 60 | assert get_operation_ast(doc, "TestQ") == doc.definitions[0] 61 | assert get_operation_ast(doc, "TestM") == doc.definitions[1] 62 | assert get_operation_ast(doc, "TestS") == doc.definitions[2] 63 | -------------------------------------------------------------------------------- /src/graphql/utilities/type_from_ast.py: -------------------------------------------------------------------------------- 1 | from typing import cast, overload, Optional 2 | 3 | from ..language import ListTypeNode, NamedTypeNode, NonNullTypeNode, TypeNode 4 | from ..pyutils import inspect 5 | from ..type import ( 6 | GraphQLSchema, 7 | GraphQLNamedType, 8 | GraphQLList, 9 | GraphQLNonNull, 10 | GraphQLNullableType, 11 | GraphQLType, 12 | ) 13 | 14 | __all__ = ["type_from_ast"] 15 | 16 | 17 | @overload 18 | def type_from_ast( 19 | schema: GraphQLSchema, type_node: NamedTypeNode 20 | ) -> Optional[GraphQLNamedType]: 21 | ... 22 | 23 | 24 | @overload 25 | def type_from_ast( 26 | schema: GraphQLSchema, type_node: ListTypeNode 27 | ) -> Optional[GraphQLList]: 28 | ... 29 | 30 | 31 | @overload 32 | def type_from_ast( 33 | schema: GraphQLSchema, type_node: NonNullTypeNode 34 | ) -> Optional[GraphQLNonNull]: 35 | ... 36 | 37 | 38 | @overload 39 | def type_from_ast(schema: GraphQLSchema, type_node: TypeNode) -> Optional[GraphQLType]: 40 | ... 41 | 42 | 43 | def type_from_ast( 44 | schema: GraphQLSchema, 45 | type_node: TypeNode, 46 | ) -> Optional[GraphQLType]: 47 | """Get the GraphQL type definition from an AST node. 48 | 49 | Given a Schema and an AST node describing a type, return a GraphQLType definition 50 | which applies to that type. For example, if provided the parsed AST node for 51 | ``[User]``, a GraphQLList instance will be returned, containing the type called 52 | "User" found in the schema. If a type called "User" is not found in the schema, 53 | then None will be returned. 54 | """ 55 | inner_type: Optional[GraphQLType] 56 | if isinstance(type_node, ListTypeNode): 57 | inner_type = type_from_ast(schema, type_node.type) 58 | return GraphQLList(inner_type) if inner_type else None 59 | if isinstance(type_node, NonNullTypeNode): 60 | inner_type = type_from_ast(schema, type_node.type) 61 | inner_type = cast(GraphQLNullableType, inner_type) 62 | return GraphQLNonNull(inner_type) if inner_type else None 63 | if isinstance(type_node, NamedTypeNode): 64 | return schema.get_type(type_node.name.value) 65 | 66 | # Not reachable. All possible type nodes have been considered. 67 | raise TypeError(f"Unexpected type node: {inspect(type_node)}.") 68 | -------------------------------------------------------------------------------- /docs/usage/methods.rst: -------------------------------------------------------------------------------- 1 | Using resolver methods 2 | ---------------------- 3 | 4 | .. currentmodule:: graphql 5 | 6 | Above we have attached resolver functions to the schema only. However, it is also 7 | possible to define resolver methods on the resolved objects, starting with the 8 | ``root_value`` object that you can pass to the :func:`graphql` function when executing 9 | a query. 10 | 11 | In our case, we could create a ``Root`` class with three methods as root resolvers, like 12 | so:: 13 | 14 | class Root: 15 | """The root resolvers""" 16 | 17 | def hero(self, info, episode): 18 | return luke if episode == 5 else artoo 19 | 20 | def human(self, info, id): 21 | return human_data.get(id) 22 | 23 | def droid(self, info, id): 24 | return droid_data.get(id) 25 | 26 | 27 | Since we have defined synchronous methods only, we will use the :func:`graphql_sync` 28 | function to execute a query, passing a ``Root()`` object as the ``root_value``:: 29 | 30 | from graphql import graphql_sync 31 | 32 | result = graphql_sync(schema, """ 33 | { 34 | droid(id: "2001") { 35 | name 36 | primaryFunction 37 | } 38 | } 39 | """, Root()) 40 | print(result) 41 | 42 | Even if we haven't attached a resolver to the ``hero`` field as we did above, this would 43 | now still resolve and give the following output:: 44 | 45 | ExecutionResult( 46 | data={'droid': {'name': 'R2-D2', 'primaryFunction': 'Astromech'}}, 47 | errors=None) 48 | 49 | Of course you can also define asynchronous methods as resolvers, and execute queries 50 | asynchronously with :func:`graphql`. 51 | 52 | In a similar vein, you can also attach resolvers as methods to the resolved objects on 53 | deeper levels than the root of the query. In that case, instead of resolving to 54 | dictionaries with keys for all the fields, as we did above, you would resolve to objects 55 | with attributes for all the fields. For instance, you would define a class ``Human`` 56 | with a method ``friends()`` for resolving the friends of a human. You can also make 57 | use of inheritance in this case. The ``Human`` class and a ``Droid`` class could inherit 58 | from a ``Character`` class and use its methods as resolvers for common fields. 59 | -------------------------------------------------------------------------------- /tests/pyutils/test_suggestion_list.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from graphql.pyutils import suggestion_list 4 | 5 | 6 | def expect_suggestions(input_: str, options: List[str], expected: List[str]) -> None: 7 | assert suggestion_list(input_, options) == expected 8 | 9 | 10 | def describe_suggestion_list(): 11 | def returns_results_when_input_is_empty(): 12 | expect_suggestions("", ["a"], ["a"]) 13 | 14 | def returns_empty_array_when_there_are_no_options(): 15 | expect_suggestions("input", [], []) 16 | 17 | def returns_options_with_small_lexical_distance(): 18 | expect_suggestions("greenish", ["green"], ["green"]) 19 | expect_suggestions("green", ["greenish"], ["greenish"]) 20 | 21 | def rejects_options_with_distance_that_exceeds_threshold(): 22 | expect_suggestions("aaaa", ["aaab"], ["aaab"]) 23 | expect_suggestions("aaaa", ["aabb"], ["aabb"]) 24 | expect_suggestions("aaaa", ["abbb"], []) 25 | 26 | expect_suggestions("ab", ["ca"], []) 27 | 28 | def returns_options_with_different_case(): 29 | expect_suggestions("verylongstring", ["VERYLONGSTRING"], ["VERYLONGSTRING"]) 30 | 31 | expect_suggestions("VERYLONGSTRING", ["verylongstring"], ["verylongstring"]) 32 | 33 | expect_suggestions("VERYLONGSTRING", ["VeryLongString"], ["VeryLongString"]) 34 | 35 | def returns_options_with_transpositions(): 36 | expect_suggestions("agr", ["arg"], ["arg"]) 37 | 38 | expect_suggestions("214365879", ["123456789"], ["123456789"]) 39 | 40 | def returns_options_sorted_based_on_lexical_distance(): 41 | expect_suggestions("abc", ["a", "ab", "abc"], ["abc", "ab", "a"]) 42 | 43 | expect_suggestions( 44 | "GraphQl", 45 | ["graphics", "SQL", "GraphQL", "quarks", "mark"], 46 | ["GraphQL", "graphics"], 47 | ) 48 | 49 | def returns_options_with_the_same_lexical_distance_sorted_naturally(): 50 | expect_suggestions("a", ["az", "ax", "ay"], ["ax", "ay", "az"]) 51 | 52 | expect_suggestions("boo", ["moo", "foo", "zoo"], ["foo", "moo", "zoo"]) 53 | 54 | expect_suggestions("abc", ["a1", "a12", "a2"], ["a1", "a2", "a12"]) 55 | 56 | def returns_options_sorted_first_by_lexical_distance_then_naturally(): 57 | expect_suggestions( 58 | "csutomer", 59 | ["store", "customer", "stomer", "some", "more"], 60 | ["customer", "stomer", "some", "store"], 61 | ) 62 | -------------------------------------------------------------------------------- /src/graphql/validation/rules/unique_enum_value_names.py: -------------------------------------------------------------------------------- 1 | from collections import defaultdict 2 | from typing import cast, Any, Dict 3 | 4 | from ...error import GraphQLError 5 | from ...language import NameNode, EnumTypeDefinitionNode, VisitorAction, SKIP 6 | from ...type import is_enum_type, GraphQLEnumType 7 | from . import SDLValidationContext, SDLValidationRule 8 | 9 | __all__ = ["UniqueEnumValueNamesRule"] 10 | 11 | 12 | class UniqueEnumValueNamesRule(SDLValidationRule): 13 | """Unique enum value names 14 | 15 | A GraphQL enum type is only valid if all its values are uniquely named. 16 | """ 17 | 18 | def __init__(self, context: SDLValidationContext): 19 | super().__init__(context) 20 | schema = context.schema 21 | self.existing_type_map = schema.type_map if schema else {} 22 | self.known_value_names: Dict[str, Dict[str, NameNode]] = defaultdict(dict) 23 | 24 | def check_value_uniqueness( 25 | self, node: EnumTypeDefinitionNode, *_args: Any 26 | ) -> VisitorAction: 27 | existing_type_map = self.existing_type_map 28 | type_name = node.name.value 29 | value_names = self.known_value_names[type_name] 30 | 31 | for value_def in node.values or []: 32 | value_name = value_def.name.value 33 | 34 | existing_type = existing_type_map.get(type_name) 35 | if ( 36 | is_enum_type(existing_type) 37 | and value_name in cast(GraphQLEnumType, existing_type).values 38 | ): 39 | self.report_error( 40 | GraphQLError( 41 | f"Enum value '{type_name}.{value_name}'" 42 | " already exists in the schema." 43 | " It cannot also be defined in this type extension.", 44 | value_def.name, 45 | ) 46 | ) 47 | elif value_name in value_names: 48 | self.report_error( 49 | GraphQLError( 50 | f"Enum value '{type_name}.{value_name}'" 51 | " can only be defined once.", 52 | [value_names[value_name], value_def.name], 53 | ) 54 | ) 55 | else: 56 | value_names[value_name] = value_def.name 57 | 58 | return SKIP 59 | 60 | enter_enum_type_definition = check_value_uniqueness 61 | enter_enum_type_extension = check_value_uniqueness 62 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "graphql-core" 3 | version = "3.2.0rc2" 4 | description = """ 5 | GraphQL-core is a Python port of GraphQL.js, 6 | the JavaScript reference implementation for GraphQL.""" 7 | license = "MIT" 8 | authors = [ 9 | "Christoph Zwerschke " 10 | ] 11 | readme = "README.md" 12 | homepage = "https://github.com/graphql-python/graphql-core" 13 | repository = "https://github.com/graphql-python/graphql-core" 14 | documentation = "https://graphql-core-3.readthedocs.io/" 15 | keywords = ["graphql"] 16 | classifiers = [ 17 | "Development Status :: 5 - Production/Stable", 18 | "Intended Audience :: Developers", 19 | "License :: OSI Approved :: MIT License", 20 | "Programming Language :: Python :: 3", 21 | "Programming Language :: Python :: 3.6", 22 | "Programming Language :: Python :: 3.7", 23 | "Programming Language :: Python :: 3.8", 24 | "Programming Language :: Python :: 3.9", 25 | "Programming Language :: Python :: 3.10" 26 | ] 27 | packages = [ 28 | { include = "graphql", from = "src" }, 29 | { include = "tests", format = "sdist" }, 30 | { include = "docs", format = "sdist" }, 31 | { include = '.bumpversion.cfg', format = "sdist" }, 32 | { include = '.coveragerc', format = "sdist" }, 33 | { include = '.editorconfig', format = "sdist" }, 34 | { include = '.flake8', format = "sdist" }, 35 | { include = '.mypy.ini', format = "sdist" }, 36 | { include = 'poetry.lock', format = "sdist" }, 37 | { include = 'tox.ini', format = "sdist" }, 38 | { include = 'setup.cfg', format = "sdist" }, 39 | { include = 'CODEOWNERS', format = "sdist" }, 40 | { include = 'MANIFEST.in', format = "sdist" } 41 | ] 42 | 43 | [tool.poetry.dependencies] 44 | python = "^3.6" 45 | 46 | [tool.poetry.dev-dependencies] 47 | pytest = "^6.2" 48 | pytest-asyncio = ">=0.16,<1" 49 | pytest-benchmark = "^3.4" 50 | pytest-cov = "^3.0" 51 | pytest-describe = "^2.0" 52 | pytest-timeout = "^1.4" 53 | typing-extensions = { version = "^3.10", python = "<3.8" } 54 | black = [ 55 | {version = "21.11b1", python = ">=3.6.2"}, 56 | {version = "20.8b1", python = "<3.6.2"} 57 | ] 58 | flake8 = "^4.0" 59 | mypy = "0.910" 60 | sphinx = "^4.3" 61 | sphinx_rtd_theme = ">=1,<2" 62 | check-manifest = ">=0.46,<1" 63 | bump2version = ">=1.0,<2" 64 | tox = "^3.24" 65 | 66 | [tool.black] 67 | target-version = ['py36', 'py37', 'py38', 'py39', 'py310'] 68 | 69 | [build-system] 70 | requires = ["poetry_core>=1,<2"] 71 | build-backend = "poetry.core.masonry.api" 72 | -------------------------------------------------------------------------------- /src/graphql/language/source.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from .location import SourceLocation 4 | 5 | __all__ = ["Source", "is_source"] 6 | 7 | 8 | class Source: 9 | """A representation of source input to GraphQL.""" 10 | 11 | # allow custom attributes and weak references (not used internally) 12 | __slots__ = "__weakref__", "__dict__", "body", "name", "location_offset" 13 | 14 | def __init__( 15 | self, 16 | body: str, 17 | name: str = "GraphQL request", 18 | location_offset: SourceLocation = SourceLocation(1, 1), 19 | ) -> None: 20 | """Initialize source input. 21 | 22 | The ``name`` and ``location_offset`` parameters are optional, but they are 23 | useful for clients who store GraphQL documents in source files. For example, 24 | if the GraphQL input starts at line 40 in a file named ``Foo.graphql``, it might 25 | be useful for ``name`` to be ``"Foo.graphql"`` and location to be ``(40, 0)``. 26 | 27 | The ``line`` and ``column`` attributes in ``location_offset`` are 1-indexed. 28 | """ 29 | self.body = body 30 | self.name = name 31 | if not isinstance(location_offset, SourceLocation): 32 | location_offset = SourceLocation._make(location_offset) 33 | if location_offset.line <= 0: 34 | raise ValueError( 35 | "line in location_offset is 1-indexed and must be positive." 36 | ) 37 | if location_offset.column <= 0: 38 | raise ValueError( 39 | "column in location_offset is 1-indexed and must be positive." 40 | ) 41 | self.location_offset = location_offset 42 | 43 | def get_location(self, position: int) -> SourceLocation: 44 | lines = self.body[:position].splitlines() 45 | if lines: 46 | line = len(lines) 47 | column = len(lines[-1]) + 1 48 | else: 49 | line = 1 50 | column = 1 51 | return SourceLocation(line, column) 52 | 53 | def __repr__(self) -> str: 54 | return f"<{self.__class__.__name__} name={self.name!r}>" 55 | 56 | def __eq__(self, other: Any) -> bool: 57 | return (isinstance(other, Source) and other.body == self.body) or ( 58 | isinstance(other, str) and other == self.body 59 | ) 60 | 61 | def __ne__(self, other: Any) -> bool: 62 | return not self == other 63 | 64 | 65 | def is_source(source: Any) -> bool: 66 | """Test if the given value is a Source object. 67 | 68 | For internal use only. 69 | """ 70 | return isinstance(source, Source) 71 | -------------------------------------------------------------------------------- /tests/language/test_print_string.py: -------------------------------------------------------------------------------- 1 | from graphql.language.print_string import print_string 2 | 3 | 4 | def describe_print_string(): 5 | def prints_a_simple_string(): 6 | assert print_string("hello world") == '"hello world"' 7 | 8 | def escapes_quotes(): 9 | assert print_string('"hello world"') == '"\\"hello world\\""' 10 | 11 | def escapes_backslashes(): 12 | assert print_string("escape: \\") == '"escape: \\\\"' 13 | 14 | def escapes_well_known_control_chars(): 15 | assert print_string("\b\f\n\r\t") == '"\\b\\f\\n\\r\\t"' 16 | 17 | def escapes_zero_byte(): 18 | assert print_string("\x00") == '"\\u0000"' 19 | 20 | def does_not_escape_space(): 21 | assert print_string(" ") == '" "' 22 | 23 | def does_not_escape_non_ascii_character(): 24 | assert print_string("\u21BB") == '"\u21BB"' 25 | 26 | def does_not_escape_supplementary_character(): 27 | assert print_string("\U0001f600") == '"\U0001f600"' 28 | 29 | def escapes_all_control_chars(): 30 | assert print_string( 31 | "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F" 32 | "\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F" 33 | "\x20\x21\x22\x23\x24\x25\x26\x27\x28\x29\x2A\x2B\x2C\x2D\x2E\x2F" 34 | "\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39\x3A\x3B\x3C\x3D\x3E\x3F" 35 | "\x40\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4A\x4B\x4C\x4D\x4E\x4F" 36 | "\x50\x51\x52\x53\x54\x55\x56\x57\x58\x59\x5A\x5B\x5C\x5D\x5E\x5F" 37 | "\x60\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6A\x6B\x6C\x6D\x6E\x6F" 38 | "\x70\x71\x72\x73\x74\x75\x76\x77\x78\x79\x7A\x7B\x7C\x7D\x7E\x7F" 39 | "\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8A\x8B\x8C\x8D\x8E\x8F" 40 | "\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9A\x9B\x9C\x9D\x9E\x9F" 41 | ) == ( 42 | '"\\u0000\\u0001\\u0002\\u0003\\u0004\\u0005\\u0006\\u0007' 43 | "\\b\\t\\n\\u000B\\f\\r\\u000E\\u000F" 44 | "\\u0010\\u0011\\u0012\\u0013\\u0014\\u0015\\u0016\\u0017" 45 | "\\u0018\\u0019\\u001A\\u001B\\u001C\\u001D\\u001E\\u001F" 46 | " !\\\"#$%&'()*+,-./0123456789:;<=>?" 47 | "@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\\\]^_" 48 | "`abcdefghijklmnopqrstuvwxyz{|}~\\u007F" 49 | "\\u0080\\u0081\\u0082\\u0083\\u0084\\u0085\\u0086\\u0087" 50 | "\\u0088\\u0089\\u008A\\u008B\\u008C\\u008D\\u008E\\u008F" 51 | "\\u0090\\u0091\\u0092\\u0093\\u0094\\u0095\\u0096\\u0097" 52 | '\\u0098\\u0099\\u009A\\u009B\\u009C\\u009D\\u009E\\u009F"' 53 | ) 54 | -------------------------------------------------------------------------------- /docs/usage/introspection.rst: -------------------------------------------------------------------------------- 1 | Using an Introspection Query 2 | ---------------------------- 3 | 4 | .. currentmodule:: graphql.utilities 5 | 6 | A third way of building a schema is using an introspection query on an existing server. 7 | This is what GraphiQL uses to get information about the schema on the remote server. 8 | You can create an introspection query using GraphQL-core 3 with the 9 | :func:`get_introspection_query` function:: 10 | 11 | from graphql import get_introspection_query 12 | 13 | query = get_introspection_query(descriptions=True) 14 | 15 | This will also yield the descriptions of the introspected schema fields. You can also 16 | create a query that omits the descriptions with:: 17 | 18 | query = get_introspection_query(descriptions=False) 19 | 20 | In practice you would run this query against a remote server, but we can also run it 21 | against the schema we have just built above:: 22 | 23 | from graphql import graphql_sync 24 | 25 | introspection_query_result = graphql_sync(schema, query) 26 | 27 | The ``data`` attribute of the introspection query result now gives us a dictionary, 28 | which constitutes a third way of describing a GraphQL schema:: 29 | 30 | {'__schema': { 31 | 'queryType': {'name': 'Query'}, 32 | 'mutationType': None, 'subscriptionType': None, 33 | 'types': [ 34 | {'kind': 'OBJECT', 'name': 'Query', 'description': None, 35 | 'fields': [{ 36 | 'name': 'hero', 'description': None, 37 | 'args': [{'name': 'episode', 'description': ... }], 38 | ... }, ... ], ... }, 39 | ... ], 40 | ... } 41 | } 42 | 43 | This result contains all the information that is available in the SDL description of the 44 | schema, i.e. it does not contain the resolve functions and information on the 45 | server-side values of the enum types. 46 | 47 | You can convert the introspection result into ``GraphQLSchema`` with GraphQL-core 3 by 48 | using the :func:`build_client_schema` function:: 49 | 50 | from graphql import build_client_schema 51 | 52 | client_schema = build_client_schema(introspection_query_result.data) 53 | 54 | 55 | It is also possible to convert the result to SDL with GraphQL-core 3 by using the 56 | :func:`print_schema` function:: 57 | 58 | from graphql import print_schema 59 | 60 | sdl = print_schema(client_schema) 61 | print(sdl) 62 | 63 | This prints the SDL representation of the schema that we started with. 64 | 65 | As you see, it is easy to convert between the three forms of representing a GraphQL 66 | schema in GraphQL-core 3 using the :mod:`graphql.utilities` module. 67 | -------------------------------------------------------------------------------- /tests/validation/test_executable_definitions.py: -------------------------------------------------------------------------------- 1 | from functools import partial 2 | 3 | from graphql.validation import ExecutableDefinitionsRule 4 | 5 | from .harness import assert_validation_errors 6 | 7 | assert_errors = partial(assert_validation_errors, ExecutableDefinitionsRule) 8 | 9 | assert_valid = partial(assert_errors, errors=[]) 10 | 11 | 12 | def describe_validate_executable_definitions(): 13 | def with_only_operation(): 14 | assert_valid( 15 | """ 16 | query Foo { 17 | dog { 18 | name 19 | } 20 | } 21 | """ 22 | ) 23 | 24 | def with_operation_and_fragment(): 25 | assert_valid( 26 | """ 27 | query Foo { 28 | dog { 29 | name 30 | ...Frag 31 | } 32 | } 33 | 34 | fragment Frag on Dog { 35 | name 36 | } 37 | """ 38 | ) 39 | 40 | def with_type_definition(): 41 | assert_errors( 42 | """ 43 | query Foo { 44 | dog { 45 | name 46 | } 47 | } 48 | 49 | type Cow { 50 | name: String 51 | } 52 | 53 | extend type Dog { 54 | color: String 55 | } 56 | """, 57 | [ 58 | { 59 | "message": "The 'Cow' definition is not executable.", 60 | "locations": [(8, 13)], 61 | }, 62 | { 63 | "message": "The 'Dog' definition is not executable.", 64 | "locations": [(12, 13)], 65 | }, 66 | ], 67 | ) 68 | 69 | def with_schema_definition(): 70 | assert_errors( 71 | """ 72 | schema { 73 | query: Query 74 | } 75 | 76 | type Query { 77 | test: String 78 | } 79 | 80 | extend schema @directive 81 | """, 82 | [ 83 | { 84 | "message": "The schema definition is not executable.", 85 | "locations": [(2, 13)], 86 | }, 87 | { 88 | "message": "The 'Query' definition is not executable.", 89 | "locations": [(6, 13)], 90 | }, 91 | { 92 | "message": "The schema definition is not executable.", 93 | "locations": [(10, 13)], 94 | }, 95 | ], 96 | ) 97 | -------------------------------------------------------------------------------- /src/graphql/validation/rules/unique_operation_types.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict, Optional, Union 2 | 3 | from ...error import GraphQLError 4 | from ...language import ( 5 | OperationTypeDefinitionNode, 6 | OperationType, 7 | SchemaDefinitionNode, 8 | SchemaExtensionNode, 9 | VisitorAction, 10 | SKIP, 11 | ) 12 | from ...type import GraphQLObjectType 13 | from . import SDLValidationContext, SDLValidationRule 14 | 15 | __all__ = ["UniqueOperationTypesRule"] 16 | 17 | 18 | class UniqueOperationTypesRule(SDLValidationRule): 19 | """Unique operation types 20 | 21 | A GraphQL document is only valid if it has only one type per operation. 22 | """ 23 | 24 | def __init__(self, context: SDLValidationContext): 25 | super().__init__(context) 26 | schema = context.schema 27 | self.defined_operation_types: Dict[ 28 | OperationType, OperationTypeDefinitionNode 29 | ] = {} 30 | self.existing_operation_types: Dict[ 31 | OperationType, Optional[GraphQLObjectType] 32 | ] = ( 33 | { 34 | OperationType.QUERY: schema.query_type, 35 | OperationType.MUTATION: schema.mutation_type, 36 | OperationType.SUBSCRIPTION: schema.subscription_type, 37 | } 38 | if schema 39 | else {} 40 | ) 41 | self.schema = schema 42 | 43 | def check_operation_types( 44 | self, node: Union[SchemaDefinitionNode, SchemaExtensionNode], *_args: Any 45 | ) -> VisitorAction: 46 | for operation_type in node.operation_types or []: 47 | operation = operation_type.operation 48 | already_defined_operation_type = self.defined_operation_types.get(operation) 49 | 50 | if self.existing_operation_types.get(operation): 51 | self.report_error( 52 | GraphQLError( 53 | f"Type for {operation.value} already defined in the schema." 54 | " It cannot be redefined.", 55 | operation_type, 56 | ) 57 | ) 58 | elif already_defined_operation_type: 59 | self.report_error( 60 | GraphQLError( 61 | f"There can be only one {operation.value} type in schema.", 62 | [already_defined_operation_type, operation_type], 63 | ) 64 | ) 65 | else: 66 | self.defined_operation_types[operation] = operation_type 67 | return SKIP 68 | 69 | enter_schema_definition = enter_schema_extension = check_operation_types 70 | -------------------------------------------------------------------------------- /tests/utils/test_dedent.py: -------------------------------------------------------------------------------- 1 | from . import dedent 2 | 3 | 4 | def describe_dedent(): 5 | def removes_indentation_in_typical_usage(): 6 | assert ( 7 | dedent( 8 | """ 9 | type Query { 10 | me: User 11 | } 12 | 13 | type User { 14 | id: ID 15 | name: String 16 | } 17 | """ 18 | ) 19 | == "type Query {\n me: User\n}\n\n" 20 | "type User {\n id: ID\n name: String\n}" 21 | ) 22 | 23 | def removes_only_the_first_level_of_indentation(): 24 | assert ( 25 | dedent( 26 | """ 27 | first 28 | second 29 | third 30 | fourth 31 | """ 32 | ) 33 | == "first\n second\n third\n fourth" 34 | ) 35 | 36 | def does_not_escape_special_characters(): 37 | assert ( 38 | dedent( 39 | """ 40 | type Root { 41 | field(arg: String = "wi\th de\fault"): String 42 | } 43 | """ 44 | ) 45 | == "type Root {\n" 46 | ' field(arg: String = "wi\th de\fault"): String\n}' 47 | ) 48 | 49 | def also_removes_indentation_using_tabs(): 50 | assert ( 51 | dedent( 52 | """ 53 | \t\t type Query { 54 | \t\t me: User 55 | \t\t } 56 | """ 57 | ) 58 | == "type Query {\n me: User\n}" 59 | ) 60 | 61 | def removes_leading_and_trailing_newlines(): 62 | assert ( 63 | dedent( 64 | """ 65 | 66 | 67 | type Query { 68 | me: User 69 | } 70 | 71 | 72 | """ 73 | ) 74 | == "type Query {\n me: User\n}" 75 | ) 76 | 77 | def removes_all_trailing_spaces_and_tabs(): 78 | assert ( 79 | dedent( 80 | """ 81 | type Query { 82 | me: User 83 | } 84 | \t\t \t """ 85 | ) 86 | == "type Query {\n me: User\n}" 87 | ) 88 | 89 | def works_on_text_without_leading_newline(): 90 | assert ( 91 | dedent( 92 | """ type Query { 93 | me: User 94 | } 95 | """ 96 | ) 97 | == "type Query {\n me: User\n}" 98 | ) 99 | -------------------------------------------------------------------------------- /src/graphql/execution/middleware.py: -------------------------------------------------------------------------------- 1 | from functools import partial, reduce 2 | from inspect import isfunction 3 | 4 | from typing import Callable, Iterator, Dict, List, Tuple, Any, Optional 5 | 6 | __all__ = ["MiddlewareManager"] 7 | 8 | GraphQLFieldResolver = Callable[..., Any] 9 | 10 | 11 | class MiddlewareManager: 12 | """Manager for the middleware chain. 13 | 14 | This class helps to wrap resolver functions with the provided middleware functions 15 | and/or objects. The functions take the next middleware function as first argument. 16 | If middleware is provided as an object, it must provide a method ``resolve`` that is 17 | used as the middleware function. 18 | 19 | Note that since resolvers return "AwaitableOrValue"s, all middleware functions 20 | must be aware of this and check whether values are awaitable before awaiting them. 21 | """ 22 | 23 | # allow custom attributes (not used internally) 24 | __slots__ = "__dict__", "middlewares", "_middleware_resolvers", "_cached_resolvers" 25 | 26 | _cached_resolvers: Dict[GraphQLFieldResolver, GraphQLFieldResolver] 27 | _middleware_resolvers: Optional[List[Callable]] 28 | 29 | def __init__(self, *middlewares: Any): 30 | self.middlewares = middlewares 31 | self._middleware_resolvers = ( 32 | list(get_middleware_resolvers(middlewares)) if middlewares else None 33 | ) 34 | self._cached_resolvers = {} 35 | 36 | def get_field_resolver( 37 | self, field_resolver: GraphQLFieldResolver 38 | ) -> GraphQLFieldResolver: 39 | """Wrap the provided resolver with the middleware. 40 | 41 | Returns a function that chains the middleware functions with the provided 42 | resolver function. 43 | """ 44 | if self._middleware_resolvers is None: 45 | return field_resolver 46 | if field_resolver not in self._cached_resolvers: 47 | self._cached_resolvers[field_resolver] = reduce( 48 | lambda chained_fns, next_fn: partial(next_fn, chained_fns), 49 | self._middleware_resolvers, 50 | field_resolver, 51 | ) 52 | return self._cached_resolvers[field_resolver] 53 | 54 | 55 | def get_middleware_resolvers(middlewares: Tuple[Any, ...]) -> Iterator[Callable]: 56 | """Get a list of resolver functions from a list of classes or functions.""" 57 | for middleware in middlewares: 58 | if isfunction(middleware): 59 | yield middleware 60 | else: # middleware provided as object with 'resolve' method 61 | resolver_func = getattr(middleware, "resolve", None) 62 | if resolver_func is not None: 63 | yield resolver_func 64 | -------------------------------------------------------------------------------- /src/graphql/validation/rules/possible_fragment_spreads.py: -------------------------------------------------------------------------------- 1 | from typing import cast, Any, Optional 2 | 3 | from ...error import GraphQLError 4 | from ...language import FragmentSpreadNode, InlineFragmentNode 5 | from ...type import GraphQLCompositeType, is_composite_type 6 | from ...utilities import do_types_overlap, type_from_ast 7 | from . import ValidationRule 8 | 9 | __all__ = ["PossibleFragmentSpreadsRule"] 10 | 11 | 12 | class PossibleFragmentSpreadsRule(ValidationRule): 13 | """Possible fragment spread 14 | 15 | A fragment spread is only valid if the type condition could ever possibly be true: 16 | if there is a non-empty intersection of the possible parent types, and possible 17 | types which pass the type condition. 18 | """ 19 | 20 | def enter_inline_fragment(self, node: InlineFragmentNode, *_args: Any) -> None: 21 | context = self.context 22 | frag_type = context.get_type() 23 | parent_type = context.get_parent_type() 24 | if ( 25 | is_composite_type(frag_type) 26 | and is_composite_type(parent_type) 27 | and not do_types_overlap( 28 | context.schema, 29 | cast(GraphQLCompositeType, frag_type), 30 | cast(GraphQLCompositeType, parent_type), 31 | ) 32 | ): 33 | context.report_error( 34 | GraphQLError( 35 | f"Fragment cannot be spread here as objects" 36 | f" of type '{parent_type}' can never be of type '{frag_type}'.", 37 | node, 38 | ) 39 | ) 40 | 41 | def enter_fragment_spread(self, node: FragmentSpreadNode, *_args: Any) -> None: 42 | context = self.context 43 | frag_name = node.name.value 44 | frag_type = self.get_fragment_type(frag_name) 45 | parent_type = context.get_parent_type() 46 | if ( 47 | frag_type 48 | and parent_type 49 | and not do_types_overlap(context.schema, frag_type, parent_type) 50 | ): 51 | context.report_error( 52 | GraphQLError( 53 | f"Fragment '{frag_name}' cannot be spread here as objects" 54 | f" of type '{parent_type}' can never be of type '{frag_type}'.", 55 | node, 56 | ) 57 | ) 58 | 59 | def get_fragment_type(self, name: str) -> Optional[GraphQLCompositeType]: 60 | context = self.context 61 | frag = context.get_fragment(name) 62 | if frag: 63 | type_ = type_from_ast(context.schema, frag.type_condition) 64 | if is_composite_type(type_): 65 | return cast(GraphQLCompositeType, type_) 66 | return None 67 | -------------------------------------------------------------------------------- /src/graphql/validation/rules/unique_field_definition_names.py: -------------------------------------------------------------------------------- 1 | from collections import defaultdict 2 | from typing import Any, Dict 3 | 4 | from ...error import GraphQLError 5 | from ...language import NameNode, ObjectTypeDefinitionNode, VisitorAction, SKIP 6 | from ...type import is_object_type, is_interface_type, is_input_object_type 7 | from . import SDLValidationContext, SDLValidationRule 8 | 9 | __all__ = ["UniqueFieldDefinitionNamesRule"] 10 | 11 | 12 | class UniqueFieldDefinitionNamesRule(SDLValidationRule): 13 | """Unique field definition names 14 | 15 | A GraphQL complex type is only valid if all its fields are uniquely named. 16 | """ 17 | 18 | def __init__(self, context: SDLValidationContext): 19 | super().__init__(context) 20 | schema = context.schema 21 | self.existing_type_map = schema.type_map if schema else {} 22 | self.known_field_names: Dict[str, Dict[str, NameNode]] = defaultdict(dict) 23 | 24 | def check_field_uniqueness( 25 | self, node: ObjectTypeDefinitionNode, *_args: Any 26 | ) -> VisitorAction: 27 | existing_type_map = self.existing_type_map 28 | type_name = node.name.value 29 | field_names = self.known_field_names[type_name] 30 | 31 | for field_def in node.fields or []: 32 | field_name = field_def.name.value 33 | 34 | if has_field(existing_type_map.get(type_name), field_name): 35 | self.report_error( 36 | GraphQLError( 37 | f"Field '{type_name}.{field_name}'" 38 | " already exists in the schema." 39 | " It cannot also be defined in this type extension.", 40 | field_def.name, 41 | ) 42 | ) 43 | elif field_name in field_names: 44 | self.report_error( 45 | GraphQLError( 46 | f"Field '{type_name}.{field_name}'" 47 | " can only be defined once.", 48 | [field_names[field_name], field_def.name], 49 | ) 50 | ) 51 | else: 52 | field_names[field_name] = field_def.name 53 | 54 | return SKIP 55 | 56 | enter_input_object_type_definition = check_field_uniqueness 57 | enter_input_object_type_extension = check_field_uniqueness 58 | enter_interface_type_definition = check_field_uniqueness 59 | enter_interface_type_extension = check_field_uniqueness 60 | enter_object_type_definition = check_field_uniqueness 61 | enter_object_type_extension = check_field_uniqueness 62 | 63 | 64 | def has_field(type_: Any, field_name: str) -> bool: 65 | if is_object_type(type_) or is_interface_type(type_) or is_input_object_type(type_): 66 | return field_name in type_.fields 67 | return False 68 | -------------------------------------------------------------------------------- /src/graphql/language/predicates.py: -------------------------------------------------------------------------------- 1 | from .ast import ( 2 | Node, 3 | DefinitionNode, 4 | ExecutableDefinitionNode, 5 | ListValueNode, 6 | ObjectValueNode, 7 | SchemaExtensionNode, 8 | SelectionNode, 9 | TypeDefinitionNode, 10 | TypeExtensionNode, 11 | TypeNode, 12 | TypeSystemDefinitionNode, 13 | ValueNode, 14 | VariableNode, 15 | ) 16 | 17 | __all__ = [ 18 | "is_definition_node", 19 | "is_executable_definition_node", 20 | "is_selection_node", 21 | "is_value_node", 22 | "is_const_value_node", 23 | "is_type_node", 24 | "is_type_system_definition_node", 25 | "is_type_definition_node", 26 | "is_type_system_extension_node", 27 | "is_type_extension_node", 28 | ] 29 | 30 | 31 | def is_definition_node(node: Node) -> bool: 32 | """Check whether the given node represents a definition.""" 33 | return isinstance(node, DefinitionNode) 34 | 35 | 36 | def is_executable_definition_node(node: Node) -> bool: 37 | """Check whether the given node represents an executable definition.""" 38 | return isinstance(node, ExecutableDefinitionNode) 39 | 40 | 41 | def is_selection_node(node: Node) -> bool: 42 | """Check whether the given node represents a selection.""" 43 | return isinstance(node, SelectionNode) 44 | 45 | 46 | def is_value_node(node: Node) -> bool: 47 | """Check whether the given node represents a value.""" 48 | return isinstance(node, ValueNode) 49 | 50 | 51 | def is_const_value_node(node: Node) -> bool: 52 | """Check whether the given node represents a constant value.""" 53 | return is_value_node(node) and ( 54 | any(is_const_value_node(value) for value in node.values) 55 | if isinstance(node, ListValueNode) 56 | else any(is_const_value_node(field.value) for field in node.fields) 57 | if isinstance(node, ObjectValueNode) 58 | else not isinstance(node, VariableNode) 59 | ) 60 | 61 | 62 | def is_type_node(node: Node) -> bool: 63 | """Check whether the given node represents a type.""" 64 | return isinstance(node, TypeNode) 65 | 66 | 67 | def is_type_system_definition_node(node: Node) -> bool: 68 | """Check whether the given node represents a type system definition.""" 69 | return isinstance(node, TypeSystemDefinitionNode) 70 | 71 | 72 | def is_type_definition_node(node: Node) -> bool: 73 | """Check whether the given node represents a type definition.""" 74 | return isinstance(node, TypeDefinitionNode) 75 | 76 | 77 | def is_type_system_extension_node(node: Node) -> bool: 78 | """Check whether the given node represents a type system extension.""" 79 | return isinstance(node, (SchemaExtensionNode, TypeExtensionNode)) 80 | 81 | 82 | def is_type_extension_node(node: Node) -> bool: 83 | """Check whether the given node represents a type extension.""" 84 | return isinstance(node, TypeExtensionNode) 85 | -------------------------------------------------------------------------------- /src/graphql/pyutils/simple_pub_sub.py: -------------------------------------------------------------------------------- 1 | from asyncio import Future, Queue, ensure_future, sleep 2 | from inspect import isawaitable 3 | from typing import Any, AsyncIterator, Callable, Optional, Set 4 | 5 | try: 6 | from asyncio import get_running_loop 7 | except ImportError: 8 | from asyncio import get_event_loop as get_running_loop # Python < 3.7 9 | 10 | 11 | __all__ = ["SimplePubSub", "SimplePubSubIterator"] 12 | 13 | 14 | class SimplePubSub: 15 | """A very simple publish-subscript system. 16 | 17 | Creates an AsyncIterator from an EventEmitter. 18 | 19 | Useful for mocking a PubSub system for tests. 20 | """ 21 | 22 | subscribers: Set[Callable] 23 | 24 | def __init__(self) -> None: 25 | self.subscribers = set() 26 | 27 | def emit(self, event: Any) -> bool: 28 | """Emit an event.""" 29 | for subscriber in self.subscribers: 30 | result = subscriber(event) 31 | if isawaitable(result): 32 | ensure_future(result) 33 | return bool(self.subscribers) 34 | 35 | def get_subscriber( 36 | self, transform: Optional[Callable] = None 37 | ) -> "SimplePubSubIterator": 38 | return SimplePubSubIterator(self, transform) 39 | 40 | 41 | class SimplePubSubIterator(AsyncIterator): 42 | def __init__(self, pubsub: SimplePubSub, transform: Optional[Callable]) -> None: 43 | self.pubsub = pubsub 44 | self.transform = transform 45 | self.pull_queue: Queue[Future] = Queue() 46 | self.push_queue: Queue[Any] = Queue() 47 | self.listening = True 48 | pubsub.subscribers.add(self.push_value) 49 | 50 | def __aiter__(self) -> "SimplePubSubIterator": 51 | return self 52 | 53 | async def __anext__(self) -> Any: 54 | if not self.listening: 55 | raise StopAsyncIteration 56 | await sleep(0) 57 | if not self.push_queue.empty(): 58 | return await self.push_queue.get() 59 | future = get_running_loop().create_future() 60 | await self.pull_queue.put(future) 61 | return future 62 | 63 | async def aclose(self) -> None: 64 | if self.listening: 65 | await self.empty_queue() 66 | 67 | async def empty_queue(self) -> None: 68 | self.listening = False 69 | self.pubsub.subscribers.remove(self.push_value) 70 | while not self.pull_queue.empty(): 71 | future = await self.pull_queue.get() 72 | future.cancel() 73 | while not self.push_queue.empty(): 74 | await self.push_queue.get() 75 | 76 | async def push_value(self, event: Any) -> None: 77 | value = event if self.transform is None else self.transform(event) 78 | if self.pull_queue.empty(): 79 | await self.push_queue.put(value) 80 | else: 81 | (await self.pull_queue.get()).set_result(value) 82 | -------------------------------------------------------------------------------- /docs/usage/sdl.rst: -------------------------------------------------------------------------------- 1 | Using the Schema Definition Language 2 | ------------------------------------ 3 | 4 | .. currentmodule:: graphql.type 5 | 6 | Above we defined the GraphQL schema as Python code, using the :class:`GraphQLSchema` 7 | class and other classes representing the various GraphQL types. 8 | 9 | GraphQL-core 3 also provides a language-agnostic way of defining a GraphQL schema 10 | using the GraphQL schema definition language (SDL) which is also part of the GraphQL 11 | specification. To do this, we simply feed the SDL as a string to the 12 | :func:`~graphql.utilities.build_schema` function in :mod:`graphql.utilities`:: 13 | 14 | from graphql import build_schema 15 | 16 | schema = build_schema(""" 17 | 18 | enum Episode { NEWHOPE, EMPIRE, JEDI } 19 | 20 | interface Character { 21 | id: String! 22 | name: String 23 | friends: [Character] 24 | appearsIn: [Episode] 25 | } 26 | 27 | type Human implements Character { 28 | id: String! 29 | name: String 30 | friends: [Character] 31 | appearsIn: [Episode] 32 | homePlanet: String 33 | } 34 | 35 | type Droid implements Character { 36 | id: String! 37 | name: String 38 | friends: [Character] 39 | appearsIn: [Episode] 40 | primaryFunction: String 41 | } 42 | 43 | type Query { 44 | hero(episode: Episode): Character 45 | human(id: String!): Human 46 | droid(id: String!): Droid 47 | } 48 | """) 49 | 50 | The result is a :class:`GraphQLSchema` object just like the one we defined above, except 51 | for the resolver functions which cannot be defined in the SDL. 52 | 53 | We would need to manually attach these functions to the schema, like so:: 54 | 55 | schema.query_type.fields['hero'].resolve = get_hero 56 | schema.get_type('Character').resolve_type = get_character_type 57 | 58 | Another problem is that the SDL does not define the server side values of the 59 | ``Episode`` enum type which are returned by the resolver functions and which are 60 | different from the names used for the episode. 61 | 62 | So we would also need to manually define these values, like so:: 63 | 64 | for name, value in schema.get_type('Episode').values.items(): 65 | value.value = EpisodeEnum[name].value 66 | 67 | This would allow us to query the schema built from SDL just like the manually assembled 68 | schema:: 69 | 70 | from graphql import graphql_sync 71 | 72 | result = graphql_sync(schema, """ 73 | { 74 | hero(episode: EMPIRE) { 75 | name 76 | appearsIn 77 | } 78 | } 79 | """) 80 | print(result) 81 | 82 | And we would get the expected result:: 83 | 84 | ExecutionResult( 85 | data={'hero': {'name': 'Luke Skywalker', 86 | 'appearsIn': ['NEWHOPE', 'EMPIRE', 'JEDI']}}, 87 | errors=None) 88 | -------------------------------------------------------------------------------- /docs/usage/parser.rst: -------------------------------------------------------------------------------- 1 | Parsing GraphQL Queries and Schema Notation 2 | ------------------------------------------- 3 | 4 | .. currentmodule:: graphql.language 5 | 6 | When executing GraphQL queries, the first step that happens under the hood is parsing 7 | the query. But GraphQL-core 3 also exposes the parser for direct usage via the 8 | :func:`parse` function. When you pass this function a GraphQL source code, it will be 9 | parsed and returned as a Document, i.e. an abstract syntax tree (AST) of :class:`Node` 10 | objects. The root node will be a :class:`DocumentNode`, with child nodes of different 11 | kinds corresponding to the GraphQL source. The nodes also carry information on the 12 | location in the source code that they correspond to. 13 | 14 | Here is an example:: 15 | 16 | from graphql import parse 17 | 18 | document = parse(""" 19 | type Query { 20 | me: User 21 | } 22 | 23 | type User { 24 | id: ID 25 | name: String 26 | } 27 | """) 28 | 29 | You can also leave out the information on the location in the source code when creating 30 | the AST document:: 31 | 32 | document = parse(..., no_location=True) 33 | 34 | This will give the same result as manually creating the AST document:: 35 | 36 | from graphql.language.ast import * 37 | 38 | document = DocumentNode(definitions=[ 39 | ObjectTypeDefinitionNode( 40 | name=NameNode(value='Query'), 41 | fields=[ 42 | FieldDefinitionNode( 43 | name=NameNode(value='me'), 44 | type=NamedTypeNode(name=NameNode(value='User')), 45 | arguments=[], directives=[]) 46 | ], directives=[], interfaces=[]), 47 | ObjectTypeDefinitionNode( 48 | name=NameNode(value='User'), 49 | fields=[ 50 | FieldDefinitionNode( 51 | name=NameNode(value='id'), 52 | type=NamedTypeNode( 53 | name=NameNode(value='ID')), 54 | arguments=[], directives=[]), 55 | FieldDefinitionNode( 56 | name=NameNode(value='name'), 57 | type=NamedTypeNode( 58 | name=NameNode(value='String')), 59 | arguments=[], directives=[]), 60 | ], directives=[], interfaces=[]), 61 | ]) 62 | 63 | 64 | When parsing with ``no_location=False`` (the default), the AST nodes will also have a 65 | ``loc`` attribute carrying the information on the source code location corresponding 66 | to the AST nodes. 67 | 68 | When there is a syntax error in the GraphQL source code, then the :func:`parse` function 69 | will raise a :exc:`~graphql.error.GraphQLSyntaxError`. 70 | 71 | The parser can not only be used to parse GraphQL queries, but also to parse the GraphQL 72 | schema definition language. This will result in another way of representing a GraphQL 73 | schema, as an AST document. 74 | -------------------------------------------------------------------------------- /src/graphql/language/print_location.py: -------------------------------------------------------------------------------- 1 | import re 2 | from typing import Optional, Tuple, cast 3 | 4 | from .ast import Location 5 | from .location import SourceLocation, get_location 6 | from .source import Source 7 | 8 | 9 | __all__ = ["print_location", "print_source_location"] 10 | 11 | 12 | def print_location(location: Location) -> str: 13 | """Render a helpful description of the location in the GraphQL Source document.""" 14 | return print_source_location( 15 | location.source, get_location(location.source, location.start) 16 | ) 17 | 18 | 19 | _re_newline = re.compile(r"\r\n|[\n\r]") 20 | 21 | 22 | def print_source_location(source: Source, source_location: SourceLocation) -> str: 23 | """Render a helpful description of the location in the GraphQL Source document.""" 24 | first_line_column_offset = source.location_offset.column - 1 25 | body = "".rjust(first_line_column_offset) + source.body 26 | 27 | line_index = source_location.line - 1 28 | line_offset = source.location_offset.line - 1 29 | line_num = source_location.line + line_offset 30 | 31 | column_offset = first_line_column_offset if source_location.line == 1 else 0 32 | column_num = source_location.column + column_offset 33 | location_str = f"{source.name}:{line_num}:{column_num}\n" 34 | 35 | lines = _re_newline.split(body) # works a bit different from splitlines() 36 | location_line = lines[line_index] 37 | 38 | # Special case for minified documents 39 | if len(location_line) > 120: 40 | sub_line_index, sub_line_column_num = divmod(column_num, 80) 41 | sub_lines = [ 42 | location_line[i : i + 80] for i in range(0, len(location_line), 80) 43 | ] 44 | 45 | return location_str + print_prefixed_lines( 46 | (f"{line_num} |", sub_lines[0]), 47 | *[("|", sub_line) for sub_line in sub_lines[1 : sub_line_index + 1]], 48 | ("|", "^".rjust(sub_line_column_num)), 49 | ( 50 | "|", 51 | sub_lines[sub_line_index + 1] 52 | if sub_line_index < len(sub_lines) - 1 53 | else None, 54 | ), 55 | ) 56 | 57 | return location_str + print_prefixed_lines( 58 | (f"{line_num - 1} |", lines[line_index - 1] if line_index > 0 else None), 59 | (f"{line_num} |", location_line), 60 | ("|", "^".rjust(column_num)), 61 | ( 62 | f"{line_num + 1} |", 63 | lines[line_index + 1] if line_index < len(lines) - 1 else None, 64 | ), 65 | ) 66 | 67 | 68 | def print_prefixed_lines(*lines: Tuple[str, Optional[str]]) -> str: 69 | """Print lines specified like this: ("prefix", "string")""" 70 | existing_lines = [ 71 | cast(Tuple[str, str], line) for line in lines if line[1] is not None 72 | ] 73 | pad_len = max(len(line[0]) for line in existing_lines) 74 | return "\n".join( 75 | prefix.rjust(pad_len) + (" " + line if line else "") 76 | for prefix, line in existing_lines 77 | ) 78 | -------------------------------------------------------------------------------- /tests/utilities/test_value_from_ast_untyped.py: -------------------------------------------------------------------------------- 1 | from math import nan 2 | from typing import Any, Dict, Optional 3 | 4 | from graphql.language import parse_value, FloatValueNode, IntValueNode 5 | from graphql.pyutils import Undefined 6 | from graphql.utilities import value_from_ast_untyped 7 | 8 | 9 | def describe_value_from_ast_untyped(): 10 | def _compare_value(value: Any, expected: Any): 11 | if expected is None: 12 | assert value is None 13 | elif expected is Undefined: 14 | assert value is Undefined 15 | elif expected is nan: 16 | assert value is nan 17 | else: 18 | assert value == expected 19 | 20 | def _expect_value_from(value_text: str, expected: Any): 21 | ast = parse_value(value_text) 22 | value = value_from_ast_untyped(ast) 23 | _compare_value(value, expected) 24 | 25 | def _expect_value_from_vars( 26 | value_text: str, variables: Optional[Dict[str, Any]], expected: Any 27 | ): 28 | ast = parse_value(value_text) 29 | value = value_from_ast_untyped(ast, variables) 30 | _compare_value(value, expected) 31 | 32 | def parses_simple_values(): 33 | _expect_value_from("null", None) 34 | _expect_value_from("true", True) 35 | _expect_value_from("false", False) 36 | _expect_value_from("123", 123) 37 | _expect_value_from("123.456", 123.456) 38 | _expect_value_from('"abc123"', "abc123") 39 | 40 | def parses_lists_of_values(): 41 | _expect_value_from("[true, false]", [True, False]) 42 | _expect_value_from("[true, 123.45]", [True, 123.45]) 43 | _expect_value_from("[true, null]", [True, None]) 44 | _expect_value_from('[true, ["foo", 1.2]]', [True, ["foo", 1.2]]) 45 | 46 | def parses_input_objects(): 47 | _expect_value_from("{ int: 123, bool: false }", {"int": 123, "bool": False}) 48 | _expect_value_from('{ foo: [ { bar: "baz"} ] }', {"foo": [{"bar": "baz"}]}) 49 | 50 | def parses_enum_values_as_plain_strings(): 51 | _expect_value_from("TEST_ENUM_VALUE", "TEST_ENUM_VALUE") 52 | _expect_value_from("[TEST_ENUM_VALUE]", ["TEST_ENUM_VALUE"]) 53 | 54 | def parses_variables(): 55 | _expect_value_from_vars("$testVariable", {"testVariable": "foo"}, "foo") 56 | _expect_value_from_vars("[$testVariable]", {"testVariable": "foo"}, ["foo"]) 57 | _expect_value_from_vars( 58 | "{a:[$testVariable]}", {"testVariable": "foo"}, {"a": ["foo"]} 59 | ) 60 | _expect_value_from_vars("$testVariable", {"testVariable": None}, None) 61 | _expect_value_from_vars("$testVariable", {"testVariable": nan}, nan) 62 | _expect_value_from_vars("$testVariable", {}, Undefined) 63 | _expect_value_from_vars("$testVariable", None, Undefined) 64 | 65 | def parse_invalid_int_as_nan(): 66 | assert value_from_ast_untyped(IntValueNode(value="invalid")) is nan 67 | 68 | def parse_invalid_float_as_nan(): 69 | assert value_from_ast_untyped(FloatValueNode(value="invalid")) is nan 70 | -------------------------------------------------------------------------------- /src/graphql/validation/rules/known_type_names.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Collection, List, Union, cast 2 | 3 | from ...error import GraphQLError 4 | from ...language import ( 5 | is_type_definition_node, 6 | is_type_system_definition_node, 7 | is_type_system_extension_node, 8 | Node, 9 | NamedTypeNode, 10 | TypeDefinitionNode, 11 | ) 12 | from ...type import introspection_types, specified_scalar_types 13 | from ...pyutils import did_you_mean, suggestion_list 14 | from . import ASTValidationRule, ValidationContext, SDLValidationContext 15 | 16 | __all__ = ["KnownTypeNamesRule"] 17 | 18 | 19 | class KnownTypeNamesRule(ASTValidationRule): 20 | """Known type names 21 | 22 | A GraphQL document is only valid if referenced types (specifically variable 23 | definitions and fragment conditions) are defined by the type schema. 24 | """ 25 | 26 | def __init__(self, context: Union[ValidationContext, SDLValidationContext]): 27 | super().__init__(context) 28 | schema = context.schema 29 | self.existing_types_map = schema.type_map if schema else {} 30 | 31 | defined_types = [] 32 | for def_ in context.document.definitions: 33 | if is_type_definition_node(def_): 34 | def_ = cast(TypeDefinitionNode, def_) 35 | defined_types.append(def_.name.value) 36 | self.defined_types = set(defined_types) 37 | 38 | self.type_names = list(self.existing_types_map) + defined_types 39 | 40 | def enter_named_type( 41 | self, 42 | node: NamedTypeNode, 43 | _key: Any, 44 | parent: Node, 45 | _path: Any, 46 | ancestors: List[Node], 47 | ) -> None: 48 | type_name = node.name.value 49 | if ( 50 | type_name not in self.existing_types_map 51 | and type_name not in self.defined_types 52 | ): 53 | try: 54 | definition_node = ancestors[2] 55 | except IndexError: 56 | definition_node = parent 57 | is_sdl = is_sdl_node(definition_node) 58 | if is_sdl and type_name in standard_type_names: 59 | return 60 | 61 | suggested_types = suggestion_list( 62 | type_name, 63 | list(standard_type_names) + self.type_names 64 | if is_sdl 65 | else self.type_names, 66 | ) 67 | self.report_error( 68 | GraphQLError( 69 | f"Unknown type '{type_name}'." + did_you_mean(suggested_types), 70 | node, 71 | ) 72 | ) 73 | 74 | 75 | standard_type_names = set(specified_scalar_types).union(introspection_types) 76 | 77 | 78 | def is_sdl_node(value: Union[Node, Collection[Node], None]) -> bool: 79 | return ( 80 | value is not None 81 | and not isinstance(value, list) 82 | and ( 83 | is_type_system_definition_node(cast(Node, value)) 84 | or is_type_system_extension_node(cast(Node, value)) 85 | ) 86 | ) 87 | -------------------------------------------------------------------------------- /docs/modules/utilities.rst: -------------------------------------------------------------------------------- 1 | Utilities 2 | ========= 3 | 4 | .. currentmodule:: graphql.utilities 5 | 6 | .. automodule:: graphql.utilities 7 | :no-members: 8 | :no-inherited-members: 9 | 10 | The GraphQL query recommended for a full schema introspection: 11 | 12 | .. autofunction:: get_introspection_query 13 | 14 | Get the target Operation from a Document: 15 | 16 | .. autofunction:: get_operation_ast 17 | 18 | Get the Type for the target Operation AST: 19 | 20 | .. autofunction:: get_operation_root_type 21 | 22 | Convert a GraphQLSchema to an IntrospectionQuery: 23 | 24 | .. autofunction:: introspection_from_schema 25 | 26 | Build a GraphQLSchema from an introspection result: 27 | 28 | .. autofunction:: build_client_schema 29 | 30 | Build a GraphQLSchema from GraphQL Schema language: 31 | 32 | .. autofunction:: build_ast_schema 33 | .. autofunction:: build_schema 34 | 35 | Extend an existing GraphQLSchema from a parsed GraphQL Schema language AST: 36 | 37 | .. autofunction:: extend_schema 38 | 39 | Sort a GraphQLSchema: 40 | 41 | .. autofunction:: lexicographic_sort_schema 42 | 43 | Print a GraphQLSchema to GraphQL Schema language: 44 | 45 | .. autofunction:: print_introspection_schema 46 | .. autofunction:: print_schema 47 | .. autofunction:: print_type 48 | .. autofunction:: print_value 49 | 50 | Create a GraphQLType from a GraphQL language AST: 51 | 52 | .. autofunction:: type_from_ast 53 | 54 | Create a Python value from a GraphQL language AST with a type: 55 | 56 | .. autofunction:: value_from_ast 57 | 58 | Create a Python value from a GraphQL language AST without a type: 59 | 60 | .. autofunction:: value_from_ast_untyped 61 | 62 | Create a GraphQL language AST from a Python value: 63 | 64 | .. autofunction:: ast_from_value 65 | 66 | A helper to use within recursive-descent visitors which need to be aware of the GraphQL 67 | type system: 68 | 69 | .. autoclass:: TypeInfo 70 | .. autoclass:: TypeInfoVisitor 71 | 72 | Coerce a Python value to a GraphQL type, or produce errors: 73 | 74 | .. autofunction:: coerce_input_value 75 | 76 | Concatenate multiple ASTs together: 77 | 78 | .. autofunction:: concat_ast 79 | 80 | Separate an AST into an AST per Operation: 81 | 82 | .. autofunction:: separate_operations 83 | 84 | Strip characters that are not significant to the validity or execution 85 | of a GraphQL document: 86 | 87 | .. autofunction:: strip_ignored_characters 88 | 89 | Comparators for types: 90 | 91 | .. autofunction:: is_equal_type 92 | .. autofunction:: is_type_sub_type_of 93 | .. autofunction:: do_types_overlap 94 | 95 | Assert that a string is a valid GraphQL name: 96 | 97 | .. autofunction:: assert_valid_name 98 | .. autofunction:: is_valid_name_error 99 | 100 | Compare two GraphQLSchemas and detect breaking changes: 101 | 102 | .. autofunction:: find_breaking_changes 103 | .. autofunction:: find_dangerous_changes 104 | 105 | .. autoclass:: BreakingChange 106 | .. autoclass:: BreakingChangeType 107 | .. autoclass:: DangerousChange 108 | .. autoclass:: DangerousChangeType 109 | -------------------------------------------------------------------------------- /src/graphql/validation/rules/no_fragment_cycles.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict, List, Set 2 | 3 | from ...error import GraphQLError 4 | from ...language import FragmentDefinitionNode, FragmentSpreadNode, VisitorAction, SKIP 5 | from . import ASTValidationContext, ASTValidationRule 6 | 7 | __all__ = ["NoFragmentCyclesRule"] 8 | 9 | 10 | class NoFragmentCyclesRule(ASTValidationRule): 11 | """No fragment cycles""" 12 | 13 | def __init__(self, context: ASTValidationContext): 14 | super().__init__(context) 15 | # Tracks already visited fragments to maintain O(N) and to ensure that 16 | # cycles are not redundantly reported. 17 | self.visited_frags: Set[str] = set() 18 | # List of AST nodes used to produce meaningful errors 19 | self.spread_path: List[FragmentSpreadNode] = [] 20 | # Position in the spread path 21 | self.spread_path_index_by_name: Dict[str, int] = {} 22 | 23 | @staticmethod 24 | def enter_operation_definition(*_args: Any) -> VisitorAction: 25 | return SKIP 26 | 27 | def enter_fragment_definition( 28 | self, node: FragmentDefinitionNode, *_args: Any 29 | ) -> VisitorAction: 30 | self.detect_cycle_recursive(node) 31 | return SKIP 32 | 33 | def detect_cycle_recursive(self, fragment: FragmentDefinitionNode) -> None: 34 | # This does a straight-forward DFS to find cycles. 35 | # It does not terminate when a cycle was found but continues to explore 36 | # the graph to find all possible cycles. 37 | if fragment.name.value in self.visited_frags: 38 | return 39 | 40 | fragment_name = fragment.name.value 41 | visited_frags = self.visited_frags 42 | visited_frags.add(fragment_name) 43 | 44 | spread_nodes = self.context.get_fragment_spreads(fragment.selection_set) 45 | if not spread_nodes: 46 | return 47 | 48 | spread_path = self.spread_path 49 | spread_path_index = self.spread_path_index_by_name 50 | spread_path_index[fragment_name] = len(spread_path) 51 | get_fragment = self.context.get_fragment 52 | 53 | for spread_node in spread_nodes: 54 | spread_name = spread_node.name.value 55 | cycle_index = spread_path_index.get(spread_name) 56 | 57 | spread_path.append(spread_node) 58 | if cycle_index is None: 59 | spread_fragment = get_fragment(spread_name) 60 | if spread_fragment: 61 | self.detect_cycle_recursive(spread_fragment) 62 | else: 63 | cycle_path = spread_path[cycle_index:] 64 | via_path = ", ".join("'" + s.name.value + "'" for s in cycle_path[:-1]) 65 | self.report_error( 66 | GraphQLError( 67 | f"Cannot spread fragment '{spread_name}' within itself" 68 | + (f" via {via_path}." if via_path else "."), 69 | cycle_path, 70 | ) 71 | ) 72 | spread_path.pop() 73 | 74 | del spread_path_index[fragment_name] 75 | -------------------------------------------------------------------------------- /tests/error/test_print_location.py: -------------------------------------------------------------------------------- 1 | from graphql.language import print_source_location, Source, SourceLocation 2 | 3 | from ..utils import dedent 4 | 5 | 6 | def describe_print_location(): 7 | def prints_minified_documents(): 8 | minified_source = Source( 9 | "query SomeMinifiedQueryWithErrorInside(" 10 | "$foo:String!=FIRST_ERROR_HERE$bar:String)" 11 | "{someField(foo:$foo bar:$bar baz:SECOND_ERROR_HERE)" 12 | "{fieldA fieldB{fieldC fieldD...on THIRD_ERROR_HERE}}}" 13 | ) 14 | 15 | first_location = print_source_location( 16 | minified_source, 17 | SourceLocation(1, minified_source.body.index("FIRST_ERROR_HERE") + 1), 18 | ) 19 | assert first_location == dedent( 20 | """ 21 | GraphQL request:1:53 22 | 1 | query SomeMinifiedQueryWithErrorInside($foo:String!=FIRST_ERROR_HERE$bar:String) 23 | | ^ 24 | | {someField(foo:$foo bar:$bar baz:SECOND_ERROR_HERE){fieldA fieldB{fieldC fieldD. 25 | """ # noqa: E501 26 | ) 27 | 28 | second_location = print_source_location( 29 | minified_source, 30 | SourceLocation(1, minified_source.body.index("SECOND_ERROR_HERE") + 1), 31 | ) 32 | assert second_location == dedent( 33 | """ 34 | GraphQL request:1:114 35 | 1 | query SomeMinifiedQueryWithErrorInside($foo:String!=FIRST_ERROR_HERE$bar:String) 36 | | {someField(foo:$foo bar:$bar baz:SECOND_ERROR_HERE){fieldA fieldB{fieldC fieldD. 37 | | ^ 38 | | ..on THIRD_ERROR_HERE}}} 39 | """ # noqa: E501 40 | ) 41 | 42 | third_location = print_source_location( 43 | minified_source, 44 | SourceLocation(1, minified_source.body.index("THIRD_ERROR_HERE") + 1), 45 | ) 46 | assert third_location == dedent( 47 | """ 48 | GraphQL request:1:166 49 | 1 | query SomeMinifiedQueryWithErrorInside($foo:String!=FIRST_ERROR_HERE$bar:String) 50 | | {someField(foo:$foo bar:$bar baz:SECOND_ERROR_HERE){fieldA fieldB{fieldC fieldD. 51 | | ..on THIRD_ERROR_HERE}}} 52 | | ^ 53 | """ # noqa: E501 54 | ) 55 | 56 | def prints_single_digit_line_number_with_no_padding(): 57 | result = print_source_location( 58 | Source("*", "Test", SourceLocation(9, 1)), SourceLocation(1, 1) 59 | ) 60 | 61 | assert result == dedent( 62 | """ 63 | Test:9:1 64 | 9 | * 65 | | ^ 66 | """ 67 | ) 68 | 69 | def prints_line_numbers_with_correct_padding(): 70 | result = print_source_location( 71 | Source("*\n", "Test", SourceLocation(9, 1)), SourceLocation(1, 1) 72 | ) 73 | 74 | assert result == dedent( 75 | """ 76 | Test:9:1 77 | 9 | * 78 | | ^ 79 | 10 | 80 | """ 81 | ) 82 | -------------------------------------------------------------------------------- /tests/validation/test_unique_input_field_names.py: -------------------------------------------------------------------------------- 1 | from functools import partial 2 | 3 | from graphql.validation import UniqueInputFieldNamesRule 4 | 5 | from .harness import assert_validation_errors 6 | 7 | assert_errors = partial(assert_validation_errors, UniqueInputFieldNamesRule) 8 | 9 | assert_valid = partial(assert_errors, errors=[]) 10 | 11 | 12 | def describe_validate_unique_input_field_names(): 13 | def input_object_with_fields(): 14 | assert_valid( 15 | """ 16 | { 17 | field(arg: { f: true }) 18 | } 19 | """ 20 | ) 21 | 22 | def same_input_object_within_two_args(): 23 | assert_valid( 24 | """ 25 | { 26 | field(arg1: { f: true }, arg2: { f: true }) 27 | } 28 | """ 29 | ) 30 | 31 | def multiple_input_object_fields(): 32 | assert_valid( 33 | """ 34 | { 35 | field(arg: { f1: "value", f2: "value", f3: "value" }) 36 | } 37 | """ 38 | ) 39 | 40 | def allows_for_nested_input_objects_with_similar_fields(): 41 | assert_valid( 42 | """ 43 | { 44 | field(arg: { 45 | deep: { 46 | deep: { 47 | id: 1 48 | } 49 | id: 1 50 | } 51 | id: 1 52 | }) 53 | } 54 | """ 55 | ) 56 | 57 | def duplicate_input_object_fields(): 58 | assert_errors( 59 | """ 60 | { 61 | field(arg: { f1: "value", f1: "value" }) 62 | } 63 | """, 64 | [ 65 | { 66 | "message": "There can be only one input field named 'f1'.", 67 | "locations": [(3, 28), (3, 41)], 68 | }, 69 | ], 70 | ) 71 | 72 | def many_duplicate_input_object_fields(): 73 | assert_errors( 74 | """ 75 | { 76 | field(arg: { f1: "value", f1: "value", f1: "value" }) 77 | } 78 | """, 79 | [ 80 | { 81 | "message": "There can be only one input field named 'f1'.", 82 | "locations": [(3, 28), (3, 41)], 83 | }, 84 | { 85 | "message": "There can be only one input field named 'f1'.", 86 | "locations": [(3, 28), (3, 54)], 87 | }, 88 | ], 89 | ) 90 | 91 | def nested_duplicate_input_object_fields(): 92 | assert_errors( 93 | """ 94 | { 95 | field(arg: { f1: {f2: "value", f2: "value" }}) 96 | } 97 | """, 98 | [ 99 | { 100 | "message": "There can be only one input field named 'f2'.", 101 | "locations": [(3, 33), (3, 46)], 102 | }, 103 | ], 104 | ) 105 | -------------------------------------------------------------------------------- /tests/validation/test_unique_fragment_names.py: -------------------------------------------------------------------------------- 1 | from functools import partial 2 | 3 | from graphql.validation import UniqueFragmentNamesRule 4 | 5 | from .harness import assert_validation_errors 6 | 7 | assert_errors = partial(assert_validation_errors, UniqueFragmentNamesRule) 8 | 9 | assert_valid = partial(assert_errors, errors=[]) 10 | 11 | 12 | def describe_validate_unique_fragment_names(): 13 | def no_fragments(): 14 | assert_valid( 15 | """ 16 | { 17 | field 18 | } 19 | """ 20 | ) 21 | 22 | def one_fragment(): 23 | assert_valid( 24 | """ 25 | { 26 | ...fragA 27 | } 28 | fragment fragA on Type { 29 | field 30 | } 31 | """ 32 | ) 33 | 34 | def many_fragments(): 35 | assert_valid( 36 | """ 37 | { 38 | ...fragA 39 | ...fragB 40 | ...fragC 41 | } 42 | fragment fragA on Type { 43 | fieldA 44 | } 45 | fragment fragB on Type { 46 | fieldB 47 | } 48 | fragment fragC on Type { 49 | fieldC 50 | } 51 | """ 52 | ) 53 | 54 | def inline_fragments_are_always_unique(): 55 | assert_valid( 56 | """ 57 | { 58 | ...on Type { 59 | fieldA 60 | } 61 | ...on Type { 62 | fieldB 63 | } 64 | } 65 | """ 66 | ) 67 | 68 | def fragment_and_operation_named_the_same(): 69 | assert_valid( 70 | """ 71 | query Foo { 72 | ...Foo 73 | } 74 | fragment Foo on Type { 75 | field 76 | } 77 | """ 78 | ) 79 | 80 | def fragments_named_the_same(): 81 | assert_errors( 82 | """ 83 | { 84 | ...fragA 85 | } 86 | fragment fragA on Type { 87 | fieldA 88 | } 89 | fragment fragA on Type { 90 | fieldB 91 | } 92 | """, 93 | [ 94 | { 95 | "message": "There can be only one fragment named 'fragA'.", 96 | "locations": [(5, 22), (8, 22)], 97 | }, 98 | ], 99 | ) 100 | 101 | def fragments_named_the_same_without_being_referenced(): 102 | assert_errors( 103 | """ 104 | fragment fragA on Type { 105 | fieldA 106 | } 107 | fragment fragA on Type { 108 | fieldB 109 | } 110 | """, 111 | [ 112 | { 113 | "message": "There can be only one fragment named 'fragA'.", 114 | "locations": [(2, 22), (5, 22)], 115 | }, 116 | ], 117 | ) 118 | -------------------------------------------------------------------------------- /tests/validation/test_lone_anonymous_operation.py: -------------------------------------------------------------------------------- 1 | from functools import partial 2 | 3 | from graphql.validation import LoneAnonymousOperationRule 4 | 5 | from .harness import assert_validation_errors 6 | 7 | assert_errors = partial(assert_validation_errors, LoneAnonymousOperationRule) 8 | 9 | assert_valid = partial(assert_errors, errors=[]) 10 | 11 | 12 | def describe_validate_anonymous_operation_must_be_alone(): 13 | def no_operations(): 14 | assert_valid( 15 | """ 16 | fragment fragA on Type { 17 | field 18 | } 19 | """ 20 | ) 21 | 22 | def one_anon_operation(): 23 | assert_valid( 24 | """ 25 | { 26 | field 27 | } 28 | """ 29 | ) 30 | 31 | def multiple_named_operation(): 32 | assert_valid( 33 | """ 34 | query Foo { 35 | field 36 | } 37 | 38 | query Bar { 39 | field 40 | } 41 | """ 42 | ) 43 | 44 | def anon_operation_with_fragment(): 45 | assert_valid( 46 | """ 47 | { 48 | ...Foo 49 | } 50 | fragment Foo on Type { 51 | field 52 | } 53 | """ 54 | ) 55 | 56 | def multiple_anon_operations(): 57 | assert_errors( 58 | """ 59 | { 60 | fieldA 61 | } 62 | { 63 | fieldB 64 | } 65 | """, 66 | [ 67 | { 68 | "message": "This anonymous operation" 69 | " must be the only defined operation.", 70 | "locations": [(2, 13)], 71 | }, 72 | { 73 | "message": "This anonymous operation" 74 | " must be the only defined operation.", 75 | "locations": [(5, 13)], 76 | }, 77 | ], 78 | ) 79 | 80 | def anon_operation_with_a_mutation(): 81 | assert_errors( 82 | """ 83 | { 84 | fieldA 85 | } 86 | mutation Foo { 87 | fieldB 88 | } 89 | """, 90 | [ 91 | { 92 | "message": "This anonymous operation" 93 | " must be the only defined operation.", 94 | "locations": [(2, 13)], 95 | }, 96 | ], 97 | ) 98 | 99 | def anon_operation_with_a_subscription(): 100 | assert_errors( 101 | """ 102 | { 103 | fieldA 104 | } 105 | subscription Foo { 106 | fieldB 107 | } 108 | """, 109 | [ 110 | { 111 | "message": "This anonymous operation" 112 | " must be the only defined operation.", 113 | "locations": [(2, 13)], 114 | }, 115 | ], 116 | ) 117 | -------------------------------------------------------------------------------- /tests/pyutils/test_frozen_dict.py: -------------------------------------------------------------------------------- 1 | from copy import copy, deepcopy 2 | 3 | from pytest import raises 4 | 5 | from graphql.pyutils import FrozenError, FrozenDict 6 | 7 | 8 | def describe_frozen_list(): 9 | def can_read(): 10 | fd = FrozenDict({1: 2, 3: 4}) 11 | assert fd == {1: 2, 3: 4} 12 | assert list(i for i in fd) == [1, 3] 13 | assert fd.copy() == fd 14 | assert 3 in fd 15 | assert 2 not in fd 16 | assert fd[1] == 2 17 | with raises(KeyError): 18 | # noinspection PyStatementEffect 19 | fd[2] 20 | assert len(fd) == 2 21 | assert fd.get(1) == 2 22 | assert fd.get(2, 5) == 5 23 | assert list(fd.items()) == [(1, 2), (3, 4)] 24 | assert list(fd.keys()) == [1, 3] 25 | assert list(fd.values()) == [2, 4] 26 | 27 | def cannot_write(): 28 | fd = FrozenDict({1: 2, 3: 4}) 29 | with raises(FrozenError): 30 | fd[1] = 2 31 | with raises(FrozenError): 32 | fd[4] = 5 33 | with raises(FrozenError): 34 | del fd[1] 35 | with raises(FrozenError): 36 | del fd[3] 37 | with raises(FrozenError): 38 | fd.clear() 39 | with raises(FrozenError): 40 | fd.pop(1) 41 | with raises(FrozenError): 42 | fd.pop(4, 5) 43 | with raises(FrozenError): 44 | fd.popitem() 45 | with raises(FrozenError): 46 | fd.setdefault(1, 2) 47 | with raises(FrozenError): 48 | fd.setdefault(4, 5) 49 | with raises(FrozenError): 50 | fd.update({1: 2}) 51 | with raises(FrozenError): 52 | fd.update({4: 5}) 53 | with raises(FrozenError): 54 | fd += {4: 5} 55 | assert fd == {1: 2, 3: 4} 56 | 57 | def can_hash(): 58 | fd1 = FrozenDict({1: 2, 3: 4}) 59 | fd2 = FrozenDict({1: 2, 3: 4}) 60 | assert fd2 == fd1 61 | assert fd2 is not fd1 62 | assert hash(fd2) is not hash(fd1) 63 | fd3 = FrozenDict({1: 2, 3: 5}) 64 | assert fd3 != fd1 65 | assert hash(fd3) != hash(fd1) 66 | 67 | def can_copy(): 68 | fd1 = FrozenDict({1: 2, 3: 4}) 69 | fd2 = fd1.copy() 70 | assert isinstance(fd2, FrozenDict) 71 | assert fd2 == fd1 72 | assert hash(fd2) == hash(fd1) 73 | assert fd2 is not fd1 74 | fd3 = copy(fd1) 75 | assert isinstance(fd3, FrozenDict) 76 | assert fd3 == fd1 77 | assert hash(fd3) == hash(fd1) 78 | assert fd3 is not fd1 79 | 80 | def can_deep_copy(): 81 | fd11 = FrozenDict({1: 2, 3: 4}) 82 | fd12 = FrozenDict({2: 1, 4: 3}) 83 | fd1 = FrozenDict({1: fd11, 2: fd12}) 84 | assert fd1[1] is fd11 85 | assert fd1[2] is fd12 86 | fd2 = deepcopy(fd1) 87 | assert isinstance(fd2, FrozenDict) 88 | assert fd2 == fd1 89 | assert hash(fd2) == hash(fd1) 90 | fd21 = fd2[1] 91 | fd22 = fd2[2] 92 | assert isinstance(fd21, FrozenDict) 93 | assert isinstance(fd22, FrozenDict) 94 | assert fd21 == fd11 95 | assert fd21 is not fd11 96 | assert fd22 == fd12 97 | assert fd22 is not fd12 98 | -------------------------------------------------------------------------------- /tests/validation/test_unique_directive_names.py: -------------------------------------------------------------------------------- 1 | from functools import partial 2 | 3 | from graphql.utilities import build_schema 4 | from graphql.validation.rules.unique_directive_names import UniqueDirectiveNamesRule 5 | 6 | from .harness import assert_sdl_validation_errors 7 | 8 | assert_errors = partial(assert_sdl_validation_errors, UniqueDirectiveNamesRule) 9 | 10 | assert_valid = partial(assert_errors, errors=[]) 11 | 12 | 13 | def describe_validate_unique_directive_names(): 14 | def no_directive(): 15 | assert_valid( 16 | """ 17 | type Foo 18 | """ 19 | ) 20 | 21 | def one_directive(): 22 | assert_valid( 23 | """ 24 | directive @foo on SCHEMA 25 | """ 26 | ) 27 | 28 | def many_directives(): 29 | assert_valid( 30 | """ 31 | directive @foo on SCHEMA 32 | directive @bar on SCHEMA 33 | directive @baz on SCHEMA 34 | """ 35 | ) 36 | 37 | def directive_and_non_directive_definitions_named_the_same(): 38 | assert_valid( 39 | """ 40 | query foo { __typename } 41 | fragment foo on foo { __typename } 42 | type foo 43 | 44 | directive @foo on SCHEMA 45 | """ 46 | ) 47 | 48 | def directives_named_the_same(): 49 | assert_errors( 50 | """ 51 | directive @foo on SCHEMA 52 | 53 | directive @foo on SCHEMA 54 | """, 55 | [ 56 | { 57 | "message": "There can be only one directive named '@foo'.", 58 | "locations": [(2, 24), (4, 24)], 59 | } 60 | ], 61 | ) 62 | 63 | def adding_new_directive_to_existing_schema(): 64 | schema = build_schema("directive @foo on SCHEMA") 65 | 66 | assert_valid("directive @bar on SCHEMA", schema=schema) 67 | 68 | def adding_new_directive_with_standard_name_to_existing_schema(): 69 | schema = build_schema("type foo") 70 | 71 | assert_errors( 72 | "directive @skip on SCHEMA", 73 | [ 74 | { 75 | "message": "Directive '@skip' already exists in the schema." 76 | " It cannot be redefined.", 77 | "locations": [(1, 12)], 78 | } 79 | ], 80 | schema, 81 | ) 82 | 83 | def adding_new_directive_to_existing_schema_with_same_named_type(): 84 | schema = build_schema("type foo") 85 | 86 | assert_valid("directive @foo on SCHEMA", schema=schema) 87 | 88 | def adding_conflicting_directives_to_existing_schema(): 89 | schema = build_schema("directive @foo on SCHEMA") 90 | 91 | assert_errors( 92 | "directive @foo on SCHEMA", 93 | [ 94 | { 95 | "message": "Directive '@foo' already exists in the schema." 96 | " It cannot be redefined.", 97 | "locations": [(1, 12)], 98 | } 99 | ], 100 | schema, 101 | ) 102 | -------------------------------------------------------------------------------- /tests/utilities/test_get_introspection_query.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from typing import Pattern 4 | 5 | from graphql.utilities import get_introspection_query 6 | 7 | 8 | class ExcpectIntrospectionQuery: 9 | def __init__(self, **options): 10 | self.query = get_introspection_query(**options) 11 | 12 | def to_match(self, name: str, times: int = 1) -> None: 13 | pattern = self.to_reg_exp(name) 14 | assert len(pattern.findall(self.query)) == times 15 | 16 | def to_not_match(self, name: str) -> None: 17 | pattern = self.to_reg_exp(name) 18 | assert not pattern.search(self.query) 19 | 20 | @staticmethod 21 | def to_reg_exp(name: str) -> Pattern: 22 | return re.compile(fr"\b{name}\b") 23 | 24 | 25 | def describe_get_introspection_query(): 26 | def skips_all_description_fields(): 27 | ExcpectIntrospectionQuery().to_match("description", 5) 28 | ExcpectIntrospectionQuery(descriptions=True).to_match("description", 5) 29 | ExcpectIntrospectionQuery(descriptions=False).to_not_match("description") 30 | 31 | def includes_is_repeatable_field_on_directives(): 32 | ExcpectIntrospectionQuery().to_not_match("isRepeatable") 33 | ExcpectIntrospectionQuery(directive_is_repeatable=True).to_match("isRepeatable") 34 | ExcpectIntrospectionQuery(directive_is_repeatable=False).to_not_match( 35 | "isRepeatable" 36 | ) 37 | 38 | def includes_description_field_on_schema(): 39 | ExcpectIntrospectionQuery().to_match("description", 5) 40 | ExcpectIntrospectionQuery(schema_description=False).to_match("description", 5) 41 | ExcpectIntrospectionQuery(schema_description=True).to_match("description", 6) 42 | ExcpectIntrospectionQuery( 43 | descriptions=False, schema_description=True 44 | ).to_not_match("description") 45 | 46 | def includes_specified_by_url_field(): 47 | ExcpectIntrospectionQuery().to_not_match("specifiedByURL") 48 | ExcpectIntrospectionQuery(specified_by_url=True).to_match("specifiedByURL") 49 | ExcpectIntrospectionQuery(specified_by_url=False).to_not_match("specifiedByURL") 50 | 51 | def includes_is_deprecated_field_on_input_values(): 52 | ExcpectIntrospectionQuery().to_match("isDeprecated", 2) 53 | ExcpectIntrospectionQuery(input_value_deprecation=True).to_match( 54 | "isDeprecated", 3 55 | ) 56 | ExcpectIntrospectionQuery(input_value_deprecation=False).to_match( 57 | "isDeprecated", 2 58 | ) 59 | 60 | def includes_deprecation_reason_field_on_input_values(): 61 | ExcpectIntrospectionQuery().to_match("deprecationReason", 2) 62 | ExcpectIntrospectionQuery(input_value_deprecation=True).to_match( 63 | "deprecationReason", 3 64 | ) 65 | ExcpectIntrospectionQuery(input_value_deprecation=False).to_match( 66 | "deprecationReason", 2 67 | ) 68 | 69 | def includes_deprecated_input_field_and_args(): 70 | ExcpectIntrospectionQuery().to_match("includeDeprecated: true", 2) 71 | ExcpectIntrospectionQuery(input_value_deprecation=True).to_match( 72 | "includeDeprecated: true", 5 73 | ) 74 | ExcpectIntrospectionQuery(input_value_deprecation=False).to_match( 75 | "includeDeprecated: true", 2 76 | ) 77 | --------------------------------------------------------------------------------