diff --git a/.bumpversion.cfg b/.bumpversion.cfg index b7e42d6d..28887672 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 3.2.2 +current_version = 3.2.11 commit = False tag = False diff --git a/.coveragerc b/.coveragerc index 8bdacb69..537f4767 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,6 +1,8 @@ [run] branch = True -source = src +source = + src + tests omit = */conftest.py */test_*_fuzz.py @@ -15,6 +17,7 @@ exclude_lines = pragma: no cover except ImportError: \# Python < + sys\.version_info < raise NotImplementedError raise TypeError\(f?"Unexpected assert False, @@ -23,4 +26,5 @@ exclude_lines = if TYPE_CHECKING: ^\s+\.\.\.$ ^\s+pass$ + \: \.\.\.$ ignore_errors = True diff --git a/.flake8 b/.flake8 index ccded588..0be28606 100644 --- a/.flake8 +++ b/.flake8 @@ -1,4 +1,4 @@ [flake8] -ignore = E203,W503 +ignore = E203,E704,W503 exclude = .git,.mypy_cache,.pytest_cache,.tox,.venv,__pycache__,build,dist,docs max-line-length = 88 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 626f94c2..18efc2c7 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -7,12 +7,12 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v6 - - name: Set up Python 3.9 - uses: actions/setup-python@v2 + - name: Set up Python 3.14 + uses: actions/setup-python@v6 with: - python-version: 3.9 + python-version: 3.14 - name: Install dependencies run: | diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index fc166745..45db9910 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -9,21 +9,21 @@ jobs: build: runs-on: ubuntu-latest + permissions: + id-token: write # mandatory for trusted publishing + steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v6 - - name: Set up Python 3.9 - uses: actions/setup-python@v2 + - name: Set up Python 3.14 + uses: actions/setup-python@v6 with: - python-version: 3.9 + python-version: 3.14 - name: Build wheel and source tarball run: | - pip install wheel + pip install setuptools wheel python setup.py sdist bdist_wheel - name: Publish a Python distribution to PyPI uses: pypa/gh-action-pypi-publish@release/v1 - with: - user: __token__ - password: ${{ secrets.PYPI_TOKEN }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 09278c14..b5456414 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -3,25 +3,48 @@ name: Tests on: [push, pull_request] jobs: - build: + tests: runs-on: ubuntu-latest strategy: matrix: - python: ['3.6', '3.7', '3.8', '3.9', '3.10', 'pypy3'] + python-version: ['3.9', '3.10', '3.11', '3.12', '3.13', '3.14', 'pypy3.9', 'pypy3.10', 'pypy3.11'] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v6 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip - pip install tox tox-gh-actions + pip install "tox>=4.30,<5" "tox-gh-actions>=3.5,<4" - name: Run unit tests with tox run: tox + + tests-old: + runs-on: ubuntu-22.04 + + strategy: + matrix: + python-version: ['3.7', '3.8'] + + steps: + - uses: actions/checkout@v6 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v6 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install "tox>=4.8,<5" "tox-gh-actions>=3.5,<4" + + - name: Run unit tests with tox + run: tox \ No newline at end of file diff --git a/.gitignore b/.gitignore index 6b51313b..a15cbec4 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ .tox/ .venv*/ .vs/ +.vscode/ build/ dist/ diff --git a/.mypy.ini b/.mypy.ini index 4b539ae9..85bb837b 100644 --- a/.mypy.ini +++ b/.mypy.ini @@ -1,5 +1,5 @@ [mypy] -python_version = 3.9 +python_version = 3.10 check_untyped_defs = True no_implicit_optional = True strict_optional = True diff --git a/.readthedocs.yaml b/.readthedocs.yaml index bb8e1846..93c341fa 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -5,9 +5,9 @@ version: 2 build: - os: ubuntu-20.04 + os: ubuntu-24.04 tools: - python: "3.9" + python: "3.12" sphinx: configuration: docs/conf.py diff --git a/README.md b/README.md index 910d0144..524c3ac4 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # GraphQL-core 3 -GraphQL-core 3 is a Python 3.6+ port of [GraphQL.js](https://github.com/graphql/graphql-js), +GraphQL-core 3 is a Python 3.7+ port of [GraphQL.js](https://github.com/graphql/graphql-js), the JavaScript reference implementation for [GraphQL](https://graphql.org/), a query language for APIs created by Facebook. @@ -10,13 +10,18 @@ a query language for APIs created by Facebook. ![Lint Status](https://github.com/graphql-python/graphql-core/actions/workflows/lint.yml/badge.svg) [![Code Style](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/ambv/black) -The current version 3.2.2 of GraphQL-core is up-to-date with GraphQL.js version 16.4.0. +The current version 3.2.11 of GraphQL-core is up-to-date with GraphQL.js version 16.14.1. -An extensive test suite with over 2300 unit tests and 100% coverage comprises a +An extensive test suite with over 2500 unit tests and 100% coverage comprises a replication of the complete test suite of GraphQL.js, making sure this port is reliable and compatible with GraphQL.js. -Note that for various reasons, GraphQL-core does not use SemVer like GraphQL.js. Increases in the major version of GraphQL.js are reflected in the minor version of GraphQL-core instead. This means there can be breaking changes in the API when the minor version changes, and only patch releases are fully backward compatible. Therefore, we recommend something like `=~ 3.2.0` as version specifier when including GraphQL-core as a dependency. +Note that for various reasons, GraphQL-core does not use SemVer like GraphQL.js. +Changes in the major version of GraphQL.js are reflected in the minor version of +GraphQL-core instead. This means there can be breaking changes in the API +when the minor version changes, and only patch releases are fully backward compatible. +Therefore, we recommend using something like `~= 3.2.0` as the version specifier +when including GraphQL-core as a dependency. ## Documentation @@ -48,8 +53,8 @@ GraphQL-core 3 can be installed from PyPI using the built-in pip command: python -m pip install graphql-core -You can also use [poetry](https://github.com/python-poetry/poetry) for installation in a -virtual environment: +You can also use [poetry](https://github.com/python-poetry/poetry) for installation +in a virtual environment: poetry install @@ -191,11 +196,15 @@ Design goals for the GraphQL-core 3 library were: Some restrictions (mostly in line with the design goals): -* requires Python 3.6 or newer +* requires Python 3.7 or newer * does not support some already deprecated methods and options of GraphQL.js * supports asynchronous operations only via async.io (does not support the additional executors in GraphQL-core) +Note that meanwhile we are using the amazing [ruff](https://docs.astral.sh/ruff/) tool +to both format and check the code of GraphQL-core 3, +in addition to using [mypy](https://mypy-lang.org/) as type checker. + ## Integration with other libraries and roadmap @@ -205,14 +214,12 @@ Some restrictions (mostly in line with the design goals): also been created by Syrus Akbary, who meanwhile has handed over the maintenance and future development to members of the GraphQL-Python community. - The current version 2 of Graphene is using Graphql-core 2 as core library for much of - the heavy lifting. Note that Graphene 2 is not compatible with GraphQL-core 3. - The new version 3 of Graphene will use GraphQL-core 3 instead of GraphQL-core 2. + Graphene 3 is now using Graphql-core 3 as core library for much of the heavy lifting. * [Ariadne](https://github.com/mirumee/ariadne) is a Python library for implementing GraphQL servers using schema-first approach created by Mirumee Software. - Ariadne is already using GraphQL-core 3 as its GraphQL implementation. + Ariadne is also using GraphQL-core 3 as its GraphQL implementation. * [Strawberry](https://github.com/strawberry-graphql/strawberry), created by Patrick Arminio, is a new GraphQL library for Python 3, inspired by dataclasses, @@ -228,6 +235,7 @@ Changes are tracked as ## Credits and history The GraphQL-core 3 library + * has been created and is maintained by Christoph Zwerschke * uses ideas and code from GraphQL-core 2, a prior work by Syrus Akbary * is a Python port of GraphQL.js which has been developed by Lee Byron and others diff --git a/docs/conf.py b/docs/conf.py index d5eca20e..b695eae2 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -30,29 +30,29 @@ # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ - 'sphinx.ext.autodoc', + "sphinx.ext.autodoc", ] # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # # source_suffix = ['.rst', '.md'] -source_suffix = '.rst' +source_suffix = ".rst" # The encoding of source files. # # source_encoding = 'utf-8-sig' # The master toctree document. -master_doc = 'index' +master_doc = "index" # General information about the project. -project = 'GraphQL-core 3' -copyright = '2022, Christoph Zwerschke' -author = 'Christoph Zwerschke' +project = "GraphQL-core 3" +copyright = "2026, Christoph Zwerschke" +author = "Christoph Zwerschke" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -61,14 +61,14 @@ # The short X.Y version. # version = '3.2' # The full version, including alpha/beta/rc tags. -version = release = '3.2.2' +version = release = "3.2.11" # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. -language = None +language = "en" # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: @@ -82,23 +82,23 @@ # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This patterns also effect to html_static_path and html_extra_path -exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] # AutoDoc configuration autoclass_content = "class" autodoc_default_options = { - 'members': True, - 'inherited-members': True, - 'undoc-members': True, - 'show-inheritance': True + "members": True, + "inherited-members": True, + "undoc-members": True, + "show-inheritance": True, } autosummary_generate = True autodoc_type_aliases = { - 'AwaitableOrValue': 'graphql.pyutils.AwaitableOrValue', - 'FormattedSourceLocation': 'graphql.language.FormattedSourceLocation', - 'Middleware': 'graphql.execution.Middleware', - 'TypeMap': 'graphql.schema.TypeMap' + "AwaitableOrValue": "graphql.pyutils.AwaitableOrValue", + "FormattedSourceLocation": "graphql.language.FormattedSourceLocation", + "Middleware": "graphql.execution.Middleware", + "TypeMap": "graphql.schema.TypeMap", } # GraphQL-core top level modules with submodules that can be omitted. @@ -106,39 +106,52 @@ # qualified form, but the documentation has the shorter form. # We need to give autodoc a little help in this cases. graphql_modules = { - 'error': ['graphql_error'], - 'execution': ['execute', 'middleware'], - 'language': ['ast', 'directive_locations', 'location', - 'source', 'token_kind', 'visitor'], - 'pyutils': ['simple_pub_sub', 'frozen_list', 'path'], - 'type': ['definition', 'directives', 'schema'], - 'utilities': ['find_breaking_changes', 'type_info'], - 'validation': ['rules', 'validation_context']} + "error": ["graphql_error"], + "execution": ["execute", "middleware"], + "language": [ + "ast", + "directive_locations", + "location", + "source", + "token_kind", + "visitor", + ], + "pyutils": ["simple_pub_sub", "frozen_list", "path"], + "type": ["definition", "directives", "schema"], + "utilities": ["find_breaking_changes", "type_info"], + "validation": ["rules", "validation_context"], +} # GraphQL-core classes that autodoc sometimes cannot find # (e.g. where specified as string in type hints). # We need to give autodoc a little help in this cases, too: graphql_classes = { - 'GraphQLAbstractType': 'type', - 'GraphQLObjectType': 'type', - 'GraphQLOutputType': 'type', - 'GraphQLTypeResolver': 'type', - 'Node': 'language', - 'Source': 'language', - 'SourceLocation': 'language' + "GraphQLAbstractType": "type", + "GraphQLObjectType": "type", + "GraphQLOutputType": "type", + "GraphQLTypeResolver": "type", + "Node": "language", + "Source": "language", + "SourceLocation": "language", } # ignore the following undocumented or internal references: -ignore_references = set(''' +ignore_references = set( + """ GNT GT KT T VT +_asyncio.Future +asyncio.queues.Queue enum.Enum traceback types.TracebackType EnterLeaveVisitor FormattedSourceLocation GraphQLAbstractType +GraphQLErrorExtensions +GraphQLFormattedErrorExtensions GraphQLOutputType asyncio.events.AbstractEventLoop +graphql.execution.execute.CollectedErrors graphql.execution.map_async_iterator.MapAsyncIterator graphql.language.lexer.EscapeSequence graphql.language.visitor.EnterLeaveVisitor @@ -146,77 +159,79 @@ graphql.validation.validation_context.VariableUsage graphql.validation.rules.known_argument_names.KnownArgumentNamesOnDirectivesRule graphql.validation.rules.provided_required_arguments.ProvidedRequiredArgumentsOnDirectivesRule -'''.split()) +""".split() +) ignore_references.update(__builtins__.keys()) def on_missing_reference(app, env, node, contnode): """Fix or skip any missing references.""" - if node.get('refdomain') != 'py': + if node.get("refdomain") != "py": return None - target = node.get('reftarget') + target = node.get("reftarget") if not target: return None - if target in ignore_references or target.endswith('Kwargs'): + if target in ignore_references or target.endswith("Kwargs"): return contnode - typ = node.get('reftype') - name = target.rsplit('.', 1)[-1] - if name in ('GT', 'GNT', 'KT', 'T', 'VT'): + typ = node.get("reftype") + name = target.rsplit(".", 1)[-1] + if name in ("GT", "GNT", "KT", "T", "VT"): return contnode - if typ == 'obj': - if target.startswith('typing.'): - if name in ('Any', 'Optional', 'Union'): + if typ == "obj": + if target.startswith("typing."): + if name in ("Any", "Optional", "Union"): return contnode - if typ != 'class': + if typ != "class": return None - if '.' in target: # maybe too specific - base_module, target = target.split('.', 1) - if base_module == 'graphql': - if '.' not in target: + if "." in target: # maybe too specific + base_module, target = target.split(".", 1) + if base_module == "graphql": + if "." not in target: return None - base_module, target = target.split('.', 1) - if '.' not in target: + base_module, target = target.split(".", 1) + if "." not in target: return None sub_modules = graphql_modules.get(base_module) if not sub_modules: return - sub_module = target.split('.', 1)[0] + sub_module = target.split(".", 1)[0] if sub_module not in sub_modules: return None - target = 'graphql.' + base_module + '.' + target.rsplit('.', 1)[-1] + target = "graphql." + base_module + "." + target.rsplit(".", 1)[-1] else: # maybe not specific enough base_module = graphql_classes.get(target) if not base_module: return None - target = 'graphql.' + base_module + '.' + target + target = "graphql." + base_module + "." + target # replace target - if contnode.__class__.__name__ == 'Text': + if contnode.__class__.__name__ == "Text": contnode = contnode.__class__(target) - elif contnode.__class__.__name__ == 'literal': + elif contnode.__class__.__name__ == "literal": if len(contnode.children) != 1: return None textnode = contnode.children[0] contnode.children[0] = textnode.__class__(target) else: return None - node['reftarget'] = target - fromdoc = node.get('refdoc') + node["reftarget"] = target + fromdoc = node.get("refdoc") if not fromdoc: - doc_module = node.get('py:module') + doc_module = node.get("py:module") if doc_module: - if doc_module.startswith('graphql.'): - doc_module = doc_module.split('.', 1)[-1] - if doc_module not in graphql_modules and doc_module != 'graphql': + if doc_module.startswith("graphql."): + doc_module = doc_module.split(".", 1)[-1] + if doc_module not in graphql_modules and doc_module != "graphql": doc_module = None - fromdoc = 'modules/' + (doc_module or base_module) + fromdoc = "modules/" + (doc_module or base_module) # try resolving again with replaced target - return env.domains['py'].resolve_xref( - env, fromdoc, app.builder, typ, target, node, contnode) + return env.domains["py"].resolve_xref( + env, fromdoc, app.builder, typ, target, node, contnode + ) def on_skip_member(_app, what, name, _obj, skip, _options): - if what == 'class' and name == "__init__": + if what == "class" and name == "__init__": # we could set "special-members" to "__init__", # but this gives an error when documenting modules return False @@ -224,7 +239,7 @@ def on_skip_member(_app, what, name, _obj, skip, _options): def setup(app): - app.connect('missing-reference', on_missing_reference) + app.connect("missing-reference", on_missing_reference) app.connect("autodoc-skip-member", on_skip_member) @@ -252,7 +267,7 @@ def setup(app): # show_authors = False # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = "sphinx" # A list of ignored prefixes for module index sorting. # modindex_common_prefix = [] @@ -268,15 +283,13 @@ def setup(app): # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # -html_theme = 'sphinx_rtd_theme' +html_theme = "sphinx_rtd_theme" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. # -html_theme_options = { - 'navigation_depth': 5 -} +html_theme_options = {"navigation_depth": 5} # Add any paths that contain custom themes here, relative to this directory. # html_theme_path = [] @@ -384,34 +397,36 @@ def setup(app): # html_search_scorer = 'scorer.js' # Output file base name for HTML help builder. -htmlhelp_basename = 'GraphQL-core-3-doc' +htmlhelp_basename = "GraphQL-core-3-doc" # -- Options for LaTeX output --------------------------------------------- latex_elements = { - # The paper size ('letterpaper' or 'a4paper'). - # - # 'papersize': 'letterpaper', - - # The font size ('10pt', '11pt' or '12pt'). - # - # 'pointsize': '10pt', - - # Additional stuff for the LaTeX preamble. - # - # 'preamble': '', - - # Latex figure (float) alignment - # - # 'figure_align': 'htbp', + # The paper size ('letterpaper' or 'a4paper'). + # + # 'papersize': 'letterpaper', + # The font size ('10pt', '11pt' or '12pt'). + # + # 'pointsize': '10pt', + # Additional stuff for the LaTeX preamble. + # + # 'preamble': '', + # Latex figure (float) alignment + # + # 'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - (master_doc, 'GraphQL-core-3.tex', 'GraphQL-core 3 Documentation', - 'Christoph Zwerschke', 'manual'), + ( + master_doc, + "GraphQL-core-3.tex", + "GraphQL-core 3 Documentation", + "Christoph Zwerschke", + "manual", + ), ] # The name of an image file (relative to this directory) to place at the top of @@ -445,10 +460,7 @@ def setup(app): # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). -man_pages = [ - (master_doc, 'graphql-core', 'GraphQL-core 3 Documentation', - [author], 1) -] +man_pages = [(master_doc, "graphql-core", "GraphQL-core 3 Documentation", [author], 1)] # If true, show URL addresses after external links. # @@ -461,9 +473,15 @@ def setup(app): # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - (master_doc, 'GraphQL-core', 'GraphQL-core 3 Documentation', - author, 'GraphQL-core 3', 'One line description of project.', - 'Miscellaneous'), + ( + master_doc, + "GraphQL-core", + "GraphQL-core 3 Documentation", + author, + "GraphQL-core 3", + "One line description of project.", + "Miscellaneous", + ), ] # Documents to append as an appendix to all manuals. diff --git a/docs/modules/language.rst b/docs/modules/language.rst index b447787e..35a28873 100644 --- a/docs/modules/language.rst +++ b/docs/modules/language.rst @@ -25,6 +25,7 @@ Each kind of AST node has its own class: .. autoclass:: ConstValueNode .. autoclass:: DefinitionNode .. autoclass:: DirectiveDefinitionNode +.. autoclass:: DirectiveExtensionNode .. autoclass:: DirectiveNode .. autoclass:: DocumentNode .. autoclass:: EnumTypeDefinitionNode @@ -75,6 +76,15 @@ Each kind of AST node has its own class: .. autoclass:: VariableDefinitionNode .. autoclass:: VariableNode +Schema coordinates are represented using the following node classes: + +.. autoclass:: SchemaCoordinateNode +.. autoclass:: TypeCoordinateNode +.. autoclass:: MemberCoordinateNode +.. autoclass:: ArgumentCoordinateNode +.. autoclass:: DirectiveCoordinateNode +.. autoclass:: DirectiveArgumentCoordinateNode + Directive locations are specified using the following enumeration: .. autoclass:: DirectiveLocation @@ -91,6 +101,7 @@ You can also check the type of nodes with the following predicates: .. autofunction:: is_type_definition_node .. autofunction:: is_type_system_extension_node .. autofunction:: is_type_extension_node +.. autofunction:: is_schema_coordinate_node Lexer ----- @@ -116,6 +127,7 @@ Parser .. autofunction:: parse_type .. autofunction:: parse_value .. autofunction:: parse_const_value +.. autofunction:: parse_schema_coordinate Printer ------- diff --git a/docs/modules/utilities.rst b/docs/modules/utilities.rst index 21571404..e7e283f5 100644 --- a/docs/modules/utilities.rst +++ b/docs/modules/utilities.rst @@ -112,3 +112,17 @@ Compare two GraphQLSchemas and detect breaking changes: .. autoclass:: BreakingChangeType .. autoclass:: DangerousChange .. autoclass:: DangerousChangeType + +Resolve a schema coordinate to the schema element it refers to: + +.. autofunction:: resolve_schema_coordinate +.. autofunction:: resolve_ast_schema_coordinate + +.. autoclass:: ResolvedNamedType +.. autoclass:: ResolvedField +.. autoclass:: ResolvedInputField +.. autoclass:: ResolvedEnumValue +.. autoclass:: ResolvedFieldArgument +.. autoclass:: ResolvedDirective +.. autoclass:: ResolvedDirectiveArgument +.. autoclass:: ResolvedSchemaElement diff --git a/docs/modules/validation.rst b/docs/modules/validation.rst index 4c088d93..bb61b38a 100644 --- a/docs/modules/validation.rst +++ b/docs/modules/validation.rst @@ -137,6 +137,10 @@ Rules .. autoclass:: VariablesInAllowedPositionRule +**No spec section: "Maximum introspection depth"** + +.. autoclass:: MaxIntrospectionDepthRule + **SDL-specific validation rules** .. autoclass:: LoneSchemaDefinitionRule diff --git a/docs/requirements.txt b/docs/requirements.txt index 0ad38822..c1ad14fb 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,2 +1,2 @@ -sphinx>=4.3,<5 -sphinx_rtd_theme>=1,<2 +sphinx>=9.1,<10 +sphinx_rtd_theme>=3.1,<4 diff --git a/docs/usage/introspection.rst b/docs/usage/introspection.rst index 1faa946c..325380a3 100644 --- a/docs/usage/introspection.rst +++ b/docs/usage/introspection.rst @@ -28,7 +28,7 @@ The ``data`` attribute of the introspection query result now gives us a dictiona which constitutes a third way of describing a GraphQL schema:: {'__schema': { - 'queryType': {'name': 'Query'}, + 'queryType': {'name': 'Query', 'kind': 'OBJECT'}, 'mutationType': None, 'subscriptionType': None, 'types': [ {'kind': 'OBJECT', 'name': 'Query', 'description': None, diff --git a/poetry.lock b/poetry.lock index 7c0e0394..75a66793 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,1499 +1,19 @@ -[[package]] -name = "alabaster" -version = "0.7.12" -description = "A configurable sidebar-enabled Sphinx theme" -category = "dev" -optional = false -python-versions = "*" - -[[package]] -name = "appdirs" -version = "1.4.4" -description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." -category = "dev" -optional = false -python-versions = "*" - -[[package]] -name = "atomicwrites" -version = "1.4.1" -description = "Atomic file writes." -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" - -[[package]] -name = "attrs" -version = "22.1.0" -description = "Classes Without Boilerplate" -category = "dev" -optional = false -python-versions = ">=3.5" - -[package.extras] -dev = ["cloudpickle", "coverage[toml] (>=5.0.2)", "furo", "hypothesis", "mypy (>=0.900,!=0.940)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "sphinx", "sphinx-notfound-page", "zope.interface"] -docs = ["furo", "sphinx", "sphinx-notfound-page", "zope.interface"] -tests = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "zope.interface"] -tests_no_zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins"] - -[[package]] -name = "Babel" -version = "2.10.3" -description = "Internationalization utilities" -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -pytz = ">=2015.7" - -[[package]] -name = "black" -version = "20.8b1" -description = "The uncompromising code formatter." -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -appdirs = "*" -click = ">=7.1.2" -dataclasses = {version = ">=0.6", markers = "python_version < \"3.7\""} -mypy-extensions = ">=0.4.3" -pathspec = ">=0.6,<1" -regex = ">=2020.1.8" -toml = ">=0.10.1" -typed-ast = ">=1.4.0" -typing-extensions = ">=3.7.4" - -[package.extras] -colorama = ["colorama (>=0.4.3)"] -d = ["aiohttp (>=3.3.2)", "aiohttp-cors"] - -[[package]] -name = "black" -version = "22.8.0" -description = "The uncompromising code formatter." -category = "dev" -optional = false -python-versions = ">=3.6.2" - -[package.dependencies] -click = ">=8.0.0" -dataclasses = {version = ">=0.6", markers = "python_version < \"3.7\""} -mypy-extensions = ">=0.4.3" -pathspec = ">=0.9.0" -platformdirs = ">=2" -tomli = {version = ">=1.1.0", markers = "python_full_version < \"3.11.0a7\""} -typed-ast = {version = ">=1.4.2", markers = "python_version < \"3.8\" and implementation_name == \"cpython\""} -typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""} - -[package.extras] -colorama = ["colorama (>=0.4.3)"] -d = ["aiohttp (>=3.7.4)"] -jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] -uvloop = ["uvloop (>=0.15.2)"] - -[[package]] -name = "build" -version = "0.8.0" -description = "A simple, correct PEP 517 build frontend" -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -colorama = {version = "*", markers = "os_name == \"nt\""} -importlib-metadata = {version = ">=0.22", markers = "python_version < \"3.8\""} -packaging = ">=19.0" -pep517 = ">=0.9.1" -tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} - -[package.extras] -docs = ["furo (>=2021.08.31)", "sphinx (>=4.0,<5.0)", "sphinx-argparse-cli (>=1.5)", "sphinx-autodoc-typehints (>=1.10)"] -test = ["filelock (>=3)", "pytest (>=6.2.4)", "pytest-cov (>=2.12)", "pytest-mock (>=2)", "pytest-rerunfailures (>=9.1)", "pytest-xdist (>=1.34)", "setuptools (>=42.0.0)", "setuptools (>=56.0.0)", "toml (>=0.10.0)", "wheel (>=0.36.0)"] -typing = ["importlib-metadata (>=4.6.4)", "mypy (==0.950)", "typing-extensions (>=3.7.4.3)"] -virtualenv = ["virtualenv (>=20.0.35)"] - -[[package]] -name = "bump2version" -version = "1.0.1" -description = "Version-bump your software with a single command!" -category = "dev" -optional = false -python-versions = ">=3.5" - -[[package]] -name = "certifi" -version = "2022.9.14" -description = "Python package for providing Mozilla's CA Bundle." -category = "dev" -optional = false -python-versions = ">=3.6" - -[[package]] -name = "charset-normalizer" -version = "2.0.12" -description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -category = "dev" -optional = false -python-versions = ">=3.5.0" - -[package.extras] -unicode_backport = ["unicodedata2"] - -[[package]] -name = "check-manifest" -version = "0.48" -description = "Check MANIFEST.in in a Python source package for completeness" -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -build = ">=0.1" -setuptools = "*" -tomli = "*" - -[package.extras] -test = ["mock (>=3.0.0)", "pytest"] - -[[package]] -name = "click" -version = "8.0.4" -description = "Composable command line interface toolkit" -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -colorama = {version = "*", markers = "platform_system == \"Windows\""} -importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} - -[[package]] -name = "colorama" -version = "0.4.5" -description = "Cross-platform colored terminal text." -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" - -[[package]] -name = "coverage" -version = "6.2" -description = "Code coverage measurement for Python" -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -tomli = {version = "*", optional = true, markers = "extra == \"toml\""} - -[package.extras] -toml = ["tomli"] - -[[package]] -name = "dataclasses" -version = "0.8" -description = "A backport of the dataclasses module for Python 3.6" -category = "dev" -optional = false -python-versions = ">=3.6, <3.7" - -[[package]] -name = "distlib" -version = "0.3.6" -description = "Distribution utilities" -category = "dev" -optional = false -python-versions = "*" - -[[package]] -name = "docutils" -version = "0.17.1" -description = "Docutils -- Python Documentation Utilities" -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" - -[[package]] -name = "filelock" -version = "3.4.1" -description = "A platform independent file lock." -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.extras] -docs = ["furo (>=2021.8.17b43)", "sphinx (>=4.1)", "sphinx-autodoc-typehints (>=1.12)"] -testing = ["covdefaults (>=1.2.0)", "coverage (>=4)", "pytest (>=4)", "pytest-cov", "pytest-timeout (>=1.4.2)"] - -[[package]] -name = "filelock" -version = "3.8.0" -description = "A platform independent file lock." -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.extras] -docs = ["furo (>=2022.6.21)", "sphinx (>=5.1.1)", "sphinx-autodoc-typehints (>=1.19.1)"] -testing = ["covdefaults (>=2.2)", "coverage (>=6.4.2)", "pytest (>=7.1.2)", "pytest-cov (>=3)", "pytest-timeout (>=2.1)"] - -[[package]] -name = "flake8" -version = "4.0.1" -description = "the modular source code checker: pep8 pyflakes and co" -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -importlib-metadata = {version = "<4.3", markers = "python_version < \"3.8\""} -mccabe = ">=0.6.0,<0.7.0" -pycodestyle = ">=2.8.0,<2.9.0" -pyflakes = ">=2.4.0,<2.5.0" - -[[package]] -name = "flake8" -version = "5.0.4" -description = "the modular source code checker: pep8 pyflakes and co" -category = "dev" -optional = false -python-versions = ">=3.6.1" - -[package.dependencies] -importlib-metadata = {version = ">=1.1.0,<4.3", markers = "python_version < \"3.8\""} -mccabe = ">=0.7.0,<0.8.0" -pycodestyle = ">=2.9.0,<2.10.0" -pyflakes = ">=2.5.0,<2.6.0" - -[[package]] -name = "idna" -version = "3.4" -description = "Internationalized Domain Names in Applications (IDNA)" -category = "dev" -optional = false -python-versions = ">=3.5" - -[[package]] -name = "imagesize" -version = "1.4.1" -description = "Getting image size from png/jpeg/jpeg2000/gif file" -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" - -[[package]] -name = "importlib-metadata" -version = "4.2.0" -description = "Read metadata from Python packages" -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} -zipp = ">=0.5" - -[package.extras] -docs = ["jaraco.packaging (>=8.2)", "rst.linker (>=1.9)", "sphinx"] -testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pep517", "pyfakefs", "pytest (>=4.6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.0.1)", "pytest-flake8", "pytest-mypy"] - -[[package]] -name = "importlib-resources" -version = "5.4.0" -description = "Read resources from Python packages" -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""} - -[package.extras] -docs = ["jaraco.packaging (>=8.2)", "rst.linker (>=1.9)", "sphinx"] -testing = ["pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.0.1)", "pytest-flake8", "pytest-mypy"] - -[[package]] -name = "iniconfig" -version = "1.1.1" -description = "iniconfig: brain-dead simple config-ini parsing" -category = "dev" -optional = false -python-versions = "*" - -[[package]] -name = "Jinja2" -version = "3.0.3" -description = "A very fast and expressive template engine." -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -MarkupSafe = ">=2.0" - -[package.extras] -i18n = ["Babel (>=2.7)"] - -[[package]] -name = "MarkupSafe" -version = "2.0.1" -description = "Safely add untrusted strings to HTML/XML markup." -category = "dev" -optional = false -python-versions = ">=3.6" - -[[package]] -name = "mccabe" -version = "0.6.1" -description = "McCabe checker, plugin for flake8" -category = "dev" -optional = false -python-versions = "*" - -[[package]] -name = "mccabe" -version = "0.7.0" -description = "McCabe checker, plugin for flake8" -category = "dev" -optional = false -python-versions = ">=3.6" - -[[package]] -name = "mypy" -version = "0.971" -description = "Optional static typing for Python" -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -mypy-extensions = ">=0.4.3" -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -typed-ast = {version = ">=1.4.0,<2", markers = "python_version < \"3.8\""} -typing-extensions = ">=3.10" - -[package.extras] -dmypy = ["psutil (>=4.0)"] -python2 = ["typed-ast (>=1.4.0,<2)"] -reports = ["lxml"] - -[[package]] -name = "mypy-extensions" -version = "0.4.3" -description = "Experimental type system extensions for programs checked with the mypy typechecker." -category = "dev" -optional = false -python-versions = "*" - -[[package]] -name = "packaging" -version = "21.3" -description = "Core utilities for Python packages" -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" - -[[package]] -name = "pathspec" -version = "0.9.0" -description = "Utility library for gitignore style pattern matching of file paths." -category = "dev" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" - -[[package]] -name = "pep517" -version = "0.13.0" -description = "Wrappers to build Python packages using PEP 517 hooks" -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -importlib_metadata = {version = "*", markers = "python_version < \"3.8\""} -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -zipp = {version = "*", markers = "python_version < \"3.8\""} - -[[package]] -name = "platformdirs" -version = "2.4.0" -description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.extras] -docs = ["Sphinx (>=4)", "furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)"] -test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"] - -[[package]] -name = "pluggy" -version = "1.0.0" -description = "plugin and hook calling mechanisms for python" -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} - -[package.extras] -dev = ["pre-commit", "tox"] -testing = ["pytest", "pytest-benchmark"] - -[[package]] -name = "py" -version = "1.11.0" -description = "library with cross-python path, ini-parsing, io, code, log facilities" -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" - -[[package]] -name = "py-cpuinfo" -version = "8.0.0" -description = "Get CPU info with pure Python 2 & 3" -category = "dev" -optional = false -python-versions = "*" - -[[package]] -name = "pycodestyle" -version = "2.8.0" -description = "Python style guide checker" -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" - -[[package]] -name = "pycodestyle" -version = "2.9.1" -description = "Python style guide checker" -category = "dev" -optional = false -python-versions = ">=3.6" - -[[package]] -name = "pyflakes" -version = "2.4.0" -description = "passive checker of Python programs" -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" - -[[package]] -name = "pyflakes" -version = "2.5.0" -description = "passive checker of Python programs" -category = "dev" -optional = false -python-versions = ">=3.6" - -[[package]] -name = "Pygments" -version = "2.13.0" -description = "Pygments is a syntax highlighting package written in Python." -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.extras] -plugins = ["importlib-metadata"] - -[[package]] -name = "pyparsing" -version = "3.0.7" -description = "Python parsing module" -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.extras] -diagrams = ["jinja2", "railroad-diagrams"] - -[[package]] -name = "pytest" -version = "6.2.5" -description = "pytest: simple powerful testing with Python" -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} -attrs = ">=19.2.0" -colorama = {version = "*", markers = "sys_platform == \"win32\""} -importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} -iniconfig = "*" -packaging = "*" -pluggy = ">=0.12,<2.0" -py = ">=1.8.2" -toml = "*" - -[package.extras] -testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] - -[[package]] -name = "pytest-asyncio" -version = "0.16.0" -description = "Pytest support for asyncio." -category = "dev" -optional = false -python-versions = ">= 3.6" - -[package.dependencies] -pytest = ">=5.4.0" - -[package.extras] -testing = ["coverage", "hypothesis (>=5.7.1)"] - -[[package]] -name = "pytest-asyncio" -version = "0.19.0" -description = "Pytest support for asyncio" -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -pytest = ">=6.1.0" -typing-extensions = {version = ">=3.7.2", markers = "python_version < \"3.8\""} - -[package.extras] -testing = ["coverage (>=6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy (>=0.931)", "pytest-trio (>=0.7.0)"] - -[[package]] -name = "pytest-benchmark" -version = "3.4.1" -description = "A ``pytest`` fixture for benchmarking code. It will group the tests into rounds that are calibrated to the chosen timer." -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" - -[package.dependencies] -py-cpuinfo = "*" -pytest = ">=3.8" - -[package.extras] -aspect = ["aspectlib"] -elasticsearch = ["elasticsearch"] -histogram = ["pygal", "pygaljs"] - -[[package]] -name = "pytest-cov" -version = "3.0.0" -description = "Pytest plugin for measuring coverage." -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -coverage = {version = ">=5.2.1", extras = ["toml"]} -pytest = ">=4.6" - -[package.extras] -testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] - -[[package]] -name = "pytest-describe" -version = "2.0.1" -description = "Describe-style plugin for pytest" -category = "dev" -optional = false -python-versions = "*" - -[package.dependencies] -pytest = ">=4.0.0" - -[[package]] -name = "pytest-timeout" -version = "2.1.0" -description = "pytest plugin to abort hanging tests" -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -pytest = ">=5.0.0" - -[[package]] -name = "pytz" -version = "2022.2.1" -description = "World timezone definitions, modern and historical" -category = "dev" -optional = false -python-versions = "*" - -[[package]] -name = "regex" -version = "2022.9.13" -description = "Alternative regular expression module, to replace re." -category = "dev" -optional = false -python-versions = ">=3.6" - -[[package]] -name = "requests" -version = "2.27.1" -description = "Python HTTP for Humans." -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" - -[package.dependencies] -certifi = ">=2017.4.17" -charset-normalizer = {version = ">=2.0.0,<2.1.0", markers = "python_version >= \"3\""} -idna = {version = ">=2.5,<4", markers = "python_version >= \"3\""} -urllib3 = ">=1.21.1,<1.27" - -[package.extras] -socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] -use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"] - -[[package]] -name = "setuptools" -version = "59.6.0" -description = "Easily download, build, install, upgrade, and uninstall Python packages" -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.extras] -docs = ["furo", "jaraco.packaging (>=8.2)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx", "sphinx-inline-tabs", "sphinxcontrib-towncrier"] -testing = ["flake8-2020", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mock", "paver", "pip (>=19.1)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.0.1)", "pytest-flake8", "pytest-mypy", "pytest-virtualenv (>=1.2.7)", "pytest-xdist", "sphinx", "virtualenv (>=13.0.0)", "wheel"] - -[[package]] -name = "six" -version = "1.16.0" -description = "Python 2 and 3 compatibility utilities" -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" - -[[package]] -name = "snowballstemmer" -version = "2.2.0" -description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." -category = "dev" -optional = false -python-versions = "*" - -[[package]] -name = "Sphinx" -version = "4.3.2" -description = "Python documentation generator" -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -alabaster = ">=0.7,<0.8" -babel = ">=1.3" -colorama = {version = ">=0.3.5", markers = "sys_platform == \"win32\""} -docutils = ">=0.14,<0.18" -imagesize = "*" -Jinja2 = ">=2.3" -packaging = "*" -Pygments = ">=2.0" -requests = ">=2.5.0" -setuptools = "*" -snowballstemmer = ">=1.1" -sphinxcontrib-applehelp = "*" -sphinxcontrib-devhelp = "*" -sphinxcontrib-htmlhelp = ">=2.0.0" -sphinxcontrib-jsmath = "*" -sphinxcontrib-qthelp = "*" -sphinxcontrib-serializinghtml = ">=1.1.5" - -[package.extras] -docs = ["sphinxcontrib-websupport"] -lint = ["docutils-stubs", "flake8 (>=3.5.0)", "isort", "mypy (>=0.920)", "types-pkg-resources", "types-requests", "types-typed-ast"] -test = ["cython", "html5lib", "pytest", "pytest-cov", "typed-ast"] - -[[package]] -name = "sphinx-rtd-theme" -version = "1.0.0" -description = "Read the Docs theme for Sphinx" -category = "dev" -optional = false -python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" - -[package.dependencies] -docutils = "<0.18" -sphinx = ">=1.6" - -[package.extras] -dev = ["bump2version", "sphinxcontrib-httpdomain", "transifex-client"] - -[[package]] -name = "sphinxcontrib-applehelp" -version = "1.0.2" -description = "sphinxcontrib-applehelp is a sphinx extension which outputs Apple help books" -category = "dev" -optional = false -python-versions = ">=3.5" - -[package.extras] -lint = ["docutils-stubs", "flake8", "mypy"] -test = ["pytest"] - -[[package]] -name = "sphinxcontrib-devhelp" -version = "1.0.2" -description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp document." -category = "dev" -optional = false -python-versions = ">=3.5" - -[package.extras] -lint = ["docutils-stubs", "flake8", "mypy"] -test = ["pytest"] - -[[package]] -name = "sphinxcontrib-htmlhelp" -version = "2.0.0" -description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.extras] -lint = ["docutils-stubs", "flake8", "mypy"] -test = ["html5lib", "pytest"] - -[[package]] -name = "sphinxcontrib-jsmath" -version = "1.0.1" -description = "A sphinx extension which renders display math in HTML via JavaScript" -category = "dev" -optional = false -python-versions = ">=3.5" - -[package.extras] -test = ["flake8", "mypy", "pytest"] - -[[package]] -name = "sphinxcontrib-qthelp" -version = "1.0.3" -description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp document." -category = "dev" -optional = false -python-versions = ">=3.5" - -[package.extras] -lint = ["docutils-stubs", "flake8", "mypy"] -test = ["pytest"] - -[[package]] -name = "sphinxcontrib-serializinghtml" -version = "1.1.5" -description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)." -category = "dev" -optional = false -python-versions = ">=3.5" - -[package.extras] -lint = ["docutils-stubs", "flake8", "mypy"] -test = ["pytest"] - -[[package]] -name = "toml" -version = "0.10.2" -description = "Python Library for Tom's Obvious, Minimal Language" -category = "dev" -optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" - -[[package]] -name = "tomli" -version = "1.2.3" -description = "A lil' TOML parser" -category = "dev" -optional = false -python-versions = ">=3.6" - -[[package]] -name = "tomli" -version = "2.0.1" -description = "A lil' TOML parser" -category = "dev" -optional = false -python-versions = ">=3.7" - -[[package]] -name = "tox" -version = "3.25.0" -description = "tox is a generic virtualenv management and test command line tool" -category = "dev" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" - -[package.dependencies] -colorama = {version = ">=0.4.1", markers = "platform_system == \"Windows\""} -filelock = ">=3.0.0" -importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} -packaging = ">=14" -pluggy = ">=0.12.0" -py = ">=1.4.17" -six = ">=1.14.0" -toml = ">=0.9.4" -virtualenv = ">=16.0.0,<20.0.0 || >20.0.0,<20.0.1 || >20.0.1,<20.0.2 || >20.0.2,<20.0.3 || >20.0.3,<20.0.4 || >20.0.4,<20.0.5 || >20.0.5,<20.0.6 || >20.0.6,<20.0.7 || >20.0.7" - -[package.extras] -docs = ["pygments-github-lexers (>=0.0.5)", "sphinx (>=2.0.0)", "sphinxcontrib-autoprogram (>=0.1.5)", "towncrier (>=18.5.0)"] -testing = ["flaky (>=3.4.0)", "freezegun (>=0.3.11)", "pathlib2 (>=2.3.3)", "psutil (>=5.6.1)", "pytest (>=4.0.0)", "pytest-cov (>=2.5.1)", "pytest-mock (>=1.10.0)", "pytest-randomly (>=1.0.0)"] - -[[package]] -name = "tox" -version = "3.26.0" -description = "tox is a generic virtualenv management and test command line tool" -category = "dev" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" - -[package.dependencies] -colorama = {version = ">=0.4.1", markers = "platform_system == \"Windows\""} -filelock = ">=3.0.0" -importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} -packaging = ">=14" -pluggy = ">=0.12.0" -py = ">=1.4.17" -six = ">=1.14.0" -tomli = {version = ">=2.0.1", markers = "python_version >= \"3.7\" and python_version < \"3.11\""} -virtualenv = ">=16.0.0,<20.0.0 || >20.0.0,<20.0.1 || >20.0.1,<20.0.2 || >20.0.2,<20.0.3 || >20.0.3,<20.0.4 || >20.0.4,<20.0.5 || >20.0.5,<20.0.6 || >20.0.6,<20.0.7 || >20.0.7" - -[package.extras] -docs = ["pygments-github-lexers (>=0.0.5)", "sphinx (>=2.0.0)", "sphinxcontrib-autoprogram (>=0.1.5)", "towncrier (>=18.5.0)"] -testing = ["flaky (>=3.4.0)", "freezegun (>=0.3.11)", "pathlib2 (>=2.3.3)", "psutil (>=5.6.1)", "pytest (>=4.0.0)", "pytest-cov (>=2.5.1)", "pytest-mock (>=1.10.0)", "pytest-randomly (>=1.0.0)"] - -[[package]] -name = "typed-ast" -version = "1.5.4" -description = "a fork of Python 2 and 3 ast modules with type comment support" -category = "dev" -optional = false -python-versions = ">=3.6" - -[[package]] -name = "typing-extensions" -version = "4.1.1" -description = "Backported and Experimental Type Hints for Python 3.6+" -category = "main" -optional = false -python-versions = ">=3.6" +# This file is automatically @generated by Poetry 2.4.1 and should not be changed by hand. [[package]] name = "typing-extensions" -version = "4.3.0" +version = "4.7.1" description = "Backported and Experimental Type Hints for Python 3.7+" -category = "main" optional = false python-versions = ">=3.7" - -[[package]] -name = "urllib3" -version = "1.26.12" -description = "HTTP library with thread-safe connection pooling, file post, and more." -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, <4" - -[package.extras] -brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] -secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] -socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] - -[[package]] -name = "virtualenv" -version = "20.16.2" -description = "Virtual Python Environment builder" -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -distlib = ">=0.3.1,<1" -filelock = ">=3.2,<4" -importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} -importlib-resources = {version = ">=1.0", markers = "python_version < \"3.7\""} -platformdirs = ">=2,<3" - -[package.extras] -docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=21.3)"] -testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "packaging (>=20.0)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)"] - -[[package]] -name = "zipp" -version = "3.6.0" -description = "Backport of pathlib-compatible object wrapper for zip files" -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.extras] -docs = ["jaraco.packaging (>=8.2)", "rst.linker (>=1.9)", "sphinx"] -testing = ["func-timeout", "jaraco.itertools", "pytest (>=4.6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.0.1)", "pytest-flake8", "pytest-mypy"] +groups = ["main"] +markers = "python_version < \"3.10\"" +files = [ + {file = "typing_extensions-4.7.1-py3-none-any.whl", hash = "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36"}, + {file = "typing_extensions-4.7.1.tar.gz", hash = "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2"}, +] [metadata] -lock-version = "1.1" -python-versions = "^3.6" -content-hash = "dc1f4a6e11fed7704f09dcd5e3b3af090398383b36458c966a52c840b7fc6c78" - -[metadata.files] -alabaster = [ - {file = "alabaster-0.7.12-py2.py3-none-any.whl", hash = "sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359"}, - {file = "alabaster-0.7.12.tar.gz", hash = "sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02"}, -] -appdirs = [ - {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, - {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, -] -atomicwrites = [ - {file = "atomicwrites-1.4.1.tar.gz", hash = "sha256:81b2c9071a49367a7f770170e5eec8cb66567cfbbc8c73d20ce5ca4a8d71cf11"}, -] -attrs = [ - {file = "attrs-22.1.0-py2.py3-none-any.whl", hash = "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c"}, - {file = "attrs-22.1.0.tar.gz", hash = "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6"}, -] -Babel = [ - {file = "Babel-2.10.3-py3-none-any.whl", hash = "sha256:ff56f4892c1c4bf0d814575ea23471c230d544203c7748e8c68f0089478d48eb"}, - {file = "Babel-2.10.3.tar.gz", hash = "sha256:7614553711ee97490f732126dc077f8d0ae084ebc6a96e23db1482afabdb2c51"}, -] -black = [ - {file = "black-20.8b1.tar.gz", hash = "sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea"}, - {file = "black-22.8.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ce957f1d6b78a8a231b18e0dd2d94a33d2ba738cd88a7fe64f53f659eea49fdd"}, - {file = "black-22.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5107ea36b2b61917956d018bd25129baf9ad1125e39324a9b18248d362156a27"}, - {file = "black-22.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e8166b7bfe5dcb56d325385bd1d1e0f635f24aae14b3ae437102dedc0c186747"}, - {file = "black-22.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd82842bb272297503cbec1a2600b6bfb338dae017186f8f215c8958f8acf869"}, - {file = "black-22.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:d839150f61d09e7217f52917259831fe2b689f5c8e5e32611736351b89bb2a90"}, - {file = "black-22.8.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:a05da0430bd5ced89176db098567973be52ce175a55677436a271102d7eaa3fe"}, - {file = "black-22.8.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a098a69a02596e1f2a58a2a1c8d5a05d5a74461af552b371e82f9fa4ada8342"}, - {file = "black-22.8.0-cp36-cp36m-win_amd64.whl", hash = "sha256:5594efbdc35426e35a7defa1ea1a1cb97c7dbd34c0e49af7fb593a36bd45edab"}, - {file = "black-22.8.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a983526af1bea1e4cf6768e649990f28ee4f4137266921c2c3cee8116ae42ec3"}, - {file = "black-22.8.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b2c25f8dea5e8444bdc6788a2f543e1fb01494e144480bc17f806178378005e"}, - {file = "black-22.8.0-cp37-cp37m-win_amd64.whl", hash = "sha256:78dd85caaab7c3153054756b9fe8c611efa63d9e7aecfa33e533060cb14b6d16"}, - {file = "black-22.8.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:cea1b2542d4e2c02c332e83150e41e3ca80dc0fb8de20df3c5e98e242156222c"}, - {file = "black-22.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5b879eb439094751185d1cfdca43023bc6786bd3c60372462b6f051efa6281a5"}, - {file = "black-22.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0a12e4e1353819af41df998b02c6742643cfef58282915f781d0e4dd7a200411"}, - {file = "black-22.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c3a73f66b6d5ba7288cd5d6dad9b4c9b43f4e8a4b789a94bf5abfb878c663eb3"}, - {file = "black-22.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:e981e20ec152dfb3e77418fb616077937378b322d7b26aa1ff87717fb18b4875"}, - {file = "black-22.8.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8ce13ffed7e66dda0da3e0b2eb1bdfc83f5812f66e09aca2b0978593ed636b6c"}, - {file = "black-22.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:32a4b17f644fc288c6ee2bafdf5e3b045f4eff84693ac069d87b1a347d861497"}, - {file = "black-22.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0ad827325a3a634bae88ae7747db1a395d5ee02cf05d9aa7a9bd77dfb10e940c"}, - {file = "black-22.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:53198e28a1fb865e9fe97f88220da2e44df6da82b18833b588b1883b16bb5d41"}, - {file = "black-22.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:bc4d4123830a2d190e9cc42a2e43570f82ace35c3aeb26a512a2102bce5af7ec"}, - {file = "black-22.8.0-py3-none-any.whl", hash = "sha256:d2c21d439b2baf7aa80d6dd4e3659259be64c6f49dfd0f32091063db0e006db4"}, - {file = "black-22.8.0.tar.gz", hash = "sha256:792f7eb540ba9a17e8656538701d3eb1afcb134e3b45b71f20b25c77a8db7e6e"}, -] -build = [ - {file = "build-0.8.0-py3-none-any.whl", hash = "sha256:19b0ed489f92ace6947698c3ca8436cb0556a66e2aa2d34cd70e2a5d27cd0437"}, - {file = "build-0.8.0.tar.gz", hash = "sha256:887a6d471c901b1a6e6574ebaeeebb45e5269a79d095fe9a8f88d6614ed2e5f0"}, -] -bump2version = [ - {file = "bump2version-1.0.1-py2.py3-none-any.whl", hash = "sha256:37f927ea17cde7ae2d7baf832f8e80ce3777624554a653006c9144f8017fe410"}, - {file = "bump2version-1.0.1.tar.gz", hash = "sha256:762cb2bfad61f4ec8e2bdf452c7c267416f8c70dd9ecb1653fd0bbb01fa936e6"}, -] -certifi = [ - {file = "certifi-2022.9.14-py3-none-any.whl", hash = "sha256:e232343de1ab72c2aa521b625c80f699e356830fd0e2c620b465b304b17b0516"}, - {file = "certifi-2022.9.14.tar.gz", hash = "sha256:36973885b9542e6bd01dea287b2b4b3b21236307c56324fcc3f1160f2d655ed5"}, -] -charset-normalizer = [ - {file = "charset-normalizer-2.0.12.tar.gz", hash = "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597"}, - {file = "charset_normalizer-2.0.12-py3-none-any.whl", hash = "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"}, -] -check-manifest = [ - {file = "check-manifest-0.48.tar.gz", hash = "sha256:3b575f1dade7beb3078ef4bf33a94519834457c7281dbc726b15c5466b55c657"}, - {file = "check_manifest-0.48-py3-none-any.whl", hash = "sha256:b1923685f98c1c2468601a1a7bed655db549a25d43c583caded3860ad8308f8c"}, -] -click = [ - {file = "click-8.0.4-py3-none-any.whl", hash = "sha256:6a7a62563bbfabfda3a38f3023a1db4a35978c0abd76f6c9605ecd6554d6d9b1"}, - {file = "click-8.0.4.tar.gz", hash = "sha256:8458d7b1287c5fb128c90e23381cf99dcde74beaf6c7ff6384ce84d6fe090adb"}, -] -colorama = [ - {file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"}, - {file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"}, -] -coverage = [ - {file = "coverage-6.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6dbc1536e105adda7a6312c778f15aaabe583b0e9a0b0a324990334fd458c94b"}, - {file = "coverage-6.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:174cf9b4bef0db2e8244f82059a5a72bd47e1d40e71c68ab055425172b16b7d0"}, - {file = "coverage-6.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:92b8c845527eae547a2a6617d336adc56394050c3ed8a6918683646328fbb6da"}, - {file = "coverage-6.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c7912d1526299cb04c88288e148c6c87c0df600eca76efd99d84396cfe00ef1d"}, - {file = "coverage-6.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d5d2033d5db1d58ae2d62f095e1aefb6988af65b4b12cb8987af409587cc0739"}, - {file = "coverage-6.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:3feac4084291642165c3a0d9eaebedf19ffa505016c4d3db15bfe235718d4971"}, - {file = "coverage-6.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:276651978c94a8c5672ea60a2656e95a3cce2a3f31e9fb2d5ebd4c215d095840"}, - {file = "coverage-6.2-cp310-cp310-win32.whl", hash = "sha256:f506af4f27def639ba45789fa6fde45f9a217da0be05f8910458e4557eed020c"}, - {file = "coverage-6.2-cp310-cp310-win_amd64.whl", hash = "sha256:3f7c17209eef285c86f819ff04a6d4cbee9b33ef05cbcaae4c0b4e8e06b3ec8f"}, - {file = "coverage-6.2-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:13362889b2d46e8d9f97c421539c97c963e34031ab0cb89e8ca83a10cc71ac76"}, - {file = "coverage-6.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:22e60a3ca5acba37d1d4a2ee66e051f5b0e1b9ac950b5b0cf4aa5366eda41d47"}, - {file = "coverage-6.2-cp311-cp311-win_amd64.whl", hash = "sha256:b637c57fdb8be84e91fac60d9325a66a5981f8086c954ea2772efe28425eaf64"}, - {file = "coverage-6.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f467bbb837691ab5a8ca359199d3429a11a01e6dfb3d9dcc676dc035ca93c0a9"}, - {file = "coverage-6.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2641f803ee9f95b1f387f3e8f3bf28d83d9b69a39e9911e5bfee832bea75240d"}, - {file = "coverage-6.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1219d760ccfafc03c0822ae2e06e3b1248a8e6d1a70928966bafc6838d3c9e48"}, - {file = "coverage-6.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:9a2b5b52be0a8626fcbffd7e689781bf8c2ac01613e77feda93d96184949a98e"}, - {file = "coverage-6.2-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:8e2c35a4c1f269704e90888e56f794e2d9c0262fb0c1b1c8c4ee44d9b9e77b5d"}, - {file = "coverage-6.2-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:5d6b09c972ce9200264c35a1d53d43ca55ef61836d9ec60f0d44273a31aa9f17"}, - {file = "coverage-6.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:e3db840a4dee542e37e09f30859f1612da90e1c5239a6a2498c473183a50e781"}, - {file = "coverage-6.2-cp36-cp36m-win32.whl", hash = "sha256:4e547122ca2d244f7c090fe3f4b5a5861255ff66b7ab6d98f44a0222aaf8671a"}, - {file = "coverage-6.2-cp36-cp36m-win_amd64.whl", hash = "sha256:01774a2c2c729619760320270e42cd9e797427ecfddd32c2a7b639cdc481f3c0"}, - {file = "coverage-6.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:fb8b8ee99b3fffe4fd86f4c81b35a6bf7e4462cba019997af2fe679365db0c49"}, - {file = "coverage-6.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:619346d57c7126ae49ac95b11b0dc8e36c1dd49d148477461bb66c8cf13bb521"}, - {file = "coverage-6.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0a7726f74ff63f41e95ed3a89fef002916c828bb5fcae83b505b49d81a066884"}, - {file = "coverage-6.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cfd9386c1d6f13b37e05a91a8583e802f8059bebfccde61a418c5808dea6bbfa"}, - {file = "coverage-6.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:17e6c11038d4ed6e8af1407d9e89a2904d573be29d51515f14262d7f10ef0a64"}, - {file = "coverage-6.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c254b03032d5a06de049ce8bca8338a5185f07fb76600afff3c161e053d88617"}, - {file = "coverage-6.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:dca38a21e4423f3edb821292e97cec7ad38086f84313462098568baedf4331f8"}, - {file = "coverage-6.2-cp37-cp37m-win32.whl", hash = "sha256:600617008aa82032ddeace2535626d1bc212dfff32b43989539deda63b3f36e4"}, - {file = "coverage-6.2-cp37-cp37m-win_amd64.whl", hash = "sha256:bf154ba7ee2fd613eb541c2bc03d3d9ac667080a737449d1a3fb342740eb1a74"}, - {file = "coverage-6.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f9afb5b746781fc2abce26193d1c817b7eb0e11459510fba65d2bd77fe161d9e"}, - {file = "coverage-6.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:edcada2e24ed68f019175c2b2af2a8b481d3d084798b8c20d15d34f5c733fa58"}, - {file = "coverage-6.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a9c8c4283e17690ff1a7427123ffb428ad6a52ed720d550e299e8291e33184dc"}, - {file = "coverage-6.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f614fc9956d76d8a88a88bb41ddc12709caa755666f580af3a688899721efecd"}, - {file = "coverage-6.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9365ed5cce5d0cf2c10afc6add145c5037d3148585b8ae0e77cc1efdd6aa2953"}, - {file = "coverage-6.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8bdfe9ff3a4ea37d17f172ac0dff1e1c383aec17a636b9b35906babc9f0f5475"}, - {file = "coverage-6.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:63c424e6f5b4ab1cf1e23a43b12f542b0ec2e54f99ec9f11b75382152981df57"}, - {file = "coverage-6.2-cp38-cp38-win32.whl", hash = "sha256:49dbff64961bc9bdd2289a2bda6a3a5a331964ba5497f694e2cbd540d656dc1c"}, - {file = "coverage-6.2-cp38-cp38-win_amd64.whl", hash = "sha256:9a29311bd6429be317c1f3fe4bc06c4c5ee45e2fa61b2a19d4d1d6111cb94af2"}, - {file = "coverage-6.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:03b20e52b7d31be571c9c06b74746746d4eb82fc260e594dc662ed48145e9efd"}, - {file = "coverage-6.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:215f8afcc02a24c2d9a10d3790b21054b58d71f4b3c6f055d4bb1b15cecce685"}, - {file = "coverage-6.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a4bdeb0a52d1d04123b41d90a4390b096f3ef38eee35e11f0b22c2d031222c6c"}, - {file = "coverage-6.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c332d8f8d448ded473b97fefe4a0983265af21917d8b0cdcb8bb06b2afe632c3"}, - {file = "coverage-6.2-cp39-cp39-win32.whl", hash = "sha256:6e1394d24d5938e561fbeaa0cd3d356207579c28bd1792f25a068743f2d5b282"}, - {file = "coverage-6.2-cp39-cp39-win_amd64.whl", hash = "sha256:86f2e78b1eff847609b1ca8050c9e1fa3bd44ce755b2ec30e70f2d3ba3844644"}, - {file = "coverage-6.2-pp36.pp37.pp38-none-any.whl", hash = "sha256:5829192582c0ec8ca4a2532407bc14c2f338d9878a10442f5d03804a95fac9de"}, - {file = "coverage-6.2.tar.gz", hash = "sha256:e2cad8093172b7d1595b4ad66f24270808658e11acf43a8f95b41276162eb5b8"}, -] -dataclasses = [ - {file = "dataclasses-0.8-py3-none-any.whl", hash = "sha256:0201d89fa866f68c8ebd9d08ee6ff50c0b255f8ec63a71c16fda7af82bb887bf"}, - {file = "dataclasses-0.8.tar.gz", hash = "sha256:8479067f342acf957dc82ec415d355ab5edb7e7646b90dc6e2fd1d96ad084c97"}, -] -distlib = [ - {file = "distlib-0.3.6-py2.py3-none-any.whl", hash = "sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e"}, - {file = "distlib-0.3.6.tar.gz", hash = "sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46"}, -] -docutils = [ - {file = "docutils-0.17.1-py2.py3-none-any.whl", hash = "sha256:cf316c8370a737a022b72b56874f6602acf974a37a9fba42ec2876387549fc61"}, - {file = "docutils-0.17.1.tar.gz", hash = "sha256:686577d2e4c32380bb50cbb22f575ed742d58168cee37e99117a854bcd88f125"}, -] -filelock = [ - {file = "filelock-3.4.1-py3-none-any.whl", hash = "sha256:a4bc51381e01502a30e9f06dd4fa19a1712eab852b6fb0f84fd7cce0793d8ca3"}, - {file = "filelock-3.4.1.tar.gz", hash = "sha256:0f12f552b42b5bf60dba233710bf71337d35494fc8bdd4fd6d9f6d082ad45e06"}, - {file = "filelock-3.8.0-py3-none-any.whl", hash = "sha256:617eb4e5eedc82fc5f47b6d61e4d11cb837c56cb4544e39081099fa17ad109d4"}, - {file = "filelock-3.8.0.tar.gz", hash = "sha256:55447caa666f2198c5b6b13a26d2084d26fa5b115c00d065664b2124680c4edc"}, -] -flake8 = [ - {file = "flake8-4.0.1-py2.py3-none-any.whl", hash = "sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d"}, - {file = "flake8-4.0.1.tar.gz", hash = "sha256:806e034dda44114815e23c16ef92f95c91e4c71100ff52813adf7132a6ad870d"}, - {file = "flake8-5.0.4-py2.py3-none-any.whl", hash = "sha256:7a1cf6b73744f5806ab95e526f6f0d8c01c66d7bbe349562d22dfca20610b248"}, - {file = "flake8-5.0.4.tar.gz", hash = "sha256:6fbe320aad8d6b95cec8b8e47bc933004678dc63095be98528b7bdd2a9f510db"}, -] -idna = [ - {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, - {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, -] -imagesize = [ - {file = "imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b"}, - {file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"}, -] -importlib-metadata = [ - {file = "importlib_metadata-4.2.0-py3-none-any.whl", hash = "sha256:057e92c15bc8d9e8109738a48db0ccb31b4d9d5cfbee5a8670879a30be66304b"}, - {file = "importlib_metadata-4.2.0.tar.gz", hash = "sha256:b7e52a1f8dec14a75ea73e0891f3060099ca1d8e6a462a4dff11c3e119ea1b31"}, -] -importlib-resources = [ - {file = "importlib_resources-5.4.0-py3-none-any.whl", hash = "sha256:33a95faed5fc19b4bc16b29a6eeae248a3fe69dd55d4d229d2b480e23eeaad45"}, - {file = "importlib_resources-5.4.0.tar.gz", hash = "sha256:d756e2f85dd4de2ba89be0b21dba2a3bbec2e871a42a3a16719258a11f87506b"}, -] -iniconfig = [ - {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, - {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, -] -Jinja2 = [ - {file = "Jinja2-3.0.3-py3-none-any.whl", hash = "sha256:077ce6014f7b40d03b47d1f1ca4b0fc8328a692bd284016f806ed0eaca390ad8"}, - {file = "Jinja2-3.0.3.tar.gz", hash = "sha256:611bb273cd68f3b993fabdc4064fc858c5b47a973cb5aa7999ec1ba405c87cd7"}, -] -MarkupSafe = [ - {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4dc8f9fb58f7364b63fd9f85013b780ef83c11857ae79f2feda41e270468dd9b"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:20dca64a3ef2d6e4d5d615a3fd418ad3bde77a47ec8a23d984a12b5b4c74491a"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cdfba22ea2f0029c9261a4bd07e830a8da012291fbe44dc794e488b6c9bb353a"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-win32.whl", hash = "sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:baa1a4e8f868845af802979fcdbf0bb11f94f1cb7ced4c4b8a351bb60d108145"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:deb993cacb280823246a026e3b2d81c493c53de6acfd5e6bfe31ab3402bb37dd"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:63f3268ba69ace99cab4e3e3b5840b03340efed0948ab8f78d2fd87ee5442a4f"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:8d206346619592c6200148b01a2142798c989edcb9c896f9ac9722a99d4e77e6"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-win32.whl", hash = "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d6c7ebd4e944c85e2c3421e612a7057a2f48d478d79e61800d81468a8d842207"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f0567c4dc99f264f49fe27da5f735f414c4e7e7dd850cfd8e69f0862d7c74ea9"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:89c687013cb1cd489a0f0ac24febe8c7a666e6e221b783e53ac50ebf68e45d86"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-win32.whl", hash = "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:aca6377c0cb8a8253e493c6b451565ac77e98c2951c45f913e0b52facdcff83f"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:04635854b943835a6ea959e948d19dcd311762c5c0c6e1f0e16ee57022669194"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6300b8454aa6930a24b9618fbb54b5a68135092bc666f7b06901f897fa5c2fee"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-win32.whl", hash = "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4296f2b1ce8c86a6aea78613c34bb1a672ea0e3de9c6ba08a960efe0b0a09047"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f02365d4e99430a12647f09b6cc8bab61a6564363f313126f775eb4f6ef798e"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5b6d930f030f8ed98e3e6c98ffa0652bdb82601e7a016ec2ab5d7ff23baa78d1"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-win32.whl", hash = "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8"}, - {file = "MarkupSafe-2.0.1.tar.gz", hash = "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a"}, -] -mccabe = [ - {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, - {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, - {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, - {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, -] -mypy = [ - {file = "mypy-0.971-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f2899a3cbd394da157194f913a931edfd4be5f274a88041c9dc2d9cdcb1c315c"}, - {file = "mypy-0.971-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:98e02d56ebe93981c41211c05adb630d1d26c14195d04d95e49cd97dbc046dc5"}, - {file = "mypy-0.971-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:19830b7dba7d5356d3e26e2427a2ec91c994cd92d983142cbd025ebe81d69cf3"}, - {file = "mypy-0.971-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:02ef476f6dcb86e6f502ae39a16b93285fef97e7f1ff22932b657d1ef1f28655"}, - {file = "mypy-0.971-cp310-cp310-win_amd64.whl", hash = "sha256:25c5750ba5609a0c7550b73a33deb314ecfb559c350bb050b655505e8aed4103"}, - {file = "mypy-0.971-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d3348e7eb2eea2472db611486846742d5d52d1290576de99d59edeb7cd4a42ca"}, - {file = "mypy-0.971-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3fa7a477b9900be9b7dd4bab30a12759e5abe9586574ceb944bc29cddf8f0417"}, - {file = "mypy-0.971-cp36-cp36m-win_amd64.whl", hash = "sha256:2ad53cf9c3adc43cf3bea0a7d01a2f2e86db9fe7596dfecb4496a5dda63cbb09"}, - {file = "mypy-0.971-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:855048b6feb6dfe09d3353466004490b1872887150c5bb5caad7838b57328cc8"}, - {file = "mypy-0.971-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:23488a14a83bca6e54402c2e6435467a4138785df93ec85aeff64c6170077fb0"}, - {file = "mypy-0.971-cp37-cp37m-win_amd64.whl", hash = "sha256:4b21e5b1a70dfb972490035128f305c39bc4bc253f34e96a4adf9127cf943eb2"}, - {file = "mypy-0.971-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:9796a2ba7b4b538649caa5cecd398d873f4022ed2333ffde58eaf604c4d2cb27"}, - {file = "mypy-0.971-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5a361d92635ad4ada1b1b2d3630fc2f53f2127d51cf2def9db83cba32e47c856"}, - {file = "mypy-0.971-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b793b899f7cf563b1e7044a5c97361196b938e92f0a4343a5d27966a53d2ec71"}, - {file = "mypy-0.971-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d1ea5d12c8e2d266b5fb8c7a5d2e9c0219fedfeb493b7ed60cd350322384ac27"}, - {file = "mypy-0.971-cp38-cp38-win_amd64.whl", hash = "sha256:23c7ff43fff4b0df93a186581885c8512bc50fc4d4910e0f838e35d6bb6b5e58"}, - {file = "mypy-0.971-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:1f7656b69974a6933e987ee8ffb951d836272d6c0f81d727f1d0e2696074d9e6"}, - {file = "mypy-0.971-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d2022bfadb7a5c2ef410d6a7c9763188afdb7f3533f22a0a32be10d571ee4bbe"}, - {file = "mypy-0.971-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ef943c72a786b0f8d90fd76e9b39ce81fb7171172daf84bf43eaf937e9f220a9"}, - {file = "mypy-0.971-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d744f72eb39f69312bc6c2abf8ff6656973120e2eb3f3ec4f758ed47e414a4bf"}, - {file = "mypy-0.971-cp39-cp39-win_amd64.whl", hash = "sha256:77a514ea15d3007d33a9e2157b0ba9c267496acf12a7f2b9b9f8446337aac5b0"}, - {file = "mypy-0.971-py3-none-any.whl", hash = "sha256:0d054ef16b071149917085f51f89555a576e2618d5d9dd70bd6eea6410af3ac9"}, - {file = "mypy-0.971.tar.gz", hash = "sha256:40b0f21484238269ae6a57200c807d80debc6459d444c0489a102d7c6a75fa56"}, -] -mypy-extensions = [ - {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, - {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, -] -packaging = [ - {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, - {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, -] -pathspec = [ - {file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"}, - {file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"}, -] -pep517 = [ - {file = "pep517-0.13.0-py3-none-any.whl", hash = "sha256:4ba4446d80aed5b5eac6509ade100bff3e7943a8489de249654a5ae9b33ee35b"}, - {file = "pep517-0.13.0.tar.gz", hash = "sha256:ae69927c5c172be1add9203726d4b84cf3ebad1edcd5f71fcdc746e66e829f59"}, -] -platformdirs = [ - {file = "platformdirs-2.4.0-py3-none-any.whl", hash = "sha256:8868bbe3c3c80d42f20156f22e7131d2fb321f5bc86a2a345375c6481a67021d"}, - {file = "platformdirs-2.4.0.tar.gz", hash = "sha256:367a5e80b3d04d2428ffa76d33f124cf11e8fff2acdaa9b43d545f5c7d661ef2"}, -] -pluggy = [ - {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, - {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, -] -py = [ - {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, - {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, -] -py-cpuinfo = [ - {file = "py-cpuinfo-8.0.0.tar.gz", hash = "sha256:5f269be0e08e33fd959de96b34cd4aeeeacac014dd8305f70eb28d06de2345c5"}, -] -pycodestyle = [ - {file = "pycodestyle-2.8.0-py2.py3-none-any.whl", hash = "sha256:720f8b39dde8b293825e7ff02c475f3077124006db4f440dcbc9a20b76548a20"}, - {file = "pycodestyle-2.8.0.tar.gz", hash = "sha256:eddd5847ef438ea1c7870ca7eb78a9d47ce0cdb4851a5523949f2601d0cbbe7f"}, - {file = "pycodestyle-2.9.1-py2.py3-none-any.whl", hash = "sha256:d1735fc58b418fd7c5f658d28d943854f8a849b01a5d0a1e6f3f3fdd0166804b"}, - {file = "pycodestyle-2.9.1.tar.gz", hash = "sha256:2c9607871d58c76354b697b42f5d57e1ada7d261c261efac224b664affdc5785"}, -] -pyflakes = [ - {file = "pyflakes-2.4.0-py2.py3-none-any.whl", hash = "sha256:3bb3a3f256f4b7968c9c788781e4ff07dce46bdf12339dcda61053375426ee2e"}, - {file = "pyflakes-2.4.0.tar.gz", hash = "sha256:05a85c2872edf37a4ed30b0cce2f6093e1d0581f8c19d7393122da7e25b2b24c"}, - {file = "pyflakes-2.5.0-py2.py3-none-any.whl", hash = "sha256:4579f67d887f804e67edb544428f264b7b24f435b263c4614f384135cea553d2"}, - {file = "pyflakes-2.5.0.tar.gz", hash = "sha256:491feb020dca48ccc562a8c0cbe8df07ee13078df59813b83959cbdada312ea3"}, -] -Pygments = [ - {file = "Pygments-2.13.0-py3-none-any.whl", hash = "sha256:f643f331ab57ba3c9d89212ee4a2dabc6e94f117cf4eefde99a0574720d14c42"}, - {file = "Pygments-2.13.0.tar.gz", hash = "sha256:56a8508ae95f98e2b9bdf93a6be5ae3f7d8af858b43e02c5a2ff083726be40c1"}, -] -pyparsing = [ - {file = "pyparsing-3.0.7-py3-none-any.whl", hash = "sha256:a6c06a88f252e6c322f65faf8f418b16213b51bdfaece0524c1c1bc30c63c484"}, - {file = "pyparsing-3.0.7.tar.gz", hash = "sha256:18ee9022775d270c55187733956460083db60b37d0d0fb357445f3094eed3eea"}, -] -pytest = [ - {file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"}, - {file = "pytest-6.2.5.tar.gz", hash = "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89"}, -] -pytest-asyncio = [ - {file = "pytest-asyncio-0.16.0.tar.gz", hash = "sha256:7496c5977ce88c34379df64a66459fe395cd05543f0a2f837016e7144391fcfb"}, - {file = "pytest_asyncio-0.16.0-py3-none-any.whl", hash = "sha256:5f2a21273c47b331ae6aa5b36087047b4899e40f03f18397c0e65fa5cca54e9b"}, - {file = "pytest-asyncio-0.19.0.tar.gz", hash = "sha256:ac4ebf3b6207259750bc32f4c1d8fcd7e79739edbc67ad0c58dd150b1d072fed"}, - {file = "pytest_asyncio-0.19.0-py3-none-any.whl", hash = "sha256:7a97e37cfe1ed296e2e84941384bdd37c376453912d397ed39293e0916f521fa"}, -] -pytest-benchmark = [ - {file = "pytest-benchmark-3.4.1.tar.gz", hash = "sha256:40e263f912de5a81d891619032983557d62a3d85843f9a9f30b98baea0cd7b47"}, - {file = "pytest_benchmark-3.4.1-py2.py3-none-any.whl", hash = "sha256:36d2b08c4882f6f997fd3126a3d6dfd70f3249cde178ed8bbc0b73db7c20f809"}, -] -pytest-cov = [ - {file = "pytest-cov-3.0.0.tar.gz", hash = "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470"}, - {file = "pytest_cov-3.0.0-py3-none-any.whl", hash = "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6"}, -] -pytest-describe = [ - {file = "pytest-describe-2.0.1.tar.gz", hash = "sha256:e5cbaa31169f0060348ad5ca0191027e5f1f41f3f27fdeef208365e09c55eb9a"}, - {file = "pytest_describe-2.0.1-py3-none-any.whl", hash = "sha256:ea347838bdf774b498ee7cb4a0b802a40be89e667a399fb63d860e3223bf4183"}, -] -pytest-timeout = [ - {file = "pytest-timeout-2.1.0.tar.gz", hash = "sha256:c07ca07404c612f8abbe22294b23c368e2e5104b521c1790195561f37e1ac3d9"}, - {file = "pytest_timeout-2.1.0-py3-none-any.whl", hash = "sha256:f6f50101443ce70ad325ceb4473c4255e9d74e3c7cd0ef827309dfa4c0d975c6"}, -] -pytz = [ - {file = "pytz-2022.2.1-py2.py3-none-any.whl", hash = "sha256:220f481bdafa09c3955dfbdddb7b57780e9a94f5127e35456a48589b9e0c0197"}, - {file = "pytz-2022.2.1.tar.gz", hash = "sha256:cea221417204f2d1a2aa03ddae3e867921971d0d76f14d87abb4414415bbdcf5"}, -] -regex = [ - {file = "regex-2022.9.13-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0394265391a86e2bbaa7606e59ac71bd9f1edf8665a59e42771a9c9adbf6fd4f"}, - {file = "regex-2022.9.13-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:86df2049b18745f3cd4b0f4c4ef672bfac4b80ca488e6ecfd2bbfe68d2423a2c"}, - {file = "regex-2022.9.13-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce331b076b2b013e7d7f07157f957974ef0b0881a808e8a4a4b3b5105aee5d04"}, - {file = "regex-2022.9.13-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:360ffbc9357794ae41336b681dff1c0463193199dfb91fcad3ec385ea4972f46"}, - {file = "regex-2022.9.13-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18e503b1e515a10282b3f14f1b3d856194ecece4250e850fad230842ed31227f"}, - {file = "regex-2022.9.13-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6e167d1ccd41d27b7b6655bb7a2dcb1b1eb1e0d2d662043470bd3b4315d8b2b"}, - {file = "regex-2022.9.13-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4146cb7ae6029fc83b5c905ec6d806b7e5568dc14297c423e66b86294bad6c39"}, - {file = "regex-2022.9.13-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a1aec4ae549fd7b3f52ceaf67e133010e2fba1538bf4d5fc5cd162a5e058d5df"}, - {file = "regex-2022.9.13-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:cab548d6d972e1de584161487b2ac1aa82edd8430d1bde69587ba61698ad1cfb"}, - {file = "regex-2022.9.13-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:3d64e1a7e6d98a4cdc8b29cb8d8ed38f73f49e55fbaa737bdb5933db99b9de22"}, - {file = "regex-2022.9.13-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:67a4c625361db04ae40ef7c49d3cbe2c1f5ff10b5a4491327ab20f19f2fb5d40"}, - {file = "regex-2022.9.13-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:5d0dd8b06896423211ce18fba0c75dacc49182a1d6514c004b535be7163dca0f"}, - {file = "regex-2022.9.13-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4318f69b79f9f7d84a7420e97d4bfe872dc767c72f891d4fea5fa721c74685f7"}, - {file = "regex-2022.9.13-cp310-cp310-win32.whl", hash = "sha256:26df88c9636a0c3f3bd9189dd435850a0c49d0b7d6e932500db3f99a6dd604d1"}, - {file = "regex-2022.9.13-cp310-cp310-win_amd64.whl", hash = "sha256:6fe1dd1021e0f8f3f454ce2811f1b0b148f2d25bb38c712fec00316551e93650"}, - {file = "regex-2022.9.13-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:83cc32a1a2fa5bac00f4abc0e6ce142e3c05d3a6d57e23bd0f187c59b4e1e43b"}, - {file = "regex-2022.9.13-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a2effeaf50a6838f3dd4d3c5d265f06eabc748f476e8441892645ae3a697e273"}, - {file = "regex-2022.9.13-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59a786a55d00439d8fae4caaf71581f2aaef7297d04ee60345c3594efef5648a"}, - {file = "regex-2022.9.13-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b7b701dbc124558fd2b1b08005eeca6c9160e209108fbcbd00091fcfac641ac7"}, - {file = "regex-2022.9.13-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dab81cc4d58026861445230cfba27f9825e9223557926e7ec22156a1a140d55c"}, - {file = "regex-2022.9.13-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b0c5cc3d1744a67c3b433dce91e5ef7c527d612354c1f1e8576d9e86bc5c5e2"}, - {file = "regex-2022.9.13-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:518272f25da93e02af4f1e94985f5042cec21557ef3591027d0716f2adda5d0a"}, - {file = "regex-2022.9.13-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:8418ee2cb857b83881b8f981e4c636bc50a0587b12d98cb9b947408a3c484fe7"}, - {file = "regex-2022.9.13-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:cfa4c956ff0a977c4823cb3b930b0a4e82543b060733628fec7ab3eb9b1abe37"}, - {file = "regex-2022.9.13-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:a1c4d17879dd4c4432c08a1ca1ab379f12ab54af569e945b6fc1c4cf6a74ca45"}, - {file = "regex-2022.9.13-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:77c2879d3ba51e5ca6c2b47f2dcf3d04a976a623a8fc8236010a16c9e0b0a3c7"}, - {file = "regex-2022.9.13-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d2885ec6eea629c648ecc9bde0837ec6b92208b7f36381689937fe5d64a517e8"}, - {file = "regex-2022.9.13-cp311-cp311-win32.whl", hash = "sha256:2dda4b096a6f630d6531728a45bd12c67ec3badf44342046dc77d4897277d4f2"}, - {file = "regex-2022.9.13-cp311-cp311-win_amd64.whl", hash = "sha256:592b9e2e1862168e71d9e612bfdc22c451261967dbd46681f14e76dfba7105fd"}, - {file = "regex-2022.9.13-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:df8fe00b60e4717662c7f80c810ba66dcc77309183c76b7754c0dff6f1d42054"}, - {file = "regex-2022.9.13-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:995e70bb8c91d1b99ed2aaf8ec44863e06ad1dfbb45d7df95f76ef583ec323a9"}, - {file = "regex-2022.9.13-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad75173349ad79f9d21e0d0896b27dcb37bfd233b09047bc0b4d226699cf5c87"}, - {file = "regex-2022.9.13-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7681c49da1a2d4b905b4f53d86c9ba4506e79fba50c4a664d9516056e0f7dfcc"}, - {file = "regex-2022.9.13-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9bc8edc5f8ef0ebb46f3fa0d02bd825bbe9cc63d59e428ffb6981ff9672f6de1"}, - {file = "regex-2022.9.13-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b7bee775ff05c9d519195bd9e8aaaccfe3971db60f89f89751ee0f234e8aeac5"}, - {file = "regex-2022.9.13-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1a901ce5cd42658ab8f8eade51b71a6d26ad4b68c7cfc86b87efc577dfa95602"}, - {file = "regex-2022.9.13-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:14a7ab070fa3aec288076eed6ed828587b805ef83d37c9bfccc1a4a7cfbd8111"}, - {file = "regex-2022.9.13-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:d23ac6b4bf9e32fcde5fcdb2e1fd5e7370d6693fcac51ee1d340f0e886f50d1f"}, - {file = "regex-2022.9.13-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:4cdbfa6d2befeaee0c899f19222e9b20fc5abbafe5e9c43a46ef819aeb7b75e5"}, - {file = "regex-2022.9.13-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:ab07934725e6f25c6f87465976cc69aef1141e86987af49d8c839c3ffd367c72"}, - {file = "regex-2022.9.13-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:d2a1371dc73e921f3c2e087c05359050f3525a9a34b476ebc8130e71bec55e97"}, - {file = "regex-2022.9.13-cp36-cp36m-win32.whl", hash = "sha256:fcbd1edff1473d90dc5cf4b52d355cf1f47b74eb7c85ba6e45f45d0116b8edbd"}, - {file = "regex-2022.9.13-cp36-cp36m-win_amd64.whl", hash = "sha256:fe428822b7a8c486bcd90b334e9ab541ce6cc0d6106993d59f201853e5e14121"}, - {file = "regex-2022.9.13-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d7430f041755801b712ec804aaf3b094b9b5facbaa93a6339812a8e00d7bd53a"}, - {file = "regex-2022.9.13-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:079c182f99c89524069b9cd96f5410d6af437e9dca576a7d59599a574972707e"}, - {file = "regex-2022.9.13-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:59bac44b5a07b08a261537f652c26993af9b1bbe2a29624473968dd42fc29d56"}, - {file = "regex-2022.9.13-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a59d0377e58d96a6f11636e97992f5b51b7e1e89eb66332d1c01b35adbabfe8a"}, - {file = "regex-2022.9.13-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b9d68eb704b24bc4d441b24e4a12653acd07d2c39940548761e0985a08bc1fff"}, - {file = "regex-2022.9.13-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0385d66e73cdd4462f3cc42c76a6576ddcc12472c30e02a2ae82061bff132c32"}, - {file = "regex-2022.9.13-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:db45016364eec9ddbb5af93c8740c5c92eb7f5fc8848d1ae04205a40a1a2efc6"}, - {file = "regex-2022.9.13-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:03ff695518482b946a6d3d4ce9cbbd99a21320e20d94913080aa3841f880abcd"}, - {file = "regex-2022.9.13-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:6b32b45433df1fad7fed738fe15200b6516da888e0bd1fdd6aa5e50cc16b76bc"}, - {file = "regex-2022.9.13-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:003a2e1449d425afc817b5f0b3d4c4aa9072dd5f3dfbf6c7631b8dc7b13233de"}, - {file = "regex-2022.9.13-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:a9eb9558e1d0f78e07082d8a70d5c4d631c8dd75575fae92105df9e19c736730"}, - {file = "regex-2022.9.13-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:f6e0321921d2fdc082ef90c1fd0870f129c2e691bfdc4937dcb5cd308aba95c4"}, - {file = "regex-2022.9.13-cp37-cp37m-win32.whl", hash = "sha256:3f3b4594d564ed0b2f54463a9f328cf6a5b2a32610a90cdff778d6e3e561d08b"}, - {file = "regex-2022.9.13-cp37-cp37m-win_amd64.whl", hash = "sha256:8aba0d01e3dfd335f2cb107079b07fdddb4cd7fb2d8c8a1986f9cb8ce9246c24"}, - {file = "regex-2022.9.13-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:944567bb08f52268d8600ee5bdf1798b2b62ea002cc692a39cec113244cbdd0d"}, - {file = "regex-2022.9.13-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0b664a4d33ffc6be10996606dfc25fd3248c24cc589c0b139feb4c158053565e"}, - {file = "regex-2022.9.13-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f06cc1190f3db3192ab8949e28f2c627e1809487e2cfc435b6524c1ce6a2f391"}, - {file = "regex-2022.9.13-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6c57d50d4d5eb0c862569ca3c840eba2a73412f31d9ecc46ef0d6b2e621a592b"}, - {file = "regex-2022.9.13-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:19a4da6f513045f5ba00e491215bd00122e5bd131847586522463e5a6b2bd65f"}, - {file = "regex-2022.9.13-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a926339356fe29595f8e37af71db37cd87ff764e15da8ad5129bbaff35bcc5a6"}, - {file = "regex-2022.9.13-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:091efcfdd4178a7e19a23776dc2b1fafb4f57f4d94daf340f98335817056f874"}, - {file = "regex-2022.9.13-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:880dbeb6bdde7d926b4d8e41410b16ffcd4cb3b4c6d926280fea46e2615c7a01"}, - {file = "regex-2022.9.13-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:73b985c9fc09a7896846e26d7b6f4d1fd5a20437055f4ef985d44729f9f928d0"}, - {file = "regex-2022.9.13-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:c0b7cb9598795b01f9a3dd3f770ab540889259def28a3bf9b2fa24d52edecba3"}, - {file = "regex-2022.9.13-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:37e5a26e76c46f54b3baf56a6fdd56df9db89758694516413757b7d127d4c57b"}, - {file = "regex-2022.9.13-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:99945ddb4f379bb9831c05e9f80f02f079ba361a0fb1fba1fc3b267639b6bb2e"}, - {file = "regex-2022.9.13-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8dcbcc9e72a791f622a32d17ff5011326a18996647509cac0609a7fc43adc229"}, - {file = "regex-2022.9.13-cp38-cp38-win32.whl", hash = "sha256:d3102ab9bf16bf541ca228012d45d88d2a567c9682a805ae2c145a79d3141fdd"}, - {file = "regex-2022.9.13-cp38-cp38-win_amd64.whl", hash = "sha256:14216ea15efc13f28d0ef1c463d86d93ca7158a79cd4aec0f9273f6d4c6bb047"}, - {file = "regex-2022.9.13-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9a165a05979e212b2c2d56a9f40b69c811c98a788964e669eb322de0a3e420b4"}, - {file = "regex-2022.9.13-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:14c71437ffb89479c89cc7022a5ea2075a842b728f37205e47c824cc17b30a42"}, - {file = "regex-2022.9.13-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee7045623a5ace70f3765e452528b4c1f2ce669ed31959c63f54de64fe2f6ff7"}, - {file = "regex-2022.9.13-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6e521d9db006c5e4a0f8acfef738399f72b704913d4e083516774eb51645ad7c"}, - {file = "regex-2022.9.13-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b86548b8234b2be3985dbc0b385e35f5038f0f3e6251464b827b83ebf4ed90e5"}, - {file = "regex-2022.9.13-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2b39ee3b280e15824298b97cec3f7cbbe6539d8282cc8a6047a455b9a72c598"}, - {file = "regex-2022.9.13-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e6e6e61e9a38b6cc60ca3e19caabc90261f070f23352e66307b3d21a24a34aaf"}, - {file = "regex-2022.9.13-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d837ccf3bd2474feabee96cd71144e991472e400ed26582edc8ca88ce259899c"}, - {file = "regex-2022.9.13-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:6adfe300848d61a470ec7547adc97b0ccf86de86a99e6830f1d8c8d19ecaf6b3"}, - {file = "regex-2022.9.13-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:d5b003d248e6f292475cd24b04e5f72c48412231961a675edcb653c70730e79e"}, - {file = "regex-2022.9.13-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:d5edd3eb877c9fc2e385173d4a4e1d792bf692d79e25c1ca391802d36ecfaa01"}, - {file = "regex-2022.9.13-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:50e764ffbd08b06aa8c4e86b8b568b6722c75d301b33b259099f237c46b2134e"}, - {file = "regex-2022.9.13-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6d43bd402b27e0e7eae85c612725ba1ce7798f20f6fab4e8bc3de4f263294f03"}, - {file = "regex-2022.9.13-cp39-cp39-win32.whl", hash = "sha256:7fcf7f94ccad19186820ac67e2ec7e09e0ac2dac39689f11cf71eac580503296"}, - {file = "regex-2022.9.13-cp39-cp39-win_amd64.whl", hash = "sha256:322bd5572bed36a5b39952d88e072738926759422498a96df138d93384934ff8"}, - {file = "regex-2022.9.13.tar.gz", hash = "sha256:f07373b6e56a6f3a0df3d75b651a278ca7bd357a796078a26a958ea1ce0588fd"}, -] -requests = [ - {file = "requests-2.27.1-py2.py3-none-any.whl", hash = "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d"}, - {file = "requests-2.27.1.tar.gz", hash = "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61"}, -] -setuptools = [ - {file = "setuptools-59.6.0-py3-none-any.whl", hash = "sha256:4ce92f1e1f8f01233ee9952c04f6b81d1e02939d6e1b488428154974a4d0783e"}, - {file = "setuptools-59.6.0.tar.gz", hash = "sha256:22c7348c6d2976a52632c67f7ab0cdf40147db7789f9aed18734643fe9cf3373"}, -] -six = [ - {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, - {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, -] -snowballstemmer = [ - {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, - {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, -] -Sphinx = [ - {file = "Sphinx-4.3.2-py3-none-any.whl", hash = "sha256:6a11ea5dd0bdb197f9c2abc2e0ce73e01340464feaece525e64036546d24c851"}, - {file = "Sphinx-4.3.2.tar.gz", hash = "sha256:0a8836751a68306b3fe97ecbe44db786f8479c3bf4b80e3a7f5c838657b4698c"}, -] -sphinx-rtd-theme = [ - {file = "sphinx_rtd_theme-1.0.0-py2.py3-none-any.whl", hash = "sha256:4d35a56f4508cfee4c4fb604373ede6feae2a306731d533f409ef5c3496fdbd8"}, - {file = "sphinx_rtd_theme-1.0.0.tar.gz", hash = "sha256:eec6d497e4c2195fa0e8b2016b337532b8a699a68bcb22a512870e16925c6a5c"}, -] -sphinxcontrib-applehelp = [ - {file = "sphinxcontrib-applehelp-1.0.2.tar.gz", hash = "sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58"}, - {file = "sphinxcontrib_applehelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:806111e5e962be97c29ec4c1e7fe277bfd19e9652fb1a4392105b43e01af885a"}, -] -sphinxcontrib-devhelp = [ - {file = "sphinxcontrib-devhelp-1.0.2.tar.gz", hash = "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4"}, - {file = "sphinxcontrib_devhelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e"}, -] -sphinxcontrib-htmlhelp = [ - {file = "sphinxcontrib-htmlhelp-2.0.0.tar.gz", hash = "sha256:f5f8bb2d0d629f398bf47d0d69c07bc13b65f75a81ad9e2f71a63d4b7a2f6db2"}, - {file = "sphinxcontrib_htmlhelp-2.0.0-py2.py3-none-any.whl", hash = "sha256:d412243dfb797ae3ec2b59eca0e52dac12e75a241bf0e4eb861e450d06c6ed07"}, -] -sphinxcontrib-jsmath = [ - {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, - {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"}, -] -sphinxcontrib-qthelp = [ - {file = "sphinxcontrib-qthelp-1.0.3.tar.gz", hash = "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72"}, - {file = "sphinxcontrib_qthelp-1.0.3-py2.py3-none-any.whl", hash = "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6"}, -] -sphinxcontrib-serializinghtml = [ - {file = "sphinxcontrib-serializinghtml-1.1.5.tar.gz", hash = "sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952"}, - {file = "sphinxcontrib_serializinghtml-1.1.5-py2.py3-none-any.whl", hash = "sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd"}, -] -toml = [ - {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, - {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, -] -tomli = [ - {file = "tomli-1.2.3-py3-none-any.whl", hash = "sha256:e3069e4be3ead9668e21cb9b074cd948f7b3113fd9c8bba083f48247aab8b11c"}, - {file = "tomli-1.2.3.tar.gz", hash = "sha256:05b6166bff487dc068d322585c7ea4ef78deed501cc124060e0f238e89a9231f"}, - {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, - {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, -] -tox = [ - {file = "tox-3.25.0-py2.py3-none-any.whl", hash = "sha256:0805727eb4d6b049de304977dfc9ce315a1938e6619c3ab9f38682bb04662a5a"}, - {file = "tox-3.25.0.tar.gz", hash = "sha256:37888f3092aa4e9f835fc8cc6dadbaaa0782651c41ef359e3a5743fcb0308160"}, - {file = "tox-3.26.0-py2.py3-none-any.whl", hash = "sha256:bf037662d7c740d15c9924ba23bb3e587df20598697bb985ac2b49bdc2d847f6"}, - {file = "tox-3.26.0.tar.gz", hash = "sha256:44f3c347c68c2c68799d7d44f1808f9d396fc8a1a500cbc624253375c7ae107e"}, -] -typed-ast = [ - {file = "typed_ast-1.5.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:669dd0c4167f6f2cd9f57041e03c3c2ebf9063d0757dc89f79ba1daa2bfca9d4"}, - {file = "typed_ast-1.5.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:211260621ab1cd7324e0798d6be953d00b74e0428382991adfddb352252f1d62"}, - {file = "typed_ast-1.5.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:267e3f78697a6c00c689c03db4876dd1efdfea2f251a5ad6555e82a26847b4ac"}, - {file = "typed_ast-1.5.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c542eeda69212fa10a7ada75e668876fdec5f856cd3d06829e6aa64ad17c8dfe"}, - {file = "typed_ast-1.5.4-cp310-cp310-win_amd64.whl", hash = "sha256:a9916d2bb8865f973824fb47436fa45e1ebf2efd920f2b9f99342cb7fab93f72"}, - {file = "typed_ast-1.5.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:79b1e0869db7c830ba6a981d58711c88b6677506e648496b1f64ac7d15633aec"}, - {file = "typed_ast-1.5.4-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a94d55d142c9265f4ea46fab70977a1944ecae359ae867397757d836ea5a3f47"}, - {file = "typed_ast-1.5.4-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:183afdf0ec5b1b211724dfef3d2cad2d767cbefac291f24d69b00546c1837fb6"}, - {file = "typed_ast-1.5.4-cp36-cp36m-win_amd64.whl", hash = "sha256:639c5f0b21776605dd6c9dbe592d5228f021404dafd377e2b7ac046b0349b1a1"}, - {file = "typed_ast-1.5.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cf4afcfac006ece570e32d6fa90ab74a17245b83dfd6655a6f68568098345ff6"}, - {file = "typed_ast-1.5.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed855bbe3eb3715fca349c80174cfcfd699c2f9de574d40527b8429acae23a66"}, - {file = "typed_ast-1.5.4-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6778e1b2f81dfc7bc58e4b259363b83d2e509a65198e85d5700dfae4c6c8ff1c"}, - {file = "typed_ast-1.5.4-cp37-cp37m-win_amd64.whl", hash = "sha256:0261195c2062caf107831e92a76764c81227dae162c4f75192c0d489faf751a2"}, - {file = "typed_ast-1.5.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2efae9db7a8c05ad5547d522e7dbe62c83d838d3906a3716d1478b6c1d61388d"}, - {file = "typed_ast-1.5.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7d5d014b7daa8b0bf2eaef684295acae12b036d79f54178b92a2b6a56f92278f"}, - {file = "typed_ast-1.5.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:370788a63915e82fd6f212865a596a0fefcbb7d408bbbb13dea723d971ed8bdc"}, - {file = "typed_ast-1.5.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4e964b4ff86550a7a7d56345c7864b18f403f5bd7380edf44a3c1fb4ee7ac6c6"}, - {file = "typed_ast-1.5.4-cp38-cp38-win_amd64.whl", hash = "sha256:683407d92dc953c8a7347119596f0b0e6c55eb98ebebd9b23437501b28dcbb8e"}, - {file = "typed_ast-1.5.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4879da6c9b73443f97e731b617184a596ac1235fe91f98d279a7af36c796da35"}, - {file = "typed_ast-1.5.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3e123d878ba170397916557d31c8f589951e353cc95fb7f24f6bb69adc1a8a97"}, - {file = "typed_ast-1.5.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebd9d7f80ccf7a82ac5f88c521115cc55d84e35bf8b446fcd7836eb6b98929a3"}, - {file = "typed_ast-1.5.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98f80dee3c03455e92796b58b98ff6ca0b2a6f652120c263efdba4d6c5e58f72"}, - {file = "typed_ast-1.5.4-cp39-cp39-win_amd64.whl", hash = "sha256:0fdbcf2fef0ca421a3f5912555804296f0b0960f0418c440f5d6d3abb549f3e1"}, - {file = "typed_ast-1.5.4.tar.gz", hash = "sha256:39e21ceb7388e4bb37f4c679d72707ed46c2fbf2a5609b8b8ebc4b067d977df2"}, -] -typing-extensions = [ - {file = "typing_extensions-4.1.1-py3-none-any.whl", hash = "sha256:21c85e0fe4b9a155d0799430b0ad741cdce7e359660ccbd8b530613e8df88ce2"}, - {file = "typing_extensions-4.1.1.tar.gz", hash = "sha256:1a9462dcc3347a79b1f1c0271fbe79e844580bb598bafa1ed208b94da3cdcd42"}, - {file = "typing_extensions-4.3.0-py3-none-any.whl", hash = "sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02"}, - {file = "typing_extensions-4.3.0.tar.gz", hash = "sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6"}, -] -urllib3 = [ - {file = "urllib3-1.26.12-py2.py3-none-any.whl", hash = "sha256:b930dd878d5a8afb066a637fbb35144fe7901e3b209d1cd4f524bd0e9deee997"}, - {file = "urllib3-1.26.12.tar.gz", hash = "sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e"}, -] -virtualenv = [ - {file = "virtualenv-20.16.2-py2.py3-none-any.whl", hash = "sha256:635b272a8e2f77cb051946f46c60a54ace3cb5e25568228bd6b57fc70eca9ff3"}, - {file = "virtualenv-20.16.2.tar.gz", hash = "sha256:0ef5be6d07181946891f5abc8047fda8bc2f0b4b9bf222c64e6e8963baee76db"}, -] -zipp = [ - {file = "zipp-3.6.0-py3-none-any.whl", hash = "sha256:9fe5ea21568a0a70e50f273397638d39b03353731e6cbbb3fd8502a33fec40bc"}, - {file = "zipp-3.6.0.tar.gz", hash = "sha256:71c644c5369f4a6e07636f0aa966270449561fcea2e3d6747b8d23efaa9d7832"}, -] +lock-version = "2.1" +python-versions = "^3.7" +content-hash = "d66030380f60a51d643393288c825d3e740f0b810f616d696cd1b367db19456e" diff --git a/pyproject.toml b/pyproject.toml index 439a4256..8ddc2c48 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "graphql-core" -version = "3.2.2" +version = "3.2.11" description = """ GraphQL-core is a Python port of GraphQL.js,\ the JavaScript reference implementation for GraphQL.""" @@ -18,11 +18,14 @@ classifiers = [ "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10" + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", ] packages = [ { include = "graphql", from = "src" }, @@ -37,53 +40,52 @@ packages = [ { include = 'poetry.lock', format = "sdist" }, { include = 'tox.ini', format = "sdist" }, { include = 'setup.cfg', format = "sdist" }, + { include = 'setup.py', format = "sdist" }, { include = 'CODEOWNERS', format = "sdist" }, { include = 'MANIFEST.in', format = "sdist" }, { include = 'SECURITY.md', format = "sdist" } ] [tool.poetry.dependencies] -python = "^3.6" +python = "^3.7" typing-extensions = [ - { version = "^4.3", python = ">=3.7,<3.8" }, - { version = "^4.1", python = "<3.7" } + { version = ">=4.7,<5", python = "<3.10" } ] -[tool.poetry.dev-dependencies] -pytest = "^6.2" -pytest-asyncio = [ - {version=">=0.19,<1", python = ">=3.7" }, - {version=">=0.16,<0.17", python = "<3.7" }, -] -pytest-benchmark = "^3.4" -pytest-cov = "^3.0" -pytest-describe = "^2.0" -pytest-timeout = "^2.1" -black = [ - {version = "22.8.0", python = ">=3.6.2"}, - {version = "20.8b1", python = "<3.6.2"} -] -flake8 = [ - {version = "^5.0", python = ">=3.6.1"}, - {version = "^4.0", python = "<3.6.1"} -] -mypy = "0.971" -sphinx = "^4.3" -sphinx_rtd_theme = ">=1,<2" -check-manifest = ">=0.48,<1" -bump2version = ">=1.0,<2" -tomli = [ - {version="^2", python = ">=3.7"}, - {version="^1.2", python = "<3.7"} -] -tox = [ - {version = "^3.26", python = ">=3.7"}, - {version = "3.25", python = "<3.7"} +[tool.black] +target-version = [ + 'py37', 'py38', 'py39', 'py310', 'py311', 'py312', 'py313', 'py314' ] -[tool.black] -target-version = ['py36', 'py37', 'py38', 'py39', 'py310'] +[tool.pyright] +# silence pyright since we're using mypy already +reportArgumentType = false +reportAssignmentType = false +reportAttributeAccessIssue = false +reportIncompatibleVariableOverride = false +reportInvalidTypeForm = false +reportMissingModuleSource = false +reportMissingTypeArgument = false +reportReturnType = false +reportTypedDictNotRequiredAccess = false +reportUnknownArgumentType = false +reportUnknownMemberType = false +reportUnknownParameterType = false +reportUnnecessaryIsInstance = false +reportUnknownVariableType = false +ignore = ["**/test_*"] # test functions + +[tool.pylint.basic] +max-module-lines = 2000 + +[tool.pylint.messages_control] +disable = [ + "method-hidden", + "missing-module-docstring", # test modules + "redefined-outer-name", + "unused-variable", # test functions +] [build-system] -requires = ["poetry_core>=1,<2", "setuptools>=59,<70"] +requires = ["poetry_core>=1,<3", "setuptools>=59,<83"] build-backend = "poetry.core.masonry.api" diff --git a/setup.cfg b/setup.cfg index dee6b2da..511e19c9 100644 --- a/setup.cfg +++ b/setup.cfg @@ -5,6 +5,7 @@ python-tag = py3 test = pytest [tool:pytest] +minversion = 7.4 # Only run benchmarks as tests. # To actually run the benchmarks, use --benchmark-enable on the command line. # To run the slow tests (fuzzing), add --run-slow on the command line. @@ -12,8 +13,12 @@ addopts = --benchmark-disable # Deactivate default name pattern for test classes (we use pytest_describe). python_classes = PyTest* # Handle all async fixtures and tests automatically by asyncio -asyncio_mode = auto +asyncio_mode = strict +# Default event loop scope used for asynchronous fixtures +asyncio_default_fixture_loop_scope = function # Set a timeout in seconds for aborting tests that run too long. timeout = 100 # Ignore config options not (yet) available in older Python versions. filterwarnings = ignore::pytest.PytestConfigWarning +# All tests can be found in the tests directory. +testpaths = tests diff --git a/setup.py b/setup.py index d307a66a..0f7662dd 100644 --- a/setup.py +++ b/setup.py @@ -1,5 +1,6 @@ from re import search -from setuptools import setup, find_packages + +from setuptools import find_packages, setup with open("src/graphql/version.py") as version_file: version = search('version = "(.*)"', version_file.read()).group(1) @@ -26,16 +27,19 @@ "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", ], install_requires=[ - "typing-extensions>=4.2,<5; python_version < '3.8'", + "typing-extensions>=4.7,<5; python_version < '3.10'", ], - python_requires=">=3.6,<4", + python_requires=">=3.7,<4", packages=find_packages("src"), package_dir={"": "src"}, # PEP-561: https://www.python.org/dev/peps/pep-0561/ diff --git a/src/graphql/__init__.py b/src/graphql/__init__.py index f1b21ab3..2cd8f01c 100644 --- a/src/graphql/__init__.py +++ b/src/graphql/__init__.py @@ -48,6 +48,7 @@ GraphQLError, GraphQLErrorExtensions, GraphQLFormattedError, + GraphQLFormattedErrorExtensions, GraphQLSyntaxError, located_error, ) @@ -67,6 +68,7 @@ parse_value, parse_const_value, parse_type, + parse_schema_coordinate, # Print print_ast, # Visit @@ -91,6 +93,7 @@ is_type_definition_node, is_type_system_extension_node, is_type_extension_node, + is_schema_coordinate_node, # Types SourceLocation, Location, @@ -157,6 +160,14 @@ UnionTypeExtensionNode, EnumTypeExtensionNode, InputObjectTypeExtensionNode, + DirectiveExtensionNode, + # Schema Coordinates + SchemaCoordinateNode, + TypeCoordinateNode, + MemberCoordinateNode, + ArgumentCoordinateNode, + DirectiveCoordinateNode, + DirectiveArgumentCoordinateNode, ) # Utilities for operating on GraphQL type schema and parsed sources. @@ -225,6 +236,17 @@ DangerousChangeType, find_breaking_changes, find_dangerous_changes, + # Resolve a schema coordinate to a schema element. + resolve_schema_coordinate, + resolve_ast_schema_coordinate, + ResolvedNamedType, + ResolvedField, + ResolvedInputField, + ResolvedEnumValue, + ResolvedFieldArgument, + ResolvedDirective, + ResolvedDirectiveArgument, + ResolvedSchemaElement, ) # Create and operate on GraphQL type definitions and schema. @@ -256,6 +278,7 @@ GraphQLSkipDirective, GraphQLDeprecatedDirective, GraphQLSpecifiedByDirective, + GraphQLOneOfDirective, # "Enum" of Type Kinds TypeKind, # Constant Deprecation Reason @@ -341,6 +364,7 @@ GraphQLArgumentMap, GraphQLEnumValue, GraphQLEnumValueMap, + GraphQLEnumValuesDefinition, GraphQLField, GraphQLFieldMap, GraphQLFieldResolver, @@ -378,6 +402,7 @@ SDLValidationRule, # All validation rules in the GraphQL Specification. specified_rules, + recommended_rules, # Individual validation rules. ExecutableDefinitionsRule, FieldsOnCorrectTypeRule, @@ -417,6 +442,8 @@ # Custom validation rules NoDeprecatedCustomRule, NoSchemaIntrospectionCustomRule, + # Recommended validation rules + MaxIntrospectionDepthRule, ) # Execute GraphQL documents. @@ -485,6 +512,7 @@ "GraphQLSkipDirective", "GraphQLDeprecatedDirective", "GraphQLSpecifiedByDirective", + "GraphQLOneOfDirective", "TypeKind", "DEFAULT_DEPRECATION_REASON", "introspection_types", @@ -559,6 +587,7 @@ "GraphQLArgumentMap", "GraphQLEnumValue", "GraphQLEnumValueMap", + "GraphQLEnumValuesDefinition", "GraphQLField", "GraphQLFieldMap", "GraphQLFieldResolver", @@ -594,6 +623,7 @@ "parse_value", "parse_const_value", "parse_type", + "parse_schema_coordinate", "print_ast", "visit", "ParallelVisitor", @@ -616,6 +646,7 @@ "is_type_definition_node", "is_type_system_extension_node", "is_type_extension_node", + "is_schema_coordinate_node", "SourceLocation", "Location", "Token", @@ -679,6 +710,13 @@ "UnionTypeExtensionNode", "EnumTypeExtensionNode", "InputObjectTypeExtensionNode", + "DirectiveExtensionNode", + "SchemaCoordinateNode", + "TypeCoordinateNode", + "MemberCoordinateNode", + "ArgumentCoordinateNode", + "DirectiveCoordinateNode", + "DirectiveArgumentCoordinateNode", "execute", "execute_sync", "default_field_resolver", @@ -700,6 +738,7 @@ "ASTValidationRule", "SDLValidationRule", "specified_rules", + "recommended_rules", "ExecutableDefinitionsRule", "FieldsOnCorrectTypeRule", "FragmentsOnCompositeTypesRule", @@ -736,9 +775,11 @@ "PossibleTypeExtensionsRule", "NoDeprecatedCustomRule", "NoSchemaIntrospectionCustomRule", + "MaxIntrospectionDepthRule", "GraphQLError", "GraphQLErrorExtensions", "GraphQLFormattedError", + "GraphQLFormattedErrorExtensions", "GraphQLSyntaxError", "located_error", "get_introspection_query", @@ -775,6 +816,16 @@ "BreakingChangeType", "DangerousChange", "DangerousChangeType", + "resolve_schema_coordinate", + "resolve_ast_schema_coordinate", + "ResolvedNamedType", + "ResolvedField", + "ResolvedInputField", + "ResolvedEnumValue", + "ResolvedFieldArgument", + "ResolvedDirective", + "ResolvedDirectiveArgument", + "ResolvedSchemaElement", "Undefined", "UndefinedType", ] diff --git a/src/graphql/error/__init__.py b/src/graphql/error/__init__.py index 20f5a7c5..9d2e705a 100644 --- a/src/graphql/error/__init__.py +++ b/src/graphql/error/__init__.py @@ -4,7 +4,12 @@ errors. """ -from .graphql_error import GraphQLError, GraphQLErrorExtensions, GraphQLFormattedError +from .graphql_error import ( + GraphQLError, + GraphQLErrorExtensions, + GraphQLFormattedError, + GraphQLFormattedErrorExtensions, +) from .syntax_error import GraphQLSyntaxError @@ -14,6 +19,7 @@ "GraphQLError", "GraphQLErrorExtensions", "GraphQLFormattedError", + "GraphQLFormattedErrorExtensions", "GraphQLSyntaxError", "located_error", ] diff --git a/src/graphql/error/graphql_error.py b/src/graphql/error/graphql_error.py index 75e34b6a..dfca7f1e 100644 --- a/src/graphql/error/graphql_error.py +++ b/src/graphql/error/graphql_error.py @@ -14,7 +14,12 @@ ) # noqa: F401 from ..language.source import Source # noqa: F401 -__all__ = ["GraphQLError", "GraphQLErrorExtensions", "GraphQLFormattedError"] +__all__ = [ + "GraphQLError", + "GraphQLErrorExtensions", + "GraphQLFormattedError", + "GraphQLFormattedErrorExtensions", +] # Custom extensions @@ -25,6 +30,14 @@ # a dictionary which can contain all the values you need. +# Custom formatted extensions +GraphQLFormattedErrorExtensions = Dict[str, Any] +# Use a unique identifier name for your extension, for example the name of +# your library or project. Do not use a shortened identifier as this increases +# the risk of conflicts. We recommend you add at most one extension key, +# a dictionary which can contain all the values you need. + + class GraphQLFormattedError(TypedDict, total=False): """Formatted GraphQL error""" @@ -41,7 +54,7 @@ class GraphQLFormattedError(TypedDict, total=False): path: List[Union[str, int]] # Reserved for implementors to extend the protocol however they see fit, # and hence there are no additional restrictions on its contents. - extensions: GraphQLErrorExtensions + extensions: GraphQLFormattedErrorExtensions class GraphQLError(Exception): @@ -241,6 +254,8 @@ def print_error(error: GraphQLError) -> str: """Print a GraphQLError to a string. Represents useful location information about the error's position in the source. + This deprecated helper is retained for backwards compatibility; call ``str(error)`` + instead because ``print_error`` will be removed in v3.3. .. deprecated:: 3.2 Please use ``str(error)`` instead. Will be removed in v3.3. @@ -254,7 +269,9 @@ def format_error(error: GraphQLError) -> GraphQLFormattedError: """Format a GraphQL error. Given a GraphQLError, format it according to the rules described by the "Response - Format, Errors" section of the GraphQL Specification. + Format, Errors" section of the GraphQL Specification. This deprecated helper is + retained for backwards compatibility; use ``error.formatted`` instead because + ``format_error`` will be removed in v3.3. .. deprecated:: 3.2 Please use ``error.formatted`` instead. Will be removed in v3.3. diff --git a/src/graphql/execution/execute.py b/src/graphql/execution/execute.py index 01ec288a..d6bcc94a 100644 --- a/src/graphql/execution/execute.py +++ b/src/graphql/execution/execute.py @@ -1,5 +1,6 @@ from asyncio import ensure_future, gather from collections.abc import Mapping +from contextlib import suppress from inspect import isawaitable from typing import ( Any, @@ -10,9 +11,10 @@ Iterable, List, Optional, - Union, + Set, Tuple, Type, + Union, cast, ) @@ -30,25 +32,27 @@ OperationType, ) from ..pyutils import ( - inspect, - is_awaitable as default_is_awaitable, - is_iterable, AwaitableOrValue, Path, Undefined, + inspect, + is_iterable, +) +from ..pyutils import ( + is_awaitable as default_is_awaitable, ) from ..type import ( GraphQLAbstractType, GraphQLField, + GraphQLFieldResolver, GraphQLLeafType, GraphQLList, GraphQLNonNull, GraphQLObjectType, GraphQLOutputType, + GraphQLResolveInfo, GraphQLSchema, - GraphQLFieldResolver, GraphQLTypeResolver, - GraphQLResolveInfo, SchemaMetaFieldDef, TypeMetaFieldDef, TypeNameMetaFieldDef, @@ -171,6 +175,42 @@ def __ne__(self, other: Any) -> bool: Middleware = Optional[Union[Tuple, List, MiddlewareManager]] +class CollectedErrors: + """A list of errors collected during execution, ignoring nulled positions. + + For internal use only. + """ + + _error_positions: Set[Optional[Path]] + _errors: List[GraphQLError] + + def __init__(self) -> None: + self._error_positions = set() + self._errors = [] + + @property + def errors(self) -> List[GraphQLError]: + return self._errors + + def add(self, error: GraphQLError, path: Optional[Path]) -> None: + # Do not modify errors list if the execution position for this error or + # any of its ancestors has already been nulled via error propagation. + # This check should be unnecessary for implementations able to implement + # actual cancellation. + if self._has_nulled_position(path): + return + self._error_positions.add(path) + self._errors.append(error) + + def _has_nulled_position(self, start_path: Optional[Path]) -> bool: + path = start_path + while path is not None: + if path in self._error_positions: + return True + path = path.prev + return None in self._error_positions + + class ExecutionContext: """Data that must be available at all points during query execution. @@ -187,7 +227,7 @@ class ExecutionContext: field_resolver: GraphQLFieldResolver type_resolver: GraphQLTypeResolver subscribe_field_resolver: GraphQLFieldResolver - errors: List[GraphQLError] + collected_errors: CollectedErrors middleware_manager: Optional[MiddlewareManager] is_awaitable = staticmethod(default_is_awaitable) @@ -203,7 +243,7 @@ def __init__( field_resolver: GraphQLFieldResolver, type_resolver: GraphQLTypeResolver, subscribe_field_resolver: GraphQLFieldResolver, - errors: List[GraphQLError], + collected_errors: CollectedErrors, middleware_manager: Optional[MiddlewareManager], is_awaitable: Optional[Callable[[Any], bool]], ) -> None: @@ -213,13 +253,13 @@ def __init__( self.context_value = context_value self.operation = operation self.variable_values = variable_values - self.field_resolver = field_resolver # type: ignore - self.type_resolver = type_resolver # type: ignore - self.subscribe_field_resolver = subscribe_field_resolver # type: ignore - self.errors = errors + self.field_resolver = field_resolver + self.type_resolver = type_resolver + self.subscribe_field_resolver = subscribe_field_resolver + self.collected_errors = collected_errors self.middleware_manager = middleware_manager if is_awaitable: - self.is_awaitable = is_awaitable + self.is_awaitable = is_awaitable # type: ignore self._subfields_cache: Dict[Tuple, Dict[str, List[FieldNode]]] = {} @classmethod @@ -234,6 +274,7 @@ def build( field_resolver: Optional[GraphQLFieldResolver] = None, type_resolver: Optional[GraphQLTypeResolver] = None, subscribe_field_resolver: Optional[GraphQLFieldResolver] = None, + max_coercion_errors: int = 50, middleware: Optional[Middleware] = None, is_awaitable: Optional[Callable[[Any], bool]] = None, ) -> Union[List[GraphQLError], "ExecutionContext"]: @@ -286,7 +327,7 @@ def build( schema, operation.variable_definitions or (), raw_variable_values or {}, - max_errors=50, + max_errors=max_coercion_errors, ) if isinstance(coerced_variable_values, list): @@ -302,7 +343,7 @@ def build( field_resolver or default_field_resolver, type_resolver or default_type_resolver, subscribe_field_resolver or default_field_resolver, - [], + CollectedErrors(), middleware_manager, is_awaitable, ) @@ -532,7 +573,7 @@ async def await_result() -> Any: return completed except Exception as raw_error: error = located_error(raw_error, field_nodes, path.as_list()) - self.handle_field_error(error, return_type) + self.handle_field_error(error, return_type, path) return None return await_result() @@ -547,7 +588,7 @@ async def await_completed() -> Any: return await completed except Exception as raw_error: error = located_error(raw_error, field_nodes, path.as_list()) - self.handle_field_error(error, return_type) + self.handle_field_error(error, return_type, path) return None return await_completed() @@ -555,13 +596,14 @@ async def await_completed() -> Any: return completed except Exception as raw_error: error = located_error(raw_error, field_nodes, path.as_list()) - self.handle_field_error(error, return_type) + self.handle_field_error(error, return_type, path) return None def handle_field_error( self, error: GraphQLError, return_type: GraphQLOutputType, + path: Path, ) -> None: # If the field type is non-nullable, then it is resolved without any protection # from errors, however it still properly locates the error. @@ -569,7 +611,7 @@ def handle_field_error( raise error # Otherwise, error protection is applied, logging the error and resolving a # null value for this field if one is encountered. - self.errors.append(error) + self.collected_errors.add(error, path) return None def complete_value( @@ -717,7 +759,7 @@ async def await_completed(item: Any, item_path: Path) -> Any: error = located_error( raw_error, field_nodes, item_path.as_list() ) - self.handle_field_error(error, item_type) + self.handle_field_error(error, item_type, item_path) return None completed_item = await_completed(item, item_path) @@ -735,13 +777,13 @@ async def await_completed(item: Any, item_path: Path) -> Any: error = located_error( raw_error, field_nodes, item_path.as_list() ) - self.handle_field_error(error, item_type) + self.handle_field_error(error, item_type, item_path) return None completed_item = await_completed(completed_item, item_path) except Exception as raw_error: error = located_error(raw_error, field_nodes, item_path.as_list()) - self.handle_field_error(error, item_type) + self.handle_field_error(error, item_type, item_path) completed_item = None if is_awaitable(completed_item): @@ -793,7 +835,7 @@ def complete_abstract_value( that value, then complete the value for that type. """ resolve_type_fn = return_type.resolve_type or self.type_resolver - runtime_type = resolve_type_fn(result, info, return_type) # type: ignore + runtime_type = resolve_type_fn(result, info, return_type) if self.is_awaitable(runtime_type): runtime_type = cast(Awaitable, runtime_type) @@ -972,6 +1014,7 @@ def execute( field_resolver: Optional[GraphQLFieldResolver] = None, type_resolver: Optional[GraphQLTypeResolver] = None, subscribe_field_resolver: Optional[GraphQLFieldResolver] = None, + max_coercion_errors: int = 50, middleware: Optional[Middleware] = None, execution_context_class: Optional[Type["ExecutionContext"]] = None, is_awaitable: Optional[Callable[[Any], bool]] = None, @@ -1004,6 +1047,7 @@ def execute( field_resolver, type_resolver, subscribe_field_resolver, + max_coercion_errors, middleware, is_awaitable, ) @@ -1023,7 +1067,7 @@ def execute( # Errors from sub-fields of a NonNull type may propagate to the top level, # at which point we still log the error and null the parent field, which # in this case is the entire response. - errors = exe_context.errors + collected_errors = exe_context.collected_errors build_response = exe_context.build_response try: operation = exe_context.operation @@ -1033,17 +1077,18 @@ def execute( # noinspection PyShadowingNames async def await_result() -> Any: try: - return build_response(await result, errors) # type: ignore + data = await result # type: ignore + return build_response(data, collected_errors.errors) except GraphQLError as error: - errors.append(error) - return build_response(None, errors) + collected_errors.add(error, None) + return build_response(None, collected_errors.errors) return await_result() except GraphQLError as error: - errors.append(error) - return build_response(None, errors) + collected_errors.add(error, None) + return build_response(None, collected_errors.errors) else: - return build_response(result, errors) # type: ignore + return build_response(result, collected_errors.errors) # type: ignore def assume_not_awaitable(_value: Any) -> bool: @@ -1060,6 +1105,7 @@ def execute_sync( operation_name: Optional[str] = None, field_resolver: Optional[GraphQLFieldResolver] = None, type_resolver: Optional[GraphQLTypeResolver] = None, + max_coercion_errors: int = 50, middleware: Optional[Middleware] = None, execution_context_class: Optional[Type["ExecutionContext"]] = None, check_sync: bool = False, @@ -1089,6 +1135,7 @@ def execute_sync( field_resolver, type_resolver, None, + max_coercion_errors, middleware, execution_context_class, is_awaitable, @@ -1096,10 +1143,10 @@ def execute_sync( # Assert that the execution was synchronous. if isawaitable(result): - ensure_future(cast(Awaitable[ExecutionResult], result)).cancel() + ensure_future(result).cancel() raise RuntimeError("GraphQL execution failed to complete synchronously.") - return cast(ExecutionResult, result) + return result def assert_valid_execution_arguments( @@ -1212,6 +1259,16 @@ def default_type_resolver( append_awaitable_results(cast(Awaitable, is_type_of_result)) append_awaitable_types(type_) elif is_type_of_result: + if awaitable_is_type_of_results: + # noinspection PyShadowingNames + async def await_is_type_of_and_return_type( + resolved_type_name: str = type_.name, + ) -> str: + with suppress(Exception): + await gather(*awaitable_is_type_of_results) + return resolved_type_name + + return await_is_type_of_and_return_type() return type_.name if awaitable_is_type_of_results: diff --git a/src/graphql/execution/map_async_iterator.py b/src/graphql/execution/map_async_iterator.py index 43400fd3..3010a5f4 100644 --- a/src/graphql/execution/map_async_iterator.py +++ b/src/graphql/execution/map_async_iterator.py @@ -1,8 +1,8 @@ from asyncio import CancelledError, Event, Task, ensure_future, wait from concurrent.futures import FIRST_COMPLETED from inspect import isasyncgen, isawaitable -from typing import cast, Any, AsyncIterable, Callable, Optional, Set, Type, Union from types import TracebackType +from typing import Any, AsyncIterable, Callable, Optional, Set, Type, Union __all__ = ["MapAsyncIterator"] @@ -73,19 +73,19 @@ async def athrow( """Throw an exception into the asynchronous iterator.""" if self.is_closed: return + if isinstance(type_, BaseException): + value = type_ + type_ = type(value) + traceback = value.__traceback__ athrow = getattr(self.iterator, "athrow", None) if athrow: - await athrow(type_, value, traceback) + await athrow(type_ if value is None else value) else: await self.aclose() if value is None: if traceback is None: - raise type_ - value = ( - type_ - if isinstance(value, BaseException) - else cast(Type[BaseException], type_)() - ) + raise type_ # pragma: no cover + value = type_ if isinstance(value, BaseException) else type_() if traceback is not None: value = value.with_traceback(traceback) raise value diff --git a/src/graphql/execution/subscribe.py b/src/graphql/execution/subscribe.py index 21fe4db3..eaed2db0 100644 --- a/src/graphql/execution/subscribe.py +++ b/src/graphql/execution/subscribe.py @@ -35,6 +35,7 @@ async def subscribe( operation_name: Optional[str] = None, field_resolver: Optional[GraphQLFieldResolver] = None, subscribe_field_resolver: Optional[GraphQLFieldResolver] = None, + max_coercion_errors: int = 50, ) -> Union[AsyncIterator[ExecutionResult], ExecutionResult]: """Create a GraphQL subscription. @@ -63,6 +64,7 @@ async def subscribe( variable_values, operation_name, subscribe_field_resolver, + max_coercion_errors, ) if isinstance(result_or_stream, ExecutionResult): return result_or_stream @@ -100,6 +102,7 @@ async def create_source_event_stream( variable_values: Optional[Dict[str, Any]] = None, operation_name: Optional[str] = None, subscribe_field_resolver: Optional[GraphQLFieldResolver] = None, + max_coercion_errors: int = 50, ) -> Union[AsyncIterable[Any], ExecutionResult]: """Create source event stream @@ -138,6 +141,7 @@ async def create_source_event_stream( variable_values, operation_name, subscribe_field_resolver=subscribe_field_resolver, + max_coercion_errors=max_coercion_errors, ) # Return early errors if execution context failed. diff --git a/src/graphql/execution/values.py b/src/graphql/execution/values.py index e11733fc..4628aaa0 100644 --- a/src/graphql/execution/values.py +++ b/src/graphql/execution/values.py @@ -2,11 +2,13 @@ from ..error import GraphQLError from ..language import ( + DirectiveDefinitionNode, + DirectiveExtensionNode, DirectiveNode, EnumValueDefinitionNode, ExecutableDefinitionNode, - FieldNode, FieldDefinitionNode, + FieldNode, InputValueDefinitionNode, NullValueNode, SchemaDefinitionNode, @@ -17,12 +19,13 @@ VariableNode, print_ast, ) -from ..pyutils import inspect, print_path_list, Undefined +from ..pyutils import Undefined, inspect, print_path_list from ..type import ( GraphQLDirective, GraphQLField, GraphQLInputType, GraphQLSchema, + is_input_object_type, is_input_type, is_non_null_type, ) @@ -121,7 +124,11 @@ def coerce_variable_values( continue def on_input_value_error( - path: List[Union[str, int]], invalid_value: Any, error: GraphQLError + path: List[Union[str, int]], + invalid_value: Any, + error: GraphQLError, + var_name: str = var_name, + var_def_node: VariableDefinitionNode = var_def_node, ) -> None: invalid_str = inspect(invalid_value) prefix = f"Variable '${var_name}' got invalid value {invalid_str}" @@ -131,7 +138,7 @@ def on_input_value_error( GraphQLError( prefix + "; " + error.message, var_def_node, - original_error=error.original_error, + original_error=error, ) ) @@ -160,8 +167,13 @@ def get_argument_values( argument_node = arg_node_map.get(name) if argument_node is None: - if arg_def.default_value is not Undefined: - coerced_values[arg_def.out_name or name] = arg_def.default_value + value = arg_def.default_value + if value is not Undefined: + if is_input_object_type(arg_def.type): + # coerce input value so that out_names are used + value = coerce_input_value(value, arg_def.type) + + coerced_values[arg_def.out_name or name] = value elif is_non_null_type(arg_type): # pragma: no cover else raise GraphQLError( f"Argument '{name}' of required type '{arg_type}'" @@ -176,8 +188,12 @@ def get_argument_values( if isinstance(value_node, VariableNode): variable_name = value_node.name.value if variable_values is None or variable_name not in variable_values: - if arg_def.default_value is not Undefined: - coerced_values[arg_def.out_name or name] = arg_def.default_value + value = arg_def.default_value + if value is not Undefined: + if is_input_object_type(arg_def.type): + # coerce input value so that out_names are used + value = coerce_input_value(value, arg_def.type) + coerced_values[arg_def.out_name or name] = value elif is_non_null_type(arg_type): # pragma: no cover else raise GraphQLError( f"Argument '{name}' of required type '{arg_type}'" @@ -186,7 +202,8 @@ def get_argument_values( value_node, ) continue # pragma: no cover - is_null = variable_values[variable_name] is None + variable_value = variable_values[variable_name] + is_null = variable_value is None or variable_value is Undefined if is_null and is_non_null_type(arg_type): raise GraphQLError( @@ -209,6 +226,8 @@ def get_argument_values( NodeWithDirective = Union[ + DirectiveDefinitionNode, + DirectiveExtensionNode, EnumValueDefinitionNode, ExecutableDefinitionNode, FieldDefinitionNode, diff --git a/src/graphql/graphql.py b/src/graphql/graphql.py index c2e804cd..c36919c7 100644 --- a/src/graphql/graphql.py +++ b/src/graphql/graphql.py @@ -1,10 +1,10 @@ from asyncio import ensure_future from inspect import isawaitable -from typing import Any, Awaitable, Callable, Dict, Optional, Union, Type, cast +from typing import Any, Callable, Dict, Optional, Type, Union from .error import GraphQLError -from .execution import execute, ExecutionResult, ExecutionContext, Middleware -from .language import parse, Source +from .execution import ExecutionContext, ExecutionResult, Middleware, execute +from .language import Source, parse from .pyutils import AwaitableOrValue from .type import ( GraphQLFieldResolver, @@ -90,9 +90,9 @@ async def graphql( ) if isawaitable(result): - return await cast(Awaitable[ExecutionResult], result) + return await result - return cast(ExecutionResult, result) + return result def assume_not_awaitable(_value: Any) -> bool: @@ -143,10 +143,10 @@ def graphql_sync( # Assert that the execution was synchronous. if isawaitable(result): - ensure_future(cast(Awaitable[ExecutionResult], result)).cancel() + ensure_future(result).cancel() raise RuntimeError("GraphQL execution failed to complete synchronously.") - return cast(ExecutionResult, result) + return result def graphql_impl( @@ -192,6 +192,7 @@ def graphql_impl( field_resolver, type_resolver, None, + 50, middleware, execution_context_class, is_awaitable, diff --git a/src/graphql/language/__init__.py b/src/graphql/language/__init__.py index 7d3120f5..4cf2c86d 100644 --- a/src/graphql/language/__init__.py +++ b/src/graphql/language/__init__.py @@ -14,7 +14,13 @@ from .lexer import Lexer -from .parser import parse, parse_type, parse_value, parse_const_value +from .parser import ( + parse, + parse_type, + parse_value, + parse_const_value, + parse_schema_coordinate, +) from .printer import print_ast @@ -94,6 +100,13 @@ UnionTypeExtensionNode, EnumTypeExtensionNode, InputObjectTypeExtensionNode, + DirectiveExtensionNode, + SchemaCoordinateNode, + TypeCoordinateNode, + MemberCoordinateNode, + ArgumentCoordinateNode, + DirectiveCoordinateNode, + DirectiveArgumentCoordinateNode, ) from .predicates import ( is_definition_node, @@ -106,6 +119,7 @@ is_type_definition_node, is_type_system_extension_node, is_type_extension_node, + is_schema_coordinate_node, ) from .directive_locations import DirectiveLocation @@ -121,6 +135,7 @@ "parse_value", "parse_const_value", "parse_type", + "parse_schema_coordinate", "print_ast", "Source", "visit", @@ -195,6 +210,13 @@ "UnionTypeExtensionNode", "EnumTypeExtensionNode", "InputObjectTypeExtensionNode", + "DirectiveExtensionNode", + "SchemaCoordinateNode", + "TypeCoordinateNode", + "MemberCoordinateNode", + "ArgumentCoordinateNode", + "DirectiveCoordinateNode", + "DirectiveArgumentCoordinateNode", "is_definition_node", "is_executable_definition_node", "is_selection_node", @@ -205,4 +227,5 @@ "is_type_definition_node", "is_type_system_extension_node", "is_type_extension_node", + "is_schema_coordinate_node", ] diff --git a/src/graphql/language/ast.py b/src/graphql/language/ast.py index a2470560..822a324e 100644 --- a/src/graphql/language/ast.py +++ b/src/graphql/language/ast.py @@ -69,6 +69,13 @@ "UnionTypeExtensionNode", "EnumTypeExtensionNode", "InputObjectTypeExtensionNode", + "DirectiveExtensionNode", + "SchemaCoordinateNode", + "TypeCoordinateNode", + "MemberCoordinateNode", + "ArgumentCoordinateNode", + "DirectiveCoordinateNode", + "DirectiveArgumentCoordinateNode", "QUERY_DOCUMENT_KEYS", ] @@ -242,12 +249,19 @@ class OperationType(Enum): "name": (), "document": ("definitions",), "operation_definition": ( + "description", "name", "variable_definitions", "directives", "selection_set", ), - "variable_definition": ("variable", "type", "default_value", "directives"), + "variable_definition": ( + "description", + "variable", + "type", + "default_value", + "directives", + ), "variable": ("name",), "selection_set": ("selections",), "field": ("alias", "name", "arguments", "directives", "selection_set"), @@ -255,6 +269,7 @@ class OperationType(Enum): "fragment_spread": ("name", "directives"), "inline_fragment": ("type_condition", "directives", "selection_set"), "fragment_definition": ( + "description", # Note: fragment variable definitions are deprecated and will be removed in v3.3 "name", "variable_definitions", @@ -298,14 +313,26 @@ class OperationType(Enum): "enum_type_definition": ("description", "name", "directives", "values"), "enum_value_definition": ("description", "name", "directives"), "input_object_type_definition": ("description", "name", "directives", "fields"), - "directive_definition": ("description", "name", "arguments", "locations"), + "directive_definition": ( + "description", + "name", + "arguments", + "directives", + "locations", + ), "schema_extension": ("directives", "operation_types"), + "directive_extension": ("name", "directives"), "scalar_type_extension": ("name", "directives"), "object_type_extension": ("name", "interfaces", "directives", "fields"), "interface_type_extension": ("name", "interfaces", "directives", "fields"), "union_type_extension": ("name", "directives", "types"), "enum_type_extension": ("name", "directives", "values"), "input_object_type_extension": ("name", "directives", "fields"), + "type_coordinate": ("name",), + "member_coordinate": ("name", "member_name"), + "argument_coordinate": ("name", "field_name", "argument_name"), + "directive_coordinate": ("name",), + "directive_argument_coordinate": ("name", "argument_name"), } @@ -411,6 +438,11 @@ class DocumentNode(Node): __slots__ = ("definitions",) definitions: Tuple["DefinitionNode", ...] + # The number of tokens in the parsed document. Set by the parser per instance + # and deliberately kept out of ``__slots__`` (and therefore out of ``keys``) so + # that it is not treated as a traversable attribute, the equivalent of the + # non-enumerable ``tokenCount`` property in graphql-js. + token_count: int = 0 class DefinitionNode(Node): @@ -418,8 +450,15 @@ class DefinitionNode(Node): class ExecutableDefinitionNode(DefinitionNode): - __slots__ = "name", "directives", "variable_definitions", "selection_set" + __slots__ = ( + "description", + "name", + "directives", + "variable_definitions", + "selection_set", + ) + description: Optional["StringValueNode"] name: Optional[NameNode] directives: Tuple["DirectiveNode", ...] variable_definitions: Tuple["VariableDefinitionNode", ...] @@ -433,8 +472,9 @@ class OperationDefinitionNode(ExecutableDefinitionNode): class VariableDefinitionNode(Node): - __slots__ = "variable", "type", "default_value", "directives" + __slots__ = "description", "variable", "type", "default_value", "directives" + description: Optional["StringValueNode"] variable: "VariableNode" type: "TypeNode" default_value: Optional["ConstValueNode"] @@ -739,11 +779,19 @@ class InputObjectTypeDefinitionNode(TypeDefinitionNode): class DirectiveDefinitionNode(TypeSystemDefinitionNode): - __slots__ = "description", "name", "arguments", "repeatable", "locations" + __slots__ = ( + "description", + "name", + "arguments", + "directives", + "repeatable", + "locations", + ) description: Optional[StringValueNode] name: NameNode arguments: Tuple[InputValueDefinitionNode, ...] + directives: Tuple[ConstDirectiveNode, ...] repeatable: bool locations: Tuple[NameNode, ...] @@ -758,6 +806,13 @@ class SchemaExtensionNode(Node): operation_types: Tuple[OperationTypeDefinitionNode, ...] +class DirectiveExtensionNode(Node): + __slots__ = "name", "directives" + + name: NameNode + directives: Tuple[ConstDirectiveNode, ...] + + # Type Extensions @@ -768,7 +823,9 @@ class TypeExtensionNode(TypeSystemDefinitionNode): directives: Tuple[ConstDirectiveNode, ...] -TypeSystemExtensionNode = Union[SchemaExtensionNode, TypeExtensionNode] +TypeSystemExtensionNode = Union[ + SchemaExtensionNode, TypeExtensionNode, DirectiveExtensionNode +] class ScalarTypeExtensionNode(TypeExtensionNode): @@ -805,3 +862,49 @@ class InputObjectTypeExtensionNode(TypeExtensionNode): __slots__ = ("fields",) fields: Tuple[InputValueDefinitionNode, ...] + + +# Schema Coordinates + + +class TypeCoordinateNode(Node): + __slots__ = ("name",) + + name: NameNode + + +class MemberCoordinateNode(Node): + __slots__ = "name", "member_name" + + name: NameNode + member_name: NameNode + + +class ArgumentCoordinateNode(Node): + __slots__ = "name", "field_name", "argument_name" + + name: NameNode + field_name: NameNode + argument_name: NameNode + + +class DirectiveCoordinateNode(Node): + __slots__ = ("name",) + + name: NameNode + + +class DirectiveArgumentCoordinateNode(Node): + __slots__ = "name", "argument_name" + + name: NameNode + argument_name: NameNode + + +SchemaCoordinateNode = Union[ + TypeCoordinateNode, + MemberCoordinateNode, + ArgumentCoordinateNode, + DirectiveCoordinateNode, + DirectiveArgumentCoordinateNode, +] diff --git a/src/graphql/language/directive_locations.py b/src/graphql/language/directive_locations.py index dfce34d9..a1e75236 100644 --- a/src/graphql/language/directive_locations.py +++ b/src/graphql/language/directive_locations.py @@ -28,3 +28,4 @@ class DirectiveLocation(Enum): ENUM_VALUE = "enum value" INPUT_OBJECT = "input object" INPUT_FIELD_DEFINITION = "input field definition" + DIRECTIVE_DEFINITION = "directive definition" diff --git a/src/graphql/language/lexer.py b/src/graphql/language/lexer.py index f41932bf..2cd02253 100644 --- a/src/graphql/language/lexer.py +++ b/src/graphql/language/lexer.py @@ -69,7 +69,7 @@ def print_code_point_at(self, location: int) -> str: return TokenKind.EOF.value char = body[location] # Printable ASCII - if "\x20" <= char <= "\x7E": + if "\x20" <= char <= "\x7e": return "'\"'" if char == '"' else f"'{char}'" # Unicode code point point = ord( @@ -461,6 +461,7 @@ def read_name(self, start: int) -> Token: TokenKind.AMP, TokenKind.PAREN_L, TokenKind.PAREN_R, + TokenKind.DOT, TokenKind.SPREAD, TokenKind.COLON, TokenKind.EQUALS, diff --git a/src/graphql/language/parser.py b/src/graphql/language/parser.py index 389913a5..f4845b9c 100644 --- a/src/graphql/language/parser.py +++ b/src/graphql/language/parser.py @@ -1,14 +1,19 @@ -from typing import Callable, Dict, List, Optional, Union, TypeVar, cast from functools import partial +from typing import Callable, Dict, List, Optional, TypeVar, Union, cast +from ..error import GraphQLError, GraphQLSyntaxError from .ast import ( + ArgumentCoordinateNode, ArgumentNode, BooleanValueNode, ConstArgumentNode, ConstDirectiveNode, ConstValueNode, DefinitionNode, + DirectiveArgumentCoordinateNode, + DirectiveCoordinateNode, DirectiveDefinitionNode, + DirectiveExtensionNode, DirectiveNode, DocumentNode, EnumTypeDefinitionNode, @@ -24,14 +29,15 @@ InputObjectTypeDefinitionNode, InputObjectTypeExtensionNode, InputValueDefinitionNode, - IntValueNode, InterfaceTypeDefinitionNode, InterfaceTypeExtensionNode, + IntValueNode, ListTypeNode, ListValueNode, Location, - NameNode, + MemberCoordinateNode, NamedTypeNode, + NameNode, NonNullTypeNode, NullValueNode, ObjectFieldNode, @@ -43,11 +49,14 @@ OperationTypeDefinitionNode, ScalarTypeDefinitionNode, ScalarTypeExtensionNode, + SchemaCoordinateNode, SchemaDefinitionNode, SchemaExtensionNode, SelectionNode, SelectionSetNode, StringValueNode, + Token, + TypeCoordinateNode, TypeNode, TypeSystemExtensionNode, UnionTypeDefinitionNode, @@ -57,13 +66,18 @@ VariableNode, ) from .directive_locations import DirectiveLocation -from .ast import Token from .lexer import Lexer, is_punctuator_token_kind +from .schema_coordinate_lexer import SchemaCoordinateLexer from .source import Source, is_source from .token_kind import TokenKind -from ..error import GraphQLError, GraphQLSyntaxError -__all__ = ["parse", "parse_type", "parse_value", "parse_const_value"] +__all__ = [ + "parse", + "parse_type", + "parse_value", + "parse_const_value", + "parse_schema_coordinate", +] T = TypeVar("T") @@ -73,15 +87,23 @@ def parse( source: SourceType, no_location: bool = False, + max_tokens: Optional[int] = None, allow_legacy_fragment_variables: bool = False, + experimental_directives_on_directive_definitions: bool = False, ) -> DocumentNode: """Given a GraphQL source, parse it into a Document. Throws GraphQLError if a syntax error is encountered. By default, the parser creates AST nodes that know the location in the source that - they correspond to. The ``no_location`` option disables that behavior for - performance or testing. + they correspond to. Setting the ``no_location`` parameter to False disables that + behavior for performance or testing. + + Parser CPU and memory usage is linear to the number of tokens in a document, + however in extreme cases it becomes quadratic due to memory exhaustion. + Parsing happens before validation, so even invalid queries can burn lots of + CPU time and memory. To prevent this, you can set a maximum number of tokens + allowed within a document using the ``max_tokens`` parameter. Legacy feature (will be removed in v3.3): @@ -91,16 +113,33 @@ def parse( :attr:`~graphql.language.FragmentDefinitionNode.variable_definitions` field of the :class:`~graphql.language.FragmentDefinitionNode`. + This legacy fragment variable syntax is deprecated. Move variable definitions to + operations for spec-compliant documents; if you need variables or arguments scoped + to fragments, the more complete experimental fragment-arguments feature in + graphql-core 3.3 should be used instead. + The syntax is identical to normal, query-defined variables. For example:: fragment A($var: Boolean = false) on T { ... } + + Experimental feature: + + If ``experimental_directives_on_directive_definitions`` is set to ``True``, the + parser will understand and parse directives on directive definitions. This syntax + is not part of the GraphQL specification and may change. For example:: + + directive @foo @bar on FIELD """ parser = Parser( source, no_location=no_location, + max_tokens=max_tokens, allow_legacy_fragment_variables=allow_legacy_fragment_variables, + experimental_directives_on_directive_definitions=( + experimental_directives_on_directive_definitions + ), ) return parser.parse_document() @@ -108,7 +147,9 @@ def parse( def parse_value( source: SourceType, no_location: bool = False, + max_tokens: Optional[int] = None, allow_legacy_fragment_variables: bool = False, + experimental_directives_on_directive_definitions: bool = False, ) -> ValueNode: """Parse the AST for a given string containing a GraphQL value. @@ -123,7 +164,11 @@ def parse_value( parser = Parser( source, no_location=no_location, + max_tokens=max_tokens, allow_legacy_fragment_variables=allow_legacy_fragment_variables, + experimental_directives_on_directive_definitions=( + experimental_directives_on_directive_definitions + ), ) parser.expect_token(TokenKind.SOF) value = parser.parse_value_literal(False) @@ -134,7 +179,9 @@ def parse_value( def parse_const_value( source: SourceType, no_location: bool = False, + max_tokens: Optional[int] = None, allow_legacy_fragment_variables: bool = False, + experimental_directives_on_directive_definitions: bool = False, ) -> ConstValueNode: """Parse the AST for a given string containing a GraphQL constant value. @@ -144,7 +191,11 @@ def parse_const_value( parser = Parser( source, no_location=no_location, + max_tokens=max_tokens, allow_legacy_fragment_variables=allow_legacy_fragment_variables, + experimental_directives_on_directive_definitions=( + experimental_directives_on_directive_definitions + ), ) parser.expect_token(TokenKind.SOF) value = parser.parse_const_value_literal() @@ -155,7 +206,9 @@ def parse_const_value( def parse_type( source: SourceType, no_location: bool = False, + max_tokens: Optional[int] = None, allow_legacy_fragment_variables: bool = False, + experimental_directives_on_directive_definitions: bool = False, ) -> TypeNode: """Parse the AST for a given string containing a GraphQL Type. @@ -170,7 +223,11 @@ def parse_type( parser = Parser( source, no_location=no_location, + max_tokens=max_tokens, allow_legacy_fragment_variables=allow_legacy_fragment_variables, + experimental_directives_on_directive_definitions=( + experimental_directives_on_directive_definitions + ), ) parser.expect_token(TokenKind.SOF) type_ = parser.parse_type_reference() @@ -178,6 +235,32 @@ def parse_type( return type_ +def parse_schema_coordinate( + source: SourceType, + no_location: bool = False, + max_tokens: Optional[int] = None, +) -> SchemaCoordinateNode: + """Parse the AST for a given string containing a GraphQL schema coordinate. + + Throws GraphQLError if a syntax error is encountered. + + This is useful within tools that operate upon GraphQL schema coordinates + (ex. ``Type.field``) directly and in isolation of complete GraphQL documents. + + Consider providing the results to the utility function: + :func:`~graphql.utilities.resolve_ast_schema_coordinate`. Or calling + :func:`~graphql.utilities.resolve_schema_coordinate` directly with an + unparsed source. + """ + source = cast(Source, source) if is_source(source) else Source(cast(str, source)) + lexer = SchemaCoordinateLexer(source) + parser = Parser(source, no_location=no_location, max_tokens=max_tokens, lexer=lexer) + parser.expect_token(TokenKind.SOF) + coordinate = parser.parse_schema_coordinate() + parser.expect_token(TokenKind.EOF) + return coordinate + + class Parser: """GraphQL AST parser. @@ -191,22 +274,35 @@ class Parser: """ _lexer: Lexer - _no_Location: bool + _no_location: bool + _max_tokens: Optional[int] _allow_legacy_fragment_variables: bool + _experimental_directives_on_directive_definitions: bool + _token_counter: int def __init__( self, source: SourceType, no_location: bool = False, + max_tokens: Optional[int] = None, allow_legacy_fragment_variables: bool = False, + experimental_directives_on_directive_definitions: bool = False, + lexer: Optional[Lexer] = None, ): source = ( cast(Source, source) if is_source(source) else Source(cast(str, source)) ) - self._lexer = Lexer(source) + # You may override the lexer used to lex the source; this is used by schema + # coordinates to introduce a lexer with a restricted syntax. + self._lexer = lexer if lexer is not None else Lexer(source) self._no_location = no_location + self._max_tokens = max_tokens self._allow_legacy_fragment_variables = allow_legacy_fragment_variables + self._experimental_directives_on_directive_definitions = ( + experimental_directives_on_directive_definitions + ) + self._token_counter = 0 def parse_name(self) -> NameNode: """Convert a name lex token into a name parse node.""" @@ -215,13 +311,21 @@ def parse_name(self) -> NameNode: # Implement the parsing rules in the Document section. + @property + def token_count(self) -> int: + """Get the number of tokens that have been parsed so far.""" + return self._token_counter + def parse_document(self) -> DocumentNode: """Document: Definition+""" start = self._lexer.token - return DocumentNode( + document = DocumentNode( definitions=self.many(TokenKind.SOF, self.parse_definition, TokenKind.EOF), loc=self.loc(start), ) + # Expose the token count as a (non-traversable) attribute on the document. + document.token_count = self.token_count + return document _parse_type_system_definition_method_names: Dict[str, str] = { "schema": "schema_definition", @@ -234,9 +338,12 @@ def parse_document(self) -> DocumentNode: "directive": "directive_definition", } - _parse_other_definition_method_names: Dict[str, str] = { + _parse_executable_definition_method_names: Dict[str, str] = { **dict.fromkeys(("query", "mutation", "subscription"), "operation_definition"), "fragment": "fragment_definition", + } + + _parse_other_definition_method_names: Dict[str, str] = { "extend": "type_system_extension", } @@ -260,6 +367,14 @@ def parse_definition(self) -> DefinitionNode: self._lexer.lookahead() if has_description else self._lexer.token ) + if has_description and keyword_token.kind is TokenKind.BRACE_L: + raise GraphQLSyntaxError( + self._lexer.source, + self._lexer.token.start, + "Unexpected description," + " descriptions are not supported on shorthand queries.", + ) + if keyword_token.kind is TokenKind.NAME: token_name = cast(str, keyword_token.value) method_name = self._parse_type_system_definition_method_names.get( @@ -268,12 +383,16 @@ def parse_definition(self) -> DefinitionNode: if method_name: return getattr(self, f"parse_{method_name}")() + method_name = self._parse_executable_definition_method_names.get(token_name) + if method_name: + return getattr(self, f"parse_{method_name}")() + if has_description: raise GraphQLSyntaxError( self._lexer.source, self._lexer.token.start, "Unexpected description," - " descriptions are supported only on type definitions.", + " only GraphQL definitions support descriptions.", ) method_name = self._parse_other_definition_method_names.get(token_name) @@ -290,16 +409,19 @@ def parse_operation_definition(self) -> OperationDefinitionNode: if self.peek(TokenKind.BRACE_L): return OperationDefinitionNode( operation=OperationType.QUERY, + description=None, name=None, variable_definitions=[], directives=[], selection_set=self.parse_selection_set(), loc=self.loc(start), ) + description = self.parse_description() operation = self.parse_operation_type() name = self.parse_name() if self.peek(TokenKind.NAME) else None return OperationDefinitionNode( operation=operation, + description=description, name=name, variable_definitions=self.parse_variable_definitions(), directives=self.parse_directives(False), @@ -325,11 +447,14 @@ def parse_variable_definition(self) -> VariableDefinitionNode: """VariableDefinition: Variable: Type DefaultValue? Directives[Const]?""" start = self._lexer.token return VariableDefinitionNode( + description=self.parse_description(), variable=self.parse_variable(), type=self.expect_token(TokenKind.COLON) and self.parse_type_reference(), - default_value=self.parse_const_value_literal() - if self.expect_optional_token(TokenKind.EQUALS) - else None, + default_value=( + self.parse_const_value_literal() + if self.expect_optional_token(TokenKind.EQUALS) + else None + ), directives=self.parse_const_directives(), loc=self.loc(start), ) @@ -371,17 +496,18 @@ def parse_field(self) -> FieldNode: name=name, arguments=self.parse_arguments(False), directives=self.parse_directives(False), - selection_set=self.parse_selection_set() - if self.peek(TokenKind.BRACE_L) - else None, + selection_set=( + self.parse_selection_set() if self.peek(TokenKind.BRACE_L) else None + ), loc=self.loc(start), ) def parse_arguments(self, is_const: bool) -> List[ArgumentNode]: """Arguments[Const]: (Argument[?Const]+)""" item = self.parse_const_argument if is_const else self.parse_argument - item = cast(Callable[[], ArgumentNode], item) - return self.optional_many(TokenKind.PAREN_L, item, TokenKind.PAREN_R) + return self.optional_many( + TokenKind.PAREN_L, cast(Callable[[], ArgumentNode], item), TokenKind.PAREN_R + ) def parse_argument(self, is_const: bool = False) -> ArgumentNode: """Argument[Const]: Name : Value[?Const]""" @@ -425,11 +551,13 @@ def parse_fragment(self) -> Union[FragmentSpreadNode, InlineFragmentNode]: def parse_fragment_definition(self) -> FragmentDefinitionNode: """FragmentDefinition""" start = self._lexer.token + description = self.parse_description() self.expect_keyword("fragment") # Legacy support for defining variables within fragments changes # the grammar of FragmentDefinition if self._allow_legacy_fragment_variables: return FragmentDefinitionNode( + description=description, name=self.parse_fragment_name(), variable_definitions=self.parse_variable_definitions(), type_condition=self.parse_type_condition(), @@ -438,6 +566,7 @@ def parse_fragment_definition(self) -> FragmentDefinitionNode: loc=self.loc(start), ) return FragmentDefinitionNode( + description=description, name=self.parse_fragment_name(), type_condition=self.parse_type_condition(), directives=self.parse_directives(False), @@ -477,7 +606,7 @@ def parse_value_literal(self, is_const: bool) -> ValueNode: def parse_string_literal(self, _is_const: bool = False) -> StringValueNode: token = self._lexer.token - self._lexer.advance() + self.advance_lexer() return StringValueNode( value=token.value, block=token.kind == TokenKind.BLOCK_STRING, @@ -514,18 +643,18 @@ def parse_object(self, is_const: bool) -> ObjectValueNode: def parse_int(self, _is_const: bool = False) -> IntValueNode: token = self._lexer.token - self._lexer.advance() + self.advance_lexer() return IntValueNode(value=token.value, loc=self.loc(token)) def parse_float(self, _is_const: bool = False) -> FloatValueNode: token = self._lexer.token - self._lexer.advance() + self.advance_lexer() return FloatValueNode(value=token.value, loc=self.loc(token)) def parse_named_values(self, _is_const: bool = False) -> ValueNode: token = self._lexer.token value = token.value - self._lexer.advance() + self.advance_lexer() if value == "true": return BooleanValueNode(value=True, loc=self.loc(token)) if value == "false": @@ -616,6 +745,11 @@ def parse_type_system_extension(self) -> TypeSystemExtensionNode: ) if method_name: # pragma: no cover return getattr(self, f"parse_{method_name}")() + if ( + keyword_token.value == "directive" + and self._experimental_directives_on_directive_definitions + ): + return self.parse_directive_definition_extension() raise self.unexpected(keyword_token) def peek_description(self) -> bool: @@ -963,6 +1097,22 @@ def parse_input_object_type_extension(self) -> InputObjectTypeExtensionNode: name=name, directives=directives, fields=fields, loc=self.loc(start) ) + def parse_directive_definition_extension(self) -> DirectiveExtensionNode: + """DirectiveDefinitionExtension""" + start = self._lexer.token + self.expect_keyword("extend") + self.expect_keyword("directive") + self.expect_token(TokenKind.AT) + name = self.parse_name() + directives = self.parse_const_directives() + if not directives: + raise self.unexpected() + return DirectiveExtensionNode( + name=name, + directives=directives, + loc=self.loc(start), + ) + def parse_directive_definition(self) -> DirectiveDefinitionNode: """DirectiveDefinition""" start = self._lexer.token @@ -971,6 +1121,11 @@ def parse_directive_definition(self) -> DirectiveDefinitionNode: self.expect_token(TokenKind.AT) name = self.parse_name() args = self.parse_argument_defs() + directives = ( + self.parse_const_directives() + if self._experimental_directives_on_directive_definitions + else [] + ) repeatable = self.expect_optional_keyword("repeatable") self.expect_keyword("on") locations = self.parse_directive_locations() @@ -978,6 +1133,7 @@ def parse_directive_definition(self) -> DirectiveDefinitionNode: description=description, name=name, arguments=args, + directives=directives, repeatable=repeatable, locations=locations, loc=self.loc(start), @@ -995,6 +1151,50 @@ def parse_directive_location(self) -> NameNode: return name raise self.unexpected(start) + # Schema Coordinates + + def parse_schema_coordinate(self) -> SchemaCoordinateNode: + """SchemaCoordinate + + - Name + - Name . Name + - Name . Name ( Name : ) + - @ Name + - @ Name ( Name : ) + """ + start = self._lexer.token + of_directive = self.expect_optional_token(TokenKind.AT) + name = self.parse_name() + member_name: Optional[NameNode] = None + if not of_directive and self.expect_optional_token(TokenKind.DOT): + member_name = self.parse_name() + argument_name: Optional[NameNode] = None + if (of_directive or member_name) and self.expect_optional_token( + TokenKind.PAREN_L + ): + argument_name = self.parse_name() + self.expect_token(TokenKind.COLON) + self.expect_token(TokenKind.PAREN_R) + + if of_directive: + if argument_name: + return DirectiveArgumentCoordinateNode( + name=name, argument_name=argument_name, loc=self.loc(start) + ) + return DirectiveCoordinateNode(name=name, loc=self.loc(start)) + if member_name: + if argument_name: + return ArgumentCoordinateNode( + name=name, + field_name=member_name, + argument_name=argument_name, + loc=self.loc(start), + ) + return MemberCoordinateNode( + name=name, member_name=member_name, loc=self.loc(start) + ) + return TypeCoordinateNode(name=name, loc=self.loc(start)) + # Core parsing utility functions def loc(self, start_token: Token) -> Optional[Location]: @@ -1020,7 +1220,7 @@ def expect_token(self, kind: TokenKind) -> Token: """ token = self._lexer.token if token.kind == kind: - self._lexer.advance() + self.advance_lexer() return token raise GraphQLSyntaxError( @@ -1037,7 +1237,7 @@ def expect_optional_token(self, kind: TokenKind) -> bool: """ token = self._lexer.token if token.kind == kind: - self._lexer.advance() + self.advance_lexer() return True return False @@ -1050,7 +1250,7 @@ def expect_keyword(self, value: str) -> None: """ token = self._lexer.token if token.kind == TokenKind.NAME and token.value == value: - self._lexer.advance() + self.advance_lexer() else: raise GraphQLSyntaxError( self._lexer.source, @@ -1066,7 +1266,7 @@ def expect_optional_keyword(self, value: str) -> bool: """ token = self._lexer.token if token.kind == TokenKind.NAME and token.value == value: - self._lexer.advance() + self.advance_lexer() return True return False @@ -1154,6 +1354,20 @@ def delimited_many( break return nodes + def advance_lexer(self) -> None: + token = self._lexer.advance() + + if token.kind != TokenKind.EOF: + self._token_counter += 1 + max_tokens = self._max_tokens + if max_tokens is not None and self._token_counter > max_tokens: + raise GraphQLSyntaxError( + self._lexer.source, + token.start, + f"Document contains more than {max_tokens} tokens." + " Parsing aborted.", + ) + def get_token_desc(token: Token) -> str: """Describe a token as a string for debugging.""" diff --git a/src/graphql/language/predicates.py b/src/graphql/language/predicates.py index 24d7c7a5..e29b557b 100644 --- a/src/graphql/language/predicates.py +++ b/src/graphql/language/predicates.py @@ -1,11 +1,17 @@ from .ast import ( + ArgumentCoordinateNode, + DirectiveArgumentCoordinateNode, + DirectiveCoordinateNode, + MemberCoordinateNode, Node, DefinitionNode, + DirectiveExtensionNode, ExecutableDefinitionNode, ListValueNode, ObjectValueNode, SchemaExtensionNode, SelectionNode, + TypeCoordinateNode, TypeDefinitionNode, TypeExtensionNode, TypeNode, @@ -25,6 +31,7 @@ "is_type_definition_node", "is_type_system_extension_node", "is_type_extension_node", + "is_schema_coordinate_node", ] @@ -53,9 +60,11 @@ def is_const_value_node(node: Node) -> bool: return is_value_node(node) and ( any(is_const_value_node(value) for value in node.values) if isinstance(node, ListValueNode) - else any(is_const_value_node(field.value) for field in node.fields) - if isinstance(node, ObjectValueNode) - else not isinstance(node, VariableNode) + else ( + any(is_const_value_node(field.value) for field in node.fields) + if isinstance(node, ObjectValueNode) + else not isinstance(node, VariableNode) + ) ) @@ -76,9 +85,25 @@ def is_type_definition_node(node: Node) -> bool: def is_type_system_extension_node(node: Node) -> bool: """Check whether the given node represents a type system extension.""" - return isinstance(node, (SchemaExtensionNode, TypeExtensionNode)) + return isinstance( + node, (SchemaExtensionNode, DirectiveExtensionNode, TypeExtensionNode) + ) def is_type_extension_node(node: Node) -> bool: """Check whether the given node represents a type extension.""" return isinstance(node, TypeExtensionNode) + + +def is_schema_coordinate_node(node: Node) -> bool: + """Check whether the given node represents a schema coordinate.""" + return isinstance( + node, + ( + TypeCoordinateNode, + MemberCoordinateNode, + ArgumentCoordinateNode, + DirectiveCoordinateNode, + DirectiveArgumentCoordinateNode, + ), + ) diff --git a/src/graphql/language/print_location.py b/src/graphql/language/print_location.py index 6d13b1e1..ef0b5cfa 100644 --- a/src/graphql/language/print_location.py +++ b/src/graphql/language/print_location.py @@ -5,7 +5,6 @@ from .location import SourceLocation, get_location from .source import Source - __all__ = ["print_location", "print_source_location"] @@ -48,9 +47,11 @@ def print_source_location(source: Source, source_location: SourceLocation) -> st ("|", "^".rjust(sub_line_column_num)), ( "|", - sub_lines[sub_line_index + 1] - if sub_line_index < len(sub_lines) - 1 - else None, + ( + sub_lines[sub_line_index + 1] + if sub_line_index < len(sub_lines) - 1 + else None + ), ), ) diff --git a/src/graphql/language/printer.py b/src/graphql/language/printer.py index 07ed1b15..1512f66a 100644 --- a/src/graphql/language/printer.py +++ b/src/graphql/language/printer.py @@ -17,15 +17,18 @@ class PrintedNode: """A union type for all nodes that have been processed by the printer.""" alias: str + argument_name: str arguments: Strings block: bool default_value: str definitions: Strings description: str directives: str + field_name: str fields: Strings interfaces: Strings locations: Strings + member_name: str name: str operation: OperationType operation_types: Strings @@ -66,8 +69,12 @@ def leave_document(node: PrintedNode, *_args: Any) -> str: @staticmethod def leave_operation_definition(node: PrintedNode, *_args: Any) -> str: - var_defs = wrap("(", join(node.variable_definitions, ", "), ")") - prefix = join( + var_defs = ( + wrap("(\n", join(node.variable_definitions, "\n"), "\n)") + if has_multiline_items(node.variable_definitions) + else wrap("(", join(node.variable_definitions, ", "), ")") + ) + prefix = wrap("", node.description, "\n") + join( ( node.operation.value, join((node.name, var_defs)), @@ -82,7 +89,7 @@ def leave_operation_definition(node: PrintedNode, *_args: Any) -> str: @staticmethod def leave_variable_definition(node: PrintedNode, *_args: Any) -> str: return ( - f"{node.variable}: {node.type}" + wrap("", node.description, "\n") + f"{node.variable}: {node.type}" f"{wrap(' = ', node.default_value)}" f"{wrap(' ', join(node.directives, ' '))}" ) @@ -127,7 +134,7 @@ def leave_inline_fragment(node: PrintedNode, *_args: Any) -> str: def leave_fragment_definition(node: PrintedNode, *_args: Any) -> str: # Note: fragment variable definitions are deprecated and will be removed in v3.3 return ( - f"fragment {node.name}" + wrap("", node.description, "\n") + f"fragment {node.name}" f"{wrap('(', join(node.variable_definitions, ', '), ')')}" f" on {node.type_condition}" f" {wrap('', join(node.directives, ' '), ' ')}" @@ -311,11 +318,12 @@ def leave_directive_definition(node: PrintedNode, *_args: Any) -> str: if has_multiline_items(args) else wrap("(", join(args, ", "), ")") ) + directives = wrap(" ", join(node.directives, " ")) repeatable = " repeatable" if node.repeatable else "" locations = join(node.locations, " | ") return ( wrap("", node.description, "\n") - + f"directive @{node.name}{args}{repeatable} on {locations}" + + f"directive @{node.name}{args}{directives}{repeatable} on {locations}" ) @staticmethod @@ -325,6 +333,10 @@ def leave_schema_extension(node: PrintedNode, *_args: Any) -> str: " ", ) + @staticmethod + def leave_directive_extension(node: PrintedNode, *_args: Any) -> str: + return join((f"extend directive @{node.name}", join(node.directives, " ")), " ") + @staticmethod def leave_scalar_type_extension(node: PrintedNode, *_args: Any) -> str: return join(("extend scalar", node.name, join(node.directives, " ")), " ") @@ -381,6 +393,30 @@ def leave_input_object_type_extension(node: PrintedNode, *_args: Any) -> str: " ", ) + # Schema Coordinates + + @staticmethod + def leave_type_coordinate(node: PrintedNode, *_args: Any) -> str: + return node.name + + @staticmethod + def leave_member_coordinate(node: PrintedNode, *_args: Any) -> str: + return join((node.name, wrap(".", node.member_name))) + + @staticmethod + def leave_argument_coordinate(node: PrintedNode, *_args: Any) -> str: + return join( + (node.name, wrap(".", node.field_name), wrap("(", node.argument_name, ":)")) + ) + + @staticmethod + def leave_directive_coordinate(node: PrintedNode, *_args: Any) -> str: + return f"@{node.name}" + + @staticmethod + def leave_directive_argument_coordinate(node: PrintedNode, *_args: Any) -> str: + return f"@{node.name}{wrap('(', node.argument_name, ':)')}" + def join(strings: Optional[Strings], separator: str = "") -> str: """Join strings in a given collection. diff --git a/src/graphql/language/schema_coordinate_lexer.py b/src/graphql/language/schema_coordinate_lexer.py new file mode 100644 index 00000000..030217c9 --- /dev/null +++ b/src/graphql/language/schema_coordinate_lexer.py @@ -0,0 +1,58 @@ +from ..error import GraphQLSyntaxError +from .ast import Token +from .character_classes import is_name_start +from .lexer import Lexer +from .token_kind import TokenKind + +__all__ = ["SchemaCoordinateLexer"] + + +_KIND_FOR_PUNCT = { + ".": TokenKind.DOT, + "(": TokenKind.PAREN_L, + ")": TokenKind.PAREN_R, + ":": TokenKind.COLON, + "@": TokenKind.AT, +} + + +class SchemaCoordinateLexer(Lexer): + """GraphQL Schema Coordinate Lexer + + A SchemaCoordinateLexer is a stateful stream generator in that every time it is + advanced, it returns the next token in the Source. Assuming the source lexes, the + final Token emitted by the lexer will be of kind EOF, after which the lexer will + repeatedly return the same EOF token whenever called. + + Unlike the regular Lexer, this lexer uses a restricted syntax that does not allow + any ignored tokens (such as whitespace or comments). Since a schema coordinate may + not contain a newline, the line is always 1 and the line start is always 0. + """ + + def read_next_token(self, start: int) -> Token: + """Get the next token from the source starting at the given position. + + This lexes punctuators and names only, raising a syntax error on any other + character (including ignored tokens such as whitespace and comments). + """ + body = self.source.body + body_length = len(body) + position = start + + if position < body_length: + char = body[position] + + kind = _KIND_FOR_PUNCT.get(char) + if kind: + return self.create_token(kind, position, position + 1) + + if is_name_start(char): + return self.read_name(position) + + raise GraphQLSyntaxError( + self.source, + position, + f"Invalid character: {self.print_code_point_at(position)}.", + ) + + return self.create_token(TokenKind.EOF, body_length, body_length) diff --git a/src/graphql/language/token_kind.py b/src/graphql/language/token_kind.py index 45f6e82a..4383fac5 100644 --- a/src/graphql/language/token_kind.py +++ b/src/graphql/language/token_kind.py @@ -13,6 +13,7 @@ class TokenKind(Enum): AMP = "&" PAREN_L = "(" PAREN_R = ")" + DOT = "." SPREAD = "..." COLON = ":" EQUALS = "=" diff --git a/src/graphql/language/visitor.py b/src/graphql/language/visitor.py index 996c7194..8195cacd 100644 --- a/src/graphql/language/visitor.py +++ b/src/graphql/language/visitor.py @@ -14,8 +14,7 @@ from ..pyutils import inspect, snake_to_camel from . import ast - -from .ast import Node, QUERY_DOCUMENT_KEYS +from .ast import QUERY_DOCUMENT_KEYS, Node __all__ = [ "Visitor", @@ -150,6 +149,10 @@ def get_visit_fn( ) -> Optional[Callable[..., Optional[VisitorAction]]]: """Get the visit function for the given node kind and direction. + This deprecated compatibility helper delegates to + ``get_enter_leave_for_kind``; call ``get_enter_leave_for_kind`` directly + because ``get_visit_fn`` will be removed in v3.3. + .. deprecated:: 3.2 Please use ``get_enter_leave_for_kind`` instead. Will be removed in v3.3. """ @@ -288,7 +291,7 @@ def visit( else: stack = Stack(in_array, idx, keys, edits, stack) in_array = isinstance(node, tuple) - keys = node if in_array else visitor_keys.get(node.kind, ()) + keys = node if in_array else visitor_keys.get(node.kind, ()) # type: ignore idx = -1 edits = [] if parent: diff --git a/src/graphql/pyutils/description.py b/src/graphql/pyutils/description.py index ccb858ea..ba1f1869 100644 --- a/src/graphql/pyutils/description.py +++ b/src/graphql/pyutils/description.py @@ -1,4 +1,4 @@ -from typing import Any, Tuple, Union +from typing import Any, Tuple, Type, Union __all__ = [ "Description", @@ -17,14 +17,14 @@ class Description: If you register(object), any object will be allowed as description. """ - bases: Union[type, Tuple[type, ...]] = str + bases: Union[Type[Any], Tuple[Type[Any], ...]] = str @classmethod def isinstance(cls, obj: Any) -> bool: return isinstance(obj, cls.bases) @classmethod - def register(cls, base: type) -> None: + def register(cls, base: Type[Any]) -> None: """Register a class that shall be accepted as a description.""" if not isinstance(base, type): raise TypeError("Only types can be registered.") @@ -39,12 +39,12 @@ def register(cls, base: type) -> None: cls.bases += (base,) @classmethod - def unregister(cls, base: type) -> None: + def unregister(cls, base: Type[Any]) -> None: """Unregister a class that shall no more be accepted as a description.""" if not isinstance(base, type): raise TypeError("Only types can be unregistered.") if isinstance(cls.bases, tuple): - if base in cls.bases: + if base in cls.bases: # pragma: no branch cls.bases = tuple(b for b in cls.bases if b is not base) if not cls.bases: cls.bases = object diff --git a/src/graphql/pyutils/frozen_dict.py b/src/graphql/pyutils/frozen_dict.py index 93283596..bf0770a7 100644 --- a/src/graphql/pyutils/frozen_dict.py +++ b/src/graphql/pyutils/frozen_dict.py @@ -6,7 +6,7 @@ __all__ = ["FrozenDict"] KT = TypeVar("KT") -VT = TypeVar("VT", covariant=True) +VT = TypeVar("VT") class FrozenDict(Dict[KT, VT]): @@ -25,7 +25,7 @@ def __setitem__(self, key, value): def __iadd__(self, value): raise FrozenError - def __hash__(self): + def __hash__(self) -> int: # type: ignore return hash(tuple(self.items())) def __copy__(self) -> "FrozenDict": diff --git a/src/graphql/pyutils/frozen_list.py b/src/graphql/pyutils/frozen_list.py index 01ead7c4..1e6c6a13 100644 --- a/src/graphql/pyutils/frozen_list.py +++ b/src/graphql/pyutils/frozen_list.py @@ -6,7 +6,7 @@ __all__ = ["FrozenList"] -T = TypeVar("T", covariant=True) +T = TypeVar("T") class FrozenList(List[T]): @@ -36,7 +36,7 @@ def __mul__(self, value): def __imul__(self, value): raise FrozenError - def __hash__(self): + def __hash__(self) -> int: # type: ignore return hash(tuple(self)) def __copy__(self) -> "FrozenList": diff --git a/src/graphql/pyutils/inspect.py b/src/graphql/pyutils/inspect.py index 8fc99dce..d3470fec 100644 --- a/src/graphql/pyutils/inspect.py +++ b/src/graphql/pyutils/inspect.py @@ -62,11 +62,13 @@ def inspect_recursive(value: Any, seen_values: List) -> str: items = trunc_list(items) if isinstance(value, dict): s = ", ".join( - "..." - if v is ELLIPSIS - else inspect_recursive(v[0], seen_values) - + ": " - + inspect_recursive(v[1], seen_values) + ( + "..." + if v is ELLIPSIS + else inspect_recursive(v[0], seen_values) + + ": " + + inspect_recursive(v[1], seen_values) + ) for v in items ) else: diff --git a/src/graphql/pyutils/is_iterable.py b/src/graphql/pyutils/is_iterable.py index cffbc51c..142c3a14 100644 --- a/src/graphql/pyutils/is_iterable.py +++ b/src/graphql/pyutils/is_iterable.py @@ -1,12 +1,5 @@ -from typing import ( - Any, - ByteString, - Collection, - Iterable, - Mapping, - Text, - ValuesView, -) +from collections.abc import Collection, Iterable, Mapping, ValuesView +from typing import Any __all__ = ["is_collection", "is_iterable"] @@ -14,7 +7,7 @@ if not isinstance({}.values(), Collection): # Python < 3.7.2 collection_types = (Collection, ValuesView) iterable_types: Any = Iterable -not_iterable_types: Any = (ByteString, Mapping, Text) +not_iterable_types: Any = (bytes, bytearray, memoryview, str, Mapping) def is_collection(value: Any) -> bool: diff --git a/src/graphql/pyutils/undefined.py b/src/graphql/pyutils/undefined.py index 73dc5314..49a111eb 100644 --- a/src/graphql/pyutils/undefined.py +++ b/src/graphql/pyutils/undefined.py @@ -1,4 +1,5 @@ -from typing import Any +import warnings +from typing import Any, Optional __all__ = ["Undefined", "UndefinedType"] @@ -6,6 +7,18 @@ class UndefinedType(ValueError): """Auxiliary class for creating the Undefined singleton.""" + _instance: Optional["UndefinedType"] = None + + def __new__(cls) -> "UndefinedType": + if cls._instance is None: + cls._instance = super().__new__(cls) + else: + warnings.warn("Redefinition of 'Undefined'", RuntimeWarning, stacklevel=2) + return cls._instance + + def __reduce__(self) -> str: + return "Undefined" + def __repr__(self) -> str: return "Undefined" diff --git a/src/graphql/type/__init__.py b/src/graphql/type/__init__.py index 6a86c0f7..66a8ee7f 100644 --- a/src/graphql/type/__init__.py +++ b/src/graphql/type/__init__.py @@ -92,6 +92,7 @@ GraphQLArgumentMap, GraphQLEnumValue, GraphQLEnumValueMap, + GraphQLEnumValuesDefinition, GraphQLField, GraphQLFieldMap, GraphQLInputField, @@ -132,6 +133,7 @@ GraphQLSkipDirective, GraphQLDeprecatedDirective, GraphQLSpecifiedByDirective, + GraphQLOneOfDirective, # Keyword Args GraphQLDirectiveKwargs, # Constant Deprecation Reason @@ -244,6 +246,7 @@ "GraphQLArgumentMap", "GraphQLEnumValue", "GraphQLEnumValueMap", + "GraphQLEnumValuesDefinition", "GraphQLField", "GraphQLFieldMap", "GraphQLInputField", @@ -276,6 +279,7 @@ "GraphQLSkipDirective", "GraphQLDeprecatedDirective", "GraphQLSpecifiedByDirective", + "GraphQLOneOfDirective", "GraphQLDirectiveKwargs", "DEFAULT_DEPRECATION_REASON", "is_specified_scalar_type", diff --git a/src/graphql/type/definition.py b/src/graphql/type/definition.py index dbe03ada..507a0394 100644 --- a/src/graphql/type/definition.py +++ b/src/graphql/type/definition.py @@ -1,5 +1,6 @@ from enum import Enum from typing import ( + TYPE_CHECKING, Any, Callable, Collection, @@ -10,7 +11,6 @@ NamedTuple, Optional, Tuple, - TYPE_CHECKING, Type, TypeVar, Union, @@ -21,8 +21,8 @@ from ..error import GraphQLError from ..language import ( EnumTypeDefinitionNode, - EnumValueDefinitionNode, EnumTypeExtensionNode, + EnumValueDefinitionNode, EnumValueNode, FieldDefinitionNode, FieldNode, @@ -47,16 +47,16 @@ from ..pyutils import ( AwaitableOrValue, Path, + Undefined, cached_property, did_you_mean, inspect, is_collection, is_description, suggestion_list, - Undefined, ) from ..utilities.value_from_ast_untyped import value_from_ast_untyped -from .assert_name import assert_name, assert_enum_value_name +from .assert_name import assert_enum_value_name, assert_name try: from typing import TypedDict @@ -116,6 +116,7 @@ "GraphQLEnumValue", "GraphQLEnumValueKwargs", "GraphQLEnumValueMap", + "GraphQLEnumValuesDefinition", "GraphQLField", "GraphQLFieldKwargs", "GraphQLFieldMap", @@ -228,6 +229,23 @@ class GraphQLNamedType(GraphQLType): ast_node: Optional[TypeDefinitionNode] extension_ast_nodes: Tuple[TypeExtensionNode, ...] + reserved_types: Dict[str, "GraphQLNamedType"] = {} + + def __new__(cls, name: str, *_args: Any, **_kwargs: Any) -> "GraphQLNamedType": + if name in cls.reserved_types: + raise TypeError(f"Redefinition of reserved type {name!r}") + return super().__new__(cls) + + def __reduce__(self) -> Tuple[Callable, Tuple]: + return self._get_instance, (self.name, tuple(self.to_kwargs().items())) + + @classmethod + def _get_instance(cls, name: str, args: Tuple) -> "GraphQLNamedType": + try: + return cls.reserved_types[name] + except KeyError: + return cls(**dict(args)) + def __init__( self, name: str, @@ -438,16 +456,22 @@ def to_kwargs(self) -> GraphQLScalarTypeKwargs: # noinspection PyArgumentList return GraphQLScalarTypeKwargs( # type: ignore super().to_kwargs(), - serialize=None - if self.serialize is GraphQLScalarType.serialize - else self.serialize, - parse_value=None - if self.parse_value is GraphQLScalarType.parse_value - else self.parse_value, - parse_literal=None - if getattr(self.parse_literal, "__func__", None) - is GraphQLScalarType.parse_literal - else self.parse_literal, + serialize=( + None + if self.serialize is GraphQLScalarType.serialize + else self.serialize + ), + parse_value=( + None + if self.parse_value is GraphQLScalarType.parse_value + else self.parse_value + ), + parse_literal=( + None + if getattr(self.parse_literal, "__func__", None) + is GraphQLScalarType.parse_literal + else self.parse_literal + ), specified_by_url=self.specified_by_url, ) @@ -517,9 +541,11 @@ def __init__( ) else: args = { - assert_name(name): value - if isinstance(value, GraphQLArgument) - else GraphQLArgument(cast(GraphQLInputType, value)) + assert_name(name): ( + value + if isinstance(value, GraphQLArgument) + else GraphQLArgument(cast(GraphQLInputType, value)) + ) for name, value in args.items() } if resolve is not None and not callable(resolve): @@ -824,9 +850,9 @@ def fields(self) -> GraphQLFieldMap: f"{self.name} fields must be GraphQLField or output type objects." ) return { - assert_name(name): value - if isinstance(value, GraphQLField) - else GraphQLField(value) # type: ignore + assert_name(name): ( + value if isinstance(value, GraphQLField) else GraphQLField(value) + ) for name, value in fields.items() } @@ -958,9 +984,9 @@ def fields(self) -> GraphQLFieldMap: f"{self.name} fields must be GraphQLField or output type objects." ) return { - assert_name(name): value - if isinstance(value, GraphQLField) - else GraphQLField(value) # type: ignore + assert_name(name): ( + value if isinstance(value, GraphQLField) else GraphQLField(value) + ) for name, value in fields.items() } @@ -1098,6 +1124,8 @@ def assert_union_type(type_: Any) -> GraphQLUnionType: GraphQLEnumValueMap = Dict[str, "GraphQLEnumValue"] +GraphQLEnumValuesDefinition = Union[GraphQLEnumValueMap, Mapping[str, Any], Type[Enum]] + class GraphQLEnumTypeKwargs(GraphQLNamedTypeKwargs, total=False): values: GraphQLEnumValueMap @@ -1145,7 +1173,7 @@ class RGBEnum(enum.Enum): def __init__( self, name: str, - values: Union[GraphQLEnumValueMap, Mapping[str, Any], Type[Enum]], + values: Thunk[GraphQLEnumValuesDefinition], names_as_values: Optional[bool] = False, description: Optional[str] = None, extensions: Optional[Dict[str, Any]] = None, @@ -1159,6 +1187,8 @@ def __init__( ast_node=ast_node, extension_ast_nodes=extension_ast_nodes, ) + if not isinstance(values, type): + values = resolve_thunk(values) # type: ignore try: # check for enum values = cast(Enum, values).__members__ # type: ignore except AttributeError: @@ -1168,11 +1198,11 @@ def __init__( try: # noinspection PyTypeChecker values = dict(values) # type: ignore - except (TypeError, ValueError): + except (TypeError, ValueError) as error: raise TypeError( f"{name} values must be an Enum or a mapping" " with value names as keys." - ) + ) from error values = cast(Dict[str, Any], values) else: values = cast(Dict[str, Enum], values) @@ -1181,9 +1211,11 @@ def __init__( elif names_as_values is True: values = {key: key for key in values} values = { - assert_enum_value_name(key): value - if isinstance(value, GraphQLEnumValue) - else GraphQLEnumValue(value) + assert_enum_value_name(key): ( + value + if isinstance(value, GraphQLEnumValue) + else GraphQLEnumValue(value) + ) for key, value in values.items() } if ast_node and not isinstance(ast_node, EnumTypeDefinitionNode): @@ -1363,6 +1395,7 @@ def __copy__(self) -> "GraphQLEnumValue": # pragma: no cover class GraphQLInputObjectTypeKwargs(GraphQLNamedTypeKwargs, total=False): fields: GraphQLInputFieldMap out_type: Optional[GraphQLInputFieldOutType] + is_one_of: bool class GraphQLInputObjectType(GraphQLNamedType): @@ -1375,7 +1408,7 @@ class GraphQLInputObjectType(GraphQLNamedType): Example:: - NonNullFloat = GraphQLNonNull(GraphQLFloat()) + NonNullFloat = GraphQLNonNull(GraphQLFloat) class GeoPoint(GraphQLInputObjectType): name = 'GeoPoint' @@ -1383,7 +1416,7 @@ class GeoPoint(GraphQLInputObjectType): 'lat': GraphQLInputField(NonNullFloat), 'lon': GraphQLInputField(NonNullFloat), 'alt': GraphQLInputField( - GraphQLFloat(), default_value=0) + GraphQLFloat, default_value=0) } The outbound values will be Python dictionaries by default, but you can have them @@ -1392,6 +1425,7 @@ class GeoPoint(GraphQLInputObjectType): ast_node: Optional[InputObjectTypeDefinitionNode] extension_ast_nodes: Tuple[InputObjectTypeExtensionNode, ...] + is_one_of: bool def __init__( self, @@ -1402,6 +1436,7 @@ def __init__( extensions: Optional[Dict[str, Any]] = None, ast_node: Optional[InputObjectTypeDefinitionNode] = None, extension_ast_nodes: Optional[Collection[InputObjectTypeExtensionNode]] = None, + is_one_of: bool = False, ) -> None: super().__init__( name=name, @@ -1427,6 +1462,7 @@ def __init__( self._fields = fields if out_type is not None: self.out_type = out_type # type: ignore + self.is_one_of = is_one_of @staticmethod def out_type(value: Dict[str, Any]) -> Any: @@ -1441,9 +1477,12 @@ def to_kwargs(self) -> GraphQLInputObjectTypeKwargs: return GraphQLInputObjectTypeKwargs( # type: ignore super().to_kwargs(), fields=self.fields.copy(), - out_type=None - if self.out_type is GraphQLInputObjectType.out_type - else self.out_type, + out_type=( + None + if self.out_type is GraphQLInputObjectType.out_type + else self.out_type + ), + is_one_of=self.is_one_of, ) def __copy__(self) -> "GraphQLInputObjectType": # pragma: no cover @@ -1473,9 +1512,11 @@ def fields(self) -> GraphQLInputFieldMap: " GraphQLInputField or input type objects." ) return { - assert_name(name): value - if isinstance(value, GraphQLInputField) - else GraphQLInputField(value) # type: ignore + assert_name(name): ( + value + if isinstance(value, GraphQLInputField) + else GraphQLInputField(value) + ) for name, value in fields.items() } @@ -1633,7 +1674,7 @@ class GraphQLNonNull(GraphQLWrappingType[GNT], Generic[GNT]): class RowType(GraphQLObjectType): name = 'Row' fields = { - 'id': GraphQLField(GraphQLNonNull(GraphQLString())) + 'id': GraphQLField(GraphQLNonNull(GraphQLString)) } Note: the enforcement of non-nullability occurs within the executor. @@ -1695,22 +1736,19 @@ def assert_nullable_type(type_: Any) -> GraphQLNullableType: @overload -def get_nullable_type(type_: None) -> None: - ... +def get_nullable_type(type_: None) -> None: ... @overload -def get_nullable_type(type_: GraphQLNullableType) -> GraphQLNullableType: - ... +def get_nullable_type(type_: GraphQLNullableType) -> GraphQLNullableType: ... @overload -def get_nullable_type(type_: GraphQLNonNull) -> GraphQLNullableType: - ... +def get_nullable_type(type_: GraphQLNonNull) -> GraphQLNullableType: ... def get_nullable_type( - type_: Optional[Union[GraphQLNullableType, GraphQLNonNull]] + type_: Optional[Union[GraphQLNullableType, GraphQLNonNull]], ) -> Optional[GraphQLNullableType]: """Unwrap possible non-null type""" if is_non_null_type(type_): @@ -1798,13 +1836,11 @@ def assert_named_type(type_: Any) -> GraphQLNamedType: @overload -def get_named_type(type_: None) -> None: - ... +def get_named_type(type_: None) -> None: ... @overload -def get_named_type(type_: GraphQLType) -> GraphQLNamedType: - ... +def get_named_type(type_: GraphQLType) -> GraphQLNamedType: ... def get_named_type(type_: Optional[GraphQLType]) -> Optional[GraphQLNamedType]: diff --git a/src/graphql/type/directives.py b/src/graphql/type/directives.py index 310968d1..c3e83f22 100644 --- a/src/graphql/type/directives.py +++ b/src/graphql/type/directives.py @@ -1,7 +1,7 @@ from typing import Any, Collection, Dict, Optional, Tuple, cast -from ..language import ast, DirectiveLocation -from ..pyutils import inspect, is_description +from ..language import DirectiveLocation, ast +from ..pyutils import inspect, is_collection, is_description from .assert_name import assert_name from .definition import GraphQLArgument, GraphQLInputType, GraphQLNonNull, is_input_type from .scalars import GraphQLBoolean, GraphQLString @@ -22,6 +22,7 @@ "GraphQLSkipDirective", "GraphQLDeprecatedDirective", "GraphQLSpecifiedByDirective", + "GraphQLOneOfDirective", "DirectiveLocation", "DEFAULT_DEPRECATION_REASON", ] @@ -32,9 +33,11 @@ class GraphQLDirectiveKwargs(TypedDict, total=False): locations: Tuple[DirectiveLocation, ...] args: Dict[str, GraphQLArgument] is_repeatable: bool + deprecation_reason: Optional[str] description: Optional[str] extensions: Dict[str, Any] ast_node: Optional[ast.DirectiveDefinitionNode] + extension_ast_nodes: Tuple[ast.DirectiveExtensionNode, ...] class GraphQLDirective: @@ -48,9 +51,11 @@ class GraphQLDirective: locations: Tuple[DirectiveLocation, ...] is_repeatable: bool args: Dict[str, GraphQLArgument] + deprecation_reason: Optional[str] description: Optional[str] extensions: Dict[str, Any] ast_node: Optional[ast.DirectiveDefinitionNode] + extension_ast_nodes: Tuple[ast.DirectiveExtensionNode, ...] def __init__( self, @@ -58,16 +63,20 @@ def __init__( locations: Collection[DirectiveLocation], args: Optional[Dict[str, GraphQLArgument]] = None, is_repeatable: bool = False, + deprecation_reason: Optional[str] = None, description: Optional[str] = None, extensions: Optional[Dict[str, Any]] = None, ast_node: Optional[ast.DirectiveDefinitionNode] = None, + extension_ast_nodes: Optional[Collection[ast.DirectiveExtensionNode]] = None, ) -> None: assert_name(name) try: locations = tuple( - value - if isinstance(value, DirectiveLocation) - else DirectiveLocation[cast(str, value)] + ( + value + if isinstance(value, DirectiveLocation) + else DirectiveLocation[cast(str, value)] + ) for value in locations ) except (KeyError, TypeError): @@ -90,13 +99,17 @@ def __init__( ) else: args = { - assert_name(name): value - if isinstance(value, GraphQLArgument) - else GraphQLArgument(cast(GraphQLInputType, value)) + assert_name(name): ( + value + if isinstance(value, GraphQLArgument) + else GraphQLArgument(cast(GraphQLInputType, value)) + ) for name, value in args.items() } if not isinstance(is_repeatable, bool): raise TypeError(f"{name} is_repeatable flag must be True or False.") + if deprecation_reason is not None and not is_description(deprecation_reason): + raise TypeError(f"{name} deprecation reason must be a string.") if ast_node and not isinstance(ast_node, ast.DirectiveDefinitionNode): raise TypeError(f"{name} AST node must be a DirectiveDefinitionNode.") if description is not None and not is_description(description): @@ -107,13 +120,28 @@ def __init__( isinstance(key, str) for key in extensions ): raise TypeError(f"{name} extensions must be a dictionary with string keys.") + if extension_ast_nodes: + if not is_collection(extension_ast_nodes) or not all( + isinstance(node, ast.DirectiveExtensionNode) + for node in extension_ast_nodes + ): + raise TypeError( + f"{name} extension AST nodes must be specified" + " as a collection of DirectiveExtensionNode instances." + ) + if not isinstance(extension_ast_nodes, tuple): + extension_ast_nodes = tuple(extension_ast_nodes) + else: + extension_ast_nodes = () self.name = name self.locations = locations self.args = args self.is_repeatable = is_repeatable + self.deprecation_reason = deprecation_reason self.description = description self.extensions = extensions self.ast_node = ast_node + self.extension_ast_nodes = extension_ast_nodes def __str__(self) -> str: return f"@{self.name}" @@ -128,6 +156,7 @@ def __eq__(self, other: Any) -> bool: and self.locations == other.locations and self.args == other.args and self.is_repeatable == other.is_repeatable + and self.deprecation_reason == other.deprecation_reason and self.description == other.description and self.extensions == other.extensions ) @@ -138,9 +167,11 @@ def to_kwargs(self) -> GraphQLDirectiveKwargs: locations=self.locations, args=self.args, is_repeatable=self.is_repeatable, + deprecation_reason=self.deprecation_reason, description=self.description, extensions=self.extensions, ast_node=self.ast_node, + extension_ast_nodes=self.extension_ast_nodes, ) def __copy__(self) -> "GraphQLDirective": # pragma: no cover @@ -205,6 +236,7 @@ def assert_directive(directive: Any) -> GraphQLDirective: DirectiveLocation.ARGUMENT_DEFINITION, DirectiveLocation.INPUT_FIELD_DEFINITION, DirectiveLocation.ENUM_VALUE, + DirectiveLocation.DIRECTIVE_DEFINITION, ], args={ "reason": GraphQLArgument( @@ -220,17 +252,25 @@ def assert_directive(directive: Any) -> GraphQLDirective: description="Marks an element of a GraphQL schema as no longer supported.", ) -# Used to provide a URL for specifying the behaviour of custom scalar definitions: +# Used to provide a URL for specifying the behavior of custom scalar definitions: GraphQLSpecifiedByDirective = GraphQLDirective( name="specifiedBy", locations=[DirectiveLocation.SCALAR], args={ "url": GraphQLArgument( GraphQLNonNull(GraphQLString), - description="The URL that specifies the behaviour of this scalar.", + description="The URL that specifies the behavior of this scalar.", ) }, - description="Exposes a URL that specifies the behaviour of this scalar.", + description="Exposes a URL that specifies the behavior of this scalar.", +) + +# Used to declare an Input Object as a OneOf Input Objects. +GraphQLOneOfDirective = GraphQLDirective( + name="oneOf", + locations=[DirectiveLocation.INPUT_OBJECT], + args={}, + description="Indicates an Input Object is a OneOf Input Object.", ) @@ -239,6 +279,7 @@ def assert_directive(directive: Any) -> GraphQLDirective: GraphQLSkipDirective, GraphQLDeprecatedDirective, GraphQLSpecifiedByDirective, + GraphQLOneOfDirective, ) """A tuple with all directives from the GraphQL specification""" diff --git a/src/graphql/type/introspection.py b/src/graphql/type/introspection.py index 123bf8f9..91afe116 100644 --- a/src/graphql/type/introspection.py +++ b/src/graphql/type/introspection.py @@ -6,6 +6,7 @@ GraphQLEnumType, GraphQLEnumValue, GraphQLField, + GraphQLFieldMap, GraphQLList, GraphQLNamedType, GraphQLNonNull, @@ -34,88 +35,128 @@ ] -__Schema: GraphQLObjectType = GraphQLObjectType( +class SchemaFields(GraphQLFieldMap): + def __new__(cls): + return { + "description": GraphQLField(GraphQLString, resolve=cls.description), + "types": GraphQLField( + GraphQLNonNull(GraphQLList(GraphQLNonNull(_Type))), + resolve=cls.types, + description="A list of all types supported by this server.", + ), + "queryType": GraphQLField( + GraphQLNonNull(_Type), + resolve=cls.query_type, + description="The type that query operations will be rooted at.", + ), + "mutationType": GraphQLField( + _Type, + resolve=cls.mutation_type, + description="If this server supports mutation, the type that" + " mutation operations will be rooted at.", + ), + "subscriptionType": GraphQLField( + _Type, + resolve=cls.subscription_type, + description="If this server supports subscription, the type that" + " subscription operations will be rooted at.", + ), + "directives": GraphQLField( + GraphQLNonNull(GraphQLList(GraphQLNonNull(_Directive))), + args={ + "includeDeprecated": GraphQLArgument( + GraphQLNonNull(GraphQLBoolean), default_value=False + ) + }, + resolve=cls.directives, + description="A list of all directives supported by this server.", + ), + } + + @staticmethod + def description(schema, _info): + return schema.description + + @staticmethod + def types(schema, _info): + return schema.type_map.values() + + @staticmethod + def query_type(schema, _info): + return schema.query_type + + @staticmethod + def mutation_type(schema, _info): + return schema.mutation_type + + @staticmethod + def subscription_type(schema, _info): + return schema.subscription_type + + # noinspection PyPep8Naming + @staticmethod + def directives(schema, _info, includeDeprecated=False): + directives = schema.directives + return ( + directives + if includeDeprecated + else [ + directive + for directive in directives + if directive.deprecation_reason is None + ] + ) + + +_Schema: GraphQLObjectType = GraphQLObjectType( name="__Schema", description="A GraphQL Schema defines the capabilities of a GraphQL" " server. It exposes all available types and directives" " on the server, as well as the entry points for query," " mutation, and subscription operations.", - fields=lambda: { - "description": GraphQLField( - GraphQLString, resolve=lambda schema, _info: schema.description - ), - "types": GraphQLField( - GraphQLNonNull(GraphQLList(GraphQLNonNull(__Type))), - resolve=lambda schema, _info: schema.type_map.values(), - description="A list of all types supported by this server.", - ), - "queryType": GraphQLField( - GraphQLNonNull(__Type), - resolve=lambda schema, _info: schema.query_type, - description="The type that query operations will be rooted at.", - ), - "mutationType": GraphQLField( - __Type, - resolve=lambda schema, _info: schema.mutation_type, - description="If this server supports mutation, the type that" - " mutation operations will be rooted at.", - ), - "subscriptionType": GraphQLField( - __Type, - resolve=lambda schema, _info: schema.subscription_type, - description="If this server support subscription, the type that" - " subscription operations will be rooted at.", - ), - "directives": GraphQLField( - GraphQLNonNull(GraphQLList(GraphQLNonNull(__Directive))), - resolve=lambda schema, _info: schema.directives, - description="A list of all directives supported by this server.", - ), - }, + fields=SchemaFields, ) -__Directive: GraphQLObjectType = GraphQLObjectType( - name="__Directive", - description="A Directive provides a way to describe alternate runtime" - " execution and type validation behavior in a GraphQL" - " document.\n\nIn some cases, you need to provide options" - " to alter GraphQL's execution behavior in ways field" - " arguments will not suffice, such as conditionally including" - " or skipping a field. Directives provide this by describing" - " additional information to the executor.", - fields=lambda: { - # Note: The fields onOperation, onFragment and onField are deprecated - "name": GraphQLField( - GraphQLNonNull(GraphQLString), - resolve=DirectiveResolvers.name, - ), - "description": GraphQLField( - GraphQLString, - resolve=DirectiveResolvers.description, - ), - "isRepeatable": GraphQLField( - GraphQLNonNull(GraphQLBoolean), - resolve=DirectiveResolvers.is_repeatable, - ), - "locations": GraphQLField( - GraphQLNonNull(GraphQLList(GraphQLNonNull(__DirectiveLocation))), - resolve=DirectiveResolvers.locations, - ), - "args": GraphQLField( - GraphQLNonNull(GraphQLList(GraphQLNonNull(__InputValue))), - args={ - "includeDeprecated": GraphQLArgument( - GraphQLBoolean, default_value=False - ) - }, - resolve=DirectiveResolvers.args, - ), - }, -) +class DirectiveFields(GraphQLFieldMap): + def __new__(cls): + return { + # Note: The fields onOperation, onFragment and onField are deprecated + "name": GraphQLField( + GraphQLNonNull(GraphQLString), + resolve=cls.name, + ), + "description": GraphQLField( + GraphQLString, + resolve=cls.description, + ), + "isRepeatable": GraphQLField( + GraphQLNonNull(GraphQLBoolean), + resolve=cls.is_repeatable, + ), + "locations": GraphQLField( + GraphQLNonNull(GraphQLList(GraphQLNonNull(_DirectiveLocation))), + resolve=cls.locations, + ), + "args": GraphQLField( + GraphQLNonNull(GraphQLList(GraphQLNonNull(_InputValue))), + args={ + "includeDeprecated": GraphQLArgument( + GraphQLBoolean, default_value=False + ) + }, + resolve=cls.args, + ), + "isDeprecated": GraphQLField( + GraphQLNonNull(GraphQLBoolean), + resolve=cls.is_deprecated, + ), + "deprecationReason": GraphQLField( + GraphQLString, + resolve=cls.deprecation_reason, + ), + } - -class DirectiveResolvers: @staticmethod def name(directive, _info): return directive.name @@ -142,8 +183,29 @@ def args(directive, _info, includeDeprecated=False): else [item for item in items if item[1].deprecation_reason is None] ) + @staticmethod + def is_deprecated(directive, _info): + return directive.deprecation_reason is not None + + @staticmethod + def deprecation_reason(directive, _info): + return directive.deprecation_reason + + +_Directive: GraphQLObjectType = GraphQLObjectType( + name="__Directive", + description="A Directive provides a way to describe alternate runtime" + " execution and type validation behavior in a GraphQL" + " document.\n\nIn some cases, you need to provide options" + " to alter GraphQL's execution behavior in ways field" + " arguments will not suffice, such as conditionally including" + " or skipping a field. Directives provide this by describing" + " additional information to the executor.", + fields=DirectiveFields, +) + -__DirectiveLocation: GraphQLEnumType = GraphQLEnumType( +_DirectiveLocation: GraphQLEnumType = GraphQLEnumType( name="__DirectiveLocation", description="A Directive can be adjacent to many parts of the GraphQL" " language, a __DirectiveLocation describes one such possible" @@ -224,69 +286,59 @@ def args(directive, _info, includeDeprecated=False): DirectiveLocation.INPUT_FIELD_DEFINITION, description="Location adjacent to an input object field definition.", ), - }, -) - - -__Type: GraphQLObjectType = GraphQLObjectType( - name="__Type", - description="The fundamental unit of any GraphQL Schema is the type." - " There are many kinds of types in GraphQL as represented" - " by the `__TypeKind` enum.\n\nDepending on the kind of a" - " type, certain fields describe information about that type." - " Scalar types provide no information beyond a name, description" - " and optional `specifiedByURL`, while Enum types provide their values." - " Object and Interface types provide the fields they describe." - " Abstract types, Union and Interface, provide the Object" - " types possible at runtime. List and NonNull types compose" - " other types.", - fields=lambda: { - "kind": GraphQLField(GraphQLNonNull(__TypeKind), resolve=TypeResolvers.kind), - "name": GraphQLField(GraphQLString, resolve=TypeResolvers.name), - "description": GraphQLField(GraphQLString, resolve=TypeResolvers.description), - "specifiedByURL": GraphQLField( - GraphQLString, resolve=TypeResolvers.specified_by_url - ), - "fields": GraphQLField( - GraphQLList(GraphQLNonNull(__Field)), - args={ - "includeDeprecated": GraphQLArgument( - GraphQLBoolean, default_value=False - ) - }, - resolve=TypeResolvers.fields, - ), - "interfaces": GraphQLField( - GraphQLList(GraphQLNonNull(__Type)), resolve=TypeResolvers.interfaces - ), - "possibleTypes": GraphQLField( - GraphQLList(GraphQLNonNull(__Type)), - resolve=TypeResolvers.possible_types, - ), - "enumValues": GraphQLField( - GraphQLList(GraphQLNonNull(__EnumValue)), - args={ - "includeDeprecated": GraphQLArgument( - GraphQLBoolean, default_value=False - ) - }, - resolve=TypeResolvers.enum_values, + "DIRECTIVE_DEFINITION": GraphQLEnumValue( + DirectiveLocation.DIRECTIVE_DEFINITION, + description="Location adjacent to a directive definition.", ), - "inputFields": GraphQLField( - GraphQLList(GraphQLNonNull(__InputValue)), - args={ - "includeDeprecated": GraphQLArgument( - GraphQLBoolean, default_value=False - ) - }, - resolve=TypeResolvers.input_fields, - ), - "ofType": GraphQLField(__Type, resolve=TypeResolvers.of_type), }, ) -class TypeResolvers: +class TypeFields(GraphQLFieldMap): + def __new__(cls): + return { + "kind": GraphQLField(GraphQLNonNull(_TypeKind), resolve=cls.kind), + "name": GraphQLField(GraphQLString, resolve=cls.name), + "description": GraphQLField(GraphQLString, resolve=cls.description), + "specifiedByURL": GraphQLField(GraphQLString, resolve=cls.specified_by_url), + "fields": GraphQLField( + GraphQLList(GraphQLNonNull(_Field)), + args={ + "includeDeprecated": GraphQLArgument( + GraphQLBoolean, default_value=False + ) + }, + resolve=cls.fields, + ), + "interfaces": GraphQLField( + GraphQLList(GraphQLNonNull(_Type)), resolve=cls.interfaces + ), + "possibleTypes": GraphQLField( + GraphQLList(GraphQLNonNull(_Type)), + resolve=cls.possible_types, + ), + "enumValues": GraphQLField( + GraphQLList(GraphQLNonNull(_EnumValue)), + args={ + "includeDeprecated": GraphQLArgument( + GraphQLBoolean, default_value=False + ) + }, + resolve=cls.enum_values, + ), + "inputFields": GraphQLField( + GraphQLList(GraphQLNonNull(_InputValue)), + args={ + "includeDeprecated": GraphQLArgument( + GraphQLBoolean, default_value=False + ) + }, + resolve=cls.input_fields, + ), + "ofType": GraphQLField(_Type, resolve=cls.of_type), + "isOneOf": GraphQLField(GraphQLBoolean, resolve=cls.is_one_of), + } + @staticmethod def kind(type_, _info): if is_scalar_type(type_): @@ -368,39 +420,54 @@ def input_fields(type_, _info, includeDeprecated=False): def of_type(type_, _info): return getattr(type_, "of_type", None) + @staticmethod + def is_one_of(type_, _info): + return type_.is_one_of if is_input_object_type(type_) else None -__Field: GraphQLObjectType = GraphQLObjectType( - name="__Field", - description="Object and Interface types are described by a list of Fields," - " each of which has a name, potentially a list of arguments," - " and a return type.", - fields=lambda: { - "name": GraphQLField( - GraphQLNonNull(GraphQLString), resolve=FieldResolvers.name - ), - "description": GraphQLField(GraphQLString, resolve=FieldResolvers.description), - "args": GraphQLField( - GraphQLNonNull(GraphQLList(GraphQLNonNull(__InputValue))), - args={ - "includeDeprecated": GraphQLArgument( - GraphQLBoolean, default_value=False - ) - }, - resolve=FieldResolvers.args, - ), - "type": GraphQLField(GraphQLNonNull(__Type), resolve=FieldResolvers.type), - "isDeprecated": GraphQLField( - GraphQLNonNull(GraphQLBoolean), - resolve=FieldResolvers.is_deprecated, - ), - "deprecationReason": GraphQLField( - GraphQLString, resolve=FieldResolvers.deprecation_reason - ), - }, + +TypeResolvers = TypeFields # for backward compatibility + + +_Type: GraphQLObjectType = GraphQLObjectType( + name="__Type", + description="The fundamental unit of any GraphQL Schema is the type." + " There are many kinds of types in GraphQL as represented" + " by the `__TypeKind` enum.\n\nDepending on the kind of a" + " type, certain fields describe information about that type." + " Scalar types provide no information beyond a name, description" + " and optional `specifiedByURL`, while Enum types provide their values." + " Object and Interface types provide the fields they describe." + " Abstract types, Union and Interface, provide the Object" + " types possible at runtime. List and NonNull types compose" + " other types.", + fields=TypeFields, ) -class FieldResolvers: +class FieldFields(GraphQLFieldMap): + def __new__(cls): + return { + "name": GraphQLField(GraphQLNonNull(GraphQLString), resolve=cls.name), + "description": GraphQLField(GraphQLString, resolve=cls.description), + "args": GraphQLField( + GraphQLNonNull(GraphQLList(GraphQLNonNull(_InputValue))), + args={ + "includeDeprecated": GraphQLArgument( + GraphQLBoolean, default_value=False + ) + }, + resolve=cls.args, + ), + "type": GraphQLField(GraphQLNonNull(_Type), resolve=cls.type), + "isDeprecated": GraphQLField( + GraphQLNonNull(GraphQLBoolean), + resolve=cls.is_deprecated, + ), + "deprecationReason": GraphQLField( + GraphQLString, resolve=cls.deprecation_reason + ), + } + @staticmethod def name(item, _info): return item[0] @@ -432,39 +499,38 @@ def deprecation_reason(item, _info): return item[1].deprecation_reason -__InputValue: GraphQLObjectType = GraphQLObjectType( - name="__InputValue", - description="Arguments provided to Fields or Directives and the input" - " fields of an InputObject are represented as Input Values" - " which describe their type and optionally a default value.", - fields=lambda: { - "name": GraphQLField( - GraphQLNonNull(GraphQLString), resolve=InputValueFieldResolvers.name - ), - "description": GraphQLField( - GraphQLString, resolve=InputValueFieldResolvers.description - ), - "type": GraphQLField( - GraphQLNonNull(__Type), resolve=InputValueFieldResolvers.type - ), - "defaultValue": GraphQLField( - GraphQLString, - description="A GraphQL-formatted string representing" - " the default value for this input value.", - resolve=InputValueFieldResolvers.default_value, - ), - "isDeprecated": GraphQLField( - GraphQLNonNull(GraphQLBoolean), - resolve=InputValueFieldResolvers.is_deprecated, - ), - "deprecationReason": GraphQLField( - GraphQLString, resolve=InputValueFieldResolvers.deprecation_reason - ), - }, +_Field: GraphQLObjectType = GraphQLObjectType( + name="__Field", + description="Object and Interface types are described by a list of Fields," + " each of which has a name, potentially a list of arguments," + " and a return type.", + fields=FieldFields, ) -class InputValueFieldResolvers: +class InputValueFields(GraphQLFieldMap): + def __new__(cls): + return { + "name": GraphQLField(GraphQLNonNull(GraphQLString), resolve=cls.name), + "description": GraphQLField( + GraphQLString, resolve=InputValueFields.description + ), + "type": GraphQLField(GraphQLNonNull(_Type), resolve=cls.type), + "defaultValue": GraphQLField( + GraphQLString, + description="A GraphQL-formatted string representing" + " the default value for this input value.", + resolve=cls.default_value, + ), + "isDeprecated": GraphQLField( + GraphQLNonNull(GraphQLBoolean), + resolve=cls.is_deprecated, + ), + "deprecationReason": GraphQLField( + GraphQLString, resolve=cls.deprecation_reason + ), + } + @staticmethod def name(item, _info): return item[0] @@ -494,27 +560,57 @@ def deprecation_reason(item, _info): return item[1].deprecation_reason -__EnumValue: GraphQLObjectType = GraphQLObjectType( +_InputValue: GraphQLObjectType = GraphQLObjectType( + name="__InputValue", + description="Arguments provided to Fields or Directives and the input" + " fields of an InputObject are represented as Input Values" + " which describe their type and optionally a default value.", + fields=InputValueFields, +) + + +class EnumValueFields(GraphQLFieldMap): + def __new__(cls): + return { + "name": GraphQLField( + GraphQLNonNull(GraphQLString), resolve=EnumValueFields.name + ), + "description": GraphQLField( + GraphQLString, resolve=EnumValueFields.description + ), + "isDeprecated": GraphQLField( + GraphQLNonNull(GraphQLBoolean), + resolve=EnumValueFields.is_deprecated, + ), + "deprecationReason": GraphQLField( + GraphQLString, resolve=EnumValueFields.deprecation_reason + ), + } + + @staticmethod + def name(item, _info): + return item[0] + + @staticmethod + def description(item, _info): + return item[1].description + + @staticmethod + def is_deprecated(item, _info): + return item[1].deprecation_reason is not None + + @staticmethod + def deprecation_reason(item, _info): + return item[1].deprecation_reason + + +_EnumValue: GraphQLObjectType = GraphQLObjectType( name="__EnumValue", description="One possible value for a given Enum. Enum values are unique" " values, not a placeholder for a string or numeric value." " However an Enum value is returned in a JSON response as a" " string.", - fields=lambda: { - "name": GraphQLField( - GraphQLNonNull(GraphQLString), resolve=lambda item, _info: item[0] - ), - "description": GraphQLField( - GraphQLString, resolve=lambda item, _info: item[1].description - ), - "isDeprecated": GraphQLField( - GraphQLNonNull(GraphQLBoolean), - resolve=lambda item, _info: item[1].deprecation_reason is not None, - ), - "deprecationReason": GraphQLField( - GraphQLString, resolve=lambda item, _info: item[1].deprecation_reason - ), - }, + fields=EnumValueFields, ) @@ -529,7 +625,7 @@ class TypeKind(Enum): NON_NULL = "non-null" -__TypeKind: GraphQLEnumType = GraphQLEnumType( +_TypeKind: GraphQLEnumType = GraphQLEnumType( name="__TypeKind", description="An enum describing what kind of type a given `__Type` is.", values={ @@ -567,26 +663,39 @@ class TypeKind(Enum): ), "NON_NULL": GraphQLEnumValue( TypeKind.NON_NULL, - description="Indicates this type is a non-null." - " `ofType` is a valid field.", + description="Indicates this type is a non-null. `ofType` is a valid field.", ), }, ) +class MetaFields: + @staticmethod + def schema(_source, info): + return info.schema + + @staticmethod + def type(_source, info, **args): + return info.schema.get_type(args["name"]) + + @staticmethod + def type_name(_source, info, **_args): + return info.parent_type.name + + SchemaMetaFieldDef = GraphQLField( - GraphQLNonNull(__Schema), # name = '__schema' + GraphQLNonNull(_Schema), # name = '__schema' description="Access the current type schema of this server.", args={}, - resolve=lambda _source, info: info.schema, + resolve=MetaFields.schema, ) TypeMetaFieldDef = GraphQLField( - __Type, # name = '__type' + _Type, # name = '__type' description="Request the type information of a single type.", args={"name": GraphQLArgument(GraphQLNonNull(GraphQLString))}, - resolve=lambda _source, info, **args: info.schema.get_type(args["name"]), + resolve=MetaFields.type, ) @@ -594,21 +703,21 @@ class TypeKind(Enum): GraphQLNonNull(GraphQLString), # name='__typename' description="The name of the current Object type at runtime.", args={}, - resolve=lambda _source, info, **_args: info.parent_type.name, + resolve=MetaFields.type_name, ) # Since double underscore names are subject to name mangling in Python, # the introspection classes are best imported via this dictionary: introspection_types: Mapping[str, GraphQLNamedType] = { # treat as read-only - "__Schema": __Schema, - "__Directive": __Directive, - "__DirectiveLocation": __DirectiveLocation, - "__Type": __Type, - "__Field": __Field, - "__InputValue": __InputValue, - "__EnumValue": __EnumValue, - "__TypeKind": __TypeKind, + "__Schema": _Schema, + "__Directive": _Directive, + "__DirectiveLocation": _DirectiveLocation, + "__Type": _Type, + "__Field": _Field, + "__InputValue": _InputValue, + "__EnumValue": _EnumValue, + "__TypeKind": _TypeKind, } """A mapping containing all introspection types with their names as keys""" @@ -616,3 +725,7 @@ class TypeKind(Enum): def is_introspection_type(type_: GraphQLNamedType) -> bool: """Check whether the given named GraphQL type is an introspection type.""" return type_.name in introspection_types + + +# register the introspection types to avoid redefinition +GraphQLNamedType.reserved_types.update(introspection_types) diff --git a/src/graphql/type/scalars.py b/src/graphql/type/scalars.py index 2609d095..9e65368e 100644 --- a/src/graphql/type/scalars.py +++ b/src/graphql/type/scalars.py @@ -319,3 +319,7 @@ def parse_id_literal(value_node: ValueNode, _variables: Any = None) -> str: def is_specified_scalar_type(type_: GraphQLNamedType) -> bool: """Check whether the given named GraphQL type is a specified scalar type.""" return type_.name in specified_scalar_types + + +# register the scalar types to avoid redefinition +GraphQLNamedType.reserved_types.update(specified_scalar_types) diff --git a/src/graphql/type/schema.py b/src/graphql/type/schema.py index 321659c5..4b211b72 100644 --- a/src/graphql/type/schema.py +++ b/src/graphql/type/schema.py @@ -13,16 +13,17 @@ ) from ..error import GraphQLError -from ..language import ast, OperationType +from ..language import OperationType, ast from ..pyutils import inspect, is_collection, is_description from .definition import ( GraphQLAbstractType, - GraphQLInterfaceType, GraphQLInputObjectType, + GraphQLInputType, + GraphQLInterfaceType, GraphQLNamedType, GraphQLObjectType, - GraphQLUnionType, GraphQLType, + GraphQLUnionType, GraphQLWrappingType, get_named_type, is_input_object_type, @@ -31,7 +32,7 @@ is_union_type, is_wrapping_type, ) -from .directives import GraphQLDirective, specified_directives, is_directive +from .directives import GraphQLDirective, is_directive, specified_directives from .introspection import introspection_types try: @@ -205,8 +206,8 @@ def __init__( # Provide specified directives (e.g. @include and @skip) by default self.directives = specified_directives if directives is None else directives - # To preserve order of user-provided types, we add first to add them to - # the set of "collected" types, so `collect_referenced_types` ignore them. + # To preserve order of user-provided types, we first add them to the set + # of "collected" types, so `collect_referenced_types` ignores them. if types: all_referenced_types = TypeSet.with_initial_types(types) collect_referenced_types = all_referenced_types.collect_referenced_types @@ -259,6 +260,7 @@ def __init__( "Schema must contain uniquely named types" f" but contains multiple types named '{type_name}'." ) + type_map[type_name] = named_type if is_interface_type(named_type): @@ -270,9 +272,9 @@ def __init__( if iface.name in implementations_map: implementations = implementations_map[iface.name] else: - implementations = implementations_map[ - iface.name - ] = InterfaceImplementations(objects=[], interfaces=[]) + implementations = implementations_map[iface.name] = ( + InterfaceImplementations(objects=[], interfaces=[]) + ) implementations.interfaces.append(named_type) elif is_object_type(named_type): @@ -284,9 +286,9 @@ def __init__( if iface.name in implementations_map: implementations = implementations_map[iface.name] else: - implementations = implementations_map[ - iface.name - ] = InterfaceImplementations(objects=[], interfaces=[]) + implementations = implementations_map[iface.name] = ( + InterfaceImplementations(objects=[], interfaces=[]) + ) implementations.objects.append(named_type) @@ -310,8 +312,8 @@ def __copy__(self) -> "GraphQLSchema": # pragma: no cover def __deepcopy__(self, memo_: Dict) -> "GraphQLSchema": from ..type import ( is_introspection_type, - is_specified_scalar_type, is_specified_directive, + is_specified_scalar_type, ) type_map: TypeMap = { @@ -326,6 +328,8 @@ def __deepcopy__(self, memo_: Dict) -> "GraphQLSchema": directive if is_specified_directive(directive) else copy(directive) for directive in self.directives ] + for directive in directives: + remap_directive(directive, type_map) return self.__class__( self.query_type and cast(GraphQLObjectType, type_map[self.query_type.name]), self.mutation_type @@ -461,12 +465,7 @@ def remapped_type(type_: GraphQLType, type_map: TypeMap) -> GraphQLType: def remap_named_type(type_: GraphQLNamedType, type_map: TypeMap) -> None: """Change all references in the given named type to use this type map.""" - if is_union_type(type_): - type_ = cast(GraphQLUnionType, type_) - type_.types = [ - type_map.get(member_type.name, member_type) for member_type in type_.types - ] - elif is_object_type(type_) or is_interface_type(type_): + if is_object_type(type_) or is_interface_type(type_): type_ = cast(Union[GraphQLObjectType, GraphQLInterfaceType], type_) type_.interfaces = [ type_map.get(interface_type.name, interface_type) @@ -482,6 +481,11 @@ def remap_named_type(type_: GraphQLNamedType, type_map: TypeMap) -> None: arg.type = remapped_type(arg.type, type_map) args[arg_name] = arg fields[field_name] = field + elif is_union_type(type_): + type_ = cast(GraphQLUnionType, type_) + type_.types = [ + type_map.get(member_type.name, member_type) for member_type in type_.types + ] elif is_input_object_type(type_): type_ = cast(GraphQLInputObjectType, type_) fields = type_.fields @@ -489,3 +493,12 @@ def remap_named_type(type_: GraphQLNamedType, type_map: TypeMap) -> None: field = copy(field) field.type = remapped_type(field.type, type_map) fields[field_name] = field + + +def remap_directive(directive: GraphQLDirective, type_map: TypeMap) -> None: + """Change all references in the given directive to use this type map.""" + args = directive.args + for arg_name, arg in args.items(): + arg = copy(arg) # noqa: PLW2901 + arg.type = cast(GraphQLInputType, remapped_type(arg.type, type_map)) + args[arg_name] = arg diff --git a/src/graphql/type/validate.py b/src/graphql/type/validate.py index 555bc30d..b93cde22 100644 --- a/src/graphql/type/validate.py +++ b/src/graphql/type/validate.py @@ -12,7 +12,7 @@ ) from ..error import GraphQLError -from ..pyutils import inspect +from ..pyutils import Undefined, inspect from ..language import ( DirectiveNode, InputValueDefinitionNode, @@ -154,6 +154,12 @@ def validate_directives(self) -> None: # Ensure they are named correctly. self.validate_name(directive) + if not directive.locations: + self.report_error( + f"Directive @{directive.name} must include 1 or more locations.", + directive.ast_node, + ) + # Ensure the arguments are valid. for arg_name, arg in directive.args.items(): # Ensure they are named correctly. @@ -408,11 +414,13 @@ def validate_type_implements_ancestors( for transitive in iface_interfaces: if transitive not in type_interfaces: self.report_error( - f"Type {type_.name} cannot implement {iface.name}" - " because it would create a circular reference." - if transitive is type_ - else f"Type {type_.name} must implement {transitive.name}" - f" because it is implemented by {iface.name}.", + ( + f"Type {type_.name} cannot implement {iface.name}" + " because it would create a circular reference." + if transitive is type_ + else f"Type {type_.name} must implement {transitive.name}" + f" because it is implemented by {iface.name}." + ), get_all_implements_interface_nodes(iface, transitive) + get_all_implements_interface_nodes(type_, iface), ) @@ -491,6 +499,28 @@ def validate_input_fields(self, input_obj: GraphQLInputObjectType) -> None: ], ) + if input_obj.is_one_of: + self.validate_one_of_input_object_field(input_obj, field_name, field) + + def validate_one_of_input_object_field( + self, + type_: GraphQLInputObjectType, + field_name: str, + field: GraphQLInputField, + ) -> None: + if is_non_null_type(field.type): + self.report_error( + f"OneOf input field {type_.name}.{field_name} must be nullable.", + field.ast_node and field.ast_node.type, + ) + + if field.default_value is not Undefined: + self.report_error( + f"OneOf input field {type_.name}.{field_name}" + " cannot have a default value.", + field.ast_node, + ) + def get_operation_type_node( schema: GraphQLSchema, operation: OperationType @@ -559,7 +589,8 @@ def __call__(self, input_obj: GraphQLInputObjectType) -> None: def get_all_implements_interface_nodes( - type_: Union[GraphQLObjectType, GraphQLInterfaceType], iface: GraphQLInterfaceType + type_: Union[GraphQLObjectType, GraphQLInterfaceType], + iface: Union[GraphQLObjectType, GraphQLInterfaceType], ) -> List[NamedTypeNode]: ast_node = type_.ast_node nodes = type_.extension_ast_nodes diff --git a/src/graphql/utilities/__init__.py b/src/graphql/utilities/__init__.py index 1571485b..8312567d 100644 --- a/src/graphql/utilities/__init__.py +++ b/src/graphql/utilities/__init__.py @@ -84,12 +84,36 @@ find_dangerous_changes, ) +# Resolve a schema coordinate to a schema element. +from .resolve_schema_coordinate import ( + resolve_schema_coordinate, + resolve_ast_schema_coordinate, + ResolvedNamedType, + ResolvedField, + ResolvedInputField, + ResolvedEnumValue, + ResolvedFieldArgument, + ResolvedDirective, + ResolvedDirectiveArgument, + ResolvedSchemaElement, +) + __all__ = [ "BreakingChange", "BreakingChangeType", "DangerousChange", "DangerousChangeType", "IntrospectionQuery", + "ResolvedNamedType", + "ResolvedField", + "ResolvedInputField", + "ResolvedEnumValue", + "ResolvedFieldArgument", + "ResolvedDirective", + "ResolvedDirectiveArgument", + "ResolvedSchemaElement", + "resolve_schema_coordinate", + "resolve_ast_schema_coordinate", "TypeInfo", "TypeInfoVisitor", "assert_valid_name", diff --git a/src/graphql/utilities/assert_valid_name.py b/src/graphql/utilities/assert_valid_name.py index e727a482..2e5bc319 100644 --- a/src/graphql/utilities/assert_valid_name.py +++ b/src/graphql/utilities/assert_valid_name.py @@ -9,6 +9,9 @@ def assert_valid_name(name: str) -> str: """Uphold the spec rules about naming. + This deprecated helper is retained for backwards compatibility; call ``assert_name`` + instead because ``assert_valid_name`` will be removed in v3.3. + .. deprecated:: 3.2 Please use ``assert_name`` instead. Will be removed in v3.3. """ @@ -21,6 +24,10 @@ def assert_valid_name(name: str) -> str: def is_valid_name_error(name: str) -> Optional[GraphQLError]: """Return an Error if a name is invalid. + This deprecated helper is retained for backwards compatibility; call ``assert_name`` + and catch the raised GraphQLError instead because ``is_valid_name_error`` will be + removed in v3.3. + .. deprecated:: 3.2 Please use ``assert_name`` instead. Will be removed in v3.3. """ diff --git a/src/graphql/utilities/ast_from_value.py b/src/graphql/utilities/ast_from_value.py index 208d9d95..9d71d743 100644 --- a/src/graphql/utilities/ast_from_value.py +++ b/src/graphql/utilities/ast_from_value.py @@ -38,9 +38,7 @@ def ast_from_value(value: Any, type_: GraphQLInputType) -> Optional[ValueNode]: """Produce a GraphQL Value AST given a Python object. This function will match Python/JSON values to GraphQL AST schema format by using - the suggested GraphQLInputType. For example:: - - ast_from_value('value', GraphQLString) + the suggested GraphQLInputType. A GraphQL type must be provided, which will be used to interpret different Python values. diff --git a/src/graphql/utilities/ast_to_dict.py b/src/graphql/utilities/ast_to_dict.py index 9cacd8ab..f344a5e2 100644 --- a/src/graphql/utilities/ast_to_dict.py +++ b/src/graphql/utilities/ast_to_dict.py @@ -3,15 +3,13 @@ from ..language import Node, OperationType from ..pyutils import is_iterable - __all__ = ["ast_to_dict"] @overload def ast_to_dict( node: Node, locations: bool = False, cache: Optional[Dict[Node, Any]] = None -) -> Dict: - ... +) -> Dict: ... @overload @@ -19,8 +17,7 @@ def ast_to_dict( node: Collection[Node], locations: bool = False, cache: Optional[Dict[Node, Any]] = None, -) -> List[Node]: - ... +) -> List[Node]: ... @overload @@ -28,8 +25,7 @@ def ast_to_dict( node: OperationType, locations: bool = False, cache: Optional[Dict[Node, Any]] = None, -) -> str: - ... +) -> str: ... def ast_to_dict( @@ -37,10 +33,8 @@ def ast_to_dict( ) -> Any: """Convert a language AST to a nested Python dictionary. - Set `location` to True in order to get the locations as well. + Set `locations` to True in order to get the locations as well. """ - - """Convert a node to a nested Python dictionary.""" if isinstance(node, Node): if cache is None: cache = {} diff --git a/src/graphql/utilities/build_ast_schema.py b/src/graphql/utilities/build_ast_schema.py index 30990f49..ef4e3591 100644 --- a/src/graphql/utilities/build_ast_schema.py +++ b/src/graphql/utilities/build_ast_schema.py @@ -7,7 +7,7 @@ GraphQLSchemaKwargs, specified_directives, ) -from .extend_schema import extend_schema_impl +from .extend_schema import ExtendSchemaImpl __all__ = [ "build_ast_schema", @@ -56,7 +56,9 @@ def build_ast_schema( extension_ast_nodes=(), assume_valid=False, ) - schema_kwargs = extend_schema_impl(empty_schema_kwargs, document_ast, assume_valid) + schema_kwargs = ExtendSchemaImpl.extend_schema_args( + empty_schema_kwargs, document_ast, assume_valid + ) if not schema_kwargs["ast_node"]: for type_ in schema_kwargs["types"] or (): @@ -90,6 +92,7 @@ def build_schema( assume_valid_sdl: bool = False, no_location: bool = False, allow_legacy_fragment_variables: bool = False, + experimental_directives_on_directive_definitions: bool = False, ) -> GraphQLSchema: """Build a GraphQLSchema directly from a source document.""" return build_ast_schema( @@ -97,6 +100,9 @@ def build_schema( source, no_location=no_location, allow_legacy_fragment_variables=allow_legacy_fragment_variables, + experimental_directives_on_directive_definitions=( + experimental_directives_on_directive_definitions + ), ), assume_valid=assume_valid, assume_valid_sdl=assume_valid_sdl, diff --git a/src/graphql/utilities/build_client_schema.py b/src/graphql/utilities/build_client_schema.py index b9ad32ed..feda1f23 100644 --- a/src/graphql/utilities/build_client_schema.py +++ b/src/graphql/utilities/build_client_schema.py @@ -133,11 +133,15 @@ def build_type(type_: IntrospectionType) -> GraphQLNamedType: def build_scalar_def( scalar_introspection: IntrospectionScalarType, ) -> GraphQLScalarType: - return GraphQLScalarType( - name=scalar_introspection["name"], - description=scalar_introspection.get("description"), - specified_by_url=scalar_introspection.get("specifiedByURL"), - ) + name = scalar_introspection["name"] + try: + return cast(GraphQLScalarType, GraphQLScalarType.reserved_types[name]) + except KeyError: + return GraphQLScalarType( + name=name, + description=scalar_introspection.get("description"), + specified_by_url=scalar_introspection.get("specifiedByURL"), + ) def build_implementations_list( implementing_introspection: Union[ @@ -160,12 +164,16 @@ def build_implementations_list( def build_object_def( object_introspection: IntrospectionObjectType, ) -> GraphQLObjectType: - return GraphQLObjectType( - name=object_introspection["name"], - description=object_introspection.get("description"), - interfaces=lambda: build_implementations_list(object_introspection), - fields=lambda: build_field_def_map(object_introspection), - ) + name = object_introspection["name"] + try: + return cast(GraphQLObjectType, GraphQLObjectType.reserved_types[name]) + except KeyError: + return GraphQLObjectType( + name=name, + description=object_introspection.get("description"), + interfaces=lambda: build_implementations_list(object_introspection), + fields=lambda: build_field_def_map(object_introspection), + ) def build_interface_def( interface_introspection: IntrospectionInterfaceType, @@ -199,18 +207,22 @@ def build_enum_def(enum_introspection: IntrospectionEnumType) -> GraphQLEnumType "Introspection result missing enumValues:" f" {inspect(enum_introspection)}." ) - return GraphQLEnumType( - name=enum_introspection["name"], - description=enum_introspection.get("description"), - values={ - value_introspect["name"]: GraphQLEnumValue( - value=value_introspect["name"], - description=value_introspect.get("description"), - deprecation_reason=value_introspect.get("deprecationReason"), - ) - for value_introspect in enum_introspection["enumValues"] - }, - ) + name = enum_introspection["name"] + try: + return cast(GraphQLEnumType, GraphQLEnumType.reserved_types[name]) + except KeyError: + return GraphQLEnumType( + name=name, + description=enum_introspection.get("description"), + values={ + value_introspect["name"]: GraphQLEnumValue( + value=value_introspect["name"], + description=value_introspect.get("description"), + deprecation_reason=value_introspect.get("deprecationReason"), + ) + for value_introspect in enum_introspection["enumValues"] + }, + ) def build_input_object_def( input_object_introspection: IntrospectionInputObjectType, @@ -226,6 +238,7 @@ def build_input_object_def( fields=lambda: build_input_value_def_map( input_object_introspection["inputFields"] ), + is_one_of=input_object_introspection.get("isOneOf", False), ) type_builders: Dict[str, Callable[[IntrospectionType], GraphQLNamedType]] = { @@ -358,6 +371,7 @@ def build_directive( name=directive_introspection["name"], description=directive_introspection.get("description"), is_repeatable=directive_introspection.get("isRepeatable", False), + deprecation_reason=directive_introspection.get("deprecationReason"), locations=list( cast( Collection[DirectiveLocation], diff --git a/src/graphql/utilities/coerce_input_value.py b/src/graphql/utilities/coerce_input_value.py index 6901c892..fb472612 100644 --- a/src/graphql/utilities/coerce_input_value.py +++ b/src/graphql/utilities/coerce_input_value.py @@ -127,6 +127,30 @@ def coerce_input_value( + did_you_mean(suggestions) ), ) + + if type_.is_one_of: + keys = list(coerced_dict) + if len(keys) != 1: + on_error( + path.as_list() if path else [], + input_value, + GraphQLError( + "Exactly one key must be specified" + f" for OneOf type '{type_.name}'.", + ), + ) + else: + key = keys[0] + value = coerced_dict[key] + if value is None: + on_error( + (path.as_list() if path else []) + [key], + value, + GraphQLError( + f"Field '{key}' must be non-null.", + ), + ) + return type_.out_type(coerced_dict) if is_leaf_type(type_): diff --git a/src/graphql/utilities/extend_schema.py b/src/graphql/utilities/extend_schema.py index 93c8dce4..b5d3fb6f 100644 --- a/src/graphql/utilities/extend_schema.py +++ b/src/graphql/utilities/extend_schema.py @@ -1,19 +1,21 @@ from collections import defaultdict +from functools import partial from typing import ( Any, - Callable, Collection, DefaultDict, Dict, List, Mapping, Optional, + Tuple, Union, cast, ) from ..language import ( DirectiveDefinitionNode, + DirectiveExtensionNode, DirectiveLocation, DocumentNode, EnumTypeDefinitionNode, @@ -54,21 +56,26 @@ GraphQLFieldMap, GraphQLInputField, GraphQLInputObjectType, + GraphQLInputObjectTypeKwargs, GraphQLInputType, GraphQLInputFieldMap, GraphQLInterfaceType, + GraphQLInterfaceTypeKwargs, GraphQLList, GraphQLNamedType, GraphQLNonNull, GraphQLNullableType, GraphQLObjectType, + GraphQLObjectTypeKwargs, GraphQLOutputType, GraphQLScalarType, GraphQLSchema, GraphQLSchemaKwargs, GraphQLSpecifiedByDirective, + GraphQLOneOfDirective, GraphQLType, GraphQLUnionType, + GraphQLUnionTypeKwargs, assert_schema, is_enum_type, is_input_object_type, @@ -77,6 +84,7 @@ is_non_null_type, is_object_type, is_scalar_type, + is_specified_directive, is_union_type, is_introspection_type, is_specified_scalar_type, @@ -87,7 +95,7 @@ __all__ = [ "extend_schema", - "extend_schema_impl", + "ExtendSchemaImpl", ] @@ -125,153 +133,266 @@ def extend_schema( assert_valid_sdl_extension(document_ast, schema) schema_kwargs = schema.to_kwargs() - extended_kwargs = extend_schema_impl(schema_kwargs, document_ast, assume_valid) + extended_kwargs = ExtendSchemaImpl.extend_schema_args( + schema_kwargs, document_ast, assume_valid + ) return ( schema if schema_kwargs is extended_kwargs else GraphQLSchema(**extended_kwargs) ) -def extend_schema_impl( - schema_kwargs: GraphQLSchemaKwargs, - document_ast: DocumentNode, - assume_valid: bool = False, -) -> GraphQLSchemaKwargs: - """Extend the given schema arguments with extensions from a given document. +class ExtendSchemaImpl: + """Helper class implementing the methods to extend a schema. + + Note: We use a class instead of an implementation with local functions + and lambda functions so that the extended schema can be pickled. For internal use only. """ - # Note: schema_kwargs should become a TypedDict once we require Python 3.8 - - # Collect the type definitions and extensions found in the document. - type_defs: List[TypeDefinitionNode] = [] - type_extensions_map: DefaultDict[str, Any] = defaultdict(list) - - # New directives and types are separate because a directives and types can have the - # same name. For example, a type named "skip". - directive_defs: List[DirectiveDefinitionNode] = [] - - schema_def: Optional[SchemaDefinitionNode] = None - # Schema extensions are collected which may add additional operation types. - schema_extensions: List[SchemaExtensionNode] = [] - - for def_ in document_ast.definitions: - if isinstance(def_, SchemaDefinitionNode): - schema_def = def_ - elif isinstance(def_, SchemaExtensionNode): - schema_extensions.append(def_) - elif isinstance(def_, TypeDefinitionNode): - type_defs.append(def_) - elif isinstance(def_, TypeExtensionNode): - extended_type_name = def_.name.value - type_extensions_map[extended_type_name].append(def_) - elif isinstance(def_, DirectiveDefinitionNode): - directive_defs.append(def_) - - # If this document contains no new types, extensions, or directives then return the - # same unmodified GraphQLSchema instance. - if ( - not type_extensions_map - and not type_defs - and not directive_defs - and not schema_extensions - and not schema_def + + type_map: Dict[str, GraphQLNamedType] + type_extensions_map: Dict[str, Any] + directive_extensions_map: Dict[str, List[DirectiveExtensionNode]] + + def __init__( + self, + type_extensions_map: Dict[str, Any], + directive_extensions_map: Dict[str, List[DirectiveExtensionNode]], ): - return schema_kwargs + self.type_map = {} + self.type_extensions_map = type_extensions_map + self.directive_extensions_map = directive_extensions_map + + @classmethod + def extend_schema_args( + cls, + schema_kwargs: GraphQLSchemaKwargs, + document_ast: DocumentNode, + assume_valid: bool = False, + ) -> GraphQLSchemaKwargs: + """Extend the given schema arguments with extensions from a given document. + + For internal use only. + """ + # Collect the type definitions and extensions found in the document. + type_defs: List[TypeDefinitionNode] = [] + type_extensions_map: DefaultDict[str, Any] = defaultdict(list) + directive_extensions_map: DefaultDict[str, List[DirectiveExtensionNode]] = ( + defaultdict(list) + ) - # Below are functions used for producing this schema that have closed over this - # scope and have access to the schema, cache, and newly defined types. + # New directives and types are separate because a directives and types can have + # the same name. For example, a type named "skip". + directive_defs: List[DirectiveDefinitionNode] = [] + + schema_def: Optional[SchemaDefinitionNode] = None + # Schema extensions are collected which may add additional operation types. + schema_extensions: List[SchemaExtensionNode] = [] + + for def_ in document_ast.definitions: + if isinstance(def_, SchemaDefinitionNode): + schema_def = def_ + elif isinstance(def_, SchemaExtensionNode): + schema_extensions.append(def_) + elif isinstance(def_, TypeDefinitionNode): + type_defs.append(def_) + elif isinstance(def_, TypeExtensionNode): + extended_type_name = def_.name.value + type_extensions_map[extended_type_name].append(def_) + elif isinstance(def_, DirectiveDefinitionNode): + directive_defs.append(def_) + elif isinstance(def_, DirectiveExtensionNode): + extended_directive_name = def_.name.value + directive_extensions_map[extended_directive_name].append(def_) + + # If this document contains no new types, extensions, or directives then return + # the same unmodified GraphQLSchema instance. + if ( + not type_extensions_map + and not directive_extensions_map + and not type_defs + and not directive_defs + and not schema_extensions + and not schema_def + ): + return schema_kwargs + + self = cls(type_extensions_map, directive_extensions_map) + for existing_type in schema_kwargs["types"] or (): + self.type_map[existing_type.name] = self.extend_named_type(existing_type) + for type_node in type_defs: + name = type_node.name.value + self.type_map[name] = std_type_map.get(name) or self.build_type(type_node) + + directive_map: Dict[str, GraphQLDirective] = { + existing_directive.name: self.extend_directive(existing_directive) + for existing_directive in schema_kwargs["directives"] + } + + # Get the extended root operation types. + operation_types: Dict[OperationType, GraphQLNamedType] = {} + for operation_type in OperationType: + original_type = schema_kwargs[operation_type.value] + if original_type: + operation_types[operation_type] = self.replace_named_type(original_type) + # Then, incorporate schema definition and all schema extensions. + if schema_def: + operation_types.update(self.get_operation_types([schema_def])) + if schema_extensions: + operation_types.update(self.get_operation_types(schema_extensions)) + + # Then produce and return the kwargs for a Schema with these types. + get_operation = operation_types.get + description = ( + schema_def.description.value + if schema_def and schema_def.description + else None + ) + if description is None: + description = schema_kwargs["description"] + return GraphQLSchemaKwargs( + query=get_operation(OperationType.QUERY), # type: ignore + mutation=get_operation(OperationType.MUTATION), # type: ignore + subscription=get_operation(OperationType.SUBSCRIPTION), # type: ignore + types=tuple(self.type_map.values()), + directives=tuple( + self.replace_directive(directive) + for directive in directive_map.values() + ) + + tuple(self.build_directive(directive) for directive in directive_defs), + description=description, + extensions=schema_kwargs["extensions"], + ast_node=schema_def or schema_kwargs["ast_node"], + extension_ast_nodes=schema_kwargs["extension_ast_nodes"] + + tuple(schema_extensions), + assume_valid=assume_valid, + ) # noinspection PyTypeChecker,PyUnresolvedReferences - def replace_type(type_: GraphQLType) -> GraphQLType: + def replace_type(self, type_: GraphQLType) -> GraphQLType: if is_list_type(type_): - return GraphQLList(replace_type(type_.of_type)) # type: ignore + return GraphQLList(self.replace_type(type_.of_type)) # type: ignore if is_non_null_type(type_): - return GraphQLNonNull(replace_type(type_.of_type)) # type: ignore - return replace_named_type(type_) # type: ignore + return GraphQLNonNull(self.replace_type(type_.of_type)) # type: ignore + return self.replace_named_type(type_) # type: ignore - def replace_named_type(type_: GraphQLNamedType) -> GraphQLNamedType: + def replace_named_type(self, type_: GraphQLNamedType) -> GraphQLNamedType: # Note: While this could make early assertions to get the correctly # typed values below, that would throw immediately while type system # validation with validate_schema() will produce more actionable results. - return type_map[type_.name] + return self.type_map[type_.name] # noinspection PyShadowingNames - def replace_directive(directive: GraphQLDirective) -> GraphQLDirective: + def replace_directive(self, directive: GraphQLDirective) -> GraphQLDirective: + if is_specified_directive(directive): + # Builtin directives are not extended. + return directive + kwargs = directive.to_kwargs() + return GraphQLDirective( + **merge_kwargs( + kwargs, + args={ + name: self.extend_arg(arg) for name, arg in kwargs["args"].items() + }, + ) + ) + + def extend_directive(self, directive: GraphQLDirective) -> GraphQLDirective: kwargs = directive.to_kwargs() + extensions = tuple(self.directive_extensions_map[kwargs["name"]]) + deprecation_reason = kwargs["deprecation_reason"] + if deprecation_reason is None: + deprecation_reason = next( + ( + reason + for reason in (get_deprecation_reason(ext) for ext in extensions) + if reason is not None + ), + None, + ) return GraphQLDirective( **merge_kwargs( kwargs, - args={name: extend_arg(arg) for name, arg in kwargs["args"].items()}, + deprecation_reason=deprecation_reason, + extension_ast_nodes=kwargs["extension_ast_nodes"] + extensions, ) ) - def extend_named_type(type_: GraphQLNamedType) -> GraphQLNamedType: + def extend_named_type(self, type_: GraphQLNamedType) -> GraphQLNamedType: if is_introspection_type(type_) or is_specified_scalar_type(type_): # Builtin types are not extended. return type_ if is_scalar_type(type_): type_ = cast(GraphQLScalarType, type_) - return extend_scalar_type(type_) + return self.extend_scalar_type(type_) if is_object_type(type_): type_ = cast(GraphQLObjectType, type_) - return extend_object_type(type_) + return self.extend_object_type(type_) if is_interface_type(type_): type_ = cast(GraphQLInterfaceType, type_) - return extend_interface_type(type_) + return self.extend_interface_type(type_) if is_union_type(type_): type_ = cast(GraphQLUnionType, type_) - return extend_union_type(type_) + return self.extend_union_type(type_) if is_enum_type(type_): type_ = cast(GraphQLEnumType, type_) - return extend_enum_type(type_) + return self.extend_enum_type(type_) if is_input_object_type(type_): type_ = cast(GraphQLInputObjectType, type_) - return extend_input_object_type(type_) + return self.extend_input_object_type(type_) # Not reachable. All possible types have been considered. raise TypeError(f"Unexpected type: {inspect(type_)}.") # pragma: no cover + def extend_input_object_type_fields( + self, kwargs: GraphQLInputObjectTypeKwargs, extensions: Tuple[Any, ...] + ) -> GraphQLInputFieldMap: + return { + **{ + name: GraphQLInputField( + **merge_kwargs( + field.to_kwargs(), + type_=self.replace_type(field.type), + ) + ) + for name, field in kwargs["fields"].items() + }, + **self.build_input_field_map(extensions), + } + # noinspection PyShadowingNames def extend_input_object_type( + self, type_: GraphQLInputObjectType, ) -> GraphQLInputObjectType: kwargs = type_.to_kwargs() - extensions = tuple(type_extensions_map[kwargs["name"]]) + extensions = tuple(self.type_extensions_map[kwargs["name"]]) return GraphQLInputObjectType( **merge_kwargs( kwargs, - fields=lambda: { - **{ - name: GraphQLInputField( - **merge_kwargs( - field.to_kwargs(), - type_=replace_type(field.type), - ) - ) - for name, field in kwargs["fields"].items() - }, - **build_input_field_map(extensions), - }, + fields=partial( + self.extend_input_object_type_fields, kwargs, extensions + ), extension_ast_nodes=kwargs["extension_ast_nodes"] + extensions, ) ) - def extend_enum_type(type_: GraphQLEnumType) -> GraphQLEnumType: + def extend_enum_type(self, type_: GraphQLEnumType) -> GraphQLEnumType: kwargs = type_.to_kwargs() - extensions = tuple(type_extensions_map[kwargs["name"]]) + extensions = tuple(self.type_extensions_map[kwargs["name"]]) return GraphQLEnumType( **merge_kwargs( kwargs, - values={**kwargs["values"], **build_enum_value_map(extensions)}, + values={**kwargs["values"], **self.build_enum_value_map(extensions)}, extension_ast_nodes=kwargs["extension_ast_nodes"] + extensions, ) ) - def extend_scalar_type(type_: GraphQLScalarType) -> GraphQLScalarType: + def extend_scalar_type(self, type_: GraphQLScalarType) -> GraphQLScalarType: kwargs = type_.to_kwargs() - extensions = tuple(type_extensions_map[kwargs["name"]]) + extensions = tuple(self.type_extensions_map[kwargs["name"]]) specified_by_url = kwargs["specified_by_url"] for extension_node in extensions: @@ -285,132 +406,174 @@ def extend_scalar_type(type_: GraphQLScalarType) -> GraphQLScalarType: ) ) + def extend_object_type_interfaces( + self, kwargs: GraphQLObjectTypeKwargs, extensions: Tuple[Any, ...] + ) -> List[GraphQLInterfaceType]: + return [ + cast(GraphQLInterfaceType, self.replace_named_type(interface)) + for interface in kwargs["interfaces"] + ] + self.build_interfaces(extensions) + + def extend_object_type_fields( + self, kwargs: GraphQLObjectTypeKwargs, extensions: Tuple[Any, ...] + ) -> GraphQLFieldMap: + return { + **{ + name: self.extend_field(field) + for name, field in kwargs["fields"].items() + }, + **self.build_field_map(extensions), + } + # noinspection PyShadowingNames - def extend_object_type(type_: GraphQLObjectType) -> GraphQLObjectType: + def extend_object_type(self, type_: GraphQLObjectType) -> GraphQLObjectType: kwargs = type_.to_kwargs() - extensions = tuple(type_extensions_map[kwargs["name"]]) + extensions = tuple(self.type_extensions_map[kwargs["name"]]) return GraphQLObjectType( **merge_kwargs( kwargs, - interfaces=lambda: [ - cast(GraphQLInterfaceType, replace_named_type(interface)) - for interface in kwargs["interfaces"] - ] - + build_interfaces(extensions), - fields=lambda: { - **{ - name: extend_field(field) - for name, field in kwargs["fields"].items() - }, - **build_field_map(extensions), - }, + interfaces=partial( + self.extend_object_type_interfaces, kwargs, extensions + ), + fields=partial(self.extend_object_type_fields, kwargs, extensions), extension_ast_nodes=kwargs["extension_ast_nodes"] + extensions, ) ) + def extend_interface_type_interfaces( + self, kwargs: GraphQLInterfaceTypeKwargs, extensions: Tuple[Any, ...] + ) -> List[GraphQLInterfaceType]: + return [ + cast(GraphQLInterfaceType, self.replace_named_type(interface)) + for interface in kwargs["interfaces"] + ] + self.build_interfaces(extensions) + + def extend_interface_type_fields( + self, kwargs: GraphQLInterfaceTypeKwargs, extensions: Tuple[Any, ...] + ) -> GraphQLFieldMap: + return { + **{ + name: self.extend_field(field) + for name, field in kwargs["fields"].items() + }, + **self.build_field_map(extensions), + } + # noinspection PyShadowingNames - def extend_interface_type(type_: GraphQLInterfaceType) -> GraphQLInterfaceType: + def extend_interface_type( + self, type_: GraphQLInterfaceType + ) -> GraphQLInterfaceType: kwargs = type_.to_kwargs() - extensions = tuple(type_extensions_map[kwargs["name"]]) + extensions = tuple(self.type_extensions_map[kwargs["name"]]) return GraphQLInterfaceType( **merge_kwargs( kwargs, - interfaces=lambda: [ - cast(GraphQLInterfaceType, replace_named_type(interface)) - for interface in kwargs["interfaces"] - ] - + build_interfaces(extensions), - fields=lambda: { - **{ - name: extend_field(field) - for name, field in kwargs["fields"].items() - }, - **build_field_map(extensions), - }, + interfaces=partial( + self.extend_interface_type_interfaces, kwargs, extensions + ), + fields=partial(self.extend_interface_type_fields, kwargs, extensions), extension_ast_nodes=kwargs["extension_ast_nodes"] + extensions, ) ) - def extend_union_type(type_: GraphQLUnionType) -> GraphQLUnionType: + def extend_union_type_types( + self, kwargs: GraphQLUnionTypeKwargs, extensions: Tuple[Any, ...] + ) -> List[GraphQLObjectType]: + return [ + cast(GraphQLObjectType, self.replace_named_type(member_type)) + for member_type in kwargs["types"] + ] + self.build_union_types(extensions) + + def extend_union_type(self, type_: GraphQLUnionType) -> GraphQLUnionType: kwargs = type_.to_kwargs() - extensions = tuple(type_extensions_map[kwargs["name"]]) + extensions = tuple(self.type_extensions_map[kwargs["name"]]) return GraphQLUnionType( **merge_kwargs( kwargs, - types=lambda: [ - cast(GraphQLObjectType, replace_named_type(member_type)) - for member_type in kwargs["types"] - ] - + build_union_types(extensions), + types=partial(self.extend_union_type_types, kwargs, extensions), extension_ast_nodes=kwargs["extension_ast_nodes"] + extensions, - ) + ), ) # noinspection PyShadowingNames - def extend_field(field: GraphQLField) -> GraphQLField: + def extend_field(self, field: GraphQLField) -> GraphQLField: return GraphQLField( **merge_kwargs( field.to_kwargs(), - type_=replace_type(field.type), - args={name: extend_arg(arg) for name, arg in field.args.items()}, + type_=self.replace_type(field.type), + args={name: self.extend_arg(arg) for name, arg in field.args.items()}, ) ) - def extend_arg(arg: GraphQLArgument) -> GraphQLArgument: + def extend_arg(self, arg: GraphQLArgument) -> GraphQLArgument: return GraphQLArgument( **merge_kwargs( arg.to_kwargs(), - type_=replace_type(arg.type), + type_=self.replace_type(arg.type), ) ) # noinspection PyShadowingNames def get_operation_types( - nodes: Collection[Union[SchemaDefinitionNode, SchemaExtensionNode]] + self, nodes: Collection[Union[SchemaDefinitionNode, SchemaExtensionNode]] ) -> Dict[OperationType, GraphQLNamedType]: # Note: While this could make early assertions to get the correctly # typed values below, that would throw immediately while type system # validation with validate_schema() will produce more actionable results. return { - operation_type.operation: get_named_type(operation_type.type) + operation_type.operation: self.get_named_type(operation_type.type) for node in nodes for operation_type in node.operation_types or [] } # noinspection PyShadowingNames - def get_named_type(node: NamedTypeNode) -> GraphQLNamedType: + def get_named_type(self, node: NamedTypeNode) -> GraphQLNamedType: name = node.name.value - type_ = std_type_map.get(name) or type_map.get(name) + type_ = std_type_map.get(name) or self.type_map.get(name) if not type_: raise TypeError(f"Unknown type: '{name}'.") return type_ - def get_wrapped_type(node: TypeNode) -> GraphQLType: + def get_wrapped_type(self, node: TypeNode) -> GraphQLType: if isinstance(node, ListTypeNode): - return GraphQLList(get_wrapped_type(node.type)) + return GraphQLList(self.get_wrapped_type(node.type)) if isinstance(node, NonNullTypeNode): return GraphQLNonNull( - cast(GraphQLNullableType, get_wrapped_type(node.type)) + cast(GraphQLNullableType, self.get_wrapped_type(node.type)) ) - return get_named_type(cast(NamedTypeNode, node)) + return self.get_named_type(cast(NamedTypeNode, node)) - def build_directive(node: DirectiveDefinitionNode) -> GraphQLDirective: + def build_directive(self, node: DirectiveDefinitionNode) -> GraphQLDirective: locations = [DirectiveLocation[node.value] for node in node.locations] + extensions = tuple(self.directive_extensions_map[node.name.value]) + deprecation_reason = get_deprecation_reason(node) + if deprecation_reason is None: + deprecation_reason = next( + ( + reason + for reason in (get_deprecation_reason(ext) for ext in extensions) + if reason is not None + ), + None, + ) return GraphQLDirective( name=node.name.value, description=node.description.value if node.description else None, locations=locations, is_repeatable=node.repeatable, - args=build_argument_map(node.arguments), + args=self.build_argument_map(node.arguments), + deprecation_reason=deprecation_reason, ast_node=node, + extension_ast_nodes=extensions, ) def build_field_map( + self, nodes: Collection[ Union[ InterfaceTypeDefinitionNode, @@ -427,15 +590,16 @@ def build_field_map( # value, that would throw immediately while type system validation # with validate_schema() will produce more actionable results. field_map[field.name.value] = GraphQLField( - type_=cast(GraphQLOutputType, get_wrapped_type(field.type)), + type_=cast(GraphQLOutputType, self.get_wrapped_type(field.type)), description=field.description.value if field.description else None, - args=build_argument_map(field.arguments), + args=self.build_argument_map(field.arguments), deprecation_reason=get_deprecation_reason(field), ast_node=field, ) return field_map def build_argument_map( + self, args: Optional[Collection[InputValueDefinitionNode]], ) -> GraphQLArgumentMap: arg_map: GraphQLArgumentMap = {} @@ -443,7 +607,7 @@ def build_argument_map( # Note: While this could make assertions to get the correctly typed # value, that would throw immediately while type system validation # with validate_schema() will produce more actionable results. - type_ = cast(GraphQLInputType, get_wrapped_type(arg.type)) + type_ = cast(GraphQLInputType, self.get_wrapped_type(arg.type)) arg_map[arg.name.value] = GraphQLArgument( type_=type_, description=arg.description.value if arg.description else None, @@ -454,6 +618,7 @@ def build_argument_map( return arg_map def build_input_field_map( + self, nodes: Collection[ Union[InputObjectTypeDefinitionNode, InputObjectTypeExtensionNode] ], @@ -464,7 +629,7 @@ def build_input_field_map( # Note: While this could make assertions to get the correctly typed # value, that would throw immediately while type system validation # with validate_schema() will produce more actionable results. - type_ = cast(GraphQLInputType, get_wrapped_type(field.type)) + type_ = cast(GraphQLInputType, self.get_wrapped_type(field.type)) input_field_map[field.name.value] = GraphQLInputField( type_=type_, description=field.description.value if field.description else None, @@ -474,8 +639,9 @@ def build_input_field_map( ) return input_field_map + @staticmethod def build_enum_value_map( - nodes: Collection[Union[EnumTypeDefinitionNode, EnumTypeExtensionNode]] + nodes: Collection[Union[EnumTypeDefinitionNode, EnumTypeExtensionNode]], ) -> GraphQLEnumValueMap: enum_value_map: GraphQLEnumValueMap = {} for node in nodes: @@ -493,6 +659,7 @@ def build_enum_value_map( return enum_value_map def build_interfaces( + self, nodes: Collection[ Union[ InterfaceTypeDefinitionNode, @@ -502,29 +669,32 @@ def build_interfaces( ] ], ) -> List[GraphQLInterfaceType]: - interfaces: List[GraphQLInterfaceType] = [] - for node in nodes: - for type_ in node.interfaces or []: - # Note: While this could make assertions to get the correctly typed - # value, that would throw immediately while type system validation - # with validate_schema() will produce more actionable results. - interfaces.append(cast(GraphQLInterfaceType, get_named_type(type_))) - return interfaces + # Note: While this could make assertions to get the correctly typed + # value, that would throw immediately while type system validation + # with validate_schema() will produce more actionable results. + return [ + cast(GraphQLInterfaceType, self.get_named_type(type_)) + for node in nodes + for type_ in node.interfaces or [] + ] def build_union_types( + self, nodes: Collection[Union[UnionTypeDefinitionNode, UnionTypeExtensionNode]], ) -> List[GraphQLObjectType]: - types: List[GraphQLObjectType] = [] - for node in nodes: - for type_ in node.types or []: - # Note: While this could make assertions to get the correctly typed - # value, that would throw immediately while type system validation - # with validate_schema() will produce more actionable results. - types.append(cast(GraphQLObjectType, get_named_type(type_))) - return types + # Note: While this could make assertions to get the correctly typed + # value, that would throw immediately while type system validation + # with validate_schema() will produce more actionable results. + return [ + cast(GraphQLObjectType, self.get_named_type(type_)) + for node in nodes + for type_ in node.types or [] + ] - def build_object_type(ast_node: ObjectTypeDefinitionNode) -> GraphQLObjectType: - extension_nodes = type_extensions_map[ast_node.name.value] + def build_object_type( + self, ast_node: ObjectTypeDefinitionNode + ) -> GraphQLObjectType: + extension_nodes = self.type_extensions_map[ast_node.name.value] all_nodes: List[Union[ObjectTypeDefinitionNode, ObjectTypeExtensionNode]] = [ ast_node, *extension_nodes, @@ -532,30 +702,31 @@ def build_object_type(ast_node: ObjectTypeDefinitionNode) -> GraphQLObjectType: return GraphQLObjectType( name=ast_node.name.value, description=ast_node.description.value if ast_node.description else None, - interfaces=lambda: build_interfaces(all_nodes), - fields=lambda: build_field_map(all_nodes), + interfaces=partial(self.build_interfaces, all_nodes), + fields=partial(self.build_field_map, all_nodes), ast_node=ast_node, extension_ast_nodes=extension_nodes, ) def build_interface_type( + self, ast_node: InterfaceTypeDefinitionNode, ) -> GraphQLInterfaceType: - extension_nodes = type_extensions_map[ast_node.name.value] + extension_nodes = self.type_extensions_map[ast_node.name.value] all_nodes: List[ Union[InterfaceTypeDefinitionNode, InterfaceTypeExtensionNode] ] = [ast_node, *extension_nodes] return GraphQLInterfaceType( name=ast_node.name.value, description=ast_node.description.value if ast_node.description else None, - interfaces=lambda: build_interfaces(all_nodes), - fields=lambda: build_field_map(all_nodes), + interfaces=partial(self.build_interfaces, all_nodes), + fields=partial(self.build_field_map, all_nodes), ast_node=ast_node, extension_ast_nodes=extension_nodes, ) - def build_enum_type(ast_node: EnumTypeDefinitionNode) -> GraphQLEnumType: - extension_nodes = type_extensions_map[ast_node.name.value] + def build_enum_type(self, ast_node: EnumTypeDefinitionNode) -> GraphQLEnumType: + extension_nodes = self.type_extensions_map[ast_node.name.value] all_nodes: List[Union[EnumTypeDefinitionNode, EnumTypeExtensionNode]] = [ ast_node, *extension_nodes, @@ -563,13 +734,13 @@ def build_enum_type(ast_node: EnumTypeDefinitionNode) -> GraphQLEnumType: return GraphQLEnumType( name=ast_node.name.value, description=ast_node.description.value if ast_node.description else None, - values=build_enum_value_map(all_nodes), + values=self.build_enum_value_map(all_nodes), ast_node=ast_node, extension_ast_nodes=extension_nodes, ) - def build_union_type(ast_node: UnionTypeDefinitionNode) -> GraphQLUnionType: - extension_nodes = type_extensions_map[ast_node.name.value] + def build_union_type(self, ast_node: UnionTypeDefinitionNode) -> GraphQLUnionType: + extension_nodes = self.type_extensions_map[ast_node.name.value] all_nodes: List[Union[UnionTypeDefinitionNode, UnionTypeExtensionNode]] = [ ast_node, *extension_nodes, @@ -577,13 +748,15 @@ def build_union_type(ast_node: UnionTypeDefinitionNode) -> GraphQLUnionType: return GraphQLUnionType( name=ast_node.name.value, description=ast_node.description.value if ast_node.description else None, - types=lambda: build_union_types(all_nodes), + types=partial(self.build_union_types, all_nodes), ast_node=ast_node, extension_ast_nodes=extension_nodes, ) - def build_scalar_type(ast_node: ScalarTypeDefinitionNode) -> GraphQLScalarType: - extension_nodes = type_extensions_map[ast_node.name.value] + def build_scalar_type( + self, ast_node: ScalarTypeDefinitionNode + ) -> GraphQLScalarType: + extension_nodes = self.type_extensions_map[ast_node.name.value] return GraphQLScalarType( name=ast_node.name.value, description=ast_node.description.value if ast_node.description else None, @@ -593,83 +766,37 @@ def build_scalar_type(ast_node: ScalarTypeDefinitionNode) -> GraphQLScalarType: ) def build_input_object_type( + self, ast_node: InputObjectTypeDefinitionNode, ) -> GraphQLInputObjectType: - extension_nodes = type_extensions_map[ast_node.name.value] + extension_nodes = self.type_extensions_map[ast_node.name.value] all_nodes: List[ Union[InputObjectTypeDefinitionNode, InputObjectTypeExtensionNode] ] = [ast_node, *extension_nodes] return GraphQLInputObjectType( name=ast_node.name.value, description=ast_node.description.value if ast_node.description else None, - fields=lambda: build_input_field_map(all_nodes), + fields=partial(self.build_input_field_map, all_nodes), ast_node=ast_node, extension_ast_nodes=extension_nodes, + is_one_of=is_one_of(ast_node), ) - build_type_for_kind = cast( - Dict[str, Callable[[TypeDefinitionNode], GraphQLNamedType]], - { - "object_type_definition": build_object_type, - "interface_type_definition": build_interface_type, - "enum_type_definition": build_enum_type, - "union_type_definition": build_union_type, - "scalar_type_definition": build_scalar_type, - "input_object_type_definition": build_input_object_type, - }, - ) - - def build_type(ast_node: TypeDefinitionNode) -> GraphQLNamedType: + def build_type(self, ast_node: TypeDefinitionNode) -> GraphQLNamedType: + kind = ast_node.kind + try: + kind = kind.removesuffix("_definition") + except AttributeError: # pragma: no cover (Python < 3.9) + if kind.endswith("_definition"): + kind = kind[:-11] try: - # object_type_definition_node is built with _build_object_type etc. - build_function = build_type_for_kind[ast_node.kind] - except KeyError: # pragma: no cover + build = getattr(self, f"build_{kind}") + except AttributeError: # pragma: no cover # Not reachable. All possible type definition nodes have been considered. raise TypeError( # pragma: no cover f"Unexpected type definition node: {inspect(ast_node)}." ) - else: - return build_function(ast_node) - - type_map: Dict[str, GraphQLNamedType] = {} - for existing_type in schema_kwargs["types"] or (): - type_map[existing_type.name] = extend_named_type(existing_type) - for type_node in type_defs: - name = type_node.name.value - type_map[name] = std_type_map.get(name) or build_type(type_node) - - # Get the extended root operation types. - operation_types: Dict[OperationType, GraphQLNamedType] = {} - for operation_type in OperationType: - original_type = schema_kwargs[operation_type.value] - if original_type: - operation_types[operation_type] = replace_named_type(original_type) - # Then, incorporate schema definition and all schema extensions. - if schema_def: - operation_types.update(get_operation_types([schema_def])) - if schema_extensions: - operation_types.update(get_operation_types(schema_extensions)) - - # Then produce and return the kwargs for a Schema with these types. - get_operation = operation_types.get - return GraphQLSchemaKwargs( - query=get_operation(OperationType.QUERY), # type: ignore - mutation=get_operation(OperationType.MUTATION), # type: ignore - subscription=get_operation(OperationType.SUBSCRIPTION), # type: ignore - types=tuple(type_map.values()), - directives=tuple( - replace_directive(directive) for directive in schema_kwargs["directives"] - ) - + tuple(build_directive(directive) for directive in directive_defs), - description=schema_def.description.value - if schema_def and schema_def.description - else None, - extensions={}, - ast_node=schema_def or schema_kwargs["ast_node"], - extension_ast_nodes=schema_kwargs["extension_ast_nodes"] - + tuple(schema_extensions), - assume_valid=assume_valid, - ) + return build(ast_node) std_type_map: Mapping[str, Union[GraphQLNamedType, GraphQLObjectType]] = { @@ -679,7 +806,13 @@ def build_type(ast_node: TypeDefinitionNode) -> GraphQLNamedType: def get_deprecation_reason( - node: Union[EnumValueDefinitionNode, FieldDefinitionNode, InputValueDefinitionNode] + node: Union[ + EnumValueDefinitionNode, + FieldDefinitionNode, + InputValueDefinitionNode, + DirectiveDefinitionNode, + DirectiveExtensionNode, + ], ) -> Optional[str]: """Given a field or enum value node, get deprecation reason as string.""" from ..execution import get_directive_values @@ -689,10 +822,17 @@ def get_deprecation_reason( def get_specified_by_url( - node: Union[ScalarTypeDefinitionNode, ScalarTypeExtensionNode] + node: Union[ScalarTypeDefinitionNode, ScalarTypeExtensionNode], ) -> Optional[str]: """Given a scalar node, return the string value for the specifiedByURL.""" from ..execution import get_directive_values specified_by_url = get_directive_values(GraphQLSpecifiedByDirective, node) return specified_by_url["url"] if specified_by_url else None + + +def is_one_of(node: InputObjectTypeDefinitionNode) -> bool: + """Given an input object node, returns if the node should be OneOf.""" + from ..execution import get_directive_values + + return get_directive_values(GraphQLOneOfDirective, node) is not None diff --git a/src/graphql/utilities/find_breaking_changes.py b/src/graphql/utilities/find_breaking_changes.py index 281a2def..290b9e2b 100644 --- a/src/graphql/utilities/find_breaking_changes.py +++ b/src/graphql/utilities/find_breaking_changes.py @@ -134,7 +134,7 @@ def find_directive_changes( ) ) - for (old_directive, new_directive) in directives_diff.persisted: + for old_directive, new_directive in directives_diff.persisted: args_diff = dict_diff(old_directive.args, new_directive.args) for arg_name, new_arg in args_diff.added.items(): @@ -185,10 +185,12 @@ def find_type_changes( schema_changes.append( BreakingChange( BreakingChangeType.TYPE_REMOVED, - f"Standard scalar {type_name} was removed" - " because it is not referenced anymore." - if is_specified_scalar_type(old_type) - else f"{type_name} was removed.", + ( + f"Standard scalar {type_name} was removed" + " because it is not referenced anymore." + if is_specified_scalar_type(old_type) + else f"{type_name} was removed." + ), ) ) diff --git a/src/graphql/utilities/get_introspection_query.py b/src/graphql/utilities/get_introspection_query.py index aed2348a..c06c02cf 100644 --- a/src/graphql/utilities/get_introspection_query.py +++ b/src/graphql/utilities/get_introspection_query.py @@ -4,9 +4,9 @@ from ..language import DirectiveLocation try: - from typing import TypedDict, Literal + from typing import Literal, TypedDict except ImportError: # Python < 3.8 - from typing_extensions import TypedDict, Literal # type: ignore + from typing_extensions import Literal, TypedDict __all__ = [ "get_introspection_query", @@ -34,36 +34,65 @@ def get_introspection_query( directive_is_repeatable: bool = False, schema_description: bool = False, input_value_deprecation: bool = False, + experimental_directive_deprecation: bool = False, + input_object_one_of: bool = False, + type_depth: int = 9, ) -> str: """Get a query for introspection. Optionally, you can exclude descriptions, include specification URLs, include repeatability of directives, and specify whether to include the schema description as well. + + The ``type_depth`` argument controls how deep to recurse into nested types. + Larger values will result in more accurate results, but have a higher load + on the server. Some servers might restrict the maximum query depth or + complexity. If that's the case, try decreasing this value. The default is 9. """ maybe_description = "description" if descriptions else "" maybe_specified_by_url = "specifiedByURL" if specified_by_url else "" maybe_directive_is_repeatable = "isRepeatable" if directive_is_repeatable else "" maybe_schema_description = maybe_description if schema_description else "" + maybe_input_object_one_of = "isOneOf" if input_object_one_of else "" def input_deprecation(string: str) -> Optional[str]: return string if input_value_deprecation else "" - return dedent( - f""" + def directive_deprecation(string: str) -> Optional[str]: + return string if experimental_directive_deprecation else "" + + def of_type(level: int, indent: str) -> str: + if level <= 0: + return "" + if level > 100: + msg = ( + "Please set type_depth to a reasonable value" + " between 0 and 100; the default is 9." + ) + raise ValueError(msg) + return ( + f"\n{indent}ofType {{" + f"\n{indent} name" + f"\n{indent} kind{of_type(level - 1, indent + ' ')}" + f"\n{indent}}}" + ) + + return dedent(f""" query IntrospectionQuery {{ __schema {{ {maybe_schema_description} - queryType {{ name }} - mutationType {{ name }} - subscriptionType {{ name }} + queryType {{ name kind }} + mutationType {{ name kind }} + subscriptionType {{ name kind }} types {{ ...FullType }} - directives {{ + directives{directive_deprecation("(includeDeprecated: true)")} {{ name {maybe_description} {maybe_directive_is_repeatable} + {directive_deprecation("isDeprecated")} + {directive_deprecation("deprecationReason")} locations args{input_deprecation("(includeDeprecated: true)")} {{ ...InputValue @@ -77,6 +106,7 @@ def input_deprecation(string: str) -> Optional[str]: name {maybe_description} {maybe_specified_by_url} + {maybe_input_object_one_of} fields(includeDeprecated: true) {{ name {maybe_description} @@ -117,38 +147,9 @@ def input_deprecation(string: str) -> Optional[str]: fragment TypeRef on __Type {{ kind - name - ofType {{ - kind - name - ofType {{ - kind - name - ofType {{ - kind - name - ofType {{ - kind - name - ofType {{ - kind - name - ofType {{ - kind - name - ofType {{ - kind - name - }} - }} - }} - }} - }} - }} - }} + name{of_type(type_depth, " ")} }} - """ - ) + """) # Unfortunately, the following type definitions are a bit simplistic @@ -200,7 +201,7 @@ class MaybeWithIsRepeatable(TypedDict, total=False): isRepeatable: bool -class IntrospectionDirective(WithName, MaybeWithIsRepeatable): +class IntrospectionDirective(WithName, MaybeWithIsRepeatable, MaybeWithDeprecated): locations: List[DirectiveLocation] args: List[IntrospectionInputValue] @@ -235,6 +236,7 @@ class IntrospectionEnumType(WithName): class IntrospectionInputObjectType(WithName): kind: Literal["input_object"] inputFields: List[IntrospectionInputValue] + isOneOf: bool IntrospectionType = Union[ @@ -246,7 +248,6 @@ class IntrospectionInputObjectType(WithName): IntrospectionInputObjectType, ] - IntrospectionOutputType = Union[ IntrospectionScalarType, IntrospectionObjectType, @@ -255,7 +256,6 @@ class IntrospectionInputObjectType(WithName): IntrospectionEnumType, ] - IntrospectionInputType = Union[ IntrospectionScalarType, IntrospectionEnumType, IntrospectionInputObjectType ] @@ -284,7 +284,9 @@ class IntrospectionSchema(MaybeWithDescription): directives: List[IntrospectionDirective] -class IntrospectionQuery(TypedDict): - """The root typed dictionary for schema introspections.""" - - __schema: IntrospectionSchema +# The root typed dictionary for schema introspections. +# Note: We don't use class syntax here since the key looks like a private attribute. +IntrospectionQuery = TypedDict( + "IntrospectionQuery", + {"__schema": IntrospectionSchema}, +) diff --git a/src/graphql/utilities/get_operation_root_type.py b/src/graphql/utilities/get_operation_root_type.py index be0e5e6e..1a434e8c 100644 --- a/src/graphql/utilities/get_operation_root_type.py +++ b/src/graphql/utilities/get_operation_root_type.py @@ -17,8 +17,12 @@ def get_operation_root_type( ) -> GraphQLObjectType: """Extract the root type of the operation from the schema. + This deprecated helper is retained for backwards compatibility; call + :meth:`GraphQLSchema.get_root_type ` + instead because ``get_operation_root_type`` will be removed in v3.3. + .. deprecated:: 3.2 - Please use `GraphQLSchema.getRootType` instead. Will be removed in v3.3. + Please use ``GraphQLSchema.get_root_type`` instead. Will be removed in v3.3. """ operation_type = operation.operation if operation_type == OperationType.QUERY: diff --git a/src/graphql/utilities/introspection_from_schema.py b/src/graphql/utilities/introspection_from_schema.py index e0634860..4d36f2f7 100644 --- a/src/graphql/utilities/introspection_from_schema.py +++ b/src/graphql/utilities/introspection_from_schema.py @@ -15,6 +15,8 @@ def introspection_from_schema( directive_is_repeatable: bool = True, schema_description: bool = True, input_value_deprecation: bool = True, + experimental_directive_deprecation: bool = True, + input_object_one_of: bool = True, ) -> IntrospectionQuery: """Build an IntrospectionQuery from a GraphQLSchema @@ -31,6 +33,8 @@ def introspection_from_schema( directive_is_repeatable, schema_description, input_value_deprecation, + experimental_directive_deprecation, + input_object_one_of, ) ) diff --git a/src/graphql/utilities/lexicographic_sort_schema.py b/src/graphql/utilities/lexicographic_sort_schema.py index de6326cb..8b813950 100644 --- a/src/graphql/utilities/lexicographic_sort_schema.py +++ b/src/graphql/utilities/lexicographic_sort_schema.py @@ -39,7 +39,7 @@ def lexicographic_sort_schema(schema: GraphQLSchema) -> GraphQLSchema: """ def replace_type( - type_: Union[GraphQLList, GraphQLNonNull, GraphQLNamedType] + type_: Union[GraphQLList, GraphQLNonNull, GraphQLNamedType], ) -> Union[GraphQLList, GraphQLNonNull, GraphQLNamedType]: if is_list_type(type_): return GraphQLList(replace_type(cast(GraphQLList, type_).of_type)) @@ -88,16 +88,17 @@ def sort_fields(fields_map: Dict[str, GraphQLField]) -> Dict[str, GraphQLField]: return fields def sort_input_fields( - fields_map: Dict[str, GraphQLInputField] + fields_map: Dict[str, GraphQLInputField], ) -> Dict[str, GraphQLInputField]: return { name: GraphQLInputField( - cast( - GraphQLInputType, replace_type(cast(GraphQLNamedType, field.type)) - ), - description=field.description, - default_value=field.default_value, - ast_node=field.ast_node, + **merge_kwargs( + field.to_kwargs(), + type_=cast( + GraphQLInputType, + replace_type(cast(GraphQLNamedType, field.type)), + ), + ) ) for name, field in sorted(fields_map.items()) } @@ -143,6 +144,7 @@ def sort_named_type(type_: GraphQLNamedType) -> GraphQLNamedType: val.value, description=val.description, deprecation_reason=val.deprecation_reason, + extensions=val.extensions, ast_node=val.ast_node, ) for name, val in sorted(type_.values.items()) @@ -167,23 +169,28 @@ def sort_named_type(type_: GraphQLNamedType) -> GraphQLNamedType: } return GraphQLSchema( - types=type_map.values(), - directives=[ - sort_directive(directive) - for directive in sorted(schema.directives, key=sort_by_name_key) - ], - query=cast(Optional[GraphQLObjectType], replace_maybe_type(schema.query_type)), - mutation=cast( - Optional[GraphQLObjectType], replace_maybe_type(schema.mutation_type) - ), - subscription=cast( - Optional[GraphQLObjectType], replace_maybe_type(schema.subscription_type) - ), - ast_node=schema.ast_node, + **merge_kwargs( + schema.to_kwargs(), + types=type_map.values(), + directives=[ + sort_directive(directive) + for directive in sorted(schema.directives, key=sort_by_name_key) + ], + query=cast( + Optional[GraphQLObjectType], replace_maybe_type(schema.query_type) + ), + mutation=cast( + Optional[GraphQLObjectType], replace_maybe_type(schema.mutation_type) + ), + subscription=cast( + Optional[GraphQLObjectType], + replace_maybe_type(schema.subscription_type), + ), + ) ) def sort_by_name_key( - type_: Union[GraphQLNamedType, GraphQLDirective, DirectiveLocation] + type_: Union[GraphQLNamedType, GraphQLDirective, DirectiveLocation], ) -> Tuple: return natural_comparison_key(type_.name) diff --git a/src/graphql/utilities/print_schema.py b/src/graphql/utilities/print_schema.py index 55a6a58a..5473dd19 100644 --- a/src/graphql/utilities/print_schema.py +++ b/src/graphql/utilities/print_schema.py @@ -144,7 +144,7 @@ def print_scalar(type_: GraphQLScalarType) -> str: def print_implemented_interfaces( - type_: Union[GraphQLObjectType, GraphQLInterfaceType] + type_: Union[GraphQLObjectType, GraphQLInterfaceType], ) -> str: interfaces = type_.interfaces return " implements " + " & ".join(i.name for i in interfaces) if interfaces else "" @@ -189,7 +189,12 @@ def print_input_object(type_: GraphQLInputObjectType) -> str: print_description(field, " ", not i) + " " + print_input_value(name, field) for i, (name, field) in enumerate(type_.fields.items()) ] - return print_description(type_) + f"input {type_.name}" + print_block(fields) + return ( + print_description(type_) + + f"input {type_.name}" + + (" @oneOf" if type_.is_one_of else "") + + print_block(fields) + ) def print_fields(type_: Union[GraphQLObjectType, GraphQLInterfaceType]) -> str: @@ -245,6 +250,7 @@ def print_directive(directive: GraphQLDirective) -> str: print_description(directive) + f"directive @{directive.name}" + print_args(directive.args) + + print_deprecated(directive.deprecation_reason) + (" repeatable" if directive.is_repeatable else "") + " on " + " | ".join(location.name for location in directive.locations) diff --git a/src/graphql/utilities/resolve_schema_coordinate.py b/src/graphql/utilities/resolve_schema_coordinate.py new file mode 100644 index 00000000..47fdbbc7 --- /dev/null +++ b/src/graphql/utilities/resolve_schema_coordinate.py @@ -0,0 +1,295 @@ +from typing import NamedTuple, Optional, Union + +from ..language import ( + ArgumentCoordinateNode, + DirectiveArgumentCoordinateNode, + DirectiveCoordinateNode, + MemberCoordinateNode, + SchemaCoordinateNode, + Source, + TypeCoordinateNode, + parse_schema_coordinate, +) +from ..pyutils import inspect +from ..type import ( + GraphQLArgument, + GraphQLDirective, + GraphQLEnumType, + GraphQLEnumValue, + GraphQLField, + GraphQLInputField, + GraphQLInputObjectType, + GraphQLInterfaceType, + GraphQLNamedType, + GraphQLObjectType, + GraphQLSchema, +) + +__all__ = [ + "resolve_schema_coordinate", + "resolve_ast_schema_coordinate", + "ResolvedNamedType", + "ResolvedField", + "ResolvedInputField", + "ResolvedEnumValue", + "ResolvedFieldArgument", + "ResolvedDirective", + "ResolvedDirectiveArgument", + "ResolvedSchemaElement", +] + + +class ResolvedNamedType(NamedTuple): + """A named type resolved from a schema coordinate.""" + + type: GraphQLNamedType + kind: str = "NamedType" + + +class ResolvedField(NamedTuple): + """A field resolved from a schema coordinate.""" + + type: Union[GraphQLObjectType, GraphQLInterfaceType] + field: GraphQLField + kind: str = "Field" + + +class ResolvedInputField(NamedTuple): + """An input field resolved from a schema coordinate.""" + + type: GraphQLInputObjectType + input_field: GraphQLInputField + kind: str = "InputField" + + +class ResolvedEnumValue(NamedTuple): + """An enum value resolved from a schema coordinate.""" + + type: GraphQLEnumType + enum_value: GraphQLEnumValue + kind: str = "EnumValue" + + +class ResolvedFieldArgument(NamedTuple): + """A field argument resolved from a schema coordinate.""" + + type: Union[GraphQLObjectType, GraphQLInterfaceType] + field: GraphQLField + field_argument: GraphQLArgument + kind: str = "FieldArgument" + + +class ResolvedDirective(NamedTuple): + """A directive resolved from a schema coordinate.""" + + directive: GraphQLDirective + kind: str = "Directive" + + +class ResolvedDirectiveArgument(NamedTuple): + """A directive argument resolved from a schema coordinate.""" + + directive: GraphQLDirective + directive_argument: GraphQLArgument + kind: str = "DirectiveArgument" + + +ResolvedSchemaElement = Union[ + ResolvedNamedType, + ResolvedField, + ResolvedInputField, + ResolvedEnumValue, + ResolvedFieldArgument, + ResolvedDirective, + ResolvedDirectiveArgument, +] + + +def resolve_schema_coordinate( + schema: GraphQLSchema, schema_coordinate: Union[str, Source] +) -> Optional[ResolvedSchemaElement]: + """Resolve a string schema coordinate in the context of a GraphQL schema. + + A schema coordinate is resolved in the context of a GraphQL schema to uniquely + identify a schema element. It returns None if the schema coordinate does not + resolve to a schema element, meta-field, or introspection schema element. It will + raise an error if the containing schema element (if applicable) does not exist. + + ``_ + """ + return resolve_ast_schema_coordinate( + schema, parse_schema_coordinate(schema_coordinate) + ) + + +def resolve_type_coordinate( + schema: GraphQLSchema, schema_coordinate: TypeCoordinateNode +) -> Optional[ResolvedNamedType]: + """TypeCoordinate : Name""" + # 1. Let {typeName} be the value of {Name}. + type_name = schema_coordinate.name.value + type_ = schema.get_type(type_name) + + # 2. Return the type in the {schema} named {typeName} if it exists. + if type_ is None: + return None + + return ResolvedNamedType(type_) + + +def resolve_member_coordinate( + schema: GraphQLSchema, schema_coordinate: MemberCoordinateNode +) -> Optional[Union[ResolvedField, ResolvedInputField, ResolvedEnumValue]]: + """MemberCoordinate : Name . Name""" + # 1. Let {typeName} be the value of the first {Name}. + # 2. Let {type} be the type in the {schema} named {typeName}. + type_name = schema_coordinate.name.value + type_ = schema.get_type(type_name) + + # 3. Assert: {type} must exist, and must be an Enum, Input Object, Object or + # Interface type. + if type_ is None: + raise TypeError( + f"Expected {inspect(type_name)} to be defined as a type in the schema." + ) + if not isinstance( + type_, + ( + GraphQLEnumType, + GraphQLInputObjectType, + GraphQLObjectType, + GraphQLInterfaceType, + ), + ): + raise TypeError( + f"Expected {inspect(type_name)}" + " to be an Enum, Input Object, Object or Interface type." + ) + + member_name = schema_coordinate.member_name.value + + # 4. If {type} is an Enum type: + if isinstance(type_, GraphQLEnumType): + # 1. Let {enumValueName} be the value of the second {Name}. + # 2. Return the enum value of {type} named {enumValueName} if it exists. + enum_value = type_.values.get(member_name) + if enum_value is None: + return None + return ResolvedEnumValue(type_, enum_value) + + # 5. Otherwise, if {type} is an Input Object type: + if isinstance(type_, GraphQLInputObjectType): + # 1. Let {inputFieldName} be the value of the second {Name}. + # 2. Return the input field of {type} named {inputFieldName} if it exists. + input_field = type_.fields.get(member_name) + if input_field is None: + return None + return ResolvedInputField(type_, input_field) + + # 6. Otherwise: + # 1. Let {fieldName} be the value of the second {Name}. + # 2. Return the field of {type} named {fieldName} if it exists. + field = type_.fields.get(member_name) + if field is None: + return None + return ResolvedField(type_, field) + + +def resolve_argument_coordinate( + schema: GraphQLSchema, schema_coordinate: ArgumentCoordinateNode +) -> Optional[ResolvedFieldArgument]: + """ArgumentCoordinate : Name . Name ( Name : )""" + # 1. Let {typeName} be the value of the first {Name}. + # 2. Let {type} be the type in the {schema} named {typeName}. + type_name = schema_coordinate.name.value + type_ = schema.get_type(type_name) + + # 3. Assert: {type} must exist, and be an Object or Interface type. + if type_ is None: + raise TypeError( + f"Expected {inspect(type_name)} to be defined as a type in the schema." + ) + if not isinstance(type_, (GraphQLObjectType, GraphQLInterfaceType)): + raise TypeError( + f"Expected {inspect(type_name)} to be an object type or interface type." + ) + + # 4. Let {fieldName} be the value of the second {Name}. + # 5. Let {field} be the field of {type} named {fieldName}. + field_name = schema_coordinate.field_name.value + field = type_.fields.get(field_name) + + # 6. Assert: {field} must exist. + if field is None: + raise TypeError( + f"Expected {inspect(field_name)} to exist as a field" + f" of type {inspect(type_name)} in the schema." + ) + + # 7. Let {fieldArgumentName} be the value of the third {Name}. + field_argument_name = schema_coordinate.argument_name.value + field_argument = field.args.get(field_argument_name) + + # 8. Return the argument of {field} named {fieldArgumentName} if it exists. + if field_argument is None: + return None + + return ResolvedFieldArgument(type_, field, field_argument) + + +def resolve_directive_coordinate( + schema: GraphQLSchema, schema_coordinate: DirectiveCoordinateNode +) -> Optional[ResolvedDirective]: + """DirectiveCoordinate : @ Name""" + # 1. Let {directiveName} be the value of {Name}. + directive_name = schema_coordinate.name.value + directive = schema.get_directive(directive_name) + + # 2. Return the directive in the {schema} named {directiveName} if it exists. + if directive is None: + return None + + return ResolvedDirective(directive) + + +def resolve_directive_argument_coordinate( + schema: GraphQLSchema, schema_coordinate: DirectiveArgumentCoordinateNode +) -> Optional[ResolvedDirectiveArgument]: + """DirectiveArgumentCoordinate : @ Name ( Name : )""" + # 1. Let {directiveName} be the value of the first {Name}. + # 2. Let {directive} be the directive in the {schema} named {directiveName}. + directive_name = schema_coordinate.name.value + directive = schema.get_directive(directive_name) + + # 3. Assert {directive} must exist. + if directive is None: + raise TypeError( + f"Expected {inspect(directive_name)}" + " to be defined as a directive in the schema." + ) + + # 4. Let {directiveArgumentName} be the value of the second {Name}. + directive_argument_name = schema_coordinate.argument_name.value + directive_argument = directive.args.get(directive_argument_name) + + # 5. Return the argument of {directive} named {directiveArgumentName} if it exists. + if directive_argument is None: + return None + + return ResolvedDirectiveArgument(directive, directive_argument) + + +def resolve_ast_schema_coordinate( + schema: GraphQLSchema, schema_coordinate: SchemaCoordinateNode +) -> Optional[ResolvedSchemaElement]: + """Resolve schema coordinate from a parsed SchemaCoordinate node.""" + if isinstance(schema_coordinate, TypeCoordinateNode): + return resolve_type_coordinate(schema, schema_coordinate) + if isinstance(schema_coordinate, MemberCoordinateNode): + return resolve_member_coordinate(schema, schema_coordinate) + if isinstance(schema_coordinate, ArgumentCoordinateNode): + return resolve_argument_coordinate(schema, schema_coordinate) + if isinstance(schema_coordinate, DirectiveCoordinateNode): + return resolve_directive_coordinate(schema, schema_coordinate) + # DirectiveArgumentCoordinateNode is the only remaining kind. + return resolve_directive_argument_coordinate(schema, schema_coordinate) diff --git a/src/graphql/utilities/type_comparators.py b/src/graphql/utilities/type_comparators.py index 62883785..170eb649 100644 --- a/src/graphql/utilities/type_comparators.py +++ b/src/graphql/utilities/type_comparators.py @@ -29,12 +29,12 @@ def is_equal_type(type_a: GraphQLType, type_b: GraphQLType) -> bool: # If either type is non-null, the other must also be non-null. if is_non_null_type(type_a) and is_non_null_type(type_b): # noinspection PyUnresolvedReferences - return is_equal_type(type_a.of_type, type_b.of_type) # type:ignore + return is_equal_type(type_a.of_type, type_b.of_type) # type: ignore # If either type is a list, the other must also be a list. if is_list_type(type_a) and is_list_type(type_b): # noinspection PyUnresolvedReferences - return is_equal_type(type_a.of_type, type_b.of_type) # type:ignore + return is_equal_type(type_a.of_type, type_b.of_type) # type: ignore # Otherwise the types are not equal. return False diff --git a/src/graphql/utilities/type_from_ast.py b/src/graphql/utilities/type_from_ast.py index d8f2a5be..5c4623b0 100644 --- a/src/graphql/utilities/type_from_ast.py +++ b/src/graphql/utilities/type_from_ast.py @@ -1,13 +1,13 @@ -from typing import cast, overload, Optional +from typing import Optional, cast, overload from ..language import ListTypeNode, NamedTypeNode, NonNullTypeNode, TypeNode from ..pyutils import inspect from ..type import ( - GraphQLSchema, - GraphQLNamedType, GraphQLList, + GraphQLNamedType, GraphQLNonNull, GraphQLNullableType, + GraphQLSchema, GraphQLType, ) @@ -17,27 +17,25 @@ @overload def type_from_ast( schema: GraphQLSchema, type_node: NamedTypeNode -) -> Optional[GraphQLNamedType]: - ... +) -> Optional[GraphQLNamedType]: ... @overload def type_from_ast( schema: GraphQLSchema, type_node: ListTypeNode -) -> Optional[GraphQLList]: - ... +) -> Optional[GraphQLList]: ... @overload def type_from_ast( schema: GraphQLSchema, type_node: NonNullTypeNode -) -> Optional[GraphQLNonNull]: - ... +) -> Optional[GraphQLNonNull]: ... @overload -def type_from_ast(schema: GraphQLSchema, type_node: TypeNode) -> Optional[GraphQLType]: - ... +def type_from_ast( + schema: GraphQLSchema, type_node: TypeNode +) -> Optional[GraphQLType]: ... def type_from_ast( diff --git a/src/graphql/utilities/type_info.py b/src/graphql/utilities/type_info.py index 80a4ef3d..8798c6dc 100644 --- a/src/graphql/utilities/type_info.py +++ b/src/graphql/utilities/type_info.py @@ -74,7 +74,8 @@ def __init__( Initial type may be provided in rare cases to facilitate traversals beginning somewhere other than documents. - The optional last parameter is deprecated and will be removed in v3.3. + The optional ``get_field_def_fn`` parameter is deprecated; omit it so that + TypeInfo uses its built-in field definition lookup. It will be removed in v3.3. """ self._schema = schema self._type_stack: List[Optional[GraphQLOutputType]] = [] diff --git a/src/graphql/utilities/value_from_ast.py b/src/graphql/utilities/value_from_ast.py index 62a506c8..774f52d7 100644 --- a/src/graphql/utilities/value_from_ast.py +++ b/src/graphql/utilities/value_from_ast.py @@ -120,6 +120,14 @@ def value_from_ast( return Undefined coerced_obj[field.out_name or field_name] = field_value + if type_.is_one_of: + keys = list(coerced_obj) + if len(keys) != 1: + return Undefined + + if coerced_obj[keys[0]] is None: + return Undefined + return type_.out_type(coerced_obj) if is_leaf_type(type_): diff --git a/src/graphql/validation/__init__.py b/src/graphql/validation/__init__.py index 313073a5..a2ddc97c 100644 --- a/src/graphql/validation/__init__.py +++ b/src/graphql/validation/__init__.py @@ -15,7 +15,7 @@ from .rules import ValidationRule, ASTValidationRule, SDLValidationRule # All validation rules in the GraphQL Specification. -from .specified_rules import specified_rules +from .specified_rules import specified_rules, recommended_rules # Spec Section: "Executable Definitions" from .rules.executable_definitions import ExecutableDefinitionsRule @@ -95,6 +95,9 @@ # Spec Section: "All Variable Usages Are Allowed" from .rules.variables_in_allowed_position import VariablesInAllowedPositionRule +# No spec section: "Maximum introspection depth" +from .rules.max_introspection_depth_rule import MaxIntrospectionDepthRule + # SDL-specific validation rules from .rules.lone_schema_definition import LoneSchemaDefinitionRule from .rules.unique_operation_types import UniqueOperationTypesRule @@ -118,6 +121,7 @@ "ValidationContext", "ValidationRule", "specified_rules", + "recommended_rules", "ExecutableDefinitionsRule", "FieldsOnCorrectTypeRule", "FragmentsOnCompositeTypesRule", @@ -126,6 +130,7 @@ "KnownFragmentNamesRule", "KnownTypeNamesRule", "LoneAnonymousOperationRule", + "MaxIntrospectionDepthRule", "NoFragmentCyclesRule", "NoUndefinedVariablesRule", "NoUnusedFragmentsRule", diff --git a/src/graphql/validation/rules/known_directives.py b/src/graphql/validation/rules/known_directives.py index 26c5c75b..883634da 100644 --- a/src/graphql/validation/rules/known_directives.py +++ b/src/graphql/validation/rules/known_directives.py @@ -95,6 +95,8 @@ def enter_directive( "enum_value_definition": DirectiveLocation.ENUM_VALUE, "input_object_type_definition": DirectiveLocation.INPUT_OBJECT, "input_object_type_extension": DirectiveLocation.INPUT_OBJECT, + "directive_definition": DirectiveLocation.DIRECTIVE_DEFINITION, + "directive_extension": DirectiveLocation.DIRECTIVE_DEFINITION, } diff --git a/src/graphql/validation/rules/known_type_names.py b/src/graphql/validation/rules/known_type_names.py index 68e10454..aa210899 100644 --- a/src/graphql/validation/rules/known_type_names.py +++ b/src/graphql/validation/rules/known_type_names.py @@ -62,9 +62,11 @@ def enter_named_type( suggested_types = suggestion_list( type_name, - list(standard_type_names) + self.type_names - if is_sdl - else self.type_names, + ( + list(standard_type_names) + self.type_names + if is_sdl + else self.type_names + ), ) self.report_error( GraphQLError( diff --git a/src/graphql/validation/rules/max_introspection_depth_rule.py b/src/graphql/validation/rules/max_introspection_depth_rule.py new file mode 100644 index 00000000..a6b6b4bc --- /dev/null +++ b/src/graphql/validation/rules/max_introspection_depth_rule.py @@ -0,0 +1,81 @@ +"""Max introspection depth rule""" + +from typing import Dict, Any + +from ...error import GraphQLError +from ...language import SKIP, FieldNode, FragmentSpreadNode, Node, VisitorAction +from . import ASTValidationRule, ValidationContext + +__all__ = ["MaxIntrospectionDepthRule"] + +MAX_LIST_DEPTH = 3 + + +class MaxIntrospectionDepthRule(ASTValidationRule): + """Checks maximum introspection depth""" + + def __init__(self, context: ValidationContext) -> None: + super().__init__(context) + self._visited_fragments: Dict[str, None] = {} + self._get_fragment = context.get_fragment + + def _check_depth(self, node: Node, depth: int = 0) -> bool: + """Check whether the maximum introspection depth has been reached. + + Counts the depth of list fields in "__Type" recursively + and returns `True` if the limit has been reached. + """ + if isinstance(node, FragmentSpreadNode): + visited_fragments = self._visited_fragments + fragment_name = node.name.value + if fragment_name in visited_fragments: + # Fragment cycles are handled by `NoFragmentCyclesRule`. + return False + fragment = self._get_fragment(fragment_name) + if not fragment: + # Missing fragments checks are handled by the `KnownFragmentNamesRule`. + return False + + # Rather than following an immutable programming pattern which has + # significant memory and garbage collection overhead, we've opted to take + # a mutable approach for efficiency's sake. Importantly visiting a fragment + # twice is fine, so long as you don't do one visit inside the other. + visited_fragments[fragment_name] = None + try: + return self._check_depth(fragment, depth) + finally: + del visited_fragments[fragment_name] + + if isinstance(node, FieldNode) and node.name.value in ( + # check all introspection lists + "fields", + "interfaces", + "possibleTypes", + "inputFields", + ): + depth += 1 + if depth >= MAX_LIST_DEPTH: + return True + + # hendle fields and inline fragments + try: + selection_set = node.selection_set # type: ignore[attr-defined] + except AttributeError: # pragma: no cover + selection_set = None + if selection_set: + for child in selection_set.selections: + if self._check_depth(child, depth): + return True + + return False + + def enter_field(self, node: FieldNode, *_args: Any) -> VisitorAction: + if node.name.value in ("__schema", "__type") and self._check_depth(node): + self.report_error( + GraphQLError( + "Maximum introspection depth exceeded", + [node], + ) + ) + return SKIP + return None diff --git a/src/graphql/validation/rules/no_undefined_variables.py b/src/graphql/validation/rules/no_undefined_variables.py index a890473f..7e4a6873 100644 --- a/src/graphql/validation/rules/no_undefined_variables.py +++ b/src/graphql/validation/rules/no_undefined_variables.py @@ -34,10 +34,12 @@ def leave_operation_definition( if var_name not in defined_variables: self.report_error( GraphQLError( - f"Variable '${var_name}' is not defined" - f" by operation '{operation.name.value}'." - if operation.name - else f"Variable '${var_name}' is not defined.", + ( + f"Variable '${var_name}' is not defined" + f" by operation '{operation.name.value}'." + if operation.name + else f"Variable '${var_name}' is not defined." + ), [node, operation], ) ) diff --git a/src/graphql/validation/rules/no_unused_variables.py b/src/graphql/validation/rules/no_unused_variables.py index b8770944..968b0a4b 100644 --- a/src/graphql/validation/rules/no_unused_variables.py +++ b/src/graphql/validation/rules/no_unused_variables.py @@ -37,10 +37,12 @@ def leave_operation_definition( if variable_name not in variable_name_used: self.report_error( GraphQLError( - f"Variable '${variable_name}' is never used" - f" in operation '{operation.name.value}'." - if operation.name - else f"Variable '${variable_name}' is never used.", + ( + f"Variable '${variable_name}' is never used" + f" in operation '{operation.name.value}'." + if operation.name + else f"Variable '${variable_name}' is never used." + ), variable_def, ) ) diff --git a/src/graphql/validation/rules/overlapping_fields_can_be_merged.py b/src/graphql/validation/rules/overlapping_fields_can_be_merged.py index 33c1b09e..0b117482 100644 --- a/src/graphql/validation/rules/overlapping_fields_can_be_merged.py +++ b/src/graphql/validation/rules/overlapping_fields_can_be_merged.py @@ -3,13 +3,13 @@ from ...error import GraphQLError from ...language import ( + DirectiveNode, FieldNode, FragmentDefinitionNode, FragmentSpreadNode, InlineFragmentNode, - ObjectFieldNode, - ObjectValueNode, SelectionSetNode, + ValueNode, print_ast, ) from ...type import ( @@ -56,9 +56,10 @@ class OverlappingFieldsCanBeMergedRule(ValidationRule): def __init__(self, context: ValidationContext): super().__init__(context) - # A memoization for when two fragments are compared "between" each other for - # conflicts. Two fragments may be compared many times, so memoizing this can - # dramatically improve the performance of this validator. + # A memoization for when fields and a fragment or two fragments are compared + # "between" each other for conflicts. Comparisons may be made many times, so + # memoizing this can dramatically improve the performance of this validator. + self.compared_fields_and_fragment_pairs = OrderedPairSet() self.compared_fragment_pairs = PairSet() # A cache for the "field map" and list of fragment names found in any given @@ -70,6 +71,7 @@ def enter_selection_set(self, selection_set: SelectionSetNode, *_args: Any) -> N conflicts = find_conflicts_within_selection_set( self.context, self.cached_fields_and_fragment_names, + self.compared_fields_and_fragment_pairs, self.compared_fragment_pairs, self.context.get_parent_type(), selection_set, @@ -156,6 +158,7 @@ def enter_selection_set(self, selection_set: SelectionSetNode, *_args: Any) -> N def find_conflicts_within_selection_set( context: ValidationContext, cached_fields_and_fragment_names: Dict, + compared_fields_and_fragment_pairs: "OrderedPairSet", compared_fragment_pairs: "PairSet", parent_type: Optional[GraphQLNamedType], selection_set: SelectionSetNode, @@ -179,6 +182,7 @@ def find_conflicts_within_selection_set( context, conflicts, cached_fields_and_fragment_names, + compared_fields_and_fragment_pairs, compared_fragment_pairs, field_map, ) @@ -191,6 +195,7 @@ def find_conflicts_within_selection_set( context, conflicts, cached_fields_and_fragment_names, + compared_fields_and_fragment_pairs, compared_fragment_pairs, False, field_map, @@ -205,6 +210,7 @@ def find_conflicts_within_selection_set( context, conflicts, cached_fields_and_fragment_names, + compared_fields_and_fragment_pairs, compared_fragment_pairs, False, fragment_name, @@ -218,6 +224,7 @@ def collect_conflicts_between_fields_and_fragment( context: ValidationContext, conflicts: List[Conflict], cached_fields_and_fragment_names: Dict, + compared_fields_and_fragment_pairs: "OrderedPairSet", compared_fragment_pairs: "PairSet", are_mutually_exclusive: bool, field_map: NodeAndDefCollection, @@ -228,6 +235,16 @@ def collect_conflicts_between_fields_and_fragment( Collect all conflicts found between a set of fields and a fragment reference including via spreading in any nested fragments. """ + # Memoize so the fields and fragments are not compared for conflicts more + # than once. + if compared_fields_and_fragment_pairs.has( + field_map, fragment_name, are_mutually_exclusive + ): + return + compared_fields_and_fragment_pairs.add( + field_map, fragment_name, are_mutually_exclusive + ) + fragment = context.get_fragment(fragment_name) if not fragment: return None @@ -246,6 +263,7 @@ def collect_conflicts_between_fields_and_fragment( context, conflicts, cached_fields_and_fragment_names, + compared_fields_and_fragment_pairs, compared_fragment_pairs, are_mutually_exclusive, field_map, @@ -255,19 +273,11 @@ def collect_conflicts_between_fields_and_fragment( # (E) Then collect any conflicts between the provided collection of fields and any # fragment names found in the given fragment. for referenced_fragment_name in referenced_fragment_names: - # Memoize so two fragments are not compared for conflicts more than once. - if compared_fragment_pairs.has( - referenced_fragment_name, fragment_name, are_mutually_exclusive - ): - continue - compared_fragment_pairs.add( - referenced_fragment_name, fragment_name, are_mutually_exclusive - ) - collect_conflicts_between_fields_and_fragment( context, conflicts, cached_fields_and_fragment_names, + compared_fields_and_fragment_pairs, compared_fragment_pairs, are_mutually_exclusive, field_map, @@ -279,6 +289,7 @@ def collect_conflicts_between_fragments( context: ValidationContext, conflicts: List[Conflict], cached_fields_and_fragment_names: Dict, + compared_fields_and_fragment_pairs: "OrderedPairSet", compared_fragment_pairs: "PairSet", are_mutually_exclusive: bool, fragment_name1: str, @@ -319,6 +330,7 @@ def collect_conflicts_between_fragments( context, conflicts, cached_fields_and_fragment_names, + compared_fields_and_fragment_pairs, compared_fragment_pairs, are_mutually_exclusive, field_map1, @@ -332,6 +344,7 @@ def collect_conflicts_between_fragments( context, conflicts, cached_fields_and_fragment_names, + compared_fields_and_fragment_pairs, compared_fragment_pairs, are_mutually_exclusive, fragment_name1, @@ -345,6 +358,7 @@ def collect_conflicts_between_fragments( context, conflicts, cached_fields_and_fragment_names, + compared_fields_and_fragment_pairs, compared_fragment_pairs, are_mutually_exclusive, referenced_fragment_name1, @@ -355,6 +369,7 @@ def collect_conflicts_between_fragments( def find_conflicts_between_sub_selection_sets( context: ValidationContext, cached_fields_and_fragment_names: Dict, + compared_fields_and_fragment_pairs: "OrderedPairSet", compared_fragment_pairs: "PairSet", are_mutually_exclusive: bool, parent_type1: Optional[GraphQLNamedType], @@ -382,6 +397,7 @@ def find_conflicts_between_sub_selection_sets( context, conflicts, cached_fields_and_fragment_names, + compared_fields_and_fragment_pairs, compared_fragment_pairs, are_mutually_exclusive, field_map1, @@ -396,6 +412,7 @@ def find_conflicts_between_sub_selection_sets( context, conflicts, cached_fields_and_fragment_names, + compared_fields_and_fragment_pairs, compared_fragment_pairs, are_mutually_exclusive, field_map1, @@ -410,6 +427,7 @@ def find_conflicts_between_sub_selection_sets( context, conflicts, cached_fields_and_fragment_names, + compared_fields_and_fragment_pairs, compared_fragment_pairs, are_mutually_exclusive, field_map2, @@ -425,6 +443,7 @@ def find_conflicts_between_sub_selection_sets( context, conflicts, cached_fields_and_fragment_names, + compared_fields_and_fragment_pairs, compared_fragment_pairs, are_mutually_exclusive, fragment_name1, @@ -438,6 +457,7 @@ def collect_conflicts_within( context: ValidationContext, conflicts: List[Conflict], cached_fields_and_fragment_names: Dict, + compared_fields_and_fragment_pairs: "OrderedPairSet", compared_fragment_pairs: "PairSet", field_map: NodeAndDefCollection, ) -> None: @@ -456,6 +476,7 @@ def collect_conflicts_within( conflict = find_conflict( context, cached_fields_and_fragment_names, + compared_fields_and_fragment_pairs, compared_fragment_pairs, # within one collection is never mutually exclusive False, @@ -471,6 +492,7 @@ def collect_conflicts_between( context: ValidationContext, conflicts: List[Conflict], cached_fields_and_fragment_names: Dict, + compared_fields_and_fragment_pairs: "OrderedPairSet", compared_fragment_pairs: "PairSet", parent_fields_are_mutually_exclusive: bool, field_map1: NodeAndDefCollection, @@ -496,6 +518,7 @@ def collect_conflicts_between( conflict = find_conflict( context, cached_fields_and_fragment_names, + compared_fields_and_fragment_pairs, compared_fragment_pairs, parent_fields_are_mutually_exclusive, response_name, @@ -509,6 +532,7 @@ def collect_conflicts_between( def find_conflict( context: ValidationContext, cached_fields_and_fragment_names: Dict, + compared_fields_and_fragment_pairs: "OrderedPairSet", compared_fragment_pairs: "PairSet", parent_fields_are_mutually_exclusive: bool, response_name: str, @@ -551,7 +575,7 @@ def find_conflict( ) # Two field calls must have the same arguments. - if stringify_arguments(node1) != stringify_arguments(node2): + if not same_arguments(node1, node2): return (response_name, "they have differing arguments"), [node1], [node2] if type1 and type2 and do_types_conflict(type1, type2): @@ -570,6 +594,7 @@ def find_conflict( conflicts = find_conflicts_between_sub_selection_sets( context, cached_fields_and_fragment_names, + compared_fields_and_fragment_pairs, compared_fragment_pairs, are_mutually_exclusive, get_named_type(type1), @@ -582,14 +607,34 @@ def find_conflict( return None # no conflict -def stringify_arguments(field_node: FieldNode) -> str: - input_object_with_args = ObjectValueNode( - fields=tuple( - ObjectFieldNode(name=arg_node.name, value=arg_node.value) - for arg_node in field_node.arguments - ) - ) - return print_ast(sort_value_node(input_object_with_args)) +def same_arguments( + node1: Union[FieldNode, DirectiveNode], node2: Union[FieldNode, DirectiveNode] +) -> bool: + args1 = node1.arguments + args2 = node2.arguments + + if not args1: + return not args2 + + if not args2: + return False + + if len(args1) != len(args2): + return False # pragma: no cover + + values2 = {arg.name.value: arg.value for arg in args2} + + for arg1 in args1: + value1 = arg1.value + value2 = values2.get(arg1.name.value) + if value2 is None or stringify_value(value1) != stringify_value(value2): + return False + + return True + + +def stringify_value(value: ValueNode) -> str: + return print_ast(sort_value_node(value)) def do_types_conflict(type1: GraphQLOutputType, type2: GraphQLOutputType) -> bool: @@ -725,6 +770,43 @@ def subfield_conflicts( return None # no conflict +class OrderedPairSet: + """Ordered pair set + + A way to keep track of pairs of things where the ordering of the pair matters. + + Provides a third argument for has/add to allow flagging the pair as weakly or + strongly present within the collection. + + The first element is matched by object identity (its ``id``), since field maps + are unhashable mappings that are kept alive for the duration of the validation. + """ + + __slots__ = ("_data",) + + _data: Dict[int, Dict[str, bool]] + + def __init__(self) -> None: + self._data = {} + + def has(self, a: NodeAndDefCollection, b: str, weakly_present: bool) -> bool: + map_ = self._data.get(id(a)) + if map_ is None: + return False + result = map_.get(b) + if result is None: + return False + + return True if weakly_present else weakly_present == result + + def add(self, a: NodeAndDefCollection, b: str, weakly_present: bool) -> None: + map_ = self._data.get(id(a)) + if map_ is None: + self._data[id(a)] = {b: weakly_present} + else: + map_[b] = weakly_present + + class PairSet: """Pair set diff --git a/src/graphql/validation/rules/scalar_leafs.py b/src/graphql/validation/rules/scalar_leafs.py index 174df4e4..549f45c5 100644 --- a/src/graphql/validation/rules/scalar_leafs.py +++ b/src/graphql/validation/rules/scalar_leafs.py @@ -39,3 +39,12 @@ def enter_field(self, node: FieldNode, *_args: Any) -> None: node, ) ) + elif not selection_set.selections: + field_name = node.name.value + self.report_error( + GraphQLError( + f"Field '{field_name}' of type '{type_}'" + " must have at least one field selected.", + node, + ) + ) diff --git a/src/graphql/validation/rules/unique_directives_per_location.py b/src/graphql/validation/rules/unique_directives_per_location.py index 98e592ac..d0e4d1f4 100644 --- a/src/graphql/validation/rules/unique_directives_per_location.py +++ b/src/graphql/validation/rules/unique_directives_per_location.py @@ -4,6 +4,7 @@ from ...error import GraphQLError from ...language import ( DirectiveDefinitionNode, + DirectiveExtensionNode, DirectiveNode, Node, SchemaDefinitionNode, @@ -51,6 +52,9 @@ def __init__(self, context: Union[ValidationContext, SDLValidationContext]): self.type_directives_map: Dict[str, Dict[str, DirectiveNode]] = defaultdict( dict ) + self.directive_directives_map: Dict[str, Dict[str, DirectiveNode]] = ( + defaultdict(dict) + ) # Many different AST nodes may contain directives. Rather than listing them all, # just listen for entering any node, and check to see if it defines any directives. @@ -66,6 +70,9 @@ def enter(self, node: Node, *_args: Any) -> None: node = cast(Union[TypeDefinitionNode, TypeExtensionNode], node) type_name = node.name.value seen_directives = self.type_directives_map[type_name] + elif isinstance(node, (DirectiveDefinitionNode, DirectiveExtensionNode)): + directive_name = node.name.value + seen_directives = self.directive_directives_map[directive_name] else: seen_directives = {} diff --git a/src/graphql/validation/rules/values_of_correct_type.py b/src/graphql/validation/rules/values_of_correct_type.py index 44dc6a0d..bb8dc924 100644 --- a/src/graphql/validation/rules/values_of_correct_type.py +++ b/src/graphql/validation/rules/values_of_correct_type.py @@ -1,4 +1,4 @@ -from typing import cast, Any +from typing import cast, Any, Mapping from ...error import GraphQLError from ...language import ( @@ -28,7 +28,7 @@ is_non_null_type, is_required_input_field, ) -from . import ValidationRule +from . import ValidationContext, ValidationRule __all__ = ["ValuesOfCorrectTypeRule"] @@ -70,6 +70,8 @@ def enter_object_value(self, node: ObjectValueNode, *_args: Any) -> VisitorActio node, ) ) + if type_.is_one_of: + validate_one_of_input_object(self.context, node, type_, field_node_map) return None def enter_object_field(self, node: ObjectFieldNode, *_args: Any) -> None: @@ -105,6 +107,11 @@ def enter_int_value(self, node: IntValueNode, *_args: Any) -> None: def enter_float_value(self, node: FloatValueNode, *_args: Any) -> None: self.is_valid_value_node(node) + # Descriptions are string values that would not validate according + # to the below logic, but since (per the specification) descriptions must + # not affect validation, they are ignored entirely when visiting the AST + # and do not require special handling. + # See https://spec.graphql.org/draft/#sec-Descriptions def enter_string_value(self, node: StringValueNode, *_args: Any) -> None: self.is_valid_value_node(node) @@ -161,3 +168,34 @@ def is_valid_value_node(self, node: ValueNode) -> None: ) return + + +def validate_one_of_input_object( + context: ValidationContext, + node: ObjectValueNode, + type_: GraphQLInputObjectType, + field_node_map: Mapping[str, ObjectFieldNode], +) -> None: + keys = list(field_node_map) + is_not_exactly_one_filed = len(keys) != 1 + + if is_not_exactly_one_filed: + context.report_error( + GraphQLError( + f"OneOf Input Object '{type_.name}' must specify exactly one key.", + node, + ) + ) + return + + object_field_node = field_node_map.get(keys[0]) + value = object_field_node.value if object_field_node else None + is_null_literal = not value or isinstance(value, NullValueNode) + + if is_null_literal: + context.report_error( + GraphQLError( + f"Field '{type_.name}.{keys[0]}' must be non-null.", + node, + ) + ) diff --git a/src/graphql/validation/rules/variables_in_allowed_position.py b/src/graphql/validation/rules/variables_in_allowed_position.py index 49d3b416..2d8e0b1b 100644 --- a/src/graphql/validation/rules/variables_in_allowed_position.py +++ b/src/graphql/validation/rules/variables_in_allowed_position.py @@ -8,7 +8,15 @@ VariableDefinitionNode, ) from ...pyutils import Undefined -from ...type import GraphQLNonNull, GraphQLSchema, GraphQLType, is_non_null_type +from ...type import ( + GraphQLInputObjectType, + GraphQLNonNull, + GraphQLSchema, + GraphQLType, + is_input_object_type, + is_non_null_type, + is_nullable_type, +) from ...utilities import type_from_ast, is_type_sub_type_of from . import ValidationContext, ValidationRule @@ -39,6 +47,7 @@ def leave_operation_definition( for usage in usages: node, type_ = usage.node, usage.type default_value = usage.default_value + parent_type = usage.parent_type var_name = node.name.value var_def = var_def_map.get(var_name) if var_def and type_: @@ -60,6 +69,20 @@ def leave_operation_definition( ) ) + if ( + is_input_object_type(parent_type) + and cast(GraphQLInputObjectType, parent_type).is_one_of + and is_nullable_type(var_type) + ): + self.report_error( + GraphQLError( + f"Variable '${var_name}' is of type '{var_type}'" + " but must be non-nullable to be used for OneOf" + f" Input Object '{parent_type}'.", + [var_def, node], + ) + ) + def enter_variable_definition( self, node: VariableDefinitionNode, *_args: Any ) -> None: diff --git a/src/graphql/validation/specified_rules.py b/src/graphql/validation/specified_rules.py index db990aeb..249586ee 100644 --- a/src/graphql/validation/specified_rules.py +++ b/src/graphql/validation/specified_rules.py @@ -82,6 +82,9 @@ # Spec Section: "Input Object Field Uniqueness" from .rules.unique_input_field_names import UniqueInputFieldNamesRule +# No spec section: "Maximum introspection depth" +from .rules.max_introspection_depth_rule import MaxIntrospectionDepthRule + # Schema definition language: from .rules.lone_schema_definition import LoneSchemaDefinitionRule from .rules.unique_operation_types import UniqueOperationTypesRule @@ -92,7 +95,13 @@ from .rules.unique_directive_names import UniqueDirectiveNamesRule from .rules.possible_type_extensions import PossibleTypeExtensionsRule -__all__ = ["specified_rules", "specified_sdl_rules"] +__all__ = ["specified_rules", "specified_sdl_rules", "recommended_rules"] + +# Technically these aren't part of the spec but they are strongly encouraged +# validation rules. + +recommended_rules: Tuple[Type[ASTValidationRule], ...] = (MaxIntrospectionDepthRule,) +"""A tuple with all recommended validation rules.""" # This list includes all validation rules defined by the GraphQL spec. @@ -127,6 +136,7 @@ VariablesInAllowedPositionRule, OverlappingFieldsCanBeMergedRule, UniqueInputFieldNamesRule, + *recommended_rules, ) """A tuple with all validation rules defined by the GraphQL specification. diff --git a/src/graphql/validation/validate.py b/src/graphql/validation/validate.py index 13dc5243..ff62fa70 100644 --- a/src/graphql/validation/validate.py +++ b/src/graphql/validation/validate.py @@ -1,9 +1,10 @@ -from typing import Collection, List, Optional, Type +from typing import Collection, Dict, List, Optional, Tuple, Type from ..error import GraphQLError from ..language import DocumentNode, ParallelVisitor, visit -from ..type import GraphQLSchema, assert_valid_schema +from ..language.ast import QUERY_DOCUMENT_KEYS from ..pyutils import inspect, is_collection +from ..type import GraphQLSchema, assert_valid_schema from ..utilities import TypeInfo, TypeInfoVisitor from .rules import ASTValidationRule from .specified_rules import specified_rules, specified_sdl_rules @@ -16,6 +17,14 @@ class ValidationAbortedError(RuntimeError): """Error when a validation has been aborted (error limit reached).""" +# Per the specification, descriptions must not affect validation. +# See https://spec.graphql.org/draft/#sec-Descriptions +query_document_keys_to_validate: Dict[str, Tuple[str, ...]] = { + kind: tuple(key for key in keys if key != "description") + for kind, keys in QUERY_DOCUMENT_KEYS.items() +} + + def validate( schema: GraphQLSchema, document_ast: DocumentNode, @@ -39,7 +48,8 @@ def validate( Attackers can send pathologically invalid queries to induce a DoS attack, so by default ``max_errors`` set to 100 errors. - Providing a custom TypeInfo instance is deprecated and will be removed in v3.3. + Providing a custom TypeInfo instance is deprecated; omit the ``type_info`` + argument so that validate creates the TypeInfo instance. It will be removed in v3.3. """ if not document_ast or not isinstance(document_ast, DocumentNode): raise TypeError("Must provide document.") @@ -65,7 +75,7 @@ def validate( errors: List[GraphQLError] = [] def on_error(error: GraphQLError) -> None: - if len(errors) >= max_errors: # type: ignore + if len(errors) >= max_errors: errors.append( GraphQLError( "Too many validation errors, error limit reached." @@ -83,7 +93,11 @@ def on_error(error: GraphQLError) -> None: # Visit the whole document with each instance of all provided rules. try: - visit(document_ast, TypeInfoVisitor(type_info, ParallelVisitor(visitors))) + visit( + document_ast, + TypeInfoVisitor(type_info, ParallelVisitor(visitors)), + query_document_keys_to_validate, + ) except ValidationAbortedError: pass return errors diff --git a/src/graphql/validation/validation_context.py b/src/graphql/validation/validation_context.py index 931a19d9..9ab24941 100644 --- a/src/graphql/validation/validation_context.py +++ b/src/graphql/validation/validation_context.py @@ -39,6 +39,7 @@ class VariableUsage(NamedTuple): node: VariableNode type: Optional[GraphQLInputType] default_value: Any + parent_type: Optional[GraphQLInputType] class VariableUsageVisitor(Visitor): @@ -58,7 +59,10 @@ def enter_variable_definition(self, *_args: Any) -> VisitorAction: def enter_variable(self, node: VariableNode, *_args: Any) -> VisitorAction: type_info = self._type_info usage = VariableUsage( - node, type_info.get_input_type(), type_info.get_default_value() + node, + type_info.get_input_type(), + type_info.get_default_value(), + type_info.get_parent_input_type(), ) self._append_usage(usage) return None diff --git a/src/graphql/version.py b/src/graphql/version.py index 424a9851..a5266e27 100644 --- a/src/graphql/version.py +++ b/src/graphql/version.py @@ -4,9 +4,9 @@ __all__ = ["version", "version_info", "version_js", "version_info_js"] -version = "3.2.2" +version = "3.2.11" -version_js = "16.4.0" +version_js = "16.14.1" _re_version = re.compile(r"(\d+)\.(\d+)\.(\d+)(\D*)(\d*)") diff --git a/tests/benchmarks/test_execution_async.py b/tests/benchmarks/test_execution_async.py index de7de2e5..30eee79d 100644 --- a/tests/benchmarks/test_execution_async.py +++ b/tests/benchmarks/test_execution_async.py @@ -7,7 +7,6 @@ graphql, ) - user = GraphQLObjectType( name="User", fields={ diff --git a/tests/benchmarks/test_execution_sync.py b/tests/benchmarks/test_execution_sync.py index bfdb7cc2..8ef46d4b 100644 --- a/tests/benchmarks/test_execution_sync.py +++ b/tests/benchmarks/test_execution_sync.py @@ -6,7 +6,6 @@ graphql_sync, ) - user = GraphQLObjectType( name="User", fields={ diff --git a/tests/benchmarks/test_print.py b/tests/benchmarks/test_print.py new file mode 100644 index 00000000..5445ccb7 --- /dev/null +++ b/tests/benchmarks/test_print.py @@ -0,0 +1,9 @@ +from graphql import parse, print_ast + +from ..fixtures import kitchen_sink_query # noqa: F401 + + +def test_print_kitchen_sink(benchmark, kitchen_sink_query): # noqa: F811 + document = parse(kitchen_sink_query) + result = benchmark(lambda: print_ast(document)) + assert isinstance(result, str) diff --git a/tests/benchmarks/test_repeated_fields.py b/tests/benchmarks/test_repeated_fields.py new file mode 100644 index 00000000..daba6169 --- /dev/null +++ b/tests/benchmarks/test_repeated_fields.py @@ -0,0 +1,25 @@ +from graphql import ( + GraphQLField, + GraphQLObjectType, + GraphQLSchema, + GraphQLString, + graphql_sync, +) + +schema = GraphQLSchema( + query=GraphQLObjectType( + name="Query", + fields={ + "hello": GraphQLField( + GraphQLString, + resolve=lambda _obj, _info: "world", + ) + }, + ) +) +source = f"{{ {'hello ' * 250}}}" + + +def test_many_repeated_fields(benchmark): + result = benchmark(lambda: graphql_sync(schema, source)) + assert result == ({"hello": "world"}, None) diff --git a/tests/benchmarks/test_validate_invalid_gql.py b/tests/benchmarks/test_validate_invalid_gql.py index dec9bbc6..9d981192 100644 --- a/tests/benchmarks/test_validate_invalid_gql.py +++ b/tests/benchmarks/test_validate_invalid_gql.py @@ -5,8 +5,7 @@ def test_validate_invalid_query(benchmark, big_schema_sdl): # noqa: F811 schema = build_schema(big_schema_sdl, assume_valid=True) - query_ast = parse( - """ + query_ast = parse(""" { unknownField ... on unknownType { @@ -18,8 +17,7 @@ def test_validate_invalid_query(benchmark, big_schema_sdl): # noqa: F811 fragment TestFragment on anotherUnknownType { yetAnotherUnknownField } - """ - ) + """) result = benchmark(lambda: validate(schema, query_ast)) assert result == [ { diff --git a/tests/error/test_graphql_error.py b/tests/error/test_graphql_error.py index 6c4689da..0bc639be 100644 --- a/tests/error/test_graphql_error.py +++ b/tests/error/test_graphql_error.py @@ -13,16 +13,11 @@ from ..utils import dedent - -source = Source( - dedent( - """ +source = Source(dedent(""" { field } - """ - ) -) + """)) ast = parse(source) operation_node = ast.definitions[0] @@ -298,13 +293,11 @@ def prints_an_error_using_node_without_location(): def prints_an_error_with_nodes_from_different_sources(): doc_a = parse( Source( - dedent( - """ + dedent(""" type Foo { field: String } - """ - ), + """), "SourceA", ) ) @@ -314,13 +307,11 @@ def prints_an_error_with_nodes_from_different_sources(): field_a = op_a.fields[0] doc_b = parse( Source( - dedent( - """ + dedent(""" type Foo { field: Int } - """ - ), + """), "SourceB", ) ) @@ -333,8 +324,7 @@ def prints_an_error_with_nodes_from_different_sources(): "Example error with two nodes", [field_a.type, field_b.type] ) - assert str(error) == dedent( - """ + assert str(error) == dedent(""" Example error with two nodes SourceA:2:10 @@ -348,8 +338,7 @@ def prints_an_error_with_nodes_from_different_sources(): 2 | field: Int | ^ 3 | } - """ - ) + """) def describe_formatted(): @@ -372,13 +361,11 @@ def formats_graphql_error(): error = GraphQLError( "test message", Node(), - Source( - """ + Source(""" query { something } - """ - ), + """), [16, 41], ["one", 2], ValueError("original"), diff --git a/tests/error/test_print_location.py b/tests/error/test_print_location.py index 69f6143e..6a6c80e9 100644 --- a/tests/error/test_print_location.py +++ b/tests/error/test_print_location.py @@ -16,66 +16,56 @@ def prints_minified_documents(): minified_source, SourceLocation(1, minified_source.body.index("FIRST_ERROR_HERE") + 1), ) - assert first_location == dedent( - """ + assert first_location == dedent(""" GraphQL request:1:53 1 | query SomeMinifiedQueryWithErrorInside($foo:String!=FIRST_ERROR_HERE$bar:String) | ^ | {someField(foo:$foo bar:$bar baz:SECOND_ERROR_HERE){fieldA fieldB{fieldC fieldD. - """ # noqa: E501 - ) + """) # noqa: E501 second_location = print_source_location( minified_source, SourceLocation(1, minified_source.body.index("SECOND_ERROR_HERE") + 1), ) - assert second_location == dedent( - """ + assert second_location == dedent(""" GraphQL request:1:114 1 | query SomeMinifiedQueryWithErrorInside($foo:String!=FIRST_ERROR_HERE$bar:String) | {someField(foo:$foo bar:$bar baz:SECOND_ERROR_HERE){fieldA fieldB{fieldC fieldD. | ^ | ..on THIRD_ERROR_HERE}}} - """ # noqa: E501 - ) + """) # noqa: E501 third_location = print_source_location( minified_source, SourceLocation(1, minified_source.body.index("THIRD_ERROR_HERE") + 1), ) - assert third_location == dedent( - """ + assert third_location == dedent(""" GraphQL request:1:166 1 | query SomeMinifiedQueryWithErrorInside($foo:String!=FIRST_ERROR_HERE$bar:String) | {someField(foo:$foo bar:$bar baz:SECOND_ERROR_HERE){fieldA fieldB{fieldC fieldD. | ..on THIRD_ERROR_HERE}}} | ^ - """ # noqa: E501 - ) + """) # noqa: E501 def prints_single_digit_line_number_with_no_padding(): result = print_source_location( Source("*", "Test", SourceLocation(9, 1)), SourceLocation(1, 1) ) - assert result == dedent( - """ + assert result == dedent(""" Test:9:1 9 | * | ^ - """ - ) + """) def prints_line_numbers_with_correct_padding(): result = print_source_location( Source("*\n", "Test", SourceLocation(9, 1)), SourceLocation(1, 1) ) - assert result == dedent( - """ + assert result == dedent(""" Test:9:1 9 | * | ^ 10 | - """ - ) + """) diff --git a/tests/execution/test_abstract.py b/tests/execution/test_abstract.py index efc130f1..cc8e4d6f 100644 --- a/tests/execution/test_abstract.py +++ b/tests/execution/test_abstract.py @@ -3,7 +3,7 @@ from pytest import mark -from graphql.execution import execute, execute_sync, ExecutionResult +from graphql.execution import ExecutionResult, execute, execute_sync from graphql.language import parse from graphql.type import ( GraphQLBoolean, @@ -40,9 +40,7 @@ async def execute_query( assert isinstance(schema, GraphQLSchema) assert isinstance(query, str) document = parse(query) - result = (execute_sync if sync else execute)( - schema, document, root_value - ) # type: ignore + result = (execute_sync if sync else execute)(schema, document, root_value) if not sync and isawaitable(result): result = await result assert isinstance(result, ExecutionResult) @@ -484,8 +482,7 @@ class RootValueWithInheritance: def describe_union_type(): - schema = build_schema( - """ + schema = build_schema(""" type Query { pets: [Pet] } @@ -501,8 +498,7 @@ def describe_union_type(): name: String woofs: Boolean } - """ - ) + """) query = """ { @@ -525,8 +521,7 @@ async def resolve(sync, access): assert await execute_query(sync, schema, query, root_value) == expected def describe_interface_type(): - schema = build_schema( - """ + schema = build_schema(""" type Query { pets: [Pet] } @@ -544,8 +539,7 @@ def describe_interface_type(): name: String woofs: Boolean } - """ - ) + """) query = """ { @@ -568,8 +562,7 @@ async def resolve(sync, access): assert await execute_query(sync, schema, query, root_value) == expected def resolve_type_on_interface_yields_useful_error(): - schema = build_schema( - """ + schema = build_schema(""" type Query { pet: Pet } @@ -585,18 +578,15 @@ def resolve_type_on_interface_yields_useful_error(): type Dog implements Pet { name: String } - """ - ) + """) - document = parse( - """ + document = parse(""" { pet { name } } - """ - ) + """) def expect_error(for_type_name: Any, message: str) -> None: root_value = {"pet": {"__typename": for_type_name}} diff --git a/tests/execution/test_directives.py b/tests/execution/test_directives.py index 2beadc77..5e75cb38 100644 --- a/tests/execution/test_directives.py +++ b/tests/execution/test_directives.py @@ -48,8 +48,7 @@ def unless_true_omits_scalar(): def describe_works_on_fragment_spreads(): def if_false_omits_fragment_spread(): - result = execute_test_query( - """ + result = execute_test_query(""" query Q { a ...Frag @include(if: false) @@ -57,13 +56,11 @@ def if_false_omits_fragment_spread(): fragment Frag on TestType { b } - """ - ) + """) assert result == ({"a": "a"}, None) def if_true_includes_fragment_spread(): - result = execute_test_query( - """ + result = execute_test_query(""" query Q { a ...Frag @include(if: true) @@ -71,13 +68,11 @@ def if_true_includes_fragment_spread(): fragment Frag on TestType { b } - """ - ) + """) assert result == ({"a": "a", "b": "b"}, None) def unless_false_includes_fragment_spread(): - result = execute_test_query( - """ + result = execute_test_query(""" query Q { a ...Frag @skip(if: false) @@ -85,13 +80,11 @@ def unless_false_includes_fragment_spread(): fragment Frag on TestType { b } - """ - ) + """) assert result == ({"a": "a", "b": "b"}, None) def unless_true_omits_fragment_spread(): - result = execute_test_query( - """ + result = execute_test_query(""" query Q { a ...Frag @skip(if: true) @@ -99,146 +92,123 @@ def unless_true_omits_fragment_spread(): fragment Frag on TestType { b } - """ - ) + """) assert result == ({"a": "a"}, None) def describe_works_on_inline_fragment(): def if_false_omits_inline_fragment(): - result = execute_test_query( - """ + result = execute_test_query(""" query Q { a ... on TestType @include(if: false) { b } } - """ - ) + """) assert result == ({"a": "a"}, None) def if_true_includes_inline_fragment(): - result = execute_test_query( - """ + result = execute_test_query(""" query Q { a ... on TestType @include(if: true) { b } } - """ - ) + """) assert result == ({"a": "a", "b": "b"}, None) def unless_false_includes_inline_fragment(): - result = execute_test_query( - """ + result = execute_test_query(""" query Q { a ... on TestType @skip(if: false) { b } } - """ - ) + """) assert result == ({"a": "a", "b": "b"}, None) def unless_true_omits_inline_fragment(): - result = execute_test_query( - """ + result = execute_test_query(""" query Q { a ... on TestType @skip(if: true) { b } } - """ - ) + """) assert result == ({"a": "a"}, None) def describe_works_on_anonymous_inline_fragment(): def if_false_omits_anonymous_inline_fragment(): - result = execute_test_query( - """ + result = execute_test_query(""" query { a ... @include(if: false) { b } } - """ - ) + """) assert result == ({"a": "a"}, None) def if_true_includes_anonymous_inline_fragment(): - result = execute_test_query( - """ + result = execute_test_query(""" query { a ... @include(if: true) { b } } - """ - ) + """) assert result == ({"a": "a", "b": "b"}, None) def unless_false_includes_anonymous_inline_fragment(): - result = execute_test_query( - """ + result = execute_test_query(""" query { a ... @skip(if: false) { b } } - """ - ) + """) assert result == ({"a": "a", "b": "b"}, None) def unless_true_omits_anonymous_inline_fragment(): - result = execute_test_query( - """ + result = execute_test_query(""" query { a ... @skip(if: true) { b } } - """ - ) + """) assert result == ({"a": "a"}, None) def describe_works_with_skip_and_include_directives(): def include_and_no_skip(): - result = execute_test_query( - """ + result = execute_test_query(""" { a b @include(if: true) @skip(if: false) } - """ - ) + """) assert result == ({"a": "a", "b": "b"}, None) def include_and_skip(): - result = execute_test_query( - """ + result = execute_test_query(""" { a b @include(if: true) @skip(if: true) } - """ - ) + """) assert result == ({"a": "a"}, None) def no_include_or_skip(): - result = execute_test_query( - """ + result = execute_test_query(""" { a b @include(if: false) @skip(if: false) } - """ - ) + """) assert result == ({"a": "a"}, None) diff --git a/tests/execution/test_executor.py b/tests/execution/test_executor.py index 7cd2260a..256b08bb 100644 --- a/tests/execution/test_executor.py +++ b/tests/execution/test_executor.py @@ -1,28 +1,29 @@ import asyncio -from typing import cast, Any, Awaitable, Optional - -from pytest import mark, raises +from typing import Any, Awaitable, Optional, cast from graphql.error import GraphQLError from graphql.execution import execute, execute_sync -from graphql.language import parse, FieldNode, OperationDefinitionNode -from graphql.pyutils import inspect, Undefined +from graphql.language import FieldNode, OperationDefinitionNode, parse +from graphql.pyutils import Undefined, inspect from graphql.type import ( GraphQLArgument, GraphQLBoolean, GraphQLField, + GraphQLInputField, + GraphQLInputObjectType, GraphQLInt, GraphQLInterfaceType, GraphQLList, GraphQLNonNull, GraphQLObjectType, GraphQLResolveInfo, - GraphQLSchema, GraphQLScalarType, + GraphQLSchema, GraphQLString, GraphQLUnionType, ResponsePath, ) +from pytest import mark, raises def describe_execute_handles_basic_execution_tasks(): @@ -57,13 +58,11 @@ def throws_on_invalid_variables(): }, ) ) - document = parse( - """ + document = parse(""" query ($a: Int) { fieldA(argA: $a) } - """ - ) + """) variable_values = "{'a': 1}" with raises(TypeError) as exc_info: @@ -171,8 +170,7 @@ async def promise_data(): }, ) - document = parse( - """ + document = parse(""" query ($size: Int) { a, b, @@ -200,8 +198,7 @@ async def promise_data(): d e } - """ - ) + """) awaitable_result = execute( GraphQLSchema(DataType), document, Data(), variable_values={"size": 100} @@ -245,8 +242,7 @@ def merges_parallel_fragments(): ) schema = GraphQLSchema(Type) - ast = parse( - """ + ast = parse(""" { a, ...FragOne, ...FragTwo } fragment FragOne on Type { @@ -258,8 +254,7 @@ def merges_parallel_fragments(): c deep { c, deeper: deep { c } } } - """ - ) + """) result = execute_sync(schema, ast) assert result == ( @@ -339,8 +334,7 @@ def resolve_type(_val, _info, _type): ) schema = GraphQLSchema(test_type) root_value: Any = {"test": [{}]} - document = parse( - """ + document = parse(""" query { l1: test { ... on SomeObject { @@ -348,8 +342,7 @@ def resolve_type(_val, _info, _type): } } } - """ - ) + """) execute_sync(schema, document, root_value) @@ -409,13 +402,11 @@ def resolve(_obj, _info, **args): ) ) - document = parse( - """ + document = parse(""" query Example { b(numArg: 123, stringArg: "foo") } - """ - ) + """) execute_sync(schema, document) @@ -424,8 +415,7 @@ def resolve(_obj, _info, **args): @mark.asyncio async def nulls_out_error_subtrees(): - document = parse( - """ + document = parse(""" { syncOk syncError @@ -438,8 +428,7 @@ async def nulls_out_error_subtrees(): asyncReturnError asyncReturnErrorWithExtensions } - """ - ) + """) schema = GraphQLSchema( GraphQLObjectType( @@ -567,6 +556,52 @@ async def asyncReturnErrorWithExtensions(self, _info): ], ) + @mark.filterwarnings("ignore:.* was never awaited:RuntimeWarning") + def handles_sync_errors_combined_with_async_ones(): + is_async_resolver_finished = False + + async def async_resolver(_obj, _info): + nonlocal is_async_resolver_finished + is_async_resolver_finished = True # pragma: no cover + + schema = GraphQLSchema( + GraphQLObjectType( + "Query", + { + "syncNullError": GraphQLField( + GraphQLNonNull(GraphQLString), resolve=lambda _obj, _info: None + ), + "asyncNullError": GraphQLField( + GraphQLNonNull(GraphQLString), resolve=async_resolver + ), + }, + ) + ) + + document = parse(""" + { + asyncNullError + syncNullError + } + """) + + result = execute(schema, document) + + assert is_async_resolver_finished is False + + assert result == ( + None, + [ + { + "message": "Cannot return null" + " for non-nullable field Query.syncNullError.", + "locations": [(4, 15)], + "path": ["syncNullError"], + } + ], + ) + + @mark.filterwarnings("ignore:.* was never awaited:RuntimeWarning") def full_response_path_is_included_for_non_nullable_fields(): def resolve_ok(*_args): return {} @@ -589,8 +624,7 @@ def resolve_error(*_args): ) ) - document = parse( - """ + document = parse(""" query { nullableA { aliasedA: nullableA { @@ -602,8 +636,7 @@ def resolve_error(*_args): } } } - """ - ) + """) assert execute_sync(schema, document) == ( {"nullableA": {"aliasedA": None}}, @@ -616,6 +649,7 @@ def resolve_error(*_args): ], ) + @mark.filterwarnings("ignore:.* was never awaited:RuntimeWarning") def uses_the_inline_operation_if_no_operation_name_is_provided(): schema = GraphQLSchema( GraphQLObjectType("Type", {"a": GraphQLField(GraphQLString)}) @@ -629,6 +663,7 @@ class Data: result = execute_sync(schema, document, Data()) assert result == ({"a": "b"}, None) + @mark.filterwarnings("ignore:.* was never awaited:RuntimeWarning") def uses_the_only_operation_if_no_operation_name_is_provided(): schema = GraphQLSchema( GraphQLObjectType("Type", {"a": GraphQLField(GraphQLString)}) @@ -642,17 +677,16 @@ class Data: result = execute_sync(schema, document, Data()) assert result == ({"a": "b"}, None) + @mark.filterwarnings("ignore:.* was never awaited:RuntimeWarning") def uses_the_named_operation_if_operation_name_is_provided(): schema = GraphQLSchema( GraphQLObjectType("Type", {"a": GraphQLField(GraphQLString)}) ) - document = parse( - """ + document = parse(""" query Example { first: a } query OtherExample { second: a } - """ - ) + """) class Data: a = "b" @@ -678,12 +712,10 @@ def errors_if_no_operation_name_is_provided_with_multiple_operations(): GraphQLObjectType("Type", {"a": GraphQLField(GraphQLString)}) ) - document = parse( - """ + document = parse(""" query Example { a } query OtherExample { a } - """ - ) + """) result = execute_sync(schema, document) assert result == ( @@ -701,12 +733,10 @@ def errors_if_unknown_operation_name_is_provided(): GraphQLObjectType("Type", {"a": GraphQLField(GraphQLString)}) ) - document = parse( - """ + document = parse(""" query Example { a } query OtherExample { a } - """ - ) + """) result = execute_sync(schema, document, operation_name="UnknownExample") assert result == ( @@ -734,13 +764,11 @@ def uses_the_query_schema_for_queries(): GraphQLObjectType("S", {"a": GraphQLField(GraphQLString)}), ) - document = parse( - """ + document = parse(""" query Q { a } mutation M { c } subscription S { a } - """ - ) + """) class Data: a = "b" @@ -749,18 +777,17 @@ class Data: result = execute_sync(schema, document, Data(), operation_name="Q") assert result == ({"a": "b"}, None) + @mark.filterwarnings("ignore:.* was never awaited:RuntimeWarning") def uses_the_mutation_schema_for_mutations(): schema = GraphQLSchema( GraphQLObjectType("Q", {"a": GraphQLField(GraphQLString)}), GraphQLObjectType("M", {"c": GraphQLField(GraphQLString)}), ) - document = parse( - """ + document = parse(""" query Q { a } mutation M { c } - """ - ) + """) class Data: a = "b" @@ -775,12 +802,10 @@ def uses_the_subscription_schema_for_subscriptions(): subscription=GraphQLObjectType("S", {"a": GraphQLField(GraphQLString)}), ) - document = parse( - """ + document = parse(""" query Q { a } subscription S { a } - """ - ) + """) class Data: a = "b" @@ -792,13 +817,11 @@ class Data: def resolves_to_an_error_if_schema_does_not_support_operation(): schema = GraphQLSchema(assume_valid=True) - document = parse( - """ + document = parse(""" query Q { __typename } mutation M { __typename } subscription S { __typename } - """ - ) + """) assert execute_sync(schema, document, operation_name="Q") == ( None, @@ -833,6 +856,7 @@ def resolves_to_an_error_if_schema_does_not_support_operation(): ) @mark.asyncio + @mark.filterwarnings("ignore:.* was never awaited:RuntimeWarning") async def correct_field_ordering_despite_execution_order(): schema = GraphQLSchema( GraphQLObjectType( @@ -877,8 +901,7 @@ def avoids_recursion(): GraphQLObjectType("Type", {"a": GraphQLField(GraphQLString)}) ) - document = parse( - """ + document = parse(""" query Q { a ...Frag @@ -889,8 +912,7 @@ def avoids_recursion(): a, ...Frag } - """ - ) + """) class Data: a = "b" @@ -1039,13 +1061,11 @@ def executes_ignoring_invalid_non_executable_definitions(): GraphQLObjectType("Query", {"foo": GraphQLField(GraphQLString)}) ) - document = parse( - """ + document = parse(""" { foo } type Query { bar: String } - """ - ) + """) result = execute_sync(schema, document) assert result == ({"foo": None}, None) @@ -1092,3 +1112,67 @@ def type_resolver(_source, info, abstract_type): assert result == ({"foo": {"bar": "bar"}}, None) assert possible_types == [foo_object] + + def uses_a_different_number_of_max_coercion_errors(): + schema = GraphQLSchema( + query=GraphQLObjectType("Query", {"dummy": GraphQLField(GraphQLString)}), + mutation=GraphQLObjectType( + "Mutation", + { + "updateUser": GraphQLField( + GraphQLString, + args={ + "data": GraphQLArgument( + GraphQLInputObjectType( + "User", + { + "email": GraphQLInputField( + GraphQLNonNull(GraphQLString) + ) + }, + ) + ) + }, + ) + }, + ), + ) + + document = parse(""" + mutation ($data: User) { + updateUser(data: $data) + } + """) + + result = execute_sync( + schema, + document, + variable_values={ + "data": { + "email": "", + "wrongArg": "wrong", + "wrongArg2": "wrong", + "wrongArg3": "wrong", + } + }, + max_coercion_errors=1, + ) + + # Returns at least 2 errors, one for the first 'wrongArg', and one for the + # coercion limit + assert result == ( + None, + [ + { + "message": "Variable '$data' got invalid value" + " {'email': '', 'wrongArg': 'wrong'," + " 'wrongArg2': 'wrong', 'wrongArg3': 'wrong'};" + " Field 'wrongArg' is not defined by type 'User'.", + "locations": [(2, 23)], + }, + { + "message": "Too many errors processing variables," + " error limit reached. Execution aborted.", + }, + ], + ) diff --git a/tests/execution/test_lists.py b/tests/execution/test_lists.py index 729c1191..dd826950 100644 --- a/tests/execution/test_lists.py +++ b/tests/execution/test_lists.py @@ -73,6 +73,7 @@ def get_args(*args): None, ) + @mark.filterwarnings("ignore:.* was never awaited:RuntimeWarning") def does_not_accept_a_dict_as_a_list_value(): assert _complete({1: "one", 2: "two"}) == ( {"listField": None}, diff --git a/tests/execution/test_map_async_iterator.py b/tests/execution/test_map_async_iterator.py index 299d010a..a6eeb176 100644 --- a/tests/execution/test_map_async_iterator.py +++ b/tests/execution/test_map_async_iterator.py @@ -1,10 +1,11 @@ +import platform import sys from asyncio import CancelledError, Event, ensure_future, sleep -from pytest import mark, raises - from graphql.execution import MapAsyncIterator +from pytest import mark, raises +is_pypy = platform.python_implementation() == "PyPy" try: # pragma: no cover anext @@ -344,6 +345,10 @@ def double(x): with raises(StopAsyncIteration): await anext(doubles) + # no more exceptions should be thrown + if is_pypy: + # need to investigate why this is needed with PyPy + await doubles.aclose() # pragma: no cover await doubles.athrow(RuntimeError("no more ouch")) with raises(StopAsyncIteration): diff --git a/tests/execution/test_mutations.py b/tests/execution/test_mutations.py index 819eddd4..9008605d 100644 --- a/tests/execution/test_mutations.py +++ b/tests/execution/test_mutations.py @@ -93,8 +93,7 @@ async def promise_and_fail_to_change_the_number(self, newNumber: int): def describe_execute_handles_mutation_execution_ordering(): @mark.asyncio async def evaluates_mutations_serially(): - document = parse( - """ + document = parse(""" mutation M { first: immediatelyChangeTheNumber(newNumber: 1) { theNumber @@ -112,8 +111,7 @@ async def evaluates_mutations_serially(): theNumber } } - """ - ) + """) root_value = Root(6) awaitable_result = execute( @@ -141,8 +139,7 @@ def does_not_include_illegal_mutation_fields_in_output(): @mark.asyncio async def evaluates_mutations_correctly_in_presence_of_a_failed_mutation(): - document = parse( - """ + document = parse(""" mutation M { first: immediatelyChangeTheNumber(newNumber: 1) { theNumber @@ -163,8 +160,7 @@ async def evaluates_mutations_correctly_in_presence_of_a_failed_mutation(): theNumber } } - """ - ) + """) root_value = Root(6) awaitable_result = execute( diff --git a/tests/execution/test_nonnull.py b/tests/execution/test_nonnull.py index da15e1b6..59d450df 100644 --- a/tests/execution/test_nonnull.py +++ b/tests/execution/test_nonnull.py @@ -1,4 +1,5 @@ import re +from asyncio import sleep from typing import Any, Awaitable, cast from pytest import mark @@ -76,8 +77,7 @@ async def promiseNonNullNest(self, _info): return NullingData() -schema = build_schema( - """ +schema = build_schema(""" type DataType { sync: String syncNonNull: String! @@ -92,8 +92,7 @@ async def promiseNonNullNest(self, _info): schema { query: DataType } - """ -) + """) def execute_query(query: str, root_value: Any) -> AwaitableOrValue[ExecutionResult]: @@ -507,6 +506,142 @@ async def throws(): ], ) + def describe_handles_multiple_errors_for_a_single_response_position(): + @mark.asyncio + async def nullable_and_non_nullable_root_fields_throw_nested_errors(): + query = """ + { + promiseNonNullNest { + syncNonNull + } + promiseNest { + syncNonNull + } + } + """ + result = await cast( + Awaitable[ExecutionResult], execute_query(query, ThrowingData()) + ) + assert result == ( + None, + [ + { + "message": str(sync_non_null_error), + "path": ["promiseNonNullNest", "syncNonNull"], + "locations": [(4, 21)], + }, + { + "message": str(sync_non_null_error), + "path": ["promiseNest", "syncNonNull"], + "locations": [(7, 21)], + }, + ], + ) + + @mark.asyncio + async def slower_nullable_after_non_nullable_root_field(): + query = """ + { + promiseNonNullNest { + syncNonNull + } + promiseNest { + promiseNonNull + } + } + """ + result = await cast( + Awaitable[ExecutionResult], execute_query(query, ThrowingData()) + ) + assert result == ( + None, + [ + { + "message": str(sync_non_null_error), + "path": ["promiseNonNullNest", "syncNonNull"], + "locations": [(4, 21)], + }, + ], + ) + + # allow time for slower error to reject + assert result.errors is not None + initial_errors = list(result.errors) + for _ in range(5): + await sleep(0) + assert result.errors == initial_errors + + @mark.asyncio + async def nullable_and_non_nullable_nested_fields_throw_nested_errors(): + query = """ + { + syncNest { + promiseNonNullNest { + syncNonNull + } + promiseNest { + syncNonNull + } + } + } + """ + result = await cast( + Awaitable[ExecutionResult], execute_query(query, ThrowingData()) + ) + assert result == ( + {"syncNest": None}, + [ + { + "message": str(sync_non_null_error), + "path": ["syncNest", "promiseNonNullNest", "syncNonNull"], + "locations": [(5, 23)], + }, + { + "message": str(sync_non_null_error), + "path": ["syncNest", "promiseNest", "syncNonNull"], + "locations": [(8, 23)], + }, + ], + ) + + @mark.asyncio + async def slower_nullable_after_non_nullable_nested_field(): + query = """ + { + syncNest { + promiseNonNullNest { + syncNonNull + } + promiseNest { + promiseNest { + promiseNest { + promiseNonNull + } + } + } + } + } + """ + result = await cast( + Awaitable[ExecutionResult], execute_query(query, ThrowingData()) + ) + assert result == ( + {"syncNest": None}, + [ + { + "message": str(sync_non_null_error), + "path": ["syncNest", "promiseNonNullNest", "syncNonNull"], + "locations": [(5, 23)], + }, + ], + ) + + assert result.errors is not None + initial_errors = list(result.errors) + for _ in range(20): + await sleep(0) + assert result.errors == initial_errors + def describe_handles_non_null_argument(): # noinspection PyPep8Naming @@ -531,13 +666,11 @@ def describe_handles_non_null_argument(): def succeeds_when_passed_non_null_literal_value(): result = execute_sync( schema_with_non_null_arg, - parse( - """ + parse(""" query { withNonNullArg (cannotBeNull: "literal value") } - """ - ), + """), ) assert result == ({"withNonNullArg": "Passed: literal value"}, None) @@ -545,13 +678,11 @@ def succeeds_when_passed_non_null_literal_value(): def succeeds_when_passed_non_null_variable_value(): result = execute_sync( schema_with_non_null_arg, - parse( - """ + parse(""" query ($testVar: String!) { withNonNullArg (cannotBeNull: $testVar) } - """ - ), + """), variable_values={ "testVar": "variable value", }, @@ -562,13 +693,11 @@ def succeeds_when_passed_non_null_variable_value(): def succeeds_when_missing_variable_has_default_value(): result = execute_sync( schema_with_non_null_arg, - parse( - """ + parse(""" query ($testVar: String = "default value") { withNonNullArg (cannotBeNull: $testVar) } - """ - ), + """), variable_values={}, # intentionally missing variable ) @@ -580,13 +709,11 @@ def field_error_when_missing_non_null_arg(): # protect against this. result = execute_sync( schema_with_non_null_arg, - parse( - """ + parse(""" query { withNonNullArg } - """ - ), + """), ) assert result == ( @@ -607,13 +734,11 @@ def field_error_when_non_null_arg_provided_null(): # should still protect against this. result = execute_sync( schema_with_non_null_arg, - parse( - """ + parse(""" query { withNonNullArg(cannotBeNull: null) } - """ - ), + """), ) assert result == ( @@ -634,13 +759,11 @@ def field_error_when_non_null_arg_not_provided_variable_value(): # should still protect against this. result = execute_sync( schema_with_non_null_arg, - parse( - """ + parse(""" query ($testVar: String) { withNonNullArg(cannotBeNull: $testVar) } - """ - ), + """), variable_values={}, ) # intentionally missing variable @@ -661,13 +784,11 @@ def field_error_when_non_null_arg_not_provided_variable_value(): def field_error_when_non_null_arg_provided_explicit_null_variable(): result = execute_sync( schema_with_non_null_arg, - parse( - """ + parse(""" query ($testVar: String = "default value") { withNonNullArg (cannotBeNull: $testVar) } - """ - ), + """), variable_values={"testVar": None}, ) diff --git a/tests/execution/test_one_of.py b/tests/execution/test_one_of.py new file mode 100644 index 00000000..29cbb9e5 --- /dev/null +++ b/tests/execution/test_one_of.py @@ -0,0 +1,147 @@ +from typing import Any, Dict, Optional + +from graphql.execution import ExecutionResult, execute +from graphql.language import parse +from graphql.pyutils import AwaitableOrValue +from graphql.utilities import build_schema + +schema = build_schema(""" + type Query { + test(input: TestInputObject!): TestObject + } + + input TestInputObject @oneOf { + a: String + b: Int + } + + type TestObject { + a: String + b: Int + } + """) + + +def execute_query( + query: str, root_value: Any, variable_values: Optional[Dict[str, Any]] = None +) -> AwaitableOrValue[ExecutionResult]: + return execute(schema, parse(query), root_value, variable_values=variable_values) + + +def describe_execute_handles_one_of_input_objects(): + def describe_one_of_input_objects(): + root_value = { + "test": lambda _info, input: input, # noqa: A002 + } + + def accepts_a_good_default_value(): + query = """ + query ($input: TestInputObject! = {a: "abc"}) { + test(input: $input) { + a + b + } + } + """ + result = execute_query(query, root_value) + + assert result == ({"test": {"a": "abc", "b": None}}, None) + + def rejects_a_bad_default_value(): + query = """ + query ($input: TestInputObject! = {a: "abc", b: 123}) { + test(input: $input) { + a + b + } + } + """ + result = execute_query(query, root_value) + + assert result == ( + {"test": None}, + [ + { + # This type of error would be caught at validation-time + # hence the vague error message here. + "message": "Argument 'input' of non-null type" + " 'TestInputObject!' must not be null.", + "locations": [(3, 31)], + "path": ["test"], + } + ], + ) + + def accepts_a_good_variable(): + query = """ + query ($input: TestInputObject!) { + test(input: $input) { + a + b + } + } + """ + result = execute_query(query, root_value, {"input": {"a": "abc"}}) + + assert result == ({"test": {"a": "abc", "b": None}}, None) + + def accepts_a_good_variable_with_an_undefined_key(): + query = """ + query ($input: TestInputObject!) { + test(input: $input) { + a + b + } + } + """ + result = execute_query(query, root_value, {"input": {"a": "abc"}}) + + assert result == ({"test": {"a": "abc", "b": None}}, None) + + def rejects_a_variable_with_multiple_non_null_keys(): + query = """ + query ($input: TestInputObject!) { + test(input: $input) { + a + b + } + } + """ + result = execute_query(query, root_value, {"input": {"a": "abc", "b": 123}}) + + assert result == ( + None, + [ + { + "message": "Variable '$input' got invalid value" + " {'a': 'abc', 'b': 123}; Exactly one key must be specified" + " for OneOf type 'TestInputObject'.", + "locations": [(2, 24)], + } + ], + ) + + def rejects_a_variable_with_multiple_nullable_keys(): + query = """ + query ($input: TestInputObject!) { + test(input: $input) { + a + b + } + } + """ + result = execute_query( + query, root_value, {"input": {"a": "abc", "b": None}} + ) + + assert result == ( + None, + [ + { + "message": "Variable '$input' got invalid value" + " {'a': 'abc', 'b': None}; Exactly one key must be specified" + " for OneOf type 'TestInputObject'.", + "locations": [(2, 24)], + } + ], + ) diff --git a/tests/execution/test_parallel.py b/tests/execution/test_parallel.py index aeb2a142..0bd4f6d7 100644 --- a/tests/execution/test_parallel.py +++ b/tests/execution/test_parallel.py @@ -132,8 +132,7 @@ async def is_type_of_baz(obj, *_args): types=[BarType, BazType], ) - ast = parse( - """ + ast = parse(""" { foo { foo @@ -141,8 +140,7 @@ async def is_type_of_baz(obj, *_args): ... on Baz { foobaz } } } - """ - ) + """) # raises TimeoutError if not parallel awaitable_result = execute(schema, ast) diff --git a/tests/execution/test_resolve.py b/tests/execution/test_resolve.py index f945e6c3..a30d19a8 100644 --- a/tests/execution/test_resolve.py +++ b/tests/execution/test_resolve.py @@ -2,14 +2,16 @@ from typing import Any from graphql.error import GraphQLError -from graphql.execution import execute_sync, ExecutionResult -from graphql.language import parse, SourceLocation +from graphql.execution import ExecutionResult, execute_sync +from graphql.language import SourceLocation, parse from graphql.type import ( GraphQLArgument, GraphQLField, + GraphQLID, GraphQLInputField, GraphQLInputObjectType, GraphQLInt, + GraphQLList, GraphQLObjectType, GraphQLSchema, GraphQLString, @@ -213,6 +215,85 @@ def execute_query(query: str, root_value: Any = None) -> ExecutionResult: None, ) + def transforms_default_values_using_out_names(): + # This is an extension of GraphQL.js. + resolver_kwargs: Any + + def search_resolver(_obj: None, _info, **kwargs): + nonlocal resolver_kwargs + resolver_kwargs = kwargs + return [{"id": "42"}] + + filters_type = GraphQLInputObjectType( + "SearchFilters", + {"pageSize": GraphQLInputField(GraphQLInt, out_name="page_size")}, + ) + result_type = GraphQLObjectType("SearchResult", {"id": GraphQLField(GraphQLID)}) + query = GraphQLObjectType( + "Query", + { + "search": GraphQLField( + GraphQLList(result_type), + { + "searchFilters": GraphQLArgument( + filters_type, {"pageSize": 10}, out_name="search_filters" + ) + }, + resolve=search_resolver, + ) + }, + ) + schema = GraphQLSchema(query) + + resolver_kwargs = None + result = execute_sync(schema, parse("{ search { id } }")) + assert result == ({"search": [{"id": "42"}]}, None) + assert resolver_kwargs == {"search_filters": {"page_size": 10}} + + resolver_kwargs = None + result = execute_sync( + schema, parse("{ search(searchFilters:{pageSize: 25}) { id } }") + ) + assert result == ({"search": [{"id": "42"}]}, None) + assert resolver_kwargs == {"search_filters": {"page_size": 25}} + + resolver_kwargs = None + result = execute_sync( + schema, + parse(""" + query ($searchFilters: SearchFilters) { + search(searchFilters: $searchFilters) { id } + } + """), + ) + assert result == ({"search": [{"id": "42"}]}, None) + assert resolver_kwargs == {"search_filters": {"page_size": 10}} + + resolver_kwargs = None + result = execute_sync( + schema, + parse(""" + query ($searchFilters: SearchFilters) { + search(searchFilters: $searchFilters) { id } + } + """), + variable_values={"searchFilters": {"pageSize": 25}}, + ) + assert result == ({"search": [{"id": "42"}]}, None) + assert resolver_kwargs == {"search_filters": {"page_size": 25}} + + resolver_kwargs = None + result = execute_sync( + schema, + parse(""" + query ($searchFilters: SearchFilters = {pageSize: 25}) { + search(searchFilters: $searchFilters) { id } + } + """), + ) + assert result == ({"search": [{"id": "42"}]}, None) + assert resolver_kwargs == {"search_filters": {"page_size": 25}} + def pass_error_from_resolver_wrapped_as_located_graphql_error(): def resolve(_obj, _info): raise ValueError("Some error") diff --git a/tests/execution/test_schema.py b/tests/execution/test_schema.py index 150f1681..090f0358 100644 --- a/tests/execution/test_schema.py +++ b/tests/execution/test_schema.py @@ -107,8 +107,7 @@ def __init__(self, uid: int, width: int, height: int): self.width = f"{width}" self.height = f"{height}" - document = parse( - """ + document = parse(""" { feed { id, @@ -140,8 +139,7 @@ def __init__(self, uid: int, width: int, height: int): hidden, notDefined } - """ - ) + """) # Note: this is intentionally not validating to ensure appropriate # behavior occurs when executing an invalid query. diff --git a/tests/execution/test_subscribe.py b/tests/execution/test_subscribe.py index 8b1fe639..f4ece9f9 100644 --- a/tests/execution/test_subscribe.py +++ b/tests/execution/test_subscribe.py @@ -1,9 +1,12 @@ import asyncio +from typing import Any, Callable, Dict, List -from typing import Any, Dict, List, Callable - -from pytest import mark, raises - +from graphql.execution import ( + ExecutionResult, + MapAsyncIterator, + create_source_event_stream, + subscribe, +) from graphql.language import parse from graphql.pyutils import SimplePubSub from graphql.type import ( @@ -16,7 +19,7 @@ GraphQLSchema, GraphQLString, ) -from graphql.execution import create_source_event_stream, subscribe, MapAsyncIterator +from pytest import mark, raises try: anext @@ -77,8 +80,7 @@ async def anext(iterator): def create_subscription(pubsub: SimplePubSub): - document = parse( - """ + document = parse(""" subscription ($priority: Int = 0) { importantEmail(priority: $priority) { email { @@ -91,8 +93,7 @@ def create_subscription(pubsub: SimplePubSub): } } } - """ - ) + """) emails: List[Email] = [ { @@ -123,13 +124,11 @@ def transform(new_email): def describe_subscription_initialization_phase(): @mark.asyncio async def accepts_positional_arguments(): - document = parse( - """ + document = parse(""" subscription { importantEmail } - """ - ) + """) async def empty_async_iterator(_info): for value in (): # type: ignore @@ -436,18 +435,18 @@ async def resolves_to_an_error_if_variables_were_wrong_type(): ) variable_values = {"arg": "meow"} - document = parse( - """ + document = parse(""" subscription ($arg: Int) { foo(arg: $arg) } - """ - ) + """) # If we receive variables that cannot be coerced correctly, subscribe() will # resolve to an ExecutionResult that contains an informative error description. result = await subscribe(schema, document, variable_values=variable_values) + assert isinstance(result, ExecutionResult) + assert result == ( None, [ @@ -459,7 +458,9 @@ async def resolves_to_an_error_if_variables_were_wrong_type(): ], ) - assert result.errors[0].original_error is None # type: ignore + errors = result.errors + assert errors + assert errors[0].original_error # Once a subscription returns a valid AsyncIterator, it can still yield errors. @@ -855,7 +856,7 @@ def resolve_message(message, _info): subscription = await subscribe(schema, document) assert isinstance(subscription, MapAsyncIterator) - assert await (anext(subscription)) == ({"newMessage": "Hello"}, None) + assert await anext(subscription) == ({"newMessage": "Hello"}, None) with raises(RuntimeError) as exc_info: await anext(subscription) diff --git a/tests/execution/test_union_interface.py b/tests/execution/test_union_interface.py index e6858e43..143aef39 100644 --- a/tests/execution/test_union_interface.py +++ b/tests/execution/test_union_interface.py @@ -1,6 +1,8 @@ from typing import Optional, Union, List -from graphql.execution import execute_sync +from pytest import mark + +from graphql.execution import execute, execute_sync, ExecutionResult from graphql.language import parse from graphql.type import ( GraphQLBoolean, @@ -46,21 +48,32 @@ def __init__(self, name: str, meows: bool): self.progeny = [] +class Plant: + + name: str + + def __init__(self, name: str): + self.name = name + + class Person: name: str pets: Optional[List[Union[Dog, Cat]]] friends: Optional[List[Union[Dog, Cat, "Person"]]] + responsibilities: Optional[List[Union[Dog, Cat, Plant]]] def __init__( self, name: str, pets: Optional[List[Union[Dog, Cat]]] = None, friends: Optional[List[Union[Dog, Cat, "Person"]]] = None, + responsibilities: Optional[List[Union[Dog, Cat, Plant]]] = None, ): self.name = name self.pets = pets self.friends = friends + self.responsibilities = responsibilities NamedType = GraphQLInterfaceType("Named", {"name": GraphQLField(GraphQLString)}) @@ -106,6 +119,18 @@ def __init__( ) +async def resolve_plant_type(_value, _info): + raise RuntimeError("Not sure if this is a plant") + + +PlantType = GraphQLObjectType( + "Plant", + lambda: {"name": GraphQLField(GraphQLString)}, + interfaces=[NamedType], + is_type_of=resolve_plant_type, +) + + def resolve_pet_type(value, _info, _type): if isinstance(value, Dog): return DogType.name @@ -118,12 +143,15 @@ def resolve_pet_type(value, _info, _type): PetType = GraphQLUnionType("Pet", [DogType, CatType], resolve_type=resolve_pet_type) +PetOrPlantType = GraphQLUnionType("PetOrPlantType", [PlantType, DogType, CatType]) + PersonType = GraphQLObjectType( "Person", lambda: { "name": GraphQLField(GraphQLString), "pets": GraphQLField(GraphQLList(PetType)), "friends": GraphQLField(GraphQLList(NamedType)), + "responsibilities": GraphQLField(GraphQLList(PetOrPlantType)), "progeny": GraphQLField(GraphQLList(PersonType)), # type: ignore "mother": GraphQLField(PersonType), # type: ignore "father": GraphQLField(PersonType), # type: ignore @@ -142,14 +170,14 @@ def resolve_pet_type(value, _info, _type): odie.mother = Dog("Odie's Mom", True) odie.mother.progeny = [odie] +fern = Plant("Fern") liz = Person("Liz", [], []) -john = Person("John", [garfield, odie], [liz, odie]) +john = Person("John", [garfield, odie], [liz, odie], [garfield, fern]) def describe_execute_union_and_intersection_types(): def can_introspect_on_union_and_intersection_types(): - document = parse( - """ + document = parse(""" { Named: __type(name: "Named") { kind @@ -179,8 +207,7 @@ def can_introspect_on_union_and_intersection_types(): inputFields { name } } } - """ - ) + """) assert execute_sync(schema=schema, document=document) == ( { @@ -193,6 +220,7 @@ def can_introspect_on_union_and_intersection_types(): {"name": "Dog"}, {"name": "Cat"}, {"name": "Person"}, + {"name": "Plant"}, ], "enumValues": None, "inputFields": None, @@ -229,8 +257,7 @@ def can_introspect_on_union_and_intersection_types(): def executes_using_union_types(): # NOTE: This is an *invalid* query, but it should be *executable*. - document = parse( - """ + document = parse(""" { __typename name @@ -241,8 +268,7 @@ def executes_using_union_types(): meows } } - """ - ) + """) assert execute_sync(schema=schema, document=document, root_value=john) == ( { @@ -258,8 +284,7 @@ def executes_using_union_types(): def executes_union_types_with_inline_fragment(): # This is the valid version of the query in the above test. - document = parse( - """ + document = parse(""" { __typename name @@ -275,8 +300,7 @@ def executes_union_types_with_inline_fragment(): } } } - """ - ) + """) assert execute_sync(schema=schema, document=document, root_value=john) == ( { @@ -292,8 +316,7 @@ def executes_union_types_with_inline_fragment(): def executes_using_interface_types(): # NOTE: This is an *invalid* query, but it should be a *executable*. - document = parse( - """ + document = parse(""" { __typename name @@ -304,8 +327,7 @@ def executes_using_interface_types(): meows } } - """ - ) + """) assert execute_sync(schema=schema, document=document, root_value=john) == ( { @@ -321,8 +343,7 @@ def executes_using_interface_types(): def executes_interface_types_with_inline_fragment(): # This is the valid version of the query in the above test. - document = parse( - """ + document = parse(""" { __typename name @@ -351,8 +372,7 @@ def executes_interface_types_with_inline_fragment(): } } } - """ - ) + """) assert execute_sync(schema=schema, document=document, root_value=john) == ( { @@ -376,8 +396,7 @@ def executes_interface_types_with_inline_fragment(): ) def executes_interface_types_with_named_fragments(): - document = parse( - """ + document = parse(""" { __typename name @@ -396,8 +415,7 @@ def executes_interface_types_with_named_fragments(): fragment CatMeows on Cat { meows } - """ - ) + """) assert execute_sync(schema=schema, document=document, root_value=john) == ( { @@ -412,8 +430,7 @@ def executes_interface_types_with_named_fragments(): ) def allows_fragment_conditions_to_be_abstract_types(): - document = parse( - """ + document = parse(""" { __typename name @@ -456,8 +473,7 @@ def allows_fragment_conditions_to_be_abstract_types(): __typename } } - """ - ) + """) assert execute_sync(schema=schema, document=document, root_value=john) == ( { @@ -528,3 +544,39 @@ def resolve_type(_source, info, _type): "root_value": root_value, "context": context_value, } + + @mark.asyncio + @mark.filterwarnings("error:.*was never awaited:RuntimeWarning") + async def handles_rejections_from_is_type_of_after_an_is_type_of_returns_true(): + document = parse(""" + { + responsibilities { + __typename + ... on Dog { + name + barks + } + ... on Cat { + name + meows + } + } + } + """) + + root_value = Person("John", [], [liz], [garfield]) + context_value = {"authToken": "123abc"} + + result = execute(schema, document, root_value, context_value) + assert not isinstance(result, ExecutionResult) + result = await result + assert isinstance(result, ExecutionResult) + + assert result == ( + { + "responsibilities": [ + {"__typename": "Cat", "name": "Garfield", "meows": False}, + ], + }, + None, + ) diff --git a/tests/execution/test_variables.py b/tests/execution/test_variables.py index dde674c7..8c7147cf 100644 --- a/tests/execution/test_variables.py +++ b/tests/execution/test_variables.py @@ -1,9 +1,10 @@ from math import nan from typing import Any, Dict, Optional -from graphql.execution import execute_sync, ExecutionResult +from graphql.error import GraphQLError +from graphql.execution import ExecutionResult, execute_sync from graphql.execution.values import get_variable_values -from graphql.language import parse, OperationDefinitionNode, StringValueNode, ValueNode +from graphql.language import OperationDefinitionNode, StringValueNode, ValueNode, parse from graphql.pyutils import Undefined from graphql.type import ( GraphQLArgument, @@ -21,6 +22,25 @@ GraphQLString, ) +TestFaultyScalarGraphQLError = GraphQLError( + "FaultyScalarErrorMessage", extensions={"code": "FaultyScalarExtensionCode"} +) + + +def faulty_parse_value(value: str) -> str: + raise TestFaultyScalarGraphQLError + + +def faulty_parse_literal(ast: ValueNode, _variables=None) -> str: + raise TestFaultyScalarGraphQLError + + +TestFaultyScalar = GraphQLScalarType( + name="FaultyScalar", + parse_value=faulty_parse_value, + parse_literal=faulty_parse_literal, +) + def parse_serialized_value(value: str) -> str: assert value == "SerializedValue" @@ -47,6 +67,7 @@ def parse_literal_value(ast: ValueNode, _variables=None) -> str: "b": GraphQLInputField(GraphQLList(GraphQLString)), "c": GraphQLInputField(GraphQLNonNull(GraphQLString)), "d": GraphQLInputField(TestComplexScalar), + "e": GraphQLInputField(TestFaultyScalar), }, ) @@ -83,9 +104,9 @@ def field_with_input_arg(input_arg: GraphQLArgument): return GraphQLField( GraphQLString, args={"input": input_arg}, - resolve=lambda _obj, _info, **args: repr(args["input"]) - if "input" in args - else None, + resolve=lambda _obj, _info, **args: ( + repr(args["input"]) if "input" in args else None + ), ) @@ -142,14 +163,12 @@ def describe_execute_handles_inputs(): def describe_handles_objects_and_nullability(): def describe_using_inline_struct(): def executes_with_complex_input(): - result = execute_query( - """ + result = execute_query(""" { fieldWithObjectInput( input: {a: "foo", b: ["bar"], c: "baz"}) } - """ - ) + """) assert result == ( {"fieldWithObjectInput": "{'a': 'foo', 'b': ['bar'], 'c': 'baz'}"}, @@ -158,14 +177,12 @@ def executes_with_complex_input(): def executes_with_custom_input(): # This is an extension of GraphQL.js. - result = execute_query( - """ + result = execute_query(""" { fieldWithCustomObjectInput( input: {x: -3.0, y: 4.5}) } - """ - ) + """) assert result == ( {"fieldWithCustomObjectInput": "'(x|y) = (-3.0|4.5)'"}, @@ -173,13 +190,11 @@ def executes_with_custom_input(): ) def properly_parses_single_value_to_list(): - result = execute_query( - """ + result = execute_query(""" { fieldWithObjectInput(input: {a: "foo", b: "bar", c: "baz"}) } - """ - ) + """) assert result == ( {"fieldWithObjectInput": "{'a': 'foo', 'b': ['bar'], 'c': 'baz'}"}, @@ -187,14 +202,12 @@ def properly_parses_single_value_to_list(): ) def properly_parses_null_value_to_null(): - result = execute_query( - """ + result = execute_query(""" { fieldWithObjectInput( input: {a: null, b: null, c: "C", d: null}) } - """ - ) + """) assert result == ( { @@ -205,13 +218,11 @@ def properly_parses_null_value_to_null(): ) def properly_parses_null_value_in_list(): - result = execute_query( - """ + result = execute_query(""" { fieldWithObjectInput(input: {b: ["A",null,"C"], c: "C"}) } - """ - ) + """) assert result == ( {"fieldWithObjectInput": "{'b': ['A', None, 'C'], 'c': 'C'}"}, @@ -219,13 +230,11 @@ def properly_parses_null_value_in_list(): ) def does_not_use_incorrect_value(): - result = execute_query( - """ + result = execute_query(""" { fieldWithObjectInput(input: ["foo", "bar", "baz"]) } - """ - ) + """) assert result == ( {"fieldWithObjectInput": None}, @@ -240,19 +249,36 @@ def does_not_use_incorrect_value(): ) def properly_runs_parse_literal_on_complex_scalar_types(): - result = execute_query( - """ + result = execute_query(""" { fieldWithObjectInput(input: {c: "foo", d: "SerializedValue"}) } - """ - ) + """) assert result == ( {"fieldWithObjectInput": "{'c': 'foo', 'd': 'DeserializedValue'}"}, None, ) + def errors_on_faulty_scalar_type_input(): + result = execute_query(""" + { + fieldWithObjectInput(input: {c: "foo", e: "bar"}) + } + """) + + assert result == ( + {"fieldWithObjectInput": None}, + [ + { + "message": "Argument 'input' has invalid value" + ' {c: "foo", e: "bar"}.', + "path": ["fieldWithObjectInput"], + "locations": [(3, 51)], + } + ], + ) + def describe_using_variables(): doc = """ query ($input: TestInputObject) { @@ -294,14 +320,12 @@ def uses_null_when_variable_provided_explicit_null_value(): assert result == ({"fieldWithNullableStringInput": "None"}, None) def uses_default_value_when_not_provided(): - result = execute_query( - """ + result = execute_query(""" query ($input: TestInputObject = { a: "foo", b: ["bar"], c: "baz"}) { fieldWithObjectInput(input: $input) } - """ - ) + """) assert result == ( {"fieldWithObjectInput": "{'a': 'foo', 'b': ['bar'], 'c': 'baz'}"}, @@ -365,6 +389,22 @@ def executes_with_complex_scalar_input(): None, ) + def errors_on_faulty_scalar_type_input(): + params = {"input": {"c": "foo", "e": "SerializedValue"}} + result = execute_query(doc, params) + + assert result == ( + None, + [ + { + "message": "Variable '$input' got invalid value" + " 'SerializedValue' at 'input.e'; FaultyScalarErrorMessage", + "locations": [(2, 24)], + "extensions": {"code": "FaultyScalarExtensionCode"}, + } + ], + ) + def errors_on_null_for_nested_non_null(): params = {"input": {"a": "foo", "b": "bar", "c": None}} result = execute_query(doc, params) @@ -455,8 +495,7 @@ def errors_on_addition_of_unknown_input_field(): def describe_handles_custom_enum_values(): def allows_custom_enum_values_as_inputs(): - result = execute_query( - """ + result = execute_query(""" { null: fieldWithEnumInput(input: NULL) NaN: fieldWithEnumInput(input: NAN) @@ -464,8 +503,7 @@ def allows_custom_enum_values_as_inputs(): customValue: fieldWithEnumInput(input: CUSTOM) defaultValue: fieldWithEnumInput(input: DEFAULT_VALUE) } - """ - ) + """) assert result == ( { @@ -480,47 +518,39 @@ def allows_custom_enum_values_as_inputs(): ) def allows_non_nullable_inputs_to_have_null_as_enum_custom_value(): - result = execute_query( - """ + result = execute_query(""" { fieldWithNonNullableEnumInput(input: NULL) } - """ - ) + """) assert result == ({"fieldWithNonNullableEnumInput": "None"}, None) def describe_handles_nullable_scalars(): def allows_nullable_inputs_to_be_omitted(): - result = execute_query( - """ + result = execute_query(""" { fieldWithNullableStringInput } - """ - ) + """) assert result == ({"fieldWithNullableStringInput": None}, None) def allows_nullable_inputs_to_be_omitted_in_a_variable(): - result = execute_query( - """ + result = execute_query(""" query ($value: String) { fieldWithNullableStringInput(input: $value) } - """ - ) + """) assert result == ({"fieldWithNullableStringInput": None}, None) def allows_nullable_inputs_to_be_omitted_in_an_unlisted_variable(): - result = execute_query( - """ + result = execute_query(""" query SetsNullable { fieldWithNullableStringInput(input: $value) } - """ - ) + """) assert result == ({"fieldWithNullableStringInput": None}, None) @@ -545,47 +575,39 @@ def allows_nullable_inputs_to_be_set_to_a_value_in_a_variable(): assert result == ({"fieldWithNullableStringInput": "'a'"}, None) def allows_nullable_inputs_to_be_set_to_a_value_directly(): - result = execute_query( - """ + result = execute_query(""" { fieldWithNullableStringInput(input: "a") } - """ - ) + """) assert result == ({"fieldWithNullableStringInput": "'a'"}, None) def describe_handles_non_nullable_scalars(): def allows_non_nullable_variable_to_be_omitted_given_a_default(): - result = execute_query( - """ + result = execute_query(""" query ($value: String! = "default") { fieldWithNullableStringInput(input: $value) } - """ - ) + """) assert result == ({"fieldWithNullableStringInput": "'default'"}, None) def allows_non_nullable_inputs_to_be_omitted_given_a_default(): - result = execute_query( - """ + result = execute_query(""" query ($value: String = "default") { fieldWithNonNullableStringInput(input: $value) } - """ - ) + """) assert result == ({"fieldWithNonNullableStringInput": "'default'"}, None) def does_not_allow_non_nullable_inputs_to_be_omitted_in_a_variable(): - result = execute_query( - """ + result = execute_query(""" query ($value: String!) { fieldWithNonNullableStringInput(input: $value) } - """ - ) + """) assert result == ( None, @@ -630,13 +652,11 @@ def allows_non_nullable_inputs_to_be_set_to_a_value_in_a_variable(): assert result == ({"fieldWithNonNullableStringInput": "'a'"}, None) def allows_non_nullable_inputs_to_be_set_to_a_value_directly(): - result = execute_query( - """ + result = execute_query(""" { fieldWithNonNullableStringInput(input: "a") } - """ - ) + """) assert result == ({"fieldWithNonNullableStringInput": "'a'"}, None) @@ -676,8 +696,8 @@ def reports_error_for_array_passed_into_string_input(): ) errors = result.errors - assert errors is not None - assert errors[0].original_error is None + assert errors + assert errors[0].original_error def reports_error_for_non_provided_variables_for_non_nullable_inputs(): # Note: this test would typically fail validation before @@ -686,13 +706,11 @@ def reports_error_for_non_provided_variables_for_non_nullable_inputs(): # have introduced a breaking change to make a formerly non-required # argument required, this asserts failure before allowing the # underlying code to receive a non-null value. - result = execute_query( - """ + result = execute_query(""" { fieldWithNonNullableStringInput(input: $foo) } - """ - ) + """) assert result == ( {"fieldWithNonNullableStringInput": None}, @@ -921,24 +939,20 @@ def when_no_argument_provided(): assert result == ({"fieldWithDefaultArgumentValue": "'Hello World'"}, None) def when_omitted_variable_provided(): - result = execute_query( - """ + result = execute_query(""" query ($optional: String) { fieldWithDefaultArgumentValue(input: $optional) } - """ - ) + """) assert result == ({"fieldWithDefaultArgumentValue": "'Hello World'"}, None) def not_when_argument_cannot_be_coerced(): - result = execute_query( - """ + result = execute_query(""" { fieldWithDefaultArgumentValue(input: WRONG_TYPE) } - """ - ) + """) assert result == ( {"fieldWithDefaultArgumentValue": None}, @@ -952,13 +966,11 @@ def not_when_argument_cannot_be_coerced(): ) def when_no_runtime_value_is_provided_to_a_non_null_argument(): - result = execute_query( - """ + result = execute_query(""" query optionalVariable($optional: String) { fieldWithNonNullableStringInputAndDefaultArgValue(input: $optional) } - """ - ) + """) assert result == ( {"fieldWithNonNullableStringInputAndDefaultArgValue": "'Hello World'"}, @@ -966,13 +978,11 @@ def when_no_runtime_value_is_provided_to_a_non_null_argument(): ) def describe_get_variable_values_limit_maximum_number_of_coercion_errors(): - doc = parse( - """ + doc = parse(""" query ($input: [String!]) { listNN(input: $input) } - """ - ) + """) operation = doc.definitions[0] assert isinstance(operation, OperationDefinitionNode) diff --git a/tests/fixtures/__init__.py b/tests/fixtures/__init__.py index fe63e16b..3dcb8791 100644 --- a/tests/fixtures/__init__.py +++ b/tests/fixtures/__init__.py @@ -1,4 +1,5 @@ """Fixtures for graphql tests""" + import json from os.path import dirname, join diff --git a/tests/fixtures/kitchen_sink.graphql b/tests/fixtures/kitchen_sink.graphql index a2d9f671..29c5e0c0 100644 --- a/tests/fixtures/kitchen_sink.graphql +++ b/tests/fixtures/kitchen_sink.graphql @@ -1,4 +1,9 @@ -query queryName($foo: ComplexType, $site: Site = MOBILE) @onQuery { +"Query description" +query queryName( + "Very complex variable" + $foo: ComplexType, + $site: Site = MOBILE +) @onQuery { whoever123is: node(id: [123, 456]) { id , ... on User @onInlineFragment { @@ -42,6 +47,9 @@ subscription StoryLikeSubscription( } } +""" + Fragment description +""" fragment frag on Friend @onFragmentDefinition { foo(size: $size, bar: $b, obj: {key: "value", block: """ diff --git a/tests/fixtures/schema_kitchen_sink.graphql b/tests/fixtures/schema_kitchen_sink.graphql index 8ec1f2d8..c1d9d06e 100644 --- a/tests/fixtures/schema_kitchen_sink.graphql +++ b/tests/fixtures/schema_kitchen_sink.graphql @@ -26,6 +26,7 @@ type Foo implements Bar & Baz & Two { five(argument: [String] = ["string", "string"]): String six(argument: InputType = {key: "value"}): Type seven(argument: Int = null): Type + eight(argument: OneOfInputType): Type } type AnnotatedObject @onObject(arg: "value") { @@ -115,6 +116,11 @@ input InputType { answer: Int = 42 } +input OneOfInputType @oneOf { + string: String + int: Int +} + input AnnotatedInput @onInputObject { annotatedField: Type @onInputFieldDefinition } diff --git a/tests/language/test_block_string_fuzz.py b/tests/language/test_block_string_fuzz.py index b780ce9f..e6715564 100644 --- a/tests/language/test_block_string_fuzz.py +++ b/tests/language/test_block_string_fuzz.py @@ -6,7 +6,7 @@ is_printable_as_block_string, ) -from ..utils import dedent, gen_fuzz_strings +from ..utils import dedent, gen_fuzz_strings, timeout_factor def lex_value(s: str) -> str: @@ -20,29 +20,25 @@ def lex_value(s: str) -> str: def assert_printable_block_string(test_value: str, minimize: bool = False) -> None: block_string = print_block_string(test_value, minimize=minimize) printed_value = lex_value(block_string) - assert test_value == printed_value, dedent( - f""" + assert test_value == printed_value, dedent(f""" Expected lexValue({block_string!r}) to equal {test_value!r} but got {printed_value!r} - """ - ) + """) def assert_non_printable_block_string(test_value: str) -> None: block_string = print_block_string(test_value) printed_value = lex_value(block_string) - assert test_value != printed_value, dedent( - f""" + assert test_value != printed_value, dedent(f""" Expected lexValue({block_string!r}) to not equal {test_value!r} - """ - ) + """) def describe_print_block_string(): @mark.slow - @mark.timeout(20) + @mark.timeout(80 * timeout_factor) def correctly_print_random_strings(): # Testing with length >7 is taking exponentially more time. However, it is # highly recommended testing with increased limit if you make any change. diff --git a/tests/language/test_lexer.py b/tests/language/test_lexer.py index a1ac5abf..5bee2440 100644 --- a/tests/language/test_lexer.py +++ b/tests/language/test_lexer.py @@ -34,7 +34,7 @@ def assert_syntax_error(text: str, message: str, location: Location) -> None: def describe_lexer(): def ignores_bom_header(): - token = lex_one("\uFEFF foo") + token = lex_one("\ufeff foo") assert token == Token(TokenKind.NAME, 2, 5, 1, 3, "foo") def tracks_line_breaks(): @@ -75,8 +75,7 @@ def errors_respect_whitespace(): with raises(GraphQLSyntaxError) as exc_info: lex_one("\n\n ~\n") - assert str(exc_info.value) == dedent( - """ + assert str(exc_info.value) == dedent(""" Syntax Error: Unexpected character: '~'. GraphQL request:3:2 @@ -84,16 +83,14 @@ def errors_respect_whitespace(): 3 | ~ | ^ 4 | - """ - ) + """) def updates_line_numbers_in_error_for_file_context(): s = "\n\n ~\n\n" source = Source(s, "foo.js", SourceLocation(11, 12)) with raises(GraphQLSyntaxError) as exc_info: Lexer(source).advance() - assert str(exc_info.value) == dedent( - """ + assert str(exc_info.value) == dedent(""" Syntax Error: Unexpected character: '~'. foo.js:13:6 @@ -101,22 +98,19 @@ def updates_line_numbers_in_error_for_file_context(): 13 | ~ | ^ 14 | - """ - ) + """) def updates_column_numbers_in_error_for_file_context(): source = Source("~", "foo.js", SourceLocation(1, 5)) with raises(GraphQLSyntaxError) as exc_info: Lexer(source).advance() - assert str(exc_info.value) == dedent( - """ + assert str(exc_info.value) == dedent(""" Syntax Error: Unexpected character: '~'. foo.js:1:5 1 | ~ | ^ - """ - ) + """) # noinspection PyArgumentEqualDefault def lexes_empty_string(): @@ -138,8 +132,8 @@ def lexes_strings(): assert lex_one('"slashes \\\\ \\/"') == Token( TokenKind.STRING, 0, 15, 1, 1, "slashes \\ /" ) - assert lex_one('"unescaped surrogate pair \uD83D\uDE00"') == Token( - TokenKind.STRING, 0, 29, 1, 1, "unescaped surrogate pair \uD83D\uDE00" + assert lex_one('"unescaped surrogate pair \ud83d\ude00"') == Token( + TokenKind.STRING, 0, 29, 1, 1, "unescaped surrogate pair \ud83d\ude00" ) assert lex_one('"unescaped unicode outside BMP \U0001f600"') == Token( TokenKind.STRING, 0, 33, 1, 1, "unescaped unicode outside BMP \U0001f600" @@ -153,10 +147,10 @@ def lexes_strings(): "unescaped maximal unicode outside BMP \U0010ffff", ) assert lex_one('"unicode \\u1234\\u5678\\u90AB\\uCDEF"') == Token( - TokenKind.STRING, 0, 34, 1, 1, "unicode \u1234\u5678\u90AB\uCDEF" + TokenKind.STRING, 0, 34, 1, 1, "unicode \u1234\u5678\u90ab\ucdef" ) assert lex_one('"unicode \\u{1234}\\u{5678}\\u{90AB}\\u{CDEF}"') == Token( - TokenKind.STRING, 0, 42, 1, 1, "unicode \u1234\u5678\u90AB\uCDEF" + TokenKind.STRING, 0, 42, 1, 1, "unicode \u1234\u5678\u90ab\ucdef" ) assert lex_one('"string with unicode escape outside BMP \\u{1F600}"') == Token( TokenKind.STRING, @@ -164,7 +158,7 @@ def lexes_strings(): 50, 1, 1, - "string with unicode escape outside BMP \U0001F600", + "string with unicode escape outside BMP \U0001f600", ) assert lex_one('"string with minimal unicode escape \\u{0}"') == Token( TokenKind.STRING, 0, 42, 1, 1, "string with minimal unicode escape \u0000" @@ -175,7 +169,7 @@ def lexes_strings(): 47, 1, 1, - "string with maximal unicode escape \U0010FFFF", + "string with maximal unicode escape \U0010ffff", ) assert lex_one( '"string with maximal minimal unicode escape \\u{00000000}"' @@ -215,7 +209,7 @@ def lexes_strings(): 56, 1, 1, - "string with unicode surrogate pair escape \U0010FFFF", + "string with unicode surrogate pair escape \U0010ffff", ) def lex_reports_useful_string_errors(): @@ -230,17 +224,17 @@ def lex_reports_useful_string_errors(): (1, 1), ) assert_syntax_error( - '"bad surrogate \uDEAD"', + '"bad surrogate \udead"', "Invalid character within String: U+DEAD.", (1, 16), ) assert_syntax_error( - '"bad high surrogate pair \uDEAD\uDEAD"', + '"bad high surrogate pair \udead\udead"', "Invalid character within String: U+DEAD.", (1, 26), ) assert_syntax_error( - '"bad low surrogate pair \uD800\uD800"', + '"bad low surrogate pair \ud800\ud800"', "Invalid character within String: U+D800.", (1, 25), ) @@ -322,12 +316,12 @@ def lex_reports_useful_string_errors(): (1, 25), ) assert_syntax_error( - '"cannot escape half a pair \uD83D\\uDE00 esc"', + '"cannot escape half a pair \ud83d\\uDE00 esc"', "Invalid character within String: U+D83D.", (1, 28), ) assert_syntax_error( - '"cannot escape half a pair \\uD83D\uDE00 esc"', + '"cannot escape half a pair \\uD83D\ude00 esc"', "Invalid Unicode escape sequence: '\\uD83D'.", (1, 28), ) @@ -366,13 +360,13 @@ def lexes_block_strings(): 1, "unescaped \\n\\r\\b\\t\\f\\u1234", ) - assert lex_one('"""unescaped surrogate pair \uD83D\uDE00"""') == Token( + assert lex_one('"""unescaped surrogate pair \ud83d\ude00"""') == Token( TokenKind.BLOCK_STRING, 0, 33, 1, 1, - "unescaped surrogate pair \uD83D\uDE00", + "unescaped surrogate pair \ud83d\ude00", ) assert lex_one('"""unescaped unicode outside BMP \U0001f600"""') == Token( TokenKind.BLOCK_STRING, @@ -391,24 +385,19 @@ def lexes_block_strings(): ) == Token(TokenKind.BLOCK_STRING, 0, 68, 1, 1, "spans\n multiple\n lines") def advance_line_after_lexing_multiline_block_string(): - assert ( - lex_second( - '''""" + assert lex_second('''""" spans multiple lines - \n """ second_token''' - ) - == Token(TokenKind.NAME, 71, 83, 8, 6, "second_token") - ) + \n """ second_token''') == Token(TokenKind.NAME, 71, 83, 8, 6, "second_token") def lex_reports_useful_block_string_errors(): assert_syntax_error('"""', "Unterminated string.", (1, 4)) assert_syntax_error('"""no end quote', "Unterminated string.", (1, 16)) assert_syntax_error( - '"""contains invalid surrogate \uDEAD"""', + '"""contains invalid surrogate \udead"""', "Invalid character within String: U+DEAD.", (1, 31), ) @@ -530,16 +519,16 @@ def lex_reports_useful_unknown_character_error(): assert_syntax_error("~", "Unexpected character: '~'.", (1, 1)) assert_syntax_error("\x00", "Unexpected character: U+0000.", (1, 1)) assert_syntax_error("\b", "Unexpected character: U+0008.", (1, 1)) - assert_syntax_error("\xAA", "Unexpected character: U+00AA.", (1, 1)) - assert_syntax_error("\u0AAA", "Unexpected character: U+0AAA.", (1, 1)) - assert_syntax_error("\u203B", "Unexpected character: U+203B.", (1, 1)) + assert_syntax_error("\xaa", "Unexpected character: U+00AA.", (1, 1)) + assert_syntax_error("\u0aaa", "Unexpected character: U+0AAA.", (1, 1)) + assert_syntax_error("\u203b", "Unexpected character: U+203B.", (1, 1)) assert_syntax_error("\U0001f600", "Unexpected character: U+1F600.", (1, 1)) - assert_syntax_error("\uD83D\uDE00", "Unexpected character: U+1F600.", (1, 1)) - assert_syntax_error("\uD800\uDC00", "Unexpected character: U+10000.", (1, 1)) - assert_syntax_error("\uDBFF\uDFFF", "Unexpected character: U+10FFFF.", (1, 1)) - assert_syntax_error("\uD800", "Invalid character: U+D800.", (1, 1)) - assert_syntax_error("\uDBFF", "Invalid character: U+DBFF.", (1, 1)) - assert_syntax_error("\uDEAD", "Invalid character: U+DEAD.", (1, 1)) + assert_syntax_error("\ud83d\ude00", "Unexpected character: U+1F600.", (1, 1)) + assert_syntax_error("\ud800\udc00", "Unexpected character: U+10000.", (1, 1)) + assert_syntax_error("\udbff\udfff", "Unexpected character: U+10FFFF.", (1, 1)) + assert_syntax_error("\ud800", "Invalid character: U+D800.", (1, 1)) + assert_syntax_error("\udbff", "Invalid character: U+DBFF.", (1, 1)) + assert_syntax_error("\udead", "Invalid character: U+DEAD.", (1, 1)) # noinspection PyArgumentEqualDefault def lex_reports_useful_information_for_dashes_in_names(): @@ -556,14 +545,12 @@ def lex_reports_useful_information_for_dashes_in_names(): assert error.locations == [(1, 3)] def produces_double_linked_list_of_tokens_including_comments(): - source = Source( - """ + source = Source(""" { #comment field } - """ - ) + """) lexer = Lexer(source) start_token = lexer.token while True: @@ -601,11 +588,11 @@ def lexes_comments(): assert lex_one("# Comment \U0001f600").prev == Token( TokenKind.COMMENT, 0, 11, 1, 1, " Comment \U0001f600" ) - assert lex_one("# Comment \uD83D\uDE00").prev == Token( - TokenKind.COMMENT, 0, 12, 1, 1, " Comment \uD83D\uDE00" + assert lex_one("# Comment \ud83d\ude00").prev == Token( + TokenKind.COMMENT, 0, 12, 1, 1, " Comment \ud83d\ude00" ) assert_syntax_error( - "# Invalid surrogate \uDEAD", "Invalid character: U+DEAD.", (1, 21) + "# Invalid surrogate \udead", "Invalid character: U+DEAD.", (1, 21) ) diff --git a/tests/language/test_parser.py b/tests/language/test_parser.py index 027a605b..8f4b0f60 100644 --- a/tests/language/test_parser.py +++ b/tests/language/test_parser.py @@ -4,13 +4,18 @@ from graphql.error import GraphQLSyntaxError from graphql.language import ( + ArgumentCoordinateNode, ArgumentNode, DefinitionNode, + DirectiveArgumentCoordinateNode, + DirectiveCoordinateNode, DocumentNode, FieldNode, + FragmentDefinitionNode, IntValueNode, ListTypeNode, ListValueNode, + MemberCoordinateNode, NameNode, NamedTypeNode, NonNullTypeNode, @@ -21,11 +26,14 @@ OperationType, SelectionSetNode, StringValueNode, + TypeCoordinateNode, ValueNode, + VariableDefinitionNode, VariableNode, Token, TokenKind, parse, + parse_schema_coordinate, parse_type, parse_value, parse_const_value, @@ -56,15 +64,13 @@ def parse_provides_useful_errors(): assert error.message == "Syntax Error: Expected Name, found ." assert error.positions == [1] assert error.locations == [(1, 2)] - assert str(error) == dedent( - """ + assert str(error) == dedent(""" Syntax Error: Expected Name, found . GraphQL request:1:2 1 | { | ^ - """ - ) + """) assert_syntax_error( "\n { ...MissingOn }\n fragment MissingOn Type", "Expected 'on', found Name 'Type'.", @@ -81,15 +87,34 @@ def parse_provides_useful_error_when_using_source(): with raises(GraphQLSyntaxError) as exc_info: parse(Source("query", "MyQuery.graphql")) error = exc_info.value - assert str(error) == dedent( - """ + assert str(error) == dedent(""" Syntax Error: Expected '{', found . MyQuery.graphql:1:6 1 | query | ^ - """ - ) + """) + + def exposes_the_token_count(): + assert parse("{ foo }").token_count == 3 + assert parse('{ foo(bar: "baz") }').token_count == 8 + + def limits_maximum_number_of_tokens(): + assert parse("{ foo }", max_tokens=3) + with raises( + GraphQLSyntaxError, + match="Syntax Error: Document contains more than 2 tokens." + " Parsing aborted.", + ): + assert parse("{ foo }", max_tokens=2) + + assert parse('{ foo(bar: "baz") }', max_tokens=8) + with raises( + GraphQLSyntaxError, + match="Syntax Error: Document contains more than 7 tokens." + " Parsing aborted.", + ): + assert parse('{ foo(bar: "baz") }', max_tokens=7) def parses_variable_inline_values(): parse("{ field(complex: { a: { b: [ $var ] } }) }") @@ -131,12 +156,10 @@ def does_not_allow_true_false_or_null_as_enum_value(): def parses_multi_byte_characters(): # Note: \u0A0A could be naively interpreted as two line-feed chars. - doc = parse( - """ - # This comment has a \u0A0A multi-byte character. - { field(arg: "Has a \u0A0A multi-byte character.") } - """ - ) + doc = parse(""" + # This comment has a \u0a0a multi-byte character. + { field(arg: "Has a \u0a0a multi-byte character.") } + """) definitions = doc.definitions assert isinstance(definitions, tuple) assert len(definitions) == 1 @@ -149,7 +172,7 @@ def parses_multi_byte_characters(): assert len(arguments) == 1 value = arguments[0].value assert isinstance(value, StringValueNode) - assert value.value == "Has a \u0A0A multi-byte character." + assert value.value == "Has a \u0a0a multi-byte character." # noinspection PyShadowingNames def parses_kitchen_sink(kitchen_sink_query): # noqa: F811 @@ -181,54 +204,42 @@ def allows_non_keywords_anywhere_a_name_is_allowed(): parse(document) def parses_anonymous_mutation_operations(): - parse( - """ + parse(""" mutation { mutationField } - """ - ) + """) def parses_anonymous_subscription_operations(): - parse( - """ + parse(""" subscription { subscriptionField } - """ - ) + """) def parses_named_mutation_operations(): - parse( - """ + parse(""" mutation Foo { mutationField } - """ - ) + """) def parses_named_subscription_operations(): - parse( - """ + parse(""" subscription Foo { subscriptionField } - """ - ) + """) def creates_ast(): - doc = parse( - dedent( - """ + doc = parse(dedent(""" { node(id: 4) { id, name } } - """ - ) - ) + """)) assert isinstance(doc, DocumentNode) assert doc.loc == (0, 40) definitions = doc.definitions @@ -238,6 +249,7 @@ def creates_ast(): assert isinstance(definition, DefinitionNode) assert definition.loc == (0, 40) assert definition.operation == OperationType.QUERY + assert definition.description is None assert definition.name is None assert definition.variable_definitions == () assert definition.directives == () @@ -311,17 +323,13 @@ def creates_ast(): assert field.selection_set is None def creates_ast_from_nameless_query_without_variables(): - doc = parse( - dedent( - """ + doc = parse(dedent(""" query { node { id } } - """ - ) - ) + """)) assert isinstance(doc, DocumentNode) assert doc.loc == (0, 29) definitions = doc.definitions @@ -331,6 +339,7 @@ def creates_ast_from_nameless_query_without_variables(): assert isinstance(definition, OperationDefinitionNode) assert definition.loc == (0, 29) assert definition.operation == OperationType.QUERY + assert definition.description is None assert definition.name is None assert definition.variable_definitions == () assert definition.directives == () @@ -368,6 +377,66 @@ def creates_ast_from_nameless_query_without_variables(): assert field.directives == () assert field.selection_set is None + def creates_ast_from_nameless_query_with_description(): + doc = parse(dedent(""" + "Description" + query { + node { + id + } + } + """)) + assert isinstance(doc, DocumentNode) + assert doc.loc == (0, 43) + definitions = doc.definitions + assert isinstance(definitions, tuple) + assert len(definitions) == 1 + definition = definitions[0] + assert isinstance(definition, OperationDefinitionNode) + assert definition.loc == (0, 43) + description = definition.description + assert isinstance(description, StringValueNode) + assert description.loc == (0, 13) + assert description.value == "Description" + assert description.block is False + assert definition.operation == OperationType.QUERY + assert definition.name is None + assert definition.variable_definitions == () + assert definition.directives == () + selection_set: Optional[SelectionSetNode] = definition.selection_set + assert isinstance(selection_set, SelectionSetNode) + assert selection_set.loc == (20, 43) + selections = selection_set.selections + assert isinstance(selections, tuple) + assert len(selections) == 1 + field = selections[0] + assert isinstance(field, FieldNode) + assert field.loc == (24, 41) + assert field.alias is None + name = field.name + assert isinstance(name, NameNode) + assert name.loc == (24, 28) + assert name.value == "node" + assert field.arguments == () + assert field.directives == () + selection_set = field.selection_set + assert isinstance(selection_set, SelectionSetNode) + assert selection_set.loc == (29, 41) + selections = selection_set.selections + assert isinstance(selections, tuple) + assert len(selections) == 1 + field = selections[0] + assert isinstance(field, FieldNode) + assert field.loc == (35, 37) + assert field.alias is None + name = field.name + assert isinstance(name, NameNode) + assert name.loc == (35, 37) + assert name.value == "id" + assert field.arguments == () + assert field.directives == () + assert field.selection_set is None + def allows_parsing_without_source_location_information(): result = parse("{ id }", no_location=True) assert result.loc is None @@ -400,13 +469,11 @@ def contains_references_to_start_and_end_tokens(): def allows_comments_everywhere_in_the_source(): # make sure first and last line can be comment - result = parse( - """# top comment + result = parse("""# top comment { field # field comment } - # bottom comment""" - ) + # bottom comment""") top_comment = result.loc and result.loc.start_token.next assert top_comment and top_comment.kind is TokenKind.COMMENT assert top_comment.value == " top comment" @@ -417,6 +484,167 @@ def allows_comments_everywhere_in_the_source(): assert bottom_comment and bottom_comment.kind is TokenKind.COMMENT assert bottom_comment.value == " bottom comment" + def describe_operation_and_variable_definition_descriptions(): + def parses_operation_with_description_and_variable_descriptions(): + source = ( + '"Operation description"\n' + "query myQuery(\n" + ' "Variable a description"\n' + " $a: Int,\n" + ' """Variable b\nmultiline description"""\n' + " $b: String\n" + ") {\n" + " field(a: $a, b: $b)\n" + "}" + ) + doc = parse(source) + op_def = doc.definitions[0] + assert isinstance(op_def, OperationDefinitionNode) + assert op_def.operation == OperationType.QUERY + assert op_def.loc == (0, 158) + description = op_def.description + assert isinstance(description, StringValueNode) + assert description.value == "Operation description" + assert description.block is False + assert description.loc == (0, 23) + name = op_def.name + assert isinstance(name, NameNode) + assert name.value == "myQuery" + assert name.loc == (30, 37) + var_defs = op_def.variable_definitions + assert isinstance(var_defs, tuple) + assert len(var_defs) == 2 + var_def = var_defs[0] + assert isinstance(var_def, VariableDefinitionNode) + assert var_def.loc == (41, 75) + description = var_def.description + assert isinstance(description, StringValueNode) + assert description.value == "Variable a description" + assert description.block is False + assert description.loc == (41, 65) + variable = var_def.variable + assert isinstance(variable, VariableNode) + assert variable.loc == (68, 70) + assert variable.name.value == "a" + assert variable.name.loc == (69, 70) + type_ = var_def.type + assert isinstance(type_, NamedTypeNode) + assert type_.name.value == "Int" + assert type_.loc == (72, 75) + assert var_def.default_value is None + assert var_def.directives == () + var_def = var_defs[1] + assert isinstance(var_def, VariableDefinitionNode) + assert var_def.loc == (79, 130) + description = var_def.description + assert isinstance(description, StringValueNode) + assert description.value == "Variable b\nmultiline description" + assert description.block is True + assert description.loc == (79, 117) + variable = var_def.variable + assert isinstance(variable, VariableNode) + assert variable.loc == (120, 122) + assert variable.name.value == "b" + assert variable.name.loc == (121, 122) + type_ = var_def.type + assert isinstance(type_, NamedTypeNode) + assert type_.name.value == "String" + assert type_.loc == (124, 130) + assert var_def.default_value is None + assert var_def.directives == () + assert op_def.directives == () + selection_set = op_def.selection_set + assert isinstance(selection_set, SelectionSetNode) + assert selection_set.loc == (133, 158) + field = selection_set.selections[0] + assert isinstance(field, FieldNode) + assert field.name.value == "field" + assert field.name.loc == (137, 142) + assert field.loc == (137, 156) + args = field.arguments + assert isinstance(args, tuple) + assert len(args) == 2 + assert args[0].name.value == "a" + assert args[0].loc == (143, 148) + assert args[1].name.value == "b" + assert args[1].loc == (150, 155) + + def descriptions_on_a_short_hand_query_produce_a_sensible_error(): + with raises(GraphQLSyntaxError) as exc_info: + parse('"""Invalid"""\n { __typename }') + assert exc_info.value.message == ( + "Syntax Error: Unexpected description," + " descriptions are not supported on shorthand queries." + ) + + def parses_variable_definition_with_description_default_and_directives(): + doc = parse(dedent(""" + query ( + "desc" + $foo: Int = 42 @dir + ) { + field(foo: $foo) + } + """)) + op_def = doc.definitions[0] + assert isinstance(op_def, OperationDefinitionNode) + var_def = op_def.variable_definitions[0] + assert isinstance(var_def, VariableDefinitionNode) + assert var_def.loc == (10, 38) + default_value = var_def.default_value + assert isinstance(default_value, IntValueNode) + assert default_value.value == "42" + assert default_value.loc == (31, 33) + directives = var_def.directives + assert isinstance(directives, tuple) + assert len(directives) == 1 + directive = directives[0] + assert directive.name.value == "dir" + assert directive.name.loc == (35, 38) + assert directive.arguments == () + assert directive.loc == (34, 38) + description = var_def.description + assert isinstance(description, StringValueNode) + assert description.value == "desc" + assert description.block is False + assert description.loc == (10, 16) + variable = var_def.variable + assert isinstance(variable, VariableNode) + assert variable.name.value == "foo" + assert variable.name.loc == (20, 23) + assert variable.loc == (19, 23) + type_ = var_def.type + assert isinstance(type_, NamedTypeNode) + assert type_.name.value == "Int" + assert type_.loc == (25, 28) + + def parses_fragment_with_variable_description_legacy(): + doc = parse( + 'fragment Foo("desc" $foo: Int) on Bar { baz }', + allow_legacy_fragment_variables=True, + ) + frag_def = doc.definitions[0] + assert isinstance(frag_def, FragmentDefinitionNode) + var_def = frag_def.variable_definitions[0] + assert isinstance(var_def, VariableDefinitionNode) + assert var_def.loc == (13, 29) + description = var_def.description + assert isinstance(description, StringValueNode) + assert description.value == "desc" + assert description.block is False + assert description.loc == (13, 19) + variable = var_def.variable + assert isinstance(variable, VariableNode) + assert variable.name.value == "foo" + assert variable.name.loc == (21, 24) + assert variable.loc == (20, 24) + type_ = var_def.type + assert isinstance(type_, NamedTypeNode) + assert type_.name.value == "Int" + assert type_.loc == (26, 29) + assert var_def.default_value is None + assert var_def.directives == () + def describe_parse_value(): def parses_null_value(): @@ -593,3 +821,134 @@ def parses_nested_types(): assert isinstance(name, NameNode) assert name.loc == (1, 7) assert name.value == "MyType" + + +def describe_parse_schema_coordinate(): + def parses_name(): + result = parse_schema_coordinate("MyType") + assert isinstance(result, TypeCoordinateNode) + assert result.loc == (0, 6) + name = result.name + assert isinstance(name, NameNode) + assert name.loc == (0, 6) + assert name.value == "MyType" + + def parses_name_dot_name(): + result = parse_schema_coordinate("MyType.field") + assert isinstance(result, MemberCoordinateNode) + assert result.loc == (0, 12) + name = result.name + assert isinstance(name, NameNode) + assert name.loc == (0, 6) + assert name.value == "MyType" + member_name = result.member_name + assert isinstance(member_name, NameNode) + assert member_name.loc == (7, 12) + assert member_name.value == "field" + + def rejects_name_dot_name_dot_name(): + with raises(GraphQLSyntaxError) as exc_info: + parse_schema_coordinate("MyType.field.deep") + assert exc_info.value == { + "message": "Syntax Error: Expected , found '.'.", + "locations": [(1, 13)], + } + + def parses_name_dot_name_argument(): + result = parse_schema_coordinate("MyType.field(arg:)") + assert isinstance(result, ArgumentCoordinateNode) + assert result.loc == (0, 18) + name = result.name + assert isinstance(name, NameNode) + assert name.loc == (0, 6) + assert name.value == "MyType" + field_name = result.field_name + assert isinstance(field_name, NameNode) + assert field_name.loc == (7, 12) + assert field_name.value == "field" + argument_name = result.argument_name + assert isinstance(argument_name, NameNode) + assert argument_name.loc == (13, 16) + assert argument_name.value == "arg" + + def rejects_name_dot_name_argument_with_value(): + with raises(GraphQLSyntaxError) as exc_info: + parse_schema_coordinate("MyType.field(arg: value)") + assert exc_info.value == { + "message": "Syntax Error: Invalid character: ' '.", + "locations": [(1, 18)], + } + + def parses_directive(): + result = parse_schema_coordinate("@myDirective") + assert isinstance(result, DirectiveCoordinateNode) + assert result.loc == (0, 12) + name = result.name + assert isinstance(name, NameNode) + assert name.loc == (1, 12) + assert name.value == "myDirective" + + def parses_directive_argument(): + result = parse_schema_coordinate("@myDirective(arg:)") + assert isinstance(result, DirectiveArgumentCoordinateNode) + assert result.loc == (0, 18) + name = result.name + assert isinstance(name, NameNode) + assert name.loc == (1, 12) + assert name.value == "myDirective" + argument_name = result.argument_name + assert isinstance(argument_name, NameNode) + assert argument_name.loc == (13, 16) + assert argument_name.value == "arg" + + def parses_meta_type(): + result = parse_schema_coordinate("__Type") + assert isinstance(result, TypeCoordinateNode) + assert result.loc == (0, 6) + name = result.name + assert isinstance(name, NameNode) + assert name.loc == (0, 6) + assert name.value == "__Type" + + def parses_meta_field(): + result = parse_schema_coordinate("Type.__metafield") + assert isinstance(result, MemberCoordinateNode) + assert result.loc == (0, 16) + name = result.name + assert isinstance(name, NameNode) + assert name.loc == (0, 4) + assert name.value == "Type" + member_name = result.member_name + assert isinstance(member_name, NameNode) + assert member_name.loc == (5, 16) + assert member_name.value == "__metafield" + + def parses_meta_field_argument(): + result = parse_schema_coordinate("Type.__metafield(arg:)") + assert isinstance(result, ArgumentCoordinateNode) + assert result.loc == (0, 22) + name = result.name + assert isinstance(name, NameNode) + assert name.loc == (0, 4) + assert name.value == "Type" + field_name = result.field_name + assert isinstance(field_name, NameNode) + assert field_name.loc == (5, 16) + assert field_name.value == "__metafield" + argument_name = result.argument_name + assert isinstance(argument_name, NameNode) + assert argument_name.loc == (17, 20) + assert argument_name.value == "arg" + + def rejects_directive_dot_name(): + with raises(GraphQLSyntaxError) as exc_info: + parse_schema_coordinate("@myDirective.field") + assert exc_info.value == { + "message": "Syntax Error: Expected , found '.'.", + "locations": [(1, 13)], + } + + def accepts_a_source_object(): + assert parse_schema_coordinate("MyType") == parse_schema_coordinate( + Source("MyType") + ) diff --git a/tests/language/test_predicates.py b/tests/language/test_predicates.py index 06369b0a..2d27bba0 100644 --- a/tests/language/test_predicates.py +++ b/tests/language/test_predicates.py @@ -15,6 +15,7 @@ is_type_definition_node, is_type_system_extension_node, is_type_extension_node, + is_schema_coordinate_node, ) all_ast_nodes = sorted( @@ -143,6 +144,7 @@ def check_type_definition_node(): def check_type_system_extension_node(): assert filter_nodes(is_type_system_extension_node) == [ + "directive_extension", "enum_type_extension", "input_object_type_extension", "interface_type_extension", @@ -163,3 +165,12 @@ def check_type_extension_node(): "type_extension", "union_type_extension", ] + + def check_schema_coordinate_node(): + assert filter_nodes(is_schema_coordinate_node) == [ + "argument_coordinate", + "directive_argument_coordinate", + "directive_coordinate", + "member_coordinate", + "type_coordinate", + ] diff --git a/tests/language/test_print_string.py b/tests/language/test_print_string.py index 644c6669..8daa2e27 100644 --- a/tests/language/test_print_string.py +++ b/tests/language/test_print_string.py @@ -21,23 +21,23 @@ def does_not_escape_space(): assert print_string(" ") == '" "' def does_not_escape_non_ascii_character(): - assert print_string("\u21BB") == '"\u21BB"' + assert print_string("\u21bb") == '"\u21bb"' def does_not_escape_supplementary_character(): assert print_string("\U0001f600") == '"\U0001f600"' def escapes_all_control_chars(): assert print_string( - "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F" - "\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F" - "\x20\x21\x22\x23\x24\x25\x26\x27\x28\x29\x2A\x2B\x2C\x2D\x2E\x2F" - "\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39\x3A\x3B\x3C\x3D\x3E\x3F" - "\x40\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4A\x4B\x4C\x4D\x4E\x4F" - "\x50\x51\x52\x53\x54\x55\x56\x57\x58\x59\x5A\x5B\x5C\x5D\x5E\x5F" - "\x60\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6A\x6B\x6C\x6D\x6E\x6F" - "\x70\x71\x72\x73\x74\x75\x76\x77\x78\x79\x7A\x7B\x7C\x7D\x7E\x7F" - "\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8A\x8B\x8C\x8D\x8E\x8F" - "\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9A\x9B\x9C\x9D\x9E\x9F" + "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f" + "\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f" + "\x20\x21\x22\x23\x24\x25\x26\x27\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f" + "\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39\x3a\x3b\x3c\x3d\x3e\x3f" + "\x40\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a\x4b\x4c\x4d\x4e\x4f" + "\x50\x51\x52\x53\x54\x55\x56\x57\x58\x59\x5a\x5b\x5c\x5d\x5e\x5f" + "\x60\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6a\x6b\x6c\x6d\x6e\x6f" + "\x70\x71\x72\x73\x74\x75\x76\x77\x78\x79\x7a\x7b\x7c\x7d\x7e\x7f" + "\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f" + "\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f" ) == ( '"\\u0000\\u0001\\u0002\\u0003\\u0004\\u0005\\u0006\\u0007' "\\b\\t\\n\\u000B\\f\\r\\u000E\\u000F" diff --git a/tests/language/test_printer.py b/tests/language/test_printer.py index 4468478b..bc543a9d 100644 --- a/tests/language/test_printer.py +++ b/tests/language/test_printer.py @@ -2,7 +2,14 @@ from pytest import raises -from graphql.language import FieldNode, NameNode, parse, print_ast +from graphql.error import GraphQLSyntaxError +from graphql.language import ( + FieldNode, + NameNode, + parse, + parse_schema_coordinate, + print_ast, +) from ..fixtures import kitchen_sink_query # noqa: F401 from ..utils import dedent @@ -36,52 +43,44 @@ def correctly_prints_query_operation_with_artifacts(): query_ast_with_artifacts = parse( "query ($foo: TestType) @testDirective { id, name }" ) - assert print_ast(query_ast_with_artifacts) == dedent( - """ + assert print_ast(query_ast_with_artifacts) == dedent(""" query ($foo: TestType) @testDirective { id name } - """ - ) + """) def correctly_prints_mutation_operation_with_artifacts(): mutation_ast_with_artifacts = parse( "mutation ($foo: TestType) @testDirective { id, name }" ) - assert print_ast(mutation_ast_with_artifacts) == dedent( - """ + assert print_ast(mutation_ast_with_artifacts) == dedent(""" mutation ($foo: TestType) @testDirective { id name } - """ - ) + """) def prints_query_with_variable_directives(): query_ast_with_variable_directive = parse( "query ($foo: TestType = {a: 123}" " @testDirective(if: true) @test) { id }" ) - assert print_ast(query_ast_with_variable_directive) == dedent( - """ + assert print_ast(query_ast_with_variable_directive) == dedent(""" query ($foo: TestType = {a: 123} @testDirective(if: true) @test) { id } - """ - ) + """) def keeps_arguments_on_one_line_if_line_has_80_chars_or_less(): printed = print_ast(parse("{trip(wheelchair:false arriveBy:false){dateTime}}")) - assert printed == dedent( - """ + assert printed == dedent(""" { trip(wheelchair: false, arriveBy: false) { dateTime } } - """ - ) + """) def puts_arguments_on_multiple_lines_if_line_has_more_than_80_chars(): printed = print_ast( @@ -91,8 +90,7 @@ def puts_arguments_on_multiple_lines_if_line_has_more_than_80_chars(): ) ) - assert printed == dedent( - """ + assert printed == dedent(""" { trip( wheelchair: false @@ -103,21 +101,32 @@ def puts_arguments_on_multiple_lines_if_line_has_more_than_80_chars(): dateTime } } - """ - ) + """) def legacy_prints_fragment_with_variable_directives(): query_ast_with_variable_directive = parse( "fragment Foo($foo: TestType @test) on TestType @testDirective { id }", allow_legacy_fragment_variables=True, ) - assert print_ast(query_ast_with_variable_directive) == dedent( - """ + assert print_ast(query_ast_with_variable_directive) == dedent(""" fragment Foo($foo: TestType @test) on TestType @testDirective { id } + """) + + def experimental_prints_directives_on_directives(): + query_ast_with_variable_directive = parse( """ + directive @foo @bar on FIELD_DEFINITION + extend directive @foo @baz + """, + experimental_directives_on_directive_definitions=True, ) + assert print_ast(query_ast_with_variable_directive) == dedent(""" + directive @foo @bar on FIELD_DEFINITION + + extend directive @foo @baz + """) def legacy_correctly_prints_fragment_defined_variables(): source = """ @@ -128,6 +137,43 @@ def legacy_correctly_prints_fragment_defined_variables(): fragment_with_variable = parse(source, allow_legacy_fragment_variables=True) assert print_ast(fragment_with_variable) == dedent(source) + def prints_fragment(): + printed = print_ast(parse('"Fragment description" fragment Foo on Bar { baz }')) + assert printed == dedent(""" + "Fragment description" + fragment Foo on Bar { + baz + } + """) + + def prints_schema_coordinates(): + assert print_ast(parse_schema_coordinate("Name")) == "Name" + assert print_ast(parse_schema_coordinate("Name.field")) == "Name.field" + assert ( + print_ast(parse_schema_coordinate("Name.field(arg:)")) == "Name.field(arg:)" + ) + assert print_ast(parse_schema_coordinate("@name")) == "@name" + assert print_ast(parse_schema_coordinate("@name(arg:)")) == "@name(arg:)" + assert print_ast(parse_schema_coordinate("__Type")) == "__Type" + assert ( + print_ast(parse_schema_coordinate("Type.__metafield")) == "Type.__metafield" + ) + assert ( + print_ast(parse_schema_coordinate("Type.__metafield(arg:)")) + == "Type.__metafield(arg:)" + ) + + def throws_syntax_error_for_ignored_tokens_in_schema_coordinates(): + with raises(GraphQLSyntaxError) as exc_info: + print_ast(parse_schema_coordinate("# foo\nName")) + assert "Syntax Error: Invalid character: '#'" in str(exc_info.value) + with raises(GraphQLSyntaxError) as exc_info: + print_ast(parse_schema_coordinate("\nName")) + assert "Syntax Error: Invalid character: U+000A." in str(exc_info.value) + with raises(GraphQLSyntaxError) as exc_info: + print_ast(parse_schema_coordinate("Name .field")) + assert "Syntax Error: Invalid character: ' '" in str(exc_info.value) + def prints_kitchen_sink_without_altering_ast(kitchen_sink_query): # noqa: F811 ast = parse(kitchen_sink_query, no_location=True) @@ -137,9 +183,13 @@ def prints_kitchen_sink_without_altering_ast(kitchen_sink_query): # noqa: F811 assert printed_ast == ast assert deepcopy(ast) == ast_before_print_call - assert printed == dedent( - r''' - query queryName($foo: ComplexType, $site: Site = MOBILE) @onQuery { + assert printed == dedent(r''' + "Query description" + query queryName( + "Very complex variable" + $foo: ComplexType + $site: Site = MOBILE + ) @onQuery { whoever123is: node(id: [123, 456]) { id ... on User @onInlineFragment { @@ -181,6 +231,7 @@ def prints_kitchen_sink_without_altering_ast(kitchen_sink_query): # noqa: F811 } } + """Fragment description""" fragment frag on Friend @onFragmentDefinition { foo( size: $size @@ -199,5 +250,4 @@ def prints_kitchen_sink_without_altering_ast(kitchen_sink_query): # noqa: F811 { __typename } - ''' # noqa: E501 - ) + ''') # noqa: E501 diff --git a/tests/language/test_schema_coordinate_lexer.py b/tests/language/test_schema_coordinate_lexer.py new file mode 100644 index 00000000..9912a249 --- /dev/null +++ b/tests/language/test_schema_coordinate_lexer.py @@ -0,0 +1,41 @@ +from typing import Optional, Tuple + +from pytest import raises + +from graphql.error import GraphQLSyntaxError +from graphql.language import Source, Token, TokenKind +from graphql.language.schema_coordinate_lexer import SchemaCoordinateLexer + +Location = Optional[Tuple[int, int]] + + +def lex_second(s: str) -> Token: + lexer = SchemaCoordinateLexer(Source(s)) + lexer.advance() + return lexer.advance() + + +def assert_syntax_error(text: str, message: str, location: Location) -> None: + with raises(GraphQLSyntaxError) as exc_info: + lex_second(text) + error = exc_info.value + assert error.message == f"Syntax Error: {message}" + assert error.description == message + assert error.locations == [location] + + +def describe_schema_coordinate_lexer(): + def tracks_a_schema_coordinate(): + lexer = SchemaCoordinateLexer(Source("Name.field")) + assert lexer.advance() == Token(TokenKind.NAME, 0, 4, 1, 1, "Name") + + def forbids_ignored_tokens(): + lexer = SchemaCoordinateLexer(Source("\nName.field")) + with raises(GraphQLSyntaxError) as exc_info: + lexer.advance() + error = exc_info.value + assert error.message == "Syntax Error: Invalid character: U+000A." + assert error.locations == [(1, 1)] + + def lex_reports_a_useful_syntax_error(): + assert_syntax_error("Foo .bar", "Invalid character: ' '.", (1, 4)) diff --git a/tests/language/test_schema_parser.py b/tests/language/test_schema_parser.py index 7eef1978..60135b81 100644 --- a/tests/language/test_schema_parser.py +++ b/tests/language/test_schema_parser.py @@ -1,3 +1,5 @@ +import pickle +from copy import deepcopy from textwrap import dedent from typing import List, Optional, Tuple @@ -132,13 +134,11 @@ def directive_node(name: NameNode, arguments: List[ArgumentNode], loc: Location) def describe_schema_parser(): def simple_type(): - body = dedent( - """ + body = dedent(""" type Hello { world: String } - """ - ) + """) definition = assert_definitions(body, (0, 32)) assert isinstance(definition, ObjectTypeDefinitionNode) assert definition.name == name_node("Hello", (6, 11)) @@ -153,14 +153,12 @@ def simple_type(): assert definition.loc == (1, 31) def parses_type_with_description_string(): - body = dedent( - """ + body = dedent(""" "Description" type Hello { world: String } - """ - ) + """) definition = assert_definitions(body, (0, 46)) assert isinstance(definition, ObjectTypeDefinitionNode) assert definition.name == name_node("Hello", (20, 25)) @@ -168,16 +166,14 @@ def parses_type_with_description_string(): assert description == string_value_node("Description", False, (1, 14)) def parses_type_with_description_multi_line_string(): - body = dedent( - ''' + body = dedent(''' """ Description """ # Even with comments between them type Hello { world: String - }''' - ) + }''') definition = assert_definitions(body, (0, 85)) assert isinstance(definition, ObjectTypeDefinitionNode) assert definition.name == name_node("Hello", (60, 65)) @@ -185,14 +181,12 @@ def parses_type_with_description_multi_line_string(): assert description == string_value_node("Description", True, (1, 20)) def parses_schema_with_description_string(): - body = dedent( - """ + body = dedent(""" "Description" schema { query: Foo } - """ - ) + """) definition = assert_definitions(body, (0, 39)) assert isinstance(definition, SchemaDefinitionNode) description = definition.description @@ -202,13 +196,11 @@ def description_followed_by_something_other_than_type_system_definition_throws() assert_syntax_error('"Description" 1', "Unexpected Int '1'.", (1, 15)) def simple_extension(): - body = dedent( - """ + body = dedent(""" extend type Hello { world: String } - """ - ) + """) extension = assert_definitions(body, (0, 39)) assert isinstance(extension, ObjectTypeExtensionNode) assert extension.name == name_node("Hello", (13, 18)) @@ -298,8 +290,7 @@ def object_extension_do_not_include_descriptions(): extend type Hello { world: String }""", - "Unexpected description," - " descriptions are supported only on type definitions.", + "Unexpected description," " only GraphQL definitions support descriptions.", (2, 13), ) assert_syntax_error( @@ -318,8 +309,7 @@ def interface_extension_do_not_include_descriptions(): extend interface Hello { world: String }""", - "Unexpected description," - " descriptions are supported only on type definitions.", + "Unexpected description," " only GraphQL definitions support descriptions.", (2, 13), ) assert_syntax_error( @@ -375,13 +365,11 @@ def schema_extension_with_invalid_operation_type_throws(): ) def simple_non_null_type(): - body = dedent( - """ + body = dedent(""" type Hello { world: String! } - """ - ) + """) definition = assert_definitions(body, (0, 33)) assert isinstance(definition, ObjectTypeDefinitionNode) assert definition.name == name_node("Hello", (6, 11)) @@ -523,13 +511,11 @@ def double_value_enum(): assert definition.loc == (0, 22) def simple_interface(): - body = dedent( - """ + body = dedent(""" interface Hello { world: String } - """ - ) + """) definition = assert_definitions(body, (0, 37)) assert isinstance(definition, InterfaceTypeDefinitionNode) assert definition.name == name_node("Hello", (11, 16)) @@ -544,13 +530,11 @@ def simple_interface(): assert definition.loc == (1, 36) def simple_field_with_arg(): - body = dedent( - """ + body = dedent(""" type Hello { world(flag: Boolean): String } - """ - ) + """) definition = assert_definitions(body, (0, 47)) assert isinstance(definition, ObjectTypeDefinitionNode) assert definition.name == name_node("Hello", (6, 11)) @@ -575,13 +559,11 @@ def simple_field_with_arg(): assert definition.loc == (1, 46) def simple_field_with_arg_with_default_value(): - body = dedent( - """ + body = dedent(""" type Hello { world(flag: Boolean = true): String } - """ - ) + """) definition = assert_definitions(body, (0, 54)) assert isinstance(definition, ObjectTypeDefinitionNode) assert definition.name == name_node("Hello", (6, 11)) @@ -606,13 +588,11 @@ def simple_field_with_arg_with_default_value(): assert definition.loc == (1, 53) def simple_field_with_list_arg(): - body = dedent( - """ + body = dedent(""" type Hello { world(things: [String]): String } - """ - ) + """) definition = assert_definitions(body, (0, 50)) assert isinstance(definition, ObjectTypeDefinitionNode) assert definition.name == name_node("Hello", (6, 11)) @@ -637,13 +617,11 @@ def simple_field_with_list_arg(): assert definition.loc == (1, 49) def simple_field_with_two_args(): - body = dedent( - """ + body = dedent(""" type Hello { world(argOne: Boolean, argTwo: Int): String } - """ - ) + """) definition = assert_definitions(body, (0, 62)) assert isinstance(definition, ObjectTypeDefinitionNode) assert definition.name == name_node("Hello", (6, 11)) @@ -767,6 +745,7 @@ def directive_definition(): assert definition.name == name_node("foo", (11, 14)) assert definition.description is None assert definition.arguments == () + assert definition.directives == () assert definition.repeatable is False assert definition.locations == ( name_node("OBJECT", (18, 24)), @@ -780,12 +759,20 @@ def repeatable_directive_definition(): assert definition.name == name_node("foo", (11, 14)) assert definition.description is None assert definition.arguments == () + assert definition.directives == () assert definition.repeatable is True assert definition.locations == ( name_node("OBJECT", (29, 35)), name_node("INTERFACE", (38, 47)), ) + def directive_definition_extensions_require_the_experimental_flag(): + assert_syntax_error( + "extend directive @foo @bar", + "Unexpected Name 'directive'.", + (1, 8), + ) + def directive_with_incorrect_locations(): assert_syntax_error( "\ndirective @foo on FIELD | INCORRECT_LOCATION", @@ -796,19 +783,36 @@ def directive_with_incorrect_locations(): def parses_kitchen_sink_schema(kitchen_sink_sdl): # noqa: F811 assert parse(kitchen_sink_sdl) - def can_pickle_and_unpickle_kitchen_sink_schema_ast(kitchen_sink_sdl): # noqa: F811 - import pickle - - # create a schema AST from the kitchen sink SDL - doc = parse(kitchen_sink_sdl) - # check that the schema AST can be pickled - # (particularly, there should be no recursion error) - dumped = pickle.dumps(doc) - # check that the pickle size is reasonable - assert len(dumped) < 50 * len(kitchen_sink_sdl) - loaded = pickle.loads(dumped) - # check that the un-pickled schema AST is still the same - assert loaded == doc - # check that pickling again creates the same result - dumped_again = pickle.dumps(doc) - assert dumped_again == dumped + def describe_deepcopy_and_pickle(): + def can_deep_copy_ast(kitchen_sink_sdl): # noqa: F811 + # create a schema AST from the kitchen sink SDL + doc = parse(kitchen_sink_sdl) + # make a deepcopy of the schema AST + copied_doc = deepcopy(doc) + # check that the copied AST is equal to the original one + assert copied_doc == doc + + def can_pickle_and_unpickle_ast(kitchen_sink_sdl): # noqa: F811 + # create a schema AST from the kitchen sink SDL + doc = parse(kitchen_sink_sdl) + # check that the schema AST can be pickled + # (particularly, there should be no recursion error) + dumped = pickle.dumps(doc) + # check that the pickle size is reasonable + assert len(dumped) < 50 * len(kitchen_sink_sdl) + loaded = pickle.loads(dumped) + # check that the un-pickled schema AST is still the same + assert loaded == doc + # check that pickling again creates the same result + dumped_again = pickle.dumps(doc) + assert dumped_again == dumped + + def can_deep_copy_pickled_ast(kitchen_sink_sdl): # noqa: F811 + # create a schema AST from the kitchen sink SDL + doc = parse(kitchen_sink_sdl) + # pickle and unpickle the schema AST + loaded_doc = pickle.loads(pickle.dumps(doc)) + # make a deepcopy of this + copied_doc = deepcopy(loaded_doc) + # check that the result is still equal to the original schema AST + assert copied_doc == doc diff --git a/tests/language/test_schema_printer.py b/tests/language/test_schema_printer.py index cd097a80..15747423 100644 --- a/tests/language/test_schema_printer.py +++ b/tests/language/test_schema_printer.py @@ -32,8 +32,7 @@ def prints_kitchen_sink_without_altering_ast(kitchen_sink_sdl): # noqa: F811 assert printed_ast == ast assert deepcopy(ast) == ast_before_print_call - assert printed == dedent( - ''' + assert printed == dedent(''' """This is a description of the schema as a whole.""" schema { query: QueryType @@ -58,6 +57,7 @@ def prints_kitchen_sink_without_altering_ast(kitchen_sink_sdl): # noqa: F811 five(argument: [String] = ["string", "string"]): String six(argument: InputType = {key: "value"}): Type seven(argument: Int = null): Type + eight(argument: OneOfInputType): Type } type AnnotatedObject @onObject(arg: "value") { @@ -140,6 +140,11 @@ def prints_kitchen_sink_without_altering_ast(kitchen_sink_sdl): # noqa: F811 answer: Int = 42 } + input OneOfInputType @oneOf { + string: String + int: Int + } + input AnnotatedInput @onInputObject { annotatedField: Type @onInputFieldDefinition } @@ -169,5 +174,4 @@ def prints_kitchen_sink_without_altering_ast(kitchen_sink_sdl): # noqa: F811 extend schema @onSchema { subscription: SubscriptionType } - ''' # noqa: E501 - ) + ''') # noqa: E501 diff --git a/tests/language/test_source.py b/tests/language/test_source.py index 1b74aa1b..e692bb7a 100644 --- a/tests/language/test_source.py +++ b/tests/language/test_source.py @@ -33,13 +33,11 @@ def uses_default_arguments(): assert source.location_offset == (1, 1) def can_get_location(): - body = dedent( - """ + body = dedent(""" line 1 line 2 line 3 - """ - ) + """) source = Source(body) assert source.body == body location = source.get_location(body.find("2")) diff --git a/tests/language/test_visitor.py b/tests/language/test_visitor.py index d16fe88d..8b66e412 100644 --- a/tests/language/test_visitor.py +++ b/tests/language/test_visitor.py @@ -643,13 +643,11 @@ def enter(node, *_args): def leave(node, *_args): visited.append(["leave", node.kind, get_value(node)]) - example_document_ast = parse( - """ + example_document_ast = parse(""" query ExampleOperation { someField } - """ - ) + """) visit(example_document_ast, TestVisitor(), visitor_key_map) assert visited == [ @@ -769,9 +767,13 @@ def leave(*args): assert visited == [ ["enter", "document", None, None], ["enter", "operation_definition", 0, None], + ["enter", "string_value", "description", "operation_definition"], + ["leave", "string_value", "description", "operation_definition"], ["enter", "name", "name", "operation_definition"], ["leave", "name", "name", "operation_definition"], ["enter", "variable_definition", 0, None], + ["enter", "string_value", "description", "variable_definition"], + ["leave", "string_value", "description", "variable_definition"], ["enter", "variable", "variable", "variable_definition"], ["enter", "name", "name", "variable"], ["leave", "name", "name", "variable"], @@ -1023,6 +1025,8 @@ def leave(*args): ["leave", "selection_set", "selection_set", "operation_definition"], ["leave", "operation_definition", 2, None], ["enter", "fragment_definition", 3, None], + ["enter", "string_value", "description", "fragment_definition"], + ["leave", "string_value", "description", "fragment_definition"], ["enter", "name", "name", "fragment_definition"], ["leave", "name", "name", "fragment_definition"], ["enter", "named_type", "type_condition", "fragment_definition"], diff --git a/tests/pyutils/test_description.py b/tests/pyutils/test_description.py index af87ccb6..46cab81a 100644 --- a/tests/pyutils/test_description.py +++ b/tests/pyutils/test_description.py @@ -1,27 +1,26 @@ from contextlib import contextmanager from typing import cast -from pytest import raises - from graphql import graphql_sync +from graphql.pyutils import ( + Description, + is_description, + register_description, + unregister_description, +) from graphql.type import ( GraphQLArgument, GraphQLDirective, GraphQLEnumValue, GraphQLField, GraphQLInputField, - GraphQLObjectType, GraphQLNamedType, + GraphQLObjectType, GraphQLSchema, GraphQLString, ) -from graphql.pyutils import ( - Description, - is_description, - register_description, - unregister_description, -) from graphql.utilities import get_introspection_query, print_schema +from pytest import raises from ..utils import dedent @@ -43,7 +42,7 @@ def registered(base: type): try: yield None finally: - unregister_description(LazyString) + unregister_description(base) def describe_description(): @@ -198,7 +197,7 @@ def graphql_directive(): with raises(TypeError, match="Expected name to be a string\\."): GraphQLDirective(lazy_string, []) - def handels_introspection(): + def handles_introspection(): class Lazy: def __init__(self, text: str): self.text = text @@ -258,13 +257,11 @@ def __str__(self) -> str: assert not description.evaluated assert not deprecation_reason.evaluated - assert print_schema(schema) == dedent( - ''' + assert print_schema(schema) == dedent(''' type Query { """a lazy description""" lazyField: String @deprecated(reason: "a lazy reason") } - ''' - ) + ''') assert description.evaluated assert deprecation_reason.evaluated diff --git a/tests/pyutils/test_frozen_dict.py b/tests/pyutils/test_frozen_dict.py index 594f4826..43615b7f 100644 --- a/tests/pyutils/test_frozen_dict.py +++ b/tests/pyutils/test_frozen_dict.py @@ -1,9 +1,8 @@ from copy import copy, deepcopy +from graphql.pyutils import FrozenDict, FrozenError from pytest import raises -from graphql.pyutils import FrozenError, FrozenDict - def describe_frozen_list(): def can_read(): @@ -59,7 +58,7 @@ def can_hash(): fd2 = FrozenDict({1: 2, 3: 4}) assert fd2 == fd1 assert fd2 is not fd1 - assert hash(fd2) is not hash(fd1) + assert hash(fd2) == hash(fd1) fd3 = FrozenDict({1: 2, 3: 5}) assert fd3 != fd1 assert hash(fd3) != hash(fd1) diff --git a/tests/pyutils/test_is_awaitable.py b/tests/pyutils/test_is_awaitable.py index 896697d5..96810713 100644 --- a/tests/pyutils/test_is_awaitable.py +++ b/tests/pyutils/test_is_awaitable.py @@ -1,9 +1,9 @@ import asyncio +import sys from inspect import isawaitable -from pytest import mark - from graphql.pyutils import is_awaitable +from pytest import mark def describe_is_awaitable(): @@ -76,7 +76,11 @@ async def some_coroutine(): assert is_awaitable(some_coroutine()) @mark.filterwarnings("ignore::Warning") # Deprecation and Runtime - def recognizes_an_old_style_coroutine(): + @mark.skipif( + sys.version_info >= (3, 11), + reason="Generator-based coroutines not supported after Python 3.11", + ) + def recognizes_an_old_style_coroutine(): # pragma: no cover @asyncio.coroutine def some_old_style_coroutine(): yield False # pragma: no cover diff --git a/tests/pyutils/test_undefined.py b/tests/pyutils/test_undefined.py index b7ad8cf6..9cd5303f 100644 --- a/tests/pyutils/test_undefined.py +++ b/tests/pyutils/test_undefined.py @@ -1,7 +1,11 @@ -from graphql.pyutils import Undefined +import pickle +from pytest import warns -def describe_invalid(): +from graphql.pyutils import Undefined, UndefinedType + + +def describe_Undefined(): def has_repr(): assert repr(Undefined) == "Undefined" @@ -26,3 +30,13 @@ def only_equal_to_itself(): false_object = False assert Undefined != false_object assert not Undefined == false_object + + def cannot_be_redefined(): + with warns(RuntimeWarning, match="Redefinition of 'Undefined'"): + redefined_undefined = UndefinedType() + assert redefined_undefined is Undefined + + def can_be_pickled(): + pickled_undefined = pickle.dumps(Undefined) + unpickled_undefined = pickle.loads(pickled_undefined) + assert unpickled_undefined is Undefined diff --git a/tests/test_star_wars_introspection.py b/tests/test_star_wars_introspection.py index d22d22dc..f33caca1 100644 --- a/tests/test_star_wars_introspection.py +++ b/tests/test_star_wars_introspection.py @@ -14,8 +14,7 @@ def query_star_wars(source: str) -> Any: def describe_star_wars_introspection_tests(): def describe_basic_introspection(): def allows_querying_the_schema_for_types(): - data = query_star_wars( - """ + data = query_star_wars(""" { __schema { types { @@ -23,8 +22,7 @@ def allows_querying_the_schema_for_types(): } } } - """ - ) + """) # Include all types used by StarWars schema, introspection types and # standard directives. For example, `Boolean` is used in `@skip`, # `@include` and also inside introspection types. @@ -51,8 +49,7 @@ def allows_querying_the_schema_for_types(): } def allows_querying_the_schema_for_query_type(): - data = query_star_wars( - """ + data = query_star_wars(""" { __schema { queryType { @@ -60,52 +57,44 @@ def allows_querying_the_schema_for_query_type(): } } } - """ - ) + """) assert data == {"__schema": {"queryType": {"name": "Query"}}} def allows_querying_the_schema_for_a_specific_type(): - data = query_star_wars( - """ + data = query_star_wars(""" { __type(name: "Droid") { name } } - """ - ) + """) assert data == {"__type": {"name": "Droid"}} def allows_querying_the_schema_for_an_object_kind(): - data = query_star_wars( - """ + data = query_star_wars(""" { __type(name: "Droid") { name kind } } - """ - ) + """) assert data == {"__type": {"name": "Droid", "kind": "OBJECT"}} def allows_querying_the_schema_for_an_interface_kind(): - data = query_star_wars( - """ + data = query_star_wars(""" { __type(name: "Character") { name kind } } - """ - ) + """) assert data == {"__type": {"name": "Character", "kind": "INTERFACE"}} def allows_querying_the_schema_for_object_fields(): - data = query_star_wars( - """ + data = query_star_wars(""" { __type(name: "Droid") { name @@ -118,8 +107,7 @@ def allows_querying_the_schema_for_object_fields(): } } } - """ - ) + """) assert data == { "__type": { "name": "Droid", @@ -141,8 +129,7 @@ def allows_querying_the_schema_for_object_fields(): } def allows_querying_the_schema_for_nested_object_fields(): - data = query_star_wars( - """ + data = query_star_wars(""" { __type(name: "Droid") { name @@ -159,8 +146,7 @@ def allows_querying_the_schema_for_nested_object_fields(): } } } - """ - ) + """) assert data == { "__type": { "name": "Droid", @@ -218,8 +204,7 @@ def allows_querying_the_schema_for_nested_object_fields(): } def allows_querying_the_schema_for_field_args(): - data = query_star_wars( - """ + data = query_star_wars(""" { __schema { queryType { @@ -242,8 +227,7 @@ def allows_querying_the_schema_for_field_args(): } } } - """ - ) + """) assert data == { "__schema": { @@ -308,16 +292,14 @@ def allows_querying_the_schema_for_field_args(): } def allows_querying_the_schema_for_documentation(): - data = query_star_wars( - """ + data = query_star_wars(""" { __type(name: "Droid") { name description } } - """ - ) + """) assert data == { "__type": { diff --git a/tests/test_user_registry.py b/tests/test_user_registry.py index d5f2ba95..9e38448b 100644 --- a/tests/test_user_registry.py +++ b/tests/test_user_registry.py @@ -15,12 +15,7 @@ except ImportError: # Python < 3.7 create_task = None # type: ignore -from pytest import fixture, mark - from graphql import ( - graphql, - parse, - subscribe, GraphQLArgument, GraphQLBoolean, GraphQLEnumType, @@ -33,10 +28,13 @@ GraphQLObjectType, GraphQLSchema, GraphQLString, + graphql, + parse, + subscribe, ) - -from graphql.pyutils import SimplePubSub, SimplePubSubIterator from graphql.execution.map_async_iterator import MapAsyncIterator +from graphql.pyutils import SimplePubSub, SimplePubSubIterator +from pytest import fixture, mark class User(NamedTuple): @@ -498,22 +496,23 @@ async def mutate_users(): ) async def receive_one(): - async for result in subscription_one: # type: ignore # pragma: no cover + async for result in subscription_one: # pragma: no cover received_one.append(result) if len(received_one) == 3: # pragma: no cover else break async def receive_all(): - async for result in subscription_all: # type: ignore # pragma: no cover + async for result in subscription_all: # pragma: no cover received_all.append(result) if len(received_all) == 6: # pragma: no cover else break tasks = [ - create_task(task()) if create_task else task() + create_task(task()) if create_task else task() # type: ignore for task in (mutate_users, receive_one, receive_all) ] done, pending = await wait(tasks, timeout=1) + assert len(done) == len(tasks) assert not pending expected_data: List[Dict[str, Any]] = [ diff --git a/tests/type/test_definition.py b/tests/type/test_definition.py index 1c5b5bfc..a45a714d 100644 --- a/tests/type/test_definition.py +++ b/tests/type/test_definition.py @@ -1,21 +1,19 @@ +import pickle from enum import Enum from math import isnan, nan -from typing import cast, Dict - -from pytest import mark, raises +from typing import Dict, cast from graphql.error import GraphQLError from graphql.language import ( - parse_value, EnumTypeDefinitionNode, EnumTypeExtensionNode, EnumValueNode, - Node, InputObjectTypeDefinitionNode, InputObjectTypeExtensionNode, InputValueDefinitionNode, InterfaceTypeDefinitionNode, InterfaceTypeExtensionNode, + Node, ObjectTypeDefinitionNode, ObjectTypeExtensionNode, ScalarTypeDefinitionNode, @@ -23,15 +21,16 @@ StringValueNode, TypeDefinitionNode, TypeExtensionNode, - ValueNode, UnionTypeDefinitionNode, UnionTypeExtensionNode, + ValueNode, + parse_value, ) from graphql.pyutils import Undefined from graphql.type import ( GraphQLArgument, - GraphQLEnumValue, GraphQLEnumType, + GraphQLEnumValue, GraphQLField, GraphQLInputField, GraphQLInputObjectType, @@ -43,7 +42,9 @@ GraphQLScalarType, GraphQLString, GraphQLUnionType, + introspection_types, ) +from pytest import mark, raises ScalarType = GraphQLScalarType("Scalar") ObjectType = GraphQLObjectType("Object", {}) @@ -260,6 +261,17 @@ def rejects_a_scalar_type_with_incorrect_extension_ast_nodes(): " as a collection of ScalarTypeExtensionNode instances." ) + def pickles_a_custom_scalar_type(): + foo_type = GraphQLScalarType("Foo") + cycled_foo_type = pickle.loads(pickle.dumps(foo_type)) + assert cycled_foo_type.name == foo_type.name + assert cycled_foo_type is not foo_type + + def pickles_a_specified_scalar_type(): + cycled_int_type = pickle.loads(pickle.dumps(GraphQLInt)) + assert cycled_int_type.name == "Int" + assert cycled_int_type is GraphQLInt + def describe_type_system_fields(): def defines_a_field(): @@ -1071,42 +1083,40 @@ def defines_an_enum_using_an_enum_value_map(): assert enum_type.values == {"RED": red, "BLUE": blue} def defines_an_enum_using_a_python_enum(): - colors = Enum("Colors", "RED BLUE") - enum_type = GraphQLEnumType("SomeEnum", colors) + Colors = Enum("Colors", "RED BLUE") + enum_type = GraphQLEnumType("SomeEnum", Colors) assert enum_type.values == { "RED": GraphQLEnumValue(1), "BLUE": GraphQLEnumValue(2), } def defines_an_enum_using_values_of_a_python_enum(): - colors = Enum("Colors", "RED BLUE") - enum_type = GraphQLEnumType("SomeEnum", colors, names_as_values=False) + Colors = Enum("Colors", "RED BLUE") + enum_type = GraphQLEnumType("SomeEnum", Colors, names_as_values=False) assert enum_type.values == { "RED": GraphQLEnumValue(1), "BLUE": GraphQLEnumValue(2), } def defines_an_enum_using_names_of_a_python_enum(): - colors = Enum("Colors", "RED BLUE") - enum_type = GraphQLEnumType("SomeEnum", colors, names_as_values=True) + Colors = Enum("Colors", "RED BLUE") + enum_type = GraphQLEnumType("SomeEnum", Colors, names_as_values=True) assert enum_type.values == { "RED": GraphQLEnumValue("RED"), "BLUE": GraphQLEnumValue("BLUE"), } def defines_an_enum_using_members_of_a_python_enum(): - colors = Enum("Colors", "RED BLUE") - enum_type = GraphQLEnumType("SomeEnum", colors, names_as_values=None) + Colors = Enum("Colors", "RED BLUE") + enum_type = GraphQLEnumType("SomeEnum", Colors, names_as_values=None) assert enum_type.values == { - "RED": GraphQLEnumValue(colors.RED), - "BLUE": GraphQLEnumValue(colors.BLUE), + "RED": GraphQLEnumValue(Colors.RED), + "BLUE": GraphQLEnumValue(Colors.BLUE), } def defines_an_enum_type_with_a_description(): description = "nice enum" - enum_type = GraphQLEnumType( - "SomeEnum", {}, description=description # type: ignore - ) + enum_type = GraphQLEnumType("SomeEnum", {}, description=description) assert enum_type.description is description assert enum_type.to_kwargs()["description"] is description @@ -1256,7 +1266,7 @@ def accepts_an_enum_type_with_ast_node_and_extension_ast_nodes(): extension_ast_nodes = [EnumTypeExtensionNode()] enum_type = GraphQLEnumType( "SomeEnum", - {}, # type: ignore + {}, ast_node=ast_node, extension_ast_nodes=extension_ast_nodes, ) @@ -1902,3 +1912,11 @@ def fields_have_repr(): repr(GraphQLField(GraphQLList(GraphQLInt))) == ">>" ) + + +def describe_type_system_introspection_types(): + def cannot_redefine_introspection_types(): + for name, introspection_type in introspection_types.items(): + assert introspection_type.name == name + with raises(TypeError, match=f"Redefinition of reserved type '{name}'"): + introspection_type.__class__(**introspection_type.to_kwargs()) diff --git a/tests/type/test_directives.py b/tests/type/test_directives.py index 278d1d05..6f28c62e 100644 --- a/tests/type/test_directives.py +++ b/tests/type/test_directives.py @@ -1,7 +1,12 @@ from pytest import raises from graphql.error import GraphQLError -from graphql.language import DirectiveLocation, DirectiveDefinitionNode, Node +from graphql.language import ( + DirectiveDefinitionNode, + DirectiveExtensionNode, + DirectiveLocation, + Node, +) from graphql.type import GraphQLArgument, GraphQLDirective, GraphQLInt, GraphQLString @@ -58,6 +63,18 @@ def defines_a_repeatable_directive(): assert directive.is_repeatable is True assert directive.locations == tuple(locations) + def defines_a_deprecated_directive(): + locations = [DirectiveLocation.QUERY] + directive = GraphQLDirective( + "Foo", locations=locations, deprecation_reason="Some reason" + ) + + assert directive.name == "Foo" + assert directive.args == {} + assert directive.is_repeatable is False + assert directive.locations == tuple(locations) + assert directive.deprecation_reason == "Some reason" + def directive_accepts_input_types_as_arguments(): # noinspection PyTypeChecker directive = GraphQLDirective( @@ -210,6 +227,14 @@ def rejects_a_directive_with_incorrectly_typed_description(): ) assert str(exc_info.value) == "Foo description must be a string." + def rejects_a_directive_with_incorrectly_typed_deprecation_reason(): + with raises(TypeError) as exc_info: + # noinspection PyTypeChecker + GraphQLDirective( + "Foo", locations=[], deprecation_reason={"bad": True} # type: ignore + ) + assert str(exc_info.value) == "Foo deprecation reason must be a string." + def rejects_a_directive_with_incorrectly_typed_ast_node(): with raises(TypeError) as exc_info: # noinspection PyTypeChecker @@ -217,3 +242,21 @@ def rejects_a_directive_with_incorrectly_typed_ast_node(): assert str(exc_info.value) == ( "Foo AST node must be a DirectiveDefinitionNode." ) + + def accepts_a_directive_with_extension_ast_nodes(): + extension_node = DirectiveExtensionNode() + directive = GraphQLDirective( + "Foo", locations=[], extension_ast_nodes=[extension_node] + ) + assert directive.extension_ast_nodes == (extension_node,) + + def rejects_a_directive_with_incorrectly_typed_extension_ast_nodes(): + with raises(TypeError) as exc_info: + # noinspection PyTypeChecker + GraphQLDirective( + "Foo", locations=[], extension_ast_nodes=[Node()] # type: ignore + ) + assert str(exc_info.value) == ( + "Foo extension AST nodes must be specified" + " as a collection of DirectiveExtensionNode instances." + ) diff --git a/tests/type/test_enum.py b/tests/type/test_enum.py index f85a466b..917298b2 100644 --- a/tests/type/test_enum.py +++ b/tests/type/test_enum.py @@ -41,6 +41,8 @@ class Complex2: ColorType2 = GraphQLEnumType("Color", ColorTypeEnumValues) +ThunkValuesEnum = GraphQLEnumType("ThunkValues", lambda: {"A": "a", "B": "b"}) + QueryType = GraphQLObjectType( "Query", { @@ -75,10 +77,20 @@ class Complex2: resolve=lambda _source, info, **args: # Note: this is one of the references of the internal values # which ComplexEnum allows. - complex2 if args.get("provideGoodValue") - # Note: similar object, but not the same *reference* as - # complex2 above. Enum internal values require object equality. - else Complex2() if args.get("provideBadValue") else args.get("fromEnum"), + ( + complex2 + if args.get("provideGoodValue") + # Note: similar object, but not the same *reference* as + # complex2 above. Enum internal values require object equality. + else Complex2() if args.get("provideBadValue") else args.get("fromEnum") + ), + ), + "thunkValuesString": GraphQLField( + GraphQLString, + args={ + "fromEnum": GraphQLArgument(ThunkValuesEnum), + }, + resolve=lambda _source, _info, fromEnum: fromEnum, ), }, ) @@ -286,26 +298,22 @@ def does_not_accept_internal_value_variable_as_enum_input(): ) def enum_value_may_have_an_internal_value_of_0(): - result = execute_query( - """ + result = execute_query(""" { colorEnum(fromEnum: RED) colorInt(fromEnum: RED) } - """ - ) + """) assert result == ({"colorEnum": "RED", "colorInt": 0}, None) def enum_inputs_may_be_nullable(): - result = execute_query( - """ + result = execute_query(""" { colorEnum colorInt } - """ - ) + """) assert result == ({"colorEnum": None, "colorInt": None}, None) @@ -319,16 +327,14 @@ def presents_a_values_property_for_complex_enums(): } def may_be_internally_represented_with_complex_values(): - result = execute_query( - """ + result = execute_query(""" { first: complexEnum second: complexEnum(fromEnum: TWO) good: complexEnum(provideGoodValue: true) bad: complexEnum(provideBadValue: true) } - """ - ) + """) assert result == ( {"first": "ONE", "second": "TWO", "good": "TWO", "bad": None}, @@ -342,5 +348,10 @@ def may_be_internally_represented_with_complex_values(): ], ) + def may_have_values_specified_via_a_callable(): + result = execute_query("{ thunkValuesString(fromEnum: B) }") + + assert result == ({"thunkValuesString": "b"}, None) + def can_be_introspected_without_error(): introspection_from_schema(schema) diff --git a/tests/type/test_introspection.py b/tests/type/test_introspection.py index f51d8f06..42739b34 100644 --- a/tests/type/test_introspection.py +++ b/tests/type/test_introspection.py @@ -4,8 +4,7 @@ def describe_introspection(): def executes_an_introspection_query(): - schema = build_schema( - """ + schema = build_schema(""" type SomeObject { someField: String } @@ -13,8 +12,7 @@ def executes_an_introspection_query(): schema { query: SomeObject } - """ - ) + """) source = get_introspection_query( descriptions=False, specified_by_url=True, directive_is_repeatable=True @@ -24,7 +22,7 @@ def executes_an_introspection_query(): assert result.errors is None assert result.data == { "__schema": { - "queryType": {"name": "SomeObject"}, + "queryType": {"name": "SomeObject", "kind": "OBJECT"}, "mutationType": None, "subscriptionType": None, "types": [ @@ -148,7 +146,21 @@ def executes_an_introspection_query(): }, { "name": "directives", - "args": [], + "args": [ + { + "name": "includeDeprecated", + "type": { + "kind": "NON_NULL", + "name": None, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": None, + }, + }, + "defaultValue": "false", + } + ], "type": { "kind": "NON_NULL", "name": None, @@ -364,6 +376,17 @@ def executes_an_introspection_query(): "isDeprecated": False, "deprecationReason": None, }, + { + "name": "isOneOf", + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": None, + }, + "isDeprecated": False, + "deprecationReason": None, + }, ], "inputFields": None, "interfaces": [], @@ -786,6 +809,32 @@ def executes_an_introspection_query(): "isDeprecated": False, "deprecationReason": None, }, + { + "name": "isDeprecated", + "args": [], + "type": { + "kind": "NON_NULL", + "name": None, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": None, + }, + }, + "isDeprecated": False, + "deprecationReason": None, + }, + { + "name": "deprecationReason", + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": None, + }, + "isDeprecated": False, + "deprecationReason": None, + }, ], "inputFields": None, "interfaces": [], @@ -895,6 +944,11 @@ def executes_an_introspection_query(): "isDeprecated": False, "deprecationReason": None, }, + { + "name": "DIRECTIVE_DEFINITION", + "isDeprecated": False, + "deprecationReason": None, + }, ], "possibleTypes": None, }, @@ -948,6 +1002,7 @@ def executes_an_introspection_query(): "ARGUMENT_DEFINITION", "INPUT_FIELD_DEFINITION", "ENUM_VALUE", + "DIRECTIVE_DEFINITION", ], "args": [ { @@ -981,13 +1036,18 @@ def executes_an_introspection_query(): } ], }, + { + "name": "oneOf", + "isRepeatable": False, + "locations": ["INPUT_OBJECT"], + "args": [], + }, ], } } def introspects_on_input_object(): - schema = build_schema( - """ + schema = build_schema(""" input SomeInputObject { a: String = "tes\\t de\\fault" b: [String] @@ -997,8 +1057,7 @@ def introspects_on_input_object(): type Query { someField(someArg: SomeInputObject): String } - """ - ) + """) source = """ { @@ -1075,8 +1134,7 @@ def introspects_on_input_object(): ) def introspects_any_default_value(): - schema = build_schema( - """ + schema = build_schema(""" input InputObjectWithDefaultValues { a: String = "Emoji: \\u{1F600}" b: Complex = {x: ["abc"], y: 123} @@ -1090,8 +1148,7 @@ def introspects_any_default_value(): type Query { someField(someArg: InputObjectWithDefaultValues): String } - """ - ) + """) source = """ { @@ -1117,13 +1174,11 @@ def introspects_any_default_value(): ) def supports_the_type_root_field(): - schema = build_schema( - """ + schema = build_schema(""" type Query { someField: String } - """ - ) + """) source = """ { @@ -1139,15 +1194,13 @@ def supports_the_type_root_field(): ) def identifies_deprecated_fields(): - schema = build_schema( - """ + schema = build_schema(""" type Query { nonDeprecated: String deprecated: String @deprecated(reason: "Removed in 1.0") deprecatedWithEmptyReason: String @deprecated(reason: "") } - """ - ) + """) source = """ { @@ -1187,14 +1240,12 @@ def identifies_deprecated_fields(): ) def respects_the_include_deprecated_parameter_for_fields(): - schema = build_schema( - """ + schema = build_schema(""" type Query { nonDeprecated: String deprecated: String @deprecated(reason: "Removed in 1.0") } - """ - ) + """) source = """ { @@ -1224,8 +1275,7 @@ def respects_the_include_deprecated_parameter_for_fields(): ) def identifies_deprecated_args(): - schema = build_schema( - """ + schema = build_schema(""" type Query { someField( nonDeprecated: String @@ -1233,8 +1283,7 @@ def identifies_deprecated_args(): deprecatedWithEmptyReason: String @deprecated(reason: "") ): String } - """ - ) + """) source = """ { @@ -1280,16 +1329,14 @@ def identifies_deprecated_args(): ) def respects_the_include_deprecated_parameter_for_args(): - schema = build_schema( - """ + schema = build_schema(""" type Query { someField( nonDeprecated: String deprecated: String @deprecated(reason: "Removed in 1.0") ): String } - """ - ) + """) source = """ { @@ -1328,8 +1375,7 @@ def respects_the_include_deprecated_parameter_for_args(): ) def identifies_deprecated_enum_values(): - schema = build_schema( - """ + schema = build_schema(""" enum SomeEnum { NON_DEPRECATED DEPRECATED @deprecated(reason: "Removed in 1.0") @@ -1339,8 +1385,7 @@ def identifies_deprecated_enum_values(): type Query { someField(someArg: SomeEnum): String } - """ - ) + """) source = """ { @@ -1380,8 +1425,7 @@ def identifies_deprecated_enum_values(): ) def respects_the_include_deprecated_parameter_for_enum_values(): - schema = build_schema( - """ + schema = build_schema(""" enum SomeEnum { NON_DEPRECATED DEPRECATED @deprecated(reason: "Removed in 1.0") @@ -1392,8 +1436,7 @@ def respects_the_include_deprecated_parameter_for_enum_values(): type Query { someField(someArg: SomeEnum): String } - """ - ) + """) source = """ { @@ -1433,15 +1476,112 @@ def respects_the_include_deprecated_parameter_for_enum_values(): None, ) - def fails_as_expected_on_the_type_root_field_without_an_arg(): - schema = build_schema( - """ + def identifies_one_of_for_input_objects(): + schema = build_schema(""" + input SomeInputObject @oneOf { + a: String + } + + input AnotherInputObject { + a: String + b: String + } + type Query { - someField: String + someField(someArg: SomeInputObject): String + anotherField(anotherArg: AnotherInputObject): String + } + """) + + source = """ + { + oneOfInputObject: __type(name: "SomeInputObject") { + isOneOf + } + inputObject: __type(name: "AnotherInputObject") { + isOneOf + } } """ + + assert graphql_sync(schema=schema, source=source) == ( + { + "oneOfInputObject": { + "isOneOf": True, + }, + "inputObject": { + "isOneOf": False, + }, + }, + None, ) + def returns_null_for_one_of_for_other_types(): + schema = build_schema(""" + type SomeObject implements SomeInterface { + fieldA: String + } + enum SomeEnum { + SomeObject + } + interface SomeInterface { + fieldA: String + } + union SomeUnion = SomeObject + type Query { + someField(enum: SomeEnum): SomeUnion + anotherField(enum: SomeEnum): SomeInterface + } + """) + + source = """ + { + object: __type(name: "SomeObject") { + isOneOf + } + enum: __type(name: "SomeEnum") { + isOneOf + } + interface: __type(name: "SomeInterface") { + isOneOf + } + scalar: __type(name: "String") { + isOneOf + } + union: __type(name: "SomeUnion") { + isOneOf + } + } + """ + + assert graphql_sync(schema=schema, source=source) == ( + { + "object": { + "isOneOf": None, + }, + "enum": { + "isOneOf": None, + }, + "interface": { + "isOneOf": None, + }, + "scalar": { + "isOneOf": None, + }, + "union": { + "isOneOf": None, + }, + }, + None, + ) + + def fails_as_expected_on_the_type_root_field_without_an_arg(): + schema = build_schema(""" + type Query { + someField: String + } + """) + source = """ { __type { @@ -1461,8 +1601,7 @@ def fails_as_expected_on_the_type_root_field_without_an_arg(): ) def exposes_descriptions(): - schema = build_schema( - ''' + schema = build_schema(''' """Enum description""" enum SomeEnum { """Value description""" @@ -1479,8 +1618,7 @@ def exposes_descriptions(): schema { query: SomeObject } - ''' - ) + ''') source = """ { @@ -1530,13 +1668,11 @@ def exposes_descriptions(): ) def executes_introspection_query_without_calling_global_resolvers(): - schema = build_schema( - """ + schema = build_schema(""" type Query { someField: String } - """ - ) + """) source = get_introspection_query( specified_by_url=True, directive_is_repeatable=True, schema_description=True @@ -1556,3 +1692,139 @@ def type_resolver(_obj, info, _abstract_type): ) assert result.errors is None + + def identifies_deprecated_directives(): + schema = build_schema( + """ +type Query { + someField: String +} +directive @isNotDeprecated on FIELD_DEFINITION +directive @isDeprecated @deprecated(reason: "No longer supported") on FIELD_DEFINITION +directive @isDeprecatedWithEmptyReason @deprecated(reason: "") on FIELD_DEFINITION +""", + experimental_directives_on_directive_definitions=True, + ) + + source = """ + { + __schema { + directives(includeDeprecated: true) { + name + isDeprecated + deprecationReason + } + } + } + """ + + assert graphql_sync(schema=schema, source=source) == ( + { + "__schema": { + "directives": [ + { + "name": "isNotDeprecated", + "isDeprecated": False, + "deprecationReason": None, + }, + { + "name": "isDeprecated", + "isDeprecated": True, + "deprecationReason": "No longer supported", + }, + { + "name": "isDeprecatedWithEmptyReason", + "isDeprecated": True, + "deprecationReason": "", + }, + { + "name": "include", + "isDeprecated": False, + "deprecationReason": None, + }, + { + "name": "skip", + "isDeprecated": False, + "deprecationReason": None, + }, + { + "name": "deprecated", + "isDeprecated": False, + "deprecationReason": None, + }, + { + "name": "specifiedBy", + "isDeprecated": False, + "deprecationReason": None, + }, + { + "name": "oneOf", + "isDeprecated": False, + "deprecationReason": None, + }, + ], + }, + }, + None, + ) + + def respects_the_include_deprecated_parameter_for_directives(): + schema = build_schema( + """ +type Query { + someField: String +} +directive @isNotDeprecated on FIELD_DEFINITION +directive @isDeprecated @deprecated(reason: "No longer supported") on FIELD_DEFINITION +""", + experimental_directives_on_directive_definitions=True, + ) + + source = """ + { + __schema { + trueDirectives: directives(includeDeprecated: true) { + name + } + falseDirectives: directives(includeDeprecated: false) { + name + } + omittedDirectives: directives { + name + } + } + } + """ + + assert graphql_sync(schema=schema, source=source) == ( + { + "__schema": { + "trueDirectives": [ + {"name": "isNotDeprecated"}, + {"name": "isDeprecated"}, + {"name": "include"}, + {"name": "skip"}, + {"name": "deprecated"}, + {"name": "specifiedBy"}, + {"name": "oneOf"}, + ], + "falseDirectives": [ + {"name": "isNotDeprecated"}, + {"name": "include"}, + {"name": "skip"}, + {"name": "deprecated"}, + {"name": "specifiedBy"}, + {"name": "oneOf"}, + ], + "omittedDirectives": [ + {"name": "isNotDeprecated"}, + {"name": "include"}, + {"name": "skip"}, + {"name": "deprecated"}, + {"name": "specifiedBy"}, + {"name": "oneOf"}, + ], + }, + }, + None, + ) diff --git a/tests/type/test_scalars.py b/tests/type/test_scalars.py index e5dd7c6f..37a51423 100644 --- a/tests/type/test_scalars.py +++ b/tests/type/test_scalars.py @@ -1,3 +1,4 @@ +import pickle from math import inf, nan, pi from typing import Any @@ -12,6 +13,7 @@ GraphQLString, GraphQLBoolean, GraphQLID, + GraphQLScalarType, ) @@ -172,6 +174,13 @@ def serializes(): serialize([5]) assert str(exc_info.value) == "Int cannot represent non-integer value: [5]" + def cannot_be_redefined(): + with raises(TypeError, match="Redefinition of reserved type 'Int'"): + GraphQLScalarType(name="Int") + + def pickles(): + assert pickle.loads(pickle.dumps(GraphQLInt)) is GraphQLInt + def describe_graphql_float(): def parse_value(): _parse_value = GraphQLFloat.parse_value @@ -294,6 +303,13 @@ def serializes(): str(exc_info.value) == "Float cannot represent non numeric value: [5]" ) + def cannot_be_redefined(): + with raises(TypeError, match="Redefinition of reserved type 'Float'"): + GraphQLScalarType(name="Float") + + def pickles(): + assert pickle.loads(pickle.dumps(GraphQLFloat)) is GraphQLFloat + def describe_graphql_string(): def parse_value(): _parse_value = GraphQLString.parse_value @@ -400,6 +416,13 @@ def __str__(self): " {'value_of': 'value_of string'}" ) + def cannot_be_redefined(): + with raises(TypeError, match="Redefinition of reserved type 'String'"): + GraphQLScalarType(name="String") + + def pickles(): + assert pickle.loads(pickle.dumps(GraphQLString)) is GraphQLString + def describe_graphql_boolean(): def parse_value(): _parse_value = GraphQLBoolean.parse_value @@ -542,6 +565,13 @@ def serializes(): "Boolean cannot represent a non boolean value: {}" ) + def cannot_be_redefined(): + with raises(TypeError, match="Redefinition of reserved type 'Boolean'"): + GraphQLScalarType(name="Boolean") + + def pickles(): + assert pickle.loads(pickle.dumps(GraphQLBoolean)) is GraphQLBoolean + def describe_graphql_id(): def parse_value(): _parse_value = GraphQLID.parse_value @@ -662,3 +692,10 @@ def __str__(self): with raises(GraphQLError) as exc_info: serialize(["abc"]) assert str(exc_info.value) == "ID cannot represent value: ['abc']" + + def cannot_be_redefined(): + with raises(TypeError, match="Redefinition of reserved type 'ID'"): + GraphQLScalarType(name="ID") + + def pickles(): + assert pickle.loads(pickle.dumps(GraphQLID)) is GraphQLID diff --git a/tests/type/test_schema.py b/tests/type/test_schema.py index ef483b54..aa9e96b9 100644 --- a/tests/type/test_schema.py +++ b/tests/type/test_schema.py @@ -20,6 +20,7 @@ GraphQLInt, GraphQLInterfaceType, GraphQLList, + GraphQLNamedType, GraphQLObjectType, GraphQLScalarType, GraphQLSchema, @@ -117,8 +118,7 @@ def define_sample_schema(): "assume_valid": False, } - assert print_schema(schema) == dedent( - ''' + assert print_schema(schema) == dedent(''' """Sample schema""" schema { query: Query @@ -159,8 +159,7 @@ def define_sample_schema(): type Subscription { articleSubscribe(id: String): Article } - ''' - ) + ''') def freezes_the_specified_directives(): directives_list = [GraphQLDirective("SomeDirective", [])] @@ -331,7 +330,15 @@ def check_that_query_mutation_and_subscription_are_graphql_types(): def describe_a_schema_must_contain_uniquely_named_types(): def rejects_a_schema_which_redefines_a_built_in_type(): - FakeString = GraphQLScalarType("String") + # temporarily allow redefinition of the String scalar type + reserved_types = GraphQLNamedType.reserved_types + GraphQLScalarType.reserved_types = {} + try: + # create a redefined String scalar type + FakeString = GraphQLScalarType("String") + finally: + # protect from redefinition again + GraphQLScalarType.reserved_types = reserved_types QueryType = GraphQLObjectType( "Query", diff --git a/tests/type/test_validation.py b/tests/type/test_validation.py index 875d8902..41cb2afb 100644 --- a/tests/type/test_validation.py +++ b/tests/type/test_validation.py @@ -39,8 +39,7 @@ from ..utils import dedent -SomeSchema = build_schema( - """ +SomeSchema = build_schema(""" scalar SomeScalar interface SomeInterface { f: SomeObject } @@ -54,8 +53,7 @@ input SomeInputObject { val: String = "hello" } directive @SomeDirective on QUERY - """ -) + """) get_type = SomeSchema.get_type SomeScalarType = assert_scalar_type(get_type("SomeScalar")) @@ -120,17 +118,14 @@ def schema_with_field_type(type_): def describe_type_system_a_schema_must_have_object_root_types(): def accepts_a_schema_whose_query_type_is_an_object_type(): - schema = build_schema( - """ + schema = build_schema(""" type Query { test: String } - """ - ) + """) assert validate_schema(schema) == [] - schema_with_def = build_schema( - """ + schema_with_def = build_schema(""" schema { query: QueryRoot } @@ -138,14 +133,12 @@ def accepts_a_schema_whose_query_type_is_an_object_type(): type QueryRoot { test: String } - """ - ) + """) assert validate_schema(schema_with_def) == [] def accepts_a_schema_whose_query_and_mutation_types_are_object_types(): - schema = build_schema( - """ + schema = build_schema(""" type Query { test: String } @@ -153,12 +146,10 @@ def accepts_a_schema_whose_query_and_mutation_types_are_object_types(): type Mutation { test: String } - """ - ) + """) assert validate_schema(schema) == [] - schema_with_def = build_schema( - """ + schema_with_def = build_schema(""" schema { query: QueryRoot mutation: MutationRoot @@ -171,13 +162,11 @@ def accepts_a_schema_whose_query_and_mutation_types_are_object_types(): type MutationRoot { test: String } - """ - ) + """) assert validate_schema(schema_with_def) == [] def accepts_a_schema_whose_query_and_subscription_types_are_object_types(): - schema = build_schema( - """ + schema = build_schema(""" type Query { test: String } @@ -185,12 +174,10 @@ def accepts_a_schema_whose_query_and_subscription_types_are_object_types(): type Subscription { test: String } - """ - ) + """) assert validate_schema(schema) == [] - schema_with_def = build_schema( - """ + schema_with_def = build_schema(""" schema { query: QueryRoot subscription: SubscriptionRoot @@ -203,24 +190,20 @@ def accepts_a_schema_whose_query_and_subscription_types_are_object_types(): type SubscriptionRoot { test: String } - """ - ) + """) assert validate_schema(schema_with_def) == [] def rejects_a_schema_without_a_query_type(): - schema = build_schema( - """ + schema = build_schema(""" type Mutation { test: String } - """ - ) + """) assert validate_schema(schema) == [ {"message": "Query root type must be provided.", "locations": None} ] - schema_with_def = build_schema( - """ + schema_with_def = build_schema(""" schema { mutation: MutationRoot } @@ -228,20 +211,17 @@ def rejects_a_schema_without_a_query_type(): type MutationRoot { test: String } - """ - ) + """) assert validate_schema(schema_with_def) == [ {"message": "Query root type must be provided.", "locations": [(2, 13)]} ] def rejects_a_schema_whose_query_root_type_is_not_an_object_type(): - schema = build_schema( - """ + schema = build_schema(""" input Query { test: String } - """ - ) + """) assert validate_schema(schema) == [ { "message": "Query root type must be Object type," @@ -250,8 +230,7 @@ def rejects_a_schema_whose_query_root_type_is_not_an_object_type(): } ] - schema_with_def = build_schema( - """ + schema_with_def = build_schema(""" schema { query: SomeInputObject } @@ -259,8 +238,7 @@ def rejects_a_schema_whose_query_root_type_is_not_an_object_type(): input SomeInputObject { test: String } - """ - ) + """) assert validate_schema(schema_with_def) == [ { "message": "Query root type must be Object type," @@ -270,8 +248,7 @@ def rejects_a_schema_whose_query_root_type_is_not_an_object_type(): ] def rejects_a_schema_whose_mutation_type_is_an_input_type(): - schema = build_schema( - """ + schema = build_schema(""" type Query { field: String } @@ -279,8 +256,7 @@ def rejects_a_schema_whose_mutation_type_is_an_input_type(): input Mutation { test: String } - """ - ) + """) assert validate_schema(schema) == [ { "message": "Mutation root type must be Object type if provided," @@ -289,8 +265,7 @@ def rejects_a_schema_whose_mutation_type_is_an_input_type(): } ] - schema_with_def = build_schema( - """ + schema_with_def = build_schema(""" schema { query: Query mutation: SomeInputObject @@ -303,8 +278,7 @@ def rejects_a_schema_whose_mutation_type_is_an_input_type(): input SomeInputObject { test: String } - """ - ) + """) assert validate_schema(schema_with_def) == [ { "message": "Mutation root type must be Object type if provided," @@ -314,8 +288,7 @@ def rejects_a_schema_whose_mutation_type_is_an_input_type(): ] def rejects_a_schema_whose_subscription_type_is_an_input_type(): - schema = build_schema( - """ + schema = build_schema(""" type Query { field: String } @@ -323,8 +296,7 @@ def rejects_a_schema_whose_subscription_type_is_an_input_type(): input Subscription { test: String } - """ - ) + """) assert validate_schema(schema) == [ { "message": "Subscription root type must be Object type if" @@ -333,8 +305,7 @@ def rejects_a_schema_whose_subscription_type_is_an_input_type(): } ] - schema_with_def = build_schema( - """ + schema_with_def = build_schema(""" schema { query: Query subscription: SomeInputObject @@ -347,8 +318,7 @@ def rejects_a_schema_whose_subscription_type_is_an_input_type(): input SomeInputObject { test: String } - """ - ) + """) assert validate_schema(schema_with_def) == [ { "message": "Subscription root type must be Object type if" @@ -358,42 +328,34 @@ def rejects_a_schema_whose_subscription_type_is_an_input_type(): ] def rejects_a_schema_extended_with_invalid_root_types(): - schema = build_schema( - """ + schema = build_schema(""" input SomeInputObject { test: String } - """ - ) + """) schema = extend_schema( schema, - parse( - """ + parse(""" extend schema { query: SomeInputObject } - """ - ), + """), ) schema = extend_schema( schema, - parse( - """ + parse(""" extend schema { mutation: SomeInputObject } - """ - ), + """), ) schema = extend_schema( schema, - parse( - """ + parse(""" extend schema { subscription: SomeInputObject } - """ - ), + """), ) assert validate_schema(schema) == [ { @@ -446,11 +408,25 @@ def rejects_a_schema_whose_directives_are_incorrectly_typed(): {"message": "Expected directive but got: SomeScalar."}, ] + def rejects_a_schema_whose_directives_have_empty_locations(): + bad_directive = GraphQLDirective( + name="BadDirective1", + locations=[], + ) + schema = GraphQLSchema( + SomeObjectType, + directives=[bad_directive], + ) + assert validate_schema(schema) == [ + { + "message": "Directive @BadDirective1 must include 1 or more locations.", + }, + ] + def describe_type_system_objects_must_have_fields(): def accepts_an_object_type_with_fields_object(): - schema = build_schema( - """ + schema = build_schema(""" type Query { field: SomeObject } @@ -458,20 +434,17 @@ def accepts_an_object_type_with_fields_object(): type SomeObject { field: String } - """ - ) + """) assert validate_schema(schema) == [] def rejects_an_object_type_with_missing_fields(): - schema = build_schema( - """ + schema = build_schema(""" type Query { test: IncompleteObject } type IncompleteObject - """ - ) + """) assert validate_schema(schema) == [ { "message": "Type IncompleteObject must define one or more fields.", @@ -538,8 +511,7 @@ def rejects_field_args_with_invalid_names(): def describe_type_system_union_types_must_be_valid(): def accepts_a_union_type_with_member_types(): - schema = build_schema( - """ + schema = build_schema(""" type Query { test: GoodUnion } @@ -555,30 +527,25 @@ def accepts_a_union_type_with_member_types(): union GoodUnion = | TypeA | TypeB - """ - ) + """) assert validate_schema(schema) == [] def rejects_a_union_type_with_empty_types(): - schema = build_schema( - """ + schema = build_schema(""" type Query { test: BadUnion } union BadUnion - """ - ) + """) schema = extend_schema( schema, - parse( - """ + parse(""" directive @test on UNION extend union BadUnion @test - """ - ), + """), ) assert validate_schema(schema) == [ @@ -589,8 +556,7 @@ def rejects_a_union_type_with_empty_types(): ] def rejects_a_union_type_with_duplicated_member_type(): - schema = build_schema( - """ + schema = build_schema(""" type Query { test: BadUnion } @@ -607,8 +573,7 @@ def rejects_a_union_type_with_duplicated_member_type(): | TypeA | TypeB | TypeA - """ - ) + """) assert validate_schema(schema) == [ { @@ -633,8 +598,7 @@ def rejects_a_union_type_with_duplicated_member_type(): def rejects_a_union_type_with_non_object_member_types(): # invalid schema cannot be built with Python with raises(TypeError) as exc_info: - build_schema( - """ + build_schema(""" type Query { test: BadUnion } @@ -651,15 +615,13 @@ def rejects_a_union_type_with_non_object_member_types(): | TypeA | String | TypeB - """ - ) + """) assert str(exc_info.value) == ( "BadUnion types must be specified" " as a collection of GraphQLObjectType instances." ) # construct invalid schema manually - schema = build_schema( - """ + schema = build_schema(""" type Query { test: BadUnion } @@ -676,8 +638,7 @@ def rejects_a_union_type_with_non_object_member_types(): | TypeA | TypeA | TypeB - """ - ) + """) with raises(TypeError) as exc_info: extend_schema(schema, parse("extend union BadUnion = Int")) assert str(exc_info.value) == ( @@ -744,8 +705,7 @@ def rejects_a_union_type_with_non_object_member_types(): def describe_type_system_input_objects_must_have_fields(): def accepts_an_input_object_type_with_fields(): - schema = build_schema( - """ + schema = build_schema(""" type Query { field(arg: SomeInputObject): String } @@ -753,29 +713,24 @@ def accepts_an_input_object_type_with_fields(): input SomeInputObject { field: String } - """ - ) + """) assert validate_schema(schema) == [] def rejects_an_input_object_type_with_missing_fields(): - schema = build_schema( - """ + schema = build_schema(""" type Query { field(arg: SomeInputObject): String } input SomeInputObject - """ - ) + """) schema = extend_schema( schema, - parse( - """ + parse(""" directive @test on INPUT_OBJECT extend input SomeInputObject @test - """ - ), + """), ) assert validate_schema(schema) == [ { @@ -786,8 +741,7 @@ def rejects_an_input_object_type_with_missing_fields(): ] def accepts_an_input_object_with_breakable_circular_reference(): - schema = build_schema( - """ + schema = build_schema(""" type Query { field(arg: SomeInputObject): String } @@ -803,13 +757,11 @@ def accepts_an_input_object_with_breakable_circular_reference(): input AnotherInputObject { parent: SomeInputObject } - """ - ) + """) assert validate_schema(schema) == [] def rejects_an_input_object_with_non_breakable_circular_reference(): - schema = build_schema( - """ + schema = build_schema(""" type Query { field(arg: SomeInputObject): String } @@ -825,8 +777,7 @@ def rejects_an_input_object_with_non_breakable_circular_reference(): input YetAnotherInputObject { closeLoop: SomeInputObject! } - """ - ) + """) assert validate_schema(schema) == [ { "message": "Cannot reference Input Object 'SomeInputObject'" @@ -837,8 +788,7 @@ def rejects_an_input_object_with_non_breakable_circular_reference(): ] def rejects_an_input_object_with_multiple_non_breakable_circular_reference(): - schema = build_schema( - """ + schema = build_schema(""" type Query { field(arg: SomeInputObject): String } @@ -856,8 +806,7 @@ def rejects_an_input_object_with_multiple_non_breakable_circular_reference(): closeSecondLoop: AnotherInputObject! nonNullSelf: YetAnotherInputObject! } - """ - ) + """) assert validate_schema(schema) == [ { "message": "Cannot reference Input Object 'SomeInputObject'" @@ -882,8 +831,7 @@ def rejects_an_input_object_with_multiple_non_breakable_circular_reference(): def rejects_an_input_object_type_with_incorrectly_typed_fields(): # invalid schema cannot be built with Python with raises(TypeError) as exc_info: - build_schema( - """ + build_schema(""" type Query { field(arg: SomeInputObject): String } @@ -899,15 +847,13 @@ def rejects_an_input_object_type_with_incorrectly_typed_fields(): badUnion: SomeUnion goodInputObject: SomeInputObject } - """ - ) + """) assert str(exc_info.value) == ( "SomeInputObject fields cannot be resolved." " Input field type must be a GraphQL input type." ) # construct invalid schema manually - schema = build_schema( - """ + schema = build_schema(""" type Query { field(arg: SomeInputObject): String } @@ -923,8 +869,7 @@ def rejects_an_input_object_type_with_incorrectly_typed_fields(): badUnion: SomeInputObject goodInputObject: SomeInputObject } - """ - ) + """) some_input_obj: Any = schema.get_type("SomeInputObject") some_input_obj.fields["badObject"].type = schema.get_type("SomeObject") some_input_obj.fields["badUnion"].type = schema.get_type("SomeUnion") @@ -942,8 +887,7 @@ def rejects_an_input_object_type_with_incorrectly_typed_fields(): ] def rejects_an_input_object_type_with_required_arguments_that_is_deprecated(): - schema = build_schema( - """ + schema = build_schema(""" type Query { field(arg: SomeInputObject): String } @@ -953,8 +897,7 @@ def rejects_an_input_object_type_with_required_arguments_that_is_deprecated(): optionalField: String @deprecated anotherOptionalField: String! = "" @deprecated } - """ - ) + """) assert validate_schema(schema) == [ { "message": "Required input field SomeInputObject.badField" @@ -966,25 +909,21 @@ def rejects_an_input_object_type_with_required_arguments_that_is_deprecated(): def describe_type_system_enum_types_must_be_well_defined(): def rejects_an_enum_type_without_values(): - schema = build_schema( - """ + schema = build_schema(""" type Query { field: SomeEnum } enum SomeEnum - """ - ) + """) schema = extend_schema( schema, - parse( - """ + parse(""" directive @test on ENUM extend enum SomeEnum @test - """ - ), + """), ) assert validate_schema(schema) == [ @@ -1064,8 +1003,7 @@ def rejects_a_non_type_value_as_an_object_field_type(type_): def rejects_with_relevant_locations_for_a_non_output_type(): # invalid schema cannot be built with Python with raises(TypeError) as exc_info: - build_schema( - """ + build_schema(""" type Query { field: [SomeInputObject] } @@ -1073,14 +1011,12 @@ def rejects_with_relevant_locations_for_a_non_output_type(): input SomeInputObject { field: String } - """ - ) + """) assert str(exc_info.value) == ( "Query fields cannot be resolved. Field type must be an output type." ) # therefore we need to monkey-patch a valid schema - schema = build_schema( - """ + schema = build_schema(""" type Query { field: [String] } @@ -1088,8 +1024,7 @@ def rejects_with_relevant_locations_for_a_non_output_type(): input SomeInputObject { field: String } - """ - ) + """) some_input_obj = schema.get_type("SomeInputObject") schema.query_type.fields["field"].type.of_type = some_input_obj # type: ignore assert validate_schema(schema) == [ @@ -1121,8 +1056,7 @@ def rejects_an_object_implementing_a_non_type_value(): def rejects_an_object_implementing_a_non_interface_type(): # invalid schema cannot be built with Python with raises(TypeError) as exc_info: - build_schema( - """ + build_schema(""" type Query { test: BadObject } @@ -1134,16 +1068,14 @@ def rejects_an_object_implementing_a_non_interface_type(): type BadObject implements SomeInputObject { field: String } - """ - ) + """) assert str(exc_info.value) == ( "BadObject interfaces must be specified" " as a collection of GraphQLInterfaceType instances." ) def rejects_an_object_implementing_the_same_interface_twice(): - schema = build_schema( - """ + schema = build_schema(""" type Query { test: AnotherObject } @@ -1155,8 +1087,7 @@ def rejects_an_object_implementing_the_same_interface_twice(): type AnotherObject implements AnotherInterface & AnotherInterface { field: String } - """ - ) + """) assert validate_schema(schema) == [ { "message": "Type AnotherObject can only implement" @@ -1166,8 +1097,7 @@ def rejects_an_object_implementing_the_same_interface_twice(): ] def rejects_an_object_implementing_same_interface_twice_due_to_extension(): - schema = build_schema( - """ + schema = build_schema(""" type Query { test: AnotherObject } @@ -1179,8 +1109,7 @@ def rejects_an_object_implementing_same_interface_twice_due_to_extension(): type AnotherObject implements AnotherInterface { field: String } - """ - ) + """) extended_schema = extend_schema( schema, parse("extend type AnotherObject implements AnotherInterface") ) @@ -1195,8 +1124,7 @@ def rejects_an_object_implementing_same_interface_twice_due_to_extension(): def describe_type_system_interface_extensions_should_be_valid(): def rejects_object_implementing_extended_interface_due_to_missing_field(): - schema = build_schema( - """ + schema = build_schema(""" type Query { test: AnotherObject } @@ -1208,12 +1136,10 @@ def rejects_object_implementing_extended_interface_due_to_missing_field(): type AnotherObject implements AnotherInterface { field: String } - """ - ) + """) extended_schema = extend_schema( schema, - parse( - """ + parse(""" extend interface AnotherInterface { newField: String } @@ -1221,8 +1147,7 @@ def rejects_object_implementing_extended_interface_due_to_missing_field(): extend type AnotherObject { differentNewField: String } - """ - ), + """), ) assert validate_schema(extended_schema) == [ { @@ -1233,8 +1158,7 @@ def rejects_object_implementing_extended_interface_due_to_missing_field(): ] def rejects_object_implementing_extended_interface_due_to_missing_args(): - schema = build_schema( - """ + schema = build_schema(""" type Query { test: AnotherObject } @@ -1246,12 +1170,10 @@ def rejects_object_implementing_extended_interface_due_to_missing_args(): type AnotherObject implements AnotherInterface { field: String } - """ - ) + """) extended_schema = extend_schema( schema, - parse( - """ + parse(""" extend interface AnotherInterface { newField(test: Boolean): String } @@ -1259,8 +1181,7 @@ def rejects_object_implementing_extended_interface_due_to_missing_args(): extend type AnotherObject { newField: String } - """ - ), + """), ) assert validate_schema(extended_schema) == [ { @@ -1272,8 +1193,7 @@ def rejects_object_implementing_extended_interface_due_to_missing_args(): ] def rejects_object_implementing_extended_interface_due_to_type_mismatch(): - schema = build_schema( - """ + schema = build_schema(""" type Query { test: AnotherObject } @@ -1285,12 +1205,10 @@ def rejects_object_implementing_extended_interface_due_to_type_mismatch(): type AnotherObject implements AnotherInterface { field: String } - """ - ) + """) extended_schema = extend_schema( schema, - parse( - """ + parse(""" extend interface AnotherInterface { newInterfaceField: NewInterface } @@ -1311,8 +1229,7 @@ def rejects_object_implementing_extended_interface_due_to_type_mismatch(): type DummyObject implements NewInterface & MismatchingInterface { newField: String } - """ - ), + """), ) assert validate_schema(extended_schema) == [ { @@ -1401,8 +1318,7 @@ def rejects_a_non_type_value_as_an_interface_field_type(type_): def rejects_a_non_output_type_as_an_interface_field_with_locations(): # invalid schema cannot be built with Python with raises(TypeError) as exc_info: - build_schema( - """ + build_schema(""" type Query { test: SomeInterface } @@ -1418,15 +1334,13 @@ def rejects_a_non_output_type_as_an_interface_field_with_locations(): type SomeObject implements SomeInterface { field: SomeInputObject } - """ - ) + """) assert str(exc_info.value) == ( "SomeInterface fields cannot be resolved." " Field type must be an output type." ) # therefore we need to monkey-patch a valid schema - schema = build_schema( - """ + schema = build_schema(""" type Query { test: SomeInterface } @@ -1442,8 +1356,7 @@ def rejects_a_non_output_type_as_an_interface_field_with_locations(): type SomeObject implements SomeInterface { field: String } - """ - ) + """) # therefore we need to monkey-patch a valid schema some_input_obj = schema.get_type("SomeInputObject") some_interface: Any = schema.get_type("SomeInterface") @@ -1464,8 +1377,7 @@ def rejects_a_non_output_type_as_an_interface_field_with_locations(): ] def accepts_an_interface_not_implemented_by_at_least_one_object(): - schema = build_schema( - """ + schema = build_schema(""" type Query { test: SomeInterface } @@ -1473,8 +1385,7 @@ def accepts_an_interface_not_implemented_by_at_least_one_object(): interface SomeInterface { foo: String } - """ - ) + """) assert validate_schema(schema) == [] @@ -1555,8 +1466,7 @@ def rejects_a_non_type_value_as_a_field_arg_type(type_): ] def rejects_a_required_argument_that_is_deprecated(): - schema = build_schema( - """ + schema = build_schema(""" directive @BadDirective( badArg: String! @deprecated optionalArg: String @deprecated @@ -1570,8 +1480,7 @@ def rejects_a_required_argument_that_is_deprecated(): anotherOptionalArg: String! = "" @deprecated ): String } - """ - ) + """) assert validate_schema(schema) == [ { "message": "Required argument @BadDirective(badArg:)" @@ -1588,8 +1497,7 @@ def rejects_a_required_argument_that_is_deprecated(): def rejects_a_non_input_type_as_a_field_arg_with_locations(): # invalid schema cannot be built with Python with raises(TypeError) as exc_info: - build_schema( - """ + build_schema(""" type Query { test(arg: SomeObject): String } @@ -1597,15 +1505,13 @@ def rejects_a_non_input_type_as_a_field_arg_with_locations(): type SomeObject { foo: String } - """ - ) + """) assert str(exc_info.value) == ( "Query fields cannot be resolved." " Argument type must be a GraphQL input type." ) # therefore we need to monkey-patch a valid schema - schema = build_schema( - """ + schema = build_schema(""" type Query { test(arg: String): String } @@ -1613,8 +1519,7 @@ def rejects_a_non_input_type_as_a_field_arg_with_locations(): type SomeObject { foo: String } - """ - ) + """) some_object = schema.get_type("SomeObject") schema.query_type.fields["test"].args["arg"].type = some_object # type: ignore assert validate_schema(schema) == [ @@ -1694,8 +1599,7 @@ def rejects_a_non_type_value_as_an_input_field_type(type_): def rejects_with_relevant_locations_for_a_non_input_type(): # invalid schema cannot be built with Python with raises(TypeError) as exc_info: - build_schema( - """ + build_schema(""" type Query { test(arg: SomeInputObject): String } @@ -1707,15 +1611,13 @@ def rejects_with_relevant_locations_for_a_non_input_type(): type SomeObject { bar: String } - """ - ) + """) assert str(exc_info.value) == ( "SomeInputObject fields cannot be resolved." " Input field type must be a GraphQL input type." ) # therefore we need to monkey-patch a valid schema - schema = build_schema( - """ + schema = build_schema(""" type Query { test(arg: SomeInputObject): String } @@ -1727,8 +1629,7 @@ def rejects_with_relevant_locations_for_a_non_input_type(): type SomeObject { bar: String } - """ - ) + """) some_object = schema.get_type("SomeObject") some_input_object: Any = schema.get_type("SomeInputObject") some_input_object.fields["foo"].type = some_object @@ -1741,10 +1642,48 @@ def rejects_with_relevant_locations_for_a_non_input_type(): ] +def describe_type_system_one_of_input_object_fields_must_be_nullable(): + def rejects_non_nullable_fields(): + schema = build_schema(""" + type Query { + test(arg: SomeInputObject): String + } + + input SomeInputObject @oneOf { + a: String + b: String! + } + """) + assert validate_schema(schema) == [ + { + "message": "OneOf input field SomeInputObject.b must be nullable.", + "locations": [(8, 18)], + } + ] + + def rejects_fields_with_default_values(): + schema = build_schema(""" + type Query { + test(arg: SomeInputObject): String + } + + input SomeInputObject @oneOf { + a: String + b: String = "foo" + } + """) + assert validate_schema(schema) == [ + { + "message": "OneOf input field SomeInputObject.b" + " cannot have a default value.", + "locations": [(8, 15)], + } + ] + + def describe_objects_must_adhere_to_interfaces_they_implement(): def accepts_an_object_which_implements_an_interface(): - schema = build_schema( - """ + schema = build_schema(""" type Query { test: AnotherObject } @@ -1756,13 +1695,11 @@ def accepts_an_object_which_implements_an_interface(): type AnotherObject implements AnotherInterface { field(input: String): String } - """ - ) + """) assert validate_schema(schema) == [] def accepts_an_object_which_implements_an_interface_and_with_more_fields(): - schema = build_schema( - """ + schema = build_schema(""" type Query { test: AnotherObject } @@ -1775,13 +1712,11 @@ def accepts_an_object_which_implements_an_interface_and_with_more_fields(): field(input: String): String anotherField: String } - """ - ) + """) assert validate_schema(schema) == [] def accepts_an_object_which_implements_an_interface_field_with_more_args(): - schema = build_schema( - """ + schema = build_schema(""" type Query { test: AnotherObject } @@ -1793,13 +1728,11 @@ def accepts_an_object_which_implements_an_interface_field_with_more_args(): type AnotherObject implements AnotherInterface { field(input: String, anotherInput: String): String } - """ - ) + """) assert validate_schema(schema) == [] def rejects_an_object_missing_an_interface_field(): - schema = build_schema( - """ + schema = build_schema(""" type Query { test: AnotherObject } @@ -1811,8 +1744,7 @@ def rejects_an_object_missing_an_interface_field(): type AnotherObject implements AnotherInterface { anotherField: String } - """ - ) + """) assert validate_schema(schema) == [ { "message": "Interface field AnotherInterface.field expected but" @@ -1822,8 +1754,7 @@ def rejects_an_object_missing_an_interface_field(): ] def rejects_an_object_with_an_incorrectly_typed_interface_field(): - schema = build_schema( - """ + schema = build_schema(""" type Query { test: AnotherObject } @@ -1835,8 +1766,7 @@ def rejects_an_object_with_an_incorrectly_typed_interface_field(): type AnotherObject implements AnotherInterface { field(input: String): Int } - """ - ) + """) assert validate_schema(schema) == [ { "message": "Interface field AnotherInterface.field" @@ -1847,8 +1777,7 @@ def rejects_an_object_with_an_incorrectly_typed_interface_field(): ] def rejects_an_object_with_a_differently_typed_interface_field(): - schema = build_schema( - """ + schema = build_schema(""" type Query { test: AnotherObject } @@ -1863,8 +1792,7 @@ def rejects_an_object_with_a_differently_typed_interface_field(): type AnotherObject implements AnotherInterface { field: B } - """ - ) + """) assert validate_schema(schema) == [ { "message": "Interface field AnotherInterface.field" @@ -1874,8 +1802,7 @@ def rejects_an_object_with_a_differently_typed_interface_field(): ] def accepts_an_object_with_a_subtyped_interface_field_interface(): - schema = build_schema( - """ + schema = build_schema(""" type Query { test: AnotherObject } @@ -1887,13 +1814,11 @@ def accepts_an_object_with_a_subtyped_interface_field_interface(): type AnotherObject implements AnotherInterface { field: AnotherObject } - """ - ) + """) assert validate_schema(schema) == [] def accepts_an_object_with_a_subtyped_interface_field_union(): - schema = build_schema( - """ + schema = build_schema(""" type Query { test: AnotherObject } @@ -1911,13 +1836,11 @@ def accepts_an_object_with_a_subtyped_interface_field_union(): type AnotherObject implements AnotherInterface { field: SomeObject } - """ - ) + """) assert validate_schema(schema) == [] def rejects_an_object_missing_an_interface_argument(): - schema = build_schema( - """ + schema = build_schema(""" type Query { test: AnotherObject } @@ -1929,8 +1852,7 @@ def rejects_an_object_missing_an_interface_argument(): type AnotherObject implements AnotherInterface { field: String } - """ - ) + """) assert validate_schema(schema) == [ { "message": "Interface field argument" @@ -1941,8 +1863,7 @@ def rejects_an_object_missing_an_interface_argument(): ] def rejects_an_object_with_an_incorrectly_typed_interface_argument(): - schema = build_schema( - """ + schema = build_schema(""" type Query { test: AnotherObject } @@ -1954,8 +1875,7 @@ def rejects_an_object_with_an_incorrectly_typed_interface_argument(): type AnotherObject implements AnotherInterface { field(input: Int): String } - """ - ) + """) assert validate_schema(schema) == [ { "message": "Interface field argument" @@ -1966,8 +1886,7 @@ def rejects_an_object_with_an_incorrectly_typed_interface_argument(): ] def rejects_an_object_with_an_incorrectly_typed_field_and_argument(): - schema = build_schema( - """ + schema = build_schema(""" type Query { test: AnotherObject } @@ -1979,8 +1898,7 @@ def rejects_an_object_with_an_incorrectly_typed_field_and_argument(): type AnotherObject implements AnotherInterface { field(input: Int): Int } - """ - ) + """) assert validate_schema(schema) == [ { "message": "Interface field AnotherInterface.field expects" @@ -1996,8 +1914,7 @@ def rejects_an_object_with_an_incorrectly_typed_field_and_argument(): ] def rejects_object_implementing_an_interface_field_with_additional_args(): - schema = build_schema( - """ + schema = build_schema(""" type Query { test: AnotherObject } @@ -2014,8 +1931,7 @@ def rejects_object_implementing_an_interface_field_with_additional_args(): optionalArg2: String = "", ): String } - """ - ) + """) assert validate_schema(schema) == [ { "message": "Object field AnotherObject.field includes required" @@ -2026,8 +1942,7 @@ def rejects_object_implementing_an_interface_field_with_additional_args(): ] def accepts_an_object_with_an_equivalently_wrapped_interface_field_type(): - schema = build_schema( - """ + schema = build_schema(""" type Query { test: AnotherObject } @@ -2039,13 +1954,11 @@ def accepts_an_object_with_an_equivalently_wrapped_interface_field_type(): type AnotherObject implements AnotherInterface { field: [String]! } - """ - ) + """) assert validate_schema(schema) == [] def rejects_an_object_with_a_non_list_interface_field_list_type(): - schema = build_schema( - """ + schema = build_schema(""" type Query { test: AnotherObject } @@ -2057,8 +1970,7 @@ def rejects_an_object_with_a_non_list_interface_field_list_type(): type AnotherObject implements AnotherInterface { field: String } - """ - ) + """) assert validate_schema(schema) == [ { "message": "Interface field AnotherInterface.field expects type" @@ -2068,8 +1980,7 @@ def rejects_an_object_with_a_non_list_interface_field_list_type(): ] def rejects_an_object_with_a_list_interface_field_non_list_type(): - schema = build_schema( - """ + schema = build_schema(""" type Query { test: AnotherObject } @@ -2081,8 +1992,7 @@ def rejects_an_object_with_a_list_interface_field_non_list_type(): type AnotherObject implements AnotherInterface { field: [String] } - """ - ) + """) assert validate_schema(schema) == [ { "message": "Interface field AnotherInterface.field expects type" @@ -2092,8 +2002,7 @@ def rejects_an_object_with_a_list_interface_field_non_list_type(): ] def accepts_an_object_with_a_subset_non_null_interface_field_type(): - schema = build_schema( - """ + schema = build_schema(""" type Query { test: AnotherObject } @@ -2105,13 +2014,11 @@ def accepts_an_object_with_a_subset_non_null_interface_field_type(): type AnotherObject implements AnotherInterface { field: String! } - """ - ) + """) assert validate_schema(schema) == [] def rejects_an_object_with_a_superset_nullable_interface_field_type(): - schema = build_schema( - """ + schema = build_schema(""" type Query { test: AnotherObject } @@ -2123,8 +2030,7 @@ def rejects_an_object_with_a_superset_nullable_interface_field_type(): type AnotherObject implements AnotherInterface { field: String } - """ - ) + """) assert validate_schema(schema) == [ { "message": "Interface field AnotherInterface.field expects type" @@ -2134,8 +2040,7 @@ def rejects_an_object_with_a_superset_nullable_interface_field_type(): ] def rejects_an_object_missing_a_transitive_interface(): - schema = build_schema( - """ + schema = build_schema(""" type Query { test: AnotherObject } @@ -2151,8 +2056,7 @@ def rejects_an_object_missing_a_transitive_interface(): type AnotherObject implements AnotherInterface { field: String! } - """ - ) + """) assert validate_schema(schema) == [ { "message": "Type AnotherObject must implement SuperInterface" @@ -2164,8 +2068,7 @@ def rejects_an_object_missing_a_transitive_interface(): def describe_interfaces_must_adhere_to_interface_they_implement(): def accepts_an_interface_which_implements_an_interface(): - schema = build_schema( - """ + schema = build_schema(""" type Query { test: ChildInterface } @@ -2177,13 +2080,11 @@ def accepts_an_interface_which_implements_an_interface(): interface ChildInterface implements ParentInterface { field(input: String): String } - """ - ) + """) assert validate_schema(schema) == [] def accepts_an_interface_which_implements_an_interface_along_with_more_fields(): - schema = build_schema( - """ + schema = build_schema(""" type Query { test: ChildInterface } @@ -2196,13 +2097,11 @@ def accepts_an_interface_which_implements_an_interface_along_with_more_fields(): field(input: String): String anotherField: String } - """ - ) + """) assert validate_schema(schema) == [] def accepts_an_interface_which_implements_an_interface_with_additional_args(): - schema = build_schema( - """ + schema = build_schema(""" type Query { test: ChildInterface } @@ -2214,13 +2113,11 @@ def accepts_an_interface_which_implements_an_interface_with_additional_args(): interface ChildInterface implements ParentInterface { field(input: String, anotherInput: String): String } - """ - ) + """) assert validate_schema(schema) == [] def rejects_an_interface_missing_an_interface_field(): - schema = build_schema( - """ + schema = build_schema(""" type Query { test: ChildInterface } @@ -2232,8 +2129,7 @@ def rejects_an_interface_missing_an_interface_field(): interface ChildInterface implements ParentInterface { anotherField: String } - """ - ) + """) assert validate_schema(schema) == [ { "message": "Interface field ParentInterface.field expected" @@ -2243,8 +2139,7 @@ def rejects_an_interface_missing_an_interface_field(): ] def rejects_an_interface_with_an_incorrectly_typed_interface_field(): - schema = build_schema( - """ + schema = build_schema(""" type Query { test: ChildInterface } @@ -2256,8 +2151,7 @@ def rejects_an_interface_with_an_incorrectly_typed_interface_field(): interface ChildInterface implements ParentInterface { field(input: String): Int } - """ - ) + """) assert validate_schema(schema) == [ { "message": "Interface field ParentInterface.field expects type String" @@ -2267,8 +2161,7 @@ def rejects_an_interface_with_an_incorrectly_typed_interface_field(): ] def rejects_an_interface_with_a_differently_typed_interface_field(): - schema = build_schema( - """ + schema = build_schema(""" type Query { test: ChildInterface } @@ -2283,8 +2176,7 @@ def rejects_an_interface_with_a_differently_typed_interface_field(): interface ChildInterface implements ParentInterface { field: B } - """ - ) + """) assert validate_schema(schema) == [ { "message": "Interface field ParentInterface.field expects type A" @@ -2294,8 +2186,7 @@ def rejects_an_interface_with_a_differently_typed_interface_field(): ] def accepts_an_interface_with_a_subtyped_interface_field_interface(): - schema = build_schema( - """ + schema = build_schema(""" type Query { test: ChildInterface } @@ -2307,13 +2198,11 @@ def accepts_an_interface_with_a_subtyped_interface_field_interface(): interface ChildInterface implements ParentInterface { field: ChildInterface } - """ - ) + """) assert validate_schema(schema) == [] def accepts_an_interface_with_a_subtyped_interface_field_union(): - schema = build_schema( - """ + schema = build_schema(""" type Query { test: ChildInterface } @@ -2331,15 +2220,13 @@ def accepts_an_interface_with_a_subtyped_interface_field_union(): interface ChildInterface implements ParentInterface { field: SomeObject } - """ - ) + """) assert validate_schema(schema) == [] def rejects_an_interface_implementing_a_non_interface_type(): # invalid schema cannot be built with Python with raises(TypeError) as exc_info: - build_schema( - """ + build_schema(""" type Query { field: String } @@ -2351,8 +2238,7 @@ def rejects_an_interface_implementing_a_non_interface_type(): interface BadInterface implements SomeInputObject { field: String } - """ - ) + """) assert str(exc_info.value) == ( "BadInterface interfaces must be specified as a collection" " of GraphQLInterfaceType instances." @@ -2378,8 +2264,7 @@ def rejects_an_interface_implementing_a_non_interface_type(): ] def rejects_an_interface_missing_an_interface_argument(): - schema = build_schema( - """ + schema = build_schema(""" type Query { test: ChildInterface } @@ -2391,8 +2276,7 @@ def rejects_an_interface_missing_an_interface_argument(): interface ChildInterface implements ParentInterface { field: String } - """ - ) + """) assert validate_schema(schema) == [ { "message": "Interface field argument ParentInterface.field(input:)" @@ -2402,8 +2286,7 @@ def rejects_an_interface_missing_an_interface_argument(): ] def rejects_an_interface_with_an_incorrectly_typed_interface_argument(): - schema = build_schema( - """ + schema = build_schema(""" type Query { test: ChildInterface } @@ -2415,8 +2298,7 @@ def rejects_an_interface_with_an_incorrectly_typed_interface_argument(): interface ChildInterface implements ParentInterface { field(input: Int): String } - """ - ) + """) assert validate_schema(schema) == [ { "message": "Interface field argument ParentInterface.field(input:)" @@ -2426,8 +2308,7 @@ def rejects_an_interface_with_an_incorrectly_typed_interface_argument(): ] def rejects_an_interface_with_both_an_incorrectly_typed_field_and_argument(): - schema = build_schema( - """ + schema = build_schema(""" type Query { test: ChildInterface } @@ -2439,8 +2320,7 @@ def rejects_an_interface_with_both_an_incorrectly_typed_field_and_argument(): interface ChildInterface implements ParentInterface { field(input: Int): Int } - """ - ) + """) assert validate_schema(schema) == [ { "message": "Interface field ParentInterface.field expects type String" @@ -2455,8 +2335,7 @@ def rejects_an_interface_with_both_an_incorrectly_typed_field_and_argument(): ] def rejects_an_interface_implementing_an_interface_field_with_additional_args(): - schema = build_schema( - """ + schema = build_schema(""" type Query { test: ChildInterface } @@ -2473,8 +2352,7 @@ def rejects_an_interface_implementing_an_interface_field_with_additional_args(): optionalArg2: String = "", ): String } - """ - ) + """) assert validate_schema(schema) == [ { "message": "Object field ChildInterface.field includes" @@ -2485,8 +2363,7 @@ def rejects_an_interface_implementing_an_interface_field_with_additional_args(): ] def accepts_an_interface_with_an_equivalently_wrapped_interface_field_type(): - schema = build_schema( - """ + schema = build_schema(""" type Query { test: ChildInterface } @@ -2498,13 +2375,11 @@ def accepts_an_interface_with_an_equivalently_wrapped_interface_field_type(): interface ChildInterface implements ParentInterface { field: [String]! } - """ - ) + """) assert validate_schema(schema) == [] def rejects_an_interface_with_a_non_list_interface_field_list_type(): - schema = build_schema( - """ + schema = build_schema(""" type Query { test: ChildInterface } @@ -2516,8 +2391,7 @@ def rejects_an_interface_with_a_non_list_interface_field_list_type(): interface ChildInterface implements ParentInterface { field: String } - """ - ) + """) assert validate_schema(schema) == [ { "message": "Interface field ParentInterface.field" @@ -2527,8 +2401,7 @@ def rejects_an_interface_with_a_non_list_interface_field_list_type(): ] def rejects_an_interface_with_a_list_interface_field_non_list_type(): - schema = build_schema( - """ + schema = build_schema(""" type Query { test: ChildInterface } @@ -2540,8 +2413,7 @@ def rejects_an_interface_with_a_list_interface_field_non_list_type(): interface ChildInterface implements ParentInterface { field: [String] } - """ - ) + """) assert validate_schema(schema) == [ { "message": "Interface field ParentInterface.field expects type String" @@ -2551,8 +2423,7 @@ def rejects_an_interface_with_a_list_interface_field_non_list_type(): ] def accepts_an_interface_with_a_subset_non_null_interface_field_type(): - schema = build_schema( - """ + schema = build_schema(""" type Query { test: ChildInterface } @@ -2564,13 +2435,11 @@ def accepts_an_interface_with_a_subset_non_null_interface_field_type(): interface ChildInterface implements ParentInterface { field: String! } - """ - ) + """) assert validate_schema(schema) == [] def rejects_an_interface_with_a_superset_nullable_interface_field_type(): - schema = build_schema( - """ + schema = build_schema(""" type Query { test: ChildInterface } @@ -2582,8 +2451,7 @@ def rejects_an_interface_with_a_superset_nullable_interface_field_type(): interface ChildInterface implements ParentInterface { field: String } - """ - ) + """) assert validate_schema(schema) == [ { "message": "Interface field ParentInterface.field expects type String!" @@ -2593,8 +2461,7 @@ def rejects_an_interface_with_a_superset_nullable_interface_field_type(): ] def rejects_an_object_missing_a_transitive_interface(): - schema = build_schema( - """ + schema = build_schema(""" type Query { test: ChildInterface } @@ -2610,8 +2477,7 @@ def rejects_an_object_missing_a_transitive_interface(): interface ChildInterface implements ParentInterface { field: String! } - """ - ) + """) assert validate_schema(schema) == [ { "message": "Type ChildInterface must implement SuperInterface" @@ -2621,8 +2487,7 @@ def rejects_an_object_missing_a_transitive_interface(): ] def rejects_a_self_reference_interface(): - schema = build_schema( - """ + schema = build_schema(""" type Query { test: FooInterface } @@ -2630,8 +2495,7 @@ def rejects_a_self_reference_interface(): interface FooInterface implements FooInterface { field: String } - """ - ) + """) assert validate_schema(schema) == [ { "message": "Type FooInterface cannot implement itself" @@ -2641,8 +2505,7 @@ def rejects_a_self_reference_interface(): ] def rejects_a_circular_interface_implementation(): - schema = build_schema( - """ + schema = build_schema(""" type Query { test: FooInterface } @@ -2654,8 +2517,7 @@ def rejects_a_circular_interface_implementation(): interface BarInterface implements FooInterface { field: String } - """ - ) + """) assert validate_schema(schema) == [ { "message": "Type FooInterface cannot implement BarInterface" @@ -2672,28 +2534,19 @@ def rejects_a_circular_interface_implementation(): def describe_assert_valid_schema(): def does_not_throw_on_valid_schemas(): - schema = build_schema( - ( - """ + schema = build_schema((""" type Query { foo: String } - """ - ) - ) + """)) assert_valid_schema(schema) def combines_multiple_errors(): schema = build_schema("type SomeType") with raises(TypeError) as exc_info: assert_valid_schema(schema) - assert ( - str(exc_info.value) - == dedent( - """ + assert str(exc_info.value) == dedent(""" Query root type must be provided. Type SomeType must define one or more fields. - """ - ).rstrip() - ) + """).rstrip() diff --git a/tests/utilities/test_ast_to_dict.py b/tests/utilities/test_ast_to_dict.py index d0fa1b24..b7e031b7 100644 --- a/tests/utilities/test_ast_to_dict.py +++ b/tests/utilities/test_ast_to_dict.py @@ -51,8 +51,7 @@ def converts_recursive_ast_to_recursive_dict(): } def converts_simple_schema_to_dict(): - ast = parse( - """ + ast = parse(""" type Query { me: User } @@ -61,8 +60,7 @@ def converts_simple_schema_to_dict(): id: ID name: String } - """ - ) + """) res = ast_to_dict(ast) assert ast.to_dict() == res assert res == { @@ -124,8 +122,7 @@ def converts_simple_schema_to_dict(): assert list(res)[0] == "kind" def converts_simple_schema_to_dict_with_locations(): - ast = parse( - """ + ast = parse(""" type Query { me: User } @@ -134,8 +131,7 @@ def converts_simple_schema_to_dict_with_locations(): id: ID name: String } - """ - ) + """) res = ast_to_dict(ast, locations=True) assert ast.to_dict(locations=True) == res assert res == { @@ -241,8 +237,7 @@ def converts_simple_schema_to_dict_with_locations(): assert list(res["loc"]) == ["start", "end"] def converts_simple_query_to_dict(): - ast = parse( - """ + ast = parse(""" query HeroForEpisode($ep: Episode!) { hero(episode: $ep) { name @@ -254,13 +249,13 @@ def converts_simple_query_to_dict(): } } } - """ - ) + """) res = ast_to_dict(ast) assert ast.to_dict() == res assert res == { "definitions": [ { + "description": None, "directives": [], "kind": "operation_definition", "name": {"kind": "name", "value": "HeroForEpisode"}, @@ -356,6 +351,7 @@ def converts_simple_query_to_dict(): "variable_definitions": [ { "default_value": None, + "description": None, "directives": [], "kind": "variable_definition", "type": { @@ -378,8 +374,7 @@ def converts_simple_query_to_dict(): assert list(res)[0] == "kind" def converts_simple_query_to_dict_with_locations(): - ast = parse( - """ + ast = parse(""" query HeroForEpisode($ep: Episode!) { hero(episode: $ep) { name @@ -391,13 +386,13 @@ def converts_simple_query_to_dict_with_locations(): } } } - """ - ) + """) res = ast_to_dict(ast, locations=True) assert ast.to_dict(locations=True) == res assert res == { "definitions": [ { + "description": None, "directives": [], "kind": "operation_definition", "loc": {"end": 293, "start": 13}, @@ -544,6 +539,7 @@ def converts_simple_query_to_dict_with_locations(): "variable_definitions": [ { "default_value": None, + "description": None, "directives": [], "kind": "variable_definition", "loc": {"end": 47, "start": 34}, diff --git a/tests/utilities/test_build_ast_schema.py b/tests/utilities/test_build_ast_schema.py index ba9c6050..72594844 100644 --- a/tests/utilities/test_build_ast_schema.py +++ b/tests/utilities/test_build_ast_schema.py @@ -1,27 +1,31 @@ +import pickle +import sys from collections import namedtuple +from copy import deepcopy from typing import Union from pytest import mark, raises from graphql import graphql_sync -from graphql.language import parse, print_ast, DocumentNode, InterfaceTypeDefinitionNode +from graphql.language import DocumentNode, InterfaceTypeDefinitionNode, parse, print_ast from graphql.type import ( - GraphQLDeprecatedDirective, - GraphQLIncludeDirective, - GraphQLSchema, - GraphQLSkipDirective, - GraphQLSpecifiedByDirective, - GraphQLBoolean, - GraphQLFloat, - GraphQLID, - GraphQLInt, - GraphQLString, GraphQLArgument, + GraphQLBoolean, + GraphQLDeprecatedDirective, GraphQLEnumType, GraphQLEnumValue, GraphQLField, + GraphQLFloat, + GraphQLID, + GraphQLIncludeDirective, GraphQLInputField, + GraphQLInt, GraphQLNamedType, + GraphQLSchema, + GraphQLSkipDirective, + GraphQLSpecifiedByDirective, + GraphQLOneOfDirective, + GraphQLString, assert_directive, assert_enum_type, assert_input_object_type, @@ -35,7 +39,8 @@ from graphql.utilities import build_ast_schema, build_schema, print_schema, print_type from ..fixtures import big_schema_sdl # noqa: F401 -from ..utils import dedent +from ..star_wars_schema import star_wars_schema +from ..utils import dedent, timeout_factor def cycle_sdl(sdl: str) -> str: @@ -69,15 +74,11 @@ def expect_extension_ast_nodes(obj: TypeWithExtensionAstNodes, expected: str) -> def describe_schema_builder(): def can_use_built_schema_for_limited_execution(): - schema = build_ast_schema( - parse( - """ + schema = build_ast_schema(parse(""" type Query { str: String } - """ - ) - ) + """)) root_value = namedtuple("Data", "str")(123) # type: ignore @@ -85,13 +86,11 @@ def can_use_built_schema_for_limited_execution(): assert result == ({"str": "123"}, None) def can_build_a_schema_directly_from_the_source(): - schema = build_schema( - """ + schema = build_schema(""" type Query { add(x: Int, y: Int): Int } - """ - ) + """) source = "{ add(x: 34, y: 55) }" # noinspection PyMethodMayBeStatic @@ -124,16 +123,13 @@ def match_order_of_default_types_and_directives(): assert sdl_schema.type_map == schema.type_map def empty_type(): - sdl = dedent( - """ + sdl = dedent(""" type EmptyType - """ - ) + """) assert cycle_sdl(sdl) == sdl def simple_type(): - sdl = dedent( - """ + sdl = dedent(""" type Query { str: String int: Int @@ -141,8 +137,7 @@ def simple_type(): id: ID bool: Boolean } - """ - ) + """) assert cycle_sdl(sdl) == sdl schema = build_schema(sdl) @@ -164,18 +159,15 @@ def include_standard_type_only_if_it_is_used(): assert schema.get_type("ID") is None def with_directives(): - sdl = dedent( - """ + sdl = dedent(""" directive @foo(arg: Int) on FIELD directive @repeatableFoo(arg: Int) repeatable on FIELD - """ - ) + """) assert cycle_sdl(sdl) == sdl def supports_descriptions(): - sdl = dedent( - ''' + sdl = dedent(''' """Do you agree that this is the most creative schema ever?""" schema { query: Query @@ -219,30 +211,29 @@ def supports_descriptions(): """And a field to boot""" str: String } - ''' - ) + ''') assert cycle_sdl(sdl) == sdl def maintains_include_skip_and_specified_by_url_directives(): schema = build_schema("type Query") - assert len(schema.directives) == 4 + assert len(schema.directives) == 5 assert schema.get_directive("skip") is GraphQLSkipDirective assert schema.get_directive("include") is GraphQLIncludeDirective assert schema.get_directive("deprecated") is GraphQLDeprecatedDirective assert schema.get_directive("specifiedBy") is GraphQLSpecifiedByDirective + assert schema.get_directive("oneOf") is GraphQLOneOfDirective def overriding_directives_excludes_specified(): - schema = build_schema( - """ + schema = build_schema(""" directive @skip on FIELD directive @include on FIELD directive @deprecated on FIELD_DEFINITION directive @specifiedBy on FIELD_DEFINITION - """ - ) + directive @oneOf on OBJECT + """) - assert len(schema.directives) == 4 + assert len(schema.directives) == 5 get_directive = schema.get_directive assert get_directive("skip") is not GraphQLSkipDirective assert get_directive("skip") is not None @@ -252,24 +243,24 @@ def overriding_directives_excludes_specified(): assert get_directive("deprecated") is not None assert get_directive("specifiedBy") is not GraphQLSpecifiedByDirective assert get_directive("specifiedBy") is not None + assert get_directive("oneOf") is not GraphQLOneOfDirective + assert get_directive("oneOf") is not None - def adding_directives_maintains_include_skip_and_specified_by_directives(): - schema = build_schema( - """ + def adding_directives_maintains_include_skip_and_three_other_directives(): + schema = build_schema(""" directive @foo(arg: Int) on FIELD - """ - ) + """) - assert len(schema.directives) == 5 + assert len(schema.directives) == 6 assert schema.get_directive("skip") is GraphQLSkipDirective assert schema.get_directive("include") is GraphQLIncludeDirective assert schema.get_directive("deprecated") is GraphQLDeprecatedDirective assert schema.get_directive("specifiedBy") is GraphQLSpecifiedByDirective + assert schema.get_directive("oneOf") is GraphQLOneOfDirective assert schema.get_directive("foo") is not None def type_modifiers(): - sdl = dedent( - """ + sdl = dedent(""" type Query { nonNullStr: String! listOfStrings: [String] @@ -277,24 +268,20 @@ def type_modifiers(): nonNullListOfStrings: [String]! nonNullListOfNonNullStrings: [String!]! } - """ - ) + """) assert cycle_sdl(sdl) == sdl def recursive_type(): - sdl = dedent( - """ + sdl = dedent(""" type Query { str: String recurse: Query } - """ - ) + """) assert cycle_sdl(sdl) == sdl def two_types_circular(): - sdl = dedent( - """ + sdl = dedent(""" type TypeOne { str: String typeTwo: TypeTwo @@ -304,13 +291,11 @@ def two_types_circular(): str: String typeOne: TypeOne } - """ - ) + """) assert cycle_sdl(sdl) == sdl def single_argument_field(): - sdl = dedent( - """ + sdl = dedent(""" type Query { str(int: Int): String floatToStr(float: Float): String @@ -318,26 +303,21 @@ def single_argument_field(): booleanToStr(bool: Boolean): String strToStr(bool: String): String } - """ - ) + """) assert cycle_sdl(sdl) == sdl def simple_type_with_multiple_arguments(): - sdl = dedent( - """ + sdl = dedent(""" type Query { str(int: Int, bool: Boolean): String } - """ - ) + """) assert cycle_sdl(sdl) == sdl def empty_interface(): - sdl = dedent( - """ + sdl = dedent(""" interface EmptyInterface - """ - ) + """) definition = parse(sdl).definitions[0] assert isinstance(definition, InterfaceTypeDefinitionNode) @@ -346,8 +326,7 @@ def empty_interface(): assert cycle_sdl(sdl) == sdl def simple_type_with_interface(): - sdl = dedent( - """ + sdl = dedent(""" type Query implements WorldInterface { str: String } @@ -355,13 +334,11 @@ def simple_type_with_interface(): interface WorldInterface { str: String } - """ - ) + """) assert cycle_sdl(sdl) == sdl def simple_interface_hierarchy(): - sdl = dedent( - """ + sdl = dedent(""" schema { query: Child } @@ -377,21 +354,17 @@ def simple_interface_hierarchy(): interface Parent { str: String } - """ - ) + """) assert cycle_sdl(sdl) == sdl def empty_enum(): - sdl = dedent( - """ + sdl = dedent(""" enum EmptyEnum - """ - ) + """) assert cycle_sdl(sdl) == sdl def simple_output_enum(): - sdl = dedent( - """ + sdl = dedent(""" enum Hello { WORLD } @@ -399,13 +372,11 @@ def simple_output_enum(): type Query { hello: Hello } - """ - ) + """) assert cycle_sdl(sdl) == sdl def simple_input_enum(): - sdl = dedent( - """ + sdl = dedent(""" enum Hello { WORLD } @@ -413,13 +384,11 @@ def simple_input_enum(): type Query { str(hello: Hello): String } - """ - ) + """) assert cycle_sdl(sdl) == sdl def multiple_value_enum(): - sdl = dedent( - """ + sdl = dedent(""" enum Hello { WO RLD @@ -428,8 +397,7 @@ def multiple_value_enum(): type Query { hello: Hello } - """ - ) + """) assert cycle_sdl(sdl) == sdl # check that the internal values are the same as the names @@ -439,16 +407,13 @@ def multiple_value_enum(): assert [value.value for value in enum_type.values.values()] == ["WO", "RLD"] def empty_union(): - sdl = dedent( - """ + sdl = dedent(""" union EmptyUnion - """ - ) + """) assert cycle_sdl(sdl) == sdl def simple_union(): - sdl = dedent( - """ + sdl = dedent(""" union Hello = World type Query { @@ -458,13 +423,11 @@ def simple_union(): type World { str: String } - """ - ) + """) assert cycle_sdl(sdl) == sdl def multiple_union(): - sdl = dedent( - """ + sdl = dedent(""" union Hello = WorldOne | WorldTwo type Query { @@ -478,50 +441,42 @@ def multiple_union(): type WorldTwo { str: String } - """ - ) + """) assert cycle_sdl(sdl) == sdl def can_build_recursive_union(): # invalid schema cannot be built with Python with raises(TypeError) as exc_info: - build_schema( - """ + build_schema(""" union Hello = Hello type Query { hello: Hello } - """ - ) + """) assert ( str(exc_info.value) == "Hello types must be specified" " as a collection of GraphQLObjectType instances." ) def custom_scalar(): - sdl = dedent( - """ + sdl = dedent(""" scalar CustomScalar type Query { customScalar: CustomScalar } - """ - ) + """) assert cycle_sdl(sdl) == sdl def empty_input_object(): - sdl = dedent( - """ + sdl = dedent(""" input EmptyInputObject - """ - ) + """) assert cycle_sdl(sdl) == sdl def simple_input_object(): - sdl = dedent( - """ + sdl = dedent(""" input Input { int: Int } @@ -529,35 +484,29 @@ def simple_input_object(): type Query { field(in: Input): String } - """ - ) + """) assert cycle_sdl(sdl) == sdl def simple_argument_field_with_default(): - sdl = dedent( - """ + sdl = dedent(""" type Query { str(int: Int = 2): String } - """ - ) + """) assert cycle_sdl(sdl) == sdl def custom_scalar_argument_field_with_default(): - sdl = dedent( - """ + sdl = dedent(""" scalar CustomScalar type Query { str(int: CustomScalar = 2): String } - """ - ) + """) assert cycle_sdl(sdl) == sdl def simple_type_with_mutation(): - sdl = dedent( - """ + sdl = dedent(""" schema { query: HelloScalars mutation: Mutation @@ -572,13 +521,11 @@ def simple_type_with_mutation(): type Mutation { addHelloScalars(str: String, int: Int, bool: Boolean): HelloScalars } - """ - ) + """) assert cycle_sdl(sdl) == sdl def simple_type_with_subscription(): - sdl = dedent( - """ + sdl = dedent(""" schema { query: HelloScalars subscription: Subscription @@ -593,13 +540,11 @@ def simple_type_with_subscription(): type Subscription { subscribeHelloScalars(str: String, int: Int, bool: Boolean): HelloScalars } - """ - ) + """) assert cycle_sdl(sdl) == sdl def unreferenced_type_implementing_referenced_interface(): - sdl = dedent( - """ + sdl = dedent(""" type Concrete implements Interface { key: String } @@ -611,13 +556,11 @@ def unreferenced_type_implementing_referenced_interface(): type Query { interface: Interface } - """ - ) + """) assert cycle_sdl(sdl) == sdl def unreferenced_interface_implementing_referenced_interface(): - sdl = dedent( - """ + sdl = dedent(""" interface Child implements Parent { key: String } @@ -629,13 +572,11 @@ def unreferenced_interface_implementing_referenced_interface(): type Query { interfaceField: Parent } - """ - ) + """) assert cycle_sdl(sdl) == sdl def unreferenced_type_implementing_referenced_union(): - sdl = dedent( - """ + sdl = dedent(""" type Concrete { key: String } @@ -645,13 +586,11 @@ def unreferenced_type_implementing_referenced_union(): } union Union = Concrete - """ - ) + """) assert cycle_sdl(sdl) == sdl def supports_deprecated_directive(): - sdl = dedent( - """ + sdl = dedent(""" enum MyEnum { VALUE OLD_VALUE @deprecated @@ -672,8 +611,7 @@ def supports_deprecated_directive(): field4(oldArg: String @deprecated(reason: "Why not?"), arg: String): String field5(arg: MyInput): String } - """ # noqa: E501 - ) + """) # noqa: E501 assert cycle_sdl(sdl) == sdl schema = build_schema(sdl) @@ -713,15 +651,13 @@ def supports_deprecated_directive(): assert field4_old_arg.deprecation_reason == "Why not?" def supports_specified_by_directives(): - sdl = dedent( - """ + sdl = dedent(""" scalar Foo @specifiedBy(url: "https://example.com/foo_spec") type Query { foo: Foo @deprecated } - """ - ) + """) assert cycle_sdl(sdl) == sdl schema = build_schema(sdl) @@ -730,39 +666,32 @@ def supports_specified_by_directives(): assert foo_scalar.specified_by_url == "https://example.com/foo_spec" def correctly_extend_scalar_type(): - schema = build_schema( - """ + schema = build_schema(""" scalar SomeScalar extend scalar SomeScalar @foo extend scalar SomeScalar @bar directive @foo on SCALAR directive @bar on SCALAR - """ - ) + """) some_scalar = assert_scalar_type(schema.get_type("SomeScalar")) - assert print_type(some_scalar) == dedent( - """ + assert print_type(some_scalar) == dedent(""" scalar SomeScalar - """ - ) + """) expect_ast_node(some_scalar, "scalar SomeScalar") expect_extension_ast_nodes( some_scalar, - dedent( - """ + dedent(""" extend scalar SomeScalar @foo extend scalar SomeScalar @bar - """ - ), + """), ) def correctly_extend_object_type(): - schema = build_schema( - """ + schema = build_schema(""" type SomeObject implements Foo { first: String } @@ -778,34 +707,28 @@ def correctly_extend_object_type(): interface Foo interface Bar interface Baz - """ - ) + """) some_object = assert_object_type(schema.get_type("SomeObject")) - assert print_type(some_object) == dedent( - """ + assert print_type(some_object) == dedent(""" type SomeObject implements Foo & Bar & Baz { first: String second: Int third: Float } - """ - ) + """) expect_ast_node( some_object, - dedent( - """ + dedent(""" type SomeObject implements Foo { first: String } - """ - ), + """), ) expect_extension_ast_nodes( some_object, - dedent( - """ + dedent(""" extend type SomeObject implements Bar { second: Int } @@ -813,13 +736,11 @@ def correctly_extend_object_type(): extend type SomeObject implements Baz { third: Float } - """ - ), + """), ) def correctly_extend_interface_type(): - schema = build_schema( - """ + schema = build_schema(""" interface SomeInterface { first: String } @@ -831,34 +752,28 @@ def correctly_extend_interface_type(): extend interface SomeInterface { third: Float } - """ - ) + """) some_interface = assert_interface_type(schema.get_type("SomeInterface")) - assert print_type(some_interface) == dedent( - """ + assert print_type(some_interface) == dedent(""" interface SomeInterface { first: String second: Int third: Float } - """ - ) + """) expect_ast_node( some_interface, - dedent( - """ + dedent(""" interface SomeInterface { first: String } - """ - ), + """), ) expect_extension_ast_nodes( some_interface, - dedent( - """ + dedent(""" extend interface SomeInterface { second: Int } @@ -866,13 +781,11 @@ def correctly_extend_interface_type(): extend interface SomeInterface { third: Float } - """ - ), + """), ) def correctly_extend_union_type(): - schema = build_schema( - """ + schema = build_schema(""" union SomeUnion = FirstType extend union SomeUnion = SecondType extend union SomeUnion = ThirdType @@ -880,31 +793,25 @@ def correctly_extend_union_type(): type FirstType type SecondType type ThirdType - """ - ) + """) some_union = assert_union_type(schema.get_type("SomeUnion")) - assert print_type(some_union) == dedent( - """ + assert print_type(some_union) == dedent(""" union SomeUnion = FirstType | SecondType | ThirdType - """ - ) + """) expect_ast_node(some_union, "union SomeUnion = FirstType") expect_extension_ast_nodes( some_union, - dedent( - """ + dedent(""" extend union SomeUnion = SecondType extend union SomeUnion = ThirdType - """ - ), + """), ) def correctly_extend_enum_type(): - schema = build_schema( - """ + schema = build_schema(""" enum SomeEnum { FIRST } @@ -916,34 +823,28 @@ def correctly_extend_enum_type(): extend enum SomeEnum { THIRD } - """ - ) + """) some_enum = assert_enum_type(schema.get_type("SomeEnum")) - assert print_type(some_enum) == dedent( - """ + assert print_type(some_enum) == dedent(""" enum SomeEnum { FIRST SECOND THIRD } - """ - ) + """) expect_ast_node( some_enum, - dedent( - """ + dedent(""" enum SomeEnum { FIRST } - """ - ), + """), ) expect_extension_ast_nodes( some_enum, - dedent( - """ + dedent(""" extend enum SomeEnum { SECOND } @@ -951,13 +852,11 @@ def correctly_extend_enum_type(): extend enum SomeEnum { THIRD } - """ - ), + """), ) def correctly_extend_input_object_type(): - schema = build_schema( - """ + schema = build_schema(""" input SomeInput { first: String } @@ -969,34 +868,28 @@ def correctly_extend_input_object_type(): extend input SomeInput { third: Float } - """ - ) + """) some_input = assert_input_object_type(schema.get_type("SomeInput")) - assert print_type(some_input) == dedent( - """ + assert print_type(some_input) == dedent(""" input SomeInput { first: String second: Int third: Float } - """ - ) + """) expect_ast_node( some_input, - dedent( - """ + dedent(""" input SomeInput { first: String } - """ - ), + """), ) expect_extension_ast_nodes( some_input, - dedent( - """ + dedent(""" extend input SomeInput { second: Int } @@ -1004,13 +897,11 @@ def correctly_extend_input_object_type(): extend input SomeInput { third: Float } - """ - ), + """), ) def correctly_assign_ast_nodes(): - sdl = dedent( - """ + sdl = dedent(""" schema { query: Query } @@ -1040,8 +931,7 @@ def correctly_assign_ast_nodes(): scalar TestScalar directive @test(arg: TestScalar) on FIELD - """ - ) + """) ast = parse(sdl, no_location=True) schema = build_ast_schema(ast) @@ -1078,8 +968,7 @@ def correctly_assign_ast_nodes(): expect_ast_node(test_directive.args["arg"], "arg: TestScalar") def root_operation_types_with_custom_names(): - schema = build_schema( - """ + schema = build_schema(""" schema { query: SomeQuery mutation: SomeMutation @@ -1088,8 +977,7 @@ def root_operation_types_with_custom_names(): type SomeQuery type SomeMutation type SomeSubscription - """ - ) + """) assert schema.query_type assert schema.query_type.name == "SomeQuery" @@ -1099,13 +987,11 @@ def root_operation_types_with_custom_names(): assert schema.subscription_type.name == "SomeSubscription" def default_root_operation_type_names(): - schema = build_schema( - """ + schema = build_schema(""" type Query type Mutation type Subscription - """ - ) + """) assert schema.query_type assert schema.query_type.name == "Query" @@ -1121,28 +1007,24 @@ def can_build_invalid_schema(): assert errors def do_not_override_standard_types(): - # Note: not sure it's desired behaviour to just silently ignore override + # Note: not sure it's desired behavior to just silently ignore override # attempts so just documenting it here. - schema = build_schema( - """ + schema = build_schema(""" scalar ID scalar __Schema - """ - ) + """) assert schema.get_type("ID") is GraphQLID assert schema.get_type("__Schema") is introspection_types["__Schema"] def allows_to_reference_introspection_types(): - schema = build_schema( - """ + schema = build_schema(""" type Query { introspectionField: __EnumValue } - """ - ) + """) query_type = assert_object_type(schema.get_type("Query")) __EnumValue = introspection_types["__EnumValue"] @@ -1168,6 +1050,21 @@ def allows_to_disable_sdl_validation(): build_schema(sdl, assume_valid=True) build_schema(sdl, assume_valid_sdl=True) + def forwards_parser_options_to_build_schema(): + schema = build_schema( + """ +type Query { + foo: String +} + +directive @bar @deprecated(reason: "Use another directive") on FIELD_DEFINITION +""", + experimental_directives_on_directive_definitions=True, + ) + + bar_directive = assert_directive(schema.get_directive("bar")) + assert bar_directive.deprecation_reason == "Use another directive" + def throws_on_unknown_types(): sdl = """ type Query { @@ -1186,28 +1083,110 @@ def rejects_invalid_ast(): build_ast_schema({}) # type: ignore assert str(exc_info.value) == "Must provide valid Document AST." - # This currently does not work because of how extend_schema is implemented - @mark.skip(reason="pickling of schemas is not yet supported") - def can_pickle_and_unpickle_big_schema( - big_schema_sdl, # noqa: F811 - ): # pragma: no cover - import pickle - - # create a schema from the kitchen sink SDL - schema = build_schema(big_schema_sdl, assume_valid_sdl=True) - # check that the schema can be pickled - # (particularly, there should be no recursion error, - # or errors because of trying to pickle lambdas or local functions) - dumped = pickle.dumps(schema) - # check that the pickle size is reasonable - assert len(dumped) < 50 * len(big_schema_sdl) - loaded = pickle.loads(dumped) - - # check that the un-pickled schema is still the same - assert loaded == schema - # check that pickling again creates the same result - dumped_again = pickle.dumps(schema) - assert dumped_again == dumped - - # check that printing the unpickled schema gives the same SDL - assert cycle_sdl(print_schema(schema)) == cycle_sdl(big_schema_sdl) + def describe_deepcopy_and_pickle(): # pragma: no cover + sdl = print_schema(star_wars_schema) + + def can_deep_copy_schema(): + schema = build_schema(sdl, assume_valid_sdl=True) + # create a deepcopy of the schema + copied = deepcopy(schema) + # check that printing the copied schema gives the same SDL + assert print_schema(copied) == sdl + + def can_pickle_and_unpickle_star_wars_schema(): + # create a schema from the star wars SDL + schema = build_schema(sdl, assume_valid_sdl=True) + # check that the schema can be pickled + # (particularly, there should be no recursion error, + # or errors because of trying to pickle lambdas or local functions) + dumped = pickle.dumps(schema) + + # check that the pickle size is reasonable + assert len(dumped) < 25 * len(sdl) + loaded = pickle.loads(dumped) + + # check that printing the unpickled schema gives the same SDL + assert print_schema(loaded) == sdl + + # check that pickling again creates the same result + dumped = pickle.dumps(schema) + assert len(dumped) < 25 * len(sdl) + loaded = pickle.loads(dumped) + assert print_schema(loaded) == sdl + + def can_deep_copy_pickled_schema(): + # create a schema from the star wars SDL + schema = build_schema(sdl, assume_valid_sdl=True) + # pickle and unpickle the schema + loaded = pickle.loads(pickle.dumps(schema)) + # create a deepcopy of the unpickled schema + copied = deepcopy(loaded) + # check that printing the copied schema gives the same SDL + assert print_schema(copied) == sdl + + @mark.slow + def describe_deepcopy_and_pickle_big(): # pragma: no cover + @mark.timeout(20 * timeout_factor) + def can_deep_copy_big_schema(big_schema_sdl): # noqa: F811 + # use our printing conventions + big_schema_sdl = cycle_sdl(big_schema_sdl) + + # create a schema from the big SDL + schema = build_schema(big_schema_sdl, assume_valid_sdl=True) + # create a deepcopy of the schema + copied = deepcopy(schema) + # check that printing the copied schema gives the same SDL + assert print_schema(copied) == big_schema_sdl + + @mark.timeout(60 * timeout_factor) + def can_pickle_and_unpickle_big_schema(big_schema_sdl): # noqa: F811 + # use our printing conventions + big_schema_sdl = cycle_sdl(big_schema_sdl) + + limit = sys.getrecursionlimit() + sys.setrecursionlimit(max(limit, 4000)) # needed for pickle + + try: + # create a schema from the big SDL + schema = build_schema(big_schema_sdl, assume_valid_sdl=True) + # check that the schema can be pickled + # (particularly, there should be no recursion error, + # or errors because of trying to pickle lambdas or local functions) + dumped = pickle.dumps(schema) + + # check that the pickle size is reasonable + assert len(dumped) < 25 * len(big_schema_sdl) + loaded = pickle.loads(dumped) + + # check that printing the unpickled schema gives the same SDL + assert print_schema(loaded) == big_schema_sdl + + # check that pickling again creates the same result + dumped = pickle.dumps(schema) + assert len(dumped) < 25 * len(big_schema_sdl) + loaded = pickle.loads(dumped) + assert print_schema(loaded) == big_schema_sdl + + finally: + sys.setrecursionlimit(limit) + + @mark.timeout(60 * timeout_factor) + def can_deep_copy_pickled_big_schema(big_schema_sdl): # noqa: F811 + # use our printing conventions + big_schema_sdl = cycle_sdl(big_schema_sdl) + + limit = sys.getrecursionlimit() + sys.setrecursionlimit(max(limit, 4000)) # needed for pickle + + try: + # create a schema from the big SDL + schema = build_schema(big_schema_sdl, assume_valid_sdl=True) + # pickle and unpickle the schema + loaded = pickle.loads(pickle.dumps(schema)) + # create a deepcopy of the unpickled schema + copied = deepcopy(loaded) + # check that printing the copied schema gives the same SDL + assert print_schema(copied) == big_schema_sdl + + finally: + sys.setrecursionlimit(limit) diff --git a/tests/utilities/test_build_client_schema.py b/tests/utilities/test_build_client_schema.py index b566ba1b..2178ac41 100644 --- a/tests/utilities/test_build_client_schema.py +++ b/tests/utilities/test_build_client_schema.py @@ -1,11 +1,11 @@ from typing import cast -from pytest import raises - from graphql import graphql_sync +from graphql.language import DirectiveLocation from graphql.type import ( GraphQLArgument, GraphQLBoolean, + GraphQLDirective, GraphQLEnumType, GraphQLEnumValue, GraphQLField, @@ -18,8 +18,8 @@ assert_enum_type, ) from graphql.utilities import ( - build_schema, build_client_schema, + build_schema, introspection_from_schema, print_schema, ) @@ -31,6 +31,7 @@ IntrospectionType, IntrospectionUnionType, ) +from pytest import raises from ..utils import dedent @@ -57,8 +58,7 @@ def cycle_introspection(sdl_string: str): def describe_type_system_build_schema_from_introspection(): def builds_a_simple_schema(): - sdl = dedent( - ''' + sdl = dedent(''' """Simple schema""" schema { query: Simple @@ -69,19 +69,16 @@ def builds_a_simple_schema(): """This is a string field""" string: String } - ''' - ) + ''') assert cycle_introspection(sdl) == sdl def builds_a_schema_without_the_query_type(): - sdl = dedent( - """ + sdl = dedent(""" type Query { foo: String } - """ - ) + """) schema = build_schema(sdl) introspection = introspection_from_schema(schema) @@ -92,8 +89,7 @@ def builds_a_schema_without_the_query_type(): assert print_schema(client_schema) == sdl def builds_a_simple_schema_with_all_operation_types(): - sdl = dedent( - ''' + sdl = dedent(''' schema { query: QueryType mutation: MutationType @@ -117,14 +113,12 @@ def builds_a_simple_schema_with_all_operation_types(): """This is a string field""" string: String } - ''' - ) + ''') assert cycle_introspection(sdl) == sdl def uses_built_in_scalars_when_possible(): - sdl = dedent( - """ + sdl = dedent(""" scalar CustomScalar type Query { @@ -135,8 +129,7 @@ def uses_built_in_scalars_when_possible(): id: ID custom: CustomScalar } - """ - ) + """) assert cycle_introspection(sdl) == sdl @@ -156,13 +149,11 @@ def uses_built_in_scalars_when_possible(): assert client_schema.get_type("CustomScalar") is not custom_scalar def includes_standard_types_only_if_they_are_used(): - schema = build_schema( - """ + schema = build_schema(""" type Query { foo: String } - """ - ) + """) introspection = introspection_from_schema(schema) client_schema = build_client_schema(introspection) @@ -171,8 +162,7 @@ def includes_standard_types_only_if_they_are_used(): assert client_schema.get_type("ID") is None def builds_a_schema_with_a_recursive_type_reference(): - sdl = dedent( - """ + sdl = dedent(""" schema { query: Recur } @@ -180,14 +170,12 @@ def builds_a_schema_with_a_recursive_type_reference(): type Recur { recur: Recur } - """ - ) + """) assert cycle_introspection(sdl) == sdl def builds_a_schema_with_a_circular_type_reference(): - sdl = dedent( - """ + sdl = dedent(""" type Dog { bestFriend: Human } @@ -200,14 +188,12 @@ def builds_a_schema_with_a_circular_type_reference(): dog: Dog human: Human } - """ - ) + """) assert cycle_introspection(sdl) == sdl def builds_a_schema_with_an_interface(): - sdl = dedent( - ''' + sdl = dedent(''' type Dog implements Friendly { bestFriend: Friendly } @@ -224,14 +210,12 @@ def builds_a_schema_with_an_interface(): type Query { friendly: Friendly } - ''' - ) + ''') assert cycle_introspection(sdl) == sdl def builds_a_schema_with_an_interface_hierarchy(): - sdl = dedent( - ''' + sdl = dedent(''' type Dog implements Friendly & Named { bestFriend: Friendly name: String @@ -255,14 +239,12 @@ def builds_a_schema_with_an_interface_hierarchy(): type Query { friendly: Friendly } - ''' - ) + ''') assert cycle_introspection(sdl) == sdl def builds_a_schema_with_an_implicit_interface(): - sdl = dedent( - ''' + sdl = dedent(''' type Dog implements Friendly { bestFriend: Friendly } @@ -275,14 +257,12 @@ def builds_a_schema_with_an_implicit_interface(): type Query { dog: Dog } - ''' - ) + ''') assert cycle_introspection(sdl) == sdl def builds_a_schema_with_a_union(): - sdl = dedent( - """ + sdl = dedent(""" type Dog { bestFriend: Friendly } @@ -296,14 +276,12 @@ def builds_a_schema_with_a_union(): type Query { friendly: Friendly } - """ - ) + """) assert cycle_introspection(sdl) == sdl def builds_a_schema_with_complex_field_values(): - sdl = dedent( - """ + sdl = dedent(""" type Query { string: String listOfString: [String] @@ -311,14 +289,12 @@ def builds_a_schema_with_complex_field_values(): nonNullListOfString: [String]! nonNullListOfNonNullString: [String!]! } - """ - ) + """) assert cycle_introspection(sdl) == sdl def builds_a_schema_with_field_arguments(): - sdl = dedent( - ''' + sdl = dedent(''' type Query { """A field with a single arg""" one( @@ -335,21 +311,18 @@ def builds_a_schema_with_field_arguments(): requiredArg: Boolean! ): String } - ''' - ) + ''') assert cycle_introspection(sdl) == sdl def builds_a_schema_with_default_value_on_custom_scalar_field(): - sdl = dedent( - """ + sdl = dedent(""" scalar CustomScalar type Query { testField(testArg: CustomScalar = "default"): String } - """ - ) + """) assert cycle_introspection(sdl) == sdl @@ -422,8 +395,7 @@ def builds_a_schema_with_an_enum(): } def builds_a_schema_with_an_input_object(): - sdl = dedent( - ''' + sdl = dedent(''' """An input address""" input Address { """What street is this address?""" @@ -443,14 +415,12 @@ def builds_a_schema_with_an_input_object(): address: Address ): String } - ''' - ) + ''') assert cycle_introspection(sdl) == sdl def builds_a_schema_with_field_arguments_with_default_values(): - sdl = dedent( - """ + sdl = dedent(""" input Geo { lat: Float lon: Float @@ -463,33 +433,51 @@ def builds_a_schema_with_field_arguments_with_default_values(): defaultNull(intArg: Int = null): String noDefault(intArg: Int): String } - """ - ) + """) assert cycle_introspection(sdl) == sdl def builds_a_schema_with_custom_directives(): - sdl = dedent( - ''' + sdl = dedent(''' """This is a custom directive""" directive @customDirective repeatable on FIELD type Query { string: String } - ''' - ) + ''') assert cycle_introspection(sdl) == sdl + def builds_a_schema_with_deprecated_directives(): + schema = GraphQLSchema( + query=GraphQLObjectType( + name="Query", + fields={"string": GraphQLField(GraphQLString)}, + ), + directives=[ + GraphQLDirective( + name="someDirective", + locations=[DirectiveLocation.QUERY], + deprecation_reason="Use another directive", + ), + ], + ) + introspection = introspection_from_schema(schema) + + client_schema = build_client_schema(introspection) + + directive = client_schema.get_directive("someDirective") + assert directive is not None + assert directive.name == "someDirective" + assert directive.deprecation_reason == "Use another directive" + def builds_a_schema_without_directives(): - sdl = dedent( - """ + sdl = dedent(""" type Query { foo: String } - """ - ) + """) schema = build_schema(sdl) introspection = introspection_from_schema(schema) @@ -502,8 +490,7 @@ def builds_a_schema_without_directives(): assert print_schema(client_schema) == sdl def builds_a_schema_aware_of_deprecation(): - sdl = dedent( - ''' + sdl = dedent(''' directive @someDirective( """This is a shiny new argument""" shinyArg: SomeInputObject @@ -553,14 +540,12 @@ def builds_a_schema_aware_of_deprecation(): oldArg: String @deprecated(reason: "Use shinyArg") ): String } - ''' # noqa: E501 - ) + ''') # noqa: E501 assert cycle_introspection(sdl) == sdl def builds_a_schema_with_empty_deprecation_reasons(): - sdl = dedent( - """ + sdl = dedent(""" directive @someDirective(someArg: SomeInputObject @deprecated(reason: "")) on QUERY type Query { @@ -574,34 +559,43 @@ def builds_a_schema_with_empty_deprecation_reasons(): enum SomeEnum { SOME_VALUE @deprecated(reason: "") } - """ # noqa: E501 - ) + """) # noqa: E501 assert cycle_introspection(sdl) == sdl def builds_a_schema_with_specified_by_url(): - sdl = dedent( - """ + sdl = dedent(""" scalar Foo @specifiedBy(url: "https://example.com/foo_spec") type Query { foo: Foo } - """ - ) + """) + + assert cycle_introspection(sdl) == sdl + + def builds_a_schema_with_one_of_directive(): + sdl = dedent(""" + type Query { + someField(someArg: SomeInputObject): String + } + + input SomeInputObject @oneOf { + someInputField1: String + someInputField2: String + } + """) assert cycle_introspection(sdl) == sdl def can_use_client_schema_for_limited_execution(): - schema = build_schema( - """ + schema = build_schema(""" scalar CustomScalar type Query { foo(custom1: CustomScalar, custom2: CustomScalar): String } - """ - ) + """) introspection = introspection_from_schema(schema) client_schema = build_client_schema(introspection) @@ -628,8 +622,7 @@ def can_build_invalid_schema(): assert client_schema.to_kwargs()["assume_valid"] is True def describe_throws_when_given_invalid_introspection(): - dummy_schema = build_schema( - """ + dummy_schema = build_schema(""" type Query { foo(bar: String): String } @@ -647,8 +640,7 @@ def describe_throws_when_given_invalid_introspection(): } directive @SomeDirective on QUERY - """ - ) + """) def throws_when_introspection_is_missing_schema_property(): with raises(TypeError) as exc_info: @@ -690,13 +682,11 @@ def throws_when_referenced_unknown_type(): ) def throws_when_missing_definition_for_one_of_the_standard_scalars(): - schema = build_schema( - """ + schema = build_schema(""" type Query { foo: Float } - """ - ) + """) introspection = introspection_from_schema(schema) introspection["__schema"]["types"] = [ type_ @@ -722,7 +712,7 @@ def throws_when_type_reference_is_missing_name(): with raises(TypeError) as exc_info: build_client_schema(introspection) - assert str(exc_info.value) == "Unknown type reference: {}." + assert str(exc_info.value) == "Unknown type reference: {'kind': 'OBJECT'}." def throws_when_missing_kind(): introspection = introspection_from_schema(dummy_schema) @@ -991,14 +981,12 @@ def throws_when_missing_directive_args(): build_client_schema(introspection) def describe_very_deep_decorators_are_not_supported(): - def fails_on_very_deep_lists_more_than_7_levels(): - schema = build_schema( - """ + def fails_on_very_deep_lists_more_than_8_levels(): + schema = build_schema(""" type Query { - foo: [[[[[[[[String]]]]]]]] + foo: [[[[[[[[[[String]]]]]]]]]] } - """ - ) + """) introspection = introspection_from_schema(schema) @@ -1010,14 +998,12 @@ def fails_on_very_deep_lists_more_than_7_levels(): " Decorated type deeper than introspection query." ) - def fails_on_a_very_deep_non_null_more_than_7_levels(): - schema = build_schema( - """ + def fails_on_a_very_deep_non_null_more_than_8_levels(): + schema = build_schema(""" type Query { - foo: [[[[String!]!]!]!] + foo: [[[[[String!]!]!]!]!] } - """ - ) + """) introspection = introspection_from_schema(schema) @@ -1029,15 +1015,13 @@ def fails_on_a_very_deep_non_null_more_than_7_levels(): " Decorated type deeper than introspection query." ) - def succeeds_on_deep_types_less_or_equal_7_levels(): - # e.g., fully non-null 3D matrix - sdl = dedent( - """ + def succeeds_on_deep_types_less_or_equal_8_levels(): + # e.g., fully non-null 4D matrix + sdl = dedent(""" type Query { - foo: [[[String!]!]!]! + foo: [[[[String!]!]!]!]! } - """ - ) + """) assert cycle_introspection(sdl) == sdl diff --git a/tests/utilities/test_coerce_input_value.py b/tests/utilities/test_coerce_input_value.py index f34e8564..2cd023b1 100644 --- a/tests/utilities/test_coerce_input_value.py +++ b/tests/utilities/test_coerce_input_value.py @@ -148,11 +148,19 @@ def returns_an_error_for_incorrect_value_type(): ] def describe_for_graphql_input_object(): + DeepObject = GraphQLInputObjectType( + "DeepObject", + { + "foo": GraphQLInputField(GraphQLNonNull(GraphQLInt)), + "bar": GraphQLInputField(GraphQLInt), + }, + ) TestInputObject = GraphQLInputObjectType( "TestInputObject", { "foo": GraphQLInputField(GraphQLNonNull(GraphQLInt)), "bar": GraphQLInputField(GraphQLInt), + "deepObject": GraphQLInputField(DeepObject), }, ) @@ -222,6 +230,26 @@ def returns_error_for_a_misspelled_field(): ) ] + def returns_an_error_for_an_array_type(): + result = _coerce_value([{"foo": 1}, {"bar": 1}], TestInputObject) + assert expect_errors(result) == [ + ( + "Expected type 'TestInputObject' to be a mapping.", + [], + [{"foo": 1}, {"bar": 1}], + ) + ] + + def returns_an_error_for_an_array_type_on_a_nested_field(): + result = _coerce_value({"foo": 1, "deepObject": [1, 2, 3]}, TestInputObject) + assert expect_errors(result) == [ + ( + "Expected type 'DeepObject' to be a mapping.", + ["deepObject"], + [1, 2, 3], + ) + ] + def transforms_names_using_out_name(): # This is an extension of GraphQL.js. ComplexInputObject = GraphQLInputObjectType( @@ -249,6 +277,99 @@ def transforms_values_with_out_type(): result = _coerce_value({"real": 1, "imag": 2}, ComplexInputObject) assert expect_value(result) == 1 + 2j + def describe_for_graphql_input_object_that_is_one_of(): + TestInputObject = GraphQLInputObjectType( + "TestInputObject", + { + "foo": GraphQLInputField(GraphQLInt), + "bar": GraphQLInputField(GraphQLInt), + }, + is_one_of=True, + ) + + def returns_no_error_for_a_valid_input(): + result = _coerce_value({"foo": 123}, TestInputObject) + assert expect_value(result) == {"foo": 123} + + def returns_an_error_if_more_than_one_field_is_specified(): + result = _coerce_value({"foo": 123, "bar": None}, TestInputObject) + assert expect_errors(result) == [ + ( + "Exactly one key must be specified" + " for OneOf type 'TestInputObject'.", + [], + {"foo": 123, "bar": None}, + ) + ] + + def returns_an_error_if_the_one_field_is_null(): + result = _coerce_value({"bar": None}, TestInputObject) + assert expect_errors(result) == [ + ( + "Field 'bar' must be non-null.", + ["bar"], + None, + ) + ] + + def returns_an_error_for_an_invalid_field(): + result = _coerce_value({"foo": nan}, TestInputObject) + assert expect_errors(result) == [ + ( + "Int cannot represent non-integer value: nan", + ["foo"], + nan, + ) + ] + + def returns_multiple_errors_for_multiple_invalid_fields(): + result = _coerce_value({"foo": "abc", "bar": "def"}, TestInputObject) + assert expect_errors(result) == [ + ( + "Int cannot represent non-integer value: 'abc'", + ["foo"], + "abc", + ), + ( + "Int cannot represent non-integer value: 'def'", + ["bar"], + "def", + ), + ( + "Exactly one key must be specified" + " for OneOf type 'TestInputObject'.", + [], + {"foo": "abc", "bar": "def"}, + ), + ] + + def returns_an_error_for_an_unknown_field(): + result = _coerce_value({"foo": 123, "unknownField": 123}, TestInputObject) + assert expect_errors(result) == [ + ( + "Field 'unknownField' is not defined by type 'TestInputObject'.", + [], + {"foo": 123, "unknownField": 123}, + ) + ] + + def returns_an_error_for_a_misspelled_field(): + result = _coerce_value({"bart": 123}, TestInputObject) + assert expect_errors(result) == [ + ( + "Field 'bart' is not defined by type 'TestInputObject'." + " Did you mean 'bar'?", + [], + {"bart": 123}, + ), + ( + "Exactly one key must be specified" + " for OneOf type 'TestInputObject'.", + [], + {"bart": 123}, + ), + ] + def describe_for_graphql_input_object_with_default_value(): def _get_test_input_object(default_value): return GraphQLInputObjectType( diff --git a/tests/utilities/test_concat_ast.py b/tests/utilities/test_concat_ast.py index 7d25d0b3..b432619d 100644 --- a/tests/utilities/test_concat_ast.py +++ b/tests/utilities/test_concat_ast.py @@ -6,26 +6,21 @@ def describe_concat_ast(): def concatenates_two_asts_together(): - source_a = Source( - """ + source_a = Source(""" { a, b, ... Frag } - """ - ) + """) - source_b = Source( - """ + source_b = Source(""" fragment Frag on T { c } - """ - ) + """) ast_a = parse(source_a) ast_b = parse(source_b) ast_c = concat_ast([ast_a, ast_b]) - assert print_ast(ast_c) == dedent( - """ + assert print_ast(ast_c) == dedent(""" { a b @@ -35,5 +30,4 @@ def concatenates_two_asts_together(): fragment Frag on T { c } - """ - ) + """) diff --git a/tests/utilities/test_extend_schema.py b/tests/utilities/test_extend_schema.py index 8a844abd..75d52b72 100644 --- a/tests/utilities/test_extend_schema.py +++ b/tests/utilities/test_extend_schema.py @@ -3,10 +3,12 @@ from pytest import raises from graphql import graphql_sync +from graphql.error import GraphQLSyntaxError from graphql.language import parse, print_ast from graphql.type import ( GraphQLArgument, GraphQLBoolean, + GraphQLDirective, GraphQLEnumValue, GraphQLField, GraphQLFloat, @@ -26,6 +28,7 @@ validate_schema, ) from graphql.utilities import ( + build_ast_schema, build_schema, concat_ast, extend_schema, @@ -44,6 +47,7 @@ ] TypeWithExtensionAstNodes = Union[ + GraphQLDirective, GraphQLNamedType, GraphQLSchema, ] @@ -86,13 +90,11 @@ def returns_the_original_schema_when_there_are_no_type_definitions(): def can_be_used_for_limited_execution(): schema = build_schema("type Query") - extend_ast = parse( - """ + extend_ast = parse(""" extend type Query { newField: String } - """ - ) + """) extended_schema = extend_schema(schema, extend_ast) result = graphql_sync( @@ -101,8 +103,7 @@ def can_be_used_for_limited_execution(): assert result == ({"newField": "123"}, None) def extends_objects_by_adding_new_fields(): - schema = build_schema( - ''' + schema = build_schema(''' type Query { someObject: SomeObject } @@ -121,24 +122,20 @@ def extends_objects_by_adding_new_fields(): interface AnotherInterface { self: SomeObject } - ''' - ) - extension_sdl = dedent( - ''' + ''') + extension_sdl = dedent(''' extend type SomeObject { """New field description.""" newField(arg: Boolean): String } - ''' - ) + ''') extended_schema = extend_schema(schema, parse(extension_sdl)) assert validate_schema(extended_schema) == [] expect_schema_changes( schema, extended_schema, - dedent( - ''' + dedent(''' type SomeObject implements AnotherInterface & SomeInterface { self: SomeObject tree: [SomeObject]! @@ -147,8 +144,7 @@ def extends_objects_by_adding_new_fields(): """New field description.""" newField(arg: Boolean): String } - ''' - ), + '''), ) def extends_objects_with_standard_type_fields(): @@ -161,13 +157,11 @@ def extends_objects_with_standard_type_fields(): assert schema.get_type("Boolean") is GraphQLBoolean assert schema.get_type("ID") is None - extend_ast = parse( - """ + extend_ast = parse(""" extend type Query { bool: Boolean } - """ - ) + """) extended_schema = extend_schema(schema, extend_ast) assert validate_schema(extended_schema) == [] @@ -177,15 +171,13 @@ def extends_objects_with_standard_type_fields(): assert extended_schema.get_type("Boolean") is GraphQLBoolean assert extended_schema.get_type("ID") is None - extend_twice_ast = parse( - """ + extend_twice_ast = parse(""" extend type Query { int: Int float: Float id: ID } - """ - ) + """) extended_twice_schema = extend_schema(schema, extend_twice_ast) assert validate_schema(extended_twice_schema) == [] @@ -196,8 +188,7 @@ def extends_objects_with_standard_type_fields(): assert extended_twice_schema.get_type("ID") is GraphQLID def extends_enums_by_adding_new_values(): - schema = build_schema( - ''' + schema = build_schema(''' type Query { someEnum(arg: SomeEnum): SomeEnum } @@ -208,37 +199,31 @@ def extends_enums_by_adding_new_values(): """Old value description.""" OLD_VALUE } - ''' - ) - extend_ast = parse( - ''' + ''') + extend_ast = parse(''' extend enum SomeEnum { """New value description.""" NEW_VALUE } - ''' - ) + ''') extended_schema = extend_schema(schema, extend_ast) assert validate_schema(extended_schema) == [] expect_schema_changes( schema, extended_schema, - dedent( - ''' + dedent(''' enum SomeEnum { """Old value description.""" OLD_VALUE """New value description.""" NEW_VALUE } - ''' - ), + '''), ) def extends_unions_by_adding_new_types(): - schema = build_schema( - """ + schema = build_schema(""" type Query { someUnion: SomeUnion } @@ -248,37 +233,28 @@ def extends_unions_by_adding_new_types(): type Foo { foo: String } type Biz { biz: String } type Bar { bar: String } - """ - ) - extend_ast = parse( - """ + """) + extend_ast = parse(""" extend union SomeUnion = Bar - """ - ) + """) extended_schema = extend_schema(schema, extend_ast) assert validate_schema(extended_schema) == [] expect_schema_changes( schema, extended_schema, - dedent( - """ + dedent(""" union SomeUnion = Foo | Biz | Bar - """ - ), + """), ) def allows_extension_of_union_by_adding_itself(): - schema = build_schema( - """ + schema = build_schema(""" union SomeUnion - """ - ) - extend_ast = parse( - """ + """) + extend_ast = parse(""" extend union SomeUnion = SomeUnion - """ - ) + """) # invalid schema cannot be built with Python with raises(TypeError) as exc_info: extend_schema(schema, extend_ast) @@ -288,8 +264,7 @@ def allows_extension_of_union_by_adding_itself(): ) def extends_inputs_by_adding_new_fields(): - schema = build_schema( - ''' + schema = build_schema(''' type Query { someInput(arg: SomeInput): String } @@ -300,37 +275,31 @@ def extends_inputs_by_adding_new_fields(): """Old field description.""" oldField: String } - ''' - ) - extend_ast = parse( - ''' + ''') + extend_ast = parse(''' extend input SomeInput { """New field description.""" newField: String } - ''' - ) + ''') extended_schema = extend_schema(schema, extend_ast) assert validate_schema(extended_schema) == [] expect_schema_changes( schema, extended_schema, - dedent( - ''' + dedent(''' input SomeInput { """Old field description.""" oldField: String """New field description.""" newField: String } - ''' - ), + '''), ) def extends_scalars_by_adding_new_directives(): - schema = build_schema( - """ + schema = build_schema(""" type Query { someScalar(arg: SomeScalar): SomeScalar } @@ -342,13 +311,10 @@ def extends_scalars_by_adding_new_directives(): } scalar SomeScalar - """ - ) - extension_sdl = dedent( - """ + """) + extension_sdl = dedent(""" extend scalar SomeScalar @foo - """ - ) + """) extended_schema = extend_schema(schema, parse(extension_sdl)) some_scalar = assert_scalar_type(extended_schema.get_type("SomeScalar")) @@ -356,8 +322,7 @@ def extends_scalars_by_adding_new_directives(): expect_extension_ast_nodes(some_scalar, extension_sdl) def extends_scalars_by_adding_specified_by_directive(): - schema = build_schema( - """ + schema = build_schema(""" type Query { foo: Foo } @@ -365,15 +330,12 @@ def extends_scalars_by_adding_specified_by_directive(): scalar Foo directive @foo on SCALAR - """ - ) - extension_sdl = dedent( - """ + """) + extension_sdl = dedent(""" extend scalar Foo @foo extend scalar Foo @specifiedBy(url: "https://example.com/foo_spec") - """ - ) + """) extended_schema = extend_schema(schema, parse(extension_sdl)) foo = assert_scalar_type(extended_schema.get_type("Foo")) @@ -384,8 +346,7 @@ def extends_scalars_by_adding_specified_by_directive(): expect_extension_ast_nodes(foo, extension_sdl) def correctly_assigns_ast_nodes_to_new_and_extended_types(): - schema = build_schema( - """ + schema = build_schema(""" type Query scalar SomeScalar @@ -396,10 +357,8 @@ def correctly_assigns_ast_nodes_to_new_and_extended_types(): interface SomeInterface directive @foo on SCALAR - """ - ) - first_extension_ast = parse( - """ + """) + first_extension_ast = parse(""" extend type Query { newField(testArg: TestInput): TestEnum } @@ -427,12 +386,10 @@ def correctly_assigns_ast_nodes_to_new_and_extended_types(): input TestInput { testInputField: TestEnum } - """ - ) + """) extended_schema = extend_schema(schema, first_extension_ast) - second_extension_ast = parse( - """ + second_extension_ast = parse(""" extend type Query { oneMoreNewField: TestUnion } @@ -464,8 +421,7 @@ def correctly_assigns_ast_nodes_to_new_and_extended_types(): } directive @test(arg: Int) repeatable on FIELD | SCALAR - """ - ) + """) extended_twice_schema = extend_schema(extended_schema, second_extension_ast) extend_in_one_go_schema = extend_schema( @@ -559,8 +515,7 @@ def correctly_assigns_ast_nodes_to_new_and_extended_types(): def builds_types_with_deprecated_fields_and_values(): schema = GraphQLSchema() - extend_ast = parse( - """ + extend_ast = parse(""" type SomeObject { deprecatedField: String @deprecated(reason: "not used anymore") } @@ -568,8 +523,7 @@ def builds_types_with_deprecated_fields_and_values(): enum SomeEnum { DEPRECATED_VALUE @deprecated(reason: "do not use") } - """ - ) + """) extended_schema = extend_schema(schema, extend_ast) some_type = assert_object_type(extended_schema.get_type("SomeObject")) @@ -582,13 +536,11 @@ def builds_types_with_deprecated_fields_and_values(): def extends_objects_with_deprecated_fields(): schema = build_schema("type SomeObject") - extend_ast = parse( - """ + extend_ast = parse(""" extend type SomeObject { deprecatedField: String @deprecated(reason: "not used anymore") } - """ - ) + """) extended_schema = extend_schema(schema, extend_ast) some_type = assert_object_type(extended_schema.get_type("SomeObject")) @@ -597,13 +549,11 @@ def extends_objects_with_deprecated_fields(): def extend_enums_with_deprecated_values(): schema = build_schema("enum SomeEnum") - extend_ast = parse( - """ + extend_ast = parse(""" extend enum SomeEnum { DEPRECATED_VALUE @deprecated(reason: "do not use") } - """ - ) + """) extended_schema = extend_schema(schema, extend_ast) some_enum = assert_enum_type(extended_schema.get_type("SomeEnum")) @@ -611,15 +561,12 @@ def extend_enums_with_deprecated_values(): assert deprecated_value.deprecation_reason == "do not use" def adds_new_unused_types(): - schema = build_schema( - """ + schema = build_schema(""" type Query { dummy: String } - """ - ) - extension_sdl = dedent( - """ + """) + extension_sdl = dedent(""" type DummyUnionMember { someField: String } @@ -641,25 +588,21 @@ def adds_new_unused_types(): } union UnusedUnion = DummyUnionMember - """ - ) + """) extended_schema = extend_schema(schema, parse(extension_sdl)) assert validate_schema(extended_schema) == [] expect_schema_changes(schema, extended_schema, extension_sdl) def extends_objects_by_adding_new_fields_with_arguments(): - schema = build_schema( - """ + schema = build_schema(""" type SomeObject type Query { someObject: SomeObject } - """ - ) - extend_ast = parse( - """ + """) + extend_ast = parse(""" input NewInputObj { field1: Int field2: [Float] @@ -669,16 +612,14 @@ def extends_objects_by_adding_new_fields_with_arguments(): extend type SomeObject { newField(arg1: String, arg2: NewInputObj!): String } - """ - ) + """) extended_schema = extend_schema(schema, extend_ast) assert validate_schema(extended_schema) == [] expect_schema_changes( schema, extended_schema, - dedent( - """ + dedent(""" type SomeObject { newField(arg1: String, arg2: NewInputObj!): String } @@ -688,46 +629,38 @@ def extends_objects_by_adding_new_fields_with_arguments(): field2: [Float] field3: String! } - """ - ), + """), ) def extends_objects_by_adding_new_fields_with_existing_types(): - schema = build_schema( - """ + schema = build_schema(""" type Query { someObject: SomeObject } type SomeObject enum SomeEnum { VALUE } - """ - ) - extend_ast = parse( - """ + """) + extend_ast = parse(""" extend type SomeObject { newField(arg1: SomeEnum!): SomeEnum } - """ - ) + """) extended_schema = extend_schema(schema, extend_ast) assert validate_schema(extended_schema) == [] expect_schema_changes( schema, extended_schema, - dedent( - """ + dedent(""" type SomeObject { newField(arg1: SomeEnum!): SomeEnum } - """ - ), + """), ) def extends_objects_by_adding_implemented_interfaces(): - schema = build_schema( - """ + schema = build_schema(""" type Query { someObject: SomeObject } @@ -739,31 +672,25 @@ def extends_objects_by_adding_implemented_interfaces(): interface SomeInterface { foo: String } - """ - ) - extend_ast = parse( - """ + """) + extend_ast = parse(""" extend type SomeObject implements SomeInterface - """ - ) + """) extended_schema = extend_schema(schema, extend_ast) assert validate_schema(extended_schema) == [] expect_schema_changes( schema, extended_schema, - dedent( - """ + dedent(""" type SomeObject implements SomeInterface { foo: String } - """ - ), + """), ) def extends_objects_by_including_new_types(): - schema = build_schema( - """ + schema = build_schema(""" type Query { someObject: SomeObject } @@ -771,8 +698,7 @@ def extends_objects_by_including_new_types(): type SomeObject { oldField: String } - """ - ) + """) new_types_sdl = """ enum NewEnum { VALUE @@ -790,9 +716,7 @@ def extends_objects_by_including_new_types(): union NewUnion = NewObject """ - extend_ast = parse( - new_types_sdl - + """ + extend_ast = parse(new_types_sdl + """ extend type SomeObject { newObject: NewObject newInterface: NewInterface @@ -801,16 +725,14 @@ def extends_objects_by_including_new_types(): newEnum: NewEnum newTree: [SomeObject]! } - """ - ) + """) extended_schema = extend_schema(schema, extend_ast) assert validate_schema(extended_schema) == [] expect_schema_changes( schema, extended_schema, - dedent( - """ + dedent(""" type SomeObject { oldField: String newObject: NewObject @@ -819,14 +741,11 @@ def extends_objects_by_including_new_types(): newScalar: NewScalar newEnum: NewEnum newTree: [SomeObject]! - }\n""" - + new_types_sdl - ), + }\n""" + new_types_sdl), ) def extends_objects_by_adding_implemented_new_interfaces(): - schema = build_schema( - """ + schema = build_schema(""" type Query { someObject: SomeObject } @@ -838,10 +757,8 @@ def extends_objects_by_adding_implemented_new_interfaces(): interface OldInterface { oldField: String } - """ - ) - extend_ast = parse( - """ + """) + extend_ast = parse(""" extend type SomeObject implements NewInterface { newField: String } @@ -849,16 +766,14 @@ def extends_objects_by_adding_implemented_new_interfaces(): interface NewInterface { newField: String } - """ - ) + """) extended_schema = extend_schema(schema, extend_ast) assert validate_schema(extended_schema) == [] expect_schema_changes( schema, extended_schema, - dedent( - """ + dedent(""" type SomeObject implements OldInterface & NewInterface { oldField: String newField: String @@ -867,13 +782,11 @@ def extends_objects_by_adding_implemented_new_interfaces(): interface NewInterface { newField: String } - """ - ), + """), ) def extends_different_types_multiple_times(): - schema = build_schema( - """ + schema = build_schema(""" type Query { someScalar: SomeScalar someObject(someInput: SomeInput): SomeObject @@ -901,10 +814,8 @@ def extends_different_types_multiple_times(): input SomeInput { oldField: String } - """ - ) - new_types_sdl = dedent( - """ + """) + new_types_sdl = dedent(""" scalar NewScalar scalar AnotherNewScalar @@ -924,13 +835,11 @@ def extends_different_types_multiple_times(): interface AnotherNewInterface { anotherNewField: String } - """ - ) + """) schema_with_new_types = extend_schema(schema, parse(new_types_sdl)) expect_schema_changes(schema, schema_with_new_types, new_types_sdl) - extend_ast = parse( - """ + extend_ast = parse(""" extend scalar SomeScalar @specifiedBy(url: "http://example.com/foo_spec") extend type SomeObject implements NewInterface { @@ -960,16 +869,14 @@ def extends_different_types_multiple_times(): extend input SomeInput { anotherNewField: String } - """ - ) + """) extended_schema = extend_schema(schema_with_new_types, extend_ast) assert validate_schema(extended_schema) == [] expect_schema_changes( schema, extended_schema, - dedent( - """ + dedent(""" scalar SomeScalar @specifiedBy(url: "http://example.com/foo_spec") type SomeObject implements SomeInterface & NewInterface & AnotherNewInterface { @@ -992,15 +899,11 @@ def extends_different_types_multiple_times(): anotherNewField: String } - """ # noqa: E501 - ) - + "\n\n" - + new_types_sdl, + """) + "\n\n" + new_types_sdl, # noqa: E501 ) def extends_interfaces_by_adding_new_fields(): - schema = build_schema( - """ + schema = build_schema(""" interface SomeInterface { oldField: String } @@ -1016,10 +919,8 @@ def extends_interfaces_by_adding_new_fields(): type Query { someInterface: SomeInterface } - """ - ) - extend_ast = parse( - """ + """) + extend_ast = parse(""" extend interface SomeInterface { newField: String } @@ -1031,16 +932,14 @@ def extends_interfaces_by_adding_new_fields(): extend type SomeObject { newField: String } - """ - ) + """) extended_schema = extend_schema(schema, extend_ast) assert validate_schema(extended_schema) == [] expect_schema_changes( schema, extended_schema, - dedent( - """ + dedent(""" interface SomeInterface { oldField: String newField: String @@ -1055,13 +954,11 @@ def extends_interfaces_by_adding_new_fields(): oldField: String newField: String } - """ - ), + """), ) def extends_interfaces_by_adding_new_implemented_interfaces(): - schema = build_schema( - """ + schema = build_schema(""" interface SomeInterface { oldField: String } @@ -1077,10 +974,8 @@ def extends_interfaces_by_adding_new_implemented_interfaces(): type Query { someInterface: SomeInterface } - """ - ) - extend_ast = parse( - """ + """) + extend_ast = parse(""" interface NewInterface { newField: String } @@ -1092,16 +987,14 @@ def extends_interfaces_by_adding_new_implemented_interfaces(): extend type SomeObject implements NewInterface { newField: String } - """ - ) + """) extended_schema = extend_schema(schema, extend_ast) assert validate_schema(extended_schema) == [] expect_schema_changes( schema, extended_schema, - dedent( - """ + dedent(""" interface AnotherInterface implements SomeInterface & NewInterface { oldField: String newField: String @@ -1115,13 +1008,11 @@ def extends_interfaces_by_adding_new_implemented_interfaces(): interface NewInterface { newField: String } - """ # noqa: E501 - ), + """), # noqa: E501 ) def allows_extension_of_interface_with_missing_object_fields(): - schema = build_schema( - """ + schema = build_schema(""" type Query { someInterface: SomeInterface } @@ -1133,34 +1024,28 @@ def allows_extension_of_interface_with_missing_object_fields(): interface SomeInterface { oldField: SomeInterface } - """ - ) - extend_ast = parse( - """ + """) + extend_ast = parse(""" extend interface SomeInterface { newField: String } - """ - ) + """) extended_schema = extend_schema(schema, extend_ast) assert validate_schema(extended_schema) expect_schema_changes( schema, extended_schema, - dedent( - """ + dedent(""" interface SomeInterface { oldField: SomeInterface newField: String } - """ - ), + """), ) def extends_interfaces_multiple_times(): - schema = build_schema( - """ + schema = build_schema(""" type Query { someInterface: SomeInterface } @@ -1168,10 +1053,8 @@ def extends_interfaces_multiple_times(): interface SomeInterface { some: SomeInterface } - """ - ) - extend_ast = parse( - """ + """) + extend_ast = parse(""" extend interface SomeInterface { newFieldA: Int } @@ -1179,28 +1062,24 @@ def extends_interfaces_multiple_times(): extend interface SomeInterface { newFieldB(test: Boolean): String } - """ - ) + """) extended_schema = extend_schema(schema, extend_ast) assert validate_schema(extended_schema) == [] expect_schema_changes( schema, extended_schema, - dedent( - """ + dedent(""" interface SomeInterface { some: SomeInterface newFieldA: Int newFieldB(test: Boolean): String } - """ - ), + """), ) def may_extend_mutations_and_subscriptions(): - mutation_schema = build_schema( - """ + mutation_schema = build_schema(""" type Query { queryField: String } @@ -1212,10 +1091,8 @@ def may_extend_mutations_and_subscriptions(): type Subscription { subscriptionField: String } - """ - ) - ast = parse( - """ + """) + ast = parse(""" extend type Query { newQueryField: Int } @@ -1227,14 +1104,12 @@ def may_extend_mutations_and_subscriptions(): extend type Subscription { newSubscriptionField: Int } - """ - ) + """) original_print = print_schema(mutation_schema) extended_schema = extend_schema(mutation_schema, ast) assert extended_schema != mutation_schema assert print_schema(mutation_schema) == original_print - assert print_schema(extended_schema) == dedent( - """ + assert print_schema(extended_schema) == dedent(""" type Query { queryField: String newQueryField: Int @@ -1249,23 +1124,18 @@ def may_extend_mutations_and_subscriptions(): subscriptionField: String newSubscriptionField: Int } - """ - ) + """) def may_extend_directives_with_new_directive(): - schema = build_schema( - """ + schema = build_schema(""" type Query { foo: String } - """ - ) - extension_sdl = dedent( - ''' + """) + extension_sdl = dedent(''' """New directive.""" directive @new(enable: Boolean!, tag: String) repeatable on QUERY | FIELD - ''' - ) + ''') extended_schema = extend_schema(schema, parse(extension_sdl)) assert validate_schema(extended_schema) == [] @@ -1288,13 +1158,11 @@ def allows_to_disable_sdl_validation(): def throws_on_unknown_types(): schema = GraphQLSchema() - ast = parse( - """ + ast = parse(""" type Query { unknown: UnknownType } - """ - ) + """) with raises(TypeError) as exc_info: extend_schema(schema, ast, assume_valid_sdl=True) assert str(exc_info.value).endswith("Unknown type: 'UnknownType'.") @@ -1314,11 +1182,9 @@ def rejects_invalid_ast(): def does_not_allow_replacing_a_default_directive(): schema = GraphQLSchema() - extend_ast = parse( - """ + extend_ast = parse(""" directive @include(if: Boolean!) on FIELD | FRAGMENT_SPREAD - """ - ) + """) with raises(TypeError) as exc_info: extend_schema(schema, extend_ast) @@ -1328,20 +1194,16 @@ def does_not_allow_replacing_a_default_directive(): ) def does_not_allow_replacing_an_existing_enum_value(): - schema = build_schema( - """ + schema = build_schema(""" enum SomeEnum { ONE } - """ - ) - extend_ast = parse( - """ + """) + extend_ast = parse(""" extend enum SomeEnum { ONE } - """ - ) + """) with raises(TypeError) as exc_info: extend_schema(schema, extend_ast) @@ -1359,21 +1221,17 @@ def does_not_automatically_include_common_root_type_names(): assert extended_schema.mutation_type is None def adds_schema_definition_missing_in_the_original_schema(): - schema = build_schema( - """ + schema = build_schema(""" directive @foo on SCHEMA type Foo - """ - ) + """) assert schema.query_type is None - extension_sdl = dedent( - """ + extension_sdl = dedent(""" schema @foo { query: Foo } - """ - ) + """) extended_schema = extend_schema(schema, parse(extension_sdl)) query_type = assert_object_type(extended_schema.query_type) @@ -1381,19 +1239,15 @@ def adds_schema_definition_missing_in_the_original_schema(): expect_ast_node(extended_schema, extension_sdl) def adds_new_root_types_via_schema_extension(): - schema = build_schema( - """ + schema = build_schema(""" type Query type MutationRoot - """ - ) - extension_sdl = dedent( - """ + """) + extension_sdl = dedent(""" extend schema { mutation: MutationRoot } - """ - ) + """) extended_schema = extend_schema(schema, parse(extension_sdl)) mutation_type = assert_object_type(extended_schema.mutation_type) @@ -1401,26 +1255,21 @@ def adds_new_root_types_via_schema_extension(): expect_extension_ast_nodes(extended_schema, extension_sdl) def adds_directive_via_schema_extension(): - schema = build_schema( - """ + schema = build_schema(""" type Query directive @foo on SCHEMA - """ - ) - extension_sdl = dedent( - """ + """) + extension_sdl = dedent(""" extend schema @foo - """ - ) + """) extended_schema = extend_schema(schema, parse(extension_sdl)) expect_extension_ast_nodes(extended_schema, extension_sdl) def adds_multiple_new_root_types_via_schema_extension(): schema = build_schema("type Query") - extend_ast = parse( - """ + extend_ast = parse(""" extend schema { mutation: Mutation subscription: Subscription @@ -1428,8 +1277,7 @@ def adds_multiple_new_root_types_via_schema_extension(): type Mutation type Subscription - """ - ) + """) extended_schema = extend_schema(schema, extend_ast) mutation_type = assert_object_type(extended_schema.mutation_type) @@ -1440,8 +1288,7 @@ def adds_multiple_new_root_types_via_schema_extension(): def applies_multiple_schema_extensions(): schema = build_schema("type Query") - extend_ast = parse( - """ + extend_ast = parse(""" extend schema { mutation: Mutation } @@ -1451,8 +1298,7 @@ def applies_multiple_schema_extensions(): subscription: Subscription } type Subscription - """ - ) + """) extended_schema = extend_schema(schema, extend_ast) mutation_type = assert_object_type(extended_schema.mutation_type) @@ -1462,15 +1308,12 @@ def applies_multiple_schema_extensions(): assert subscription_type.name == "Subscription" def schema_extension_ast_are_available_from_schema_object(): - schema = build_schema( - """ + schema = build_schema(""" type Query directive @foo on SCHEMA - """ - ) - extend_ast = parse( - """ + """) + extend_ast = parse(""" extend schema { mutation: Mutation } @@ -1480,8 +1323,7 @@ def schema_extension_ast_are_available_from_schema_object(): subscription: Subscription } type Subscription - """ - ) + """) extended_schema = extend_schema(schema, extend_ast) second_extend_ast = parse("extend schema @foo") @@ -1489,8 +1331,7 @@ def schema_extension_ast_are_available_from_schema_object(): expect_extension_ast_nodes( extended_twice_schema, - dedent( - """ + dedent(""" extend schema { mutation: Mutation } @@ -1500,6 +1341,96 @@ def schema_extension_ast_are_available_from_schema_object(): } extend schema @foo + """), + ) + + def extend_directive_to_make_it_deprecated(): + schema = build_schema("directive @isDeprecated on FIELD_DEFINITION") + extend_ast = parse( + """ +extend directive @isDeprecated @deprecated(reason: "use another directive") +""", + experimental_directives_on_directive_definitions=True, + ) + extended_schema = extend_schema(schema, extend_ast) + + is_deprecated_directive = assert_directive( + extended_schema.get_directive("isDeprecated") + ) + assert is_deprecated_directive.deprecation_reason == "use another directive" + + def preserves_deprecated_directives_when_extending_other_types(): + schema = build_ast_schema( + parse( """ - ), +type Query { + foo: String +} + +directive @isDeprecated @deprecated(reason: "use another directive") on FIELD_DEFINITION +""", + experimental_directives_on_directive_definitions=True, + ) + ) + extend_ast = parse(dedent(""" + extend type Query { + bar: Int + } + """)) + extended_schema = extend_schema(schema, extend_ast) + + is_deprecated_directive = assert_directive( + extended_schema.get_directive("isDeprecated") + ) + assert is_deprecated_directive.deprecation_reason == "use another directive" + + def applies_directive_extensions_defined_in_the_same_document(): + schema = build_ast_schema( + parse( + dedent(""" + directive @onDirective on DIRECTIVE_DEFINITION + directive @someDirective on FIELD_DEFINITION + + extend directive @someDirective @onDirective + """), + experimental_directives_on_directive_definitions=True, + ) + ) + + some_directive = assert_directive(schema.get_directive("someDirective")) + expect_extension_ast_nodes( + some_directive, "extend directive @someDirective @onDirective" ) + + def applies_multiple_directive_extensions_defined_in_the_same_document(): + schema = build_ast_schema( + parse( + dedent(""" + directive @onDirective on DIRECTIVE_DEFINITION + directive @otherDirective on DIRECTIVE_DEFINITION + directive @someDirective on FIELD_DEFINITION + + extend directive @someDirective @onDirective + extend directive @someDirective @otherDirective + """), + experimental_directives_on_directive_definitions=True, + ) + ) + + some_directive = assert_directive(schema.get_directive("someDirective")) + expect_extension_ast_nodes( + some_directive, + dedent(""" + extend directive @someDirective @onDirective + + extend directive @someDirective @otherDirective + """), + ) + + def extend_directive_without_adding_new_directives_is_an_error(): + with raises(GraphQLSyntaxError) as exc_info: + parse( + "extend directive @isDeprecated", + experimental_directives_on_directive_definitions=True, + ) + assert str(exc_info.value).startswith("Syntax Error: Unexpected .") diff --git a/tests/utilities/test_find_breaking_changes.py b/tests/utilities/test_find_breaking_changes.py index f0ad2ab7..4e765639 100644 --- a/tests/utilities/test_find_breaking_changes.py +++ b/tests/utilities/test_find_breaking_changes.py @@ -4,6 +4,7 @@ GraphQLIncludeDirective, GraphQLSkipDirective, GraphQLSpecifiedByDirective, + GraphQLOneOfDirective, ) from graphql.utilities import ( BreakingChangeType, @@ -16,18 +17,14 @@ def describe_find_breaking_changes(): def should_detect_if_a_type_was_removed_or_not(): - old_schema = build_schema( - """ + old_schema = build_schema(""" type Type1 type Type2 - """ - ) + """) - new_schema = build_schema( - """ + new_schema = build_schema(""" type Type2 - """ - ) + """) assert find_breaking_changes(old_schema, new_schema) == [ (BreakingChangeType.TYPE_REMOVED, "Type1 was removed.") @@ -35,21 +32,17 @@ def should_detect_if_a_type_was_removed_or_not(): assert find_breaking_changes(old_schema, old_schema) == [] def should_detect_if_a_standard_scalar_was_removed(): - old_schema = build_schema( - """ + old_schema = build_schema(""" type Query { foo: Float } - """ - ) + """) - new_schema = build_schema( - """ + new_schema = build_schema(""" type Query { foo: String } - """ - ) + """) assert find_breaking_changes(old_schema, new_schema) == [ ( @@ -64,21 +57,17 @@ def should_detect_if_a_standard_scalar_was_removed(): ] def should_detect_if_a_type_changed_its_type(): - old_schema = build_schema( - """ + old_schema = build_schema(""" scalar TypeWasScalarBecomesEnum interface TypeWasInterfaceBecomesUnion type TypeWasObjectBecomesInputObject - """ - ) + """) - new_schema = build_schema( - """ + new_schema = build_schema(""" enum TypeWasScalarBecomesEnum union TypeWasInterfaceBecomesUnion input TypeWasObjectBecomesInputObject - """ - ) + """) assert find_breaking_changes(old_schema, new_schema) == [ ( @@ -98,8 +87,7 @@ def should_detect_if_a_type_changed_its_type(): ] def should_detect_if_a_field_on_type_was_deleted_or_changed_type(): - old_schema = build_schema( - """ + old_schema = build_schema(""" type TypeA type TypeB @@ -122,11 +110,9 @@ def should_detect_if_a_field_on_type_was_deleted_or_changed_type(): field17: [Int] field18: [[Int!]!] } - """ - ) + """) - new_schema = build_schema( - """ + new_schema = build_schema(""" type TypeA type TypeB @@ -149,8 +135,7 @@ def should_detect_if_a_field_on_type_was_deleted_or_changed_type(): field17: [Int]! field18: [[Int!]] } - """ - ) + """) assert find_breaking_changes(old_schema, new_schema) == [ (BreakingChangeType.FIELD_REMOVED, "Type1.field2 was removed."), @@ -205,8 +190,7 @@ def should_detect_if_a_field_on_type_was_deleted_or_changed_type(): ] def should_detect_if_fields_on_input_types_changed_kind_or_were_removed(): - old_schema = build_schema( - """ + old_schema = build_schema(""" input InputType1 { field1: String field2: Boolean @@ -224,11 +208,9 @@ def should_detect_if_fields_on_input_types_changed_kind_or_were_removed(): field14: [[Int]!] field15: [[Int]!] } - """ - ) + """) - new_schema = build_schema( - """ + new_schema = build_schema(""" input InputType1 { field1: Int field3: String @@ -245,8 +227,7 @@ def should_detect_if_fields_on_input_types_changed_kind_or_were_removed(): field14: [[Int]] field15: [[Int!]!] } - """ - ) + """) assert find_breaking_changes(old_schema, new_schema) == [ (BreakingChangeType.FIELD_REMOVED, "InputType1.field2 was removed."), @@ -293,24 +274,20 @@ def should_detect_if_fields_on_input_types_changed_kind_or_were_removed(): ] def should_detect_if_a_required_field_is_added_to_an_input_type(): - old_schema = build_schema( - """ + old_schema = build_schema(""" input InputType1 { field1: String } - """ - ) + """) - new_schema = build_schema( - """ + new_schema = build_schema(""" input InputType1 { field1: String requiredField: Int! optionalField1: Boolean optionalField2: Boolean! = false } - """ - ) + """) assert find_breaking_changes(old_schema, new_schema) == [ ( @@ -320,25 +297,21 @@ def should_detect_if_a_required_field_is_added_to_an_input_type(): ] def should_detect_if_a_type_was_removed_from_a_union_type(): - old_schema = build_schema( - """ + old_schema = build_schema(""" type Type1 type Type2 type Type3 union UnionType1 = Type1 | Type2 - """ - ) + """) - new_schema = build_schema( - """ + new_schema = build_schema(""" type Type1 type Type2 type Type3 union UnionType1 = Type1 | Type3 - """ - ) + """) assert find_breaking_changes(old_schema, new_schema) == [ ( @@ -348,25 +321,21 @@ def should_detect_if_a_type_was_removed_from_a_union_type(): ] def should_detect_if_a_value_was_removed_from_an_enum_type(): - old_schema = build_schema( - """ + old_schema = build_schema(""" enum EnumType1 { VALUE0 VALUE1 VALUE2 } - """ - ) + """) - new_schema = build_schema( - """ + new_schema = build_schema(""" enum EnumType1 { VALUE0 VALUE2 VALUE3 } - """ - ) + """) assert find_breaking_changes(old_schema, new_schema) == [ ( @@ -376,8 +345,7 @@ def should_detect_if_a_value_was_removed_from_an_enum_type(): ] def should_detect_if_a_field_argument_was_removed(): - old_schema = build_schema( - """ + old_schema = build_schema(""" interface Interface1 { field1(arg1: Boolean, objectArg: String): String } @@ -385,11 +353,9 @@ def should_detect_if_a_field_argument_was_removed(): type Type1 { field1(name: String): String } - """ - ) + """) - new_schema = build_schema( - """ + new_schema = build_schema(""" interface Interface1 { field1: String } @@ -397,8 +363,7 @@ def should_detect_if_a_field_argument_was_removed(): type Type1 { field1: String } - """ - ) + """) assert find_breaking_changes(old_schema, new_schema) == [ (BreakingChangeType.ARG_REMOVED, "Interface1.field1 arg arg1 was removed."), @@ -410,8 +375,7 @@ def should_detect_if_a_field_argument_was_removed(): ] def should_detect_if_a_field_argument_has_changed_type(): - old_schema = build_schema( - """ + old_schema = build_schema(""" type Type1 { field1( arg1: String @@ -431,11 +395,9 @@ def should_detect_if_a_field_argument_has_changed_type(): arg15: [[Int]!] ): String } - """ - ) + """) - new_schema = build_schema( - """ + new_schema = build_schema(""" type Type1 { field1( arg1: Int @@ -455,8 +417,7 @@ def should_detect_if_a_field_argument_has_changed_type(): arg15: [[Int!]!] ): String } - """ - ) + """) assert find_breaking_changes(old_schema, new_schema) == [ ( @@ -510,16 +471,13 @@ def should_detect_if_a_field_argument_has_changed_type(): ] def should_detect_if_a_required_field_argument_was_added(): - old_schema = build_schema( - """ + old_schema = build_schema(""" type Type1 { field1(arg1: String): String } - """ - ) + """) - new_schema = build_schema( - """ + new_schema = build_schema(""" type Type1 { field1( arg1: String, @@ -528,8 +486,7 @@ def should_detect_if_a_required_field_argument_was_added(): newOptionalArg2: Int! = 0 ): String } - """ - ) + """) assert find_breaking_changes(old_schema, new_schema) == [ ( @@ -539,8 +496,7 @@ def should_detect_if_a_required_field_argument_was_added(): ] def should_not_flag_args_with_the_same_type_signature_as_breaking(): - old_schema = build_schema( - """ + old_schema = build_schema(""" input InputType1 { field1: String } @@ -548,11 +504,9 @@ def should_not_flag_args_with_the_same_type_signature_as_breaking(): type Type1 { field1(arg1: Int!, arg2: InputType1): Int } - """ - ) + """) - new_schema = build_schema( - """ + new_schema = build_schema(""" input InputType1 { field1: String } @@ -560,46 +514,37 @@ def should_not_flag_args_with_the_same_type_signature_as_breaking(): type Type1 { field1(arg1: Int!, arg2: InputType1): Int } - """ - ) + """) assert find_breaking_changes(old_schema, new_schema) == [] def should_consider_args_that_move_away_from_non_null_as_non_breaking(): - old_schema = build_schema( - """ + old_schema = build_schema(""" type Type1 { field1(name: String!): String } - """ - ) + """) - new_schema = build_schema( - """ + new_schema = build_schema(""" type Type1 { field1(name: String): String } - """ - ) + """) assert find_breaking_changes(old_schema, new_schema) == [] def should_detect_interfaces_removed_from_types(): - old_schema = build_schema( - """ + old_schema = build_schema(""" interface Interface1 type Type1 implements Interface1 - """ - ) + """) - new_schema = build_schema( - """ + new_schema = build_schema(""" interface Interface1 type Type1 - """ - ) + """) assert find_breaking_changes(old_schema, new_schema) == [ ( @@ -609,21 +554,17 @@ def should_detect_interfaces_removed_from_types(): ] def should_detect_intrefaces_removed_from_interfaces(): - old_schema = build_schema( - """ + old_schema = build_schema(""" interface Interface1 interface Interface2 implements Interface1 - """ - ) + """) - new_schema = build_schema( - """ + new_schema = build_schema(""" interface Interface1 interface Interface2 - """ - ) + """) assert find_breaking_changes(old_schema, new_schema) == [ ( @@ -633,29 +574,24 @@ def should_detect_intrefaces_removed_from_interfaces(): ] def should_ignore_changes_in_order_of_interfaces(): - old_schema = build_schema( - """ + old_schema = build_schema(""" interface FirstInterface interface SecondInterface type Type1 implements FirstInterface & SecondInterface - """ - ) + """) - new_schema = build_schema( - """ + new_schema = build_schema(""" interface FirstInterface interface SecondInterface type Type1 implements SecondInterface & FirstInterface - """ - ) + """) assert find_breaking_changes(old_schema, new_schema) == [] def should_detect_all_breaking_changes(): - old_schema = build_schema( - """ + old_schema = build_schema(""" directive @DirectiveThatIsRemoved on FIELD_DEFINITION directive @DirectiveThatRemovesArg(arg1: String) on FIELD_DEFINITION @@ -691,11 +627,9 @@ def should_detect_all_breaking_changes(): field1: String field2: String } - """ - ) + """) - new_schema = build_schema( - """ + new_schema = build_schema(""" directive @DirectiveThatRemovesArg on FIELD_DEFINITION directive @NonNullDirectiveAdded(arg1: Boolean!) on FIELD_DEFINITION @@ -726,8 +660,7 @@ def should_detect_all_breaking_changes(): interface TypeThatHasBreakingFieldChanges { field2: Boolean } - """ - ) + """) assert find_breaking_changes(old_schema, new_schema) == [ ( @@ -789,18 +722,14 @@ def should_detect_all_breaking_changes(): ] def should_detect_if_a_directive_was_explicitly_removed(): - old_schema = build_schema( - """ + old_schema = build_schema(""" directive @DirectiveThatIsRemoved on FIELD_DEFINITION directive @DirectiveThatStays on FIELD_DEFINITION - """ - ) + """) - new_schema = build_schema( - """ + new_schema = build_schema(""" directive @DirectiveThatStays on FIELD_DEFINITION - """ - ) + """) assert find_breaking_changes(old_schema, new_schema) == [ ( @@ -817,6 +746,7 @@ def should_detect_if_a_directive_was_implicitly_removed(): GraphQLSkipDirective, GraphQLIncludeDirective, GraphQLSpecifiedByDirective, + GraphQLOneOfDirective, ] ) @@ -828,17 +758,13 @@ def should_detect_if_a_directive_was_implicitly_removed(): ] def should_detect_if_a_directive_argument_was_removed(): - old_schema = build_schema( - """ + old_schema = build_schema(""" directive @DirectiveWithArg(arg1: String) on FIELD_DEFINITION - """ - ) + """) - new_schema = build_schema( - """ + new_schema = build_schema(""" directive @DirectiveWithArg on FIELD_DEFINITION - """ - ) + """) assert find_breaking_changes(old_schema, new_schema) == [ ( @@ -848,21 +774,17 @@ def should_detect_if_a_directive_argument_was_removed(): ] def should_detect_if_an_optional_directive_argument_was_added(): - old_schema = build_schema( - """ + old_schema = build_schema(""" directive @DirectiveName on FIELD_DEFINITION - """ - ) + """) - new_schema = build_schema( - """ + new_schema = build_schema(""" directive @DirectiveName( newRequiredArg: String! newOptionalArg1: Int newOptionalArg2: Int! = 0 ) on FIELD_DEFINITION - """ - ) + """) assert find_breaking_changes(old_schema, new_schema) == [ ( @@ -872,17 +794,13 @@ def should_detect_if_an_optional_directive_argument_was_added(): ] def should_detect_removal_of_repeatable_flag(): - old_schema = build_schema( - """ + old_schema = build_schema(""" directive @DirectiveName repeatable on OBJECT - """ - ) + """) - new_schema = build_schema( - """ + new_schema = build_schema(""" directive @DirectiveName on OBJECT - """ - ) + """) assert find_breaking_changes(old_schema, new_schema) == [ ( @@ -892,17 +810,13 @@ def should_detect_removal_of_repeatable_flag(): ] def should_detect_locations_removed_from_a_directive(): - old_schema = build_schema( - """ + old_schema = build_schema(""" directive @DirectiveName on FIELD_DEFINITION | QUERY - """ - ) + """) - new_schema = build_schema( - """ + new_schema = build_schema(""" directive @DirectiveName on FIELD_DEFINITION - """ - ) + """) assert find_breaking_changes(old_schema, new_schema) == [ ( @@ -940,8 +854,7 @@ def should_detect_if_a_default_value_changed_on_an_argument(): copy_of_old_schema = build_schema(old_sdl) assert find_dangerous_changes(old_schema, copy_of_old_schema) == [] - new_schema = build_schema( - """ + new_schema = build_schema(""" input Input1 { innerInputArray: [Input2] } @@ -961,8 +874,7 @@ def should_detect_if_a_default_value_changed_on_an_argument(): } ): String } - """ - ) + """) assert find_dangerous_changes(old_schema, new_schema) == [ ( @@ -992,8 +904,7 @@ def should_detect_if_a_default_value_changed_on_an_argument(): ] def should_ignore_changes_in_field_order_of_default_value(): - old_schema = build_schema( - """ + old_schema = build_schema(""" input Input1 { a: String b: String @@ -1005,11 +916,9 @@ def should_ignore_changes_in_field_order_of_default_value(): arg1: Input1 = { a: "a", b: "b", c: "c" } ): String } - """ - ) + """) - new_schema = build_schema( - """ + new_schema = build_schema(""" input Input1 { a: String b: String @@ -1021,14 +930,12 @@ def should_ignore_changes_in_field_order_of_default_value(): arg1: Input1 = { c: "c", b: "b", a: "a" } ): String } - """ - ) + """) assert find_dangerous_changes(old_schema, new_schema) == [] def should_ignore_changes_in_field_definitions_order(): - old_schema = build_schema( - """ + old_schema = build_schema(""" input Input1 { a: String b: String @@ -1040,11 +947,9 @@ def should_ignore_changes_in_field_definitions_order(): arg1: Input1 = { a: "a", b: "b", c: "c" } ): String } - """ - ) + """) - new_schema = build_schema( - """ + new_schema = build_schema(""" input Input1 { c: String b: String @@ -1056,30 +961,25 @@ def should_ignore_changes_in_field_definitions_order(): arg1: Input1 = { a: "a", b: "b", c: "c" } ): String } - """ - ) + """) assert find_dangerous_changes(old_schema, new_schema) == [] def should_detect_if_a_value_was_added_to_an_enum_type(): - old_schema = build_schema( - """ + old_schema = build_schema(""" enum EnumType1 { VALUE0 VALUE1 } - """ - ) + """) - new_schema = build_schema( - """ + new_schema = build_schema(""" enum EnumType1 { VALUE0 VALUE1 VALUE2 } - """ - ) + """) assert find_dangerous_changes(old_schema, new_schema) == [ ( @@ -1089,23 +989,19 @@ def should_detect_if_a_value_was_added_to_an_enum_type(): ] def should_detect_interfaces_added_to_types(): - old_schema = build_schema( - """ + old_schema = build_schema(""" interface OldInterface interface NewInterface type Type1 implements OldInterface - """ - ) + """) - new_schema = build_schema( - """ + new_schema = build_schema(""" interface OldInterface interface NewInterface type Type1 implements OldInterface & NewInterface - """ - ) + """) assert find_dangerous_changes(old_schema, new_schema) == [ ( @@ -1115,23 +1011,19 @@ def should_detect_interfaces_added_to_types(): ] def should_detect_interfaces_added_to_interfaces(): - old_schema = build_schema( - """ + old_schema = build_schema(""" interface OldInterface interface NewInterface interface Interface1 implements OldInterface - """ - ) + """) - new_schema = build_schema( - """ + new_schema = build_schema(""" interface OldInterface interface NewInterface interface Interface1 implements OldInterface & NewInterface - """ - ) + """) assert find_dangerous_changes(old_schema, new_schema) == [ ( @@ -1141,23 +1033,19 @@ def should_detect_interfaces_added_to_interfaces(): ] def should_detect_if_a_type_was_added_to_a_union_type(): - old_schema = build_schema( - """ + old_schema = build_schema(""" type Type1 type Type2 union UnionType1 = Type1 - """ - ) + """) - new_schema = build_schema( - """ + new_schema = build_schema(""" type Type1 type Type2 union UnionType1 = Type1 | Type2 - """ - ) + """) assert find_dangerous_changes(old_schema, new_schema) == [ ( @@ -1167,22 +1055,18 @@ def should_detect_if_a_type_was_added_to_a_union_type(): ] def should_detect_if_an_optional_field_was_added_to_an_input(): - old_schema = build_schema( - """ + old_schema = build_schema(""" input InputType1 { field1: String } - """ - ) + """) - new_schema = build_schema( - """ + new_schema = build_schema(""" input InputType1 { field1: String field2: Int } - """ - ) + """) assert find_dangerous_changes(old_schema, new_schema) == [ ( @@ -1192,8 +1076,7 @@ def should_detect_if_an_optional_field_was_added_to_an_input(): ] def should_find_all_dangerous_changes(): - old_schema = build_schema( - """ + old_schema = build_schema(""" enum EnumType1 { VALUE0 VALUE1 @@ -1208,11 +1091,9 @@ def should_find_all_dangerous_changes(): type TypeInUnion1 union UnionTypeThatGainsAType = TypeInUnion1 - """ - ) + """) - new_schema = build_schema( - """ + new_schema = build_schema(""" enum EnumType1 { VALUE0 VALUE1 @@ -1229,8 +1110,7 @@ def should_find_all_dangerous_changes(): type TypeInUnion1 type TypeInUnion2 union UnionTypeThatGainsAType = TypeInUnion1 | TypeInUnion2 - """ - ) + """) assert find_dangerous_changes(old_schema, new_schema) == [ ( @@ -1254,21 +1134,17 @@ def should_find_all_dangerous_changes(): ] def should_detect_if_an_optional_field_argument_was_added(): - old_schema = build_schema( - """ + old_schema = build_schema(""" type Type1 { field1(arg1: String): String } - """ - ) + """) - new_schema = build_schema( - """ + new_schema = build_schema(""" type Type1 { field1(arg1: String, arg2: String): String } - """ - ) + """) assert find_dangerous_changes(old_schema, new_schema) == [ ( diff --git a/tests/utilities/test_get_introspection_query.py b/tests/utilities/test_get_introspection_query.py index 279112bb..78c4d5e1 100644 --- a/tests/utilities/test_get_introspection_query.py +++ b/tests/utilities/test_get_introspection_query.py @@ -2,17 +2,17 @@ from typing import Pattern +from pytest import raises + from graphql.language import parse from graphql.utilities import build_schema, get_introspection_query from graphql.validation import validate -dummy_schema = build_schema( - """ +dummy_schema = build_schema(""" type Query { dummy: String } - """ -) + """) class ExcpectIntrospectionQuery: @@ -30,6 +30,12 @@ def to_not_match(self, name: str) -> None: pattern = self.to_reg_exp(name) assert not pattern.search(self.query) + def to_contain(self, text: str) -> None: + assert text in self.query + + def to_not_contain(self, text: str) -> None: + assert text not in self.query + @staticmethod def to_reg_exp(name: str) -> Pattern: return re.compile(rf"\b{name}\b") @@ -79,6 +85,33 @@ def includes_deprecation_reason_field_on_input_values(): "deprecationReason", 2 ) + def includes_is_deprecated_field_on_directives(): + ExcpectIntrospectionQuery().to_match("isDeprecated", 2) + ExcpectIntrospectionQuery(experimental_directive_deprecation=True).to_match( + "isDeprecated", 3 + ) + ExcpectIntrospectionQuery(experimental_directive_deprecation=False).to_match( + "isDeprecated", 2 + ) + + def includes_deprecation_reason_field_on_directives(): + query = ExcpectIntrospectionQuery() + query.to_not_contain("directives(includeDeprecated: true) {") + query.to_match("deprecationReason", 2) + + query = ExcpectIntrospectionQuery(experimental_directive_deprecation=True) + query.to_contain("directives(includeDeprecated: true) {") + query.to_match("deprecationReason", 3) + + query = ExcpectIntrospectionQuery(experimental_directive_deprecation=False) + query.to_not_contain("directives(includeDeprecated: true) {") + query.to_match("deprecationReason", 2) + + def includes_input_object_one_of_field(): + ExcpectIntrospectionQuery().to_not_match("isOneOf") + ExcpectIntrospectionQuery(input_object_one_of=True).to_match("isOneOf") + ExcpectIntrospectionQuery(input_object_one_of=False).to_not_match("isOneOf") + def includes_deprecated_input_field_and_args(): ExcpectIntrospectionQuery().to_match("includeDeprecated: true", 2) ExcpectIntrospectionQuery(input_value_deprecation=True).to_match( @@ -87,3 +120,11 @@ def includes_deprecated_input_field_and_args(): ExcpectIntrospectionQuery(input_value_deprecation=False).to_match( "includeDeprecated: true", 2 ) + + def throws_error_if_type_depth_is_too_high(): + with raises(ValueError) as exc_info: + get_introspection_query(type_depth=101) + assert str(exc_info.value) == ( + "Please set type_depth to a reasonable value" + " between 0 and 100; the default is 9." + ) diff --git a/tests/utilities/test_get_operation_ast.py b/tests/utilities/test_get_operation_ast.py index c60ca1a2..e64eec64 100644 --- a/tests/utilities/test_get_operation_ast.py +++ b/tests/utilities/test_get_operation_ast.py @@ -20,43 +20,35 @@ def does_not_get_missing_operation(): assert get_operation_ast(doc) is None def does_not_get_ambiguous_unnamed_operation(): - doc = parse( - """ + doc = parse(""" { field } mutation Test { field } subscription TestSub { field } - """ - ) + """) assert get_operation_ast(doc) is None def does_not_get_ambiguous_named_operation(): - doc = parse( - """ + doc = parse(""" query TestQ { field } mutation TestM { field } subscription TestS { field } - """ - ) + """) assert get_operation_ast(doc) is None def does_not_get_misnamed_operation(): - doc = parse( - """ + doc = parse(""" query TestQ { field } mutation TestM { field } subscription TestS { field } - """ - ) + """) assert get_operation_ast(doc, "Unknown") is None def gets_named_operation(): - doc = parse( - """ + doc = parse(""" query TestQ { field } mutation TestM { field } subscription TestS { field } - """ - ) + """) assert get_operation_ast(doc, "TestQ") == doc.definitions[0] assert get_operation_ast(doc, "TestM") == doc.definitions[1] assert get_operation_ast(doc, "TestS") == doc.definitions[2] diff --git a/tests/utilities/test_get_operation_root_type.py b/tests/utilities/test_get_operation_root_type.py index f0e8a4e3..a3a254dd 100644 --- a/tests/utilities/test_get_operation_root_type.py +++ b/tests/utilities/test_get_operation_root_type.py @@ -11,7 +11,6 @@ from graphql.type import GraphQLField, GraphQLObjectType, GraphQLSchema, GraphQLString from graphql.utilities import get_operation_root_type - query_type = GraphQLObjectType("FooQuery", {"field": GraphQLField(GraphQLString)}) mutation_type = GraphQLObjectType("FooMutation", {"field": GraphQLField(GraphQLString)}) @@ -42,15 +41,13 @@ def gets_a_query_type_for_a_named_operation_definition_node(): def gets_a_type_for_operation_definition_nodes(): test_schema = GraphQLSchema(query_type, mutation_type, subscription_type) - doc = parse( - """ + doc = parse(""" schema { query: FooQuery mutation: FooMutation subscription: FooSubscription } - """ - ) + """) schema_node = doc.definitions[0] assert isinstance(schema_node, SchemaDefinitionNode) diff --git a/tests/utilities/test_introspection_from_schema.py b/tests/utilities/test_introspection_from_schema.py index d06788c0..5eb50006 100644 --- a/tests/utilities/test_introspection_from_schema.py +++ b/tests/utilities/test_introspection_from_schema.py @@ -1,12 +1,27 @@ -from graphql.type import GraphQLSchema, GraphQLObjectType, GraphQLField, GraphQLString +import pickle +import sys +from copy import deepcopy + +from pytest import mark + +from graphql.language import DirectiveLocation +from graphql.type import ( + GraphQLDirective, + GraphQLField, + GraphQLObjectType, + GraphQLSchema, + GraphQLString, +) from graphql.utilities import ( build_client_schema, + build_schema, print_schema, introspection_from_schema, IntrospectionQuery, ) -from ..utils import dedent +from ..fixtures import big_schema_introspection_result, big_schema_sdl # noqa: F401 +from ..utils import dedent, timeout_factor def introspection_to_sdl(introspection: IntrospectionQuery) -> str: @@ -31,8 +46,7 @@ def describe_introspection_from_schema(): def converts_a_simple_schema(): introspection = introspection_from_schema(schema) - assert introspection_to_sdl(introspection) == dedent( - ''' + assert introspection_to_sdl(introspection) == dedent(''' """This is a simple schema""" schema { query: Simple @@ -43,14 +57,12 @@ def converts_a_simple_schema(): """This is a string field""" string: String } - ''' - ) + ''') def converts_a_simple_schema_without_description(): introspection = introspection_from_schema(schema, descriptions=False) - assert introspection_to_sdl(introspection) == dedent( - """ + assert introspection_to_sdl(introspection) == dedent(""" schema { query: Simple } @@ -58,5 +70,135 @@ def converts_a_simple_schema_without_description(): type Simple { string: String } - """ + """) + + def includes_deprecated_directives(): + schema_with_deprecated_directive = GraphQLSchema( + query=GraphQLObjectType( + name="Query", + fields={"string": GraphQLField(GraphQLString)}, + ), + directives=[ + GraphQLDirective( + name="deprecatedDirective", + locations=[DirectiveLocation.QUERY], + deprecation_reason="Use another directive", + ), + ], + ) + introspection = introspection_from_schema(schema_with_deprecated_directive) + deprecated_directive = next( + directive + for directive in introspection["__schema"]["directives"] + if directive["name"] == "deprecatedDirective" ) + + assert deprecated_directive["name"] == "deprecatedDirective" + assert deprecated_directive["isDeprecated"] is True + assert deprecated_directive["deprecationReason"] == "Use another directive" + + def describe_deepcopy_and_pickle(): # pragma: no cover + # introspect the schema + introspected_schema = introspection_from_schema(schema) + introspection_size = len(str(introspected_schema)) + + def can_deep_copy_schema(): + # create a deepcopy of the schema + copied = deepcopy(schema) + # check that introspecting the copied schema gives the same result + assert introspection_from_schema(copied) == introspected_schema + + def can_pickle_and_unpickle_schema(): + # check that the schema can be pickled + # (particularly, there should be no recursion error, + # or errors because of trying to pickle lambdas or local functions) + dumped = pickle.dumps(schema) + + # check that the pickle size is reasonable + assert len(dumped) < 5 * introspection_size + loaded = pickle.loads(dumped) + + # check that introspecting the unpickled schema gives the same result + assert introspection_from_schema(loaded) == introspected_schema + + # check that pickling again creates the same result + dumped = pickle.dumps(schema) + assert len(dumped) < 5 * introspection_size + loaded = pickle.loads(dumped) + assert introspection_from_schema(loaded) == introspected_schema + + def can_deep_copy_pickled_schema(): + # pickle and unpickle the schema + loaded = pickle.loads(pickle.dumps(schema)) + # create a deepcopy of the unpickled schema + copied = deepcopy(loaded) + # check that introspecting the copied schema gives the same result + assert introspection_from_schema(copied) == introspected_schema + + @mark.slow + def describe_deepcopy_and_pickle_big(): # pragma: no cover + @mark.timeout(20 * timeout_factor) + def can_deep_copy_big_schema(big_schema_sdl): # noqa: F811 + # introspect the original big schema + big_schema = build_schema(big_schema_sdl) + expected_introspection = introspection_from_schema(big_schema) + + # create a deepcopy of the schema + copied = deepcopy(big_schema) + # check that introspecting the copied schema gives the same result + assert introspection_from_schema(copied) == expected_introspection + + @mark.timeout(60 * timeout_factor) + def can_pickle_and_unpickle_big_schema(big_schema_sdl): # noqa: F811 + # introspect the original big schema + big_schema = build_schema(big_schema_sdl) + expected_introspection = introspection_from_schema(big_schema) + size_introspection = len(str(expected_introspection)) + + limit = sys.getrecursionlimit() + sys.setrecursionlimit(max(limit, 4000)) # needed for pickle + + try: + # check that the schema can be pickled + # (particularly, there should be no recursion error, + # or errors because of trying to pickle lambdas or local functions) + dumped = pickle.dumps(big_schema) + + # check that the pickle size is reasonable + assert len(dumped) < 5 * size_introspection + loaded = pickle.loads(dumped) + + # check that introspecting the pickled schema gives the same result + assert introspection_from_schema(loaded) == expected_introspection + + # check that pickling again creates the same result + dumped = pickle.dumps(loaded) + assert len(dumped) < 5 * size_introspection + loaded = pickle.loads(dumped) + + # check that introspecting the re-pickled schema gives the same result + assert introspection_from_schema(loaded) == expected_introspection + + finally: + sys.setrecursionlimit(limit) + + @mark.timeout(60 * timeout_factor) + def can_deep_copy_pickled_big_schema(big_schema_sdl): # noqa: F811 + # introspect the original big schema + big_schema = build_schema(big_schema_sdl) + expected_introspection = introspection_from_schema(big_schema) + + limit = sys.getrecursionlimit() + sys.setrecursionlimit(max(limit, 4000)) # needed for pickle + + try: + # pickle and unpickle the schema + loaded = pickle.loads(pickle.dumps(big_schema)) + # create a deepcopy of the unpickled schema + copied = deepcopy(loaded) + + # check that introspecting the copied schema gives the same result + assert introspection_from_schema(copied) == expected_introspection + + finally: + sys.setrecursionlimit(limit) diff --git a/tests/utilities/test_lexicographic_sort_schema.py b/tests/utilities/test_lexicographic_sort_schema.py index e93b680b..897f2d65 100644 --- a/tests/utilities/test_lexicographic_sort_schema.py +++ b/tests/utilities/test_lexicographic_sort_schema.py @@ -1,4 +1,15 @@ -from graphql.utilities import build_schema, print_schema, lexicographic_sort_schema +from typing import cast + +from graphql.type import ( + GraphQLArgument, + GraphQLField, + GraphQLInputField, + GraphQLInputObjectType, + GraphQLObjectType, + GraphQLSchema, + GraphQLString, +) +from graphql.utilities import build_schema, lexicographic_sort_schema, print_schema from ..utils import dedent @@ -10,8 +21,7 @@ def sort_sdl(sdl: str) -> str: def describe_lexicographic_sort_schema(): def sort_fields(): - sorted_sdl = sort_sdl( - """ + sorted_sdl = sort_sdl(""" input Bar { barB: String! barA: String @@ -33,11 +43,9 @@ def sort_fields(): type Query { dummy(arg: Bar): FooType } - """ - ) + """) - assert sorted_sdl == dedent( - """ + assert sorted_sdl == dedent(""" input Bar { barA: String barB: String! @@ -59,12 +67,10 @@ def sort_fields(): type Query { dummy(arg: Bar): FooType } - """ - ) + """) def sort_implemented_interfaces(): - sorted_sdl = sort_sdl( - """ + sorted_sdl = sort_sdl(""" interface FooA { dummy: String } @@ -80,11 +86,9 @@ def sort_implemented_interfaces(): type Query implements FooB & FooA & FooC { dummy: String } - """ - ) + """) - assert sorted_sdl == dedent( - """ + assert sorted_sdl == dedent(""" interface FooA { dummy: String } @@ -100,12 +104,10 @@ def sort_implemented_interfaces(): type Query implements FooA & FooB & FooC { dummy: String } - """ - ) + """) def sort_types_in_union(): - sorted_sdl = sort_sdl( - """ + sorted_sdl = sort_sdl(""" type FooA { dummy: String } @@ -123,11 +125,9 @@ def sort_types_in_union(): type Query { dummy: FooUnion } - """ - ) + """) - assert sorted_sdl == dedent( - """ + assert sorted_sdl == dedent(""" type FooA { dummy: String } @@ -145,12 +145,10 @@ def sort_types_in_union(): type Query { dummy: FooUnion } - """ - ) + """) def sort_enum_types(): - sorted_sdl = sort_sdl( - """ + sorted_sdl = sort_sdl(""" enum Foo { B C @@ -160,11 +158,9 @@ def sort_enum_types(): type Query { dummy: Foo } - """ - ) + """) - assert sorted_sdl == dedent( - """ + assert sorted_sdl == dedent(""" enum Foo { A B @@ -174,29 +170,23 @@ def sort_enum_types(): type Query { dummy: Foo } - """ - ) + """) def sort_field_arguments(): - sorted_sdl = sort_sdl( - """ + sorted_sdl = sort_sdl(""" type Query { dummy(argB: Int!, argA: String, argC: [Float]): ID } - """ - ) + """) - assert sorted_sdl == dedent( - """ + assert sorted_sdl == dedent(""" type Query { dummy(argA: String, argB: Int!, argC: [Float]): ID } - """ - ) + """) def sort_types(): - sorted_sdl = sort_sdl( - """ + sorted_sdl = sort_sdl(""" type Query { dummy(arg1: FooF, arg2: FooA, arg3: FooG): FooD } @@ -224,11 +214,9 @@ def sort_types(): type FooB { dummy: String } - """ - ) + """) - assert sorted_sdl == dedent( - """ + assert sorted_sdl == dedent(""" scalar FooA type FooB { @@ -256,54 +244,44 @@ def sort_types(): type Query { dummy(arg1: FooF, arg2: FooA, arg3: FooG): FooD } - """ - ) + """) def sort_directive_arguments(): - sorted_sdl = sort_sdl( - """ + sorted_sdl = sort_sdl(""" directive @test(argC: Float, argA: String, argB: Int) on FIELD type Query { dummy: String } - """ - ) + """) - assert sorted_sdl == dedent( - """ + assert sorted_sdl == dedent(""" directive @test(argA: String, argB: Int, argC: Float) on FIELD type Query { dummy: String } - """ - ) + """) def sort_directive_locations(): - sorted_sdl = sort_sdl( - """ + sorted_sdl = sort_sdl(""" directive @test(argC: Float, argA: String, argB: Int) on UNION | FIELD | ENUM type Query { dummy: String } - """ # noqa: E501 - ) + """) # noqa: E501 - assert sorted_sdl == dedent( - """ + assert sorted_sdl == dedent(""" directive @test(argA: String, argB: Int, argC: Float) on ENUM | FIELD | UNION type Query { dummy: String } - """ # noqa: E501 - ) + """) # noqa: E501 def sort_directives(): - sorted_sdl = sort_sdl( - """ + sorted_sdl = sort_sdl(""" directive @fooC on FIELD directive @fooB on UNION @@ -313,11 +291,9 @@ def sort_directives(): type Query { dummy: String } - """ - ) + """) - assert sorted_sdl == dedent( - """ + assert sorted_sdl == dedent(""" directive @fooA on ENUM directive @fooB on UNION @@ -327,12 +303,10 @@ def sort_directives(): type Query { dummy: String } - """ - ) + """) def sort_recursive_types(): - sorted_sdl = sort_sdl( - """ + sorted_sdl = sort_sdl(""" interface FooC { fooB: FooB fooA: FooA @@ -354,11 +328,9 @@ def sort_recursive_types(): fooB: FooB fooA: FooA } - """ - ) + """) - assert sorted_sdl == dedent( - """ + assert sorted_sdl == dedent(""" type FooA implements FooC { fooA: FooA fooB: FooB @@ -380,5 +352,124 @@ def sort_recursive_types(): fooB: FooB fooC: FooC } - """ - ) + """) + + def describe_input_field_properties(): + def preserves_input_field_deprecation_reason(): + input_type = GraphQLInputObjectType( + "TestInput", + { + "zField": GraphQLInputField(GraphQLString), + "aField": GraphQLInputField( + GraphQLString, deprecation_reason="Use bField instead" + ), + "mField": GraphQLInputField( + GraphQLString, deprecation_reason="Deprecated field" + ), + }, + ) + query_type = GraphQLObjectType( + "Query", + { + "dummy": GraphQLField( + GraphQLString, args={"input": GraphQLArgument(input_type)} + ) + }, + ) + schema = GraphQLSchema(query=query_type, types=[input_type]) + sorted_schema = lexicographic_sort_schema(schema) + + sorted_input_type = cast( + GraphQLInputObjectType, sorted_schema.type_map["TestInput"] + ) + field_names = list(sorted_input_type.fields) + assert field_names == ["aField", "mField", "zField"] + + assert ( + sorted_input_type.fields["aField"].deprecation_reason + == "Use bField instead" + ) + assert ( + sorted_input_type.fields["mField"].deprecation_reason + == "Deprecated field" + ) + assert sorted_input_type.fields["zField"].deprecation_reason is None + + def preserves_input_field_extensions(): + input_type = GraphQLInputObjectType( + "TestInput", + { + "zField": GraphQLInputField(GraphQLString, extensions={"x": 1}), + "aField": GraphQLInputField( + GraphQLString, extensions={"custom": "value"} + ), + }, + ) + query_type = GraphQLObjectType( + "Query", + { + "dummy": GraphQLField( + GraphQLString, args={"input": GraphQLArgument(input_type)} + ) + }, + ) + schema = GraphQLSchema(query=query_type, types=[input_type]) + sorted_schema = lexicographic_sort_schema(schema) + sorted_input_type = cast( + GraphQLInputObjectType, sorted_schema.type_map["TestInput"] + ) + + field_names = list(sorted_input_type.fields) + assert field_names == ["aField", "zField"] + + assert sorted_input_type.fields["aField"].extensions == {"custom": "value"} + assert sorted_input_type.fields["zField"].extensions == {"x": 1} + + def preserves_input_field_out_name(): + input_type = GraphQLInputObjectType( + "TestInput", + { + "zField": GraphQLInputField(GraphQLString, out_name="z_field"), + "aField": GraphQLInputField(GraphQLString, out_name="a_field"), + }, + ) + query_type = GraphQLObjectType( + "Query", + { + "dummy": GraphQLField( + GraphQLString, args={"input": GraphQLArgument(input_type)} + ) + }, + ) + schema = GraphQLSchema(query=query_type, types=[input_type]) + sorted_schema = lexicographic_sort_schema(schema) + sorted_input_type = cast( + GraphQLInputObjectType, sorted_schema.type_map["TestInput"] + ) + + field_names = list(sorted_input_type.fields) + assert field_names == ["aField", "zField"] + + assert sorted_input_type.fields["aField"].out_name == "a_field" + assert sorted_input_type.fields["zField"].out_name == "z_field" + + def describe_schema_properties(): + def preserves_schema_description(): + query_type = GraphQLObjectType( + "Query", {"dummy": GraphQLField(GraphQLString)} + ) + schema = GraphQLSchema( + query=query_type, description="This is the schema description." + ) + sorted_schema = lexicographic_sort_schema(schema) + + assert sorted_schema.description == "This is the schema description." + + def preserves_schema_extensions(): + query_type = GraphQLObjectType( + "Query", {"dummy": GraphQLField(GraphQLString)} + ) + schema = GraphQLSchema(query=query_type, extensions={"custom": "value"}) + sorted_schema = lexicographic_sort_schema(schema) + + assert sorted_schema.extensions == {"custom": "value"} diff --git a/tests/utilities/test_print_schema.py b/tests/utilities/test_print_schema.py index 37337b23..d8b27f36 100644 --- a/tests/utilities/test_print_schema.py +++ b/tests/utilities/test_print_schema.py @@ -1,12 +1,14 @@ -from typing import cast, Any, Dict +from typing import Any, Dict, cast from graphql.language import DirectiveLocation from graphql.type import ( GraphQLArgument, GraphQLBoolean, + GraphQLDirective, GraphQLEnumType, GraphQLField, GraphQLFloat, + GraphQLInputField, GraphQLInputObjectType, GraphQLInt, GraphQLInterfaceType, @@ -17,13 +19,11 @@ GraphQLSchema, GraphQLString, GraphQLUnionType, - GraphQLInputField, - GraphQLDirective, ) from graphql.utilities import ( build_schema, - print_schema, print_introspection_schema, + print_schema, print_value, ) @@ -45,69 +45,57 @@ def build_single_field_schema(field: GraphQLField): def describe_type_system_printer(): def prints_string_field(): schema = build_single_field_schema(GraphQLField(GraphQLString)) - assert expect_printed_schema(schema) == dedent( - """ + assert expect_printed_schema(schema) == dedent(""" type Query { singleField: String } - """ - ) + """) def prints_list_of_string_field(): schema = build_single_field_schema(GraphQLField(GraphQLList(GraphQLString))) - assert expect_printed_schema(schema) == dedent( - """ + assert expect_printed_schema(schema) == dedent(""" type Query { singleField: [String] } - """ - ) + """) def prints_non_null_string_field(): schema = build_single_field_schema(GraphQLField(GraphQLNonNull(GraphQLString))) - assert expect_printed_schema(schema) == dedent( - """ + assert expect_printed_schema(schema) == dedent(""" type Query { singleField: String! } - """ - ) + """) def prints_non_null_list_of_string_field(): schema = build_single_field_schema( GraphQLField(GraphQLNonNull(GraphQLList(GraphQLString))) ) - assert expect_printed_schema(schema) == dedent( - """ + assert expect_printed_schema(schema) == dedent(""" type Query { singleField: [String]! } - """ - ) + """) def prints_list_of_non_null_string_field(): schema = build_single_field_schema( GraphQLField((GraphQLList(GraphQLNonNull(GraphQLString)))) ) - assert expect_printed_schema(schema) == dedent( - """ + assert expect_printed_schema(schema) == dedent(""" type Query { singleField: [String!] } - """ - ) + """) def prints_non_null_list_of_non_null_string_field(): schema = build_single_field_schema( GraphQLField(GraphQLNonNull(GraphQLList(GraphQLNonNull(GraphQLString)))) ) - assert expect_printed_schema(schema) == dedent( - """ + assert expect_printed_schema(schema) == dedent(""" type Query { singleField: [String!]! } - """ - ) + """) def prints_object_field(): foo_type = GraphQLObjectType( @@ -115,13 +103,11 @@ def prints_object_field(): ) schema = GraphQLSchema(types=[foo_type]) - assert expect_printed_schema(schema) == dedent( - """ + assert expect_printed_schema(schema) == dedent(""" type Foo { str: String } - """ - ) + """) def prints_string_field_with_int_arg(): schema = build_single_field_schema( @@ -130,13 +116,11 @@ def prints_string_field_with_int_arg(): ) ) - assert expect_printed_schema(schema) == dedent( - """ + assert expect_printed_schema(schema) == dedent(""" type Query { singleField(argOne: Int): String } - """ - ) + """) def prints_string_field_with_int_arg_with_default(): schema = build_single_field_schema( @@ -146,13 +130,11 @@ def prints_string_field_with_int_arg_with_default(): ) ) - assert expect_printed_schema(schema) == dedent( - """ + assert expect_printed_schema(schema) == dedent(""" type Query { singleField(argOne: Int = 2): String } - """ - ) + """) def prints_string_field_with_string_arg_with_default(): schema = build_single_field_schema( @@ -166,13 +148,11 @@ def prints_string_field_with_string_arg_with_default(): ) ) - assert expect_printed_schema(schema) == dedent( - r""" + assert expect_printed_schema(schema) == dedent(r""" type Query { singleField(argOne: String = "tes\t de\fault"): String } - """ - ) + """) def prints_string_field_with_int_arg_with_default_null(): schema = build_single_field_schema( @@ -182,13 +162,11 @@ def prints_string_field_with_int_arg_with_default_null(): ) ) - assert expect_printed_schema(schema) == dedent( - """ + assert expect_printed_schema(schema) == dedent(""" type Query { singleField(argOne: Int = null): String } - """ - ) + """) def prints_string_field_with_non_null_int_arg(): schema = build_single_field_schema( @@ -198,13 +176,11 @@ def prints_string_field_with_non_null_int_arg(): ) ) - assert expect_printed_schema(schema) == dedent( - """ + assert expect_printed_schema(schema) == dedent(""" type Query { singleField(argOne: Int!): String } - """ - ) + """) def prints_string_field_with_multiple_args(): schema = build_single_field_schema( @@ -217,13 +193,11 @@ def prints_string_field_with_multiple_args(): ) ) - assert expect_printed_schema(schema) == dedent( - """ + assert expect_printed_schema(schema) == dedent(""" type Query { singleField(argOne: Int, argTwo: String): String } - """ - ) + """) def prints_string_field_with_multiple_args_first_is_default(): schema = build_single_field_schema( @@ -237,13 +211,11 @@ def prints_string_field_with_multiple_args_first_is_default(): ) ) - assert expect_printed_schema(schema) == dedent( - """ + assert expect_printed_schema(schema) == dedent(""" type Query { singleField(argOne: Int = 1, argTwo: String, argThree: Boolean): String } - """ - ) + """) def prints_string_field_with_multiple_args_second_is_default(): schema = build_single_field_schema( @@ -257,13 +229,11 @@ def prints_string_field_with_multiple_args_second_is_default(): ) ) - assert expect_printed_schema(schema) == dedent( - """ + assert expect_printed_schema(schema) == dedent(""" type Query { singleField(argOne: Int, argTwo: String = "foo", argThree: Boolean): String } - """ # noqa: E501 - ) + """) # noqa: E501 def prints_string_field_with_multiple_args_last_is_default(): schema = build_single_field_schema( @@ -277,29 +247,25 @@ def prints_string_field_with_multiple_args_last_is_default(): ) ) - assert expect_printed_schema(schema) == dedent( - """ + assert expect_printed_schema(schema) == dedent(""" type Query { singleField(argOne: Int, argTwo: String, argThree: Boolean = false): String } - """ # noqa: E501 - ) + """) # noqa: E501 def prints_schema_with_description(): schema = GraphQLSchema( description="Schema description.", query=GraphQLObjectType("Query", {}) ) - assert expect_printed_schema(schema) == dedent( - ''' + assert expect_printed_schema(schema) == dedent(''' """Schema description.""" schema { query: Query } type Query - ''' - ) + ''') def omits_schema_of_common_names(): schema = GraphQLSchema( @@ -308,54 +274,46 @@ def omits_schema_of_common_names(): subscription=GraphQLObjectType("Subscription", {}), ) - assert expect_printed_schema(schema) == dedent( - """ + assert expect_printed_schema(schema) == dedent(""" type Query type Mutation type Subscription - """ - ) + """) def prints_custom_query_root_types(): schema = GraphQLSchema(query=GraphQLObjectType("CustomType", {})) - assert expect_printed_schema(schema) == dedent( - """ + assert expect_printed_schema(schema) == dedent(""" schema { query: CustomType } type CustomType - """ - ) + """) def prints_custom_mutation_root_types(): schema = GraphQLSchema(mutation=GraphQLObjectType("CustomType", {})) - assert expect_printed_schema(schema) == dedent( - """ + assert expect_printed_schema(schema) == dedent(""" schema { mutation: CustomType } type CustomType - """ - ) + """) def prints_custom_subscription_root_types(): schema = GraphQLSchema(subscription=GraphQLObjectType("CustomType", {})) - assert expect_printed_schema(schema) == dedent( - """ + assert expect_printed_schema(schema) == dedent(""" schema { subscription: CustomType } type CustomType - """ - ) + """) def prints_interface(): foo_type = GraphQLInterfaceType( @@ -369,8 +327,7 @@ def prints_interface(): ) schema = GraphQLSchema(types=[bar_type]) - assert expect_printed_schema(schema) == dedent( - """ + assert expect_printed_schema(schema) == dedent(""" type Bar implements Foo { str: String } @@ -378,8 +335,7 @@ def prints_interface(): interface Foo { str: String } - """ - ) + """) def prints_multiple_interfaces(): foo_type = GraphQLInterfaceType( @@ -400,8 +356,7 @@ def prints_multiple_interfaces(): ) schema = GraphQLSchema(types=[bar_type]) - assert expect_printed_schema(schema) == dedent( - """ + assert expect_printed_schema(schema) == dedent(""" type Bar implements Foo & Baz { str: String int: Int @@ -414,8 +369,7 @@ def prints_multiple_interfaces(): interface Baz { int: Int } - """ - ) + """) def prints_hierarchical_interface(): foo_type = GraphQLInterfaceType( @@ -443,8 +397,7 @@ def prints_hierarchical_interface(): query = GraphQLObjectType(name="Query", fields={"bar": GraphQLField(bar_type)}) schema = GraphQLSchema(query, types=[bar_type]) - assert expect_printed_schema(schema) == dedent( - """ + assert expect_printed_schema(schema) == dedent(""" type Bar implements Foo & Baz { str: String int: Int @@ -462,8 +415,7 @@ def prints_hierarchical_interface(): type Query { bar: Bar } - """ - ) + """) def prints_unions(): foo_type = GraphQLObjectType( @@ -481,8 +433,7 @@ def prints_unions(): ) schema = GraphQLSchema(types=[single_union, multiple_union]) - assert expect_printed_schema(schema) == dedent( - """ + assert expect_printed_schema(schema) == dedent(""" union SingleUnion = Foo type Foo { @@ -494,8 +445,7 @@ def prints_unions(): type Bar { str: String } - """ - ) + """) def prints_input_type(): input_type = GraphQLInputObjectType( @@ -503,23 +453,33 @@ def prints_input_type(): ) schema = GraphQLSchema(types=[input_type]) - assert expect_printed_schema(schema) == dedent( - """ + assert expect_printed_schema(schema) == dedent(""" input InputType { int: Int } - """ + """) + + def prints_input_type_with_one_of_directive(): + input_type = GraphQLInputObjectType( + name="InputType", + fields={"int": GraphQLInputField(GraphQLInt)}, + is_one_of=True, ) + schema = GraphQLSchema(types=[input_type]) + assert expect_printed_schema(schema) == dedent(""" + input InputType @oneOf { + int: Int + } + """) + def prints_custom_scalar(): odd_type = GraphQLScalarType(name="Odd") schema = GraphQLSchema(types=[odd_type]) - assert expect_printed_schema(schema) == dedent( - """ + assert expect_printed_schema(schema) == dedent(""" scalar Odd - """ - ) + """) def prints_custom_scalar_with_specified_by_url(): foo_type = GraphQLScalarType( @@ -527,11 +487,9 @@ def prints_custom_scalar_with_specified_by_url(): ) schema = GraphQLSchema(types=[foo_type]) - assert expect_printed_schema(schema) == dedent( - """ + assert expect_printed_schema(schema) == dedent(""" scalar Foo @specifiedBy(url: "https://example.com/foo_spec") - """ - ) + """) def prints_enum(): rgb_type = GraphQLEnumType( @@ -539,15 +497,13 @@ def prints_enum(): ) schema = GraphQLSchema(types=[rgb_type]) - assert expect_printed_schema(schema) == dedent( - """ + assert expect_printed_schema(schema) == dedent(""" enum RGB { RED GREEN BLUE } - """ - ) + """) def prints_empty_types(): schema = GraphQLSchema( @@ -560,8 +516,7 @@ def prints_empty_types(): ] ) - assert expect_printed_schema(schema) == dedent( - """ + assert expect_printed_schema(schema) == dedent(""" enum SomeEnum input SomeInputObject @@ -571,8 +526,7 @@ def prints_empty_types(): type SomeObject union SomeUnion - """ - ) + """) def prints_custom_directives(): simple_directive = GraphQLDirective( @@ -590,59 +544,65 @@ def prints_custom_directives(): ) schema = GraphQLSchema(directives=[simple_directive, complex_directive]) - assert expect_printed_schema(schema) == dedent( - ''' + assert expect_printed_schema(schema) == dedent(''' directive @simpleDirective on FIELD """Complex Directive""" directive @complexDirective(stringArg: String, intArg: Int = -1) repeatable on FIELD | QUERY - ''' # noqa: E501 + ''') # noqa: E501 + + def prints_deprecated_directives(): + schema = GraphQLSchema( + directives=[ + GraphQLDirective( + name="deprecatedDirective", + locations=[DirectiveLocation.FIELD], + deprecation_reason="Use another directive", + ), + ], ) + assert print_schema(schema) == dedent(""" + directive @deprecatedDirective @deprecated(reason: "Use another directive") on FIELD + """) # noqa: E501 + def prints_an_empty_description(): schema = build_single_field_schema(GraphQLField(GraphQLString, description="")) - assert expect_printed_schema(schema) == dedent( - ''' + assert expect_printed_schema(schema) == dedent(''' type Query { """""" singleField: String } - ''' - ) + ''') def prints_a_description_with_only_whitespace(): schema = build_single_field_schema(GraphQLField(GraphQLString, description=" ")) - assert expect_printed_schema(schema) == dedent( - """ + assert expect_printed_schema(schema) == dedent(""" type Query { " " singleField: String } - """ - ) + """) def one_line_prints_a_short_description(): schema = build_single_field_schema( GraphQLField(GraphQLString, description="This field is awesome") ) - assert expect_printed_schema(schema) == dedent( - ''' + assert expect_printed_schema(schema) == dedent(''' type Query { """This field is awesome""" singleField: String } - ''' - ) + ''') def prints_introspection_schema(): schema = GraphQLSchema() output = print_introspection_schema(schema) - assert output == dedent( - ''' + assert output == dedent(''' """ Directs the executor to include this field or fragment only when the `if` argument is true. """ @@ -665,14 +625,17 @@ def prints_introspection_schema(): Explains why this element was deprecated, usually also including a suggestion for how to access supported similar data. Formatted using the Markdown syntax, as specified by [CommonMark](https://commonmark.org/). """ reason: String = "No longer supported" - ) on FIELD_DEFINITION | ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION | ENUM_VALUE + ) on FIELD_DEFINITION | ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION | ENUM_VALUE | DIRECTIVE_DEFINITION - """Exposes a URL that specifies the behaviour of this scalar.""" + """Exposes a URL that specifies the behavior of this scalar.""" directive @specifiedBy( - """The URL that specifies the behaviour of this scalar.""" + """The URL that specifies the behavior of this scalar.""" url: String! ) on SCALAR + """Indicates an Input Object is a OneOf Input Object.""" + directive @oneOf on INPUT_OBJECT + """ A GraphQL Schema defines the capabilities of a GraphQL server. It exposes all available types and directives on the server, as well as the entry points for query, mutation, and subscription operations. """ @@ -691,12 +654,12 @@ def prints_introspection_schema(): mutationType: __Type """ - If this server support subscription, the type that subscription operations will be rooted at. + If this server supports subscription, the type that subscription operations will be rooted at. """ subscriptionType: __Type """A list of all directives supported by this server.""" - directives: [__Directive!]! + directives(includeDeprecated: Boolean! = false): [__Directive!]! } """ @@ -715,6 +678,7 @@ def prints_introspection_schema(): enumValues(includeDeprecated: Boolean = false): [__EnumValue!] inputFields(includeDeprecated: Boolean = false): [__InputValue!] ofType: __Type + isOneOf: Boolean } """An enum describing what kind of type a given `__Type` is.""" @@ -799,6 +763,8 @@ def prints_introspection_schema(): isRepeatable: Boolean! locations: [__DirectiveLocation!]! args(includeDeprecated: Boolean = false): [__InputValue!]! + isDeprecated: Boolean! + deprecationReason: String } """ @@ -861,9 +827,11 @@ def prints_introspection_schema(): """Location adjacent to an input object field definition.""" INPUT_FIELD_DEFINITION + + """Location adjacent to a directive definition.""" + DIRECTIVE_DEFINITION } - ''' # noqa: E501 - ) + ''') # noqa: E501 def describe_print_value(): diff --git a/tests/utilities/test_resolve_schema_coordinate.py b/tests/utilities/test_resolve_schema_coordinate.py new file mode 100644 index 00000000..877ee449 --- /dev/null +++ b/tests/utilities/test_resolve_schema_coordinate.py @@ -0,0 +1,163 @@ +from typing import cast + +from pytest import raises + +from graphql.type import ( + GraphQLDirective, + GraphQLEnumType, + GraphQLInputObjectType, + GraphQLNamedType, + GraphQLObjectType, +) +from graphql.utilities import ( + ResolvedDirective, + ResolvedDirectiveArgument, + ResolvedEnumValue, + ResolvedField, + ResolvedFieldArgument, + ResolvedInputField, + ResolvedNamedType, + build_schema, + resolve_schema_coordinate, +) + +schema = build_schema(""" + type Query { + searchBusiness(criteria: SearchCriteria!): [Business] + } + + input SearchCriteria { + name: String + filter: SearchFilter + } + + enum SearchFilter { + OPEN_NOW + DELIVERS_TAKEOUT + VEGETARIAN_MENU + } + + type Business { + id: ID + name: String + email: String @private(scope: "loggedIn") + } + + directive @private(scope: String!) on FIELD_DEFINITION + """) + + +def describe_resolve_schema_coordinate(): + def resolves_a_named_type(): + assert resolve_schema_coordinate(schema, "Business") == ResolvedNamedType( + cast(GraphQLNamedType, schema.get_type("Business")) + ) + + assert resolve_schema_coordinate(schema, "String") == ResolvedNamedType( + cast(GraphQLNamedType, schema.get_type("String")) + ) + + assert resolve_schema_coordinate(schema, "private") is None + + assert resolve_schema_coordinate(schema, "Unknown") is None + + def resolves_a_type_field(): + type_ = cast(GraphQLObjectType, schema.get_type("Business")) + field = type_.fields["name"] + assert resolve_schema_coordinate(schema, "Business.name") == ResolvedField( + type_, field + ) + + assert resolve_schema_coordinate(schema, "Business.unknown") is None + + with raises(TypeError) as exc_info: + resolve_schema_coordinate(schema, "Unknown.field") + assert ( + str(exc_info.value) + == "Expected 'Unknown' to be defined as a type in the schema." + ) + + with raises(TypeError) as exc_info: + resolve_schema_coordinate(schema, "String.field") + assert str(exc_info.value) == ( + "Expected 'String' to be an Enum, Input Object," + " Object or Interface type." + ) + + def resolves_an_input_field(): + type_ = cast(GraphQLInputObjectType, schema.get_type("SearchCriteria")) + input_field = type_.fields["filter"] + assert resolve_schema_coordinate( + schema, "SearchCriteria.filter" + ) == ResolvedInputField(type_, input_field) + + assert resolve_schema_coordinate(schema, "SearchCriteria.unknown") is None + + def resolves_an_enum_value(): + type_ = cast(GraphQLEnumType, schema.get_type("SearchFilter")) + enum_value = type_.values["OPEN_NOW"] + assert resolve_schema_coordinate( + schema, "SearchFilter.OPEN_NOW" + ) == ResolvedEnumValue(type_, enum_value) + + assert resolve_schema_coordinate(schema, "SearchFilter.UNKNOWN") is None + + def resolves_a_field_argument(): + type_ = cast(GraphQLObjectType, schema.get_type("Query")) + field = type_.fields["searchBusiness"] + field_argument = field.args["criteria"] + assert resolve_schema_coordinate( + schema, "Query.searchBusiness(criteria:)" + ) == ResolvedFieldArgument(type_, field, field_argument) + + assert resolve_schema_coordinate(schema, "Business.name(unknown:)") is None + + with raises(TypeError) as exc_info: + resolve_schema_coordinate(schema, "Unknown.field(arg:)") + assert ( + str(exc_info.value) + == "Expected 'Unknown' to be defined as a type in the schema." + ) + + with raises(TypeError) as exc_info: + resolve_schema_coordinate(schema, "Business.unknown(arg:)") + assert str(exc_info.value) == ( + "Expected 'unknown' to exist as a field" + " of type 'Business' in the schema." + ) + + with raises(TypeError) as exc_info: + resolve_schema_coordinate(schema, "SearchCriteria.name(arg:)") + assert ( + str(exc_info.value) + == "Expected 'SearchCriteria' to be an object type or interface type." + ) + + def resolves_a_directive(): + assert resolve_schema_coordinate(schema, "@private") == ResolvedDirective( + cast(GraphQLDirective, schema.get_directive("private")) + ) + + assert resolve_schema_coordinate(schema, "@deprecated") == ResolvedDirective( + cast(GraphQLDirective, schema.get_directive("deprecated")) + ) + + assert resolve_schema_coordinate(schema, "@unknown") is None + + assert resolve_schema_coordinate(schema, "@Business") is None + + def resolves_a_directive_argument(): + directive = cast(GraphQLDirective, schema.get_directive("private")) + directive_argument = directive.args["scope"] + assert resolve_schema_coordinate( + schema, "@private(scope:)" + ) == ResolvedDirectiveArgument(directive, directive_argument) + + assert resolve_schema_coordinate(schema, "@private(unknown:)") is None + + with raises(TypeError) as exc_info: + resolve_schema_coordinate(schema, "@unknown(arg:)") + assert ( + str(exc_info.value) + == "Expected 'unknown' to be defined as a directive in the schema." + ) diff --git a/tests/utilities/test_separate_operations.py b/tests/utilities/test_separate_operations.py index e009dbe1..6e5d63c1 100644 --- a/tests/utilities/test_separate_operations.py +++ b/tests/utilities/test_separate_operations.py @@ -10,8 +10,7 @@ def separated_asts(ast): def describe_separate_operations(): def separates_one_ast_into_multiple_maintaining_document_order(): - ast = parse( - """ + ast = parse(""" { ...Y ...X @@ -47,12 +46,10 @@ def separates_one_ast_into_multiple_maintaining_document_order(): something } - """ - ) + """) assert separated_asts(ast) == { - "": dedent( - """ + "": dedent(""" { ...Y ...X @@ -65,10 +62,8 @@ def separates_one_ast_into_multiple_maintaining_document_order(): fragment Y on T { fieldY } - """ - ), - "One": dedent( - """ + """), + "One": dedent(""" query One { foo bar @@ -88,10 +83,8 @@ def separates_one_ast_into_multiple_maintaining_document_order(): fragment B on T { something } - """ - ), - "Two": dedent( - """ + """), + "Two": dedent(""" fragment A on T { field ...B @@ -110,13 +103,11 @@ def separates_one_ast_into_multiple_maintaining_document_order(): fragment B on T { something } - """ - ), + """), } def survives_circular_dependencies(): - ast = parse( - """ + ast = parse(""" query One { ...A } @@ -132,12 +123,10 @@ def survives_circular_dependencies(): query Two { ...B } - """ - ) + """) assert separated_asts(ast) == { - "One": dedent( - """ + "One": dedent(""" query One { ...A } @@ -149,10 +138,8 @@ def survives_circular_dependencies(): fragment B on T { ...A } - """ - ), - "Two": dedent( - """ + """), + "Two": dedent(""" fragment A on T { ...B } @@ -164,13 +151,11 @@ def survives_circular_dependencies(): query Two { ...B } - """ - ), + """), } def distinguishes_query_and_fragment_names(): - ast = parse( - """ + ast = parse(""" { ...NameClash } @@ -186,12 +171,10 @@ def distinguishes_query_and_fragment_names(): fragment ShouldBeSkippedInFirstQuery on T { twoField } - """ - ) + """) assert separated_asts(ast) == { - "": dedent( - """ + "": dedent(""" { ...NameClash } @@ -199,10 +182,8 @@ def distinguishes_query_and_fragment_names(): fragment NameClash on T { oneField } - """ - ), - "NameClash": dedent( - """ + """), + "NameClash": dedent(""" query NameClash { ...ShouldBeSkippedInFirstQuery } @@ -210,13 +191,11 @@ def distinguishes_query_and_fragment_names(): fragment ShouldBeSkippedInFirstQuery on T { twoField } - """ - ), + """), } def handles_unknown_fragments(): - ast = parse( - """ + ast = parse(""" { ...Unknown ...Known @@ -225,12 +204,9 @@ def handles_unknown_fragments(): fragment Known on T { someField } - """ - ) + """) - assert separated_asts(ast) == { - "": dedent( - """ + assert separated_asts(ast) == {"": dedent(""" { ...Unknown ...Known @@ -239,6 +215,4 @@ def handles_unknown_fragments(): fragment Known on T { someField } - """ - ) - } + """)} diff --git a/tests/utilities/test_strip_ignored_characters.py b/tests/utilities/test_strip_ignored_characters.py index 67e5b4e1..915544ac 100644 --- a/tests/utilities/test_strip_ignored_characters.py +++ b/tests/utilities/test_strip_ignored_characters.py @@ -12,7 +12,7 @@ ignored_tokens = [ # UnicodeBOM - "\uFEFF", # Byte Order Mark (U+FEFF) + "\ufeff", # Byte Order Mark (U+FEFF) # WhiteSpace "\t", # Horizontal Tab (U+0009) " ", # Space (U+0020) @@ -52,23 +52,19 @@ def to_equal(self, expected: str): doc_string = self.doc_string stripped = strip_ignored_characters(doc_string) - assert stripped == expected, dedent( - f""" + assert stripped == expected, dedent(f""" Expected strip_ignored_characters({doc_string!r}) to equal {expected!r} but got {stripped!r} - """ - ) + """) stripped_twice = strip_ignored_characters(stripped) - assert stripped == stripped_twice, dedent( - f"""" + assert stripped == stripped_twice, dedent(f"""" Expected strip_ignored_characters({stripped!r})" to equal {stripped!r} but got {stripped_twice!r} - """ - ) + """) def to_stay_the_same(self): self.to_equal(self.doc_string) @@ -76,8 +72,7 @@ def to_stay_the_same(self): def describe_strip_ignored_characters(): def strips_ignored_characters_from_graphql_query_document(): - query = dedent( - """ + query = dedent(""" query SomeQuery($foo: String!, $bar: String) { someField(foo: $foo, bar: $bar) { a @@ -87,8 +82,7 @@ def strips_ignored_characters_from_graphql_query_document(): } } } - """ - ) + """) assert strip_ignored_characters(query) == ( "query SomeQuery($foo:String!$bar:String)" @@ -96,8 +90,7 @@ def strips_ignored_characters_from_graphql_query_document(): ) def strips_ignored_characters_from_graphql_sdl_document(): - sdl = dedent( - ''' + sdl = dedent(''' """ Type description """ @@ -107,8 +100,7 @@ def strips_ignored_characters_from_graphql_sdl_document(): """ bar: String } - ''' - ) + ''') assert strip_ignored_characters(sdl) == ( '"""Type description""" type Foo{"""Field description""" bar:String}' @@ -118,16 +110,14 @@ def report_document_with_invalid_token(): with raises(GraphQLSyntaxError) as exc_info: strip_ignored_characters('{ foo(arg: "\n"') - assert str(exc_info.value) == dedent( - """ + assert str(exc_info.value) == dedent(""" Syntax Error: Unterminated string. GraphQL request:1:13 1 | { foo(arg: " | ^ 2 | " - """ - ) + """) def strips_non_parsable_document(): ExpectStripped('{ foo(arg: "str"').to_equal('{foo(arg:"str"') @@ -327,13 +317,11 @@ def expect_stripped_string(block_str: str): original_value = lex_value(block_str) stripped_value = lex_value(strip_ignored_characters(block_str)) - assert original_value == stripped_value, dedent( - f""" + assert original_value == stripped_value, dedent(f""" Expected lexValue(stripIgnoredCharacters({block_str!r}) to equal {original_value!r} but got {stripped_value!r} - """ - ) + """) return ExpectStripped(block_str) expect_stripped_string('""""""').to_stay_the_same() diff --git a/tests/utilities/test_strip_ignored_characters_fuzz.py b/tests/utilities/test_strip_ignored_characters_fuzz.py index 019ec5fb..0159e5f8 100644 --- a/tests/utilities/test_strip_ignored_characters_fuzz.py +++ b/tests/utilities/test_strip_ignored_characters_fuzz.py @@ -6,7 +6,7 @@ from graphql.language import Lexer, Source, TokenKind from graphql.utilities import strip_ignored_characters -from ..utils import dedent, gen_fuzz_strings +from ..utils import dedent, gen_fuzz_strings, timeout_factor def lex_value(s: str) -> Optional[str]: @@ -18,7 +18,7 @@ def lex_value(s: str) -> Optional[str]: def describe_strip_ignored_characters(): @mark.slow - @mark.timeout(20) + @mark.timeout(80 * timeout_factor) def strips_ignored_characters_inside_random_block_strings(): # Testing with length >7 is taking exponentially more time. However it is # highly recommended to test with increased limit if you make any change. @@ -32,10 +32,8 @@ def strips_ignored_characters_inside_random_block_strings(): stripped_value = lex_value(strip_ignored_characters(test_str)) - assert test_value == stripped_value, dedent( - f""" + assert test_value == stripped_value, dedent(f""" Expected lexValue(stripIgnoredCharacters({test_str!r}) to equal {test_value!r} but got {stripped_value!r} - """ - ) + """) diff --git a/tests/utilities/test_type_info.py b/tests/utilities/test_type_info.py index 650697f4..a5364757 100644 --- a/tests/utilities/test_type_info.py +++ b/tests/utilities/test_type_info.py @@ -15,9 +15,7 @@ from ..fixtures import kitchen_sink_query # noqa: F401 - -test_schema = build_schema( - """ +test_schema = build_schema(""" interface Pet { name: String } @@ -47,8 +45,7 @@ schema { query: QueryRoot } - """ -) + """) def describe_type_info(): @@ -70,8 +67,7 @@ def allow_all_methods_to_be_called_before_entering_any_mode(): def describe_visit_with_type_info(): def supports_different_operation_types(): - schema = build_schema( - """ + schema = build_schema(""" schema { query: QueryRoot mutation: MutationRoot @@ -89,15 +85,12 @@ def supports_different_operation_types(): type SubscriptionRoot { baz: String } - """ - ) - ast = parse( - """ + """) + ast = parse(""" query { foo } mutation { bar } subscription { baz } - """ - ) + """) class TestVisitor(Visitor): def __init__(self): @@ -343,13 +336,11 @@ def leave(*args): def supports_traversal_of_input_values(): visited = [] - schema = build_schema( - """ + schema = build_schema(""" input ComplexInput { stringListField: [String] } - """ - ) + """) complex_input_type = schema.get_type("ComplexInput") assert complex_input_type is not None type_info = TypeInfo(schema, complex_input_type) diff --git a/tests/utilities/test_value_from_ast.py b/tests/utilities/test_value_from_ast.py index cd72e278..a67d5b3b 100644 --- a/tests/utilities/test_value_from_ast.py +++ b/tests/utilities/test_value_from_ast.py @@ -172,6 +172,15 @@ def coerces_non_null_lists_of_non_null_values(): }, ) + test_one_of_input_obj = GraphQLInputObjectType( + "TestOneOfInput", + { + "a": GraphQLInputField(GraphQLString), + "b": GraphQLInputField(GraphQLString), + }, + is_one_of=True, + ) + def coerces_input_objects_according_to_input_coercion_rules(): assert _value_from("null", test_input_obj) is None assert _value_from("[]", test_input_obj) is Undefined @@ -191,16 +200,30 @@ def coerces_input_objects_according_to_input_coercion_rules(): ) assert _value_from("{ requiredBool: null }", test_input_obj) is Undefined assert _value_from("{ bool: true }", test_input_obj) is Undefined + assert _value_from('{ a: "abc" }', test_one_of_input_obj) == {"a": "abc"} + assert _value_from('{ b: "def" }', test_one_of_input_obj) == {"b": "def"} + assert _value_from('{ a: "abc", b: None }', test_one_of_input_obj) is Undefined + assert _value_from("{ a: null }", test_one_of_input_obj) is Undefined + assert _value_from("{ a: 1 }", test_one_of_input_obj) is Undefined + assert _value_from('{ a: "abc", b: "def" }', test_one_of_input_obj) is Undefined + assert _value_from("{}", test_one_of_input_obj) is Undefined + assert _value_from('{ c: "abc" }', test_one_of_input_obj) is Undefined def accepts_variable_values_assuming_already_coerced(): assert _value_from("$var", GraphQLBoolean, {}) is Undefined assert _value_from("$var", GraphQLBoolean, {"var": True}) is True assert _value_from("$var", GraphQLBoolean, {"var": None}) is None assert _value_from("$var", non_null_bool, {"var": None}) is Undefined + # Python dicts have no prototype chain, so an inherited attribute name + # is just a missing key, and an explicit Undefined value is missing too. + assert _value_from("$toString", GraphQLBoolean, {}) is Undefined + assert _value_from("$var", GraphQLBoolean, {"var": Undefined}) is Undefined def asserts_variables_are_provided_as_items_in_lists(): assert _value_from("[ $foo ]", list_of_bool, {}) == [None] + assert _value_from("[ $foo ]", list_of_bool, {"foo": Undefined}) == [None] assert _value_from("[ $foo ]", list_of_non_null_bool, {}) is Undefined + assert _value_from("[ $toString ]", list_of_bool, {}) == [None] assert _value_from("[ $foo ]", list_of_non_null_bool, {"foo": True}) == [True] # Note: variables are expected to have already been coerced, so we # do not expect the singleton wrapping behavior for variables. @@ -216,6 +239,9 @@ def omits_input_object_fields_for_unprovided_variables(): "int": 42, "requiredBool": True, } + assert _value_from( + "{ int: $toString, requiredBool: true }", test_input_obj, {} + ) == {"int": 42, "requiredBool": True} def transforms_names_using_out_name(): # This is an extension of GraphQL.js. diff --git a/tests/utils/__init__.py b/tests/utils/__init__.py index a6e55a48..bc3876f0 100644 --- a/tests/utils/__init__.py +++ b/tests/utils/__init__.py @@ -1,6 +1,12 @@ """Test utilities""" +from platform import python_implementation + from .dedent import dedent from .gen_fuzz_strings import gen_fuzz_strings -__all__ = ["dedent", "gen_fuzz_strings"] +# some tests can take much longer on PyPy +timeout_factor = 4 if python_implementation() == "PyPy" else 1 + + +__all__ = ["dedent", "gen_fuzz_strings", "timeout_factor"] diff --git a/tests/utils/test_dedent.py b/tests/utils/test_dedent.py index 70849608..4e14382d 100644 --- a/tests/utils/test_dedent.py +++ b/tests/utils/test_dedent.py @@ -4,8 +4,7 @@ def describe_dedent(): def removes_indentation_in_typical_usage(): assert ( - dedent( - """ + dedent(""" type Query { me: User } @@ -14,54 +13,37 @@ def removes_indentation_in_typical_usage(): id: ID name: String } - """ - ) - == "type Query {\n me: User\n}\n\n" + """) == "type Query {\n me: User\n}\n\n" "type User {\n id: ID\n name: String\n}" ) def removes_only_the_first_level_of_indentation(): - assert ( - dedent( - """ + assert dedent(""" first second third fourth - """ - ) - == "first\n second\n third\n fourth" - ) + """) == "first\n second\n third\n fourth" def does_not_escape_special_characters(): assert ( - dedent( - """ + dedent(""" type Root { field(arg: String = "wi\th de\fault"): String } - """ - ) - == "type Root {\n" + """) == "type Root {\n" ' field(arg: String = "wi\th de\fault"): String\n}' ) def also_removes_indentation_using_tabs(): - assert ( - dedent( - """ + assert dedent(""" \t\t type Query { \t\t me: User \t\t } - """ - ) - == "type Query {\n me: User\n}" - ) + """) == "type Query {\n me: User\n}" def removes_leading_and_trailing_newlines(): - assert ( - dedent( - """ + assert dedent(""" type Query { @@ -69,30 +51,17 @@ def removes_leading_and_trailing_newlines(): } - """ - ) - == "type Query {\n me: User\n}" - ) + """) == "type Query {\n me: User\n}" def removes_all_trailing_spaces_and_tabs(): - assert ( - dedent( - """ + assert dedent(""" type Query { me: User } - \t\t \t """ - ) - == "type Query {\n me: User\n}" - ) + \t\t \t """) == "type Query {\n me: User\n}" def works_on_text_without_leading_newline(): - assert ( - dedent( - """ type Query { + assert dedent(""" type Query { me: User } - """ - ) - == "type Query {\n me: User\n}" - ) + """) == "type Query {\n me: User\n}" diff --git a/tests/validation/harness.py b/tests/validation/harness.py index 214a91bc..6952f124 100644 --- a/tests/validation/harness.py +++ b/tests/validation/harness.py @@ -1,10 +1,10 @@ -from typing import List, Optional, Type +from typing import Any, Dict, List, Optional, Type, Union from graphql.error import GraphQLError from graphql.language import parse from graphql.type import GraphQLSchema from graphql.utilities import build_schema -from graphql.validation import ValidationRule, SDLValidationRule +from graphql.validation import ASTValidationRule from graphql.validation.validate import validate, validate_sdl __all__ = [ @@ -13,8 +13,7 @@ "assert_sdl_validation_errors", ] -test_schema = build_schema( - """ +test_schema = build_schema(""" interface Mammal { mother: Mammal father: Mammal @@ -82,6 +81,11 @@ stringListField: [String] } + input OneOfInput @oneOf { + stringField: String + intField: Int + } + type ComplicatedArgs { # TODO List # TODO Coercion @@ -96,6 +100,7 @@ stringListArgField(stringListArg: [String]): String stringListNonNullArgField(stringListNonNullArg: [String!]): String complexArgField(complexArg: ComplexInput): String + oneOfArgField(oneOfArg: OneOfInput): String multipleReqs(req1: Int!, req2: Int!): String nonNullFieldWithDefault(arg: Int! = 0): String multipleOpts(opt1: Int = 0, opt2: Int = 0): String @@ -116,14 +121,13 @@ } directive @onField on FIELD - """ -) + """) def assert_validation_errors( - rule: Type[ValidationRule], + rule: Type[ASTValidationRule], query_str: str, - errors: List[GraphQLError], + errors: List[Union[GraphQLError, Dict[str, Any]]], schema: GraphQLSchema = test_schema, ) -> List[GraphQLError]: doc = parse(query_str) @@ -133,9 +137,9 @@ def assert_validation_errors( def assert_sdl_validation_errors( - rule: Type[SDLValidationRule], + rule: Type[ASTValidationRule], sdl_str: str, - errors: List[GraphQLError], + errors: List[Union[GraphQLError, Dict[str, Any]]], schema: Optional[GraphQLSchema] = None, ) -> List[GraphQLError]: doc = parse(sdl_str) diff --git a/tests/validation/test_executable_definitions.py b/tests/validation/test_executable_definitions.py index 4a21c63b..ec7fc350 100644 --- a/tests/validation/test_executable_definitions.py +++ b/tests/validation/test_executable_definitions.py @@ -11,19 +11,16 @@ def describe_validate_executable_definitions(): def with_only_operation(): - assert_valid( - """ + assert_valid(""" query Foo { dog { name } } - """ - ) + """) def with_operation_and_fragment(): - assert_valid( - """ + assert_valid(""" query Foo { dog { name @@ -34,8 +31,7 @@ def with_operation_and_fragment(): fragment Frag on Dog { name } - """ - ) + """) def with_type_definition(): assert_errors( diff --git a/tests/validation/test_fields_on_correct_type.py b/tests/validation/test_fields_on_correct_type.py index 949e2c28..6ee62250 100644 --- a/tests/validation/test_fields_on_correct_type.py +++ b/tests/validation/test_fields_on_correct_type.py @@ -7,8 +7,7 @@ from .harness import assert_validation_errors -test_schema = build_schema( - """ +test_schema = build_schema(""" interface Pet { name: String } @@ -35,8 +34,7 @@ type Query { human: Human } - """ -) + """) assert_errors = partial( @@ -48,61 +46,49 @@ def describe_validate_fields_on_correct_type(): def object_field_selection(): - assert_valid( - """ + assert_valid(""" fragment objectFieldSelection on Dog { __typename name } - """ - ) + """) def aliased_object_field_selection(): - assert_valid( - """ + assert_valid(""" fragment aliasedObjectFieldSelection on Dog { tn : __typename otherName : name } - """ - ) + """) def interface_field_selection(): - assert_valid( - """ + assert_valid(""" fragment interfaceFieldSelection on Pet { __typename name } - """ - ) + """) def aliased_interface_field_selection(): - assert_valid( - """ + assert_valid(""" fragment interfaceFieldSelection on Pet { otherName : name } - """ - ) + """) def lying_alias_selection(): - assert_valid( - """ + assert_valid(""" fragment lyingAliasSelection on Dog { name : nickname } - """ - ) + """) def ignores_fields_on_unknown_type(): - assert_valid( - """ + assert_valid(""" fragment unknownSelection on UnknownType { unknownField } - """ - ) + """) def reports_errors_when_type_is_known_again(): assert_errors( @@ -259,13 +245,11 @@ def defined_on_implementors_but_not_on_interface(): ) def meta_field_selection_on_union(): - assert_valid( - """ + assert_valid(""" fragment directFieldSelectionOnUnion on CatOrDog { __typename } - """ - ) + """) def direct_field_selection_on_union(): assert_errors( @@ -300,8 +284,7 @@ def defined_on_implementors_queried_on_union(): ) def valid_field_in_inline_fragment(): - assert_valid( - """ + assert_valid(""" fragment objectFieldSelection on Pet { ... on Dog { name @@ -310,8 +293,7 @@ def valid_field_in_inline_fragment(): name } } - """ - ) + """) def describe_fields_on_correct_type_error_message(): @@ -321,50 +303,43 @@ def _error_message(schema: GraphQLSchema, query_str: str): return errors[0].message def fields_correct_type_no_suggestion(): - schema = build_schema( - """ + schema = build_schema(""" type T { fieldWithVeryLongNameThatWillNeverBeSuggested: String } type Query { t: T } - """ - ) + """) assert _error_message(schema, "{ t { f } }") == ( "Cannot query field 'f' on type 'T'." ) def works_with_no_small_numbers_of_type_suggestion(): - schema = build_schema( - """ + schema = build_schema(""" union T = A | B type Query { t: T } type A { f: String } type B { f: String } - """ - ) + """) assert _error_message(schema, "{ t { f } }") == ( "Cannot query field 'f' on type 'T'." " Did you mean to use an inline fragment on 'A' or 'B'?" ) def works_with_no_small_numbers_of_field_suggestion(): - schema = build_schema( - """ + schema = build_schema(""" type T { y: String z: String } type Query { t: T } - """ - ) + """) assert _error_message(schema, "{ t { f } }") == ( "Cannot query field 'f' on type 'T'. Did you mean 'y' or 'z'?" ) def only_shows_one_set_of_suggestions_at_a_time_preferring_types(): - schema = build_schema( - """ + schema = build_schema(""" interface T { y: String z: String @@ -381,16 +356,14 @@ def only_shows_one_set_of_suggestions_at_a_time_preferring_types(): y: String z: String } - """ - ) + """) assert _error_message(schema, "{ t { f } }") == ( "Cannot query field 'f' on type 'T'." " Did you mean to use an inline fragment on 'A' or 'B'?" ) def sort_type_suggestions_based_on_inheritance_order(): - interface_schema = build_schema( - """ + interface_schema = build_schema(""" interface T { bar: String } type Query { t: T } @@ -408,16 +381,14 @@ def sort_type_suggestions_based_on_inheritance_order(): foo: String bar: String } - """ - ) + """) assert _error_message(interface_schema, "{ t { foo } }") == ( "Cannot query field 'foo' on type 'T'." " Did you mean to use an inline fragment on 'Z', 'Y', or 'X'?" ) - union_schema = build_schema( - """ + union_schema = build_schema(""" interface Animal { name: String } interface Mammal implements Animal { name: String } @@ -429,8 +400,7 @@ def sort_type_suggestions_based_on_inheritance_order(): union CatOrDog = Cat | Dog type Query { catOrDog: CatOrDog } - """ - ) + """) assert _error_message(union_schema, "{ catOrDog { name } }") == ( "Cannot query field 'name' on type 'CatOrDog'." @@ -439,8 +409,7 @@ def sort_type_suggestions_based_on_inheritance_order(): ) def limits_lots_of_type_suggestions(): - schema = build_schema( - """ + schema = build_schema(""" union T = A | B | C | D | E | F type Query { t: T } @@ -450,16 +419,14 @@ def limits_lots_of_type_suggestions(): type D { f: String } type E { f: String } type F { f: String } - """ - ) + """) assert _error_message(schema, "{ t { f } }") == ( "Cannot query field 'f' on type 'T'. Did you mean to use" " an inline fragment on 'A', 'B', 'C', 'D', or 'E'?" ) def limits_lots_of_field_suggestions(): - schema = build_schema( - """ + schema = build_schema(""" type T { u: String v: String @@ -469,8 +436,7 @@ def limits_lots_of_field_suggestions(): z: String } type Query { t: T } - """ - ) + """) assert _error_message(schema, "{ t { f } }") == ( "Cannot query field 'f' on type 'T'." " Did you mean 'u', 'v', 'w', 'x', or 'y'?" diff --git a/tests/validation/test_fragments_on_composite_types.py b/tests/validation/test_fragments_on_composite_types.py index a4957668..e6f3a9c8 100644 --- a/tests/validation/test_fragments_on_composite_types.py +++ b/tests/validation/test_fragments_on_composite_types.py @@ -11,64 +11,52 @@ def describe_validate_fragments_on_composite_types(): def object_is_valid_fragment_type(): - assert_valid( - """ + assert_valid(""" fragment validFragment on Dog { barks } - """ - ) + """) def interface_is_valid_fragment_type(): - assert_valid( - """ + assert_valid(""" fragment validFragment on Pet { name } - """ - ) + """) def object_is_valid_inline_fragment_type(): - assert_valid( - """ + assert_valid(""" fragment validFragment on Pet { ... on Dog { barks } } - """ - ) + """) def interface_is_valid_inline_fragment_type(): - assert_valid( - """ + assert_valid(""" fragment validFragment on Mammal { ... on Canine { name } } - """ - ) + """) def inline_fragment_without_type_is_valid(): - assert_valid( - """ + assert_valid(""" fragment validFragment on Pet { ... { name } } - """ - ) + """) def union_is_valid_fragment_type(): - assert_valid( - """ + assert_valid(""" fragment validFragment on CatOrDog { __typename } - """ - ) + """) def scalar_is_invalid_fragment_type(): assert_errors( diff --git a/tests/validation/test_known_argument_names.py b/tests/validation/test_known_argument_names.py index 3291b7c9..043be7d5 100644 --- a/tests/validation/test_known_argument_names.py +++ b/tests/validation/test_known_argument_names.py @@ -21,53 +21,42 @@ def describe_validate_known_argument_names(): def single_arg_is_known(): - assert_valid( - """ + assert_valid(""" fragment argOnRequiredArg on Dog { doesKnowCommand(dogCommand: SIT) } - """ - ) + """) def multiple_args_are_known(): - assert_valid( - """ + assert_valid(""" fragment multipleArgs on ComplicatedArgs { multipleReqs(req1: 1, req2: 2) } - """ - ) + """) def ignore_args_of_unknown_fields(): - assert_valid( - """ + assert_valid(""" fragment argOnUnknownField on Dog { unknownField(unknownArg: SIT) } - """ - ) + """) def multiple_args_in_reverse_order_are_known(): - assert_valid( - """ + assert_valid(""" fragment multipleArgsReverseOrder on ComplicatedArgs { multipleReqs(req2: 2, req1: 1) } - """ - ) + """) def no_args_on_optional_arg(): - assert_valid( - """ + assert_valid(""" fragment noArgOnOptionalArg on Dog { isHouseTrained } - """ - ) + """) def args_are_known_deeply(): - assert_valid( - """ + assert_valid(""" { dog { doesKnowCommand(dogCommand: SIT) @@ -80,17 +69,14 @@ def args_are_known_deeply(): } } } - """ - ) + """) def directive_args_are_known(): - assert_valid( - """ + assert_valid(""" { dog @skip(if: true) } - """ - ) + """) def field_args_are_invalid(): assert_errors( @@ -108,13 +94,11 @@ def field_args_are_invalid(): ) def directive_without_args_is_valid(): - assert_valid( - """ + assert_valid(""" { dog @onField } - """ - ) + """) def arg_passed_to_directive_without_args_is_reported(): assert_errors( @@ -233,15 +217,13 @@ def unknown_args_deeply(): def describe_within_sdl(): def known_arg_on_directive_inside_sdl(): - assert_sdl_valid( - """ + assert_sdl_valid(""" type Query { foo: String @test(arg: "") } directive @test(arg: String) on FIELD_DEFINITION - """ - ) + """) def unknown_arg_on_directive_defined_inside_sdl(): assert_sdl_errors( @@ -312,13 +294,11 @@ def unknown_arg_on_overridden_standard_directive(): ) def unknown_arg_on_directive_defined_in_schema_extension(): - schema = build_schema( - """ + schema = build_schema(""" type Query { foo: String } - """ - ) + """) assert_sdl_errors( """ directive @test(arg: String) on OBJECT @@ -335,15 +315,13 @@ def unknown_arg_on_directive_defined_in_schema_extension(): ) def unknown_arg_on_directive_used_in_schema_extension(): - schema = build_schema( - """ + schema = build_schema(""" directive @test(arg: String) on OBJECT type Query { foo: String } - """ - ) + """) assert_sdl_errors( """ extend type Query @test(unknown: "") diff --git a/tests/validation/test_known_directives.py b/tests/validation/test_known_directives.py index 3c837ba0..ef5e7b8d 100644 --- a/tests/validation/test_known_directives.py +++ b/tests/validation/test_known_directives.py @@ -5,8 +5,7 @@ from .harness import assert_validation_errors, assert_sdl_validation_errors -schema_with_directives = build_schema( - """ +schema_with_directives = build_schema(""" type Query { dummy: String } @@ -19,11 +18,9 @@ directive @onFragmentSpread on FRAGMENT_SPREAD directive @onInlineFragment on INLINE_FRAGMENT directive @onVariableDefinition on VARIABLE_DEFINITION - """ -) + """) -schema_with_sdl_directives = build_schema( - """ +schema_with_sdl_directives = build_schema(""" directive @onSchema on SCHEMA directive @onScalar on SCALAR directive @onObject on OBJECT @@ -35,8 +32,8 @@ directive @onEnumValue on ENUM_VALUE directive @onInputObject on INPUT_OBJECT directive @onInputFieldDefinition on INPUT_FIELD_DEFINITION - """ -) + directive @onDirective on DIRECTIVE_DEFINITION + """) assert_errors = partial( assert_validation_errors, KnownDirectivesRule, schema=schema_with_directives @@ -51,8 +48,7 @@ def describe_known_directives(): def with_no_directives(): - assert_valid( - """ + assert_valid(""" query Foo { name ...Frag @@ -61,12 +57,10 @@ def with_no_directives(): fragment Frag on Dog { name } - """ - ) + """) def with_standard_directives(): - assert_valid( - """ + assert_valid(""" { human @skip(if: false) { name @@ -77,8 +71,7 @@ def with_standard_directives(): } } } - """ - ) + """) def with_unknown_directive(): assert_errors( @@ -113,8 +106,7 @@ def with_many_unknown_directives(): ) def with_well_placed_directives(): - assert_valid( - """ + assert_valid(""" query ($var: Boolean @onVariableDefinition) @onQuery { human @onField { ...Frag @onFragmentSpread @@ -135,8 +127,7 @@ def with_well_placed_directives(): fragment Frag on Human @onFragmentDefinition { name @onField } - """ - ) + """) def with_misplaced_directives(): assert_errors( @@ -236,43 +227,35 @@ def with_misplaced_variable_definition_directive(): def describe_within_sdl(): def with_directive_defined_inside_sdl(): - assert_sdl_valid( - """ + assert_sdl_valid(""" type Query { foo: String @test } directive @test on FIELD_DEFINITION - """ - ) + """) def with_standard_directive(): - assert_sdl_valid( - """ + assert_sdl_valid(""" type Query { foo: String @deprecated } - """ - ) + """) def with_overridden_standard_directive(): - assert_sdl_valid( - """ + assert_sdl_valid(""" schema @deprecated { query: Query } directive @deprecated on SCHEMA - """ - ) + """) def with_directive_defined_in_schema_extension(): - schema = build_schema( - """ + schema = build_schema(""" type Query { foo: String } - """ - ) + """) assert_sdl_valid( """ directive @test on OBJECT @@ -283,15 +266,13 @@ def with_directive_defined_in_schema_extension(): ) def with_directive_used_in_schema_extension(): - schema = build_schema( - """ + schema = build_schema(""" directive @test on OBJECT type Query { foo: String } - """ - ) + """) assert_sdl_valid( """ extend type Query @test @@ -300,13 +281,11 @@ def with_directive_used_in_schema_extension(): ) def with_unknown_directive_in_schema_extension(): - schema = build_schema( - """ + schema = build_schema(""" type Query { foo: String } - """ - ) + """) assert_sdl_errors( """ extend type Query @unknown @@ -355,6 +334,8 @@ def with_well_placed_directives(): } extend schema @onSchema + + directive @myDirective on OBJECT """, schema=schema_with_sdl_directives, ) @@ -387,6 +368,8 @@ def with_misplaced_directives(): } extend schema @onObject + + extend type MyObj @onDirective """, # noqa: E501 [ { @@ -454,6 +437,11 @@ def with_misplaced_directives(): "message": "Directive '@onObject' may not be used on schema.", "locations": [(26, 31)], }, + { + "message": "Directive '@onDirective'" + " may not be used on object.", + "locations": [(28, 35)], + }, ], schema_with_sdl_directives, ) diff --git a/tests/validation/test_known_fragment_names.py b/tests/validation/test_known_fragment_names.py index 8a9b864b..1a1d4665 100644 --- a/tests/validation/test_known_fragment_names.py +++ b/tests/validation/test_known_fragment_names.py @@ -11,8 +11,7 @@ def describe_validate_known_fragment_names(): def known_fragment_names_are_valid(): - assert_valid( - """ + assert_valid(""" { human(id: 4) { ...HumanFields1 @@ -34,8 +33,7 @@ def known_fragment_names_are_valid(): fragment HumanFields3 on Human { name } - """ - ) + """) def unknown_fragment_names_are_invalid(): assert_errors( diff --git a/tests/validation/test_known_type_names.py b/tests/validation/test_known_type_names.py index 4b4683ae..91e55af8 100644 --- a/tests/validation/test_known_type_names.py +++ b/tests/validation/test_known_type_names.py @@ -16,8 +16,7 @@ def describe_validate_known_type_names(): def known_type_names_are_valid(): - assert_valid( - """ + assert_valid(""" query Foo( $var: String $required: [Int!]! @@ -31,8 +30,7 @@ def known_type_names_are_valid(): fragment PetFields on Pet { name } - """ - ) + """) def unknown_type_names_are_invalid(): assert_errors( @@ -79,8 +77,7 @@ def references_to_standard_scalars_that_are_missing_in_schema(): def describe_within_sdl(): def use_standard_types(): - assert_sdl_valid( - """ + assert_sdl_valid(""" type Query { string: String int: Int @@ -89,12 +86,10 @@ def use_standard_types(): id: ID introspectionType: __EnumValue } - """ - ) + """) def reference_types_defined_inside_the_same_document(): - assert_sdl_valid( - """ + assert_sdl_valid(""" union SomeUnion = SomeObject | AnotherObject type SomeObject implements SomeInterface { @@ -125,8 +120,7 @@ def reference_types_defined_inside_the_same_document(): schema { query: RootQuery } - """ - ) + """) def unknown_type_references(): assert_sdl_errors( diff --git a/tests/validation/test_lone_anonymous_operation.py b/tests/validation/test_lone_anonymous_operation.py index 83e431bf..20c38988 100644 --- a/tests/validation/test_lone_anonymous_operation.py +++ b/tests/validation/test_lone_anonymous_operation.py @@ -11,26 +11,21 @@ def describe_validate_anonymous_operation_must_be_alone(): def no_operations(): - assert_valid( - """ + assert_valid(""" fragment fragA on Type { field } - """ - ) + """) def one_anon_operation(): - assert_valid( - """ + assert_valid(""" { field } - """ - ) + """) def multiple_named_operation(): - assert_valid( - """ + assert_valid(""" query Foo { field } @@ -38,20 +33,17 @@ def multiple_named_operation(): query Bar { field } - """ - ) + """) def anon_operation_with_fragment(): - assert_valid( - """ + assert_valid(""" { ...Foo } fragment Foo on Type { field } - """ - ) + """) def multiple_anon_operations(): assert_errors( diff --git a/tests/validation/test_lone_schema_definition.py b/tests/validation/test_lone_schema_definition.py index 9b6d88fa..c0c32e58 100644 --- a/tests/validation/test_lone_schema_definition.py +++ b/tests/validation/test_lone_schema_definition.py @@ -12,17 +12,14 @@ def describe_validate_schema_definition_should_be_alone(): def no_schema(): - assert_sdl_valid( - """ + assert_sdl_valid(""" type Query { foo: String } - """ - ) + """) def one_schema_definition(): - assert_sdl_valid( - """ + assert_sdl_valid(""" schema { query: Foo } @@ -30,8 +27,7 @@ def one_schema_definition(): type Foo { foo: String } - """ - ) + """) def multiple_schema_definitions(): assert_sdl_errors( @@ -65,13 +61,11 @@ def multiple_schema_definitions(): ) def define_schema_in_schema_extension(): - schema = build_schema( - """ + schema = build_schema(""" type Foo { foo: String } - """ - ) + """) assert_sdl_valid( """ @@ -83,8 +77,7 @@ def define_schema_in_schema_extension(): ) def redefine_schema_in_schema_extension(): - schema = build_schema( - """ + schema = build_schema(""" schema { query: Foo } @@ -92,8 +85,7 @@ def redefine_schema_in_schema_extension(): type Foo { foo: String } - """ - ) + """) assert_sdl_errors( """ @@ -111,8 +103,7 @@ def redefine_schema_in_schema_extension(): ) def redefine_implicit_schema_in_schema_extension(): - schema = build_schema( - """ + schema = build_schema(""" type Query { fooField: Foo } @@ -120,8 +111,7 @@ def redefine_implicit_schema_in_schema_extension(): type Foo { foo: String } - """ - ) + """) assert_sdl_errors( """ @@ -139,8 +129,7 @@ def redefine_implicit_schema_in_schema_extension(): ) def extend_schema_in_schema_extension(): - schema = build_schema( - """ + schema = build_schema(""" type Query { fooField: Foo } @@ -148,8 +137,7 @@ def extend_schema_in_schema_extension(): type Foo { foo: String } - """ - ) + """) assert_sdl_valid( """ diff --git a/tests/validation/test_max_introspection_depth_rule.py b/tests/validation/test_max_introspection_depth_rule.py new file mode 100644 index 00000000..f634ce3c --- /dev/null +++ b/tests/validation/test_max_introspection_depth_rule.py @@ -0,0 +1,490 @@ +from functools import partial + +from graphql.utilities import get_introspection_query +from graphql.validation import MaxIntrospectionDepthRule + +from .harness import assert_validation_errors + +assert_errors = partial(assert_validation_errors, MaxIntrospectionDepthRule) + +assert_valid = partial(assert_errors, errors=[]) + + +def describe_validate_max_introspection_nodes_rule(): + def default_introspection_query(): + assert_valid(get_introspection_query()) + + def all_ptions_introspection_query(): + assert_valid( + get_introspection_query( + descriptions=True, + specified_by_url=True, + directive_is_repeatable=True, + schema_description=True, + input_value_deprecation=True, + ) + ) + + def three_flat_fields_introspection_query(): + assert_valid(""" + { + __type(name: "Query") { + trueFields: fields(includeDeprecated: true) { + name + } + falseFields: fields(includeDeprecated: false) { + name + } + omittedFields: fields { + name + } + } + } + """) + + def three_fields_deep_introspection_query_from_schema(): + assert_errors( + """ + { + __schema { + types { + fields { + type { + fields { + type { + fields { + name + } + } + } + } + } + } + } + } + """, + [ + { + "message": "Maximum introspection depth exceeded", + "locations": [(3, 15)], + }, + ], + ) + + def three_interfaces_deep_introspection_query_from_schema(): + assert_errors( + """ + { + __schema { + types { + interfaces { + interfaces { + interfaces { + name + } + } + } + } + } + } + """, + [ + { + "message": "Maximum introspection depth exceeded", + "locations": [(3, 15)], + }, + ], + ) + + def three_possible_types_deep_introspection_query_from_schema(): + assert_errors( + """ + { + __schema { + types { + possibleTypes { + possibleTypes { + possibleTypes { + name + } + } + } + } + } + } + """, + [ + { + "message": "Maximum introspection depth exceeded", + "locations": [(3, 15)], + }, + ], + ) + + def three_input_fields_deep_introspection_query_from_schema(): + assert_errors( + """ + { + __schema { + types { + inputFields { + type { + inputFields { + type { + inputFields { + type { + name + } + } + } + } + } + } + } + } + } + """, + [ + { + "message": "Maximum introspection depth exceeded", + "locations": [(3, 15)], + }, + ], + ) + + def three_fields_deep_introspection_query_from_multiple_schema(): + assert_errors( + """ + { + one: __schema { + types { + fields { + type { + fields { + type { + fields { + name + } + } + } + } + } + } + } + two: __schema { + types { + fields { + type { + fields { + type { + fields { + name + } + } + } + } + } + } + } + three: __schema { + types { + fields { + type { + fields { + type { + fields { + name + } + } + } + } + } + } + } + } + """, + [ + { + "message": "Maximum introspection depth exceeded", + "locations": [(3, 15)], + }, + { + "message": "Maximum introspection depth exceeded", + "locations": [(18, 15)], + }, + { + "message": "Maximum introspection depth exceeded", + "locations": [(33, 15)], + }, + ], + ) + + def three_fields_deep_introspection_query_from_type(): + assert_errors( + """ + { + __type(name: "Query") { + types { + fields { + type { + fields { + type { + fields { + name + } + } + } + } + } + } + } + } + """, + [ + { + "message": "Maximum introspection depth exceeded", + "locations": [(3, 15)], + }, + ], + ) + + def three_fields_deep_introspection_query_from_multiple_type(): + assert_errors( + """ + { + one: __type(name: "Query") { + types { + fields { + type { + fields { + type { + fields { + name + } + } + } + } + } + } + } + two: __type(name: "Query") { + types { + fields { + type { + fields { + type { + fields { + name + } + } + } + } + } + } + } + three: __type(name: "Query") { + types { + fields { + type { + fields { + type { + fields { + name + } + } + } + } + } + } + } + } +""", + [ + { + "message": "Maximum introspection depth exceeded", + "locations": [(3, 15)], + }, + { + "message": "Maximum introspection depth exceeded", + "locations": [(18, 15)], + }, + { + "message": "Maximum introspection depth exceeded", + "locations": [(33, 15)], + }, + ], + ) + + def one_fields_deep_with_three_fields_introspection_query(): + assert_valid(""" + { + __schema { + types { + fields { + type { + oneFields: fields { + name + } + twoFields: fields { + name + } + threeFields: fields { + name + } + } + } + } + } + } + """) + + def three_fields_deep_from_varying_parents_introspection_query(): + assert_errors( + """ + { + __schema { + types { + fields { + type { + fields { + type { + ofType { + fields { + name + } + } + } + } + } + } + } + } + } + """, + [ + { + "message": "Maximum introspection depth exceeded", + "locations": [(3, 15)], + }, + ], + ) + + def three_fields_deep_introspection_query_with_inline_fragments(): + assert_errors( + """ + query test { + __schema { + types { + ... on __Type { + fields { + type { + ... on __Type { + ofType { + fields { + type { + ... on __Type { + fields { + name + } + } + } + } + } + } + } + } + } + } + } + } + """, + [ + { + "message": "Maximum introspection depth exceeded", + "locations": [(3, 15)], + }, + ], + ) + + def three_fields_deep_introspection_query_with_fragments(): + assert_errors( + """ + query test { + __schema { + types { + ...One + } + } + } + + fragment One on __Type { + fields { + type { + ...Two + } + } + } + + fragment Two on __Type { + fields { + type { + ...Three + } + } + } + + fragment Three on __Type { + fields { + name + } + } + """, + [ + { + "message": "Maximum introspection depth exceeded", + "locations": [(3, 15)], + }, + ], + ) + + def three_fields_deep_inline_fragment_on_query(): + assert_errors( + """ + { + ... { + __schema { types { fields { type { fields { type { fields { name } } } } } } } + } + } + """, # noqa: E501 + [ + { + "message": "Maximum introspection depth exceeded", + "locations": [(4, 17)], + }, + ], + ) + + def opts_out_if_fragment_is_missing(): + assert_valid(""" + query test { + __schema { + types { + ...Missing + } + } + } + """) + + def does_not_infinitely_recurse_on_fragment_cycle(): + assert_valid(""" + query test { + __schema { + types { + ...Cycle + } + } + } + fragment Cycle on __Type { + ...Cycle + } + """) diff --git a/tests/validation/test_no_deprecated.py b/tests/validation/test_no_deprecated.py index c4ac992a..6ef2b88e 100644 --- a/tests/validation/test_no_deprecated.py +++ b/tests/validation/test_no_deprecated.py @@ -23,27 +23,22 @@ def build_assertions( def describe_validate_no_deprecated(): def describe_no_deprecated_fields(): - _assert_valid, _assert_errors = build_assertions( - """ + _assert_valid, _assert_errors = build_assertions(""" type Query { normalField: String deprecatedField: String @deprecated(reason: "Some field reason.") } - """ - ) + """) def ignores_fields_and_enum_values_that_are_not_deprecated(): - _assert_valid( - """ + _assert_valid(""" { normalField } - """ - ) + """) def ignores_unknown_fields(): - _assert_valid( - """ + _assert_valid(""" { unknownField } @@ -51,8 +46,7 @@ def ignores_unknown_fields(): fragment UnknownFragment on UnknownType { deprecatedField } - """ - ) + """) def reports_error_when_a_deprecated_field_is_selected(): message = ( @@ -81,35 +75,29 @@ def reports_error_when_a_deprecated_field_is_selected(): ) def describe_no_deprecated_arguments_on_fields(): - _assert_valid, _assert_errors = build_assertions( - """ + _assert_valid, _assert_errors = build_assertions(""" type Query { someField( normalArg: String, deprecatedArg: String @deprecated(reason: "Some arg reason."), ): String } - """ - ) + """) def ignores_arguments_that_are_not_deprecated(): - _assert_valid( - """ + _assert_valid(""" { normalField(normalArg: "") } - """ - ) + """) def ignores_unknown_arguments(): - _assert_valid( - """ + _assert_valid(""" { someField(unknownArg: "") unknownField(deprecatedArg: "") } - """ - ) + """) def reports_error_when_a_deprecated_argument_is_used(): _assert_errors( @@ -128,8 +116,7 @@ def reports_error_when_a_deprecated_argument_is_used(): ) def describe_no_deprecated_arguments_on_directives(): - _assert_valid, _assert_errors = build_assertions( - """ + _assert_valid, _assert_errors = build_assertions(""" type Query { someField: String } @@ -138,27 +125,22 @@ def describe_no_deprecated_arguments_on_directives(): normalArg: String, deprecatedArg: String @deprecated(reason: "Some arg reason."), ) on FIELD - """ - ) + """) def ignores_arguments_that_are_not_deprecated(): - _assert_valid( - """ + _assert_valid(""" { someField @someDirective(normalArg: "") } - """ - ) + """) def ignores_unknown_arguments(): - _assert_valid( - """ + _assert_valid(""" { someField @someDirective(unknownArg: "") someField @unknownDirective(deprecatedArg: "") } - """ - ) + """) def reports_error_when_a_deprecated_argument_is_used(): _assert_errors( @@ -177,8 +159,7 @@ def reports_error_when_a_deprecated_argument_is_used(): ) def describe_no_deprecated_input_fields(): - _assert_valid, _assert_errors = build_assertions( - """ + _assert_valid, _assert_errors = build_assertions(""" input InputType { normalField: String deprecatedField: String @deprecated(reason: "Some input field reason.") @@ -189,23 +170,19 @@ def describe_no_deprecated_input_fields(): } directive @someDirective(someArg: InputType) on FIELD - """ - ) + """) def ignores_input_fields_that_are_not_deprecated(): - _assert_valid( - """ + _assert_valid(""" { someField( someArg: { normalField: "" } ) @someDirective(someArg: { normalField: "" }) } - """ - ) + """) def ignores_unknown_input_fields(): - _assert_valid( - """ + _assert_valid(""" { someField( someArg: { unknownField: "" } @@ -219,8 +196,7 @@ def ignores_unknown_input_fields(): unknownArg: { unknownField: "" } ) } - """ - ) + """) def reports_error_when_a_deprecated_input_field_is_used(): message = ( @@ -243,8 +219,7 @@ def reports_error_when_a_deprecated_input_field_is_used(): ) def describe_no_deprecated_enum_values(): - _assert_valid, _assert_errors = build_assertions( - """ + _assert_valid, _assert_errors = build_assertions(""" enum EnumType { NORMAL_VALUE DEPRECATED_VALUE @deprecated(reason: "Some enum reason.") @@ -253,21 +228,17 @@ def describe_no_deprecated_enum_values(): type Query { someField(enumArg: EnumType): String } - """ - ) + """) def ignores_enum_values_that_are_not_deprecated(): - _assert_valid( - """ + _assert_valid(""" { normalField(enumArg: NORMAL_VALUE) } - """ - ) + """) def ignores_unknown_enum_values(): - _assert_valid( - """ + _assert_valid(""" query ( $unknownValue: EnumType = UNKNOWN_VALUE $unknownType: UnknownType = UNKNOWN_VALUE @@ -280,8 +251,7 @@ def ignores_unknown_enum_values(): fragment SomeFragment on Query { someField(enumArg: UNKNOWN_VALUE) } - """ - ) + """) def reports_error_when_a_deprecated_enum_value_is_used(): message = ( diff --git a/tests/validation/test_no_fragment_cycles.py b/tests/validation/test_no_fragment_cycles.py index 3bc60a0a..a0abfbc1 100644 --- a/tests/validation/test_no_fragment_cycles.py +++ b/tests/validation/test_no_fragment_cycles.py @@ -11,33 +11,26 @@ def describe_validate_no_circular_fragment_spreads(): def single_reference_is_valid(): - assert_valid( - """ + assert_valid(""" fragment fragA on Dog { ...fragB } fragment fragB on Dog { name } - """ - ) + """) def spreading_twice_is_not_circular(): - assert_valid( - """ + assert_valid(""" fragment fragA on Dog { ...fragB, ...fragB } fragment fragB on Dog { name } - """ - ) + """) def spreading_twice_indirectly_is_not_circular(): - assert_valid( - """ + assert_valid(""" fragment fragA on Dog { ...fragB, ...fragC } fragment fragB on Dog { ...fragC } fragment fragC on Dog { name } - """ - ) + """) def double_spread_within_abstract_types(): - assert_valid( - """ + assert_valid(""" fragment nameFragment on Pet { ... on Dog { name } ... on Cat { name } @@ -46,17 +39,14 @@ def double_spread_within_abstract_types(): ... on Dog { ...nameFragment } ... on Cat { ...nameFragment } } - """ - ) + """) def does_not_raise_false_positive_on_unknown_fragment(): - assert_valid( - """ + assert_valid(""" fragment nameFragment on Pet { ...UnknownFragment } - """ - ) + """) def spreading_recursively_within_field_fails(): assert_errors( diff --git a/tests/validation/test_no_schema_introspection.py b/tests/validation/test_no_schema_introspection.py index 03f1ddc8..6a3a6c3e 100644 --- a/tests/validation/test_no_schema_introspection.py +++ b/tests/validation/test_no_schema_introspection.py @@ -5,8 +5,7 @@ from .harness import assert_validation_errors -schema = build_schema( - """ +schema = build_schema(""" type Query { someQuery: SomeType } @@ -15,8 +14,7 @@ someField: String introspectionField: __EnumValue } - """ -) + """) assert_errors = partial( assert_validation_errors, NoSchemaIntrospectionCustomRule, schema=schema @@ -27,25 +25,21 @@ def describe_validate_prohibit_introspection_queries(): def ignores_valid_fields_including_typename(): - assert_valid( - """ + assert_valid(""" { someQuery { __typename someField } } - """ - ) + """) def ignores_fields_not_in_the_schema(): - assert_valid( - """ + assert_valid(""" { __introspect } - """ - ) + """) def reports_error_when_a_field_with_an_introspection_type_is_requested(): assert_errors( diff --git a/tests/validation/test_no_undefined_variables.py b/tests/validation/test_no_undefined_variables.py index f9537234..4460d83b 100644 --- a/tests/validation/test_no_undefined_variables.py +++ b/tests/validation/test_no_undefined_variables.py @@ -11,17 +11,14 @@ def describe_validate_no_undefined_variables(): def all_variables_defined(): - assert_valid( - """ + assert_valid(""" query Foo($a: String, $b: String, $c: String) { field(a: $a, b: $b, c: $c) } - """ - ) + """) def all_variables_deeply_defined(): - assert_valid( - """ + assert_valid(""" query Foo($a: String, $b: String, $c: String) { field(a: $a) { field(b: $b) { @@ -29,12 +26,10 @@ def all_variables_deeply_defined(): } } } - """ - ) + """) def all_variables_deeply_in_inline_fragments_defined(): - assert_valid( - """ + assert_valid(""" query Foo($a: String, $b: String, $c: String) { ... on Type { field(a: $a) { @@ -46,12 +41,10 @@ def all_variables_deeply_in_inline_fragments_defined(): } } } - """ - ) + """) def all_variables_in_fragments_deeply_defined(): - assert_valid( - """ + assert_valid(""" query Foo($a: String, $b: String, $c: String) { ...FragA } @@ -68,12 +61,10 @@ def all_variables_in_fragments_deeply_defined(): fragment FragC on Type { field(c: $c) } - """ - ) + """) def variable_within_single_fragment_defined_in_multiple_operations(): - assert_valid( - """ + assert_valid(""" query Foo($a: String) { ...FragA } @@ -83,12 +74,10 @@ def variable_within_single_fragment_defined_in_multiple_operations(): fragment FragA on Type { field(a: $a) } - """ - ) + """) def variable_within_fragments_defined_in_operations(): - assert_valid( - """ + assert_valid(""" query Foo($a: String) { ...FragA } @@ -101,12 +90,10 @@ def variable_within_fragments_defined_in_operations(): fragment FragB on Type { field(b: $b) } - """ - ) + """) def variable_within_recursive_fragment_defined(): - assert_valid( - """ + assert_valid(""" query Foo($a: String) { ...FragA } @@ -115,8 +102,7 @@ def variable_within_recursive_fragment_defined(): ...FragA } } - """ - ) + """) def variable_not_defined(): assert_errors( diff --git a/tests/validation/test_no_unused_fragments.py b/tests/validation/test_no_unused_fragments.py index f317d1f5..0e94b863 100644 --- a/tests/validation/test_no_unused_fragments.py +++ b/tests/validation/test_no_unused_fragments.py @@ -11,8 +11,7 @@ def describe_validate_no_unused_fragments(): def all_fragment_names_are_used(): - assert_valid( - """ + assert_valid(""" { human(id: 4) { ...HumanFields1 @@ -31,12 +30,10 @@ def all_fragment_names_are_used(): fragment HumanFields3 on Human { name } - """ - ) + """) def all_fragment_names_are_used_by_multiple_operations(): - assert_valid( - """ + assert_valid(""" query Foo { human(id: 4) { ...HumanFields1 @@ -57,8 +54,7 @@ def all_fragment_names_are_used_by_multiple_operations(): fragment HumanFields3 on Human { name } - """ - ) + """) def contains_unknown_fragments(): assert_errors( diff --git a/tests/validation/test_no_unused_variables.py b/tests/validation/test_no_unused_variables.py index 7366ba55..0fe3636a 100644 --- a/tests/validation/test_no_unused_variables.py +++ b/tests/validation/test_no_unused_variables.py @@ -11,17 +11,14 @@ def describe_validate_no_unused_variables(): def uses_all_variables(): - assert_valid( - """ + assert_valid(""" query ($a: String, $b: String, $c: String) { field(a: $a, b: $b, c: $c) } - """ - ) + """) def uses_all_variables_deeply(): - assert_valid( - """ + assert_valid(""" query Foo($a: String, $b: String, $c: String) { field(a: $a) { field(b: $b) { @@ -29,12 +26,10 @@ def uses_all_variables_deeply(): } } } - """ - ) + """) def uses_all_variables_deeply_in_inline_fragments(): - assert_valid( - """ + assert_valid(""" query Foo($a: String, $b: String, $c: String) { ... on Type { field(a: $a) { @@ -46,12 +41,10 @@ def uses_all_variables_deeply_in_inline_fragments(): } } } - """ - ) + """) def uses_all_variables_in_fragment(): - assert_valid( - """ + assert_valid(""" query Foo($a: String, $b: String, $c: String) { ...FragA } @@ -68,12 +61,10 @@ def uses_all_variables_in_fragment(): fragment FragC on Type { field(c: $c) } - """ - ) + """) def variable_used_by_fragment_in_multiple_operations(): - assert_valid( - """ + assert_valid(""" query Foo($a: String) { ...FragA } @@ -86,12 +77,10 @@ def variable_used_by_fragment_in_multiple_operations(): fragment FragB on Type { field(b: $b) } - """ - ) + """) def variable_used_by_recursive_fragment(): - assert_valid( - """ + assert_valid(""" query Foo($a: String) { ...FragA } @@ -100,8 +89,7 @@ def variable_used_by_recursive_fragment(): ...FragA } } - """ - ) + """) def variable_not_used(): assert_errors( diff --git a/tests/validation/test_overlapping_fields_can_be_merged.py b/tests/validation/test_overlapping_fields_can_be_merged.py index 5f7800f7..29b5fe3d 100644 --- a/tests/validation/test_overlapping_fields_can_be_merged.py +++ b/tests/validation/test_overlapping_fields_can_be_merged.py @@ -12,77 +12,63 @@ def describe_validate_overlapping_fields_can_be_merged(): def unique_fields(): - assert_valid( - """ + assert_valid(""" fragment uniqueFields on Dog { name nickname } - """ - ) + """) def identical_fields(): - assert_valid( - """ + assert_valid(""" fragment mergeIdenticalFields on Dog { name name } - """ - ) + """) def identical_fields_with_identical_args(): - assert_valid( - """ + assert_valid(""" fragment mergeIdenticalFieldsWithIdenticalArgs on Dog { doesKnowCommand(dogCommand: SIT) doesKnowCommand(dogCommand: SIT) } - """ - ) + """) def identical_fields_with_identical_directives(): - assert_valid( - """ + assert_valid(""" fragment mergeSameFieldsWithSameDirectives on Dog { name @include(if: true) name @include(if: true) } - """ - ) + """) def different_args_with_different_aliases(): - assert_valid( - """ + assert_valid(""" fragment differentArgsWithDifferentAliases on Dog { knowsSit: doesKnowCommand(dogCommand: SIT) knowsDown: doesKnowCommand(dogCommand: DOWN) } - """ - ) + """) def different_directives_with_different_aliases(): - assert_valid( - """ + assert_valid(""" fragment differentDirectivesWithDifferentAliases on Dog { nameIfTrue: name @include(if: true) nameIfFalse: name @include(if: false) } - """ - ) + """) def different_skip_or_include_directives_accepted(): # Note: Differing skip/include directives don't create an ambiguous # return value and are acceptable in conditions where differing runtime # values may have the same desired effect of including/skipping a field - assert_valid( - """ + assert_valid(""" fragment differentDirectivesWithDifferentAliases on Dog { name @include(if: true) name @include(if: false) } - """ - ) + """) def same_aliases_with_different_field_targets(): assert_errors( @@ -105,8 +91,7 @@ def same_aliases_with_different_field_targets(): ) def same_aliases_allowed_on_non_overlapping_fields(): - assert_valid( - """ + assert_valid(""" fragment sameAliasesWithDifferentFieldTargets on Pet { ... on Dog { name @@ -115,8 +100,7 @@ def same_aliases_allowed_on_non_overlapping_fields(): name: nickname } } - """ - ) + """) def alias_masking_direct_field_access(): assert_errors( @@ -216,8 +200,7 @@ def conflicting_arg_names(): def allows_different_args_where_no_conflict_is_possible(): # This is valid since no object can be both a "Dog" and a "Cat", thus # these fields can never overlap. - assert_valid( - """ + assert_valid(""" fragment conflictingArgs on Pet { ... on Dog { name(surname: true) @@ -226,17 +209,14 @@ def allows_different_args_where_no_conflict_is_possible(): name } } - """ - ) + """) def allows_different_order_of_args(): - schema = build_schema( - """ + schema = build_schema(""" type Query { someField(a: String, b: String): String } - """ - ) + """) # This is valid since arguments are unordered, see: # https://spec.graphql.org/draft/# # sec-Language.Arguments.Arguments-are-unordered @@ -251,8 +231,7 @@ def allows_different_order_of_args(): ) def allows_different_order_of_input_object_fields_in_arg_values(): - schema = build_schema( - """ + schema = build_schema(""" input SomeInput { a: String b: String @@ -261,8 +240,7 @@ def allows_different_order_of_input_object_fields_in_arg_values(): type Query { someField(arg: SomeInput): String } - """ - ) + """) # This is valid since input object fields are unordered, see: # https://spec.graphql.org/draft/# # sec-Input-Object-Values.Input-object-fields-are-unordered @@ -559,9 +537,37 @@ def reports_deep_conflict_in_nested_fragments(): ], ) - def ignores_unknown_fragments(): - assert_valid( + def reports_deep_conflict_after_nested_fragments(): + assert_errors( """ + fragment F on T { + ...G + } + fragment G on T { + ...H + } + fragment H on T { + x: a + } + { + x: b + ...F + } + """, + [ + { + "message": "Fields 'x' conflict" + " because 'b' and 'a' are different fields." + " Use different aliases on the fields" + " to fetch both if this was intentional.", + "locations": [(12, 15), (9, 15)], + "path": None, + } + ], + ) + + def ignores_unknown_fragments(): + assert_valid(""" { field ...Unknown @@ -572,13 +578,11 @@ def ignores_unknown_fragments(): field ...OtherUnknown } - """ - ) + """) def describe_return_types_must_be_unambiguous(): - schema = build_schema( - """ + schema = build_schema(""" interface SomeBox { deepBox: SomeBox unrelatedField: String @@ -639,8 +643,7 @@ def describe_return_types_must_be_unambiguous(): someBox: SomeBox connection: Connection } - """ - ) + """) def conflicting_return_types_which_potentially_overlap(): # This is invalid since an object could potentially be both the @@ -1039,8 +1042,7 @@ def ignores_unknown_types(): ) def works_for_field_names_that_are_js_keywords(): - schema_with_keywords = build_schema( - """ + schema_with_keywords = build_schema(""" type Foo { constructor: String } @@ -1048,8 +1050,7 @@ def works_for_field_names_that_are_js_keywords(): type Query { foo: Foo } - """ - ) + """) assert_valid( """ @@ -1063,8 +1064,7 @@ def works_for_field_names_that_are_js_keywords(): ) def works_for_field_names_that_are_python_keywords(): - schema_with_keywords = build_schema( - """ + schema_with_keywords = build_schema(""" type Foo { class: String } @@ -1072,8 +1072,7 @@ def works_for_field_names_that_are_python_keywords(): type Query { foo: Foo } - """ - ) + """) assert_valid( """ @@ -1087,38 +1086,32 @@ def works_for_field_names_that_are_python_keywords(): ) def does_not_infinite_loop_on_recursive_fragments(): - assert_valid( - """ + assert_valid(""" { ...fragA } fragment fragA on Human { name, relatives { name, ...fragA } } - """ - ) + """) def does_not_infinite_loop_on_immediately_recursive_fragments(): - assert_valid( - """ + assert_valid(""" { ...fragA } fragment fragA on Human { name, ...fragA } - """ - ) + """) def does_not_infinite_loop_on_recursive_fragment_with_field_named_after_fragment(): - assert_valid( - """ + assert_valid(""" { ...fragA fragA } fragment fragA on Query { ...fragA } - """ - ) + """) def finds_invalid_cases_even_with_field_named_after_fragment(): assert_errors( @@ -1144,8 +1137,7 @@ def finds_invalid_cases_even_with_field_named_after_fragment(): ) def does_not_infinite_loop_on_transitively_recursive_fragments(): - assert_valid( - """ + assert_valid(""" { ...fragA fragB @@ -1154,8 +1146,7 @@ def does_not_infinite_loop_on_transitively_recursive_fragments(): fragment fragA on Human { name, ...fragB } fragment fragB on Human { name, ...fragC } fragment fragC on Human { name, ...fragA } - """ - ) + """) def finds_invalid_case_even_with_immediately_recursive_fragment(): assert_errors( @@ -1176,3 +1167,29 @@ def finds_invalid_case_even_with_immediately_recursive_fragment(): } ], ) + + def does_not_infinite_loop_on_recursive_fragments_separated_by_fields(): + assert_valid(""" + { + ...fragA + ...fragB + } + + fragment fragA on T { + x { + ...fragA + x { + ...fragA + } + } + } + + fragment fragB on T { + x { + ...fragB + x { + ...fragB + } + } + } + """) diff --git a/tests/validation/test_possible_fragment_spreads.py b/tests/validation/test_possible_fragment_spreads.py index 791edfc1..a8d1d816 100644 --- a/tests/validation/test_possible_fragment_spreads.py +++ b/tests/validation/test_possible_fragment_spreads.py @@ -5,8 +5,7 @@ from .harness import assert_validation_errors -test_schema = build_schema( - """ +test_schema = build_schema(""" interface Being { name: String } @@ -51,8 +50,7 @@ dogOrHuman: DogOrHuman humanOrAlien: HumanOrAlien } - """ -) + """) assert_errors = partial( @@ -64,105 +62,79 @@ def describe_validate_possible_fragment_spreads(): def of_the_same_object(): - assert_valid( - """ + assert_valid(""" fragment objectWithinObject on Dog { ...dogFragment } fragment dogFragment on Dog { barkVolume } - """ - ) + """) def of_the_same_object_inline_fragment(): - assert_valid( - """ + assert_valid(""" fragment objectWithinObjectAnon on Dog { ... on Dog { barkVolume } } - """ - ) + """) def object_into_implemented_interface(): - assert_valid( - """ + assert_valid(""" fragment objectWithinInterface on Pet { ...dogFragment } fragment dogFragment on Dog { barkVolume } - """ - ) + """) def object_into_containing_union(): - assert_valid( - """ + assert_valid(""" fragment objectWithinUnion on CatOrDog { ...dogFragment } fragment dogFragment on Dog { barkVolume } - """ - ) + """) def union_into_contained_object(): - assert_valid( - """ + assert_valid(""" fragment unionWithinObject on Dog { ...catOrDogFragment } fragment catOrDogFragment on CatOrDog { __typename } - """ - ) + """) def union_into_overlapping_interface(): - assert_valid( - """ + assert_valid(""" fragment unionWithinInterface on Pet { ...catOrDogFragment } fragment catOrDogFragment on CatOrDog { __typename } - """ - ) + """) def union_into_overlapping_union(): - assert_valid( - """ + assert_valid(""" fragment unionWithinUnion on DogOrHuman { ...catOrDogFragment } fragment catOrDogFragment on CatOrDog { __typename } - """ - ) + """) def interface_into_implemented_object(): - assert_valid( - """ + assert_valid(""" fragment interfaceWithinObject on Dog { ...petFragment } fragment petFragment on Pet { name } - """ - ) + """) def interface_into_overlapping_interface(): - assert_valid( - """ + assert_valid(""" fragment interfaceWithinInterface on Pet { ...beingFragment } fragment beingFragment on Being { name } - """ - ) + """) def interface_into_overlapping_interface_in_inline_fragment(): - assert_valid( - """ + assert_valid(""" fragment interfaceWithinInterface on Pet { ... on Being { name } } - """ - ) + """) def interface_into_overlapping_union(): - assert_valid( - """ + assert_valid(""" fragment interfaceWithinUnion on CatOrDog { ...petFragment } fragment petFragment on Pet { name } - """ - ) + """) def ignores_incorrect_type_caught_by_fragments_on_composite_types(): - assert_valid( - """ + assert_valid(""" fragment petFragment on Pet { ...badInADifferentWay } fragment badInADifferentWay on String { name } - """ - ) + """) def ignores_unknown_fragments_caught_by_known_fragment_names(): - assert_valid( - """ + assert_valid(""" fragment petFragment on Pet { ...UnknownFragment } - """ - ) + """) def different_object_into_object(): assert_errors( diff --git a/tests/validation/test_possible_type_extensions.py b/tests/validation/test_possible_type_extensions.py index 473e0c88..126cee98 100644 --- a/tests/validation/test_possible_type_extensions.py +++ b/tests/validation/test_possible_type_extensions.py @@ -12,20 +12,17 @@ def describe_validate_possible_type_extensions(): def no_extensions(): - assert_valid( - """ + assert_valid(""" scalar FooScalar type FooObject interface FooInterface union FooUnion enum FooEnum input FooInputObject - """ - ) + """) def one_extension_per_type(): - assert_valid( - """ + assert_valid(""" scalar FooScalar type FooObject interface FooInterface @@ -39,12 +36,10 @@ def one_extension_per_type(): extend union FooUnion @dummy extend enum FooEnum @dummy extend input FooInputObject @dummy - """ - ) + """) def many_extensions_per_type(): - assert_valid( - """ + assert_valid(""" scalar FooScalar type FooObject interface FooInterface @@ -65,8 +60,7 @@ def many_extensions_per_type(): extend union FooUnion @dummy extend enum FooEnum @dummy extend input FooInputObject @dummy - """ - ) + """) def extending_unknown_type(): message = ( @@ -167,16 +161,14 @@ def extending_with_different_kinds(): ) def extending_types_within_existing_schema(): - schema = build_schema( - """ + schema = build_schema(""" scalar FooScalar type FooObject interface FooInterface union FooUnion enum FooEnum input FooInputObject - """ - ) + """) sdl = """ extend scalar FooScalar @dummy extend type FooObject @dummy @@ -217,16 +209,14 @@ def extending_unknown_types_within_existing_schema(): ) def extending_types_with_different_kinds_within_existing_schema(): - schema = build_schema( - """ + schema = build_schema(""" scalar FooScalar type FooObject interface FooInterface union FooUnion enum FooEnum input FooInputObject - """ - ) + """) sdl = """ extend type FooScalar @dummy extend interface FooObject @dummy diff --git a/tests/validation/test_provided_required_arguments.py b/tests/validation/test_provided_required_arguments.py index 86bb5233..38cc705c 100644 --- a/tests/validation/test_provided_required_arguments.py +++ b/tests/validation/test_provided_required_arguments.py @@ -21,133 +21,109 @@ def describe_validate_provided_required_arguments(): def ignores_unknown_arguments(): - assert_valid( - """ + assert_valid(""" { dog { isHouseTrained(unknownArgument: true) } - }""" - ) + }""") def describe_valid_non_nullable_value(): def arg_on_optional_arg(): - assert_valid( - """ + assert_valid(""" { dog { isHouseTrained(atOtherHomes: true) } - }""" - ) + }""") def no_arg_on_optional_arg(): - assert_valid( - """ + assert_valid(""" { dog { isHouseTrained } - }""" - ) + }""") def no_arg_on_non_null_field_with_default(): - assert_valid( - """ + assert_valid(""" { complicatedArgs { nonNullFieldWithDefault } - }""" - ) + }""") def multiple_args(): - assert_valid( - """ + assert_valid(""" { complicatedArgs { multipleReqs(req1: 1, req2: 2) } } - """ - ) + """) def multiple_args_reverse_order(): - assert_valid( - """ + assert_valid(""" { complicatedArgs { multipleReqs(req2: 2, req1: 1) } } - """ - ) + """) def no_args_on_multiple_optional(): - assert_valid( - """ + assert_valid(""" { complicatedArgs { multipleOpts } } - """ - ) + """) def one_arg_on_multiple_optional(): - assert_valid( - """ + assert_valid(""" { complicatedArgs { multipleOpts(opt1: 1) } } - """ - ) + """) def second_arg_on_multiple_optional(): - assert_valid( - """ + assert_valid(""" { complicatedArgs { multipleOpts(opt2: 1) } } - """ - ) + """) def multiple_required_args_on_mixed_list(): - assert_valid( - """ + assert_valid(""" { complicatedArgs { multipleOptAndReq(req1: 3, req2: 4) } } - """ - ) + """) def multiple_required_and_one_optional_arg_on_mixed_list(): - assert_valid( - """ + assert_valid(""" { complicatedArgs { multipleOptAndReq(req1: 3, req2: 4, opt1: 5) } } - """ - ) + """) def all_required_and_optional_args_on_mixed_list(): - assert_valid( - """ + assert_valid(""" { complicatedArgs { multipleOptAndReq(req1: 3, req2: 4, opt1: 5, opt2: 6) } } - """ - ) + """) def describe_invalid_non_nullable_value(): def missing_one_non_nullable_argument(): @@ -211,17 +187,14 @@ def incorrect_value_and_missing_argument(): def describe_directive_arguments(): def ignores_unknown_directives(): - assert_valid( - """ + assert_valid(""" { dog @unknown } - """ - ) + """) def with_directives_of_valid_type(): - assert_valid( - """ + assert_valid(""" { dog @include(if: true) { name @@ -230,8 +203,7 @@ def with_directives_of_valid_type(): name } } - """ - ) + """) def with_directive_with_missing_types(): assert_errors( @@ -258,15 +230,13 @@ def with_directive_with_missing_types(): def describe_within_sdl(): def missing_optional_args_on_directive_defined_inside_sdl(): - assert_sdl_valid( - """ + assert_sdl_valid(""" type Query { foo: String @test } directive @test(arg1: String, arg2: String! = "") on FIELD_DEFINITION - """ - ) + """) def missing_arg_on_directive_defined_inside_sdl(): assert_sdl_errors( @@ -320,13 +290,11 @@ def missing_arg_on_overridden_standard_directive(): ) def missing_arg_on_directive_defined_in_schema_extension(): - schema = build_schema( - """ + schema = build_schema(""" type Query { foo: String } - """ - ) + """) assert_sdl_errors( """ directive @test(arg: String!) on OBJECT @@ -344,15 +312,13 @@ def missing_arg_on_directive_defined_in_schema_extension(): ) def missing_arg_on_directive_used_in_schema_extension(): - schema = build_schema( - """ + schema = build_schema(""" directive @test(arg: String!) on OBJECT type Query { foo: String } - """ - ) + """) assert_sdl_errors( """ extend type Query @test diff --git a/tests/validation/test_scalar_leafs.py b/tests/validation/test_scalar_leafs.py index 5d7b83d1..ffa8ce41 100644 --- a/tests/validation/test_scalar_leafs.py +++ b/tests/validation/test_scalar_leafs.py @@ -1,8 +1,17 @@ from functools import partial +from graphql.language import ( + DocumentNode, + FieldNode, + NameNode, + OperationDefinitionNode, + OperationType, + SelectionSetNode, +) from graphql.validation import ScalarLeafsRule +from graphql.validation.validate import validate -from .harness import assert_validation_errors +from .harness import assert_validation_errors, test_schema assert_errors = partial(assert_validation_errors, ScalarLeafsRule) @@ -11,13 +20,11 @@ def describe_validate_scalar_leafs(): def valid_scalar_selection(): - assert_valid( - """ + assert_valid(""" fragment scalarSelection on Dog { barks } - """ - ) + """) def object_type_missing_selection(): assert_errors( @@ -54,13 +61,11 @@ def interface_type_missing_selection(): ) def valid_scalar_selection_with_args(): - assert_valid( - """ + assert_valid(""" fragment scalarSelectionWithArgs on Dog { doesKnowCommand(dogCommand: SIT) } - """ - ) + """) def scalar_selection_not_allowed_on_boolean(): assert_errors( @@ -141,3 +146,30 @@ def scalar_selection_not_allowed_with_directives_and_args(): }, ], ) + + def object_type_having_only_one_selection(): + # We can't leverage assert_errors since it doesn't support passing in the + # document node directly. We have to do this because this is technically + # an invalid document. + doc = DocumentNode( + definitions=[ + OperationDefinitionNode( + operation=OperationType.QUERY, + selection_set=SelectionSetNode( + selections=[ + FieldNode( + name=NameNode(value="human"), + selection_set=SelectionSetNode(selections=[]), + ), + ], + ), + ), + ], + ) + errors = validate(test_schema, doc, [ScalarLeafsRule]) + assert errors == [ + { + "message": "Field 'human' of type 'Human'" + " must have at least one field selected.", + }, + ] diff --git a/tests/validation/test_single_field_subscriptions.py b/tests/validation/test_single_field_subscriptions.py index c6278f77..9e9954af 100644 --- a/tests/validation/test_single_field_subscriptions.py +++ b/tests/validation/test_single_field_subscriptions.py @@ -5,8 +5,7 @@ from .harness import assert_validation_errors -schema = build_schema( - """ +schema = build_schema(""" type Message { body: String sender: String @@ -29,8 +28,7 @@ query: QueryRoot subscription: SubscriptionRoot } - """ -) + """) assert_errors = partial( assert_validation_errors, SingleFieldSubscriptionsRule, schema=schema @@ -41,17 +39,14 @@ def describe_validate_subscriptions_with_single_field(): def valid_subscription(): - assert_valid( - """ + assert_valid(""" subscription ImportantEmails { importantEmails } - """ - ) + """) def valid_subscription_with_fragment(): - assert_valid( - """ + assert_valid(""" subscription sub { ...newMessageFields } @@ -62,12 +57,10 @@ def valid_subscription_with_fragment(): sender } } - """ - ) + """) def valid_subscription_with_fragment_and_field(): - assert_valid( - """ + assert_valid(""" subscription sub { newMessage { body @@ -81,8 +74,7 @@ def valid_subscription_with_fragment_and_field(): sender } } - """ - ) + """) def fails_with_more_than_one_root_field(): assert_errors( @@ -292,13 +284,11 @@ def fails_with_introspection_field_in_anonymous_subscription(): ) def skips_if_not_subscription_type(): - empty_schema = build_schema( - """ + empty_schema = build_schema(""" type Query { dummy: String } - """ - ) + """) assert_errors( """ subscription { diff --git a/tests/validation/test_unique_argument_definition_names.py b/tests/validation/test_unique_argument_definition_names.py index 9d6625a7..a2114f8a 100644 --- a/tests/validation/test_unique_argument_definition_names.py +++ b/tests/validation/test_unique_argument_definition_names.py @@ -15,8 +15,7 @@ def describe_validate_unique_argument_definition_names(): def no_args(): - assert_sdl_valid( - """ + assert_sdl_valid(""" type SomeObject { someField: String } @@ -26,12 +25,10 @@ def no_args(): } directive @someDirective on QUERY - """ - ) + """) def one_argument(): - assert_sdl_valid( - """ + assert_sdl_valid(""" type SomeObject { someField(foo: String): String } @@ -49,12 +46,10 @@ def one_argument(): } directive @someDirective(foo: String) on QUERY - """ - ) + """) def multiple_arguments(): - assert_sdl_valid( - """ + assert_sdl_valid(""" type SomeObject { someField( foo: String @@ -87,8 +82,7 @@ def multiple_arguments(): foo: String bar: String ) on QUERY - """ - ) + """) def duplicating_arguments(): assert_sdl_errors( diff --git a/tests/validation/test_unique_argument_names.py b/tests/validation/test_unique_argument_names.py index ef82f67e..e41fc478 100644 --- a/tests/validation/test_unique_argument_names.py +++ b/tests/validation/test_unique_argument_names.py @@ -10,86 +10,68 @@ def describe_validate_unique_argument_names(): def no_arguments_on_field(): - assert_valid( - """ + assert_valid(""" { field } - """ - ) + """) def no_arguments_on_directive(): - assert_valid( - """ + assert_valid(""" { field } - """ - ) + """) def argument_on_field(): - assert_valid( - """ + assert_valid(""" { field(arg: "value") } - """ - ) + """) def argument_on_directive(): - assert_valid( - """ + assert_valid(""" { field @directive(arg: "value") } - """ - ) + """) def same_argument_on_two_fields(): - assert_valid( - """ + assert_valid(""" { one: field(arg: "value") two: field(arg: "value") } - """ - ) + """) def same_argument_on_field_and_directive(): - assert_valid( - """ + assert_valid(""" { field(arg: "value") @directive(arg: "value") } - """ - ) + """) def same_argument_on_two_directives(): - assert_valid( - """ + assert_valid(""" { field @directive1(arg: "value") @directive2(arg: "value") } - """ - ) + """) def multiple_field_arguments(): - assert_valid( - """ + assert_valid(""" { field(arg1: "value", arg2: "value", arg3: "value") } - """ - ) + """) def multiple_directive_arguments(): - assert_valid( - """ + assert_valid(""" { field @directive(arg1: "value", arg2: "value", arg3: "value") } - """ - ) + """) def duplicate_field_arguments(): assert_errors( diff --git a/tests/validation/test_unique_directive_names.py b/tests/validation/test_unique_directive_names.py index 1cb9dc58..3a7bdd73 100644 --- a/tests/validation/test_unique_directive_names.py +++ b/tests/validation/test_unique_directive_names.py @@ -12,38 +12,30 @@ def describe_validate_unique_directive_names(): def no_directive(): - assert_valid( - """ + assert_valid(""" type Foo - """ - ) + """) def one_directive(): - assert_valid( - """ + assert_valid(""" directive @foo on SCHEMA - """ - ) + """) def many_directives(): - assert_valid( - """ + assert_valid(""" directive @foo on SCHEMA directive @bar on SCHEMA directive @baz on SCHEMA - """ - ) + """) def directive_and_non_directive_definitions_named_the_same(): - assert_valid( - """ + assert_valid(""" query foo { __typename } fragment foo on foo { __typename } type foo directive @foo on SCHEMA - """ - ) + """) def directives_named_the_same(): assert_errors( diff --git a/tests/validation/test_unique_directives_per_location.py b/tests/validation/test_unique_directives_per_location.py index 29842087..60a3b3cb 100644 --- a/tests/validation/test_unique_directives_per_location.py +++ b/tests/validation/test_unique_directives_per_location.py @@ -1,8 +1,12 @@ from functools import partial +from typing import Any, Dict, List, Optional, Union +from graphql.error import GraphQLError from graphql.language import parse +from graphql.type import GraphQLSchema from graphql.utilities import extend_schema from graphql.validation import UniqueDirectivesPerLocationRule +from graphql.validation.validate import validate_sdl from .harness import assert_validation_errors, assert_sdl_validation_errors, test_schema @@ -27,65 +31,63 @@ ) +def assert_experimental_sdl_errors( + sdl_str: str, + errors: List[Union[GraphQLError, Dict[str, Any]]], + schema: Optional[GraphQLSchema] = None, +) -> List[GraphQLError]: + doc = parse(sdl_str, experimental_directives_on_directive_definitions=True) + returned_errors = validate_sdl(doc, schema, [UniqueDirectivesPerLocationRule]) + assert returned_errors == errors + return returned_errors + + def describe_validate_directives_are_unique_per_location(): def no_directives(): - assert_valid( - """ + assert_valid(""" { field } - """ - ) + """) def unique_directives_in_different_locations(): - assert_valid( - """ + assert_valid(""" fragment Test on Type @directiveA { field @directiveB } - """ - ) + """) def unique_directives_in_same_locations(): - assert_valid( - """ + assert_valid(""" fragment Test on Type @directiveA @directiveB { field @directiveA @directiveB } - """ - ) + """) def same_directives_in_different_locations(): - assert_valid( - """ + assert_valid(""" fragment Test on Type @directiveA { field @directiveA } - """ - ) + """) def same_directives_in_similar_locations(): - assert_valid( - """ + assert_valid(""" fragment Test on Type { field @directive field @directive } - """ - ) + """) def repeatable_directives_in_same_location(): - assert_valid( - """ + assert_valid(""" fragment Test on Type @repeatable @repeatable { field @repeatable @repeatable } - """ - ) + """) def unknown_directives_must_be_ignored(): - assert_valid( - """ + assert_valid(""" type Test @unknown @unknown { field: String! @unknown @unknown } @@ -93,8 +95,7 @@ def unknown_directives_must_be_ignored(): extend type Test @unknown { anotherField: String! } - """ - ) + """) def duplicate_directives_in_one_location(): assert_errors( @@ -331,3 +332,69 @@ def duplicate_directives_between_sdl_definitions_and_extensions(): }, ], ) + + def duplicate_directives_on_directive_definitions(): + assert_experimental_sdl_errors( + """ + directive @nonRepeatable on DIRECTIVE_DEFINITION + + directive @testDirective @nonRepeatable @nonRepeatable on FIELD_DEFINITION + """, + [ + { + "message": "The directive '@nonRepeatable'" + " can only be used once at this location.", + "locations": [(4, 38), (4, 53)], + }, + ], + ) + + def duplicate_directives_on_directive_extensions(): + assert_experimental_sdl_errors( + """ + directive @nonRepeatable on DIRECTIVE_DEFINITION + + extend directive @testDirective @nonRepeatable @nonRepeatable + """, + [ + { + "message": "The directive '@nonRepeatable'" + " can only be used once at this location.", + "locations": [(4, 45), (4, 60)], + }, + ], + ) + + def duplicate_directives_between_directive_definitions_and_extensions(): + assert_experimental_sdl_errors( + """ + directive @nonRepeatable on DIRECTIVE_DEFINITION + + directive @testDirective @nonRepeatable on FIELD_DEFINITION + extend directive @testDirective @nonRepeatable + """, + [ + { + "message": "The directive '@nonRepeatable'" + " can only be used once at this location.", + "locations": [(4, 38), (5, 45)], + }, + ], + ) + + def duplicate_directives_between_directive_extensions(): + assert_experimental_sdl_errors( + """ + directive @nonRepeatable on DIRECTIVE_DEFINITION + + extend directive @testDirective @nonRepeatable + extend directive @testDirective @nonRepeatable + """, + [ + { + "message": "The directive '@nonRepeatable'" + " can only be used once at this location.", + "locations": [(4, 45), (5, 45)], + }, + ], + ) diff --git a/tests/validation/test_unique_enum_value_names.py b/tests/validation/test_unique_enum_value_names.py index 5611b45c..6e013e05 100644 --- a/tests/validation/test_unique_enum_value_names.py +++ b/tests/validation/test_unique_enum_value_names.py @@ -12,30 +12,24 @@ def describe_validate_unique_field_definition_names(): def no_values(): - assert_valid( - """ + assert_valid(""" enum SomeEnum - """ - ) + """) def one_value(): - assert_valid( - """ + assert_valid(""" enum SomeEnum { FOO } - """ - ) + """) def multiple_values(): - assert_valid( - """ + assert_valid(""" enum SomeEnum { FOO BAR } - """ - ) + """) def duplicate_values_inside_the_same_enum_definition(): assert_errors( @@ -55,8 +49,7 @@ def duplicate_values_inside_the_same_enum_definition(): ) def extend_enum_with_new_value(): - assert_valid( - """ + assert_valid(""" enum SomeEnum { FOO } @@ -66,8 +59,7 @@ def extend_enum_with_new_value(): extend enum SomeEnum { BAZ } - """ - ) + """) def extend_enum_with_duplicate_value(): assert_errors( @@ -135,13 +127,11 @@ def adding_new_value_to_the_enum_inside_existing_schema(): assert_valid(sdl, schema=schema) def adding_conflicting_value_to_existing_schema_twice(): - schema = build_schema( - """ + schema = build_schema(""" enum SomeEnum { FOO } - """ - ) + """) sdl = """ extend enum SomeEnum { FOO diff --git a/tests/validation/test_unique_field_definition_names.py b/tests/validation/test_unique_field_definition_names.py index 5e9b1c21..3874845a 100644 --- a/tests/validation/test_unique_field_definition_names.py +++ b/tests/validation/test_unique_field_definition_names.py @@ -14,17 +14,14 @@ def describe_validate_unique_field_definition_names(): def no_fields(): - assert_valid( - """ + assert_valid(""" type SomeObject interface SomeInterface input SomeInputObject - """ - ) + """) def one_field(): - assert_valid( - """ + assert_valid(""" type SomeObject { foo: String } @@ -36,12 +33,10 @@ def one_field(): input SomeInputObject { foo: String } - """ - ) + """) def multiple_fields(): - assert_valid( - """ + assert_valid(""" type SomeObject { foo: String bar: String @@ -56,8 +51,7 @@ def multiple_fields(): foo: String bar: String } - """ - ) + """) def duplicate_fields_inside_the_same_type_definition(): assert_errors( @@ -97,8 +91,7 @@ def duplicate_fields_inside_the_same_type_definition(): ) def extend_type_with_new_field(): - assert_valid( - """ + assert_valid(""" type SomeObject { foo: String } @@ -128,8 +121,7 @@ def extend_type_with_new_field(): extend input SomeInputObject { baz: String } - """ - ) + """) def extend_type_with_duplicate_field(): assert_errors( @@ -255,13 +247,11 @@ def duplicate_field_inside_different_extension(): ) def adding_new_field_to_the_type_inside_existing_schema(): - schema = build_schema( - """ + schema = build_schema(""" type SomeObject interface SomeInterface input SomeInputObject - """ - ) + """) sdl = """ extend type SomeObject { foo: String @@ -279,8 +269,7 @@ def adding_new_field_to_the_type_inside_existing_schema(): assert_valid(sdl, schema=schema) def adding_conflicting_fields_to_existing_schema_twice(): - schema = build_schema( - """ + schema = build_schema(""" type SomeObject { foo: String } @@ -292,8 +281,7 @@ def adding_conflicting_fields_to_existing_schema_twice(): input SomeInputObject { foo: String } - """ - ) + """) sdl = """ extend type SomeObject { foo: String @@ -357,13 +345,11 @@ def adding_conflicting_fields_to_existing_schema_twice(): ) def adding_fields_to_existing_schema_twice(): - schema = build_schema( - """ + schema = build_schema(""" type SomeObject interface SomeInterface input SomeInputObject - """ - ) + """) sdl = """ extend type SomeObject { foo: String diff --git a/tests/validation/test_unique_fragment_names.py b/tests/validation/test_unique_fragment_names.py index 37370c9d..dd2a97c7 100644 --- a/tests/validation/test_unique_fragment_names.py +++ b/tests/validation/test_unique_fragment_names.py @@ -11,29 +11,24 @@ def describe_validate_unique_fragment_names(): def no_fragments(): - assert_valid( - """ + assert_valid(""" { field } - """ - ) + """) def one_fragment(): - assert_valid( - """ + assert_valid(""" { ...fragA } fragment fragA on Type { field } - """ - ) + """) def many_fragments(): - assert_valid( - """ + assert_valid(""" { ...fragA ...fragB @@ -48,12 +43,10 @@ def many_fragments(): fragment fragC on Type { fieldC } - """ - ) + """) def inline_fragments_are_always_unique(): - assert_valid( - """ + assert_valid(""" { ...on Type { fieldA @@ -62,20 +55,17 @@ def inline_fragments_are_always_unique(): fieldB } } - """ - ) + """) def fragment_and_operation_named_the_same(): - assert_valid( - """ + assert_valid(""" query Foo { ...Foo } fragment Foo on Type { field } - """ - ) + """) def fragments_named_the_same(): assert_errors( diff --git a/tests/validation/test_unique_input_field_names.py b/tests/validation/test_unique_input_field_names.py index 857118da..0bc303b7 100644 --- a/tests/validation/test_unique_input_field_names.py +++ b/tests/validation/test_unique_input_field_names.py @@ -11,35 +11,28 @@ def describe_validate_unique_input_field_names(): def input_object_with_fields(): - assert_valid( - """ + assert_valid(""" { field(arg: { f: true }) } - """ - ) + """) def same_input_object_within_two_args(): - assert_valid( - """ + assert_valid(""" { field(arg1: { f: true }, arg2: { f: true }) } - """ - ) + """) def multiple_input_object_fields(): - assert_valid( - """ + assert_valid(""" { field(arg: { f1: "value", f2: "value", f3: "value" }) } - """ - ) + """) def allows_for_nested_input_objects_with_similar_fields(): - assert_valid( - """ + assert_valid(""" { field(arg: { deep: { @@ -51,8 +44,7 @@ def allows_for_nested_input_objects_with_similar_fields(): id: 1 }) } - """ - ) + """) def duplicate_input_object_fields(): assert_errors( diff --git a/tests/validation/test_unique_operation_names.py b/tests/validation/test_unique_operation_names.py index f2ba8a23..cf824b2e 100644 --- a/tests/validation/test_unique_operation_names.py +++ b/tests/validation/test_unique_operation_names.py @@ -11,35 +11,28 @@ def describe_validate_unique_operation_names(): def no_operations(): - assert_valid( - """ + assert_valid(""" fragment fragA on Type { field } - """ - ) + """) def one_anon_operation(): - assert_valid( - """ + assert_valid(""" { field } - """ - ) + """) def one_named_operation(): - assert_valid( - """ + assert_valid(""" query Foo { field } - """ - ) + """) def multiple_operations(): - assert_valid( - """ + assert_valid(""" query Foo { field } @@ -47,12 +40,10 @@ def multiple_operations(): query Bar { field } - """ - ) + """) def multiple_operations_of_different_types(): - assert_valid( - """ + assert_valid(""" query Foo { field } @@ -64,20 +55,17 @@ def multiple_operations_of_different_types(): subscription Baz { field } - """ - ) + """) def fragment_and_operation_named_the_same(): - assert_valid( - """ + assert_valid(""" query Foo { ...Foo } fragment Foo on Type { field } - """ - ) + """) def multiple_operations_of_same_name(): assert_errors( diff --git a/tests/validation/test_unique_operation_types.py b/tests/validation/test_unique_operation_types.py index c341e84c..78bb8e96 100644 --- a/tests/validation/test_unique_operation_types.py +++ b/tests/validation/test_unique_operation_types.py @@ -12,15 +12,12 @@ def describe_validate_unique_operation_types(): def no_schema_definition(): - assert_valid( - """ + assert_valid(""" type Foo - """ - ) + """) def schema_definition_with_all_types(): - assert_valid( - """ + assert_valid(""" type Foo schema { @@ -28,12 +25,10 @@ def schema_definition_with_all_types(): mutation: Foo subscription: Foo } - """ - ) + """) def schema_definition_with_single_extension(): - assert_valid( - """ + assert_valid(""" type Foo schema { query: Foo } @@ -42,31 +37,26 @@ def schema_definition_with_single_extension(): mutation: Foo subscription: Foo } - """ - ) + """) def schema_definition_with_separate_extensions(): - assert_valid( - """ + assert_valid(""" type Foo schema { query: Foo } extend schema { mutation: Foo } extend schema { subscription: Foo } - """ - ) + """) def extend_schema_before_definition(): - assert_valid( - """ + assert_valid(""" type Foo extend schema { mutation: Foo } extend schema { subscription: Foo } schema { query: Foo } - """ - ) + """) def duplicate_operation_types_inside_single_schema_definition(): assert_errors( @@ -251,15 +241,13 @@ def adding_new_operation_types_to_existing_schema(): assert_valid(sdl, schema=schema) def adding_conflicting_operation_types_to_existing_schema(): - schema = build_schema( - """ + schema = build_schema(""" type Query type Mutation type Subscription type Foo - """ - ) + """) sdl = """ extend schema { @@ -292,13 +280,11 @@ def adding_conflicting_operation_types_to_existing_schema(): ) def adding_conflicting_operation_types_to_existing_schema_twice(): - schema = build_schema( - """ + schema = build_schema(""" type Query type Mutation type Subscription - """ - ) + """) sdl = """ extend schema { diff --git a/tests/validation/test_unique_type_names.py b/tests/validation/test_unique_type_names.py index 1ff03b8f..6c6d96dc 100644 --- a/tests/validation/test_unique_type_names.py +++ b/tests/validation/test_unique_type_names.py @@ -12,38 +12,30 @@ def describe_validate_unique_type_names(): def no_types(): - assert_valid( - """ + assert_valid(""" directive @test on SCHEMA - """ - ) + """) def one_type(): - assert_valid( - """ + assert_valid(""" type Foo - """ - ) + """) def many_types(): - assert_valid( - """ + assert_valid(""" type Foo type Bar type Baz - """ - ) + """) def type_and_non_type_definitions_named_the_same(): - assert_valid( - """ + assert_valid(""" query Foo { __typename } fragment Foo on Query { __typename } directive @Foo on SCHEMA type Foo - """ - ) + """) def types_named_the_same(): assert_errors( diff --git a/tests/validation/test_unique_variable_names.py b/tests/validation/test_unique_variable_names.py index 9b5c10e6..02f28387 100644 --- a/tests/validation/test_unique_variable_names.py +++ b/tests/validation/test_unique_variable_names.py @@ -11,12 +11,10 @@ def describe_validate_unique_variable_names(): def unique_variable_names(): - assert_valid( - """ + assert_valid(""" query A($x: Int, $y: String) { __typename } query B($x: String, $y: Int) { __typename } - """ - ) + """) def duplicate_variable_names(): assert_errors( diff --git a/tests/validation/test_validation.py b/tests/validation/test_validation.py index 0f7d80e6..d4dff965 100644 --- a/tests/validation/test_validation.py +++ b/tests/validation/test_validation.py @@ -46,8 +46,7 @@ def rejects_invalid_max_errors(): ) def validates_queries(): - doc = parse( - """ + doc = parse(""" query { human { pets { @@ -60,20 +59,17 @@ def validates_queries(): } } } - """ - ) + """) errors = validate(test_schema, doc) assert errors == [] def detects_unknown_fields(): - doc = parse( - """ + doc = parse(""" { unknown } - """ - ) + """) errors = validate(test_schema, doc) assert errors == [ @@ -84,8 +80,7 @@ def deprecated_validates_using_a_custom_type_info(): # This TypeInfo will never return a valid field. type_info = TypeInfo(test_schema, None, lambda *args: None) - doc = parse( - """ + doc = parse(""" query { human { pets { @@ -98,8 +93,7 @@ def deprecated_validates_using_a_custom_type_info(): } } } - """ - ) + """) errors = validate(test_schema, doc, None, None, type_info) @@ -112,23 +106,19 @@ def deprecated_validates_using_a_custom_type_info(): ] def validates_using_a_custom_rule(): - schema = build_schema( - """ + schema = build_schema(""" directive @custom(arg: String) on FIELD type Query { foo: String } - """ - ) + """) - doc = parse( - """ + doc = parse(""" query { name @custom } - """ - ) + """) class CustomRule(ValidationRule): def enter_directive(self, node, *_args): @@ -202,3 +192,22 @@ def enter_field(self, *_args): with raises(RuntimeError, match="^Error from custom rule!$"): validate(test_schema, doc, [CustomRule], max_errors=1) + + +def describe_operation_and_variable_definition_descriptions(): + def validates_operation_with_description_and_variable_descriptions(): + schema = build_schema("type Query { field(a: Int, b: String): String }") + query = ''' + "Operation description" + query myQuery( + "Variable a description" + $a: Int, + """Variable b\nmultiline description""" + $b: String + ) { + field(a: $a, b: $b) + } + ''' + ast = parse(query) + errors = validate(schema, ast) + assert errors == [] diff --git a/tests/validation/test_values_of_correct_type.py b/tests/validation/test_values_of_correct_type.py index a41cd9f6..d0a62fca 100644 --- a/tests/validation/test_values_of_correct_type.py +++ b/tests/validation/test_values_of_correct_type.py @@ -21,146 +21,120 @@ def describe_validate_values_of_correct_type(): def describe_valid_values(): def good_int_value(): - assert_valid( - """ + assert_valid(""" { complicatedArgs { intArgField(intArg: 2) } } - """ - ) + """) def good_negative_int_value(): - assert_valid( - """ + assert_valid(""" { complicatedArgs { intArgField(intArg: -2) } } - """ - ) + """) def good_boolean_value(): - assert_valid( - """ + assert_valid(""" { complicatedArgs { booleanArgField(intArg: true) } } - """ - ) + """) def good_string_value(): - assert_valid( - """ + assert_valid(""" { complicatedArgs { stringArgField(intArg: "foo") } } - """ - ) + """) def good_float_value(): - assert_valid( - """ + assert_valid(""" { complicatedArgs { floatArgField(intArg: 1.1) } } - """ - ) + """) def good_negative_float_value(): - assert_valid( - """ + assert_valid(""" { complicatedArgs { floatArgField(intArg: -1.1) } } - """ - ) + """) def int_into_id(): - assert_valid( - """ + assert_valid(""" { complicatedArgs { idArgField(idArg: 1) } } - """ - ) + """) def string_into_id(): - assert_valid( - """ + assert_valid(""" { complicatedArgs { idArgField(idArg: "someIdString") } } - """ - ) + """) def good_enum_value(): - assert_valid( - """ + assert_valid(""" { dog { doesKnowCommand(dogCommand: SIT) } } - """ - ) + """) def enum_with_undefined_value(): - assert_valid( - """ + assert_valid(""" { complicatedArgs { enumArgField(enumArg: UNKNOWN) } } - """ - ) + """) def enum_with_null_value(): - assert_valid( - """ + assert_valid(""" { complicatedArgs { enumArgField(enumArg: NO_FUR) } } - """ - ) + """) def null_into_nullable_type(): - assert_valid( - """ + assert_valid(""" { complicatedArgs { intArgField(intArg: null) } } - """ - ) + """) - assert_valid( - """ + assert_valid(""" { dog(a: null, b: null, c:{ requiredField: true, intField: null }) { name } } - """ - ) + """) def describe_invalid_string_values(): def int_into_string(): @@ -607,48 +581,40 @@ def different_case_enum_value_into_enum(): def describe_valid_list_value(): def good_list_value(): - assert_valid( - """ + assert_valid(""" { complicatedArgs { stringListArgField(stringListArg: ["one", null, "two"]) } } - """ - ) + """) def empty_list_value(): - assert_valid( - """ + assert_valid(""" { complicatedArgs { stringListArgField(stringListArg: []) } } - """ - ) + """) def null_value(): - assert_valid( - """ + assert_valid(""" { complicatedArgs { stringListArgField(stringListArg: null) } } - """ - ) + """) def single_value_into_list(): - assert_valid( - """ + assert_valid(""" { complicatedArgs { stringListArgField(stringListArg: "one") } } - """ - ) + """) def describe_invalid_list_value(): def incorrect_item_type(): @@ -687,114 +653,94 @@ def single_value_of_incorrect_type(): def describe_valid_non_nullable_value(): def arg_on_optional_arg(): - assert_valid( - """ + assert_valid(""" { dog { isHouseTrained(atOtherHomes: true) } } - """ - ) + """) def no_arg_on_optional_arg(): - assert_valid( - """ + assert_valid(""" { dog { isHouseTrained } } - """ - ) + """) def multiple_args(): - assert_valid( - """ + assert_valid(""" { complicatedArgs { multipleReqs(req1: 1, req2: 2) } } - """ - ) + """) def multiple_args_reverse_order(): - assert_valid( - """ + assert_valid(""" { complicatedArgs { multipleReqs(req2: 2, req1: 1) } } - """ - ) + """) def no_args_on_multiple_optional(): - assert_valid( - """ + assert_valid(""" { complicatedArgs { multipleOpts } } - """ - ) + """) def one_arg_on_multiple_optional(): - assert_valid( - """ + assert_valid(""" { complicatedArgs { multipleOpts(opt1: 1) } } - """ - ) + """) def second_arg_on_multiple_optional(): - assert_valid( - """ + assert_valid(""" { complicatedArgs { multipleOpts(opt2: 1) } } - """ - ) + """) def multiple_required_args_on_mixed_list(): - assert_valid( - """ + assert_valid(""" { complicatedArgs { multipleOptAndReq(req1: 3, req2: 4) } } - """ - ) + """) def multiple_required_and_one_optional_arg_on_mixed_list(): - assert_valid( - """ + assert_valid(""" { complicatedArgs { multipleOptAndReq(req1: 3, req2: 4, opt1: 5) } } - """ - ) + """) def all_required_and_optional_args_on_mixed_list(): - assert_valid( - """ + assert_valid(""" { complicatedArgs { multipleOptAndReq(req1: 3, req2: 4, opt1: 5, opt2: 6) } } - """ - ) + """) def describe_invalid_non_nullable_value(): def incorrect_value_type(): @@ -854,52 +800,43 @@ def null_value(): def describe_valid_input_object_value(): def optional_arg_despite_required_field_in_type(): - assert_valid( - """ + assert_valid(""" { complicatedArgs { complexArgField } } - """ - ) + """) def partial_object_only_required(): - assert_valid( - """ + assert_valid(""" { complicatedArgs { complexArgField(complexArg: { requiredField: true }) } } - """ - ) + """) def partial_object_required_field_can_be_falsy(): - assert_valid( - """ + assert_valid(""" { complicatedArgs { complexArgField(complexArg: { requiredField: false }) } } - """ - ) + """) def partial_object_including_required(): - assert_valid( - """ + assert_valid(""" { complicatedArgs { complexArgField(complexArg: { requiredField: true, intField: 4 }) } } - """ - ) + """) def full_object(): - assert_valid( - """ + assert_valid(""" { complicatedArgs { complexArgField(complexArg: { @@ -911,12 +848,10 @@ def full_object(): }) } } - """ - ) + """) def full_object_with_fields_in_different_order(): - assert_valid( - """ + assert_valid(""" { complicatedArgs { complexArgField(complexArg: { @@ -928,8 +863,26 @@ def full_object_with_fields_in_different_order(): }) } } - """ - ) + """) + + def describe_valid_one_of_input_object_value(): + def exactly_one_field(): + assert_valid(""" + { + complicatedArgs { + oneOfArgField(oneOfArg: { stringField: "abc" }) + } + } + """) + + def exactly_one_non_nullable_variable(): + assert_valid(""" + query ($string: String!) { + complicatedArgs { + oneOfArgField(oneOfArg: { stringField: $string }) + } + } + """) def describe_invalid_input_object_value(): def partial_object_missing_required(): @@ -1097,10 +1050,62 @@ def allows_custom_scalar_to_accept_complex_literals(): schema=schema, ) + def describe_invalid_one_of_input_object_value(): + def invalid_field_type(): + assert_errors( + """ + { + complicatedArgs { + oneOfArgField(oneOfArg: { stringField: 2 }) + } + } + """, + [ + { + "message": "String cannot represent a non string value: 2", + "locations": [(4, 60)], + }, + ], + ) + + def exactly_one_null_field(): + assert_errors( + """ + { + complicatedArgs { + oneOfArgField(oneOfArg: { stringField: null }) + } + } + """, + [ + { + "message": "Field 'OneOfInput.stringField' must be non-null.", + "locations": [(4, 45)], + }, + ], + ) + + def more_than_one_field(): + assert_errors( + """ + { + complicatedArgs { + oneOfArgField(oneOfArg: { stringField: "abc", intField: 123 }) + } + } + """, + [ + { + "message": "OneOf Input Object 'OneOfInput'" + " must specify exactly one key.", + "locations": [(4, 45)], + }, + ], + ) + def describe_directive_arguments(): def with_directives_of_valid_types(): - assert_valid( - """ + assert_valid(""" { dog @include(if: true) { name @@ -1109,8 +1114,7 @@ def with_directives_of_valid_types(): name } } - """ - ) + """) def with_directives_with_incorrect_types(): assert_errors( @@ -1136,8 +1140,7 @@ def with_directives_with_incorrect_types(): def describe_variable_default_values(): def variables_with_valid_default_values(): - assert_valid( - """ + assert_valid(""" query WithDefaultValues( $a: Int = 1, $b: String = "ok", @@ -1146,12 +1149,10 @@ def variables_with_valid_default_values(): ) { dog { name } } - """ - ) + """) def variables_with_valid_default_null_values(): - assert_valid( - """ + assert_valid(""" query WithDefaultValues( $a: Int = null, $b: String = null, @@ -1159,8 +1160,7 @@ def variables_with_valid_default_null_values(): ) { dog { name } } - """ - ) + """) def variables_with_invalid_default_null_values(): assert_errors( diff --git a/tests/validation/test_variables_are_input_types.py b/tests/validation/test_variables_are_input_types.py index 9440f1b2..53f1c2d5 100644 --- a/tests/validation/test_variables_are_input_types.py +++ b/tests/validation/test_variables_are_input_types.py @@ -11,22 +11,18 @@ def describe_validate_variables_are_input_types(): def unknown_types_are_ignored(): - assert_valid( - """ + assert_valid(""" query Foo($a: Unknown, $b: [[Unknown!]]!) { field(a: $a, b: $b) } - """ - ) + """) def input_types_are_valid(): - assert_valid( - """ + assert_valid(""" query Foo($a: String, $b: [Boolean!]!, $c: ComplexInput) { field(a: $a, b: $b, c: $c) } - """ - ) + """) def output_types_are_invalid(): assert_errors( diff --git a/tests/validation/test_variables_in_allowed_position.py b/tests/validation/test_variables_in_allowed_position.py index 03fbbcbb..4e21ce10 100644 --- a/tests/validation/test_variables_in_allowed_position.py +++ b/tests/validation/test_variables_in_allowed_position.py @@ -11,20 +11,17 @@ def describe_validate_variables_are_in_allowed_positions(): def boolean_to_boolean(): - assert_valid( - """ + assert_valid(""" query Query($booleanArg: Boolean) { complicatedArgs { booleanArgField(booleanArg: $booleanArg) } } - """ - ) + """) def boolean_to_boolean_in_fragment(): - assert_valid( - """ + assert_valid(""" fragment booleanArgFrag on ComplicatedArgs { booleanArgField(booleanArg: $booleanArg) } @@ -34,11 +31,9 @@ def boolean_to_boolean_in_fragment(): ...booleanArgFrag } } - """ - ) + """) - assert_valid( - """ + assert_valid(""" query Query($booleanArg: Boolean) { complicatedArgs { @@ -48,24 +43,20 @@ def boolean_to_boolean_in_fragment(): fragment booleanArgFrag on ComplicatedArgs { booleanArgField(booleanArg: $booleanArg) } - """ - ) + """) def non_null_boolean_to_boolean(): - assert_valid( - """ + assert_valid(""" query Query($nonNullBooleanArg: Boolean!) { complicatedArgs { booleanArgField(booleanArg: $nonNullBooleanArg) } } - """ - ) + """) def non_null_boolean_to_boolean_within_fragment(): - assert_valid( - """ + assert_valid(""" fragment booleanArgFrag on ComplicatedArgs { booleanArgField(booleanArg: $nonNullBooleanArg) } @@ -76,90 +67,75 @@ def non_null_boolean_to_boolean_within_fragment(): ...booleanArgFrag } } - """ - ) + """) def array_of_string_to_array_of_string(): - assert_valid( - """ + assert_valid(""" query Query($stringListVar: [String]) { complicatedArgs { stringListArgField(stringListArg: $stringListVar) } } - """ - ) + """) def array_of_non_null_string_to_array_of_string(): - assert_valid( - """ + assert_valid(""" query Query($stringListVar: [String!]) { complicatedArgs { stringListArgField(stringListArg: $stringListVar) } } - """ - ) + """) def string_to_array_of_string_in_item_position(): - assert_valid( - """ + assert_valid(""" query Query($stringVar: String) { complicatedArgs { stringListArgField(stringListArg: [$stringVar]) } } - """ - ) + """) def non_null_string_to_array_of_string_in_item_position(): - assert_valid( - """ + assert_valid(""" query Query($stringVar: String!) { complicatedArgs { stringListArgField(stringListArg: [$stringVar]) } } - """ - ) + """) def complex_input_to_complex_input(): - assert_valid( - """ + assert_valid(""" query Query($complexVar: ComplexInput) { complicatedArgs { complexArgField(complexArg: $complexVar) } } - """ - ) + """) def complex_input_to_complex_input_in_field_position(): - assert_valid( - """ + assert_valid(""" query Query($boolVar: Boolean = false) { complicatedArgs { complexArgField(complexArg: {requiredArg: $boolVar}) } } - """ - ) + """) def non_null_boolean_to_non_null_boolean_in_directive(): - assert_valid( - """ + assert_valid(""" query Query($boolVar: Boolean!) { dog @include(if: $boolVar) } - """ - ) + """) def int_to_non_null_int(): assert_errors( @@ -334,32 +310,67 @@ def int_to_non_null_int_fails_when_var_provides_null_default_value(): ) def int_to_non_null_int_when_var_provides_non_null_default_value(): - assert_valid( - """ + assert_valid(""" query Query($intVar: Int = 1) { complicatedArgs { nonNullIntArgField(nonNullIntArg: $intVar) } } - """ - ) + """) def int_to_non_null_int_when_optional_arg_provides_default_value(): - assert_valid( - """ + assert_valid(""" query Query($intVar: Int) { complicatedArgs { nonNullFieldWithDefault(nonNullIntArg: $intVar) } } - """ - ) + """) def bool_to_non_null_bool_in_directive_with_default_value_with_option(): - assert_valid( - """ + assert_valid(""" query Query($boolVar: Boolean = false) { dog @include(if: $boolVar) } - """ - ) + """) + + def describe_validates_one_of_input_objects(): + def allows_exactly_one_non_nullable_variable(): + assert_valid(""" + query ($string: String!) { + complicatedArgs { + oneOfArgField(oneOfArg: { stringField: $string }) + } + } + """) + + def undefined_variable_in_one_of_input_object(): + assert_errors( + """ + { + complicatedArgs { + oneOfArgField(oneOfArg: { stringField: $undefinedVariable }) + } + } + """, + [], + ) + + def forbids_one_nullable_variable(): + assert_errors( + """ + query ($string: String) { + complicatedArgs { + oneOfArgField(oneOfArg: { stringField: $string }) + } + } + """, + [ + { + "message": "Variable '$string' is of type 'String'" + " but must be non-nullable to be used for OneOf" + " Input Object 'OneOfInput'.", + "locations": [(2, 24), (4, 60)], + } + ], + ) diff --git a/tox.ini b/tox.ini index f9c3a5e2..0242f331 100644 --- a/tox.ini +++ b/tox.ini @@ -1,59 +1,68 @@ [tox] -envlist = py3{6,7,8,9,10}, black, flake8, mypy, docs, manifest +envlist = py3{7,8,9,10,11,12,13,14}, pypy3{9,10,11}, black, flake8, mypy, docs, manifest isolated_build = true [gh-actions] python = - 3.6: py36 + 3: py313 3.7: py37 3.8: py38 3.9: py39 3.10: py310 + 3.11: py311 + 3.12: py312 + 3.13: py313 + 3.14: py314 + pypy3: pypy39 + pypy3.9: pypy39 + pypy3.10: pypy310 + pypy3.11: pypy311 [testenv:black] -basepython = python3.9 -deps = black==22.8.0 +basepython = python3.14 +deps = black==26.5.1 commands = - black src tests setup.py -t py39 --check + black src tests setup.py -t py37 --check [testenv:flake8] -basepython = python3.9 -deps = flake8>=5,<6 +basepython = python3.14 +deps = flake8>=7.3,<8 commands = flake8 src tests setup.py [testenv:mypy] -basepython = python3.9 +basepython = python3.14 deps = - mypy==0.971 - pytest>=6.2,<7 + mypy>=2.1,<3 + pytest>=8.4,<9 commands = mypy src tests [testenv:docs] -basepython = python3.9 +basepython = python3.14 deps = - sphinx>=4.3,<5 - sphinx_rtd_theme>=1,<2 + sphinx>=9.1,<10 + sphinx_rtd_theme>=3.1,<4 commands = sphinx-build -b html -nEW docs docs/_build/html [testenv:manifest] -basepython = python3.9 -deps = check-manifest>=0.48,<1 +basepython = python3.14 +deps = check-manifest>=0.51,<1 commands = check-manifest -v [testenv] deps = - py37,py38,py39,py310: pytest>=7.1,<8 - py36: pytest>=6.2,<7 - pytest-asyncio>=0.16,<1 - pytest-benchmark>=3.4,<4 - pytest-cov>=3,<4 - pytest-describe>=2,<3 - pytest-timeout>=2,<3 - py37: typing-extensions>=4.3,<5 - py36: typing-extensions>=4.1,<5 + pytest>=7.4,<9 + pytest-asyncio>=0.21,<1 + pytest-benchmark>=4.0,<6 + pytest-cov>=4.1,<8 + pytest-describe>=2,<4 + pytest-timeout>=2.4,<3 + py3{7,8,9},pypy39: typing-extensions>=4.1,<5 commands = - pytest tests {posargs: --cov-report=term-missing --cov=graphql --cov=tests --cov-fail-under=100} + # to also run the time-consuming tests: tox -e py314 -- --run-slow + # to run the benchmarks: tox -e py314 -- -k benchmarks --benchmark-enable + py3{7,8,9,10,11,13},pypy3{9,10,11}: pytest tests {posargs} + py314: pytest tests {posargs: --cov-report=term-missing --cov=graphql --cov=tests --cov-fail-under=100}