Compare commits

..

690 Commits

Author SHA1 Message Date
Erik Wrede
8290326308
release: 3.4.3 2024-11-09 21:43:17 +01:00
Philipp Hagemeister
4a274b8424
fix: raise proper error when UUID parsing fails (#1582)
* Do not raise AttributeError when parsing non-string UUIDs

When a user sends a dictionary or other object as a UUID variable like `{[123]}`, previously graphene crashed with an `AttributeError`, like this:

```
(…)
  File "…/lib/python3.12/site-packages/graphql/utils/is_valid_value.py", line 78, in is_valid_value
    parse_result = type.parse_value(value)
                   ^^^^^^^^^^^^^^^^^^^^^^^
  File "…/lib/python3.12/site-packages/graphene/types/uuid.py", line 33, in parse_value
    return _UUID(value)
           ^^^^^^^^^^^^
  File "/usr/lib/python3.12/uuid.py", line 175, in __init__
    hex = hex.replace('urn:', '').replace('uuid:', '')
          ^^^^^^^^^^^
AttributeError: 'dict' object has no attribute 'replace'
```

But an `AttributeError` makes it seem like this is the server's fault, when it's obviously the client's.

Report a proper GraphQLError.

* fix: adjust exception message structure

---------

Co-authored-by: Erik Wrede <erikwrede@users.noreply.github.com>
2024-11-09 21:42:51 +01:00
Erik Wrede
b3db1c0cb2
release: 3.4.2 2024-11-09 18:18:36 +01:00
Muhammed Al-Dulaimi
3ed7bf6362
chore: Make Union meta overridable (#1583)
This PR makes the Union Options configurable, similar to how it works with ObjectTypes
---------

Co-authored-by: Erik Wrede <erikwrede@users.noreply.github.com>
2024-11-09 18:17:42 +01:00
Erik Wrede
ccae7364e5
release: 3.4.1 2024-10-27 21:16:40 +01:00
Erik Wrede
cf97cbb1de
fix: use dateutil-parse for < 3.11 support (#1581)
* fix: use dateutil-parse for < 3.11 support

* chore: lint

* chore: lint

* fix mypy deps

* fix mypy deps

* chore: lint

* chore: fix test
2024-10-27 21:14:55 +01:00
Erik Wrede
dca31dc61d
release: 3.4.0 2024-10-18 13:43:07 +02:00
Erik Wrede
73df50e3dc
housekeeping: switch 3.13 to non-dev 2024-10-18 13:40:31 +02:00
Dulmandakh
821451fddc
CI: bump upload-artifact and codecov actions (#1567)
CI: bump actions/upload-artifact and codecov/codecov-action actions
2024-09-29 15:23:21 +02:00
Dulmandakh
f2e68141fd
CI: build package (#1564) 2024-09-29 13:40:19 +02:00
Dulmandakh
431826814d
lint: use ruff pre commit hook (#1566)
* lint: use ruff pre commit hook

* dont install ruff

---------

Co-authored-by: Erik Wrede <erikwrede@users.noreply.github.com>
2024-09-29 13:33:10 +02:00
Dulmandakh
5b3ed2c2ba
bump pre-commit to 3.7 (#1568) 2024-09-29 13:32:26 +02:00
Erik Wrede
f95e9221bb
refactor: replace @deprecated decorator with upcoming native support (via typing-extensions), bump mypy (#1578)
* refactor: replace @deprecated decorator with upcoming native support (via typing-extensions)

* chore: fix tests

* chore: ruff fmt
2024-09-29 13:31:24 +02:00
Florian Zimmermann
48678afba4
fix: run the tests in python 3.12 and 3.13 and remove snapshottest dependency (#1572)
* actually run the tests in python 3.12 and 3.13

* remove snapshottest from the example tests

so that the tests pass in 3.12 and 3.13 again

* remove the section about snapshot testing from the testing docs

because the snapshottest package doesn't work on Python 3.12 and above

* fix assertion for badly formed JSON input on Python 3.13

* fix deprecation warning about datetime.utcfromtimestamp()
2024-08-08 11:49:26 +02:00
Dulmandakh
dc3b2e49c1
CI: fix tests on Python 3.13 (#1562) 2024-07-01 17:03:49 +02:00
Dulmandakh
d53a102b08
Lint using Ruff (#1563)
* lint using Ruff

* remove isort config, flake8 comments
2024-07-01 17:03:13 +02:00
Dulmandakh
fd9ecef36e
CI: format check using Ruff (#1557)
* CI: format check using Ruff

* precommit, setup py

* gitignore ruff_cache

---------

Co-authored-by: Erik Wrede <erikwrede@users.noreply.github.com>
2024-06-28 15:05:04 +02:00
Dulmandakh
1263e9b41e
pytest 8 (#1549)
* pytest 8

* bump coveralls, pytest-cov

---------

Co-authored-by: Erik Wrede <erikwrede@users.noreply.github.com>
2024-06-28 15:04:25 +02:00
Dulmandakh
74b33ae148
remove README.rst, leave only README.md (#1559)
remove README.rst
2024-06-28 15:03:48 +02:00
Dulmandakh
6834385786
support python 3.13 (#1561) 2024-06-28 15:03:34 +02:00
Dulmandakh
c335c5f529
fix lint error in SECURITY.md (#1556)
fix lint SECURITY.md
2024-06-23 18:24:34 +02:00
Erik Wrede
d90d65cafe
chore: adjust incorrect development status 2024-06-22 12:31:14 +02:00
Dulmandakh
5924cc4150
remove Python 2 (#1547)
Co-authored-by: Erik Wrede <erikwrede@users.noreply.github.com>
2024-06-13 16:52:06 +02:00
Erik Wrede
6a668514de
docs: create security.md (#1554) 2024-06-13 16:51:43 +02:00
tijuca
88c3ec539b
pytest: Don't use nose like syntax in graphene/relay/tests/test_custom_global_id.py (#1539) (#1540)
pytest: Don't use nose like syntax

The tests in test_custom_global_id.py use the old nose specific method
'setup(self)' which isn't supported anymore in Pytest 8+. The tests fail
with this error message without modification.

E               pytest.PytestRemovedIn8Warning: Support for nose tests is deprecated and will be removed in a future release.
E               graphene/relay/tests/test_custom_global_id.py::TestIncompleteCustomGlobalID::test_must_define_resolve_global_id is using nose-specific method: `setup(self)`
E               To remove this warning, rename it to `setup_method(self)`
E               See docs: https://docs.pytest.org/en/stable/deprecations.html#support-for-tests-written-for-nose

Co-authored-by: Erik Wrede <erikwrede@users.noreply.github.com>
2024-06-13 14:38:48 +00:00
Dulmandakh
17d09c8ded
remove aniso8601, mock, iso8601 (#1548)
* remove aniso8601

* remove mock, iso8601

---------

Co-authored-by: Erik Wrede <erikwrede@users.noreply.github.com>
2024-06-13 16:35:00 +02:00
Dulmandakh
614449e651
Python 3.12 (#1550)
* python 3.12

* update classifiers
2024-06-13 14:34:16 +00:00
Dulmandakh
44dcdad182
CI: fix deprecation warning (#1551) 2024-06-13 16:32:50 +02:00
Dulmandakh
221afaf4c4
bump pytest to 7 (#1546)
* bump pytest

* downgrade pytest-cov
2024-05-16 10:17:46 +02:00
Dulmandakh
5db1af039f
Remove Python 3.7 (#1543)
* CI: add Python 3.12

* dd

* remove python 3.12
2024-05-16 10:17:26 +02:00
Dulmandakh
82d0a68a81
remove polyfill for dataclasses (#1545)
* remove polyfill for dataclasses

* fix lint
2024-05-16 10:09:37 +02:00
Dulmandakh
3cd0c30de8
CI: bump GH actions (#1544) 2024-05-16 10:09:19 +02:00
Andrew Swait
5fb7b54377
docs: update docstring for type arg of Field (#1527) 2023-10-06 22:15:26 +02:00
wongcht
baaef0d21a
chore: remove pytz (#1520) 2023-08-30 23:41:17 +02:00
Erik Wrede
93cb33d359
housekeeping: delete outdated ROADMAP.md 2023-07-26 09:43:40 +02:00
Erik Wrede
f5aba2c027
release: 3.3.0 2023-07-26 08:26:30 +02:00
garo (they/them)
ea7ccc350e
feat(relay): add option for strict connection types (#1504)
* types: add option for strict connection types

* chore: appease linter

* chore: appease linter

* test: add test

---------

Co-authored-by: Erik Wrede <erikwrede@users.noreply.github.com>
2023-07-26 08:25:57 +02:00
Dulmandakh
6b8cd2dc78
ci: drop python 3.6 (#1507)
Co-authored-by: Erik Wrede <erikwrede@users.noreply.github.com>
2023-07-19 09:08:19 +02:00
Naoya Yamashita
74db349da4
docs: add get_human function (#1380)
Co-authored-by: Erik Wrede <erikwrede@users.noreply.github.com>
2023-07-19 09:01:00 +02:00
Ransom Williams
99f0103e37
test: print schema with InputObjectType with DateTime field with default_value (#1293) (#1513)
* test [1293]: regression test print schema with InputObjectType with DateTime field with default_value

* chore: clarify test title and assertion

---------

Co-authored-by: Erik Wrede <erikwrede@users.noreply.github.com>
2023-07-19 07:00:30 +00:00
Erik Wrede
03cf2e131e
chore: remove travis ci link 2023-06-06 20:45:01 +02:00
Jeongseok Kang
d77d0b0571
chore: Use typing.TYPE_CHECKING instead of MYPY (#1503)
Co-authored-by: Erik Wrede <erikwrede@users.noreply.github.com>
2023-06-04 23:49:26 +02:00
senseysensor
c636d984c6
fix: Corrected enum metaclass to fix pickle.dumps() (#1495)
* Corrected enum metaclass to fix pickle.dumps()

* considered case with colliding class names (try to distinguish by file name)

* reverted simple solution back (without attempt to support duplicate Enum class names)

---------

Co-authored-by: sgrekov <sgrekov@lohika.com>
Co-authored-by: Erik Wrede <erikwrede@users.noreply.github.com>
2023-06-04 23:10:05 +02:00
Cadu
2da8e9db5c
feat: Enable use of Undefined in InputObjectTypes (#1506)
* Changed InputObjectType's default builder-from-dict argument to be `Undefined` instead of `None`, removing ambiguity of undefined optional inputs using dot notation access syntax.

* Move `set_default_input_object_type_to_undefined()` fixture into conftest.py for sharing it between multiple test files.
2023-06-04 23:01:05 +02:00
Firas Kafri
8ede21e063
chore: default enum description to "An enumeration." (#1502)
* Default enum description to "An enumeration."

default to this string, which is used in many tests, is causing

* Use the docstring descriptions of enums when they are present

* Added tests

* chore: add missing newline

* Fix new line

---------

Co-authored-by: Erik Wrede <erikwrede@users.noreply.github.com>
2023-05-25 12:21:55 +02:00
Erik Wrede
57cbef6666
release: 3.2.2 2023-03-13 21:24:16 +01:00
Erik Wrede
d33e38a391
chore: make relay type fields extendable (#1499) 2023-03-13 21:23:28 +01:00
Roman Solomatin
b76e89c0c2
docs: remove unpair bracket (#1500) 2023-03-09 10:09:15 +01:00
Erik Wrede
81e7eee5da
Update README.md 2023-03-03 17:35:46 +01:00
Erik Wrede
969a630541
Update README.md 2023-03-03 17:35:05 +01:00
QuentinN42
8b89afeff1
docs: update sphinx to the latest version (#1497) 2023-02-28 13:21:45 +01:00
Peder Johnsen
52143473ef
docs: Remove prerelease notice (#1487) 2022-12-25 22:59:05 +01:00
Pei-Lun H
8eb2807ce5
docs: Correct the module name of custom scalar example in documentation (#1486) 2022-12-23 07:57:45 +01:00
Erik Wrede
340d5ed12f
release: 3.2.1 2022-12-11 21:05:25 +01:00
Vladyslav Hutov
19ea63b9c5
fix: Input fields and Arguments can now be deprecated (#1472)
Non-required InputFields and arguments now support deprecation via setting the `deprecation_reason` argument upon creation.
2022-12-10 12:25:07 +01:00
Erik Wrede
d5dadb7b1b
release: 3.2.0
fixes previous release number 3.1.2 due to a pending feature release
2022-12-09 10:53:50 +01:00
Erik Wrede
8596349405
release: 3.1.2 2022-12-09 10:46:24 +01:00
Mike Roberts
a141e848c3
Do not interpret Enum members called 'description' as description properties (#1478)
This is a workaround for `TypeError`s being raised when initialising schemas with
Enum members named `description` or `deprecation_reason`.


Fixes #1321
2022-12-01 11:06:24 +01:00
Erik Wrede
f09b2e5a81
housekeeping: pin ubuntu to 20.04 for python 3.6
Ubuntu:latest doesn't include py36 anymore. Keep this until we add 3.11 and drop 3.6.
See:
https://github.com/actions/setup-python/issues/544
https://github.com/rwth-i6/returnn/issues/1226
2022-11-21 15:40:05 +01:00
Mike Roberts
7f6fa16194
feat_ (#1476)
Previously, installing graphene and trying to do `from graphene.test import Client`
as recommended in the docs caused an `ImportError`, as the 'promise' library
is imported but only listed as a requirement in the 'test' section of the setup.py
file.
2022-11-16 21:38:15 +01:00
Rens Groothuijsen
0b1bfbf65b
chore: Make Graphene enums iterable like Python enums (#1473)
* Makes Graphene enums iterable like Python enums by implementing __iter__
2022-11-16 21:30:49 +01:00
Rens Groothuijsen
f891a3683d
docs: Disambiguate argument name in quickstart docs (#1474) 2022-11-16 21:27:34 +01:00
Erik Wrede
a2b63d8d84
fix: MyPy findings due to a mypy version upgrade were corrected (#1477) 2022-11-16 21:23:37 +01:00
Rens Groothuijsen
b349632a82
Clarify execution order in middleware docs (#1475) 2022-11-15 08:48:48 +01:00
Kevin Le
ccdd35b354
hashable Enum (#1461) 2022-10-27 13:55:38 +02:00
Kristian Uzhca
6969023491
Add copy function for GrapheneGraphQLType (#1463) 2022-10-24 20:06:24 +02:00
Thomas Leonard
ee1ff975d7
feat: Add support for custom global (Issue #1276) (#1428)
Co-authored-by: Thomas Leonard <thomas@loftorbital.com>
2022-09-19 10:17:31 +02:00
Erik Wrede
b20bbdcdf7
v3.1.1 2022-09-08 10:55:05 +02:00
Cadu
694c1db21e
Vendor DataLoader from aiodataloader and move get_event_loop() out of __init__ function. (#1459)
* Vendor DataLoader from aiodataloader and also move get_event_loop behavior from `__init__` to a property which only gets resolved when actually needed (this will solve PyTest-related to early get_event_loop() issues)

* Added DataLoader's specific tests

* plug `loop` parameter into `self._loop`, so that we still have the ability to pass in a custom event loop, if needed.


Co-authored-by: Erik Wrede <erikwrede2@gmail.com>
2022-09-07 20:32:53 +02:00
Erik Wrede
20219fdc1b
Update README.md
Update
2022-09-06 13:42:38 +02:00
Christian Clauss
45986b18e7
Upgrade GitHub Actions (#1457) 2022-08-28 20:25:55 +02:00
Ülgen Sarıkavak
c5ccc9502d
Upgrade base Python version to 3.10 (#1449) 2022-08-28 17:33:35 +02:00
Erik Wrede
35c281a3cd
Fix BigInt export (#1456) 2022-08-28 17:30:26 +02:00
Ülgen Sarıkavak
355601bd5c
Remove duplicate flake8 call in tox, it's covered by pre-commit (#1448) 2022-08-27 18:13:48 +02:00
Christian Clauss
cbf59a88ad
Add Python 3.11 release candidate 1 to the testing (#1450)
* Add Python 3.11 release candidate 1 to the testing

https://www.python.org/download/pre-releases

* Update tests.yml
2022-08-27 18:06:38 +02:00
Erik Wrede
24059a8b40
Merge pull request #1370 from DrewHoo/fix/ariadne-link 2022-08-20 14:59:22 +02:00
Erik Wrede
d96ec55abb
Merge branch 'master' into fix/ariadne-link 2022-08-20 14:59:02 +02:00
Erik Wrede
f1e8f4862a
Merge pull request #1447 from ulgens/update_precommit 2022-08-19 08:57:13 +02:00
Ülgen Sarıkavak
e6429c3c5b Update pre-commit hooks 2022-08-19 09:20:51 +03:00
Erik Wrede
ed1290aaba
Merge pull request #1407 from alimcmaster1/patch-1
Update quickstart.rst
2022-08-18 09:41:32 +02:00
Erik Wrede
84faf8f57c
Merge pull request #1444 from karmingc/karmingc/readme-typo
fix: use install instead of instal for consistency
2022-08-18 09:39:32 +02:00
karming
0ac4d9397e fix: use install instead of instal for consistency 2022-08-16 19:21:29 -04:00
Erik Wrede
023452a09f
Merge pull request #1442 from graphql-python/erikwrede-patch-1
Delete coveralls.yml
2022-08-14 12:42:55 +02:00
Erik Wrede
6339f489e9
Delete coveralls.yml
We are now using Codecov
2022-08-14 12:09:07 +02:00
Erik Wrede
23ca978918
Merge pull request #1414 from loft-orbital/issue-#1413_fix-invalid-input-type
Providing an invalid value to an input type will now provoke an exception.
For example if the input as the type `UUID` and that you provide the value `2` it will now fail.
2022-08-14 11:55:45 +02:00
Erik Wrede
97abb9db42
Merge pull request #1381 from belkka/patch-1
Avoid ambiguity in graphene.Mutation docstring [documentation]
2022-08-13 15:19:52 +02:00
Erik Wrede
57f3aa3ba9
Merge pull request #1190 from graphql-python/update-dataloaderdocs
Update Dataloader docs
2022-08-13 15:14:44 +02:00
Erik Wrede
8e1c3d3102
Merge branch 'master' into update-dataloaderdocs 2022-08-13 15:11:12 +02:00
Erik Wrede
13c661332e
Fix typo
Co-authored-by: Justin Miller <justinrmiller@users.noreply.github.com>
2022-08-13 15:08:34 +02:00
Erik Wrede
80e3498750
Fix Test Failure due to #1304
assert bar_graphql_type.interfaces == [foo_graphql_type] failed only on tox, because .interfaces was a tuple instead of a list. Error didn't occur using just pytest. Fixed by explicitly
converting both to list.
2022-08-13 14:51:58 +02:00
Erik Wrede
c77d87d205
Merge pull request #1304 from closeio/support-interface-implementations
Add support for interfaces on interfaces
2022-08-13 14:28:17 +02:00
Erik Wrede
8bdcec5cd7
Merge pull request #1402 from RJ722/patch-1
Docs: Highlight .get in backticks
2022-08-13 14:18:21 +02:00
Erik Wrede
dfece7f65d
Merge pull request #1437 from timgates42/bugfix_typos 2022-07-17 00:30:54 +02:00
Tim Gates
8589aaeb98
docs: Fix a few typos
There are small typos in:
- UPGRADE-v1.0.md
- UPGRADE-v2.0.md
- docs/execution/fileuploading.rst

Fixes:
- Should read `standard` rather than `stantard`.
- Should read `library` rather than `libary`.
- Should read `explicitly` rather than `explicity`.

Signed-off-by: Tim Gates <tim.gates@iress.com>
2022-07-16 14:40:00 +10:00
Thomas Leonard
72c2fd5ec3
Merge pull request #1430 from loft-orbital/feat/enable-to-provide-enum-name
feat: add ability to provide a type name to enum when using from_enum
2022-06-27 18:07:49 +02:00
Erik Wrede
69be326290
Merge pull request #1431 from erikwrede/master
Add Codecov to github actions
2022-06-27 16:41:24 +02:00
Erik Wrede
2ee23b0b2c Add codecov action 2022-06-27 16:30:01 +02:00
Thomas Leonard
8f6a8f9c4a feat: add ability to provide a type name to enum when using from_enum 2022-06-24 18:18:04 +02:00
Thomas Leonard
3bdc67c6ae fix: input with invalid types should raise an error 2022-06-20 15:15:14 +02:00
Thomas Leonard
efe4b89015
Merge pull request #1424 from ramonwenger/master
Fix typo in union comments
2022-06-20 15:14:27 +02:00
Ramon Wenger
785fcb38b6
Merge branch 'graphql-python:master' into master 2022-06-15 13:48:25 +02:00
Jonathan Kim
5475a7ad1f
v3.1.0 2022-05-30 13:57:16 +01:00
Ramon Wenger
5d4e71f463 Fix typo in union comments 2022-05-25 17:45:28 +02:00
Christoph Zwerschke
9c3e4bb7da
Merge pull request #1421 from Cito/upgrade-dev-env
Make Graphene compatible with GraphQL-Core 3.2
2022-05-07 00:53:24 +02:00
Christoph Zwerschke
9e7e08d48a
Make Graphene compatible with Core 3.2 2022-05-07 00:50:03 +02:00
Christoph Zwerschke
e37ef00ca4
Update test and dev environment 2022-05-06 22:31:31 +02:00
Christoph Zwerschke
4e8a1e6057
Merge pull request #1420
fix: add default param _variables to parse_literal #1419
2022-05-06 21:57:57 +02:00
René Birrer
181d9f76da fix: add default param _variables to parse_literal #1419
This is to match the `graphql-core` API. If it's not respected
the `parse_literal` method will produce an error event though
dealing with a valid value.
2022-05-03 13:51:14 +02:00
Jonathan Kim
03277a5512
Merge pull request #1412 from loft-orbital/issue-#1394_fix-required 2022-04-07 15:18:17 +01:00
Thomas Leonard
19ebf08339 fix: default value for argument should be Undefined (Issue #1394) and update function from_global_id exception handling (b217aefa8c) 2022-03-20 18:49:44 +01:00
Ali McMaster
bf40e6c419
Update quickstart.rst 2022-02-14 09:01:42 +00:00
Syrus Akbary
61f0d8a8e0
Merge pull request #1405 from conao3/patch-1
fix UPGRADE-v2.0.md
2022-02-13 12:57:54 -08:00
Naoya Yamashita
763910e7b5
fix UPGRADE-v2.0.md 2022-02-07 15:51:08 +09:00
Rahul Jha
beb957382d
Highlight .get in backticks
When I first read through the documentation twice, it took me two tries and looking very hard to find out the difference b/w the two. The background highlight using backticks would be helpful in this case.
2022-01-13 15:33:09 +05:30
Syrus Akbary
06eb1a3e82
Merge pull request #1401 from Cito/test-py39-and-py310
Add Python 3.9 and 3.10 to the test matrix
2022-01-11 13:22:25 +01:00
Christoph Zwerschke
e441fa72aa
Add Python 3.9 and 3.10 to the test matrix
Also update the test dependencies and adapt two tests (#1400).
2022-01-11 12:13:12 +01:00
Syrus Akbary
7ecb4e68ba
Merge pull request #1387 from GDGSNF/master
Chore: Refactor Multi Expression Code 
2021-12-13 15:02:02 +01:00
Syrus Akbary
9311d3525d
Merge pull request #1324 from justinrmiller/doc-fixes
Various spelling and grammar fixes for the documentation.
2021-12-13 14:59:53 +01:00
Yasser Tahiri
7108bc8577
Update node.py 2021-12-02 12:04:07 +01:00
Mel van Londen
a61f0a214d update README.rst using pandoc 2021-11-13 14:25:10 -08:00
Mel van Londen
27f19e5a90 release v3 stable 2021-11-13 14:15:18 -08:00
Yasser Tahiri
9e17044ddc
Chore: Refactor Multi Expression Code 2021-11-05 02:21:14 +01:00
belkka
b274a607f4
Avoid ambiguity in graphene.Mutation docstring
The code example in docstring starts with `from graphene import Mutation` and defines a `class Mutation` later. This definition would shadow previously imported name and (which is more important) confuses a reader about usage of this class — one need to keep in mind that previous usage of `Mutation` is imported from graphene and have not been overridden yet.

This PR changes an import-from statement to an import statement, so `graphene.Mutation` is used explicitly. This approach is consistent with other code examples in docs (e. g. https://docs.graphene-python.org/en/v2.1.9/types/mutations/).

Another option is to change name of example class Mutation to something more clear (maybe SchemaMutation or RootMutation), but I'm not sure what name to choose.

Only docstring is updated, no code changes.
2021-10-11 23:46:13 +03:00
Eran Kampf
0a54094f59
v3.0.0b8 2021-09-29 23:42:36 -07:00
Syrus Akbary
9c1db0f662
Merge pull request #1376 from codebyaryan/master
Fix unseen examples
2021-09-30 08:23:09 +02:00
Syrus Akbary
03aad2799a
Merge pull request #1377 from graphql-python/fix-graphql-core-dependency
Fix GraphQL-core dependency
2021-09-30 08:22:41 +02:00
Eran Kampf
b6c8931b22
Fix GraphQL-core dependency
GraphQL-core released `3.2.0rc1` with some breaking changes and
1. We should be getting RC releases in our dependencies
2. It has breaking changes, so we shouldn't get 3.2.0 unless someone fixes it explicitly
2021-09-29 17:11:16 -07:00
Aryan Iyappan
10aee710fc
Merge branch 'graphql-python:master' into master 2021-09-29 18:13:31 +05:30
Aryan Iyappan
1d6f9e984b
Mame sure to pass correct graphql schema instance 2021-09-29 18:13:08 +05:30
Aryan Iyappan
c1bd25555c
Update queryvalidation.rst 2021-09-28 06:41:54 +05:30
Drew Hoover
78973964b8
fix: update ariadne url to the new docs 2021-09-21 13:00:19 -04:00
Syrus Akbary
f039af2810
Merge pull request #1359 from codebyaryan/fix-actions
Fix actions
2021-08-23 22:07:12 -05:00
Aryan Iyappan
47696559c7 run linters locally 2021-08-24 08:30:54 +05:30
Aryan Iyappan
2e5944eb20 format code 2021-08-22 11:03:22 +05:30
Aryan Iyappan
2c66e496f7
Update tox.ini 2021-08-22 08:48:38 +05:30
Aryan Iyappan
908d5aeaeb
Update .pre-commit-config.yaml 2021-08-22 08:48:15 +05:30
Aryan Iyappan
d5d7a0e5e0
Update tox.ini 2021-08-21 21:41:47 +05:30
Aryan Iyappan
85f06fb2a6
Update tox.ini 2021-08-21 21:37:30 +05:30
Aryan Iyappan
a3a2f999aa
Update .pre-commit-config.yaml 2021-08-21 21:33:00 +05:30
Aryan Iyappan
7087710d02
Update .pre-commit-config.yaml 2021-08-21 21:17:00 +05:30
Aryan Iyappan
1c3054b7c8
Update test_connection_async.py 2021-08-21 21:01:27 +05:30
Aryan Iyappan
5896ade2dd
Update test_connection_query.py 2021-08-21 20:58:18 +05:30
Aryan Iyappan
76701e0809
Update .pre-commit-config.yaml 2021-08-21 20:53:58 +05:30
Aryan Iyappan
3b77b5f92a
Update tox.ini 2021-08-21 20:36:41 +05:30
Aryan Iyappan
16d0b32a8f
Update .pre-commit-config.yaml 2021-08-21 20:21:46 +05:30
Aryan Iyappan
d54b819552
Update .pre-commit-config.yaml 2021-08-21 18:37:44 +05:30
Aryan Iyappan
1886ec9dcb
Update schema.py 2021-08-21 18:34:39 +05:30
Aryan Iyappan
0ebff3313d
Update test_schema.py 2021-08-21 18:30:42 +05:30
Aryan Iyappan
7827219ba2
Update schema.py 2021-08-21 18:29:56 +05:30
Aryan Iyappan
ce59f1ff15
Rename .pre-commit-config.yml to .pre-commit-config.yaml 2021-08-21 17:48:33 +05:30
Aryan Iyappan
3145543386
Update tox.ini 2021-08-21 17:46:14 +05:30
Aryan Iyappan
7960b02124
Delete lint.yml 2021-08-21 17:41:12 +05:30
Aryan Iyappan
9807d6102c
Update coveralls.yml 2021-08-21 17:40:08 +05:30
Aryan Iyappan
e66d6148ab
Create lint.yml 2021-08-21 17:25:38 +05:30
Aryan Iyappan
1654d2fa29
Update coveralls.yml 2021-08-21 17:24:11 +05:30
Aryan Iyappan
772986ac83
Create lint.yml 2021-08-21 17:23:27 +05:30
Aryan Iyappan
0aef168687
Create deploy.yml 2021-08-21 17:21:48 +05:30
Aryan Iyappan
dc6b820635
Create coveralls.yml 2021-08-21 17:19:53 +05:30
Aryan Iyappan
16551369b2
Update tests.yml 2021-08-21 17:16:43 +05:30
Aryan Iyappan
e1822c9ae9
Create stale.yml 2021-08-21 17:15:28 +05:30
Aryan Iyappan
3c50fa817a
Delete stale.yml 2021-08-21 17:14:52 +05:30
Syrus Akbary
efc03533ae
Merge pull request #1357 from codebyaryan/master
add support for query validation
2021-08-20 19:40:50 -05:00
Aryan Iyappan
74a6565ea3
Update depth_limit.py 2021-08-20 21:07:57 +05:30
Aryan Iyappan
98980b53f6
Update depth_limit.py 2021-08-20 21:04:22 +05:30
Aryan Iyappan
57a4394bf3
Update depth_limit.py 2021-08-20 20:56:19 +05:30
Aryan Iyappan
ea4e6d65e9
Update schema.py 2021-08-20 16:08:58 +05:30
Aryan Iyappan
18cd3451f9
Update test_schema.py 2021-08-20 15:59:38 +05:30
Aryan Iyappan
946c2a3807
Update schema.py 2021-08-20 15:58:43 +05:30
Syrus Akbary
7d890bf915
Update graphene/validation/disable_introspection.py 2021-08-19 14:02:45 -05:00
Aryan Iyappan
0e4c14b076 update workflow: tests 2021-08-19 15:00:09 +05:30
Aryan Iyappan
8ae4369155 remove build matrix wherever not needed 2021-08-19 12:16:13 +05:30
Aryan Iyappan
c0ddbbfaf4 update workflow matrix 2021-08-19 12:13:46 +05:30
Aryan Iyappan
467b1f8e8d add workflow: tests 2021-08-19 12:03:27 +05:30
Aryan Iyappan
b4be4a686b add notice to failing tests 2021-08-19 10:59:58 +05:30
Aryan Iyappan
4e32dac251 add tests and docs for disable introspection rule 2021-08-14 08:41:24 +05:30
Aryan Iyappan
ec982ac50b update docs typo 2021-08-14 08:22:04 +05:30
Aryan Iyappan
c68071952d mention how to implement custom validators 2021-08-14 08:20:46 +05:30
Aryan Iyappan
ac5dd90f5f fix typo in docs 2021-08-14 07:54:58 +05:30
Aryan Iyappan
7be4bd6bc6 update docs 2021-08-14 07:49:09 +05:30
Aryan Iyappan
d7b474751d add depth limit validator tests 2021-08-14 07:45:34 +05:30
Aryan Iyappan
a784ef15e5 add disable introspection 2021-08-13 20:24:53 +05:30
Aryan Iyappan
5977b1648c fix typo 2021-08-13 20:04:42 +05:30
Aryan Iyappan
4259502dc3 update docs 2021-08-13 20:02:20 +05:30
Aryan Iyappan
fc2967e276 remove unused imports 2021-08-13 18:51:23 +05:30
Aryan Iyappan
aa11681048 add depth limit validator 2021-08-13 18:22:12 +05:30
Fabian Affolter
fce45ef552
Update pytz to 2021.1 (#1330) 2021-07-16 10:12:09 -07:00
Fabian Affolter
5290c9364c
Allow later aniso8601 releases (#1331) 2021-07-16 10:11:49 -07:00
Sergey Fedoseev
69b6286861
Fix typo in docstring of ObjectType (#1343) 2021-07-16 10:10:53 -07:00
kevinr-electric
485b1ed325
fix field name in execute.rst example (#1327)
fix field name in execute.rst 'Operation Name' example
2021-04-22 20:28:05 -07:00
Minh Tu Le
c08379ed85
Use argument's default_value regardless if the input field is required (#1326)
* Use argument's default value regardless if the input field is required

* Add a test

* Format code
2021-04-19 10:03:11 -07:00
Justin Miller
55cbc4d100 Merge branch 'DocFixes' of github.com:justinrmiller/graphene into DocFixes 2021-04-12 23:54:06 -07:00
Justin Miller
fbac4d5092 Fixing grammar and spelling errors across a number of files. 2021-04-12 23:53:36 -07:00
Justin Miller
002b769db4 Fixing Dataloader docs due to tox issue. 2021-04-12 23:32:11 -07:00
Justin Miller
12302b78f9
Update schema.rst 2021-04-12 23:08:42 -07:00
Justin Miller
db9d9a08f2
Update schema.rst 2021-04-12 23:01:20 -07:00
Justin Miller
3ed8273239
Update dataloader.rst 2021-04-12 22:48:53 -07:00
Justin Miller
a5fbb2e9e5
Update middleware.rst 2021-04-12 22:44:41 -07:00
Justin Miller
5acd04aa93
Update mutations.rst 2021-04-12 22:40:08 -07:00
Justin Miller
17f6a45a47
Update unions.rst 2021-04-12 22:37:32 -07:00
Justin Miller
f5321d619c
Update interfaces.rst 2021-04-12 22:33:14 -07:00
shukryzablah
f622f1f53c
Update index.rst (#1313) 2021-03-24 20:32:51 +01:00
bartenra
6f9cdb4888
Fix links to Relay docs (#1318) 2021-03-24 20:32:35 +01:00
Alec Rosenbaum
7004515f06 implement interface interfaces on TypeMap, fix failing test 2021-01-15 13:13:05 -05:00
Alec Rosenbaum
a17f63cf03 add failing type_map test, bar_graphql_type has no interfaces 2021-01-15 13:10:14 -05:00
Alec Rosenbaum
86b7e6ac86 update InterfaceOptions to fix failing test 2021-01-15 13:02:08 -05:00
Alec Rosenbaum
ae93499a37 add failing test for interface meta 2021-01-15 13:01:43 -05:00
Jonathan Kim
2e87ebe5fc
v3.0.0b7 2021-01-06 09:58:19 +00:00
Jason Kraus
e5eeb9d831
fix(Decimal): parse integers as decimal. (#1295) 2021-01-06 09:54:45 +00:00
Varun Dey
e0d4bec2d8
Remove Object Mutation dead link from Relay docs (#1272)
The official Relay project has removed 'Relay input Object Mutation' in favour of general mutation spec from their docs in [this PR](https://github.com/facebook/relay/pull/2401/files#diff-98bee0595817d7a46cd52d86e6c3db70) and is unavailable on their [official website](https://relay.dev/docs/en/graphql-server-specification#mutations) as well
2020-11-17 08:01:21 -08:00
Jonathan Kim
7d09e5b138
Update stale.yml 2020-10-27 08:51:51 +00:00
Jonathan Kim
84582eb374
v3.0.0b6 2020-10-21 10:15:38 +01:00
Alec Rosenbaum
e24ac547d6
Add UnforgivingExecutionContext (#1255) 2020-10-21 10:13:32 +01:00
Ali Reza Yahyapour
a53b782bf8
Syntax Error Fixed for Dictionary assert (#1267) 2020-09-22 17:10:01 +01:00
Paul Bailey
8c327fc4ed
add BigInt type (#1261)
* add BigInt type

* formatting

* more Int tests
2020-08-28 17:55:46 +02:00
Varun Dey
b685e109f5
Fix typo in Schema docs (#1259) 2020-08-24 17:20:33 +01:00
Daniel T. Plop
6918db1033
Fix Typo in Docs (#1252)
The example of executing a query by passing a root value had a typo for
the trailing parenthesis, namely it was '}' instead of ')'.
2020-08-12 14:44:00 -07:00
Jonathan Kim
188ce9a6cb
Fix subscribe with arguments (#1251) 2020-08-12 14:43:35 -07:00
Jonathan Kim
86b904d327
Split out the subscriptions documentation a separate file and fix it (#1245) 2020-08-07 14:37:32 -07:00
Jonathan Kim
29dd3f8391
v3.0.0b5 2020-08-06 17:19:02 +01:00
Syrus Akbary
d085c8852b
Subscription revamp (#1235)
* Integrate async tests into main code

* Added full support for subscriptions

* Fixed syntax using black

* Fixed typo
2020-07-28 13:33:21 -07:00
Redowan Delowar
2130005406
Minor grammatical fix in the schema docs (#1237) 2020-07-27 11:56:14 -07:00
Jonathan Kim
64af43748c
v3.0.0b4 2020-07-14 14:31:54 +01:00
Jonathan Kim
81fff0f1b5
Improve enum compatibility (#1153)
* Improve enum compatibility by supporting return enum as well as values and names

* Handle invalid enum values

* Rough implementation of compat middleware

* Move enum middleware into compat module

* Fix tests

* Tweak enum examples

* Add some tests for the middleware

* Clean up tests

* Add missing imports

* Remove enum compat middleware

* Use custom dedent function and pin graphql-core to >3.1.2
2020-07-13 15:40:57 -07:00
Jonathan Kim
d042d5e95a
Expose Base64 type and add custom scalar examples (#1223) 2020-07-09 17:55:27 +01:00
Eric Rodrigues Pires
c61f0f736a
Add Base64 scalar (#1221) 2020-07-02 10:52:44 -07:00
Jonathan Kim
5b2eb1109a
ObjectType meta arguments (#1219)
* Pass extra kwargs down the meta chain

* Rename name argument to allow custom name

* Reword error message

* Explicitly define kwargs

* Revert change to explicit kwargs

* name -> name_ for Enum __new__ function
2020-06-29 15:26:08 -07:00
Jonathan Kim
ecd11ccc1e
Revert 1213 update mutation docs (#1214)
* Revert "Update requirement for Query type in mutation docs (#1213)"

This reverts commit a9625dac0e.

* Add test to check that Query type must be defined
2020-06-29 07:53:53 -07:00
Jonathan Kim
324df19d3d
Set min version of graphql-core to v3.1.1 (#1215) 2020-06-28 21:54:36 -07:00
Jonathan Ehwald
bf034ca85f
Rename variables called type to type_ (#1216)
Co-authored-by: Daniel Gallagher <daniellg@yelp.com>
2020-06-27 11:18:11 +01:00
Jonathan Kim
05d96a9833 v3.0.0b3 2020-06-25 17:57:42 +01:00
Jonathan Kim
a9625dac0e
Update requirement for Query type in mutation docs (#1213) 2020-06-24 19:22:22 -07:00
Jonathan Kim
a1fc3688aa
Remove to_const function (#1212) 2020-06-24 19:21:40 -07:00
Jonathan Kim
4b70186031
Remove @staticmethod decorator in mutations doc (#1206) 2020-06-24 19:18:59 -07:00
Christoph Zwerschke
47c63f3dd7
Fix DateTime Scalar parse_literal methods (#1199) (#1200) 2020-06-04 21:30:23 -07:00
dbgb
966aba06cd
Fix typo in quickstart document (#1201) 2020-05-28 15:41:38 +02:00
Jonathan Kim
b0c8a17ec7
Fix issue with trailing whitespace (#1197) 2020-05-19 22:12:41 -07:00
Jonathan Kim
9b756bf12c
Delete CODEOWNERS 2020-05-09 13:32:52 +01:00
Jonathan Kim
d6acfc6eae
Create config.yml 2020-05-09 13:32:38 +01:00
Jonathan Kim
0723dd1d6c Update issue templates 2020-05-09 13:31:55 +01:00
Jonathan Kim
df67e69129
v3.0b2 2020-05-09 13:04:05 +01:00
Kevin Harvey
396b278aff
Fix typos (#1192) 2020-04-29 13:38:56 +01:00
Jonathan Kim
380166989d Update dataloader docs 2020-04-26 13:22:09 +01:00
Jonathan Kim
3e4305259b Add basic test for aiodataloader 2020-04-26 13:17:00 +01:00
Jonathan Kim
a0b522fa39 Add aiodataloader to test deps 2020-04-26 13:16:51 +01:00
Jonathan Kim
12ec8dc007
Don't exclude tests from distribution 2020-04-26 11:44:16 +01:00
Radosław Kowalski
133a831ab9
Update excluded packages list to properly exclude examples pack… (#1187)
* examples package will not be installed with graphene
2020-04-17 13:27:22 +01:00
Kimball Leavitt
7a1e9d7798
added graphene import to READMEs (#1183)
it's nice to just be able to copy/paste the entire example without
having to remember the import
2020-04-14 10:25:10 +01:00
Syrus Akbary
49fcf9f2e6
Allow fast ObjectType creation based on dataclasses (#1157)
* Allow fast ObjectType creation based on dataclasses

* Fixed Python 3.8 integration

* Added repr and eq methods to ObjectType containers

* Reformatted code

* Fixed mypy issue

* Removed unused __init__ for ObjectType containers

* Use black in dataclasses

* Use latest black verison on precommit
2020-04-12 17:45:46 -07:00
rrueth
37d6eaea46
Fix resolve method parameters bullet list (#1178)
The current documentation shows all of the resolve parameters on a single line as opposed to the bullet list that was intended.
2020-04-12 12:19:56 +01:00
sduthil
871c60cf46
Docs: integrations: fix FastAPI link (#1177) 2020-04-09 19:21:04 +01:00
Jonathan Kim
0051f82b5f
v3.0.0b1 2020-04-06 09:36:53 +01:00
Jonathan Kim
a2fe8dd704
Add note about the use of args (#1170)
* Add note about the use of `args`

Closes #1037

* Some improvements

* Link to correct place
2020-04-02 19:55:00 +01:00
Jonathan Kim
9fdab033a7
Add exempt labels 2020-04-01 16:24:23 +01:00
Jonathan Kim
cb3bfe011f
Use default_resolver to resolve values when using the source at… (#1155) 2020-03-16 16:20:04 +00:00
Jonathan Kim
6f2863ef6e
Add some more tests for Interface (#1154) 2020-03-16 16:19:44 +00:00
Jonathan Kim
00e36b52d5
Remove unused function (#1160) 2020-03-16 15:51:07 +00:00
Oleh Kuchuk
f9efe15973
Fixed examples, make root object explicit inside resolvers and… (#1159) 2020-03-15 18:52:56 +00:00
Syrus Akbary
60a9609b9a
Updated all str.format(…) to f-strings (#1158)
* Updated all str.format(…) to f-strings

This revamps the PR #984

* Pass black

* Fix flake8

* Updated objecttype

* Fix black version
2020-03-14 17:32:44 -07:00
Syrus Akbary
14183012a8
Remove subclass polyfill (#1156)
The subclass polyfill was only needed for Python 2.7-3.5

Python 3.6 introduced the __init_subclass__, so since Graphene now requires Python 3.6+, this is no longer needed.

https://www.python.org/dev/peps/pep-0487/
2020-03-14 20:19:28 +00:00
Rob Blackbourn
1cf303a27b
Added support for subscription (#1107)
* Added support for subscription

* Added pre-commit hooks for black and formatted changed files

* Checked with flake8

* Integrated changes from master.

Co-authored-by: Rob Blackbourn <rblackbourn@bhdgsystematic.com>
Co-authored-by: Rob Blackbourn <rtb@beast.jetblack.net>
2020-03-14 16:48:12 +00:00
Christoph Zwerschke
88f79b2850 Fix types in Schema docstring (#1100) 2020-03-04 15:26:09 +01:00
Christoph Zwerschke
5d97c848e0 Remove misleading comment
The comment originally referred to the __metaclass__ attribute which is gone now.
2020-03-04 12:44:53 +01:00
Christoph Zwerschke
5e6f68957e Use latest graphql-core 3.1.0b1 instead of 3.0.3
Adapt Schema, because there is no type reducer in core 3.1 any more.
2020-03-04 12:23:40 +01:00
Christoph Zwerschke
ffb7701466 Create another alpha release 2020-03-04 11:37:00 +01:00
Christoph Zwerschke
796880fc5c Update dependencies 2020-03-04 11:24:42 +01:00
Jonathan Kim
98e10f0db8
Replace INVALID with Undefined (#1146) 2020-02-27 20:51:59 +00:00
Jayden Windle
ac98be7836
Use Undefined instead of the now deprecated INVALID (#1143) 2020-02-26 21:18:13 +01:00
Lem Ko
ba5b7dd3d7
Fix example query in quickstart doc (#1139) 2020-02-21 11:15:51 +00:00
정유석
be97a369f7
fix typo in class 'Interface' (#1135) 2020-02-18 08:53:48 +00:00
David Sanders
03bd6984dd
fix example middleware class in docs (#1134) 2020-02-10 14:17:16 -08:00
James
23bb52a770
Add a helpful message to when a global_id fails to parse. (#1074)
* Add a helpful message to when a global_id fails to parse.

* Update test_node to have errors on test_node_query_incorrect_id

* Black the node.py file

* Remove func wrapper used in debugging get_resolver partial

* Update node.py

* Expand error messages

Co-authored-by: Jonathan Kim <jkimbo@gmail.com>
2020-02-10 14:16:11 -08:00
Jean-Louis Fuchs
ad0b3a529c
The default_value of InputField should be INVALID (#1111)
* The default_value of InputField should be INVALID

Since GraphQL 3.0 there is a distinction between None and INVALID (no value).
The tests captured the bug and are updated.

* Update minimum graphql-core version

* Use Undefined instead of INVALID

Co-authored-by: Jonathan Kim <jkimbo@gmail.com>
2020-02-08 20:24:58 +00:00
Henry Baldursson
9a19447213 Use unidecode to handle unicode characters in constant names (#1080) 2020-02-08 09:21:25 -08:00
Jonathan Kim
55a03ba716
Update readme (#1130)
* Add slack link and dev notice to the README

* Fix formatting

* Update formatting

* Add notice to documentation
2020-01-30 08:17:12 -08:00
Jonathan Kim
f82b811377
Fix example code (#1120) 2020-01-30 12:18:00 +00:00
Jonathan Kim
bd6d8d086d
Fix tests (#1119)
* Fix tests

* Add extra folders to make test command

* Update snapshots

* Add python 3.8 to test matrix

* Add black command to makefile and black dependency to setup.py

* Add lint command

* Run format

* Remove 3.8 from test matrix

* Add Python 3.8 to test matrix

* Update setup.py
2019-12-31 14:08:30 +00:00
Tom Paoletti
81d61f82c5 Fix objecttypes DefaultResolver example (#1087) (#1088)
* Create namedtuple as expected
* Access result.data instead of result['data']
* Refer to field with camel-case name
2019-12-26 20:05:14 +00:00
Jonathan Kim
482c7fcc65
Add file uploading docs (#1084) 2019-12-26 20:02:57 +00:00
Iman
c0fbcba97a Update quickstart.rst (#1090)
A miss letter
2019-12-26 20:02:28 +00:00
Yu Mochizuki
e31b93d1fd Increase the allowed version of aniso8601 (#1072) 2019-12-26 11:27:55 +00:00
TheMelter
abc2c2a784 Fix typo in execute.rst (#1115) 2019-12-19 23:02:45 -08:00
Jonathan Kim
3f6f426946
Update stale.yml 2019-10-18 10:50:54 +01:00
Theodore Diamantidis
7c7876d37c Propagate arguments of relay.NodeField to Field (#1036)
* Propagate name, deprecation_reason arguments of relay.NodeField to Field

* Allow custom description in Node.Field and move ID description to ID argument

* Add test for Node.Field with custom name

* Add tests for description, deprecation_reason arguments of NodeField

* Pass all kwargs from NodeField to Field
2019-09-27 09:54:46 +01:00
Jonathan Kim
a3b215d891
Remove AbstractType (#1053) 2019-09-27 09:54:19 +01:00
Min ho Kim
e90aa1b712 Fix typos (#1066)
Fixed typos in docs, string literals, comments, test name
2019-09-25 19:57:53 -04:00
Mel van Londen
8e7d76bbce
Graphene v3 following v3 graphql-core (#1048)
* v3.0 - remove Python 2.x from build (#983)

* Change travis to only compile for p3.6+

* Changed tox to only run Python 3.6+

* Changed library classifiers to reflect support in Python 3.6+

* Changed version to 3.0.0 development

In [15]: get_version((3, 0, 0, "alpha", 0))
Out[15]: '3.0.dev20190601212304'

* Reorganize Tests (#985)

We no longer need a dedicated folder for Python3.6+ tests
We no longer need to check six.PY3 in tests

* Upgrade black to 19.3b0 (#987)

* Remove six dependency (#986)

* No one is using func_name

* Remove six simple usages

* Remove six requirement

* Remove `six.with_metaclass` calls

* pytest-asyncio should be a regular dependency now with Py3 move

* Change dependency to graphql-core-next (#988)

* Changed dependencies to core-next

* Converted Scalars

* ResolveInfo name change

* Ignore .venv

* Make Schema compatible with GraphQL-core-next

* Ignore more venv names and mypy and pytest caches

* Remove print statements for debugging in schema test

* core-next now provides out_type and out_name

* Adapt date and time scalar types to core-next

* Ignore the non-standard result.invalid flag

* Results are named tuples in core-next (immutable)

* Enum values are returned as dict in core-next

* Fix mutation tests with promises

* Make all 345 tests pass with graphql-core-next

* Remove the compat module which was only needed for older Py version

* Remove object as base class (not needed in Py 3)

* We can assume that dicts are ordered in Py 3.6+

* Make use of the fact that dicts are iterable

* Use consistent style of importing from pytest

* Restore compatibility with graphql-relay-py v3

Add adpaters for the PageInfo and Connection args.

* Avoid various deprecation warnings

* Use graphql-core 3 instead of graphql-core-next

* Update dependencies, reformat changes with black

* Update graphene/relay/connection.py

Co-Authored-By: Jonathan Kim <jkimbo@gmail.com>

* Run black on setup.py

* Remove trailing whitespace
2019-08-17 17:07:53 -04:00
Jonathan Kim
3d0e460be1 v2.1.8 (#1054)
Update version to v2.1.8
2019-08-17 16:52:59 -04:00
Jonathan Kim
0808e8acb3
Add stalebot (#1044) 2019-07-29 12:02:11 +02:00
Ntale Shadik
c96bd680d7 Add interfaces meta argument on Mutations (#1023) 2019-07-24 18:44:22 +01:00
Vincent Prouillet
6e4058960d Increased allowed version of aniso8601 (#1009)
Closes #1008 

The only change should not affect graphene https://bitbucket.org/nielsenb/aniso8601/issues/24/float-induced-rounding-errors-when-parsing
2019-07-24 18:43:28 +01:00
Jonathan Kim
167c8c203c Bump version 2019-07-15 21:09:41 +01:00
Jonathan Kim
6fc1a8f79d
Upgrade graphql-relay (#1032)
* Upgrade graphql-relay

* Remove pypy3 test
2019-07-15 21:02:56 +01:00
Danilo Herrera
57157ab7f4 Fix typo (#1028) 2019-07-08 22:21:50 +01:00
Mel van Londen
4170e73251
Update roadmap (#1019)
* update roadmap

* Update ROADMAP.md graphql-core-next table entry

Co-Authored-By: Jonathan Kim <jkimbo@gmail.com>

* Update ROADMAP.md graphql-core table entry

Co-Authored-By: Jonathan Kim <jkimbo@gmail.com>

* Update ROADMAP.md remove SDL schema creation

* Update ROADMAP.md remove connections structure

* fix trailing whitespace
2019-06-24 20:38:45 -07:00
Jonathan Kim
8dee85cc13
Trying to get codeowners to work (#996)
* Trying to get codeowners to work

* Update CODEOWNERS
2019-06-24 16:01:45 +01:00
Jonathan Kim
abe547fb4d
Fix wrong variable name (#1015)
Resolves #1014
2019-06-22 18:09:52 +02:00
Jonathan Kim
0e8a3c5063
Update travis.yml (#1010)
* Use xenial dist

* Only install coveralls after success

* Latest version of pypi

* Refactor travis.yml into stages and add travis-tox

* Drop 2.6

* Bump to python 3.7
2019-06-19 18:25:21 +01:00
Mel van Londen
ac6714e8fa Remove sponsorship text from readme (#1002)
* remove sponsorship text from readme

* remove sponsorship references in roadmap
2019-06-14 12:56:02 +01:00
Jonathan Kim
89ca4f58a2
v2.1.6 (#1005) 2019-06-14 11:41:29 +01:00
Adrián López Calvo
431e93cd68 Fix malformed version on aniso8601 requirement (#995)
* Fix malformed version on aniso8601 requirement

* 6 -> 6.0

Co-Authored-By: Jonathan Kim <jkimbo@gmail.com>
2019-06-10 21:16:28 -07:00
David Anderson
5cb7d91aaa Revise documentation (#969)
* Revise documentation

- Add missing reference to `flask-graphql` in integrations
- align documentation for resolver arguments (use root for 1st argument
instead of self)
- explore use of `parent` instead of `root` for first argument
- clarify resolvers and object type documentation
- add documentation for Meta class options for ObjectType
- expand quickstart documentation for first time users
- streamline order of documentation for first time users (broad ->
specific)
- document resolver quirks

* explict imports from graphene

* rename doc refs for resolvers

* suggestions typos and graphene import
2019-06-09 16:49:56 -07:00
David Anderson
da1359ecca expose livehtml autobuild in Makefile + Add API autodoc (#971)
* expose livehtml autobuild in Makefile

* add API documentation for schema

* document graphene core API

* fixes black lint

* Update graphene/types/union.py

Co-Authored-By: Jonathan Kim <jkimbo@gmail.com>

* Update graphene/types/argument.py

Co-Authored-By: Jonathan Kim <jkimbo@gmail.com>

* Update graphene/types/field.py

Co-Authored-By: Jonathan Kim <jkimbo@gmail.com>

* Update graphene/types/inputfield.py

Co-Authored-By: Jonathan Kim <jkimbo@gmail.com>

* add note about non-functional `interfaces` meta argument in mutation

* update with other virtual environment configuration

* pin autobuild

* format argument example code

* format enum input object and interface examples

* format enum mutation union examples

* revise documentation with imports, capitalization
2019-06-09 15:36:06 -07:00
Ambro
40229b8a73 Fix internal link on docs (#980) 2019-06-04 09:23:24 -07:00
Eran Kampf
89a352e93a Pypi doesnt allow raw directive 2019-06-03 10:29:59 -07:00
Eran Kampf
17fbcb6746 v.2.1.5 2019-06-03 10:12:50 -07:00
Eran Kampf
f0cc9268d2 Fix RST README 2019-06-03 09:44:03 -07:00
Jessamyn Hodge
4e3f46b2c9
updating codeowners to have additional reviewers for docs and example changes (#989) 2019-06-02 18:16:25 -04:00
Eran Kampf
40534bcce9 v2.1.4 2019-06-01 13:44:13 -07:00
David Anderson
ec9f76cfae Fix flaky date tests (#982)
* fix flaky datetime test with fixed values

* rename fixtures for clarity in date+time tests
2019-06-01 13:41:45 -07:00
Eran Kampf
eb7966eca7
Fix for metaclasses that use type annotation (Issue #979) (#981)
* Replicate error with test

* Fix - ignore parameters we do not recognize

* Seperate Python3.6+ tests to their own folder

* lint

* Unused import

* Black formatting
2019-05-31 14:31:17 -07:00
Mike Bobadilla
1fcdeaac65 Typo in documentation (#975) 2019-05-28 10:09:57 -07:00
cclauss
f73055f72b Drop support for Python 3.4 because it is EOL (#963) 2019-05-07 12:16:22 -07:00
Scott Crunkleton
a5162e9ae3 Update aniso8601 dependency to allow versions 4+. (#935)
* Update aniso8601 dependency to allow versions 4+.

* Upper bound to aniso8601 dependency version.
2019-05-07 09:31:30 -07:00
NyanKiyoshi
6a4091b3e4 from_enum can now take a deprecation reason (#957) 2019-05-06 10:17:33 -07:00
Minh Tu Le
abff3d75a3 Allow the connection node to be wrapped in a NonNull type (#934) 2019-04-12 16:29:53 -07:00
Ganesh Pandey
daf0d17647 fix type on docs (#939) 2019-04-09 11:29:40 -07:00
Eran Kampf
d0cfee5641
Make UUID compatible (Fixed issue #936) (#937)
* Make UUID compatible

* Fix typo

* black code formatting
2019-04-08 14:06:32 -07:00
Eran Kampf
bcbb66c025
Line too long in code sample caused docs UI to overflow (#938) 2019-04-08 13:59:39 -07:00
Eran Kampf
582ac59bf7 Another docs test 2019-04-08 12:36:35 -07:00
Eran Kampf
28f6b18f55 Small formatting fix to test docs integration 2019-04-08 12:33:29 -07:00
Andreas Sodeur
7b6dae7fa3 [863] DateTime, Date, and Time now accept datetime.datetime, datetime.date,… (#864)
* DateTime, Date, and Time now accept datetime.datetime, datetime.date, and datetime.time resp. inputs when used as variables

* Added tests and improved resilience against bad DateTime, Date, and Time inputs.

* Fixed string type checks too narrow for py2.7

* fixed some of pre-commit's complaints
2019-03-31 11:57:34 +01:00
Jonathan Kim
c5b2281e22 Update execution doc with correct way to use variables (#920)
* Fix issue #890

* Change to `root`
2019-03-27 16:44:41 -07:00
Jonathan Kim
21cccf4c96 Update object type docs (#921)
Now that the default resolver handles both objects and dicts
2019-03-17 12:43:49 -07:00
Jonathan Kim
0805436d45
Docs on changing the name of an ObjectType (#922)
Fixes https://github.com/graphql-python/graphene/issues/839
2019-03-16 21:58:49 +00:00
Jonathan Kim
ca9188a615 Dict or attr default resolver (#638)
* Add dict_or_attr resolver and set it as default

* Add some tests

* Dry up code

* Updated formatting
2019-03-11 10:24:11 -07:00
Sebastián Ramírez
bbe11c9b5e Add FastAPI and Starlette to integrations (#906) 2019-03-11 10:03:54 -07:00
Markus Holtermann
96d497c2b8 Fix docs on graphene.Int bounds (#891) 2019-03-11 09:32:23 -07:00
Jonathan Kim
a4681ce6b2
Fixes middleware documentation (#916)
Closes #812
2019-03-10 16:21:08 +01:00
Harry Moreno
d95163c61d Add Docs link to readme (#914)
* change header of contributing docs to building docs
2019-03-09 17:35:15 -08:00
Dan
9ae2359b87 Run pre-commit autoupdate to bump versions of precommit hooks, then run them on all files (#913) 2019-03-08 11:09:45 -08:00
Eran Kampf
fd0bd0ccd7
Adding Myn to CODEOWNERS (projectcheshire) 2019-03-08 09:35:48 -08:00
Eran Kampf
359753dc0f
Merge pull request #912 from graphql-python/feature/add_codeowners_file
Added CODEOWNERS
2019-03-07 18:32:56 -08:00
Eran Kampf
806b99f59d Initial CODEOWNERS file as decided in our community meeting 2019-03-07 18:29:40 -08:00
Syrus Akbary
ae7a5d71c7
Merge pull request #873 from asodeur/issue_872
Special characters in README.rst break setup.py on Windows
2019-01-07 16:10:23 -05:00
Syrus Akbary
88d68fa672
Merge pull request #874 from kiendang/fix-doc
Fix anchor link
2019-01-07 16:10:01 -05:00
kiendang
ff4fb4f86a Fix anchor link 2018-12-28 10:53:29 +08:00
Syrus Akbary
e32cbc3346
Merge pull request #883 from graphql-python/update-object-type-docs
Update documentation on ObjectTypes
2018-12-27 14:53:09 +01:00
Jonathan Kim
d1b1ad733f Ignore flake8 error 2018-12-27 09:39:18 +00:00
Jonathan Kim
e1a2eb5a35 Remove trailing whitespace 2018-12-26 20:32:22 +00:00
Jonathan Kim
349add5700 Update documentation on ObjectTypes
Resolves #798
2018-12-26 20:22:34 +00:00
as
2bc7699a98 black formatting and removed trailing whitespace 2018-12-11 15:03:10 +01:00
as
2c43a2ae0a Now setup.py works on Windows other tools have trouble with PKG-INFO. Forcing long_description to ASCII. 2018-12-06 10:38:17 +01:00
as
3aafe58d4d Special characters in README.rst break setup.py on Windows 2018-11-29 09:29:00 +01:00
Syrus Akbary
08c86f3def
Merge pull request #845 from nive/master
quickstart example improvement
2018-11-05 20:26:18 +01:00
Syrus Akbary
37a6c01839
Merge pull request #855 from cherls/default-mutation-field-description
Default mutation field description
2018-11-05 20:25:56 +01:00
cherls
8e53672a1d Add mutation field default arg test 2018-11-05 13:30:09 -05:00
cherls
4d5a091d16 Add default Field description as docstring 2018-11-05 13:30:05 -05:00
Syrus Akbary
80066e55f3
Merge pull request #854 from johanzietsman-em/master
Fixed typos.
2018-11-01 19:10:03 +01:00
Johan Zietsman
587ce5ae93
Update interfaces.rst 2018-10-30 14:23:29 +02:00
Johan Zietsman
52f54f51b2
Update objecttypes.rst 2018-10-30 14:17:44 +02:00
Johan Zietsman
95cfff8c37
Update enums.rst 2018-10-30 14:06:37 +02:00
Syrus Akbary
e07e89d2e2 Improved README 2018-10-26 19:36:12 +02:00
Syrus Akbary
a0be081fc0 Improved README 2018-10-26 19:34:58 +02:00
Syrus Akbary
49a41060a6 Improved documentation showcasing sponsors 2018-10-26 19:30:37 +02:00
Syrus Akbary
7cfc3ffe67
Merge pull request #850 from MarSoft/patch-1
Fix typo in docs
2018-10-26 19:16:03 +02:00
Syrus Akbary
83a5587d71
Merge pull request #851 from etandel/feature/dataloader-docs
Improve dataloader doc
2018-10-26 19:15:44 +02:00
Syrus Akbary
89ba0da6d7
Merge pull request #847 from patrick91/fix/precommit-py-version
Don't enforce python 3.6 on precommit
2018-10-23 21:06:25 +02:00
Syrus Akbary
984e7cebe1
Merge pull request #849 from nerdoc/patch-1
fix deprecated "Input" class
2018-10-23 21:05:58 +02:00
Elias Tandel Barrionovo
21dbaa93c4 Improve dataloader doc
Explicitly mention that loaded values must have the same order as the
given keys
2018-10-23 10:05:47 -03:00
Семён Марьясин
ca02095806
Fix typo in docs 2018-10-22 16:19:55 +03:00
Christian González
4752ec08ab
fix deprecated "Input" class 2018-10-20 11:16:29 +02:00
Patrick Arminio
7e17b92a18
Don't enforce python 3.6 on precommit 2018-10-18 12:28:12 -07:00
adroullier
85e6c3d3fb quickstart example improvement, rename name parameter of String field to argument 2018-10-16 13:59:25 +02:00
Syrus Akbary
705cad76b2
Merge pull request #844 from dschaller/make-docs
add root make target for creating html version of docs
2018-10-15 14:46:49 +02:00
Syrus Akbary
8eb23ed802
Merge pull request #843 from dschaller/easier-make-docs
inline doctstring of make targets
2018-10-15 14:46:21 +02:00
Derek Schaller
a4c812f886 add root make target for creating html version of docs 2018-10-13 11:27:57 -07:00
Derek Schaller
3df3754ae6 inline doctstring of make targets 2018-10-13 11:21:07 -07:00
Syrus Akbary
131cbebc88
Merge pull request #832 from danpalmer/patch-6
Add basic type documentation for Relay fields
2018-09-09 20:11:36 +02:00
Dan Palmer
2a3d92682a
Add descriptions to the fields as well 2018-09-09 18:44:01 +01:00
Dan Palmer
b8ecc3929d
Add basic type documentation for Relay fields
This adds documentation in the API for `PageInfo`s and `Edges`.

This is useful to include in Graphene because `PageInfo` is always the same, and Edges always have the same format, so documentation for both can be created automatically.
2018-09-09 18:19:58 +01:00
Syrus Akbary
8d5843dc21 Added BACKERS.md 2018-09-09 18:10:07 +02:00
Syrus Akbary
8d4b9cdc77
Merge pull request #830 from danpalmer/patch-4
Expose documentation for Union types
2018-09-08 20:07:18 +02:00
Dan Palmer
00d370b228
Expose documentation for Union types 2018-09-08 17:20:39 +01:00
Syrus Akbary
e043527d5e
Merge pull request #827 from alxpy/py37
Add Python3.7 to CI
2018-09-04 16:25:32 +02:00
Alex Kuzmenko
325ab0d002 Add Python3.7 to CI 2018-09-04 17:17:28 +03:00
Syrus Akbary
e748c5f048
Update ROADMAP.md 2018-09-04 14:36:17 +02:00
Syrus Akbary
563ef221d4
Merge pull request #824 from graphql-python/feature/async-relay
Abstract thenables (promise, coroutine) out of relay
2018-08-31 20:51:48 +02:00
Syrus Akbary
9512528a77 Fixed async funcs 2018-08-31 20:16:12 +02:00
Syrus Akbary
3d41a500c9 Fixed lint & imports 2018-08-31 20:01:03 +02:00
Syrus Akbary
3e5319cf70 Abstract thenables (promise, coroutine) out of relay Connections and Mutations 2018-08-31 19:53:21 +02:00
Syrus Akbary
5777d85f99 Improved docs for testing 2018-08-31 17:58:51 +02:00
Syrus Akbary
bf3a4a88a4 Added ROADMAP to the Project 2018-08-31 17:49:29 +02:00
Syrus Akbary
c40ce98bb8
Merge pull request #821 from danpalmer/patch-3
Fix grammar in error message
2018-08-30 19:06:06 +02:00
Dan Palmer
baec6249e5
Fix assertion 2018-08-29 18:10:37 +01:00
Dan Palmer
07ec419578
Fix grammar 2018-08-29 17:31:46 +01:00
Syrus Akbary
727e09105f
Merge pull request #819 from adamchainz/deprecated_arguments
Change deprecated execute() arguments to new ones
2018-08-29 13:49:58 +02:00
Adam Johnson
4f2b278e12 black reformat 2018-08-29 13:07:45 +03:00
Adam Johnson
7dd8305bdf Change deprecated execute() arguments to new ones
Changed in https://github.com/graphql-python/graphql-core/pull/185 , the docs here were out of date, as were the tests.
2018-08-29 12:35:44 +03:00
Syrus Akbary
d728b84e48
Merge pull request #726 from picturedots/issue-703
Issue 703 -- support for Decimal type
2018-07-24 09:19:58 -07:00
Mark Chackerian
00cc97875d Merge branch 'master' into issue-703 2018-07-20 10:25:46 -04:00
Syrus Akbary
d28dc68abc Updated requirements to use graphql-core >= 2.1 2018-07-19 14:44:20 -07:00
Syrus Akbary
bfd6fd7c49
Merge pull request #802 from sebdiem/sdr/fix_black_formatting
fix black formatting
2018-07-19 14:38:07 -07:00
Sébastien Diemer
1eae96fd43 fix black formatting 2018-07-19 23:28:26 +02:00
Mark Chackerian
8ca7b855ac more flake8 fixes 2018-07-16 18:31:32 -04:00
Mark Chackerian
c076412ba5 automatically generated linting fixes 2018-07-16 18:20:04 -04:00
Mark Chackerian
fc3dbf0963 Merge branch 'master' into issue-703
# Conflicts:
#	graphene/__init__.py
#	graphene/types/__init__.py
2018-07-16 17:50:41 -04:00
Mark Chackerian
0fdc2ca3eb should fix some import issues with python 2.7 2018-07-16 17:20:49 -04:00
Syrus Akbary
4346832f71
Merge pull request #788 from sebdiem/sdr/subclass_mutations
Enable mutations subclassing
2018-07-09 19:06:15 -07:00
Syrus Akbary
319605bfaf
Merge branch 'master' into sdr/subclass_mutations 2018-07-09 18:49:07 -07:00
Syrus Akbary
43aec720a8
Merge pull request #793 from dan98765/add_black_formatter_precommit_hook
Add black formatter precommit hook
2018-07-06 17:40:40 -05:00
Daniel Gallagher
142f4a58d8 Run black formatter via pre-commit on all files 2018-07-06 14:03:15 -07:00
Daniel Gallagher
086f9dda99 Run black formatter via pre-commit on all files 2018-07-06 12:09:23 -07:00
Daniel Gallagher
71bcbb8566 Go with base black formatter for now 2018-07-06 12:06:16 -07:00
Daniel Gallagher
bf0d23b584 Add pyproject.toml to configure black formatter 2018-07-06 11:18:24 -07:00
Daniel Gallagher
04782a2818 Add black formatter pre-commit hook and remove isort (since black also sorts imports) 2018-07-06 11:10:41 -07:00
Syrus Akbary
ae7395f9da
Merge pull request #737 from dan98765/pre_commit_runs_as_part_of_continuous_integration
Update .travis.yml file to use tox as script for running tests
2018-07-06 12:50:31 -05:00
Sébastien Diemer
181e75c952 Fetch fields from parent classes in mutations
The goal of this commit is to be able to subclass mutations like this:

```
    class BaseMutation(graphene.Mutation):
        class Arguments:
            name = graphene.String()

        def mutate(self, info, **kwargs):
            # do something

    class ChildMutation(BaseMutation):
        class Arguments(BaseMutation.Arguments):
            other_arg = graphene.String()

        def mutate(self, info, **kwargs):
            # do other things
```

Note:
    vars(x).get(key, gettattr(x, key)) is used instead of the
    simpler gettatrr(x, key) for python2.7 compat.
    Indeed python2 and python3 lead to different results for

    class Foo(object):
        def bar(self):
            pass
    getattr(Foo, 'bar')

    # python 2.7 : > unbound method bar
    # python 3.x : > function Foo.bar
2018-07-02 10:03:39 +02:00
Sébastien Diemer
9f366e93c6 __wip__ add failed test
Just to ease review.
TODO: merge with next commit.
2018-07-02 10:03:39 +02:00
Syrus Akbary
2e41db8d95
Merge pull request #786 from jkimbo/deduplicator
Deduplicator
2018-07-01 18:10:36 -07:00
Syrus Akbary
fa5f5b0acb
Merge pull request #787 from jkimbo/crunch
Crunch response data
2018-07-01 18:09:28 -07:00
Jonathan Kim
1e40eceab3 Convert inputs to OrderedDicts 2018-07-01 21:19:19 +01:00
Jonathan Kim
9ce78e32a5 Remove utf-8 characters 2018-07-01 21:11:00 +01:00
Jonathan Kim
1f541e4467 Add crunch utility 2018-07-01 21:09:12 +01:00
Jonathan Kim
56000394c4 Simplify code 2018-07-01 11:32:16 +01:00
Jonathan Kim
cbcaac66d0 Add deduplicator utility 2018-07-01 11:29:45 +01:00
Syrus Akbary
1b746e6460
Merge pull request #779 from benmosher/patch-1
docs: mutation 'Output' example (closes #543)
2018-06-26 10:59:31 -07:00
Ben Mosher
708278e6dc
docs: mutation 'Output' example (closes #543) 2018-06-26 07:14:46 -04:00
Syrus Akbary
9efdf4c46e
Merge pull request #771 from jkimbo/update-interface-documentation
Update interface documentation
2018-06-18 13:33:59 -07:00
Syrus Akbary
9da46e8c99
Merge pull request #770 from boidolr/master
Update documentation
2018-06-18 13:33:34 -07:00
Jonathan Kim
cc54c76a3e Improve wording 2018-06-17 12:05:34 +01:00
Jonathan Kim
e7ebb86e5a Re-order type list 2018-06-17 11:25:32 +01:00
Jonathan Kim
a2db7c5dae Remove an unnecessary field 2018-06-17 11:24:59 +01:00
Jonathan Kim
3f6c3a7a99 Clean up doc and add resolve_type documentation 2018-06-17 11:23:08 +01:00
Jonathan Kim
43e87768d2 Update interface documentation 2018-06-16 15:10:32 +01:00
Raphael Boidol
5c4736e102 Update documentation
* resolver function arguments changed in `objecttypes.rst`
* small typo in `mutations.rst`
2018-06-16 13:11:33 +02:00
Syrus Akbary
c102458808
Merge pull request #768 from graphql-python/v2.1.2-1
Update to v2.1.2
2018-06-13 10:34:15 -07:00
Jonathan Kim
81419de5bf
Update to v2.1.2 2018-06-12 21:48:52 +01:00
Dan
33f2b303de Add python3.6 classifier to setup.py (#763) 2018-06-12 21:38:16 +01:00
Dan
b72dfa87a4 Update README Contributing section to encourage use of virtualenv (#765) 2018-06-12 21:38:06 +01:00
Daniel Gallagher
400a98de92 Add tox env for running mypy and add that to .travis.yml 2018-06-11 09:12:27 -07:00
Dan
12ee52a13a Add pyupgrade pre-commit hook and run on all files (#736) 2018-06-09 14:01:29 +01:00
Daniel Gallagher
c8fba61a05 Exclude README.md from trailing-whitespace hook 2018-06-08 22:19:26 -07:00
Daniel Gallagher
b5542d4426 Run pre-commit autoupdate 2018-06-08 22:13:45 -07:00
Daniel Gallagher
0f3d786402 Run pre-commit on all files 2018-06-08 22:08:58 -07:00
Daniel Gallagher
87cf3d4b80 Try installing mypy only when python version is 3.6 2018-06-08 22:00:01 -07:00
Daniel Gallagher
1d49df033c Explicitly run on py27 2018-06-08 21:56:08 -07:00
Daniel Gallagher
dbb72ba06b Update to match graphql-core 2018-06-08 21:54:24 -07:00
Daniel Gallagher
6116901ab6 Merge branch 'master' of github.com:dan98765/graphene into pre_commit_runs_as_part_of_continuous_integration 2018-06-08 21:52:34 -07:00
Dan
1b3e7f3b96 Add flake8 pre-commit hook and manually edit files to pass flake8 validation (#746)
Add flake8 pre-commit hook and manually edit files to pass flake8 validation
2018-06-05 21:47:07 +01:00
Syrus Akbary
8bf937d1ff
Merge pull request #754 from femesq/patch-2
Fix parameter order for Relay's Root-field
2018-06-01 18:16:42 -07:00
Syrus Akbary
8802ab3c28
Merge pull request #752 from jlowin/input-meta
Don't overwrite fields on InputObject - closes #720
2018-06-01 18:15:43 -07:00
Felipe Mesquita
2fbd2c1cb6
Fix parameter order for Relay's Root-field 2018-06-01 17:15:34 -03:00
Jeremiah Lowin
00ccc2056b Don't overwrite fields on InputObject - closes #720 2018-05-31 21:52:35 -04:00
Syrus Akbary
332214ba9c
Merge pull request #751 from nxtman123/issue#750
Resolve #750 by editing assert message
2018-05-30 18:08:17 -07:00
Kurtis Jantzen
d6a81ee7ff Update tests to reflect changes 2018-05-30 17:06:43 -06:00
Kurtis Jantzen
aa0c401cb5 Resolve #750 by editing assert message 2018-05-30 16:56:42 -06:00
Syrus Akbary
7bd77a0817
Merge pull request #748 from danpalmer/patch-2
Fix warning output
2018-05-30 09:35:03 -07:00
Dan Palmer
4e59cf3ea6
Fix warning output
Warning filtering is the responsibility of the application, not a library, and this current use causes all warnings from an application (at least those after this function is evaluated the first time) to print their contents.

This makes the library a better citizen in the Python ecosystem, and more closely matches what developers would expect.

(For what it's worth, we also can't start using this library without this patch because the logging is too verbose and may obscure more important warnings. We depend on being able to accurately control warning and logging output)
2018-05-30 14:50:22 +01:00
Dan
f13e54b4a4 Update contributing docs about using tox and sync tox pytest cmd with travis (#744)
* Update pytest command run by tox to match the command used by travis. Updated README contributing section with info about using tox to run tests.

* Uppercase 'Graphene'
2018-05-30 12:53:44 +01:00
Daniel Gallagher
05a362c9b5 Merge remote-tracking branch 'upstream/master' 2018-05-28 14:06:35 -07:00
Dan
a7168ffc6e Fix: Make tox stop failing (#741)
* Tox stopped working due to recent changes; add in necessary dependencies to tox.ini so it passes again

* Run pre-commit on all files

* Switch testenv deps to .[test] instead of an explicit list so the list of test deps in setup.py becomes the single source of truth for test deps.
2018-05-28 21:25:15 +01:00
Daniel Gallagher
a554b986af Merge remote-tracking branch 'upstream/master' 2018-05-28 12:26:02 -07:00
Dan
034b5385a5 Add isort precommit hook & run on all files (#743)
* Add isort and seed-isort-config pre-commit hook

* Fix erroneous isort moving comment to the top of file
2018-05-28 19:18:54 +01:00
Dan
4787cdc200
Merge pull request #1 from graphql-python/master
sync
2018-05-25 21:25:12 -07:00
Syrus Akbary
9f678fdfe8
Merge pull request #740 from jkimbo/release-2.1.1
Updated version to 2.1.1
2018-05-25 09:53:13 -07:00
Jonathan Kim
ec9526d0cd Updated version to 2.1.1 2018-05-25 17:17:20 +01:00
Nikordaris
034765aebe Added partial support to Dynamic type (#725) 2018-05-25 16:20:22 +01:00
Daniel Gallagher
013c9e5077 Update .travis.yml file to use tox as script for running tests 2018-05-24 23:33:32 -07:00
Syrus Akbary
28f6353212
Merge pull request #732 from femesq/patch-1
Migrations docs improvement
2018-05-24 19:20:33 -07:00
Syrus Akbary
4720f7743e
Merge pull request #730 from dan98765/add_pre_commit_tool_to_repo
Add pre-commit tool to repository, and run on all files
2018-05-24 19:18:10 -07:00
Felipe Mesquita
ead24a94c2
Update UPGRADE-v2.0.md
type correction
2018-05-23 20:20:55 -03:00
Felipe Mesquita
5d084cf2ea
Update UPGRADE-v2.0.md
Migration docs improvement
2018-05-23 20:13:03 -03:00
Daniel Gallagher
9777184721 Add pre-commit tool to repository, and run on all files 2018-05-22 10:56:58 -07:00
Mark Chackerian
de050fa6db fix linting error 2018-05-15 12:09:12 -04:00
Mark Chackerian
d40ef4be2b adds Decimal to types __all__ 2018-05-15 11:42:43 -04:00
Mark Chackerian
49258193ed adds decimal type and associated tests 2018-05-15 11:36:08 -04:00
Syrus Akbary
12d4dab774
Merge pull request #711 from devArtoria/master
add Graphene-Mongo to intergrations index of docs/index.rst
2018-04-19 20:12:27 -07:00
Lewis
7d998f0844
add Graphene-Mongo to intergrations list
The Graphene-Mongo integration is missing from the intergration index. So I added the Graphene-Mongo docs to the integration index.
2018-04-12 10:38:48 +09:00
Syrus Akbary
9408ba70d1 Fields default_value are no longer dismissed. Fixed #702. 2018-04-07 15:31:04 -07:00
Syrus Akbary
05d7e61b84 Updated version to 2.1.0 2018-03-29 22:38:51 -07:00
Syrus Akbary
d8aacc3b9e
Merge pull request #701 from graphql-python/feature/date-improvements
Datetime improvements
2018-03-29 22:35:28 -07:00
Syrus Akbary
50cd43c6e5 Added Date, DateTime and Time to global exports 2018-03-29 22:31:36 -07:00
Syrus Akbary
85e354c139 Merge branch 'master' into feature/date-improvements
# Conflicts:
#	graphene/types/datetime.py
2018-03-29 22:10:18 -07:00
Syrus Akbary
b4255e55fd Use aniso8601 instead of iso8601 2018-03-29 22:05:12 -07:00
Syrus Akbary
d46d8e8c33 Allow mutations to be required. Improved testing. Fixed #694 2018-03-21 21:31:56 -07:00
Syrus Akbary
562cafc14f Fixed str on abstract types. Improved repr 2018-03-21 21:21:03 -07:00
Syrus Akbary
415b71f730
Merge pull request #679 from anisjonischkeit/patch-1
Made DateTime types return GraphQLError on fail
2018-03-20 20:26:46 -07:00
Syrus Akbary
f4d1553d48
Merge pull request #692 from abawchen/fix-deprecations-url
Fix deprecations url
2018-03-20 20:24:06 -07:00
abawchen
a96ed550bd Fix deprecations url in DeprecationWarning message. 2018-03-20 08:14:59 +08:00
abawchen
dbc2ee4da9
Merge pull request #1 from graphql-python/master
Sync with original repo.
2018-03-20 07:30:53 +08:00
Syrus Akbary
2594cdb614 Fixed Meta included in Enum members. Fixed #685 2018-03-14 22:15:24 -07:00
Syrus Akbary
d6df14923d
Updated docs template 2018-03-13 22:37:36 -07:00
Anis Jonischkeit
9973fd314f added tests for when bad input is passed into date/datetime/time fields 2018-03-14 13:06:10 +10:00
Anis Jonischkeit
2a67ffeb35 fixed function name for test to be what it is actually testing and prevent name colision 2018-03-14 12:51:34 +10:00
Anis Jonischkeit
1a1efbd77d
linting: added two lines after end of class 2018-02-27 10:21:41 +10:00
Anis Jonischkeit
84fbf5dc23
Made DateTime types return GraphQLError on fail
This change makes it so that when an incorrectly formatted date string gets passed to a Date / Time argument a GraphQLError is returned rather than a GraphQLLocatedError. Since Date / Time are types, their errors should not be in the same class as errors in your application. This is also inline with how other types work in graphene (graphene.Int, graphene.Float)
2018-02-27 10:00:20 +10:00
Syrus Akbary
5df134e096
Merge pull request #675 from jkimbo/fix-type-in-example
Fix kwarg name in example. Fixes #533
2018-02-19 19:10:17 -08:00
Syrus Akbary
c5ce04f31e
Merge pull request #676 from jkimbo/exclude-examples-module
Exclude examples module in setup.py
2018-02-19 19:10:08 -08:00
Jonathan Kim
79f7cc08e3 Exclude examples module in setup.py
Fixes #608
2018-02-18 17:21:19 +00:00
Jonathan Kim
d3b708533d Fix kwarg name in example. Fixes #533 2018-02-17 23:28:33 +00:00
Syrus Akbary
8c7ca74c6f
Merge pull request #673 from jkimbo/relay-connection-required
Fix bug when setting a Relay ConnectionField to be required
2018-02-17 14:06:39 -08:00
Jonathan Kim
c25bcb3345 Move NonNull check 2018-02-17 21:50:40 +00:00
Jonathan Kim
38baf7ab52 Handle relay connection field being required 2018-02-17 20:18:55 +00:00
Jonathan Kim
42c96a453f Added failing test 2018-02-17 20:18:49 +00:00
Syrus Akbary
0971a05b33 Improved support / assertion for graphql types in Schema 2018-02-09 10:49:08 -08:00
Syrus Akbary
a7a4ba62af
Enabled possibility of setting name, description or deprecation_reason in mutation Fields
Fixed  #634, $660 #626 and #593
2018-02-08 23:56:13 -08:00
Syrus Akbary
da0b2c6805
Merge pull request #661 from pjdelport/fix-GitHub-link-tags
Fix broken GitHub link tags: 2.0 -> v2.0.0
2018-02-07 19:45:27 -08:00
Syrus Akbary
4e5a49789b
Merge pull request #665 from pjdelport/patch-1
Update .gitignore for pytest 3.4+
2018-02-07 19:44:57 -08:00
Syrus Akbary
113cf8da38
Merge pull request #666 from pjdelport/patch-2
Tox: Add py36 to default envlist
2018-02-07 19:44:43 -08:00
Pi Delport
265719d11f
Tox: Add py36 to default envlist 2018-02-08 01:32:10 +02:00
Pi Delport
f4b21c7a75
Update .gitignore for pytest 3.4+
Pytest 3.4.0 changes the default cache directory from `.cache` to `.pytest_cache`.

Changelog: https://docs.pytest.org/en/latest/changelog.html#pytest-3-4-0-2018-01-30
2018-02-08 01:31:12 +02:00
Pi Delport
368a2a02fe (Fit line length < 120) 2018-02-08 01:26:27 +02:00
Syrus Akbary
c044b2431b
Merge pull request #664 from jkimbo/document-nonnull-list
Add documentation on NonNull lists
2018-02-07 13:58:26 -07:00
Jonathan Kim
e26c0a3717 Add documentation on NonNull lists 2018-02-07 12:06:29 -08:00
Pi Delport
4fa0df401a Fix broken GitHub link tags: 2.0 -> v2.0.0 2018-02-05 16:49:16 +02:00
Syrus Akbary
e94716d94a
Merge pull request #653 from dkleissa/master
Update middleware example to support introspection
2018-01-24 19:25:53 -08:00
Dean Kleissas
035ff7ef88 Update middleware example to support introspection
In the `timing_middleware` example, introspection queries will fail due to `Schema` and others not having `_meta` attributes. API will work and tests pass but introspection will fail, which can be quite confusing to the developer. Simple update makes sure the `root` variable has a `_meta` attribute before accessing it.
2018-01-24 16:48:14 -05:00
Syrus Akbary
a0fc843513
Merge pull request #641 from presencelearning/issue-610-connection-name
Allow ObjectType to set Connection name
2018-01-20 15:37:46 -08:00
Syrus Akbary
8123c4ad8f
Fix relay links. 2018-01-20 15:17:52 -08:00
Syrus Akbary
be1f7d72a4
Merge pull request #644 from frsv/issue-643_fields_capitalizes_incorrectly
Change .title method to .capitalize in to_camel_case() in str_convertes.py
2018-01-11 11:43:10 -08:00
Roman Fursov
9dde259f54 Change .title method to .capitalize in to_camel_case in str_convertes.py 2018-01-10 17:25:15 +03:00
Brian Chapman
a2178dc220 Allow ObjectType to set Connection name 2018-01-08 09:16:02 -08:00
Syrus Akbary
38db32e4f2
Merge pull request #630 from dfee/425-extended
extended support for subclassing with meta to Enum and InputObjectType
2017-12-26 17:50:13 +00:00
Syrus Akbary
25dab925ee
Merge pull request #629 from simonwoerpel/master
Docs: fix typos in code-example for relay nodes
2017-12-23 10:17:31 -08:00
Syrus Akbary
94d5e345a1
Merge pull request #635 from jkimbo/error-missing-type
Raise better error if type is missing from schema
2017-12-23 10:17:06 -08:00
Jonathan Kim
d6968c2e92 Raise better error if type is missing from schema 2017-12-20 18:43:51 +00:00
Devin Fee
a16c5ba00b extended support for subclassing with meta to Enum and InputObjectType 2017-12-16 21:49:23 -08:00
Simon Wörpel
7afeaa052b
Docs: fix typos in example for relay nodes 2017-12-16 00:03:36 +01:00
Syrus Akbary
5036d164b7
Merge pull request #623 from jkimbo/scalar-documentation
[Docs] Scalar documentation
2017-12-10 12:54:54 -08:00
Jonathan Kim
0a6921d2d0 Add extra documentation on base scalars 2017-12-10 16:07:48 +01:00
Jonathan Kim
76f8a35916 Add documentation for scalar type arguments 2017-12-10 15:53:28 +01:00
Syrus Akbary
92f6a496c5
Merge pull request #622 from jkimbo/docs-middleware-update
[Docs] Functional middleware example
2017-12-09 17:41:48 -08:00
Jonathan Kim
502597c5a4 Format duration 2017-12-08 15:03:20 +00:00
Jonathan Kim
27745078bc Document functional middleware 2017-12-08 14:58:03 +00:00
Syrus Akbary
6c92e25ae8
Merge pull request #615 from Prince-Leto/patch-1
Added Date scalar to documentation
2017-12-01 12:14:15 -08:00
Grégoire Chauvet
98366c6add
Added Date scalar to documentation 2017-11-29 14:32:17 +01:00
Syrus Akbary
375d1f494b
Merge pull request #606 from g--/patch-1
Relay documentation reflects api changes in 2.0
2017-11-26 17:48:43 -08:00
Syrus Akbary
e19e229710
Merge pull request #611 from danpalmer/patch-1
Use more Pythonic terminology here
2017-11-25 15:27:08 -08:00
Dan Palmer
834d52f9d3
Use more Pythonic terminology here
"Hash" in the Python world implies a cryptographic hash, or possibly a checksum. Here, I believe "hash" is being used to mean "hash map", which would be more commonly known in Python as a dictionary, or dict for short.
2017-11-25 12:51:53 +00:00
Geoff
ec32c252e7
Relay documentation reflects api changes in 2.0
Specifically, get_node_from_global_id.
2017-11-20 10:05:28 -05:00
Syrus Akbary
e71a52140a
Update __init__.py 2017-11-14 23:25:30 -08:00
Syrus Akbary
eb5108f81e Simplified inputobjecttype implementation+tests 2017-11-14 22:19:22 -08:00
Syrus Akbary
6dd9e5ff1f Merge branch 'master' into input-fixes 2017-11-14 22:08:56 -08:00
Syrus Akbary
f6697188ee Added tests for nested inputobjecttypes 2017-11-14 22:06:28 -08:00
Syrus Akbary
0fc7280154
Merge pull request #602 from freundallein/docs-unions-fix
fix unions.rst
2017-11-14 09:12:26 -08:00
Stan Zhavoronkov
3412dba31f
fix Unions.rst
fix Unions.rst docs (copy-paste sentence from interfaces.rst).
2017-11-14 13:04:47 +03:00
Nathaniel Parrish
b2151d62da Code cleanup 2017-11-07 09:10:41 -08:00
Nathaniel Parrish
9c27db7ed5 Handle complex input types 2017-11-07 09:06:36 -08:00
Nathaniel Parrish
b5abccb1dc Set all fields on input object types 2017-11-03 16:36:36 -07:00
Syrus Akbary
71d5b1d943
Merge pull request #587 from ekampf/feature/enum_docs_update
Improved docs for Enums
2017-11-02 22:00:23 -07:00
Syrus Akbary
3fe12ca611
Merge pull request #588 from ekampf/feature/doc_fix_middleware
Fix resolve arguments section according to 2.0 resolvers
2017-11-02 22:00:06 -07:00
Syrus Akbary
3ee94131ae Improved object container initialization. Fixed #585 2017-11-02 21:57:10 -07:00
Syrus Akbary
045d5fffbe Added Date type in datetime 2017-11-02 21:17:06 -07:00
Syrus Akbary
43cda1d46a Added extra test to objecttypes 2017-11-02 21:07:10 -07:00
Eran Kampf
1555c988e0 Fix resolve arguments section according to 2.0 resolvers 2017-10-30 14:31:20 -07:00
Eran Kampf
7f59a8fa7e Added docs 2017-10-30 13:05:02 -07:00
Syrus Akbary
f79eb57c06
Merge pull request #586 from ekampf/feature/enum_value_descriptions
Support from_enum description lambda
2017-10-30 11:18:42 -07:00
Eran Kampf
90272e5297 Increase test coverage 2017-10-30 10:31:13 -07:00
Eran Kampf
e1f8480b32 lint fix 2017-10-30 10:26:42 -07:00
Eran Kampf
16c80c173e Support from_enum description lambda 2017-10-30 10:24:36 -07:00
Syrus Akbary
a8a6555537
Merge pull request #584 from vpoulailleau/patch-1
fix documentation to access to the request in Django
2017-10-30 09:36:58 -07:00
Vincent Poulailleau
98c2af3fcc
fix access to the request in Django
With the current documentation, I get after execution:
"graphql.error.located_error.GraphQLLocatedError: name 'context' is not defined"

I don't get any error after the correction I made (I haven't yet tried to upload a file…)
2017-10-30 16:00:18 +01:00
Syrus Akbary
36a902fbfa
Merge pull request #581 from lucascosta/patch-1
Prevent requirement breaking changes
2017-10-28 10:44:31 -07:00
Syrus Akbary
05518a7615
Merge pull request #582 from varundey/patch-1
Fixed minor grammatical error
2017-10-28 10:43:58 -07:00
Varun Dey
ef507c7932 Fixed minor grammatical error 2017-10-27 15:58:32 +05:30
Lucas Costa
71177fe977 Prevent requirement breaking changes
I have a project still in 1.2.0 thats has been broken in my last release since it used `'graphql-core>=1.0.1'` in the `install_requires`. Since `graphql-core` has released version 2.0 with breaking changes and there was no instruction to maintain version 1, it was included as a dependency. This prevents this situation for the future.
2017-10-26 16:21:19 -02:00
Syrus Akbary
34d03a7bd2 Updated docs to use stable version of Graphene 2.0 2017-10-26 00:22:59 -07:00
Syrus Akbary
d65e431619 Bump Graphene to 2.0 🎉 2017-10-25 10:40:58 -07:00
Syrus Akbary
e405cea361 Use stable versions of graphql-core 2017-10-25 10:31:28 -07:00
Syrus Akbary
afcad8a8f0 Commented pypy until is stable on travis 2017-10-25 10:27:50 -07:00
Syrus Akbary
fcef703eb5 Fixed flake8 issues 2017-10-25 10:27:50 -07:00
Syrus Akbary
c38ffa5ffd Update setup.py 2017-10-14 12:16:09 +02:00
Syrus Akbary
88111610fb Fixed pytest requirements 2017-10-14 12:11:18 +02:00
Syrus Akbary
3ce6031192 Fixed pypy tests 2017-10-09 12:01:37 +02:00
Syrus Akbary
7b08dbd2a1 Merge pull request #560 from nikolas/patch-1
quickstart: use print function in example query
2017-10-05 11:38:06 +02:00
Syrus Akbary
9c10649045 Merge pull request #561 from frankier/add-union-types-index
Add unions doc to index
2017-10-05 11:37:53 +02:00
Frankie Robertson
a1bc7377bd Add unions doc to index 2017-10-05 11:50:09 +03:00
Nik Nyby
37fbbf55fa quickstart: use print function in example query 2017-10-04 23:09:18 -04:00
Syrus Akbary
2cc701f444 Improved is_node checks 2017-09-06 17:51:09 -07:00
Syrus Akbary
bb6dc43d4b Merge pull request #539 from daxlab/patch-1
fix typo in Enum docs
2017-09-03 14:49:28 -07:00
Mandeep Singh
b34b3091d4 fix typo in Enum docs 2017-09-03 20:38:06 +05:30
Syrus Akbary
ab5a11b399 Fixed Argument comparison. Fixed #530 2017-08-29 20:21:17 -07:00
Syrus Akbary
f68682e153 Improved mutation examples 2017-08-29 19:53:24 -07:00
Syrus Akbary
7455063728 Fixed mutation docs (adding missing info arg in mutate) 2017-08-29 09:26:20 -07:00
Syrus Akbary
1d52148b37 Update quickstart.rst 2017-08-27 16:06:35 -07:00
Syrus Akbary
0b92d3dba6 Merge pull request #500 from graphql-python/2.0
[WIP] Road to 2.0
2017-08-27 13:27:12 -07:00
Syrus Akbary
7eb3ab5747 Fixed Flake8 issues 2017-08-07 20:59:48 -07:00
Syrus Akbary
4585469425 Fixed travis 2017-08-07 20:55:05 -07:00
Syrus Akbary
7cfec55410 Added mypy static checking 2017-08-07 20:48:26 -07:00
Syrus Akbary
19bf9b3713 Update UPGRADE-v2.0.md 2017-08-07 12:28:18 -07:00
Syrus Akbary
1e20e2bff3 Update UPGRADE-v2.0.md 2017-08-07 12:27:23 -07:00
Syrus Akbary
48754046b2 Update UPGRADE-v2.0.md 2017-08-07 12:24:17 -07:00
Syrus Akbary
066e7bab05 Merge pull request #517 from picturedots/patch-1
minor spelling and grammar changes UPGRADE-v2.0.md
2017-08-03 18:14:43 -07:00
picturedots
8826d72ada minor spelling and grammar changes UPGRADE-v2.0.md 2017-08-03 15:20:11 -04:00
Syrus Akbary
0a97deaa4d Improved relay coverage 2017-08-01 23:55:39 -07:00
Syrus Akbary
7e901f83ae Improved test coverage 2017-08-01 23:39:27 -07:00
Syrus Akbary
eff882e5a5 Fixed snapshot 2017-08-01 23:12:21 -07:00
Syrus Akbary
7f33fbe638 Fixed field source tests 2017-08-01 15:24:31 -07:00
Syrus Akbary
10a3e86cc5 Fixed mutation payload name and added tests for covering it. 2017-08-01 15:24:22 -07:00
Syrus Akbary
d8fac58701 Fixed source resolver and added tests for it 2017-08-01 13:59:18 -07:00
Syrus Akbary
81018268aa Added support for wheel distributions. Fixed #505 2017-07-31 22:30:13 -07:00
Syrus Akbary
a4bcb94958 Merge pull request #514 from graphql-python/2.0-newresolvers
Use the new resolvers API
2017-07-31 22:19:16 -07:00
Syrus Akbary
3a83671669 Merge branch '2.0' into 2.0-newresolvers 2017-07-31 22:19:11 -07:00
Syrus Akbary
7f1c2a654b Fixed benchmark tests 2017-07-31 22:15:26 -07:00
Syrus Akbary
602d8866bb Updated graphql-core version 2017-07-31 22:08:05 -07:00
Syrus Akbary
6a85507325 Improved get_node API 2017-07-27 20:06:48 -07:00
Syrus Akbary
0002d42e38 Updated docs 2017-07-27 03:05:58 -07:00
Syrus Akbary
394a1beb07 Updated resolver api 2017-07-27 02:55:25 -07:00
Syrus Akbary
d85a4e4874 Update UPGRADE-v2.0.md 2017-07-27 00:13:12 -07:00
Syrus Akbary
586ea56693 Update UPGRADE-v2.0.md 2017-07-27 00:10:50 -07:00
Syrus Akbary
6ae9e51320 Update UPGRADE-v2.0.md 2017-07-27 00:10:22 -07:00
Syrus Akbary
66390554d9 Improved resolver consistency 2017-07-26 23:14:32 -07:00
Syrus Akbary
e6b0cbb3bc Merge branch 'master' into 2.0 2017-07-26 22:46:26 -07:00
Syrus Akbary
a56d5d9f77 Added union docs. Fixed #493 2017-07-26 22:43:22 -07:00
Syrus Akbary
fa512cfa50 Improved mutation docs 2017-07-26 22:36:27 -07:00
Syrus Akbary
5c58d9e686 Fixed lazy type instant resolved with NonNull. Fixed #494 2017-07-26 22:32:37 -07:00
Syrus Akbary
f3bdd7de69 Improved upgrade example. Fixed #509 2017-07-26 20:12:15 -07:00
Syrus Akbary
719acc6771 Update UPGRADE-v2.0.md 2017-07-26 20:08:30 -07:00
Syrus Akbary
711a096ec6 Update UPGRADE-v2.0.md 2017-07-26 20:08:11 -07:00
Syrus Akbary
6dde81ee65 Improved Mutation warning 2017-07-26 19:44:17 -07:00
Syrus Akbary
c7c611266b Allow types to be abstract 2017-07-26 19:26:54 -07:00
Syrus Akbary
e71b59a8c3 Fixed flake8 issues in mutation 2017-07-24 23:15:56 -07:00
Syrus Akbary
5696c5af4f Added UUID docs 2017-07-24 23:15:47 -07:00
Syrus Akbary
eabb9b202c Added UUID type 2017-07-24 23:09:59 -07:00
Syrus Akbary
ed4bcce0cf Improved Relay Mutation 2017-07-24 23:09:52 -07:00
Syrus Akbary
7ca5c2225f Added create_type. Support for Meta as dict 2017-07-24 21:33:08 -07:00
Syrus Akbary
33a30db5f1 Improved quickstart docs 2017-07-24 00:01:44 -07:00
Syrus Akbary
dba6256578 Changed version to 2.0.dev 2017-07-23 23:43:08 -07:00
Syrus Akbary
df58b9a48b Remove _is_annotated flag from annotate decorator 2017-07-23 23:30:56 -07:00
Syrus Akbary
0e355ee296 Improved documentation examples 2017-07-23 23:29:27 -07:00
Syrus Akbary
9769612a44 Make resolvers simple again 2017-07-23 23:10:15 -07:00
Syrus Akbary
800fbdf820 Use dev version of graphql-core and promise 2017-07-23 21:47:23 -07:00
Syrus Akbary
40a15bdd21 Improved test coverage 2017-07-23 20:56:00 -07:00
Syrus Akbary
66468e3ea5 Improved Upgrade guide 2017-07-23 20:29:35 -07:00
Syrus Akbary
907a3d9915 Fixed Python 3.5 issues 2017-07-23 19:43:58 -07:00
Syrus Akbary
6c4f4624e3 Fixed Python 2/3 issues and flake syntax issues 2017-07-23 19:30:13 -07:00
Syrus Akbary
f8561fa5c4 Fixed travis 2017-07-23 19:20:19 -07:00
Syrus Akbary
8c3a933bd9 Fixed test 425 url 2017-07-23 19:18:29 -07:00
Syrus Akbary
287a7a814d Added package test requirements into travis excluding setup 2017-07-23 19:15:51 -07:00
Syrus Akbary
85d3145862 Improved abstract type 2017-07-23 19:13:33 -07:00
Syrus Akbary
8bac3dc9e5 Use latest graphql-core and promise lib 2017-07-23 19:02:41 -07:00
Syrus Akbary
6ae9717415 Improved automatic resolver args from annotations 2017-07-23 18:57:17 -07:00
Syrus Akbary
fb4b4df500 Added auto resolver function 2017-07-23 17:55:26 -07:00
Syrus Akbary
b185f4cae7 Improved mutation with thenable check 2017-07-23 17:36:20 -07:00
Syrus Akbary
b892eee0ae Removed unnecessary files 2017-07-23 17:22:12 -07:00
Syrus Akbary
d2f1024d81 Added annotated resolver and context 2017-07-23 17:19:45 -07:00
Syrus Akbary
f7fdc9aa3d Initial version of function annotations 2017-07-23 16:08:48 -07:00
Syrus Akbary
26686da30e Added inputobjecttype containers 2017-07-23 15:17:10 -07:00
Syrus Akbary
8ff3380291 Removed testing in Python 3.4 and start testing in 3.6 2017-07-12 21:58:05 -07:00
Syrus Akbary
ec5697b185 Fixed Connection side effects and add into breaking changes. 2017-07-12 21:53:35 -07:00
Syrus Akbary
f1624af08a Fixed Flake issues 2017-07-12 21:45:06 -07:00
Syrus Akbary
3fbc3281a2 Fixed Python 3.5 issues with Enum 2017-07-12 21:41:57 -07:00
Syrus Akbary
6321c52bd2 Fixed Connection tests 2017-07-12 21:21:16 -07:00
Syrus Akbary
557ec44a13 Merge pull request #502 from Thibaut-Fatus/patch-1
Doc was missing for using variables in queries
2017-07-12 10:36:54 -07:00
Thibaut Fatus
c155b7a56e Doc was missing for using variables in queries
added an example
2017-07-12 18:52:46 +02:00
Syrus Akbary
a023aeba62 Simplified Node type 2017-07-12 01:58:32 -07:00
Syrus Akbary
7a6d741531 Fixed relay ClientIDMutation 2017-07-12 01:36:44 -07:00
Syrus Akbary
4820a7fede Improved Mutation 2017-07-12 00:33:50 -07:00
Syrus Akbary
563ca23d00 Make init_subclass work in Python 3.5 2017-07-12 00:04:37 -07:00
Syrus Akbary
e38007868e Fixed upgrade doc title 2017-07-11 23:52:24 -07:00
Syrus Akbary
58b04dfcf5 Updated Readme docs 2017-07-11 23:10:02 -07:00
Syrus Akbary
28c987bdd1 Improved docs for extending Option attrs 2017-07-11 23:07:20 -07:00
Syrus Akbary
e86f73d30c Added class arguments example for Python 3 2017-07-11 22:52:58 -07:00
Syrus Akbary
9ce1288e12 Improved Enum implementation 2017-07-11 22:33:30 -07:00
Syrus Akbary
3604c8f172 Improved mutation class 2017-07-11 21:43:25 -07:00
Syrus Akbary
858343a8f6 Removed unused functions 2017-07-11 21:33:03 -07:00
Syrus Akbary
02c203f748 Simplified Union implementation 2017-07-11 21:31:38 -07:00
Syrus Akbary
b78b8c4134 Removed unused code 2017-07-11 21:27:20 -07:00
Syrus Akbary
f0edcb224a Removed AbstractType 2017-07-11 21:23:39 -07:00
Syrus Akbary
2d557d6ed7 Simplified mutation 2017-07-11 21:16:41 -07:00
Syrus Akbary
d8b42dd1ca Simplified InputObjectType 2017-07-11 20:59:44 -07:00
Syrus Akbary
5ee6e2b8e7 Simplified Scalar 2017-07-11 20:59:02 -07:00
Syrus Akbary
e487206818 Simplified ObjectType logic 2017-07-11 20:53:49 -07:00
Syrus Akbary
c98d91ba1c Simplified Interface code 2017-07-11 20:33:03 -07:00
Syrus Akbary
3e62fcf0cc Merge pull request #498 from XatMassacrE/master
Fix doc typo
2017-07-10 21:33:40 -07:00
XatMassacrE
dc5de1f503 Fix doc typo 2017-07-11 11:08:39 +08:00
XatMassacrE
93fc03d9e0 Fix doc typo 2017-07-11 10:27:36 +08:00
Syrus Akbary
f22504c2fc Improved Mutation with custom Field and output 2017-06-29 23:46:58 -07:00
Syrus Akbary
078230ad49 Updated version to 1.4.1 2017-06-27 22:00:56 -07:00
Syrus Akbary
985252920c Fixed argument output name. Fixed #490 2017-06-24 12:46:51 -07:00
Syrus Akbary
c41b183fad Fixed lint in graphene.test 2017-06-24 12:24:20 -07:00
Syrus Akbary
6c2b940a03 Improved ClientIDMutation tests for thenables 2017-06-24 12:23:56 -07:00
Syrus Akbary
645bfddfe9 Improved Test Framework to support promises as returned GraphQL execution 2017-06-16 11:09:36 -07:00
Syrus Akbary
fccc22b651 Merge pull request #467 from affablebloke/master
Added type consistency between Field and Argument
2017-06-05 19:44:34 -07:00
Syrus Akbary
8622989da9 Merge pull request #471 from vincentfretin/patch-1
Fix typo Grapehne -> Graphene
2017-05-23 21:49:47 -07:00
Syrus Akbary
ba04abfea8 Merge pull request #475 from pmlandwehr/patch-1
Include license in manifest for source bundles
2017-05-22 16:21:52 -07:00
Peter M. Landwehr
d7dff53f46 Include license in manifest for source bundles 2017-05-22 16:18:33 -07:00
Vincent Fretin
9d30136095 Fix typo Grapehne -> Graphene 2017-05-17 11:05:53 +02:00
Syrus Akbary
cb0adcb0fe Merge pull request #470 from ryanashcraft/patch-1
Fix typo in dataloader docs
2017-05-16 21:00:47 -07:00
Ryan Ashcraft
0a9dbb608a Fix typo in dataloader docs 2017-05-15 19:06:23 -07:00
Daniel Johnston
83857bfcfe Fixed typo. 2017-05-05 14:38:46 -07:00
Daniel Johnston
388253ede4 Added type consistency. 2017-05-05 14:27:46 -07:00
Daniel Johnston
7642644d82 Removed white space. 2017-05-05 14:23:51 -07:00
Daniel Johnston
59f4ddcd94 Removed white space. 2017-05-05 14:23:21 -07:00
Syrus Akbary
606575a2b0 Merge pull request #465 from ryanwilsonperkin/patch-1
Update UPGRADE-v1.0.md
2017-05-03 11:24:58 -07:00
Ryan Wilson-Perkin
b5d15b635e Update UPGRADE-v1.0.md
Hoping to help out by fixing a few small grammatical mistakes. Thanks for the upgrade guide!
2017-05-03 10:58:56 -04:00
Syrus Akbary
23e028fe72 Merge pull request #460 from yixizhang/typo
fix typo in docs/execution
2017-04-26 23:58:17 -07:00
Syrus Akbary
342b3a703a Merge pull request #459 from MikeTYChen/update-relay-doc-link
Update Relay Documentation Link
2017-04-26 23:58:05 -07:00
Yixi Zhang
bad6f5a188 fix typo in docs/execution 2017-04-26 16:27:44 -07:00
Michael Chen
5052536787 Update Relay Documentation Link
Update the link of Relay Specification to Facebook Relay Specifications
2017-04-26 11:45:36 -07:00
188 changed files with 11699 additions and 5204 deletions

View File

@ -11,4 +11,3 @@ trim_trailing_whitespace = true
[*.{py,rst,ini}]
indent_style = space
indent_size = 4

34
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@ -0,0 +1,34 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: "\U0001F41B bug"
assignees: ''
---
**Note: for support questions, please use stackoverflow**. This repository's issues are reserved for feature requests and bug reports.
* **What is the current behavior?**
* **If the current behavior is a bug, please provide the steps to reproduce and if possible a minimal demo of the problem** via
a github repo, https://repl.it or similar.
* **What is the expected behavior?**
* **What is the motivation / use case for changing the behavior?**
* **Please tell us about your environment:**
- Version:
- Platform:
* **Other information** (e.g. detailed explanation, stacktraces, related issues, suggestions how to fix, links for us to have context, eg. stackoverflow)

1
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@ -0,0 +1 @@
blank_issues_enabled: false

View File

@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: "✨ enhancement"
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

24
.github/stale.yml vendored Normal file
View File

@ -0,0 +1,24 @@
# Number of days of inactivity before an issue becomes stale
daysUntilStale: false
# Number of days of inactivity before a stale issue is closed
daysUntilClose: false
# Issues with these labels will never be considered stale
exemptLabels:
- pinned
- security
- 🐛 bug
- 📖 documentation
- 🙋 help wanted
- ✨ enhancement
- good first issue
- work in progress
# Label to use when marking an issue as stale
staleLabel: wontfix
# Comment to post when marking an issue as stale. Set to `false` to disable
markComment: false
# markComment: >
# This issue has been automatically marked as stale because it has not had
# recent activity. It will be closed if no further activity occurs. Thank you
# for your contributions.
# Comment to post when closing a stale issue. Set to `false` to disable
closeComment: false

21
.github/workflows/build.yaml vendored Normal file
View File

@ -0,0 +1,21 @@
name: 📦 Build
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python 3.10
uses: actions/setup-python@v5
with:
python-version: "3.10"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install build twine
- name: Building package
run: python3 -m build
- name: Check package with Twine
run: twine check dist/*

26
.github/workflows/deploy.yml vendored Normal file
View File

@ -0,0 +1,26 @@
name: 🚀 Deploy to PyPI
on:
push:
tags:
- 'v*'
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python 3.10
uses: actions/setup-python@v5
with:
python-version: "3.10"
- name: Build wheel and source tarball
run: |
pip install wheel
python setup.py sdist bdist_wheel
- name: Publish a Python distribution to PyPI
uses: pypa/gh-action-pypi-publish@v1.1.0
with:
user: __token__
password: ${{ secrets.pypi_password }}

26
.github/workflows/lint.yml vendored Normal file
View File

@ -0,0 +1,26 @@
name: 💅 Lint
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python 3.10
uses: actions/setup-python@v5
with:
python-version: "3.10"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install tox
- name: Run lint
run: tox
env:
TOXENV: pre-commit
- name: Run mypy
run: tox
env:
TOXENV: mypy

64
.github/workflows/tests.yml vendored Normal file
View File

@ -0,0 +1,64 @@
name: 📄 Tests
on:
push:
branches:
- master
- '*.x'
paths-ignore:
- 'docs/**'
- '*.md'
- '*.rst'
pull_request:
branches:
- master
- '*.x'
paths-ignore:
- 'docs/**'
- '*.md'
- '*.rst'
jobs:
tests:
# runs the test suite
name: ${{ matrix.name }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- {name: '3.13', python: '3.13', os: ubuntu-latest, tox: py313}
- {name: '3.12', python: '3.12', os: ubuntu-latest, tox: py312}
- {name: '3.11', python: '3.11', os: ubuntu-latest, tox: py311}
- {name: '3.10', python: '3.10', os: ubuntu-latest, tox: py310}
- {name: '3.9', python: '3.9', os: ubuntu-latest, tox: py39}
- {name: '3.8', python: '3.8', os: ubuntu-latest, tox: py38}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python }}
- name: update pip
run: |
python -m pip install --upgrade pip
pip install --upgrade setuptools wheel
- name: get pip cache dir
id: pip-cache
run: echo "dir=$(pip cache dir)" >> $GITHUB_OUTPUT
- name: cache pip dependencies
uses: actions/cache@v3
with:
path: ${{ steps.pip-cache.outputs.dir }}
key: pip|${{ runner.os }}|${{ matrix.python }}|${{ hashFiles('setup.py') }}
- run: pip install tox
- run: tox -e ${{ matrix.tox }}
- name: Upload coverage.xml
if: ${{ matrix.python == '3.10' }}
uses: actions/upload-artifact@v4
with:
name: graphene-coverage
path: coverage.xml
if-no-files-found: error
- name: Upload coverage.xml to codecov
if: ${{ matrix.python == '3.10' }}
uses: codecov/codecov-action@v4

15
.gitignore vendored
View File

@ -10,7 +10,6 @@ __pycache__/
# Distribution / packaging
.Python
env/
build/
develop-eggs/
dist/
@ -42,9 +41,11 @@ htmlcov/
.coverage
.coverage.*
.cache
.pytest_cache
nosetests.xml
coverage.xml
*,cover
*.cover
.pytest_cache/
# Translations
*.mo
@ -59,6 +60,14 @@ docs/_build/
# PyBuilder
target/
# VirtualEnv
.env
.venv
env/
venv/
# Typing
.mypy_cache/
/tests/django.sqlite
@ -80,3 +89,5 @@ target/
# Databases
*.sqlite3
.vscode
.mypy_cache
.ruff_cache

29
.pre-commit-config.yaml Normal file
View File

@ -0,0 +1,29 @@
default_language_version:
python: python3.10
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.3.0
hooks:
- id: check-merge-conflict
- id: check-json
- id: check-yaml
- id: debug-statements
- id: end-of-file-fixer
exclude: ^docs/.*$
- id: pretty-format-json
args:
- --autofix
- id: trailing-whitespace
exclude: README.md
- repo: https://github.com/asottile/pyupgrade
rev: v2.37.3
hooks:
- id: pyupgrade
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.5.0
hooks:
- id: ruff
- id: ruff-format
args: [ --check ]

View File

@ -1,60 +0,0 @@
language: python
sudo: false
python:
- 2.7
- 3.4
- 3.5
- pypy
before_install:
- |
if [ "$TRAVIS_PYTHON_VERSION" = "pypy" ]; then
export PYENV_ROOT="$HOME/.pyenv"
if [ -f "$PYENV_ROOT/bin/pyenv" ]; then
cd "$PYENV_ROOT" && git pull
else
rm -rf "$PYENV_ROOT" && git clone --depth 1 https://github.com/yyuu/pyenv.git "$PYENV_ROOT"
fi
export PYPY_VERSION="4.0.1"
"$PYENV_ROOT/bin/pyenv" install "pypy-$PYPY_VERSION"
virtualenv --python="$PYENV_ROOT/versions/pypy-$PYPY_VERSION/bin/python" "$HOME/virtualenvs/pypy-$PYPY_VERSION"
source "$HOME/virtualenvs/pypy-$PYPY_VERSION/bin/activate"
fi
install:
- |
if [ "$TEST_TYPE" = build ]; then
pip install -e .[test]
python setup.py develop
elif [ "$TEST_TYPE" = lint ]; then
pip install flake8
fi
script:
- |
if [ "$TEST_TYPE" = lint ]; then
echo "Checking Python code lint."
flake8 graphene
exit
elif [ "$TEST_TYPE" = build ]; then
py.test --cov=graphene graphene examples
fi
after_success:
- |
if [ "$TEST_TYPE" = build ]; then
coveralls
fi
env:
matrix:
- TEST_TYPE=build
global:
secure: SQC0eCWCWw8bZxbLE8vQn+UjJOp3Z1m779s9SMK3lCLwJxro/VCLBZ7hj4xsrq1MtcFO2U2Kqf068symw4Hr/0amYI3HFTCFiwXAC3PAKXeURca03eNO2heku+FtnQcOjBanExTsIBQRLDXMOaUkf3MIztpLJ4LHqMfUupKmw9YSB0v40jDbSN8khBnndFykmOnVVHznFp8USoN5F0CiPpnfEvHnJkaX76lNf7Kc9XNShBTTtJsnsHMhuYQeInt0vg9HSjoIYC38Tv2hmMj1myNdzyrHF+LgRjI6ceGi50ApAnGepXC/DNRhXROfECKez+LON/ZSqBGdJhUILqC8A4WmWmIjNcwitVFp3JGBqO7LULS0BI96EtSLe8rD1rkkdTbjivajkbykM1Q0Tnmg1adzGwLxRUbTq9tJQlTTkHBCuXIkpKb1mAtb/TY7A6BqfnPi2xTc/++qEawUG7ePhscdTj0IBrUfZsUNUYZqD8E8XbSWKIuS3SHE+cZ+s/kdAsm4q+FFAlpZKOYGxIkwvgyfu4/Plfol4b7X6iAP9J3r1Kv0DgBVFst5CXEwzZs19/g0CgokQbCXf1N+xeNnUELl6/fImaR3RKP22EaABoil4z8vzl4EqxqVoH1nfhE+WlpryXsuSaF/1R+WklR7aQ1FwoCk8V8HxM2zrj4tI8k=
matrix:
fast_finish: true
include:
- python: '2.7'
env: TEST_TYPE=lint
deploy:
provider: pypi
user: syrusakbary
on:
tags: true
password:
secure: LHOp9DvYR+70vj4YVY8+JRNCKUOfYZREEUY3+4lMUpY7Zy5QwDfgEMXG64ybREH9dFldpUqVXRj53eeU3spfudSfh8NHkgqW7qihez2AhSnRc4dK6ooNfB+kLcSoJ4nUFGxdYImABc4V1hJvflGaUkTwDNYVxJF938bPaO797IvSbuI86llwqkvuK2Vegv9q/fy9sVGaF9VZIs4JgXwR5AyDR7FBArl+S84vWww4vTFD33hoE88VR4QvFY3/71BwRtQrnCMm7AOm31P9u29yi3bpzQpiOR2rHsgrsYdm597QzFKVxYwsmf9uAx2bpbSPy2WibunLePIvOFwm8xcfwnz4/J4ONBc5PSFmUytTWpzEnxb0bfUNLuYloIS24V6OZ8BfAhiYZ1AwySeJCQDM4Vk1V8IF6trTtyx5EW/uV9jsHCZ3LFsAD7UnFRTosIgN3SAK3ZWCEk5oF2IvjecsolEfkRXB3q9EjMkkuXRUeFDH2lWJLgNE27BzY6myvZVzPmfwZUsPBlPD/6w+WLSp97Rjgr9zS3T1d4ddqFM4ZYu04f2i7a/UUQqG+itzzuX5DWLPvzuNt37JB45mB9IsvxPyXZ6SkAcLl48NGyKok1f3vQnvphkfkl4lni29woKhaau8xlsuEDrcwOoeAsVcZXiItg+l+z2SlIwM0A06EvQ=

View File

@ -2,3 +2,4 @@ global-exclude tests/*
recursive-exclude tests *
recursive-exclude tests_py35 *
recursive-exclude examples *
include LICENSE

28
Makefile Normal file
View File

@ -0,0 +1,28 @@
.PHONY: help
help:
@echo "Please use \`make <target>' where <target> is one of"
@grep -E '^\.PHONY: [a-zA-Z_-]+ .*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = "(: |##)"}; {printf "\033[36m%-30s\033[0m %s\n", $$2, $$3}'
.PHONY: install-dev ## Install development dependencies
install-dev:
pip install -e ".[dev]"
.PHONY: test ## Run tests
test:
py.test graphene examples
.PHONY: docs ## Generate docs
docs: install-dev
cd docs && make install && make html
.PHONY: docs-live ## Generate docs with live reloading
docs-live: install-dev
cd docs && make install && make livehtml
.PHONY: format
format:
black graphene examples setup.py
.PHONY: lint
lint:
flake8 graphene examples setup.py

View File

@ -1,54 +1,51 @@
Please read [UPGRADE-v1.0.md](/UPGRADE-v1.0.md) to learn how to upgrade to Graphene `1.0`.
# ![Graphene Logo](http://graphene-python.org/favicon.png) [Graphene](http://graphene-python.org) [![PyPI version](https://badge.fury.io/py/graphene.svg)](https://badge.fury.io/py/graphene) [![Coverage Status](https://coveralls.io/repos/graphql-python/graphene/badge.svg?branch=master&service=github)](https://coveralls.io/github/graphql-python/graphene?branch=master) [![](https://dcbadge.vercel.app/api/server/T6Gp6NFYHe?style=flat)](https://discord.gg/T6Gp6NFYHe)
---
[💬 Join the community on Discord](https://discord.gg/T6Gp6NFYHe)
# ![Graphene Logo](http://graphene-python.org/favicon.png) [Graphene](http://graphene-python.org) [![Build Status](https://travis-ci.org/graphql-python/graphene.svg?branch=master)](https://travis-ci.org/graphql-python/graphene) [![PyPI version](https://badge.fury.io/py/graphene.svg)](https://badge.fury.io/py/graphene) [![Coverage Status](https://coveralls.io/repos/graphql-python/graphene/badge.svg?branch=master&service=github)](https://coveralls.io/github/graphql-python/graphene?branch=master)
**We are looking for contributors**! Please check the current issues to see how you can help ❤️
## Introduction
[Graphene](http://graphene-python.org) is a Python library for building GraphQL schemas/types fast and easily.
[Graphene](http://graphene-python.org) is an opinionated Python library for building GraphQL schemas/types fast and easily.
- **Easy to use:** Graphene helps you use GraphQL in Python without effort.
- **Relay:** Graphene has builtin support for Relay.
- **Data agnostic:** Graphene supports any kind of data source: SQL (Django, SQLAlchemy), NoSQL, custom Python objects, etc.
- **Data agnostic:** Graphene supports any kind of data source: SQL (Django, SQLAlchemy), Mongo, custom Python objects, etc.
We believe that by providing a complete API you could plug Graphene anywhere your data lives and make your data available
through GraphQL.
## Integrations
Graphene has multiple integrations with different frameworks:
| integration | Package |
|---------------|-------------------|
| Django | [graphene-django](https://github.com/graphql-python/graphene-django/) |
| ----------------- | --------------------------------------------------------------------------------------- |
| SQLAlchemy | [graphene-sqlalchemy](https://github.com/graphql-python/graphene-sqlalchemy/) |
| Google App Engine | [graphene-gae](https://github.com/graphql-python/graphene-gae/) |
| Peewee | *In progress* ([Tracking Issue](https://github.com/graphql-python/graphene/issues/289)) |
| Mongo | [graphene-mongo](https://github.com/graphql-python/graphene-mongo/) |
| Apollo Federation | [graphene-federation](https://github.com/graphql-python/graphene-federation/) |
| Django | [graphene-django](https://github.com/graphql-python/graphene-django/) |
Also, Graphene is fully compatible with the GraphQL spec, working seamlessly with all GraphQL clients, such as [Relay](https://github.com/facebook/relay), [Apollo](https://github.com/apollographql/apollo-client) and [gql](https://github.com/graphql-python/gql).
## Installation
For instaling graphene, just run this command in your shell
To install `graphene`, just run this command in your shell
```bash
pip install "graphene>=1.0"
pip install "graphene>=3.1"
```
## 1.0 Upgrade Guide
Please read [UPGRADE-v1.0.md](/UPGRADE-v1.0.md) to learn how to upgrade.
## Examples
Here is one example for you to get started:
```python
import graphene
class Query(graphene.ObjectType):
hello = graphene.String(description='A typical hello world')
def resolve_hello(self, args, context, info):
def resolve_hello(self, info):
return 'World'
schema = graphene.Schema(query=Query)
@ -67,44 +64,67 @@ result = schema.execute(query)
If you want to learn even more, you can also check the following [examples](examples/):
* **Basic Schema**: [Starwars example](examples/starwars)
* **Relay Schema**: [Starwars Relay example](examples/starwars_relay)
- **Basic Schema**: [Starwars example](examples/starwars)
- **Relay Schema**: [Starwars Relay example](examples/starwars_relay)
## Documentation
Documentation and links to additional resources are available at
https://docs.graphene-python.org/en/latest/
## Contributing
After cloning this repo, ensure dependencies are installed by running:
After cloning this repo, create a [virtualenv](https://virtualenv.pypa.io/en/stable/) and ensure dependencies are installed by running:
```sh
virtualenv venv
source venv/bin/activate
pip install -e ".[test]"
```
After developing, the full test suite can be evaluated by running:
Well-written tests and maintaining good test coverage is important to this project. While developing, run new and existing tests with:
```sh
py.test graphene --cov=graphene --benchmark-skip # Use -v -s for verbose mode
pytest graphene/relay/tests/test_node.py # Single file
pytest graphene/relay # All tests in directory
```
Add the `-s` flag if you have introduced breakpoints into the code for debugging.
Add the `-v` ("verbose") flag to get more detailed test output. For even more detailed output, use `-vv`.
Check out the [pytest documentation](https://docs.pytest.org/en/latest/) for more options and test running controls.
Regularly ensure your `pre-commit` hooks are up to date and enabled:
```sh
pre-commit install
```
You can also run the benchmarks with:
```sh
py.test graphene --benchmark-only
pytest graphene --benchmark-only
```
Graphene supports several versions of Python. To make sure that changes do not break compatibility with any of those versions, we use `tox` to create virtualenvs for each Python version and run tests with that version. To run against all Python versions defined in the `tox.ini` config file, just run:
### Documentation
```sh
tox
```
If you wish to run against a specific version defined in the `tox.ini` file:
```sh
tox -e py39
```
Tox can only use whatever versions of Python are installed on your system. When you create a pull request, GitHub Actions pipelines will also be running the same tests and report the results, so there is no need for potential contributors to try to install every single version of Python on their own system ahead of time. We appreciate opening issues and pull requests to make graphene even more stable & useful!
### Building Documentation
The documentation is generated using the excellent [Sphinx](http://www.sphinx-doc.org/) and a custom theme.
The documentation dependencies are installed by running:
An HTML version of the documentation is produced by running:
```sh
cd docs
pip install -r requirements.txt
```
Then to produce a HTML version of the documentation:
```sh
make html
make docs
```

View File

@ -1,137 +0,0 @@
Please read `UPGRADE-v1.0.md </UPGRADE-v1.0.md>`__ to learn how to
upgrade to Graphene ``1.0``.
--------------
|Graphene Logo| `Graphene <http://graphene-python.org>`__ |Build Status| |PyPI version| |Coverage Status|
=========================================================================================================
`Graphene <http://graphene-python.org>`__ is a Python library for
building GraphQL schemas/types fast and easily.
- **Easy to use:** Graphene helps you use GraphQL in Python without
effort.
- **Relay:** Graphene has builtin support for both Relay.
- **Data agnostic:** Graphene supports any kind of data source: SQL
(Django, SQLAlchemy), NoSQL, custom Python objects, etc. We believe
that by providing a complete API you could plug Graphene anywhere
your data lives and make your data available through GraphQL.
Integrations
------------
Graphene has multiple integrations with different frameworks:
+---------------------+----------------------------------------------------------------------------------------------+
| integration | Package |
+=====================+==============================================================================================+
| Django | `graphene-django <https://github.com/graphql-python/graphene-django/>`__ |
+---------------------+----------------------------------------------------------------------------------------------+
| SQLAlchemy | `graphene-sqlalchemy <https://github.com/graphql-python/graphene-sqlalchemy/>`__ |
+---------------------+----------------------------------------------------------------------------------------------+
| Google App Engine | `graphene-gae <https://github.com/graphql-python/graphene-gae/>`__ |
+---------------------+----------------------------------------------------------------------------------------------+
| Peewee | *In progress* (`Tracking Issue <https://github.com/graphql-python/graphene/issues/289>`__) |
+---------------------+----------------------------------------------------------------------------------------------+
Also, Graphene is fully compatible with the GraphQL spec, working
seamlessly with all GraphQL clients, such as
`Relay <https://github.com/facebook/relay>`__,
`Apollo <https://github.com/apollographql/apollo-client>`__ and
`gql <https://github.com/graphql-python/gql>`__.
Installation
------------
For instaling graphene, just run this command in your shell
.. code:: bash
pip install "graphene>=1.0"
1.0 Upgrade Guide
-----------------
Please read `UPGRADE-v1.0.md </UPGRADE-v1.0.md>`__ to learn how to
upgrade.
Examples
--------
Here is one example for you to get started:
.. code:: python
class Query(graphene.ObjectType):
hello = graphene.String(description='A typical hello world')
def resolve_hello(self, args, context, info):
return 'World'
schema = graphene.Schema(query=Query)
Then Querying ``graphene.Schema`` is as simple as:
.. code:: python
query = '''
query SayHello {
hello
}
'''
result = schema.execute(query)
If you want to learn even more, you can also check the following
`examples <examples/>`__:
- **Basic Schema**: `Starwars example <examples/starwars>`__
- **Relay Schema**: `Starwars Relay
example <examples/starwars_relay>`__
Contributing
------------
After cloning this repo, ensure dependencies are installed by running:
.. code:: sh
pip install -e ".[test]"
After developing, the full test suite can be evaluated by running:
.. code:: sh
py.test graphene --cov=graphene --benchmark-skip # Use -v -s for verbose mode
You can also run the benchmarks with:
.. code:: sh
py.test graphene --benchmark-only
Documentation
~~~~~~~~~~~~~
The documentation is generated using the excellent
`Sphinx <http://www.sphinx-doc.org/>`__ and a custom theme.
The documentation dependencies are installed by running:
.. code:: sh
cd docs
pip install -r requirements.txt
Then to produce a HTML version of the documentation:
.. code:: sh
make html
.. |Graphene Logo| image:: http://graphene-python.org/favicon.png
.. |Build Status| image:: https://travis-ci.org/graphql-python/graphene.svg?branch=master
:target: https://travis-ci.org/graphql-python/graphene
.. |PyPI version| image:: https://badge.fury.io/py/graphene.svg
:target: https://badge.fury.io/py/graphene
.. |Coverage Status| image:: https://coveralls.io/repos/graphql-python/graphene/badge.svg?branch=master&service=github
:target: https://coveralls.io/github/graphql-python/graphene?branch=master

15
SECURITY.md Normal file
View File

@ -0,0 +1,15 @@
# Security Policy
## Supported Versions
Support for security issues is currently provided for Graphene 3.0 and above. Support on earlier versions cannot be guaranteed by the maintainers of this library, but community PRs may be accepted in critical cases.
The preferred mitigation strategy is via an upgrade to Graphene 3.
| Version | Supported |
| ------- | ------------------ |
| 3.x | :white_check_mark: |
| <3.x | :x: |
## Reporting a Vulnerability
Please use responsible disclosure by contacting a core maintainer via Discord or E-Mail.

View File

@ -2,30 +2,29 @@
Big changes from v0.10.x to 1.0. While on the surface a lot of this just looks like shuffling around API, the entire codebase has been rewritten to handle some really great use cases and improved performance.
## Backwards Compatibility and Deprecation Warnings
This has been a community project from the start, we need your help making the upgrade as smooth as possible for everybody!
We have done our best to provide backwards compatibility with deprecated APIs.
## Deprecations
* `with_context` is no longer needed. Resolvers now always take the context argument.
- `with_context` is no longer needed. Resolvers now always take the context argument.
Before:
```python
def resolve_xxx(self, args, info):
def resolve_xxx(root, args, info):
# ...
```
With 1.0:
```python
def resolve_xxx(self, args, context, info):
def resolve_xxx(root, args, context, info):
# ...
```
* `ObjectType` and `Interface` no longer accept the `abstract` option in the `Meta`.
- `ObjectType` and `Interface` no longer accept the `abstract` option in the `Meta`.
Inheriting fields should be now achieved using `AbstractType` inheritance.
Before:
@ -42,6 +41,7 @@ We have done our best to provide backwards compatibility with deprecated APIs.
```
With 1.0:
```python
class MyBaseQuery(graphene.AbstractType):
my_field = String()
@ -50,9 +50,9 @@ We have done our best to provide backwards compatibility with deprecated APIs.
pass
```
* The `type_name` option in the Meta in types is now `name`
- The `type_name` option in the Meta in types is now `name`
* Type references no longer work with strings, but with functions.
- Type references no longer work with strings, but with functions.
Before:
@ -70,7 +70,6 @@ We have done our best to provide backwards compatibility with deprecated APIs.
users = graphene.List(lambda: User)
```
## Schema
Schemas in graphene `1.0` are `Immutable`, that means that once you create a `graphene.Schema` any
@ -80,7 +79,6 @@ The `name` argument is removed from the Schema.
The arguments `executor` and `middlewares` are also removed from the `Schema` definition.
You can still use them, but by calling explicitly in the `execute` method in `graphql`.
```python
# Old way
schema = graphene.Schema(name='My Schema')
@ -94,10 +92,9 @@ schema = graphene.Schema(
)
```
## Interfaces
For implementing an Interface in a ObjectType, you have to it onto `Meta.interfaces`.
For implementing an Interface in an ObjectType, you have to add it onto `Meta.interfaces`.
Like:
@ -131,7 +128,7 @@ class ReverseString(Mutation):
reversed = String()
def mutate(self, args, context, info):
def mutate(root, args, context, info):
reversed = args.get('input')[::-1]
return ReverseString(reversed=reversed)
@ -142,7 +139,7 @@ class Query(ObjectType):
## Nodes
Apart of implementing as showed in the previous section, for use the node field you have to
Apart from implementing as shown in the previous section, to use the node field you have to
specify the node Type.
Example:
@ -155,17 +152,16 @@ class Query(ObjectType):
node = relay.Node.Field() # New way
```
Also, if wanted to create an `ObjectType` that implements `Node`, you have to do it
explicity.
Also, if you wanted to create an `ObjectType` that implements `Node`, you have to do it
explicitly.
## Django
The Django integration with Graphene now have an independent package: `graphene-django`.
The Django integration with Graphene now has an independent package: `graphene-django`.
For installing, you have to replace the old `graphene[django]` with `graphene-django`.
* As the package is now independent, you have to import now from `graphene_django`.
* **DjangoNode no longer exists**, please use `relay.Node` instead:
- As the package is now independent, you now have to import from `graphene_django`.
- **DjangoNode no longer exists**, please use `relay.Node` instead:
```python
from graphene.relay import Node
@ -178,11 +174,11 @@ For installing, you have to replace the old `graphene[django]` with `graphene-dj
## SQLAlchemy
The SQLAlchemy integration with Graphene now have an independent package: `graphene-sqlalchemy`.
The SQLAlchemy integration with Graphene now has an independent package: `graphene-sqlalchemy`.
For installing, you have to replace the old `graphene[sqlalchemy]` with `graphene-sqlalchemy`.
* As the package is now independent, you have to import now from `graphene_sqlalchemy`.
* **SQLAlchemyNode no longer exists**, please use `relay.Node` instead:
- As the package is now independent, you have to import now from `graphene_sqlalchemy`.
- **SQLAlchemyNode no longer exists**, please use `relay.Node` instead:
```python
from graphene.relay import Node

385
UPGRADE-v2.0.md Normal file
View File

@ -0,0 +1,385 @@
# v2.0 Upgrade Guide
`ObjectType`, `Interface`, `InputObjectType`, `Scalar` and `Enum` implementations
have been quite simplified, without the need to define a explicit Metaclass for each subtype.
It also improves the field resolvers, [simplifying the code](#simpler-resolvers) the
developer has to write to use them.
**Deprecations:**
- [`AbstractType`](#abstracttype-deprecated)
- [`resolve_only_args`](#resolve_only_args)
- [`Mutation.Input`](#mutationinput)
**Breaking changes:**
- [`Simpler Resolvers`](#simpler-resolvers)
- [`Node Connections`](#node-connections)
**New Features!**
- [`InputObjectType`](#inputobjecttype)
- [`Meta as Class arguments`](#meta-as-class-arguments) (_only available for Python 3_)
> The type metaclasses are now deleted as they are no longer necessary. If your code was depending
> on this strategy for creating custom attrs, see an [example on how to do it in 2.0](https://github.com/graphql-python/graphene/blob/v2.0.0/graphene/tests/issues/test_425.py).
## Deprecations
### AbstractType deprecated
AbstractType is deprecated in graphene 2.0, you can now use normal inheritance instead.
Before:
```python
class CommonFields(AbstractType):
name = String()
class Pet(CommonFields, Interface):
pass
```
With 2.0:
```python
class CommonFields(object):
name = String()
class Pet(CommonFields, Interface):
pass
```
### resolve_only_args
`resolve_only_args` is now deprecated as the resolver API has been simplified.
Before:
```python
class User(ObjectType):
name = String()
@resolve_only_args
def resolve_name(root):
return root.name
```
With 2.0:
```python
class User(ObjectType):
name = String()
def resolve_name(root, info):
return root.name
```
### Mutation.Input
`Mutation.Input` is now deprecated in favor of using `Mutation.Arguments` (`ClientIDMutation` still uses `Input`).
Before:
```python
class User(Mutation):
class Input:
name = String()
```
With 2.0:
```python
class User(Mutation):
class Arguments:
name = String()
```
## Breaking Changes
### Simpler resolvers
All the resolvers in graphene have been simplified.
Prior to Graphene `2.0`, all resolvers required four arguments: `(root, args, context, info)`.
Now, resolver `args` are passed as keyword arguments to the function, and `context` argument dissapeared in favor of `info.context`.
Before:
```python
my_field = graphene.String(my_arg=graphene.String())
def resolve_my_field(root, args, context, info):
my_arg = args.get('my_arg')
return ...
```
With 2.0:
```python
my_field = graphene.String(my_arg=graphene.String())
def resolve_my_field(root, info, my_arg):
return ...
```
**PS.: Take care with receiving args like `my_arg` as above. This doesn't work for optional (non-required) arguments as standard `Connection`'s arguments (first, last, after, before).**
You may need something like this:
```python
def resolve_my_field(root, info, known_field1, known_field2, **args): ## get other args with: args.get('arg_key')
```
And, if you need the context in the resolver, you can use `info.context`:
```python
my_field = graphene.String(my_arg=graphene.String())
def resolve_my_field(root, info, my_arg):
context = info.context
return ...
```
### Node Connections
Node types no longer have a `Connection` by default.
In 2.0 and onwards `Connection`s should be defined explicitly.
Before:
```python
class User(ObjectType):
class Meta:
interfaces = [relay.Node]
name = String()
class Query(ObjectType):
user_connection = relay.ConnectionField(User)
```
With 2.0:
```python
class User(ObjectType):
class Meta:
interfaces = [relay.Node]
name = String()
class UserConnection(relay.Connection):
class Meta:
node = User
class Query(ObjectType):
user_connection = relay.ConnectionField(UserConnection)
```
## Node.get_node
The method `get_node` in `ObjectTypes` that have `Node` as interface, changes its API.
From `def get_node(cls, id, context, info)` to `def get_node(cls, info, id)`.
```python
class MyObject(ObjectType):
class Meta:
interfaces = (Node, )
@classmethod
def get_node(cls, id, context, info):
return ...
```
To:
```python
class MyObject(ObjectType):
class Meta:
interfaces = (Node, )
@classmethod
def get_node(cls, info, id):
return ...
```
## Node.get_node_from_global_id
The parameters' order of `get_node_from_global_id` method has changed. You may need to adjust your [Node Root Field](http://docs.graphene-python.org/en/latest/relay/nodes/#node-root-field) and maybe other places that uses this method to obtain an object.
Before:
```python
class RootQuery(object):
...
node = Field(relay.Node, id=ID(required=True))
def resolve_node(root, args, context, info):
node = relay.Node.get_node_from_global_id(args['id'], context, info)
return node
```
Now:
```python
class RootQuery(object):
...
node = Field(relay.Node, id=ID(required=True))
def resolve_node(root, info, id):
node = relay.Node.get_node_from_global_id(info, id)
return node
```
## Mutation.mutate
Now only receives (`root`, `info`, `**kwargs`) and is not a @classmethod
Before:
```python
class SomeMutation(Mutation):
...
@classmethod
def mutate(cls, instance, args, context, info):
...
```
With 2.0:
```python
class SomeMutation(Mutation):
...
def mutate(root, info, **args):
...
```
With 2.0 you can also get your declared (as above) `args` this way:
```python
class SomeMutation(Mutation):
class Arguments:
first_name = String(required=True)
last_name = String(required=True)
...
def mutate(root, info, first_name, last_name):
...
```
## ClientIDMutation.mutate_and_get_payload
Now only receives (`root`, `info`, `**input`)
### Middlewares
If you are using Middelwares, you need to some adjustments:
Before:
```python
class MyGrapheneMiddleware(object):
def resolve(self, next_mw, root, args, context, info):
## Middleware code
return next_mw(root, args, context, info)
```
With 2.0:
```python
class MyGrapheneMiddleware(object):
def resolve(self, next_mw, root, info, **args):
context = info.context
## Middleware code
info.context = context
       return next_mw(root, info, **args)
```
## New Features
### InputObjectType
If you are using `InputObjectType`, you now can access
its fields via `getattr` (`my_input.myattr`) when resolving, instead of
the classic way `my_input['myattr']`.
And also use custom defined properties on your input class.
Example. Before:
```python
class UserInput(InputObjectType):
id = ID(required=True)
def is_valid_input(input):
return input.get('id').startswith('userid_')
class Query(ObjectType):
user = graphene.Field(User, input=UserInput())
@resolve_only_args
def resolve_user(root, input):
user_id = input.get('id')
if is_valid_input(user_id):
return get_user(user_id)
```
With 2.0:
```python
class UserInput(InputObjectType):
id = ID(required=True)
@property
def is_valid(root):
return root.id.startswith('userid_')
class Query(ObjectType):
user = graphene.Field(User, input=UserInput())
def resolve_user(root, info, input):
if input.is_valid:
return get_user(input.id)
```
### Meta as Class arguments
Now you can use the meta options as class arguments (**ONLY PYTHON 3**).
Before:
```python
class Dog(ObjectType):
class Meta:
interfaces = [Pet]
name = String()
```
With 2.0:
```python
class Dog(ObjectType, interfaces=[Pet]):
name = String()
```
### Abstract types
Now you can create abstact types super easily, without the need of subclassing the meta.
```python
class Base(ObjectType):
class Meta:
abstract = True
id = ID()
def resolve_id(root, info):
return f"{root.__class__.__name__}_{root.id}"
```
### UUID Scalar
In Graphene 2.0 there is a new dedicated scalar for UUIDs, `UUID`.

View File

@ -1,7 +0,0 @@
#!/bin/bash
# Install the required scripts with
# pip install autoflake autopep8 isort
autoflake ./examples/ ./graphene/ -r --remove-unused-variables --remove-all-unused-imports --in-place
autopep8 ./examples/ ./graphene/ -r --in-place --experimental --aggressive --max-line-length 120
isort -rc ./examples/ ./graphene/

View File

@ -17,75 +17,54 @@ I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
.PHONY: help
help:
@echo "Please use \`make <target>' where <target> is one of"
@echo " html to make standalone HTML files"
@echo " dirhtml to make HTML files named index.html in directories"
@echo " singlehtml to make a single large HTML file"
@echo " pickle to make pickle files"
@echo " json to make JSON files"
@echo " htmlhelp to make HTML files and a HTML help project"
@echo " qthelp to make HTML files and a qthelp project"
@echo " applehelp to make an Apple Help Book"
@echo " devhelp to make HTML files and a Devhelp project"
@echo " epub to make an epub"
@echo " epub3 to make an epub3"
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
@echo " latexpdf to make LaTeX files and run them through pdflatex"
@echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
@echo " text to make text files"
@echo " man to make manual pages"
@echo " texinfo to make Texinfo files"
@echo " info to make Texinfo files and run them through makeinfo"
@echo " gettext to make PO message catalogs"
@echo " changes to make an overview of all changed/added/deprecated items"
@echo " xml to make Docutils-native XML files"
@echo " pseudoxml to make pseudoxml-XML files for display purposes"
@echo " linkcheck to check all external links for integrity"
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
@echo " coverage to run coverage check of the documentation (if enabled)"
@echo " dummy to check syntax errors of document sources"
@grep -E '^\.PHONY: [a-zA-Z_-]+ .*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = "(: |##)"}; {printf "\033[36m%-30s\033[0m %s\n", $$2, $$3}'
.PHONY: clean
.PHONY: install ## to install all documentation related requirements
install:
pip install -r requirements.txt
.PHONY: clean ## to remove all built documentation
clean:
rm -rf $(BUILDDIR)/*
.PHONY: html
.PHONY: html ## to make standalone HTML files
html:
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
.PHONY: dirhtml
.PHONY: dirhtml ## to make HTML files named index.html in directories
dirhtml:
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
.PHONY: singlehtml
.PHONY: singlehtml ## to make a single large HTML file
singlehtml:
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
@echo
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
.PHONY: pickle
.PHONY: pickle ## to make pickle files
pickle:
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
@echo
@echo "Build finished; now you can process the pickle files."
.PHONY: json
.PHONY: json ## to make JSON files
json:
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
@echo
@echo "Build finished; now you can process the JSON files."
.PHONY: htmlhelp
.PHONY: htmlhelp ## to make HTML files and a HTML help project
htmlhelp:
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
@echo
@echo "Build finished; now you can run HTML Help Workshop with the" \
".hhp project file in $(BUILDDIR)/htmlhelp."
.PHONY: qthelp
.PHONY: qthelp ## to make HTML files and a qthelp project
qthelp:
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
@echo
@ -95,7 +74,7 @@ qthelp:
@echo "To view the help file:"
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Graphene.qhc"
.PHONY: applehelp
.PHONY: applehelp ## to make an Apple Help Book
applehelp:
$(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp
@echo
@ -104,7 +83,7 @@ applehelp:
"~/Library/Documentation/Help or install it in your application" \
"bundle."
.PHONY: devhelp
.PHONY: devhelp ## to make HTML files and a Devhelp project
devhelp:
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
@echo
@ -114,19 +93,19 @@ devhelp:
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Graphene"
@echo "# devhelp"
.PHONY: epub
.PHONY: epub ## to make an epub
epub:
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
@echo
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
.PHONY: epub3
.PHONY: epub3 ## to make an epub3
epub3:
$(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3
@echo
@echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3."
.PHONY: latex
.PHONY: latex ## to make LaTeX files, you can set PAPER=a4 or PAPER=letter
latex:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo
@ -134,33 +113,33 @@ latex:
@echo "Run \`make' in that directory to run these through (pdf)latex" \
"(use \`make latexpdf' here to do that automatically)."
.PHONY: latexpdf
.PHONY: latexpdf ## to make LaTeX files and run them through pdflatex
latexpdf:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through pdflatex..."
$(MAKE) -C $(BUILDDIR)/latex all-pdf
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
.PHONY: latexpdfja
.PHONY: latexpdfja ## to make LaTeX files and run them through platex/dvipdfmx
latexpdfja:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through platex and dvipdfmx..."
$(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
.PHONY: text
.PHONY: text ## to make text files
text:
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
@echo
@echo "Build finished. The text files are in $(BUILDDIR)/text."
.PHONY: man
.PHONY: man ## to make manual pages
man:
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
@echo
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
.PHONY: texinfo
.PHONY: texinfo ## to make Texinfo files
texinfo:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo
@ -168,62 +147,62 @@ texinfo:
@echo "Run \`make' in that directory to run these through makeinfo" \
"(use \`make info' here to do that automatically)."
.PHONY: info
.PHONY: info ## to make Texinfo files and run them through makeinfo
info:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo "Running Texinfo files through makeinfo..."
make -C $(BUILDDIR)/texinfo info
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
.PHONY: gettext
.PHONY: gettext ## to make PO message catalogs
gettext:
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
@echo
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
.PHONY: changes
.PHONY: changes ## to make an overview of all changed/added/deprecated items
changes:
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
@echo
@echo "The overview file is in $(BUILDDIR)/changes."
.PHONY: linkcheck
.PHONY: linkcheck ## to check all external links for integrity
linkcheck:
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
@echo
@echo "Link check complete; look for any errors in the above output " \
"or in $(BUILDDIR)/linkcheck/output.txt."
.PHONY: doctest
.PHONY: doctest ## to run all doctests embedded in the documentation (if enabled)
doctest:
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
@echo "Testing of doctests in the sources finished, look at the " \
"results in $(BUILDDIR)/doctest/output.txt."
.PHONY: coverage
.PHONY: coverage ## to run coverage check of the documentation (if enabled)
coverage:
$(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage
@echo "Testing of coverage in the sources finished, look at the " \
"results in $(BUILDDIR)/coverage/python.txt."
.PHONY: xml
.PHONY: xml ## to make Docutils-native XML files
xml:
$(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
@echo
@echo "Build finished. The XML files are in $(BUILDDIR)/xml."
.PHONY: pseudoxml
.PHONY: pseudoxml ## to make pseudoxml-XML files for display purposes
pseudoxml:
$(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
@echo
@echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."
.PHONY: dummy
.PHONY: dummy ## to check syntax errors of document sources
dummy:
$(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy
@echo
@echo "Build finished. Dummy builder generates no files."
.PHONY: livehtml
.PHONY: livehtml ## to build and serve live-reloading documentation
livehtml:
sphinx-autobuild -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
sphinx-autobuild -b html --watch ../graphene $(ALLSPHINXOPTS) $(BUILDDIR)/html

110
docs/api/index.rst Normal file
View File

@ -0,0 +1,110 @@
API Reference
=============
Schema
------
.. autoclass:: graphene.types.schema.Schema
:members:
.. Uncomment sections / types as API documentation is fleshed out
.. in each class
Object types
------------
.. autoclass:: graphene.ObjectType
.. autoclass:: graphene.InputObjectType
.. autoclass:: graphene.Mutation
:members:
.. _fields-mounted-types:
Fields (Mounted Types)
----------------------
.. autoclass:: graphene.Field
.. autoclass:: graphene.Argument
.. autoclass:: graphene.InputField
Fields (Unmounted Types)
------------------------
.. autoclass:: graphene.types.unmountedtype.UnmountedType
GraphQL Scalars
---------------
.. autoclass:: graphene.Int()
.. autoclass:: graphene.Float()
.. autoclass:: graphene.String()
.. autoclass:: graphene.Boolean()
.. autoclass:: graphene.ID()
Graphene Scalars
----------------
.. autoclass:: graphene.Date()
.. autoclass:: graphene.DateTime()
.. autoclass:: graphene.Time()
.. autoclass:: graphene.Decimal()
.. autoclass:: graphene.UUID()
.. autoclass:: graphene.JSONString()
.. autoclass:: graphene.Base64()
Enum
----
.. autoclass:: graphene.Enum()
Structures
----------
.. autoclass:: graphene.List
.. autoclass:: graphene.NonNull
Type Extension
--------------
.. autoclass:: graphene.Interface()
.. autoclass:: graphene.Union()
Execution Metadata
------------------
.. autoclass:: graphene.ResolveInfo
.. autoclass:: graphene.Context
.. autoclass:: graphql.ExecutionResult
.. Relay
.. -----
.. .. autoclass:: graphene.Node
.. .. autoclass:: graphene.GlobalID
.. .. autoclass:: graphene.ClientIDMutation
.. .. autoclass:: graphene.Connection
.. .. autoclass:: graphene.ConnectionField
.. .. autoclass:: graphene.PageInfo

View File

@ -1,6 +1,9 @@
import os
import sys
on_rtd = os.environ.get('READTHEDOCS', None) == 'True'
import sphinx_graphene_theme
on_rtd = os.environ.get("READTHEDOCS", None) == "True"
# -*- coding: utf-8 -*-
#
@ -20,9 +23,8 @@ on_rtd = os.environ.get('READTHEDOCS', None) == 'True'
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
# import os
# import sys
# sys.path.insert(0, os.path.abspath('.'))
sys.path.insert(0, os.path.abspath(".."))
# -- General configuration ------------------------------------------------
@ -34,53 +36,52 @@ on_rtd = os.environ.get('READTHEDOCS', None) == 'True'
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
'sphinx.ext.autodoc',
'sphinx.ext.intersphinx',
'sphinx.ext.todo',
'sphinx.ext.coverage',
'sphinx.ext.viewcode',
"sphinx.ext.autodoc",
"sphinx.ext.intersphinx",
"sphinx.ext.todo",
"sphinx.ext.coverage",
"sphinx.ext.viewcode",
"sphinx.ext.napoleon",
]
if not on_rtd:
extensions += [
'sphinx.ext.githubpages',
]
extensions += ["sphinx.ext.githubpages"]
# 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 = u'Graphene'
copyright = u'Graphene 2016'
author = u'Syrus Akbary'
project = "Graphene"
copyright = "Graphene 2016"
author = "Syrus Akbary"
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
version = u'1.0'
version = "1.0"
# The full version, including alpha/beta/rc tags.
release = u'1.0'
release = "1.0"
# 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 = None
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
@ -94,7 +95,7 @@ language = None
# 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"]
# The reST default role (used for this markup: `text`) to use for all
# documents.
@ -116,7 +117,7 @@ exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
# 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 = []
@ -136,7 +137,6 @@ todo_include_todos = True
# html_theme = 'alabaster'
# if on_rtd:
# html_theme = 'sphinx_rtd_theme'
import sphinx_graphene_theme
html_theme = "sphinx_graphene_theme"
@ -174,7 +174,7 @@ html_theme_path = [sphinx_graphene_theme.get_html_theme_path()]
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
html_static_path = ["_static"]
# Add any extra paths that contain custom files (such as robots.txt or
# .htaccess) here, relative to this directory. These files are copied
@ -254,7 +254,7 @@ html_static_path = ['_static']
# html_search_scorer = 'scorer.js'
# Output file base name for HTML help builder.
htmlhelp_basename = 'Graphenedoc'
htmlhelp_basename = "Graphenedoc"
# -- Options for LaTeX output ---------------------------------------------
@ -262,15 +262,12 @@ 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',
@ -280,8 +277,7 @@ latex_elements = {
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
(master_doc, 'Graphene.tex', u'Graphene Documentation',
u'Syrus Akbary', 'manual'),
(master_doc, "Graphene.tex", "Graphene Documentation", "Syrus Akbary", "manual")
]
# The name of an image file (relative to this directory) to place at the top of
@ -321,10 +317,7 @@ latex_documents = [
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
(master_doc, 'graphene', u'Graphene Documentation',
[author], 1)
]
man_pages = [(master_doc, "graphene", "Graphene Documentation", [author], 1)]
# If true, show URL addresses after external links.
#
@ -337,9 +330,15 @@ man_pages = [
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
(master_doc, 'Graphene', u'Graphene Documentation',
author, 'Graphene', 'One line description of project.',
'Miscellaneous'),
(
master_doc,
"Graphene",
"Graphene Documentation",
author,
"Graphene",
"One line description of project.",
"Miscellaneous",
)
]
# Documents to append as an appendix to all manuals.
@ -413,7 +412,7 @@ epub_copyright = copyright
# epub_post_files = []
# A list of files that should not be packed into the epub file.
epub_exclude_files = ['search.html']
epub_exclude_files = ["search.html"]
# The depth of the table of contents in toc.ncx.
#
@ -446,9 +445,14 @@ epub_exclude_files = ['search.html']
# Example configuration for intersphinx: refer to the Python standard library.
intersphinx_mapping = {
'https://docs.python.org/': None,
'python': ('https://docs.python.org/', None),
'graphene_django': ('http://docs.graphene-python.org/projects/django/en/latest/', None),
'graphene_sqlalchemy': ('http://docs.graphene-python.org/projects/sqlalchemy/en/latest/', None),
'graphene_gae': ('http://docs.graphene-python.org/projects/gae/en/latest/', None),
"https://docs.python.org/": None,
"python": ("https://docs.python.org/", None),
"graphene_django": (
"http://docs.graphene-python.org/projects/django/en/latest/",
None,
),
"graphene_sqlalchemy": (
"http://docs.graphene-python.org/projects/sqlalchemy/en/latest/",
None,
),
}

View File

@ -4,7 +4,7 @@ Dataloader
DataLoader is a generic utility to be used as part of your application's
data fetching layer to provide a simplified and consistent API over
various remote data sources such as databases or web services via batching
and caching.
and caching. It is provided by a separate package `aiodataloader <https://pypi.org/project/aiodataloader/>`.
Batching
@ -15,38 +15,49 @@ Create loaders by providing a batch loading function.
.. code:: python
from promise import Promise
from promise.dataloader import DataLoader
from aiodataloader import DataLoader
class UserLoader(DataLoader):
def batch_load_fn(self, keys):
# Here we return a promise that will result on the
# corresponding user for each key in keys
return Promise.resolve([get_user(id=key) for key in keys])
async def batch_load_fn(self, keys):
# Here we call a function to return a user for each key in keys
return [get_user(id=key) for key in keys]
A batch loading function accepts an list of keys, and returns a ``Promise``
which resolves to an list of ``values``.
A batch loading async function accepts a list of keys, and returns a list of ``values``.
Then load individual values from the loader. ``DataLoader`` will coalesce all
individual loads which occur within a single frame of execution (executed once
the wrapping promise is resolved) and then call your batch function with all
requested keys.
``DataLoader`` will coalesce all individual loads which occur within a
single frame of execution (executed once the wrapping event loop is resolved)
and then call your batch function with all requested keys.
.. code:: python
user_loader = UserLoader()
user_loader.load(1).then(lambda user: user_loader.load(user.best_friend_id))
user1 = await user_loader.load(1)
user1_best_friend = await user_loader.load(user1.best_friend_id)
user_loader.load(2).then(lambda user: user_loader.load(user.best_friend_id))
user2 = await user_loader.load(2)
user2_best_friend = await user_loader.load(user2.best_friend_id)
A naive application may have issued *four* round-trips to a backend for the
required information, but with ``DataLoader`` this application will make at most *two*.
Note that loaded values are one-to-one with the keys and must have the same
order. This means that if you load all values from a single query, you must
make sure that you then order the query result for the results to match the keys:
.. code:: python
class UserLoader(DataLoader):
async def batch_load_fn(self, keys):
users = {user.id: user for user in User.objects.filter(id__in=keys)}
return [users.get(user_id) for user_id in keys]
``DataLoader`` allows you to decouple unrelated parts of your application without
sacrificing the performance of batch data-loading. While the loader presents
an API that loads individual values, all concurrent requests will be coalesced
@ -59,7 +70,7 @@ maintain minimal outgoing data requests.
Using with Graphene
-------------------
DataLoader pairs nicely well with Grapehne/GraphQL. GraphQL fields are designed
DataLoader pairs nicely well with Graphene/GraphQL. GraphQL fields are designed
to be stand-alone functions. Without a caching or batching mechanism, it's easy
for a naive GraphQL server to issue new database requests each time a field is resolved.
@ -84,12 +95,12 @@ Consider the following GraphQL request:
}
Naively, if ``me``, ``bestFriend`` and ``friends`` each need to request the backend,
If ``me``, ``bestFriend`` and ``friends`` each need to send a request to the backend,
there could be at most 13 database requests!
When using DataLoader, we could define the User type using our previous example with
learer code and at most 4 database requests, and possibly fewer if there are cache hits.
leaner code and at most 4 database requests, and possibly fewer if there are cache hits.
.. code:: python
@ -99,8 +110,8 @@ learer code and at most 4 database requests, and possibly fewer if there are cac
best_friend = graphene.Field(lambda: User)
friends = graphene.List(lambda: User)
def resolve_best_friend(self, args, context, info):
return user_loader.load(self.best_friend_id)
async def resolve_best_friend(root, info):
return await user_loader.load(root.best_friend_id)
def resolve_friends(self, args, context, info):
return user_loader.load_many(self.friend_ids)
async def resolve_friends(root, info):
return await user_loader.load_many(root.friend_ids)

View File

@ -1,32 +1,138 @@
.. _SchemaExecute:
Executing a query
=================
For executing a query a schema, you can directly call the ``execute`` method on it.
For executing a query against a schema, you can directly call the ``execute`` method on it.
.. code:: python
schema = graphene.Schema(...)
from graphene import Schema
schema = Schema(...)
result = schema.execute('{ name }')
``result`` represents he result of execution. ``result.data`` is the result of executing the query, ``result.errors`` is ``None`` if no errors occurred, and is a non-empty list if an error occurred.
``result`` represents the result of execution. ``result.data`` is the result of executing the query, ``result.errors`` is ``None`` if no errors occurred, and is a non-empty list if an error occurred.
.. _SchemaExecuteContext:
Context
_______
You can pass context to a query via ``context_value``.
You can pass context to a query via ``context``.
.. code:: python
class Query(graphene.ObjectType):
name = graphene.String()
from graphene import ObjectType, String, Schema
def resolve_name(self, args, context, info):
return context.get('name')
class Query(ObjectType):
name = String()
schema = graphene.Schema(Query)
result = schema.execute('{ name }', context_value={'name': 'Syrus'})
def resolve_name(root, info):
return info.context.get('name')
schema = Schema(Query)
result = schema.execute('{ name }', context={'name': 'Syrus'})
assert result.data['name'] == 'Syrus'
Variables
_________
You can pass variables to a query via ``variables``.
.. code:: python
from graphene import ObjectType, Field, ID, Schema
class Query(ObjectType):
user = Field(User, id=ID(required=True))
def resolve_user(root, info, id):
return get_user_by_id(id)
schema = Schema(Query)
result = schema.execute(
'''
query getUser($id: ID) {
user(id: $id) {
id
firstName
lastName
}
}
''',
variables={'id': 12},
)
Root Value
__________
Value used for :ref:`ResolverParamParent` in root queries and mutations can be overridden using ``root`` parameter.
.. code:: python
from graphene import ObjectType, Field, Schema
class Query(ObjectType):
me = Field(User)
def resolve_user(root, info):
return {'id': root.id, 'firstName': root.name}
schema = Schema(Query)
user_root = User(id=12, name='bob')
result = schema.execute(
'''
query getUser {
user {
id
firstName
lastName
}
}
''',
root=user_root
)
assert result.data['user']['id'] == user_root.id
Operation Name
______________
If there are multiple operations defined in a query string, ``operation_name`` should be used to indicate which should be executed.
.. code:: python
from graphene import ObjectType, Field, Schema
class Query(ObjectType):
user = Field(User)
def resolve_user(root, info):
return get_user_by_id(12)
schema = Schema(Query)
query_string = '''
query getUserWithFirstName {
user {
id
firstName
lastName
}
}
query getUserWithFullName {
user {
id
fullName
}
}
'''
result = schema.execute(
query_string,
operation_name='getUserWithFullName'
)
assert result.data['user']['fullName']

View File

@ -0,0 +1,8 @@
File uploading
==============
File uploading is not part of the official GraphQL spec yet and is not natively
implemented in Graphene.
If your server needs to support file uploading then you can use the library: `graphene-file-upload <https://github.com/lmcgartland/graphene-file-upload>`_ which enhances Graphene to add file
uploads and conforms to the unoffical GraphQL `multipart request spec <https://github.com/jaydenseric/graphql-multipart-request-spec>`_.

View File

@ -8,3 +8,6 @@ Execution
execute
middleware
dataloader
fileuploading
subscriptions
queryvalidation

View File

@ -3,7 +3,7 @@ Middleware
You can use ``middleware`` to affect the evaluation of fields in your schema.
A middleware is any object that responds to ``resolve(*args, next_middleware)``.
A middleware is any object or function that responds to ``resolve(next_middleware, *args)``.
Inside that method, it should either:
@ -16,12 +16,10 @@ Resolve arguments
Middlewares ``resolve`` is invoked with several arguments:
- ``next`` represents the execution chain. Call ``next`` to continue evalution.
- ``next`` represents the execution chain. Call ``next`` to continue evaluation.
- ``root`` is the root value object passed throughout the query.
- ``args`` is the hash of arguments passed to the field.
- ``context`` is the context object passed throughout the query.
- ``info`` is the resolver info.
- ``args`` is the dict of arguments passed to the field.
Example
-------
@ -31,10 +29,10 @@ This middleware only continues evaluation if the ``field_name`` is not ``'user'`
.. code:: python
class AuthorizationMiddleware(object):
def resolve(self, next, root, args, context, info):
def resolve(self, next, root, info, **args):
if info.field_name == 'user':
return None
return next(root, args, context, info)
return next(root, info, **args)
And then execute it with:
@ -42,3 +40,31 @@ And then execute it with:
.. code:: python
result = schema.execute('THE QUERY', middleware=[AuthorizationMiddleware()])
If the ``middleware`` argument includes multiple middlewares,
these middlewares will be executed bottom-up, i.e. from last to first.
Functional example
------------------
Middleware can also be defined as a function. Here we define a middleware that
logs the time it takes to resolve each field:
.. code:: python
from time import time as timer
def timing_middleware(next, root, info, **args):
start = timer()
return_value = next(root, info, **args)
duration = round((timer() - start) * 1000, 2)
parent_type_name = root._meta.name if root and hasattr(root, '_meta') else ''
logger.debug(f"{parent_type_name}.{info.field_name}: {duration} ms")
return return_value
And then execute it with:
.. code:: python
result = schema.execute('THE QUERY', middleware=[timing_middleware])

View File

@ -0,0 +1,123 @@
Query Validation
================
GraphQL uses query validators to check if Query AST is valid and can be executed. Every GraphQL server implements
standard query validators. For example, there is an validator that tests if queried field exists on queried type, that
makes query fail with "Cannot query field on type" error if it doesn't.
To help with common use cases, graphene provides a few validation rules out of the box.
Depth limit Validator
---------------------
The depth limit validator helps to prevent execution of malicious
queries. It takes in the following arguments.
- ``max_depth`` is the maximum allowed depth for any operation in a GraphQL document.
- ``ignore`` Stops recursive depth checking based on a field name. Either a string or regexp to match the name, or a function that returns a boolean
- ``callback`` Called each time validation runs. Receives an Object which is a map of the depths for each operation.
Usage
-----
Here is how you would implement depth-limiting on your schema.
.. code:: python
from graphql import validate, parse
from graphene import ObjectType, Schema, String
from graphene.validation import depth_limit_validator
class MyQuery(ObjectType):
name = String(required=True)
schema = Schema(query=MyQuery)
# queries which have a depth more than 20
# will not be executed.
validation_errors = validate(
schema=schema.graphql_schema,
document_ast=parse('THE QUERY'),
rules=(
depth_limit_validator(
max_depth=20
),
)
)
Disable Introspection
---------------------
the disable introspection validation rule ensures that your schema cannot be introspected.
This is a useful security measure in production environments.
Usage
-----
Here is how you would disable introspection for your schema.
.. code:: python
from graphql import validate, parse
from graphene import ObjectType, Schema, String
from graphene.validation import DisableIntrospection
class MyQuery(ObjectType):
name = String(required=True)
schema = Schema(query=MyQuery)
# introspection queries will not be executed.
validation_errors = validate(
schema=schema.graphql_schema,
document_ast=parse('THE QUERY'),
rules=(
DisableIntrospection,
)
)
Implementing custom validators
------------------------------
All custom query validators should extend the `ValidationRule <https://github.com/graphql-python/graphql-core/blob/v3.0.5/src/graphql/validation/rules/__init__.py#L37>`_
base class importable from the graphql.validation.rules module. Query validators are visitor classes. They are
instantiated at the time of query validation with one required argument (context: ASTValidationContext). In order to
perform validation, your validator class should define one or more of enter_* and leave_* methods. For possible
enter/leave items as well as details on function documentation, please see contents of the visitor module. To make
validation fail, you should call validator's report_error method with the instance of GraphQLError describing failure
reason. Here is an example query validator that visits field definitions in GraphQL query and fails query validation
if any of those fields are blacklisted:
.. code:: python
from graphql import GraphQLError
from graphql.language import FieldNode
from graphql.validation import ValidationRule
my_blacklist = (
"disallowed_field",
)
def is_blacklisted_field(field_name: str):
return field_name.lower() in my_blacklist
class BlackListRule(ValidationRule):
def enter_field(self, node: FieldNode, *_args):
field_name = node.name.value
if not is_blacklisted_field(field_name):
return
self.report_error(
GraphQLError(
f"Cannot query '{field_name}': field is blacklisted.", node,
)
)

View File

@ -0,0 +1,40 @@
.. _SchemaSubscription:
Subscriptions
=============
To create a subscription, you can directly call the ``subscribe`` method on the
schema. This method is async and must be awaited.
.. code:: python
import asyncio
from datetime import datetime
from graphene import ObjectType, String, Schema, Field
# Every schema requires a query.
class Query(ObjectType):
hello = String()
def resolve_hello(root, info):
return "Hello, world!"
class Subscription(ObjectType):
time_of_day = String()
async def subscribe_time_of_day(root, info):
while True:
yield datetime.now().isoformat()
await asyncio.sleep(1)
schema = Schema(query=Query, subscription=Subscription)
async def main(schema):
subscription = 'subscription { timeOfDay }'
result = await schema.subscribe(subscription)
async for item in result:
print(item.data['timeOfDay'])
asyncio.run(main(schema))
The ``result`` is an async iterator which yields items in the same manner as a query.

View File

@ -11,10 +11,16 @@ Contents:
execution/index
relay/index
testing/index
api/index
.. _Integrations:
Integrations
-----
------------
* `Graphene-Django <http://docs.graphene-python.org/projects/django/en/latest/>`_ (`source <https://github.com/graphql-python/graphene-django/>`_)
* Flask-Graphql (`source <https://github.com/graphql-python/flask-graphql>`_)
* `Graphene-SQLAlchemy <http://docs.graphene-python.org/projects/sqlalchemy/en/latest/>`_ (`source <https://github.com/graphql-python/graphene-sqlalchemy/>`_)
* `Graphene-GAE <http://docs.graphene-python.org/projects/gae/en/latest/>`_ (`source <https://github.com/graphql-python/graphene-gae/>`_)
* `Graphene-Mongo <http://graphene-mongo.readthedocs.io/en/latest/>`_ (`source <https://github.com/graphql-python/graphene-mongo>`_)
* `Starlette <https://www.starlette.io/graphql/>`_ (`source <https://github.com/encode/starlette>`_)
* `FastAPI <https://fastapi.tiangolo.com/advanced/graphql/>`_ (`source <https://github.com/tiangolo/fastapi>`_)

View File

@ -1,57 +1,144 @@
Getting started
===============
What is GraphQL?
----------------
For an introduction to GraphQL and an overview of its concepts, please refer
to `the official introduction <http://graphql.org/learn/>`_.
Lets build a basic GraphQL schema from scratch.
Requirements
Introduction
------------
- Python (2.7, 3.2, 3.3, 3.4, 3.5, pypy)
- Graphene (1.0)
What is GraphQL?
~~~~~~~~~~~~~~~~
GraphQL is a query language for your API.
It provides a standard way to:
* *describe data provided by a server* in a statically typed **Schema**
* *request data* in a **Query** which exactly describes your data requirements and
* *receive data* in a **Response** containing only the data you requested.
For an introduction to GraphQL and an overview of its concepts, please refer to `the official GraphQL documentation`_.
.. _the official GraphQL documentation: http://graphql.org/learn/
What is Graphene?
~~~~~~~~~~~~~~~~~
Graphene is a library that provides tools to implement a GraphQL API in Python using a *code-first* approach.
Compare Graphene's *code-first* approach to building a GraphQL API with *schema-first* approaches like `Apollo Server`_ (JavaScript) or Ariadne_ (Python). Instead of writing GraphQL **Schema Definition Language (SDL)**, we write Python code to describe the data provided by your server.
.. _Apollo Server: https://www.apollographql.com/docs/apollo-server/
.. _Ariadne: https://ariadnegraphql.org/
Graphene is fully featured with integrations for the most popular web frameworks and ORMs. Graphene produces schemas that are fully compliant with the GraphQL spec and provides tools and patterns for building a Relay-Compliant API as well.
An example in Graphene
----------------------
Lets build a basic GraphQL schema to say "hello" and "goodbye" in Graphene.
When we send a **Query** requesting only one **Field**, ``hello``, and specify a value for the ``firstName`` **Argument**...
.. code::
{
hello(firstName: "friend")
}
...we would expect the following Response containing only the data requested (the ``goodbye`` field is not resolved).
.. code::
{
"data": {
"hello": "Hello friend!"
}
}
Requirements
~~~~~~~~~~~~
- Python (3.8, 3.9, 3.10, 3.11, 3.12, pypy)
- Graphene (3.0)
Project setup
-------------
~~~~~~~~~~~~~
.. code:: bash
pip install "graphene>=1.0"
pip install "graphene>=3.0"
Creating a basic Schema
-----------------------
~~~~~~~~~~~~~~~~~~~~~~~
A GraphQL schema describes your data model, and provides a GraphQL
server with an associated set of resolve methods that know how to fetch
data.
We are going to create a very simple schema, with a ``Query`` with only
one field: ``hello`` and an input name. And when we query it, it should return ``"Hello {name}"``.
In Graphene, we can define a simple schema using the following code:
.. code:: python
import graphene
from graphene import ObjectType, String, Schema
class Query(graphene.ObjectType):
hello = graphene.String(name=graphene.Argument(graphene.String, default_value="stranger"))
class Query(ObjectType):
# this defines a Field `hello` in our Schema with a single Argument `first_name`
# By default, the argument name will automatically be camel-based into firstName in the generated schema
hello = String(first_name=String(default_value="stranger"))
goodbye = String()
def resolve_hello(self, args, context, info):
return 'Hello ' + args['name']
# our Resolver method takes the GraphQL context (root, info) as well as
# Argument (first_name) for the Field and returns data for the query Response
def resolve_hello(root, info, first_name):
return f'Hello {first_name}!'
schema = graphene.Schema(query=Query)
def resolve_goodbye(root, info):
return 'See ya!'
schema = Schema(query=Query)
A GraphQL **Schema** describes each **Field** in the data model provided by the server using scalar types like *String*, *Int* and *Enum* and compound types like *List* and *Object*. For more details refer to the Graphene :ref:`TypesReference`.
Our schema can also define any number of **Arguments** for our **Fields**. This is a powerful way for a **Query** to describe the exact data requirements for each **Field**.
For each **Field** in our **Schema**, we write a **Resolver** method to fetch data requested by a client's **Query** using the current context and **Arguments**. For more details, refer to this section on :ref:`Resolvers`.
Schema Definition Language (SDL)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
In the `GraphQL Schema Definition Language`_, we could describe the fields defined by our example code as shown below.
.. _GraphQL Schema Definition Language: https://graphql.org/learn/schema/
.. code::
type Query {
hello(firstName: String = "stranger"): String
goodbye: String
}
Further examples in this documentation will use SDL to describe schema created by ObjectTypes and other fields.
Querying
--------
~~~~~~~~
Then we can start querying our schema:
Then we can start querying our **Schema** by passing a GraphQL query string to ``execute``:
.. code:: python
result = schema.execute('{ hello }')
print result.data['hello'] # "Hello stranger"
# we can query for our field (with the default argument)
query_string = '{ hello }'
result = schema.execute(query_string)
print(result.data['hello'])
# "Hello stranger!"
Congrats! You got your first graphene schema working!
# or passing the argument in the query
query_with_argument = '{ hello(firstName: "GraphQL") }'
result = schema.execute(query_with_argument)
print(result.data['hello'])
# "Hello GraphQL!"
Next steps
~~~~~~~~~~
Congrats! You got your first Graphene schema working!
Normally, we don't need to directly execute a query string against our schema as Graphene provides many useful Integrations with popular web frameworks like Flask and Django. Check out :ref:`Integrations` for more information on how to get started serving your GraphQL API.

View File

@ -41,5 +41,5 @@ that implements ``Node`` will have a default Connection.
name = graphene.String()
ships = relay.ConnectionField(ShipConnection)
def resolve_ships(self, args, context, info):
def resolve_ships(root, info):
return []

View File

@ -19,11 +19,8 @@ Useful links
- `Getting started with Relay`_
- `Relay Global Identification Specification`_
- `Relay Cursor Connection Specification`_
- `Relay input Object Mutation`_
.. _Relay: https://facebook.github.io/relay/docs/graphql-relay-specification.html
.. _Relay specification: https://facebook.github.io/relay/graphql/objectidentification.htm#sec-Node-root-field
.. _Getting started with Relay: https://facebook.github.io/relay/docs/graphql-relay-specification.html
.. _Relay Global Identification Specification: https://facebook.github.io/relay/graphql/objectidentification.htm
.. _Relay Cursor Connection Specification: https://facebook.github.io/relay/graphql/connections.htm
.. _Relay input Object Mutation: https://facebook.github.io/relay/graphql/mutations.htm
.. _Relay: https://relay.dev/docs/guides/graphql-server-specification/
.. _Getting started with Relay: https://relay.dev/docs/getting-started/step-by-step-guide/
.. _Relay Global Identification Specification: https://relay.dev/graphql/objectidentification.htm
.. _Relay Cursor Connection Specification: https://relay.dev/graphql/connections.htm

View File

@ -21,9 +21,9 @@ subclass of ``relay.ClientIDMutation``.
faction = graphene.Field(Faction)
@classmethod
def mutate_and_get_payload(cls, input, context, info):
ship_name = input.get('ship_name')
faction_id = input.get('faction_id')
def mutate_and_get_payload(cls, root, info, **input):
ship_name = input.ship_name
faction_id = input.faction_id
ship = create_ship(ship_name, faction_id)
faction = get_faction(faction_id)
return IntroduceShip(ship=ship, faction=faction)
@ -46,9 +46,9 @@ Mutations can also accept files, that's how it will work with different integrat
success = graphene.String()
@classmethod
def mutate_and_get_payload(cls, input, context, info):
def mutate_and_get_payload(cls, root, info, **input):
# When using it in Django, context will be the request
files = context.FILES
files = info.context.FILES
# Or, if used in Flask, context will be the flask global request
# files = context.files

View File

@ -22,7 +22,7 @@ Example usage (taken from the `Starwars Relay example`_):
name = graphene.String(description='The name of the ship.')
@classmethod
def get_node(cls, id, context, info):
def get_node(cls, info, id):
return get_ship(id)
The ``id`` returned by the ``Ship`` type when you query it will be a
@ -51,20 +51,20 @@ Example of a custom node:
name = 'Node'
@staticmethod
def to_global_id(type, id):
return '{}:{}'.format(type, id)
def to_global_id(type_, id):
return f"{type_}:{id}"
@staticmethod
def get_node_from_global_id(global_id, context, info, only_type=None):
type, id = global_id.split(':')
if only_node:
def get_node_from_global_id(info, global_id, only_type=None):
type_, id = global_id.split(':')
if only_type:
# We assure that the node type that we want to retrieve
# is the same that was indicated in the field type
assert type == only_node._meta.name, 'Received not compatible node.'
assert type_ == only_type._meta.name, 'Received not compatible node.'
if type == 'User':
if type_ == 'User':
return get_user(id)
elif type == 'Photo':
elif type_ == 'Photo':
return get_photo(id)
@ -75,10 +75,10 @@ Accessing node types
--------------------
If we want to retrieve node instances from a ``global_id`` (scalar that identifies an instance by it's type name and id),
we can simply do ``Node.get_node_from_global_id(global_id, context, info)``.
we can simply do ``Node.get_node_from_global_id(info, global_id)``.
In the case we want to restrict the instance retrieval to a specific type, we can do:
``Node.get_node_from_global_id(global_id, context, info, only_type=Ship)``. This will raise an error
``Node.get_node_from_global_id(info, global_id, only_type=Ship)``. This will raise an error
if the ``global_id`` doesn't correspond to a Ship type.
@ -98,4 +98,5 @@ Example usage:
# Should be CustomNode.Field() if we want to use our custom Node
node = relay.Node.Field()
.. _Relay specification: https://facebook.github.io/relay/docs/graphql-relay-specification.html
.. _Starwars Relay example: https://github.com/graphql-python/graphene/blob/master/examples/starwars_relay/schema.py

View File

@ -1,4 +1,5 @@
# Required library
Sphinx==1.5.3
Sphinx==6.1.3
sphinx-autobuild==2021.3.14
# Docs template
https://github.com/graphql-python/graphene-python.org/archive/docs.zip
http://graphene-python.org/sphinx_graphene_theme.zip

View File

@ -54,7 +54,7 @@ Execute parameters
~~~~~~~~~~~~~~~~~~
You can also add extra keyword arguments to the ``execute`` method, such as
``context_value``, ``root_value``, ``variable_values``, ...:
``context``, ``root``, ``variables``, ...:
.. code:: python
@ -63,49 +63,9 @@ You can also add extra keyword arguments to the ``execute`` method, such as
def test_hey():
client = Client(my_schema)
executed = client.execute('''{ hey }''', context_value={'user': 'Peter'})
executed = client.execute('''{ hey }''', context={'user': 'Peter'})
assert executed == {
'data': {
'hey': 'hello Peter!'
}
}
Snapshot testing
~~~~~~~~~~~~~~~~
As our APIs evolve, we need to know when our changes introduce any breaking changes that might break
some of the clients of our GraphQL app.
However, writing tests and replicate the same response we expect from our GraphQL application can be
tedious and repetitive task, and sometimes it's easier to skip this process.
Because of that, we recommend the usage of `SnapshotTest <https://github.com/syrusakbary/snapshottest/>`_.
SnapshotTest let us write all this tests in a breeze, as creates automatically the ``snapshots`` for us
the first time the test is executed.
Here is a simple example on how our tests will look if we use ``pytest``:
.. code:: python
def test_hey(snapshot):
client = Client(my_schema)
# This will create a snapshot dir and a snapshot file
# the first time the test is executed, with the response
# of the execution.
snapshot.assert_match(client.execute('''{ hey }'''))
If we are using ``unittest``:
.. code:: python
from snapshottest import TestCase
class APITestCase(TestCase):
def test_api_me(self):
"""Testing the API for /me"""
client = Client(my_schema)
self.assertMatchSnapshot(client.execute('''{ hey }'''))

View File

@ -1,43 +0,0 @@
AbstractTypes
=============
An AbstractType contains fields that can be shared among
``graphene.ObjectType``, ``graphene.Interface``,
``graphene.InputObjectType`` or other ``graphene.AbstractType``.
The basics:
- Each AbstractType is a Python class that inherits from ``graphene.AbstractType``.
- Each attribute of the AbstractType represents a field (a ``graphene.Field`` or
``graphene.InputField`` depending on where it is mounted)
Quick example
-------------
In this example UserFields is an ``AbstractType`` with a name. ``User`` and
``UserInput`` are two types that have their own fields
plus the ones defined in ``UserFields``.
.. code:: python
import graphene
class UserFields(graphene.AbstractType):
name = graphene.String()
class User(graphene.ObjectType, UserFields):
pass
class UserInput(graphene.InputObjectType, UserFields):
pass
.. code::
type User {
name: String
}
inputtype UserInput {
name: String
}

View File

@ -1,7 +1,7 @@
Enums
=====
A ``Enum`` is a special ``GraphQL`` type that represents a set of
An ``Enum`` is a special ``GraphQL`` type that represents a set of
symbolic names (members) bound to unique, constant values.
Definition
@ -27,7 +27,7 @@ But also using instances of Enum:
Value descriptions
------------------
It's possible to add a description to a enum value, for that the the enum value
It's possible to add a description to an enum value, for that the enum value
needs to have the ``description`` property on it.
.. code:: python
@ -54,6 +54,16 @@ the ``Enum.from_enum`` function.
graphene.Enum.from_enum(AlreadyExistingPyEnum)
``Enum.from_enum`` supports a ``description`` and ``deprecation_reason`` lambdas as input so
you can add description etc. to your enum without changing the original:
.. code:: python
graphene.Enum.from_enum(
AlreadyExistingPyEnum,
description=lambda v: return 'foo' if v == AlreadyExistingPyEnum.Foo else 'bar'
)
Notes
-----
@ -67,6 +77,7 @@ In the Python ``Enum`` implementation you can access a member by initing the Enu
.. code:: python
from enum import Enum
class Color(Enum):
RED = 1
GREEN = 2
@ -75,11 +86,12 @@ In the Python ``Enum`` implementation you can access a member by initing the Enu
assert Color(1) == Color.RED
However, in Graphene ``Enum`` you need to call get to have the same effect:
However, in Graphene ``Enum`` you need to call `.get` to have the same effect:
.. code:: python
from graphene import Enum
class Color(Enum):
RED = 1
GREEN = 2

View File

@ -1,3 +1,5 @@
.. _TypesReference:
===============
Types Reference
===============
@ -5,11 +7,11 @@ Types Reference
.. toctree::
:maxdepth: 1
enums
schema
scalars
list-and-nonnull
interfaces
abstracttypes
objecttypes
schema
enums
interfaces
unions
mutations

View File

@ -1,60 +1,172 @@
.. _Interfaces:
Interfaces
==========
An Interface contains the essential fields that will be implemented by
multiple ObjectTypes.
An *Interface* is an abstract type that defines a certain set of fields that a
type must include to implement the interface.
The basics:
- Each Interface is a Python class that inherits from ``graphene.Interface``.
- Each attribute of the Interface represents a GraphQL field.
Quick example
-------------
This example model defines a ``Character`` interface with a name. ``Human``
and ``Droid`` are two implementations of that interface.
For example, you can define an Interface ``Character`` that represents any
character in the Star Wars trilogy:
.. code:: python
import graphene
class Character(graphene.Interface):
name = graphene.String()
id = graphene.ID(required=True)
name = graphene.String(required=True)
friends = graphene.List(lambda: Character)
Any ObjectType that implements ``Character`` will have these exact fields, with
these arguments and return types.
For example, here are some types that might implement ``Character``:
.. code:: python
# Human is a Character implementation
class Human(graphene.ObjectType):
class Meta:
interfaces = (Character, )
born_in = graphene.String()
starships = graphene.List(Starship)
home_planet = graphene.String()
# Droid is a Character implementation
class Droid(graphene.ObjectType):
class Meta:
interfaces = (Character, )
function = graphene.String()
primary_function = graphene.String()
``name`` is a field on the ``Character`` interface that will also exist on both
the ``Human`` and ``Droid`` ObjectTypes (as those implement the ``Character``
interface). Each ObjectType may define additional fields.
Both of these types have all of the fields from the ``Character`` interface,
but also bring in extra fields, ``home_planet``, ``starships`` and
``primary_function``, that are specific to that particular type of character.
The above types have the following representation in a schema:
The full GraphQL schema definition will look like this:
.. code::
interface Character {
name: String
}
type Droid implements Character {
name: String
function: String
id: ID!
name: String!
friends: [Character]
}
type Human implements Character {
name: String
bornIn: String
id: ID!
name: String!
friends: [Character]
starships: [Starship]
homePlanet: String
}
type Droid implements Character {
id: ID!
name: String!
friends: [Character]
primaryFunction: String
}
Interfaces are useful when you want to return an object or set of objects,
which might be of several different types.
For example, you can define a field ``hero`` that resolves to any
``Character``, depending on the episode, like this:
.. code:: python
class Query(graphene.ObjectType):
hero = graphene.Field(
Character,
required=True,
episode=graphene.Int(required=True)
)
def resolve_hero(root, info, episode):
# Luke is the hero of Episode V
if episode == 5:
return get_human(name='Luke Skywalker')
return get_droid(name='R2-D2')
schema = graphene.Schema(query=Query, types=[Human, Droid])
This allows you to directly query for fields that exist on the Character interface
as well as selecting specific fields on any type that implements the interface
using `inline fragments <https://graphql.org/learn/queries/#inline-fragments>`_.
For example, the following query:
.. code::
query HeroForEpisode($episode: Int!) {
hero(episode: $episode) {
__typename
name
... on Droid {
primaryFunction
}
... on Human {
homePlanet
}
}
}
Will return the following data with variables ``{ "episode": 4 }``:
.. code:: json
{
"data": {
"hero": {
"__typename": "Droid",
"name": "R2-D2",
"primaryFunction": "Astromech"
}
}
}
And different data with the variables ``{ "episode": 5 }``:
.. code:: json
{
"data": {
"hero": {
"__typename": "Human",
"name": "Luke Skywalker",
"homePlanet": "Tatooine"
}
}
}
Resolving data objects to types
-------------------------------
As you build out your schema in Graphene it's common for your resolvers to
return objects that represent the data backing your GraphQL types rather than
instances of the Graphene types (e.g. Django or SQLAlchemy models). This works
well with ``ObjectType`` and ``Scalar`` fields, however when you start using
Interfaces you might come across this error:
.. code::
"Abstract type Character must resolve to an Object type at runtime for field Query.hero ..."
This happens because Graphene doesn't have enough information to convert the
data object into a Graphene type needed to resolve the ``Interface``. To solve
this you can define a ``resolve_type`` class method on the ``Interface`` which
maps a data object to a Graphene type:
.. code:: python
class Character(graphene.Interface):
id = graphene.ID(required=True)
name = graphene.String(required=True)
@classmethod
def resolve_type(cls, instance, info):
if instance.type == 'DROID':
return Droid
return Human

View File

@ -48,3 +48,24 @@ Lists work in a similar way: We can use a type modifier to mark a type as a
``List``, which indicates that this field will return a list of that type.
It works the same for arguments, where the validation step will expect a list
for that value.
NonNull Lists
-------------
By default items in a list will be considered nullable. To define a list without
any nullable items the type needs to be marked as ``NonNull``. For example:
.. code:: python
import graphene
class Character(graphene.ObjectType):
appears_in = graphene.List(graphene.NonNull(graphene.String))
The above results in the type definition:
.. code::
type Character {
appearsIn: [String!]
}

View File

@ -13,27 +13,27 @@ This example defines a Mutation:
import graphene
class CreatePerson(graphene.Mutation):
class Input:
class Arguments:
name = graphene.String()
ok = graphene.Boolean()
person = graphene.Field(lambda: Person)
@staticmethod
def mutate(root, args, context, info):
person = Person(name=args.get('name'))
def mutate(root, info, name):
person = Person(name=name)
ok = True
return CreatePerson(person=person, ok=ok)
**person** and **ok** are the output fields of the Mutation when is
**person** and **ok** are the output fields of the Mutation when it is
resolved.
**Input** attributes are the arguments that the Mutation
**Arguments** attributes are the arguments that the Mutation
``CreatePerson`` needs for resolving, in this case **name** will be the
only argument for the mutation.
**mutate** is the function that will be applied once the mutation is
called.
called. This method is just a special resolver that we can change
data within. It takes the same arguments as the standard query :ref:`ResolverArguments`.
So, we can finish our schema like this:
@ -77,7 +77,7 @@ We should receive:
{
"createPerson": {
"person" : {
name: "Peter"
"name": "Peter"
},
"ok": true
}
@ -85,11 +85,9 @@ We should receive:
InputFields and InputObjectTypes
----------------------------------
InputFields are used in mutations to allow nested input data for mutations
To use an InputField you define an InputObjectType that specifies the structure of your input data
InputFields are used in mutations to allow nested input data for mutations.
To use an InputField you define an InputObjectType that specifies the structure of your input data:
.. code:: python
@ -97,31 +95,28 @@ To use an InputField you define an InputObjectType that specifies the structure
import graphene
class PersonInput(graphene.InputObjectType):
name = graphene.String()
age = graphene.Int()
name = graphene.String(required=True)
age = graphene.Int(required=True)
class CreatePerson(graphene.Mutation):
class Input:
person_data = graphene.Argument(PersonInput)
class Arguments:
person_data = PersonInput(required=True)
person = graphene.Field(lambda: Person)
person = graphene.Field(Person)
@staticmethod
def mutate(root, args, context, info):
p_data = args.get('person_data')
name = p_data.get('name')
age = p_data.get('age')
person = Person(name=name, age=age)
def mutate(root, info, person_data=None):
person = Person(
name=person_data.name,
age=person_data.age
)
return CreatePerson(person=person)
Note that **name** and **age** are part of **person_data** now
Note that **name** and **age** are part of **person_data** now.
Using the above mutation your new query would look like this:
.. code:: json
.. code::
mutation myFirstMutation {
createPerson(personData: {name:"Peter", age: 24}) {
@ -133,7 +128,7 @@ Using the above mutation your new query would look like this:
}
InputObjectTypes can also be fields of InputObjectTypes allowing you to have
as complex of input data as you need
as complex of input data as you need:
.. code:: python
@ -148,3 +143,41 @@ as complex of input data as you need
name = graphene.String()
latlng = graphene.InputField(LatLngInput)
Output type example
-------------------
To return an existing ObjectType instead of a mutation-specific type, set the **Output** attribute to the desired ObjectType:
.. code:: python
import graphene
class CreatePerson(graphene.Mutation):
class Arguments:
name = graphene.String()
Output = Person
def mutate(root, info, name):
return Person(name=name)
Then, if we query (``schema.execute(query_str)``) with the following:
.. code::
mutation myFirstMutation {
createPerson(name:"Peter") {
name
__typename
}
}
We should receive:
.. code:: json
{
"createPerson": {
"name": "Peter",
"__typename": "Person"
}
}

View File

@ -1,15 +1,15 @@
ObjectTypes
===========
.. _ObjectType:
An ObjectType is the single, definitive source of information about your
data. It contains the essential fields and behaviors of the data youre
querying.
ObjectType
==========
A Graphene *ObjectType* is the building block used to define the relationship between **Fields** in your **Schema** and how their data is retrieved.
The basics:
- Each ObjectType is a Python class that inherits from
``graphene.ObjectType``.
- Each ObjectType is a Python class that inherits from ``graphene.ObjectType``.
- Each attribute of the ObjectType represents a ``Field``.
- Each ``Field`` has a :ref:`resolver method<Resolvers>` to fetch data (or :ref:`DefaultResolver`).
Quick example
-------------
@ -18,19 +18,17 @@ This example model defines a Person, with a first and a last name:
.. code:: python
import graphene
from graphene import ObjectType, String
class Person(graphene.ObjectType):
first_name = graphene.String()
last_name = graphene.String()
full_name = graphene.String()
class Person(ObjectType):
first_name = String()
last_name = String()
full_name = String()
def resolve_full_name(self, args, context, info):
return '{} {}'.format(self.first_name, self.last_name)
def resolve_full_name(parent, info):
return f"{parent.first_name} {parent.last_name}"
**first\_name** and **last\_name** are fields of the ObjectType. Each
field is specified as a class attribute, and each attribute maps to a
Field.
This *ObjectType* defines the field **first\_name**, **last\_name**, and **full\_name**. Each field is specified as a class attribute, and each attribute maps to a Field. Data is fetched by our ``resolve_full_name`` :ref:`resolver method<Resolvers>` for ``full_name`` field and the :ref:`DefaultResolver` for other fields.
The above ``Person`` ObjectType has the following schema representation:
@ -42,61 +40,331 @@ The above ``Person`` ObjectType has the following schema representation:
fullName: String
}
.. _Resolvers:
Resolvers
---------
A resolver is a method that resolves certain fields within a
``ObjectType``. If not specififed otherwise, the resolver of a
field is the ``resolve_{field_name}`` method on the ``ObjectType``.
A **Resolver** is a method that helps us answer **Queries** by fetching data for a **Field** in our **Schema**.
By default resolvers take the arguments ``args``, ``context`` and ``info``.
Resolvers are lazily executed, so if a field is not included in a query, its resolver will not be executed.
NOTE: The resolvers on a ``ObjectType`` are always treated as ``staticmethod``s,
so the first argument to the resolver method ``self`` (or ``root``) need
not be an actual instance of the ``ObjectType``.
Each field on an *ObjectType* in Graphene should have a corresponding resolver method to fetch data. This resolver method should match the field name. For example, in the ``Person`` type above, the ``full_name`` field is resolved by the method ``resolve_full_name``.
Each resolver method takes the parameters:
Quick example
~~~~~~~~~~~~~
* :ref:`ResolverParamParent` for the value object use to resolve most fields
* :ref:`ResolverParamInfo` for query and schema meta information and per-request context
* :ref:`ResolverParamGraphQLArguments` as defined on the **Field**.
This example model defines a ``Query`` type, which has a reverse field
that reverses the given ``word`` argument using the ``resolve_reverse``
method in the class.
.. _ResolverArguments:
Resolver Parameters
~~~~~~~~~~~~~~~~~~~
.. _ResolverParamParent:
Parent Value Object (*parent*)
******************************
This parameter is typically used to derive the values for most fields on an *ObjectType*.
The first parameter of a resolver method (*parent*) is the value object returned from the resolver of the parent field. If there is no parent field, such as a root Query field, then the value for *parent* is set to the ``root_value`` configured while executing the query (default ``None``). See :ref:`SchemaExecute` for more details on executing queries.
Resolver example
^^^^^^^^^^^^^^^^
If we have a schema with Person type and one field on the root query.
.. code:: python
import graphene
from graphene import ObjectType, String, Field
class Query(graphene.ObjectType):
reverse = graphene.String(word=graphene.String())
def get_human(name):
first_name, last_name = name.split()
return Person(first_name, last_name)
def resolve_reverse(self, args, context, info):
word = args.get('word')
return word[::-1]
class Person(ObjectType):
full_name = String()
def resolve_full_name(parent, info):
return f"{parent.first_name} {parent.last_name}"
class Query(ObjectType):
me = Field(Person)
def resolve_me(parent, info):
# returns an object that represents a Person
return get_human(name="Luke Skywalker")
When we execute a query against that schema.
.. code:: python
schema = Schema(query=Query)
query_string = "{ me { fullName } }"
result = schema.execute(query_string)
assert result.data["me"] == {"fullName": "Luke Skywalker"}
Then we go through the following steps to resolve this query:
* ``parent`` is set with the root_value from query execution (None).
* ``Query.resolve_me`` called with ``parent`` None which returns a value object ``Person("Luke", "Skywalker")``.
* This value object is then used as ``parent`` while calling ``Person.resolve_full_name`` to resolve the scalar String value "Luke Skywalker".
* The scalar value is serialized and sent back in the query response.
Each resolver returns the next :ref:`ResolverParamParent` to be used in executing the following resolver in the chain. If the Field is a Scalar type, that value will be serialized and sent in the **Response**. Otherwise, while resolving Compound types like *ObjectType*, the value be passed forward as the next :ref:`ResolverParamParent`.
Naming convention
^^^^^^^^^^^^^^^^^
This :ref:`ResolverParamParent` is sometimes named ``obj``, ``parent``, or ``source`` in other GraphQL documentation. It can also be named after the value object being resolved (ex. ``root`` for a root Query or Mutation, and ``person`` for a Person value object). Sometimes this argument will be named ``self`` in Graphene code, but this can be misleading due to :ref:`ResolverImplicitStaticMethod` while executing queries in Graphene.
.. _ResolverParamInfo:
GraphQL Execution Info (*info*)
*******************************
The second parameter provides two things:
* reference to meta information about the execution of the current GraphQL Query (fields, schema, parsed query, etc.)
* access to per-request ``context`` which can be used to store user authentication, data loader instances or anything else useful for resolving the query.
Only context will be required for most applications. See :ref:`SchemaExecuteContext` for more information about setting context.
.. _ResolverParamGraphQLArguments:
GraphQL Arguments (*\*\*kwargs*)
********************************
Any arguments that a field defines gets passed to the resolver function as
keyword arguments. For example:
.. code:: python
from graphene import ObjectType, Field, String
class Query(ObjectType):
human_by_name = Field(Human, name=String(required=True))
def resolve_human_by_name(parent, info, name):
return get_human(name=name)
You can then execute the following query:
.. code::
query {
humanByName(name: "Luke Skywalker") {
firstName
lastName
}
}
*Note:* There are several arguments to a field that are "reserved" by Graphene
(see :ref:`fields-mounted-types`).
You can still define an argument that clashes with one of these fields by using
the ``args`` parameter like so:
.. code:: python
from graphene import ObjectType, Field, String
class Query(ObjectType):
answer = String(args={'description': String()})
def resolve_answer(parent, info, description):
return description
Convenience Features of Graphene Resolvers
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. _ResolverImplicitStaticMethod:
Implicit staticmethod
*********************
One surprising feature of Graphene is that all resolver methods are treated implicitly as staticmethods. This means that, unlike other methods in Python, the first argument of a resolver is *never* ``self`` while it is being executed by Graphene. Instead, the first argument is always :ref:`ResolverParamParent`. In practice, this is very convenient as, in GraphQL, we are almost always more concerned with the using the parent value object to resolve queries than attributes on the Python object itself.
The two resolvers in this example are effectively the same.
.. code:: python
from graphene import ObjectType, String
class Person(ObjectType):
first_name = String()
last_name = String()
@staticmethod
def resolve_first_name(parent, info):
'''
Decorating a Python method with `staticmethod` ensures that `self` will not be provided as an
argument. However, Graphene does not need this decorator for this behavior.
'''
return parent.first_name
def resolve_last_name(parent, info):
'''
Normally the first argument for this method would be `self`, but Graphene executes this as
a staticmethod implicitly.
'''
return parent.last_name
# ...
If you prefer your code to be more explicit, feel free to use ``@staticmethod`` decorators. Otherwise, your code may be cleaner without them!
.. _DefaultResolver:
Default Resolver
****************
If a resolver method is not defined for a **Field** attribute on our *ObjectType*, Graphene supplies a default resolver.
If the :ref:`ResolverParamParent` is a dictionary, the resolver will look for a dictionary key matching the field name. Otherwise, the resolver will get the attribute from the parent value object matching the field name.
.. code:: python
from collections import namedtuple
from graphene import ObjectType, String, Field, Schema
PersonValueObject = namedtuple("Person", ["first_name", "last_name"])
class Person(ObjectType):
first_name = String()
last_name = String()
class Query(ObjectType):
me = Field(Person)
my_best_friend = Field(Person)
def resolve_me(parent, info):
# always pass an object for `me` field
return PersonValueObject(first_name="Luke", last_name="Skywalker")
def resolve_my_best_friend(parent, info):
# always pass a dictionary for `my_best_fiend_field`
return {"first_name": "R2", "last_name": "D2"}
schema = Schema(query=Query)
result = schema.execute('''
{
me { firstName lastName }
myBestFriend { firstName lastName }
}
''')
# With default resolvers we can resolve attributes from an object..
assert result.data["me"] == {"firstName": "Luke", "lastName": "Skywalker"}
# With default resolvers, we can also resolve keys from a dictionary..
assert result.data["myBestFriend"] == {"firstName": "R2", "lastName": "D2"}
Advanced
~~~~~~~~
GraphQL Argument defaults
*************************
If you define an argument for a field that is not required (and in a query
execution it is not provided as an argument) it will not be passed to the
resolver function at all. This is so that the developer can differentiate
between a ``undefined`` value for an argument and an explicit ``null`` value.
For example, given this schema:
.. code:: python
from graphene import ObjectType, String
class Query(ObjectType):
hello = String(required=True, name=String())
def resolve_hello(parent, info, name):
return name if name else 'World'
And this query:
.. code::
query {
hello
}
An error will be thrown:
.. code::
TypeError: resolve_hello() missing 1 required positional argument: 'name'
You can fix this error in several ways. Either by combining all keyword arguments
into a dict:
.. code:: python
from graphene import ObjectType, String
class Query(ObjectType):
hello = String(required=True, name=String())
def resolve_hello(parent, info, **kwargs):
name = kwargs.get('name', 'World')
return f'Hello, {name}!'
Or by setting a default value for the keyword argument:
.. code:: python
from graphene import ObjectType, String
class Query(ObjectType):
hello = String(required=True, name=String())
def resolve_hello(parent, info, name='World'):
return f'Hello, {name}!'
One can also set a default value for an Argument in the GraphQL schema itself using Graphene!
.. code:: python
from graphene import ObjectType, String
class Query(ObjectType):
hello = String(
required=True,
name=String(default_value='World')
)
def resolve_hello(parent, info, name):
return f'Hello, {name}!'
Resolvers outside the class
~~~~~~~~~~~~~~~~~~~~~~~~~~~
***************************
A field can use a custom resolver from outside the class:
.. code:: python
import graphene
from graphene import ObjectType, String
def reverse(root, args, context, info):
word = args.get('word')
return word[::-1]
def resolve_full_name(person, info):
return f"{person.first_name} {person.last_name}"
class Query(graphene.ObjectType):
reverse = graphene.String(word=graphene.String(), resolver=reverse)
class Person(ObjectType):
first_name = String()
last_name = String()
full_name = String(resolver=resolve_full_name)
Instances as data containers
----------------------------
Instances as value objects
**************************
Graphene ``ObjectType``\ s can act as containers too. So with the
previous example you could do:
Graphene ``ObjectType``\ s can act as value objects too. So with the
previous example you could use ``Person`` to capture data for each of the *ObjectType*'s fields.
.. code:: python
@ -105,4 +373,63 @@ previous example you could do:
peter.first_name # prints "Peter"
peter.last_name # prints "Griffin"
Field camelcasing
*****************
Graphene automatically camelcases fields on *ObjectType* from ``field_name`` to ``fieldName`` to conform with GraphQL standards. See :ref:`SchemaAutoCamelCase` for more information.
*ObjectType* Configuration - Meta class
---------------------------------------
Graphene uses a Meta inner class on *ObjectType* to set different options.
GraphQL type name
~~~~~~~~~~~~~~~~~
By default the type name in the GraphQL schema will be the same as the class name
that defines the ``ObjectType``. This can be changed by setting the ``name``
property on the ``Meta`` class:
.. code:: python
from graphene import ObjectType
class MyGraphQlSong(ObjectType):
class Meta:
name = 'Song'
GraphQL Description
~~~~~~~~~~~~~~~~~~~
The schema description of an *ObjectType* can be set as a docstring on the Python object or on the Meta inner class.
.. code:: python
from graphene import ObjectType
class MyGraphQlSong(ObjectType):
''' We can set the schema description for an Object Type here on a docstring '''
class Meta:
description = 'But if we set the description in Meta, this value is used instead'
Interfaces & Possible Types
~~~~~~~~~~~~~~~~~~~~~~~~~~~
Setting ``interfaces`` in Meta inner class specifies the GraphQL Interfaces that this Object implements.
Providing ``possible_types`` helps Graphene resolve ambiguous types such as interfaces or Unions.
See :ref:`Interfaces` for more information.
.. code:: python
from graphene import ObjectType, Node
Song = namedtuple('Song', ('title', 'artist'))
class MyGraphQlSong(ObjectType):
class Meta:
interfaces = (Node, )
possible_types = (Song, )
.. _Interface: /docs/interfaces/

View File

@ -1,19 +1,253 @@
.. _Scalars:
Scalars
=======
Graphene defines the following base Scalar Types:
Scalar types represent concrete values at the leaves of a query. There are
several built in types that Graphene provides out of the box which represent common
values in Python. You can also create your own Scalar types to better express
values that you might have in your data model.
- ``graphene.String``
- ``graphene.Int``
- ``graphene.Float``
- ``graphene.Boolean``
- ``graphene.ID``
All Scalar types accept the following arguments. All are optional:
Graphene also provides custom scalars for Dates, Times, and JSON:
``name``: *string*
Override the name of the Field.
``description``: *string*
A description of the type to show in the GraphiQL browser.
``required``: *boolean*
If ``True``, the server will enforce a value for this field. See `NonNull <../list-and-nonnull.html#nonnull>`_. Default is ``False``.
``deprecation_reason``: *string*
Provide a deprecation reason for the Field.
``default_value``: *any*
Provide a default value for the Field.
Built in scalars
----------------
Graphene defines the following base Scalar Types that match the default `GraphQL types <https://graphql.org/learn/schema/#scalar-types>`_:
``graphene.String``
^^^^^^^^^^^^^^^^^^^
Represents textual data, represented as UTF-8
character sequences. The String type is most often used by GraphQL to
represent free-form human-readable text.
``graphene.Int``
^^^^^^^^^^^^^^^^
Represents non-fractional signed whole numeric
values. Int is a signed 32bit integer per the
`GraphQL spec <https://facebook.github.io/graphql/June2018/#sec-Int>`_
``graphene.Float``
^^^^^^^^^^^^^^^^^^
Represents signed double-precision fractional
values as specified by
`IEEE 754 <http://en.wikipedia.org/wiki/IEEE_floating_point>`_.
``graphene.Boolean``
^^^^^^^^^^^^^^^^^^^^
Represents `true` or `false`.
``graphene.ID``
^^^^^^^^^^^^^^^
Represents a unique identifier, often used to
refetch an object or as key for a cache. The ID type appears in a JSON
response as a String; however, it is not intended to be human-readable.
When expected as an input type, any string (such as `"4"`) or integer
(such as `4`) input value will be accepted as an ID.
----
Graphene also provides custom scalars for common values:
``graphene.Date``
^^^^^^^^^^^^^^^^^
Represents a Date value as specified by `iso8601 <https://en.wikipedia.org/wiki/ISO_8601>`_.
.. code:: python
import datetime
from graphene import Schema, ObjectType, Date
class Query(ObjectType):
one_week_from = Date(required=True, date_input=Date(required=True))
def resolve_one_week_from(root, info, date_input):
assert date_input == datetime.date(2006, 1, 2)
return date_input + datetime.timedelta(weeks=1)
schema = Schema(query=Query)
results = schema.execute("""
query {
oneWeekFrom(dateInput: "2006-01-02")
}
""")
assert results.data == {"oneWeekFrom": "2006-01-09"}
``graphene.DateTime``
^^^^^^^^^^^^^^^^^^^^^
Represents a DateTime value as specified by `iso8601 <https://en.wikipedia.org/wiki/ISO_8601>`_.
.. code:: python
import datetime
from graphene import Schema, ObjectType, DateTime
class Query(ObjectType):
one_hour_from = DateTime(required=True, datetime_input=DateTime(required=True))
def resolve_one_hour_from(root, info, datetime_input):
assert datetime_input == datetime.datetime(2006, 1, 2, 15, 4, 5)
return datetime_input + datetime.timedelta(hours=1)
schema = Schema(query=Query)
results = schema.execute("""
query {
oneHourFrom(datetimeInput: "2006-01-02T15:04:05")
}
""")
assert results.data == {"oneHourFrom": "2006-01-02T16:04:05"}
``graphene.Time``
^^^^^^^^^^^^^^^^^
Represents a Time value as specified by `iso8601 <https://en.wikipedia.org/wiki/ISO_8601>`_.
.. code:: python
import datetime
from graphene import Schema, ObjectType, Time
class Query(ObjectType):
one_hour_from = Time(required=True, time_input=Time(required=True))
def resolve_one_hour_from(root, info, time_input):
assert time_input == datetime.time(15, 4, 5)
tmp_time_input = datetime.datetime.combine(datetime.date(1, 1, 1), time_input)
return (tmp_time_input + datetime.timedelta(hours=1)).time()
schema = Schema(query=Query)
results = schema.execute("""
query {
oneHourFrom(timeInput: "15:04:05")
}
""")
assert results.data == {"oneHourFrom": "16:04:05"}
``graphene.Decimal``
^^^^^^^^^^^^^^^^^^^^
Represents a Python Decimal value.
.. code:: python
import decimal
from graphene import Schema, ObjectType, Decimal
class Query(ObjectType):
add_one_to = Decimal(required=True, decimal_input=Decimal(required=True))
def resolve_add_one_to(root, info, decimal_input):
assert decimal_input == decimal.Decimal("10.50")
return decimal_input + decimal.Decimal("1")
schema = Schema(query=Query)
results = schema.execute("""
query {
addOneTo(decimalInput: "10.50")
}
""")
assert results.data == {"addOneTo": "11.50"}
``graphene.JSONString``
^^^^^^^^^^^^^^^^^^^^^^^
Represents a JSON string.
.. code:: python
from graphene import Schema, ObjectType, JSONString, String
class Query(ObjectType):
update_json_key = JSONString(
required=True,
json_input=JSONString(required=True),
key=String(required=True),
value=String(required=True)
)
def resolve_update_json_key(root, info, json_input, key, value):
assert json_input == {"name": "Jane"}
json_input[key] = value
return json_input
schema = Schema(query=Query)
results = schema.execute("""
query {
updateJsonKey(jsonInput: "{\\"name\\": \\"Jane\\"}", key: "name", value: "Beth")
}
""")
assert results.data == {"updateJsonKey": "{\"name\": \"Beth\"}"}
``graphene.Base64``
^^^^^^^^^^^^^^^^^^^
Represents a Base64 encoded string.
.. code:: python
from graphene import Schema, ObjectType, Base64
class Query(ObjectType):
increment_encoded_id = Base64(
required=True,
base64_input=Base64(required=True),
)
def resolve_increment_encoded_id(root, info, base64_input):
assert base64_input == "4"
return int(base64_input) + 1
schema = Schema(query=Query)
results = schema.execute("""
query {
incrementEncodedId(base64Input: "NA==")
}
""")
assert results.data == {"incrementEncodedId": "NQ=="}
- ``graphene.types.datetime.DateTime``
- ``graphene.types.datetime.Time``
- ``graphene.types.json.JSONString``
Custom scalars
@ -36,8 +270,8 @@ The following is an example for creating a DateTime scalar:
return dt.isoformat()
@staticmethod
def parse_literal(node):
if isinstance(node, ast.StringValue):
def parse_literal(node, _variables=None):
if isinstance(node, ast.StringValueNode):
return datetime.datetime.strptime(
node.value, "%Y-%m-%dT%H:%M:%S.%f")

View File

@ -1,16 +1,42 @@
Schema
======
A Schema is created by supplying the root types of each type of operation, query and mutation (optional).
A schema definition is then supplied to the validator and executor.
A GraphQL **Schema** defines the types and relationships between **Fields** in your API.
A Schema is created by supplying the root :ref:`ObjectType` of each operation, query (mandatory), mutation and subscription.
Schema will collect all type definitions related to the root operations and then supply them to the validator and executor.
.. code:: python
my_schema = Schema(
query=MyRootQuery,
mutation=MyRootMutation,
subscription=MyRootSubscription
)
A Root Query is just a special :ref:`ObjectType` that defines the fields that are the entrypoint for your API. Root Mutation and Root Subscription are similar to Root Query, but for different operation types:
* Query fetches data
* Mutation changes data and retrieves the changes
* Subscription sends changes to clients in real-time
Review the `GraphQL documentation on Schema`_ for a brief overview of fields, schema and operations.
.. _GraphQL documentation on Schema: https://graphql.org/learn/schema/
Querying
--------
To query a schema, call the ``execute`` method on it. See :ref:`SchemaExecute` for more details.
.. code:: python
query_string = 'query whoIsMyBestFriend { myBestFriend { lastName } }'
my_schema.execute(query_string)
Types
-----
@ -18,7 +44,7 @@ There are some cases where the schema cannot access all of the types that we pla
For example, when a field returns an ``Interface``, the schema doesn't know about any of the
implementations.
In this case, we need to use the ``types`` argument when creating the Schema.
In this case, we need to use the ``types`` argument when creating the Schema:
.. code:: python
@ -28,26 +54,16 @@ In this case, we need to use the ``types`` argument when creating the Schema.
types=[SomeExtraObjectType, ]
)
.. _SchemaAutoCamelCase:
Querying
--------
To query a schema, call the ``execute`` method on it.
.. code:: python
my_schema.execute('{ lastName }')
Auto CamelCase field names
Auto camelCase field names
--------------------------
By default all field and argument names (that are not
explicitly set with the ``name`` arg) will be converted from
``snake_case`` to ``camelCase`` (as the API is usually being consumed by a js/mobile client)
For example with the ObjectType
For example with the ObjectType the ``last_name`` field name is converted to ``lastName``:
.. code:: python
@ -55,12 +71,10 @@ For example with the ObjectType
last_name = graphene.String()
other_name = graphene.String(name='_other_Name')
the ``last_name`` field name is converted to ``lastName``.
In case you don't want to apply this transformation, provide a ``name`` argument to the field constructor.
``other_name`` converts to ``_other_Name`` (without further transformations).
Your query should look like
Your query should look like:
.. code::
@ -70,7 +84,7 @@ Your query should look like
}
To disable this behavior, set the ``auto_camelcase`` to ``False`` upon schema instantiation.
To disable this behavior, set the ``auto_camelcase`` to ``False`` upon schema instantiation:
.. code:: python

63
docs/types/unions.rst Normal file
View File

@ -0,0 +1,63 @@
Unions
======
Union types are very similar to interfaces, but they don't get
to specify any common fields between the types.
The basics:
- Each Union is a Python class that inherits from ``graphene.Union``.
- Unions don't have any fields on it, just links to the possible ObjectTypes.
Quick example
-------------
This example model defines several ObjectTypes with their own fields.
``SearchResult`` is the implementation of ``Union`` of this object types.
.. code:: python
import graphene
class Human(graphene.ObjectType):
name = graphene.String()
born_in = graphene.String()
class Droid(graphene.ObjectType):
name = graphene.String()
primary_function = graphene.String()
class Starship(graphene.ObjectType):
name = graphene.String()
length = graphene.Int()
class SearchResult(graphene.Union):
class Meta:
types = (Human, Droid, Starship)
Wherever we return a SearchResult type in our schema, we might get a Human, a Droid, or a Starship.
Note that members of a union type need to be concrete object types;
you can't create a union type out of interfaces or other unions.
The above types have the following representation in a schema:
.. code::
type Droid {
name: String
primaryFunction: String
}
type Human {
name: String
bornIn: String
}
type Ship {
name: String
length: Int
}
union SearchResult = Human | Droid | Starship

View File

@ -5,39 +5,65 @@ class GeoInput(graphene.InputObjectType):
lat = graphene.Float(required=True)
lng = graphene.Float(required=True)
@property
def latlng(self):
return f"({self.lat},{self.lng})"
class Address(graphene.ObjectType):
latlng = graphene.String()
class Query(graphene.ObjectType):
address = graphene.Field(Address, geo=GeoInput())
address = graphene.Field(Address, geo=GeoInput(required=True))
def resolve_address(self, args, context, info):
geo = args.get('geo')
return Address(latlng="({},{})".format(geo.get('lat'), geo.get('lng')))
def resolve_address(root, info, geo):
return Address(latlng=geo.latlng)
schema = graphene.Schema(query=Query)
query = '''
class CreateAddress(graphene.Mutation):
class Arguments:
geo = GeoInput(required=True)
Output = Address
def mutate(root, info, geo):
return Address(latlng=geo.latlng)
class Mutation(graphene.ObjectType):
create_address = CreateAddress.Field()
schema = graphene.Schema(query=Query, mutation=Mutation)
query = """
query something{
address(geo: {lat:32.2, lng:12}) {
latlng
}
}
'''
"""
mutation = """
mutation addAddress{
createAddress(geo: {lat:32.2, lng:12}) {
latlng
}
}
"""
def test_query():
result = schema.execute(query)
assert not result.errors
assert result.data == {
'address': {
'latlng': "(32.2,12.0)",
}
}
assert result.data == {"address": {"latlng": "(32.2,12.0)"}}
if __name__ == '__main__':
def test_mutation():
result = schema.execute(mutation)
assert not result.errors
assert result.data == {"createAddress": {"latlng": "(32.2,12.0)"}}
if __name__ == "__main__":
result = schema.execute(query)
print(result.data['address']['latlng'])
print(result.data["address"]["latlng"])

View File

@ -9,31 +9,27 @@ class User(graphene.ObjectType):
class Query(graphene.ObjectType):
me = graphene.Field(User)
def resolve_me(self, args, context, info):
return context['user']
def resolve_me(root, info):
return info.context["user"]
schema = graphene.Schema(query=Query)
query = '''
query = """
query something{
me {
id
name
}
}
'''
"""
def test_query():
result = schema.execute(query, context_value={'user': User(id='1', name='Syrus')})
result = schema.execute(query, context={"user": User(id="1", name="Syrus")})
assert not result.errors
assert result.data == {
'me': {
'id': '1',
'name': 'Syrus',
}
}
assert result.data == {"me": {"id": "1", "name": "Syrus"}}
if __name__ == '__main__':
result = schema.execute(query, context_value={'user': User(id='X', name='Console')})
print(result.data['me'])
if __name__ == "__main__":
result = schema.execute(query, context={"user": User(id="X", name="Console")})
print(result.data["me"])

View File

@ -8,14 +8,14 @@ class Patron(graphene.ObjectType):
class Query(graphene.ObjectType):
patron = graphene.Field(Patron)
def resolve_patron(self, args, context, info):
return Patron(id=1, name='Syrus', age=27)
def resolve_patron(root, info):
return Patron(id=1, name="Syrus", age=27)
schema = graphene.Schema(query=Query)
query = '''
query = """
query something{
patron {
id
@ -23,21 +23,15 @@ query = '''
age
}
}
'''
"""
def test_query():
result = schema.execute(query)
assert not result.errors
assert result.data == {
'patron': {
'id': '1',
'name': 'Syrus',
'age': 27,
}
}
assert result.data == {"patron": {"id": "1", "name": "Syrus", "age": 27}}
if __name__ == '__main__':
if __name__ == "__main__":
result = schema.execute(query)
print(result.data['patron'])
print(result.data["patron"])

View File

@ -4,75 +4,73 @@ droid_data = {}
def setup():
from .schema import Human, Droid
global human_data, droid_data
luke = Human(
id='1000',
name='Luke Skywalker',
friends=['1002', '1003', '2000', '2001'],
id="1000",
name="Luke Skywalker",
friends=["1002", "1003", "2000", "2001"],
appears_in=[4, 5, 6],
home_planet='Tatooine',
home_planet="Tatooine",
)
vader = Human(
id='1001',
name='Darth Vader',
friends=['1004'],
id="1001",
name="Darth Vader",
friends=["1004"],
appears_in=[4, 5, 6],
home_planet='Tatooine',
home_planet="Tatooine",
)
han = Human(
id='1002',
name='Han Solo',
friends=['1000', '1003', '2001'],
id="1002",
name="Han Solo",
friends=["1000", "1003", "2001"],
appears_in=[4, 5, 6],
home_planet=None,
)
leia = Human(
id='1003',
name='Leia Organa',
friends=['1000', '1002', '2000', '2001'],
id="1003",
name="Leia Organa",
friends=["1000", "1002", "2000", "2001"],
appears_in=[4, 5, 6],
home_planet='Alderaan',
home_planet="Alderaan",
)
tarkin = Human(
id='1004',
name='Wilhuff Tarkin',
friends=['1001'],
id="1004",
name="Wilhuff Tarkin",
friends=["1001"],
appears_in=[4],
home_planet=None,
)
human_data = {
'1000': luke,
'1001': vader,
'1002': han,
'1003': leia,
'1004': tarkin,
"1000": luke,
"1001": vader,
"1002": han,
"1003": leia,
"1004": tarkin,
}
c3po = Droid(
id='2000',
name='C-3PO',
friends=['1000', '1002', '1003', '2001'],
id="2000",
name="C-3PO",
friends=["1000", "1002", "1003", "2001"],
appears_in=[4, 5, 6],
primary_function='Protocol',
primary_function="Protocol",
)
r2d2 = Droid(
id='2001',
name='R2-D2',
friends=['1000', '1002', '1003'],
id="2001",
name="R2-D2",
friends=["1000", "1002", "1003"],
appears_in=[4, 5, 6],
primary_function='Astromech',
primary_function="Astromech",
)
droid_data = {
'2000': c3po,
'2001': r2d2,
}
droid_data = {"2000": c3po, "2001": r2d2}
def get_character(id):
@ -85,8 +83,8 @@ def get_friends(character):
def get_hero(episode):
if episode == 5:
return human_data['1000']
return droid_data['2001']
return human_data["1000"]
return droid_data["2001"]
def get_human(id):

View File

@ -1,5 +1,4 @@
import graphene
from graphene import resolve_only_args
from .data import get_character, get_droid, get_hero, get_human
@ -16,46 +15,37 @@ class Character(graphene.Interface):
friends = graphene.List(lambda: Character)
appears_in = graphene.List(Episode)
def resolve_friends(self, args, *_):
def resolve_friends(self, info):
# The character friends is a list of strings
return [get_character(f) for f in self.friends]
class Human(graphene.ObjectType):
class Meta:
interfaces = (Character,)
home_planet = graphene.String()
class Droid(graphene.ObjectType):
class Meta:
interfaces = (Character,)
primary_function = graphene.String()
class Query(graphene.ObjectType):
hero = graphene.Field(Character,
episode=Episode()
)
human = graphene.Field(Human,
id=graphene.String()
)
droid = graphene.Field(Droid,
id=graphene.String()
)
hero = graphene.Field(Character, episode=Episode())
human = graphene.Field(Human, id=graphene.String())
droid = graphene.Field(Droid, id=graphene.String())
@resolve_only_args
def resolve_hero(self, episode=None):
def resolve_hero(root, info, episode=None):
return get_hero(episode)
@resolve_only_args
def resolve_human(self, id):
def resolve_human(root, info, id):
return get_human(id)
@resolve_only_args
def resolve_droid(self, id):
def resolve_droid(root, info, id):
return get_droid(id)

View File

@ -1,202 +0,0 @@
# -*- coding: utf-8 -*-
# snapshottest: v1 - https://goo.gl/zC4yUc
from __future__ import unicode_literals
from snapshottest import Snapshot
snapshots = Snapshot()
snapshots['test_hero_name_query 1'] = {
'data': {
'hero': {
'name': 'R2-D2'
}
}
}
snapshots['test_hero_name_and_friends_query 1'] = {
'data': {
'hero': {
'id': '2001',
'name': 'R2-D2',
'friends': [
{
'name': 'Luke Skywalker'
},
{
'name': 'Han Solo'
},
{
'name': 'Leia Organa'
}
]
}
}
}
snapshots['test_nested_query 1'] = {
'data': {
'hero': {
'name': 'R2-D2',
'friends': [
{
'name': 'Luke Skywalker',
'appearsIn': [
'NEWHOPE',
'EMPIRE',
'JEDI'
],
'friends': [
{
'name': 'Han Solo'
},
{
'name': 'Leia Organa'
},
{
'name': 'C-3PO'
},
{
'name': 'R2-D2'
}
]
},
{
'name': 'Han Solo',
'appearsIn': [
'NEWHOPE',
'EMPIRE',
'JEDI'
],
'friends': [
{
'name': 'Luke Skywalker'
},
{
'name': 'Leia Organa'
},
{
'name': 'R2-D2'
}
]
},
{
'name': 'Leia Organa',
'appearsIn': [
'NEWHOPE',
'EMPIRE',
'JEDI'
],
'friends': [
{
'name': 'Luke Skywalker'
},
{
'name': 'Han Solo'
},
{
'name': 'C-3PO'
},
{
'name': 'R2-D2'
}
]
}
]
}
}
}
snapshots['test_fetch_luke_query 1'] = {
'data': {
'human': {
'name': 'Luke Skywalker'
}
}
}
snapshots['test_fetch_some_id_query 1'] = {
'data': {
'human': {
'name': 'Luke Skywalker'
}
}
}
snapshots['test_fetch_some_id_query2 1'] = {
'data': {
'human': {
'name': 'Han Solo'
}
}
}
snapshots['test_invalid_id_query 1'] = {
'data': {
'human': None
}
}
snapshots['test_fetch_luke_aliased 1'] = {
'data': {
'luke': {
'name': 'Luke Skywalker'
}
}
}
snapshots['test_fetch_luke_and_leia_aliased 1'] = {
'data': {
'luke': {
'name': 'Luke Skywalker'
},
'leia': {
'name': 'Leia Organa'
}
}
}
snapshots['test_duplicate_fields 1'] = {
'data': {
'luke': {
'name': 'Luke Skywalker',
'homePlanet': 'Tatooine'
},
'leia': {
'name': 'Leia Organa',
'homePlanet': 'Alderaan'
}
}
}
snapshots['test_use_fragment 1'] = {
'data': {
'luke': {
'name': 'Luke Skywalker',
'homePlanet': 'Tatooine'
},
'leia': {
'name': 'Leia Organa',
'homePlanet': 'Alderaan'
}
}
}
snapshots['test_check_type_of_r2 1'] = {
'data': {
'hero': {
'__typename': 'Droid',
'name': 'R2-D2'
}
}
}
snapshots['test_check_type_of_luke 1'] = {
'data': {
'hero': {
'__typename': 'Human',
'name': 'Luke Skywalker'
}
}
}

View File

@ -1,4 +1,5 @@
from graphene.test import Client
from ..data import setup
from ..schema import schema
@ -6,20 +7,20 @@ setup()
client = Client(schema)
def test_hero_name_query(snapshot):
query = '''
def test_hero_name_query():
result = client.execute("""
query HeroNameQuery {
hero {
name
}
}
'''
snapshot.assert_match(client.execute(query))
""")
assert result == {"data": {"hero": {"name": "R2-D2"}}}
def test_hero_name_and_friends_query(snapshot):
query = '''
def test_hero_name_and_friends_query():
result = client.execute("""
query HeroNameAndFriendsQuery {
hero {
id
@ -29,12 +30,24 @@ def test_hero_name_and_friends_query(snapshot):
}
}
}
'''
snapshot.assert_match(client.execute(query))
""")
assert result == {
"data": {
"hero": {
"id": "2001",
"name": "R2-D2",
"friends": [
{"name": "Luke Skywalker"},
{"name": "Han Solo"},
{"name": "Leia Organa"},
],
}
}
}
def test_nested_query(snapshot):
query = '''
def test_nested_query():
result = client.execute("""
query NestedQuery {
hero {
name
@ -47,76 +60,113 @@ def test_nested_query(snapshot):
}
}
}
'''
snapshot.assert_match(client.execute(query))
""")
assert result == {
"data": {
"hero": {
"name": "R2-D2",
"friends": [
{
"name": "Luke Skywalker",
"appearsIn": ["NEWHOPE", "EMPIRE", "JEDI"],
"friends": [
{"name": "Han Solo"},
{"name": "Leia Organa"},
{"name": "C-3PO"},
{"name": "R2-D2"},
],
},
{
"name": "Han Solo",
"appearsIn": ["NEWHOPE", "EMPIRE", "JEDI"],
"friends": [
{"name": "Luke Skywalker"},
{"name": "Leia Organa"},
{"name": "R2-D2"},
],
},
{
"name": "Leia Organa",
"appearsIn": ["NEWHOPE", "EMPIRE", "JEDI"],
"friends": [
{"name": "Luke Skywalker"},
{"name": "Han Solo"},
{"name": "C-3PO"},
{"name": "R2-D2"},
],
},
],
}
}
}
def test_fetch_luke_query(snapshot):
query = '''
def test_fetch_luke_query():
result = client.execute("""
query FetchLukeQuery {
human(id: "1000") {
name
}
}
'''
snapshot.assert_match(client.execute(query))
""")
assert result == {"data": {"human": {"name": "Luke Skywalker"}}}
def test_fetch_some_id_query(snapshot):
query = '''
def test_fetch_some_id_query():
result = client.execute(
"""
query FetchSomeIDQuery($someId: String!) {
human(id: $someId) {
name
}
}
'''
params = {
'someId': '1000',
}
snapshot.assert_match(client.execute(query, variable_values=params))
""",
variables={"someId": "1000"},
)
assert result == {"data": {"human": {"name": "Luke Skywalker"}}}
def test_fetch_some_id_query2(snapshot):
query = '''
def test_fetch_some_id_query2():
result = client.execute(
"""
query FetchSomeIDQuery($someId: String!) {
human(id: $someId) {
name
}
}
'''
params = {
'someId': '1002',
}
snapshot.assert_match(client.execute(query, variable_values=params))
""",
variables={"someId": "1002"},
)
assert result == {"data": {"human": {"name": "Han Solo"}}}
def test_invalid_id_query(snapshot):
query = '''
def test_invalid_id_query():
result = client.execute(
"""
query humanQuery($id: String!) {
human(id: $id) {
name
}
}
'''
params = {
'id': 'not a valid id',
}
snapshot.assert_match(client.execute(query, variable_values=params))
""",
variables={"id": "not a valid id"},
)
assert result == {"data": {"human": None}}
def test_fetch_luke_aliased(snapshot):
query = '''
def test_fetch_luke_aliased():
result = client.execute("""
query FetchLukeAliased {
luke: human(id: "1000") {
name
}
}
'''
snapshot.assert_match(client.execute(query))
""")
assert result == {"data": {"luke": {"name": "Luke Skywalker"}}}
def test_fetch_luke_and_leia_aliased(snapshot):
query = '''
def test_fetch_luke_and_leia_aliased():
result = client.execute("""
query FetchLukeAndLeiaAliased {
luke: human(id: "1000") {
name
@ -125,12 +175,14 @@ def test_fetch_luke_and_leia_aliased(snapshot):
name
}
}
'''
snapshot.assert_match(client.execute(query))
""")
assert result == {
"data": {"luke": {"name": "Luke Skywalker"}, "leia": {"name": "Leia Organa"}}
}
def test_duplicate_fields(snapshot):
query = '''
def test_duplicate_fields():
result = client.execute("""
query DuplicateFields {
luke: human(id: "1000") {
name
@ -141,12 +193,17 @@ def test_duplicate_fields(snapshot):
homePlanet
}
}
'''
snapshot.assert_match(client.execute(query))
""")
assert result == {
"data": {
"luke": {"name": "Luke Skywalker", "homePlanet": "Tatooine"},
"leia": {"name": "Leia Organa", "homePlanet": "Alderaan"},
}
}
def test_use_fragment(snapshot):
query = '''
def test_use_fragment():
result = client.execute("""
query UseFragment {
luke: human(id: "1000") {
...HumanFragment
@ -159,29 +216,36 @@ def test_use_fragment(snapshot):
name
homePlanet
}
'''
snapshot.assert_match(client.execute(query))
""")
assert result == {
"data": {
"luke": {"name": "Luke Skywalker", "homePlanet": "Tatooine"},
"leia": {"name": "Leia Organa", "homePlanet": "Alderaan"},
}
}
def test_check_type_of_r2(snapshot):
query = '''
def test_check_type_of_r2():
result = client.execute("""
query CheckTypeOfR2 {
hero {
__typename
name
}
}
'''
snapshot.assert_match(client.execute(query))
""")
assert result == {"data": {"hero": {"__typename": "Droid", "name": "R2-D2"}}}
def test_check_type_of_luke(snapshot):
query = '''
def test_check_type_of_luke():
result = client.execute("""
query CheckTypeOfLuke {
hero(episode: EMPIRE) {
__typename
name
}
}
'''
snapshot.assert_match(client.execute(query))
""")
assert result == {
"data": {"hero": {"__typename": "Human", "name": "Luke Skywalker"}}
}

View File

@ -5,101 +5,67 @@ def setup():
global data
from .schema import Ship, Faction
xwing = Ship(
id='1',
name='X-Wing',
)
ywing = Ship(
id='2',
name='Y-Wing',
)
xwing = Ship(id="1", name="X-Wing")
awing = Ship(
id='3',
name='A-Wing',
)
ywing = Ship(id="2", name="Y-Wing")
awing = Ship(id="3", name="A-Wing")
# Yeah, technically it's Corellian. But it flew in the service of the rebels,
# so for the purposes of this demo it's a rebel ship.
falcon = Ship(
id='4',
name='Millenium Falcon',
)
falcon = Ship(id="4", name="Millennium Falcon")
homeOne = Ship(
id='5',
name='Home One',
)
homeOne = Ship(id="5", name="Home One")
tieFighter = Ship(
id='6',
name='TIE Fighter',
)
tieFighter = Ship(id="6", name="TIE Fighter")
tieInterceptor = Ship(
id='7',
name='TIE Interceptor',
)
tieInterceptor = Ship(id="7", name="TIE Interceptor")
executor = Ship(
id='8',
name='Executor',
)
executor = Ship(id="8", name="Executor")
rebels = Faction(
id='1',
name='Alliance to Restore the Republic',
ships=['1', '2', '3', '4', '5']
id="1", name="Alliance to Restore the Republic", ships=["1", "2", "3", "4", "5"]
)
empire = Faction(
id='2',
name='Galactic Empire',
ships=['6', '7', '8']
)
empire = Faction(id="2", name="Galactic Empire", ships=["6", "7", "8"])
data = {
'Faction': {
'1': rebels,
'2': empire
"Faction": {"1": rebels, "2": empire},
"Ship": {
"1": xwing,
"2": ywing,
"3": awing,
"4": falcon,
"5": homeOne,
"6": tieFighter,
"7": tieInterceptor,
"8": executor,
},
'Ship': {
'1': xwing,
'2': ywing,
'3': awing,
'4': falcon,
'5': homeOne,
'6': tieFighter,
'7': tieInterceptor,
'8': executor
}
}
def create_ship(ship_name, faction_id):
from .schema import Ship
next_ship = len(data['Ship'].keys()) + 1
new_ship = Ship(
id=str(next_ship),
name=ship_name
)
data['Ship'][new_ship.id] = new_ship
data['Faction'][faction_id].ships.append(new_ship.id)
next_ship = len(data["Ship"].keys()) + 1
new_ship = Ship(id=str(next_ship), name=ship_name)
data["Ship"][new_ship.id] = new_ship
data["Faction"][faction_id].ships.append(new_ship.id)
return new_ship
def get_ship(_id):
return data['Ship'][_id]
return data["Ship"][_id]
def get_faction(_id):
return data['Faction'][_id]
return data["Faction"][_id]
def get_rebels():
return get_faction('1')
return get_faction("1")
def get_empire():
return get_faction('2')
return get_faction("2")

View File

@ -1,43 +1,48 @@
import graphene
from graphene import relay, resolve_only_args
from graphene import relay
from .data import create_ship, get_empire, get_faction, get_rebels, get_ship
class Ship(graphene.ObjectType):
'''A ship in the Star Wars saga'''
"""A ship in the Star Wars saga"""
class Meta:
interfaces = (relay.Node,)
name = graphene.String(description='The name of the ship.')
name = graphene.String(description="The name of the ship.")
@classmethod
def get_node(cls, id, context, info):
def get_node(cls, info, id):
return get_ship(id)
class ShipConnection(relay.Connection):
class Meta:
node = Ship
class Faction(graphene.ObjectType):
'''A faction in the Star Wars saga'''
"""A faction in the Star Wars saga"""
class Meta:
interfaces = (relay.Node,)
name = graphene.String(description='The name of the faction.')
ships = relay.ConnectionField(Ship, description='The ships used by the faction.')
name = graphene.String(description="The name of the faction.")
ships = relay.ConnectionField(
ShipConnection, description="The ships used by the faction."
)
@resolve_only_args
def resolve_ships(self, **args):
def resolve_ships(self, info, **args):
# Transform the instance ship_ids into real instances
return [get_ship(ship_id) for ship_id in self.ships]
@classmethod
def get_node(cls, id, context, info):
def get_node(cls, info, id):
return get_faction(id)
class IntroduceShip(relay.ClientIDMutation):
class Input:
ship_name = graphene.String(required=True)
faction_id = graphene.String(required=True)
@ -46,9 +51,9 @@ class IntroduceShip(relay.ClientIDMutation):
faction = graphene.Field(Faction)
@classmethod
def mutate_and_get_payload(cls, input, context, info):
ship_name = input.get('ship_name')
faction_id = input.get('faction_id')
def mutate_and_get_payload(
cls, root, info, ship_name, faction_id, client_mutation_id=None
):
ship = create_ship(ship_name, faction_id)
faction = get_faction(faction_id)
return IntroduceShip(ship=ship, faction=faction)
@ -59,12 +64,10 @@ class Query(graphene.ObjectType):
empire = graphene.Field(Faction)
node = relay.Node.Field()
@resolve_only_args
def resolve_rebels(self):
def resolve_rebels(root, info):
return get_rebels()
@resolve_only_args
def resolve_empire(self):
def resolve_empire(root, info):
return get_empire()

View File

@ -1,32 +0,0 @@
# -*- coding: utf-8 -*-
# snapshottest: v1 - https://goo.gl/zC4yUc
from __future__ import unicode_literals
from snapshottest import Snapshot
snapshots = Snapshot()
snapshots['test_correct_fetch_first_ship_rebels 1'] = {
'data': {
'rebels': {
'name': 'Alliance to Restore the Republic',
'ships': {
'pageInfo': {
'startCursor': 'YXJyYXljb25uZWN0aW9uOjA=',
'endCursor': 'YXJyYXljb25uZWN0aW9uOjA=',
'hasNextPage': True,
'hasPreviousPage': False
},
'edges': [
{
'cursor': 'YXJyYXljb25uZWN0aW9uOjA=',
'node': {
'name': 'X-Wing'
}
}
]
}
}
}
}

View File

@ -1,62 +0,0 @@
# -*- coding: utf-8 -*-
# snapshottest: v1 - https://goo.gl/zC4yUc
from __future__ import unicode_literals
from snapshottest import Snapshot
snapshots = Snapshot()
snapshots['test_mutations 1'] = {
'data': {
'introduceShip': {
'ship': {
'id': 'U2hpcDo5',
'name': 'Peter'
},
'faction': {
'name': 'Alliance to Restore the Republic',
'ships': {
'edges': [
{
'node': {
'id': 'U2hpcDox',
'name': 'X-Wing'
}
},
{
'node': {
'id': 'U2hpcDoy',
'name': 'Y-Wing'
}
},
{
'node': {
'id': 'U2hpcDoz',
'name': 'A-Wing'
}
},
{
'node': {
'id': 'U2hpcDo0',
'name': 'Millenium Falcon'
}
},
{
'node': {
'id': 'U2hpcDo1',
'name': 'Home One'
}
},
{
'node': {
'id': 'U2hpcDo5',
'name': 'Peter'
}
}
]
}
}
}
}
}

View File

@ -1,53 +0,0 @@
# -*- coding: utf-8 -*-
# snapshottest: v1 - https://goo.gl/zC4yUc
from __future__ import unicode_literals
from snapshottest import Snapshot
snapshots = Snapshot()
snapshots['test_correctly_fetches_id_name_rebels 1'] = {
'data': {
'rebels': {
'id': 'RmFjdGlvbjox',
'name': 'Alliance to Restore the Republic'
}
}
}
snapshots['test_correctly_refetches_rebels 1'] = {
'data': {
'node': {
'id': 'RmFjdGlvbjox',
'name': 'Alliance to Restore the Republic'
}
}
}
snapshots['test_correctly_fetches_id_name_empire 1'] = {
'data': {
'empire': {
'id': 'RmFjdGlvbjoy',
'name': 'Galactic Empire'
}
}
}
snapshots['test_correctly_refetches_empire 1'] = {
'data': {
'node': {
'id': 'RmFjdGlvbjoy',
'name': 'Galactic Empire'
}
}
}
snapshots['test_correctly_refetches_xwing 1'] = {
'data': {
'node': {
'id': 'U2hpcDox',
'name': 'X-Wing'
}
}
}

View File

@ -1,4 +1,5 @@
from graphene.test import Client
from ..data import setup
from ..schema import schema
@ -7,8 +8,8 @@ setup()
client = Client(schema)
def test_correct_fetch_first_ship_rebels(snapshot):
query = '''
def test_correct_fetch_first_ship_rebels():
result = client.execute("""
query RebelsShipsQuery {
rebels {
name,
@ -28,5 +29,25 @@ def test_correct_fetch_first_ship_rebels(snapshot):
}
}
}
'''
snapshot.assert_match(client.execute(query))
""")
assert result == {
"data": {
"rebels": {
"name": "Alliance to Restore the Republic",
"ships": {
"pageInfo": {
"startCursor": "YXJyYXljb25uZWN0aW9uOjA=",
"endCursor": "YXJyYXljb25uZWN0aW9uOjA=",
"hasNextPage": True,
"hasPreviousPage": False,
},
"edges": [
{
"cursor": "YXJyYXljb25uZWN0aW9uOjA=",
"node": {"name": "X-Wing"},
}
],
},
}
}
}

View File

@ -1,4 +1,5 @@
from graphene.test import Client
from ..data import setup
from ..schema import schema
@ -7,8 +8,8 @@ setup()
client = Client(schema)
def test_mutations(snapshot):
query = '''
def test_mutations():
result = client.execute("""
mutation MyMutation {
introduceShip(input:{clientMutationId:"abc", shipName: "Peter", factionId: "1"}) {
ship {
@ -28,5 +29,24 @@ def test_mutations(snapshot):
}
}
}
'''
snapshot.assert_match(client.execute(query))
""")
assert result == {
"data": {
"introduceShip": {
"ship": {"id": "U2hpcDo5", "name": "Peter"},
"faction": {
"name": "Alliance to Restore the Republic",
"ships": {
"edges": [
{"node": {"id": "U2hpcDox", "name": "X-Wing"}},
{"node": {"id": "U2hpcDoy", "name": "Y-Wing"}},
{"node": {"id": "U2hpcDoz", "name": "A-Wing"}},
{"node": {"id": "U2hpcDo0", "name": "Millennium Falcon"}},
{"node": {"id": "U2hpcDo1", "name": "Home One"}},
{"node": {"id": "U2hpcDo5", "name": "Peter"}},
]
},
},
}
}
}

View File

@ -1,4 +1,7 @@
import textwrap
from graphene.test import Client
from ..data import setup
from ..schema import schema
@ -8,21 +11,80 @@ client = Client(schema)
def test_str_schema():
assert str(schema) == '''schema {
query: Query
mutation: Mutation
assert str(schema).strip() == textwrap.dedent(
'''\
type Query {
rebels: Faction
empire: Faction
node(
"""The ID of the object"""
id: ID!
): Node
}
"""A faction in the Star Wars saga"""
type Faction implements Node {
"""The ID of the object"""
id: ID!
"""The name of the faction."""
name: String
"""The ships used by the faction."""
ships(before: String, after: String, first: Int, last: Int): ShipConnection
}
input IntroduceShipInput {
shipName: String!
factionId: String!
clientMutationId: String
"""An object with an ID"""
interface Node {
"""The ID of the object"""
id: ID!
}
type ShipConnection {
"""Pagination data for this connection."""
pageInfo: PageInfo!
"""Contains the nodes in this connection."""
edges: [ShipEdge]!
}
"""
The Relay compliant `PageInfo` type, containing data necessary to paginate this connection.
"""
type PageInfo {
"""When paginating forwards, are there more items?"""
hasNextPage: Boolean!
"""When paginating backwards, are there more items?"""
hasPreviousPage: Boolean!
"""When paginating backwards, the cursor to continue."""
startCursor: String
"""When paginating forwards, the cursor to continue."""
endCursor: String
}
"""A Relay edge containing a `Ship` and its cursor."""
type ShipEdge {
"""The item at the end of the edge"""
node: Ship
"""A cursor for use in pagination"""
cursor: String!
}
"""A ship in the Star Wars saga"""
type Ship implements Node {
"""The ID of the object"""
id: ID!
"""The name of the ship."""
name: String
}
type Mutation {
introduceShip(input: IntroduceShipInput!): IntroduceShipPayload
}
type IntroduceShipPayload {
@ -31,58 +93,32 @@ type IntroduceShipPayload {
clientMutationId: String
}
type Mutation {
introduceShip(input: IntroduceShipInput!): IntroduceShipPayload
}
interface Node {
id: ID!
}
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}
type Query {
rebels: Faction
empire: Faction
node(id: ID!): Node
}
type Ship implements Node {
id: ID!
name: String
}
type ShipConnection {
pageInfo: PageInfo!
edges: [ShipEdge]!
}
type ShipEdge {
node: Ship
cursor: String!
}
'''
input IntroduceShipInput {
shipName: String!
factionId: String!
clientMutationId: String
}'''
)
def test_correctly_fetches_id_name_rebels(snapshot):
query = '''
def test_correctly_fetches_id_name_rebels():
result = client.execute("""
query RebelsQuery {
rebels {
id
name
}
}
'''
snapshot.assert_match(client.execute(query))
""")
assert result == {
"data": {
"rebels": {"id": "RmFjdGlvbjox", "name": "Alliance to Restore the Republic"}
}
}
def test_correctly_refetches_rebels(snapshot):
query = '''
def test_correctly_refetches_rebels():
result = client.execute("""
query RebelsRefetchQuery {
node(id: "RmFjdGlvbjox") {
id
@ -91,24 +127,30 @@ def test_correctly_refetches_rebels(snapshot):
}
}
}
'''
snapshot.assert_match(client.execute(query))
""")
assert result == {
"data": {
"node": {"id": "RmFjdGlvbjox", "name": "Alliance to Restore the Republic"}
}
}
def test_correctly_fetches_id_name_empire(snapshot):
query = '''
def test_correctly_fetches_id_name_empire():
result = client.execute("""
query EmpireQuery {
empire {
id
name
}
}
'''
snapshot.assert_match(client.execute(query))
""")
assert result == {
"data": {"empire": {"id": "RmFjdGlvbjoy", "name": "Galactic Empire"}}
}
def test_correctly_refetches_empire(snapshot):
query = '''
def test_correctly_refetches_empire():
result = client.execute("""
query EmpireRefetchQuery {
node(id: "RmFjdGlvbjoy") {
id
@ -117,12 +159,14 @@ def test_correctly_refetches_empire(snapshot):
}
}
}
'''
snapshot.assert_match(client.execute(query))
""")
assert result == {
"data": {"node": {"id": "RmFjdGlvbjoy", "name": "Galactic Empire"}}
}
def test_correctly_refetches_xwing(snapshot):
query = '''
def test_correctly_refetches_xwing():
result = client.execute("""
query XWingRefetchQuery {
node(id: "U2hpcDox") {
id
@ -131,5 +175,5 @@ def test_correctly_refetches_xwing(snapshot):
}
}
}
'''
snapshot.assert_match(client.execute(query))
""")
assert result == {"data": {"node": {"id": "U2hpcDox", "name": "X-Wing"}}}

View File

@ -1,78 +1,98 @@
from .pyutils.version import get_version
try:
# This variable is injected in the __builtins__ by the build
# process. It used to enable importing subpackages when
# the required packages are not installed
__SETUP__
except NameError:
__SETUP__ = False
VERSION = (1, 4, 0, 'final', 0)
__version__ = get_version(VERSION)
if not __SETUP__:
from .types import (
AbstractType,
ObjectType,
InputObjectType,
Interface,
Mutation,
Field,
InputField,
Schema,
Scalar,
String, ID, Int, Float, Boolean,
List, NonNull,
Enum,
Argument,
Dynamic,
Union,
)
from .relay import (
Node,
is_node,
GlobalID,
BaseGlobalIDType,
ClientIDMutation,
Connection,
ConnectionField,
PageInfo
DefaultGlobalIDType,
GlobalID,
Node,
PageInfo,
SimpleGlobalIDType,
UUIDGlobalIDType,
is_node,
)
from .types import (
ID,
UUID,
Argument,
Base64,
BigInt,
Boolean,
Context,
Date,
DateTime,
Decimal,
Dynamic,
Enum,
Field,
Float,
InputField,
InputObjectType,
Int,
Interface,
JSONString,
List,
Mutation,
NonNull,
ObjectType,
ResolveInfo,
Scalar,
Schema,
String,
Time,
Union,
)
from .utils.resolve_only_args import resolve_only_args
from .utils.module_loading import lazy_import
from .utils.resolve_only_args import resolve_only_args
VERSION = (3, 4, 3, "final", 0)
__version__ = get_version(VERSION)
__all__ = [
'AbstractType',
'ObjectType',
'InputObjectType',
'Interface',
'Mutation',
'Field',
'InputField',
'Schema',
'Scalar',
'String',
'ID',
'Int',
'Float',
'Enum',
'Boolean',
'List',
'NonNull',
'Argument',
'Dynamic',
'Union',
'resolve_only_args',
'Node',
'is_node',
'GlobalID',
'ClientIDMutation',
'Connection',
'ConnectionField',
'PageInfo',
'lazy_import',
"__version__",
"Argument",
"Base64",
"BigInt",
"BaseGlobalIDType",
"Boolean",
"ClientIDMutation",
"Connection",
"ConnectionField",
"Context",
"Date",
"DateTime",
"Decimal",
"DefaultGlobalIDType",
"Dynamic",
"Enum",
"Field",
"Float",
"GlobalID",
"ID",
"InputField",
"InputObjectType",
"Int",
"Interface",
"JSONString",
"List",
"Mutation",
"Node",
"NonNull",
"ObjectType",
"PageInfo",
"ResolveInfo",
"Scalar",
"Schema",
"SimpleGlobalIDType",
"String",
"Time",
"Union",
"UUID",
"UUIDGlobalIDType",
"is_node",
"lazy_import",
"resolve_only_args",
]

View File

@ -1,851 +0,0 @@
"""Python Enumerations"""
import sys as _sys
__all__ = ['Enum', 'IntEnum', 'unique']
version = 1, 1, 6
pyver = float('%s.%s' % _sys.version_info[:2])
try:
any
except NameError:
def any(iterable):
for element in iterable:
if element:
return True
return False
try:
from collections import OrderedDict
except ImportError:
OrderedDict = None
try:
basestring
except NameError:
# In Python 2 basestring is the ancestor of both str and unicode
# in Python 3 it's just str, but was missing in 3.1
basestring = str
try:
unicode
except NameError:
# In Python 3 unicode no longer exists (it's just str)
unicode = str
class _RouteClassAttributeToGetattr(object):
"""Route attribute access on a class to __getattr__.
This is a descriptor, used to define attributes that act differently when
accessed through an instance and through a class. Instance access remains
normal, but access to an attribute through a class will be routed to the
class's __getattr__ method; this is done by raising AttributeError.
"""
def __init__(self, fget=None):
self.fget = fget
def __get__(self, instance, ownerclass=None):
if instance is None:
raise AttributeError()
return self.fget(instance)
def __set__(self, instance, value):
raise AttributeError("can't set attribute")
def __delete__(self, instance):
raise AttributeError("can't delete attribute")
def _is_descriptor(obj):
"""Returns True if obj is a descriptor, False otherwise."""
return (
hasattr(obj, '__get__') or
hasattr(obj, '__set__') or
hasattr(obj, '__delete__'))
def _is_dunder(name):
"""Returns True if a __dunder__ name, False otherwise."""
return (len(name) > 4 and
name[:2] == name[-2:] == '__' and
name[2:3] != '_' and
name[-3:-2] != '_')
def _is_sunder(name):
"""Returns True if a _sunder_ name, False otherwise."""
return (len(name) > 2 and
name[0] == name[-1] == '_' and
name[1:2] != '_' and
name[-2:-1] != '_')
def _make_class_unpicklable(cls):
"""Make the given class un-picklable."""
def _break_on_call_reduce(self, protocol=None):
raise TypeError('%r cannot be pickled' % self)
cls.__reduce_ex__ = _break_on_call_reduce
cls.__module__ = '<unknown>'
class _EnumDict(OrderedDict):
"""Track enum member order and ensure member names are not reused.
EnumMeta will use the names found in self._member_names as the
enumeration member names.
"""
def __init__(self):
super(_EnumDict, self).__init__()
self._member_names = []
def __setitem__(self, key, value):
"""Changes anything not dundered or not a descriptor.
If a descriptor is added with the same name as an enum member, the name
is removed from _member_names (this may leave a hole in the numerical
sequence of values).
If an enum member name is used twice, an error is raised; duplicate
values are not checked for.
Single underscore (sunder) names are reserved.
Note: in 3.x __order__ is simply discarded as a not necessary piece
leftover from 2.x
"""
if pyver >= 3.0 and key in ('_order_', '__order__'):
return
elif key == '__order__':
key = '_order_'
if _is_sunder(key):
if key != '_order_':
raise ValueError('_names_ are reserved for future Enum use')
elif _is_dunder(key):
pass
elif key in self._member_names:
# descriptor overwriting an enum?
raise TypeError('Attempted to reuse key: %r' % key)
elif not _is_descriptor(value):
if key in self:
# enum overwriting a descriptor?
raise TypeError('Key already defined as: %r' % self[key])
self._member_names.append(key)
super(_EnumDict, self).__setitem__(key, value)
# Dummy value for Enum as EnumMeta explicity checks for it, but of course until
# EnumMeta finishes running the first time the Enum class doesn't exist. This
# is also why there are checks in EnumMeta like `if Enum is not None`
Enum = None
class EnumMeta(type):
"""Metaclass for Enum"""
@classmethod
def __prepare__(metacls, cls, bases):
return _EnumDict()
def __new__(metacls, cls, bases, classdict):
# an Enum class is final once enumeration items have been defined; it
# cannot be mixed with other types (int, float, etc.) if it has an
# inherited __new__ unless a new __new__ is defined (or the resulting
# class will fail).
if isinstance(classdict, dict):
original_dict = classdict
classdict = _EnumDict()
for k, v in original_dict.items():
classdict[k] = v
member_type, first_enum = metacls._get_mixins_(bases)
__new__, save_new, use_args = metacls._find_new_(classdict, member_type,
first_enum)
# save enum items into separate mapping so they don't get baked into
# the new class
members = dict((k, classdict[k]) for k in classdict._member_names)
for name in classdict._member_names:
del classdict[name]
# py2 support for definition order
_order_ = classdict.get('_order_')
if _order_ is None:
if pyver < 3.0:
try:
_order_ = [name for (name, value) in sorted(members.items(), key=lambda item: item[1])]
except TypeError:
_order_ = [name for name in sorted(members.keys())]
else:
_order_ = classdict._member_names
else:
del classdict['_order_']
if pyver < 3.0:
_order_ = _order_.replace(',', ' ').split()
aliases = [name for name in members if name not in _order_]
_order_ += aliases
# check for illegal enum names (any others?)
invalid_names = set(members) & set(['mro'])
if invalid_names:
raise ValueError('Invalid enum member name(s): %s' % (
', '.join(invalid_names), ))
# save attributes from super classes so we know if we can take
# the shortcut of storing members in the class dict
base_attributes = set([a for b in bases for a in b.__dict__])
# create our new Enum type
enum_class = super(EnumMeta, metacls).__new__(metacls, cls, bases, classdict)
enum_class._member_names_ = [] # names in random order
if OrderedDict is not None:
enum_class._member_map_ = OrderedDict()
else:
enum_class._member_map_ = {} # name->value map
enum_class._member_type_ = member_type
# Reverse value->name map for hashable values.
enum_class._value2member_map_ = {}
# instantiate them, checking for duplicates as we go
# we instantiate first instead of checking for duplicates first in case
# a custom __new__ is doing something funky with the values -- such as
# auto-numbering ;)
if __new__ is None:
__new__ = enum_class.__new__
for member_name in _order_:
value = members[member_name]
if not isinstance(value, tuple):
args = (value, )
else:
args = value
if member_type is tuple: # special case for tuple enums
args = (args, ) # wrap it one more time
if not use_args or not args:
enum_member = __new__(enum_class)
if not hasattr(enum_member, '_value_'):
enum_member._value_ = value
else:
enum_member = __new__(enum_class, *args)
if not hasattr(enum_member, '_value_'):
enum_member._value_ = member_type(*args)
value = enum_member._value_
enum_member._name_ = member_name
enum_member.__objclass__ = enum_class
enum_member.__init__(*args)
# If another member with the same value was already defined, the
# new member becomes an alias to the existing one.
for name, canonical_member in enum_class._member_map_.items():
if canonical_member.value == enum_member._value_:
enum_member = canonical_member
break
else:
# Aliases don't appear in member names (only in __members__).
enum_class._member_names_.append(member_name)
# performance boost for any member that would not shadow
# a DynamicClassAttribute (aka _RouteClassAttributeToGetattr)
if member_name not in base_attributes:
setattr(enum_class, member_name, enum_member)
# now add to _member_map_
enum_class._member_map_[member_name] = enum_member
try:
# This may fail if value is not hashable. We can't add the value
# to the map, and by-value lookups for this value will be
# linear.
enum_class._value2member_map_[value] = enum_member
except TypeError:
pass
# If a custom type is mixed into the Enum, and it does not know how
# to pickle itself, pickle.dumps will succeed but pickle.loads will
# fail. Rather than have the error show up later and possibly far
# from the source, sabotage the pickle protocol for this class so
# that pickle.dumps also fails.
#
# However, if the new class implements its own __reduce_ex__, do not
# sabotage -- it's on them to make sure it works correctly. We use
# __reduce_ex__ instead of any of the others as it is preferred by
# pickle over __reduce__, and it handles all pickle protocols.
unpicklable = False
if '__reduce_ex__' not in classdict:
if member_type is not object:
methods = ('__getnewargs_ex__', '__getnewargs__',
'__reduce_ex__', '__reduce__')
if not any(m in member_type.__dict__ for m in methods):
_make_class_unpicklable(enum_class)
unpicklable = True
# double check that repr and friends are not the mixin's or various
# things break (such as pickle)
for name in ('__repr__', '__str__', '__format__', '__reduce_ex__'):
class_method = getattr(enum_class, name)
getattr(member_type, name, None)
enum_method = getattr(first_enum, name, None)
if name not in classdict and class_method is not enum_method:
if name == '__reduce_ex__' and unpicklable:
continue
setattr(enum_class, name, enum_method)
# method resolution and int's are not playing nice
# Python's less than 2.6 use __cmp__
if pyver < 2.6:
if issubclass(enum_class, int):
setattr(enum_class, '__cmp__', getattr(int, '__cmp__'))
elif pyver < 3.0:
if issubclass(enum_class, int):
for method in (
'__le__',
'__lt__',
'__gt__',
'__ge__',
'__eq__',
'__ne__',
'__hash__',
):
setattr(enum_class, method, getattr(int, method))
# replace any other __new__ with our own (as long as Enum is not None,
# anyway) -- again, this is to support pickle
if Enum is not None:
# if the user defined their own __new__, save it before it gets
# clobbered in case they subclass later
if save_new:
setattr(enum_class, '__member_new__', enum_class.__dict__['__new__'])
setattr(enum_class, '__new__', Enum.__dict__['__new__'])
return enum_class
def __bool__(cls):
"""
classes/types should always be True.
"""
return True
def __call__(cls, value, names=None, module=None, type=None, start=1):
"""Either returns an existing member, or creates a new enum class.
This method is used both when an enum class is given a value to match
to an enumeration member (i.e. Color(3)) and for the functional API
(i.e. Color = Enum('Color', names='red green blue')).
When used for the functional API: `module`, if set, will be stored in
the new class' __module__ attribute; `type`, if set, will be mixed in
as the first base class.
Note: if `module` is not set this routine will attempt to discover the
calling module by walking the frame stack; if this is unsuccessful
the resulting class will not be pickleable.
"""
if names is None: # simple value lookup
return cls.__new__(cls, value)
# otherwise, functional API: we're creating a new Enum type
return cls._create_(value, names, module=module, type=type, start=start)
def __contains__(cls, member):
return isinstance(member, cls) and member.name in cls._member_map_
def __delattr__(cls, attr):
# nicer error message when someone tries to delete an attribute
# (see issue19025).
if attr in cls._member_map_:
raise AttributeError(
"%s: cannot delete Enum member." % cls.__name__)
super(EnumMeta, cls).__delattr__(attr)
def __dir__(self):
return (['__class__', '__doc__', '__members__', '__module__'] +
self._member_names_)
@property
def __members__(cls):
"""Returns a mapping of member name->value.
This mapping lists all enum members, including aliases. Note that this
is a copy of the internal mapping.
"""
return cls._member_map_.copy()
def __getattr__(cls, name):
"""Return the enum member matching `name`
We use __getattr__ instead of descriptors or inserting into the enum
class' __dict__ in order to support `name` and `value` being both
properties for enum members (which live in the class' __dict__) and
enum members themselves.
"""
if _is_dunder(name):
raise AttributeError(name)
try:
return cls._member_map_[name]
except KeyError:
raise AttributeError(name)
def __getitem__(cls, name):
return cls._member_map_[name]
def __iter__(cls):
return (cls._member_map_[name] for name in cls._member_names_)
def __reversed__(cls):
return (cls._member_map_[name] for name in reversed(cls._member_names_))
def __len__(cls):
return len(cls._member_names_)
__nonzero__ = __bool__
def __repr__(cls):
return "<enum %r>" % cls.__name__
def __setattr__(cls, name, value):
"""Block attempts to reassign Enum members.
A simple assignment to the class namespace only changes one of the
several possible ways to get an Enum member from the Enum class,
resulting in an inconsistent Enumeration.
"""
member_map = cls.__dict__.get('_member_map_', {})
if name in member_map:
raise AttributeError('Cannot reassign members.')
super(EnumMeta, cls).__setattr__(name, value)
def _create_(cls, class_name, names=None, module=None, type=None, start=1):
"""Convenience method to create a new Enum class.
`names` can be:
* A string containing member names, separated either with spaces or
commas. Values are auto-numbered from 1.
* An iterable of member names. Values are auto-numbered from 1.
* An iterable of (member name, value) pairs.
* A mapping of member name -> value.
"""
if pyver < 3.0:
# if class_name is unicode, attempt a conversion to ASCII
if isinstance(class_name, unicode):
try:
class_name = class_name.encode('ascii')
except UnicodeEncodeError:
raise TypeError('%r is not representable in ASCII' % class_name)
metacls = cls.__class__
if type is None:
bases = (cls, )
else:
bases = (type, cls)
classdict = metacls.__prepare__(class_name, bases)
_order_ = []
# special processing needed for names?
if isinstance(names, basestring):
names = names.replace(',', ' ').split()
if isinstance(names, (tuple, list)) and isinstance(names[0], basestring):
names = [(e, i + start) for (i, e) in enumerate(names)]
# Here, names is either an iterable of (name, value) or a mapping.
item = None # in case names is empty
for item in names:
if isinstance(item, basestring):
member_name, member_value = item, names[item]
else:
member_name, member_value = item
classdict[member_name] = member_value
_order_.append(member_name)
# only set _order_ in classdict if name/value was not from a mapping
if not isinstance(item, basestring):
classdict['_order_'] = ' '.join(_order_)
enum_class = metacls.__new__(metacls, class_name, bases, classdict)
# TODO: replace the frame hack if a blessed way to know the calling
# module is ever developed
if module is None:
try:
module = _sys._getframe(2).f_globals['__name__']
except (AttributeError, ValueError):
pass
if module is None:
_make_class_unpicklable(enum_class)
else:
enum_class.__module__ = module
return enum_class
@staticmethod
def _get_mixins_(bases):
"""Returns the type for creating enum members, and the first inherited
enum class.
bases: the tuple of bases that was given to __new__
"""
if not bases or Enum is None:
return object, Enum
# double check that we are not subclassing a class with existing
# enumeration members; while we're at it, see if any other data
# type has been mixed in so we can use the correct __new__
member_type = first_enum = None
for base in bases:
if (base is not Enum and
issubclass(base, Enum) and
base._member_names_):
raise TypeError("Cannot extend enumerations")
# base is now the last base in bases
if not issubclass(base, Enum):
raise TypeError("new enumerations must be created as "
"`ClassName([mixin_type,] enum_type)`")
# get correct mix-in type (either mix-in type of Enum subclass, or
# first base if last base is Enum)
if not issubclass(bases[0], Enum):
member_type = bases[0] # first data type
first_enum = bases[-1] # enum type
else:
for base in bases[0].__mro__:
# most common: (IntEnum, int, Enum, object)
# possible: (<Enum 'AutoIntEnum'>, <Enum 'IntEnum'>,
# <class 'int'>, <Enum 'Enum'>,
# <class 'object'>)
if issubclass(base, Enum):
if first_enum is None:
first_enum = base
else:
if member_type is None:
member_type = base
return member_type, first_enum
if pyver < 3.0:
@staticmethod
def _find_new_(classdict, member_type, first_enum):
"""Returns the __new__ to be used for creating the enum members.
classdict: the class dictionary given to __new__
member_type: the data type whose __new__ will be used by default
first_enum: enumeration to check for an overriding __new__
"""
# now find the correct __new__, checking to see of one was defined
# by the user; also check earlier enum classes in case a __new__ was
# saved as __member_new__
__new__ = classdict.get('__new__', None)
if __new__:
return None, True, True # __new__, save_new, use_args
N__new__ = getattr(None, '__new__')
O__new__ = getattr(object, '__new__')
if Enum is None:
E__new__ = N__new__
else:
E__new__ = Enum.__dict__['__new__']
# check all possibles for __member_new__ before falling back to
# __new__
for method in ('__member_new__', '__new__'):
for possible in (member_type, first_enum):
try:
target = possible.__dict__[method]
except (AttributeError, KeyError):
target = getattr(possible, method, None)
if target not in [
None,
N__new__,
O__new__,
E__new__,
]:
if method == '__member_new__':
classdict['__new__'] = target
return None, False, True
if isinstance(target, staticmethod):
target = target.__get__(member_type)
__new__ = target
break
if __new__ is not None:
break
else:
__new__ = object.__new__
# if a non-object.__new__ is used then whatever value/tuple was
# assigned to the enum member name will be passed to __new__ and to the
# new enum member's __init__
if __new__ is object.__new__:
use_args = False
else:
use_args = True
return __new__, False, use_args
else:
@staticmethod
def _find_new_(classdict, member_type, first_enum):
"""Returns the __new__ to be used for creating the enum members.
classdict: the class dictionary given to __new__
member_type: the data type whose __new__ will be used by default
first_enum: enumeration to check for an overriding __new__
"""
# now find the correct __new__, checking to see of one was defined
# by the user; also check earlier enum classes in case a __new__ was
# saved as __member_new__
__new__ = classdict.get('__new__', None)
# should __new__ be saved as __member_new__ later?
save_new = __new__ is not None
if __new__ is None:
# check all possibles for __member_new__ before falling back to
# __new__
for method in ('__member_new__', '__new__'):
for possible in (member_type, first_enum):
target = getattr(possible, method, None)
if target not in (
None,
None.__new__,
object.__new__,
Enum.__new__,
):
__new__ = target
break
if __new__ is not None:
break
else:
__new__ = object.__new__
# if a non-object.__new__ is used then whatever value/tuple was
# assigned to the enum member name will be passed to __new__ and to the
# new enum member's __init__
if __new__ is object.__new__:
use_args = False
else:
use_args = True
return __new__, save_new, use_args
########################################################
# In order to support Python 2 and 3 with a single
# codebase we have to create the Enum methods separately
# and then use the `type(name, bases, dict)` method to
# create the class.
########################################################
temp_enum_dict = {}
temp_enum_dict['__doc__'] = "Generic enumeration.\n\n Derive from this class to define new enumerations.\n\n"
def __new__(cls, value):
# all enum instances are actually created during class construction
# without calling this method; this method is called by the metaclass'
# __call__ (i.e. Color(3) ), and by pickle
if isinstance(value, cls):
# For lookups like Color(Color.red)
value = value.value
# return value
# by-value search for a matching enum member
# see if it's in the reverse mapping (for hashable values)
try:
if value in cls._value2member_map_:
return cls._value2member_map_[value]
except TypeError:
# not there, now do long search -- O(n) behavior
for member in cls._member_map_.values():
if member.value == value:
return member
raise ValueError("%s is not a valid %s" % (value, cls.__name__))
temp_enum_dict['__new__'] = __new__
del __new__
def __repr__(self):
return "<%s.%s: %r>" % (
self.__class__.__name__, self._name_, self._value_)
temp_enum_dict['__repr__'] = __repr__
del __repr__
def __str__(self):
return "%s.%s" % (self.__class__.__name__, self._name_)
temp_enum_dict['__str__'] = __str__
del __str__
if pyver >= 3.0:
def __dir__(self):
added_behavior = [
m
for cls in self.__class__.mro()
for m in cls.__dict__
if m[0] != '_' and m not in self._member_map_
]
return (['__class__', '__doc__', '__module__', ] + added_behavior)
temp_enum_dict['__dir__'] = __dir__
del __dir__
def __format__(self, format_spec):
# mixed-in Enums should use the mixed-in type's __format__, otherwise
# we can get strange results with the Enum name showing up instead of
# the value
# pure Enum branch
if self._member_type_ is object:
cls = str
val = str(self)
# mix-in branch
else:
cls = self._member_type_
val = self.value
return cls.__format__(val, format_spec)
temp_enum_dict['__format__'] = __format__
del __format__
####################################
# Python's less than 2.6 use __cmp__
if pyver < 2.6:
def __cmp__(self, other):
if isinstance(other, self.__class__):
if self is other:
return 0
return -1
return NotImplemented
raise TypeError("unorderable types: %s() and %s()" % (self.__class__.__name__, other.__class__.__name__))
temp_enum_dict['__cmp__'] = __cmp__
del __cmp__
else:
def __le__(self, other):
raise TypeError("unorderable types: %s() <= %s()" % (self.__class__.__name__, other.__class__.__name__))
temp_enum_dict['__le__'] = __le__
del __le__
def __lt__(self, other):
raise TypeError("unorderable types: %s() < %s()" % (self.__class__.__name__, other.__class__.__name__))
temp_enum_dict['__lt__'] = __lt__
del __lt__
def __ge__(self, other):
raise TypeError("unorderable types: %s() >= %s()" % (self.__class__.__name__, other.__class__.__name__))
temp_enum_dict['__ge__'] = __ge__
del __ge__
def __gt__(self, other):
raise TypeError("unorderable types: %s() > %s()" % (self.__class__.__name__, other.__class__.__name__))
temp_enum_dict['__gt__'] = __gt__
del __gt__
def __eq__(self, other):
if isinstance(other, self.__class__):
return self is other
return NotImplemented
temp_enum_dict['__eq__'] = __eq__
del __eq__
def __ne__(self, other):
if isinstance(other, self.__class__):
return self is not other
return NotImplemented
temp_enum_dict['__ne__'] = __ne__
del __ne__
def __hash__(self):
return hash(self._name_)
temp_enum_dict['__hash__'] = __hash__
del __hash__
def __reduce_ex__(self, proto):
return self.__class__, (self._value_, )
temp_enum_dict['__reduce_ex__'] = __reduce_ex__
del __reduce_ex__
# _RouteClassAttributeToGetattr is used to provide access to the `name`
# and `value` properties of enum members while keeping some measure of
# protection from modification, while still allowing for an enumeration
# to have members named `name` and `value`. This works because enumeration
# members are not set directly on the enum class -- __getattr__ is
# used to look them up.
@_RouteClassAttributeToGetattr
def name(self):
return self._name_
temp_enum_dict['name'] = name
del name
@_RouteClassAttributeToGetattr
def value(self):
return self._value_
temp_enum_dict['value'] = value
del value
@classmethod
def _convert(cls, name, module, filter, source=None):
"""
Create a new Enum subclass that replaces a collection of global constants
"""
# convert all constants from source (or module) that pass filter() to
# a new Enum called name, and export the enum and its members back to
# module;
# also, replace the __reduce_ex__ method so unpickling works in
# previous Python versions
module_globals = vars(_sys.modules[module])
if source:
source = vars(source)
else:
source = module_globals
members = dict((name, value) for name, value in source.items() if filter(name))
cls = cls(name, members, module=module)
cls.__reduce_ex__ = _reduce_ex_by_name
module_globals.update(cls.__members__)
module_globals[name] = cls
return cls
temp_enum_dict['_convert'] = _convert
del _convert
Enum = EnumMeta('Enum', (object, ), temp_enum_dict)
del temp_enum_dict
# Enum has now been created
###########################
class IntEnum(int, Enum):
"""Enum where members are also (and must be) ints"""
def _reduce_ex_by_name(self, proto):
return self.name
def unique(enumeration):
"""Class decorator that ensures only unique members exist in an enumeration."""
duplicates = []
for name, member in enumeration.__members__.items():
if name != member.name:
duplicates.append((name, member.name))
if duplicates:
duplicate_names = ', '.join(
["%s -> %s" % (alias, name) for (alias, name) in duplicates]
)
raise ValueError('duplicate names found in %r: %s' %
(enumeration, duplicate_names)
)
return enumeration

View File

@ -1,41 +0,0 @@
from ..enum import _is_dunder, _is_sunder
def test__is_dunder():
dunder_names = [
'__i__',
'__test__',
]
non_dunder_names = [
'test',
'__test',
'_test',
'_test_',
'test__',
'',
]
for name in dunder_names:
assert _is_dunder(name) is True
for name in non_dunder_names:
assert _is_dunder(name) is False
def test__is_sunder():
sunder_names = [
'_i_',
'_test_',
]
non_sunder_names = [
'__i__',
'_i__',
'__i_',
'',
]
for name in sunder_names:
assert _is_sunder(name) is True
for name in non_sunder_names:
assert _is_sunder(name) is False

View File

@ -1,5 +1,3 @@
from __future__ import unicode_literals
import datetime
import os
import subprocess
@ -16,15 +14,12 @@ def get_version(version=None):
main = get_main_version(version)
sub = ''
if version[3] == 'alpha' and version[4] == 0:
sub = ""
if version[3] == "alpha" and version[4] == 0:
git_changeset = get_git_changeset()
if git_changeset:
sub = '.dev%s' % git_changeset
else:
sub = '.dev'
elif version[3] != 'final':
mapping = {'alpha': 'a', 'beta': 'b', 'rc': 'rc'}
sub = ".dev%s" % git_changeset if git_changeset else ".dev"
elif version[3] != "final":
mapping = {"alpha": "a", "beta": "b", "rc": "rc"}
sub = mapping[version[3]] + str(version[4])
return str(main + sub)
@ -34,7 +29,7 @@ def get_main_version(version=None):
"Returns main version (X.Y[.Z]) from VERSION."
version = get_complete_version(version)
parts = 2 if version[2] == 0 else 3
return '.'.join(str(x) for x in version[:parts])
return ".".join(str(x) for x in version[:parts])
def get_complete_version(version=None):
@ -45,17 +40,17 @@ def get_complete_version(version=None):
from graphene import VERSION as version
else:
assert len(version) == 5
assert version[3] in ('alpha', 'beta', 'rc', 'final')
assert version[3] in ("alpha", "beta", "rc", "final")
return version
def get_docs_version(version=None):
version = get_complete_version(version)
if version[3] != 'final':
return 'dev'
if version[3] != "final":
return "dev"
else:
return '%d.%d' % version[:2]
return "%d.%d" % version[:2]
def get_git_changeset():
@ -67,12 +62,15 @@ def get_git_changeset():
repo_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
try:
git_log = subprocess.Popen(
'git log --pretty=format:%ct --quiet -1 HEAD',
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
shell=True, cwd=repo_dir, universal_newlines=True,
"git log --pretty=format:%ct --quiet -1 HEAD",
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
shell=True,
cwd=repo_dir,
universal_newlines=True,
)
timestamp = git_log.communicate()[0]
timestamp = datetime.datetime.utcfromtimestamp(int(timestamp))
except:
except Exception:
return None
return timestamp.strftime('%Y%m%d%H%M%S')
return timestamp.strftime("%Y%m%d%H%M%S")

View File

@ -1,13 +1,23 @@
from .node import Node, is_node, GlobalID
from .mutation import ClientIDMutation
from .connection import Connection, ConnectionField, PageInfo
from .id_type import (
BaseGlobalIDType,
DefaultGlobalIDType,
SimpleGlobalIDType,
UUIDGlobalIDType,
)
__all__ = [
'Node',
'is_node',
'GlobalID',
'ClientIDMutation',
'Connection',
'ConnectionField',
'PageInfo',
"BaseGlobalIDType",
"ClientIDMutation",
"Connection",
"ConnectionField",
"DefaultGlobalIDType",
"GlobalID",
"Node",
"PageInfo",
"SimpleGlobalIDType",
"UUIDGlobalIDType",
"is_node",
]

View File

@ -1,122 +1,167 @@
import re
from collections import Iterable, OrderedDict
from collections.abc import Iterable
from functools import partial
from typing import Type
import six
from graphql_relay import connection_from_array
from graphql_relay import connection_from_list
from promise import Promise, is_thenable
from ..types import (AbstractType, Boolean, Enum, Int, Interface, List, NonNull, Scalar, String,
Union)
from ..types import Boolean, Enum, Int, Interface, List, NonNull, Scalar, String, Union
from ..types.field import Field
from ..types.objecttype import ObjectType, ObjectTypeMeta
from ..types.options import Options
from ..utils.is_base_type import is_base_type
from ..utils.props import props
from .node import is_node
from ..types.objecttype import ObjectType, ObjectTypeOptions
from ..utils.thenables import maybe_thenable
from .node import is_node, AbstractNode
def get_edge_class(
connection_class: Type["Connection"],
_node: Type[AbstractNode],
base_name: str,
strict_types: bool = False,
):
edge_class = getattr(connection_class, "Edge", None)
class EdgeBase:
node = Field(
NonNull(_node) if strict_types else _node,
description="The item at the end of the edge",
)
cursor = String(required=True, description="A cursor for use in pagination")
class EdgeMeta:
description = f"A Relay edge containing a `{base_name}` and its cursor."
edge_name = f"{base_name}Edge"
edge_bases = [edge_class, EdgeBase] if edge_class else [EdgeBase]
if not isinstance(edge_class, ObjectType):
edge_bases = [*edge_bases, ObjectType]
return type(edge_name, tuple(edge_bases), {"Meta": EdgeMeta})
class PageInfo(ObjectType):
class Meta:
description = (
"The Relay compliant `PageInfo` type, containing data necessary to"
" paginate this connection."
)
has_next_page = Boolean(
required=True,
name='hasNextPage',
description='When paginating forwards, are there more items?',
name="hasNextPage",
description="When paginating forwards, are there more items?",
)
has_previous_page = Boolean(
required=True,
name='hasPreviousPage',
description='When paginating backwards, are there more items?',
name="hasPreviousPage",
description="When paginating backwards, are there more items?",
)
start_cursor = String(
name='startCursor',
description='When paginating backwards, the cursor to continue.',
name="startCursor",
description="When paginating backwards, the cursor to continue.",
)
end_cursor = String(
name='endCursor',
description='When paginating forwards, the cursor to continue.',
name="endCursor",
description="When paginating forwards, the cursor to continue.",
)
class ConnectionMeta(ObjectTypeMeta):
def __new__(cls, name, bases, attrs):
# Also ensure initialization is only performed for subclasses of Model
# (excluding Model class itself).
if not is_base_type(bases, ConnectionMeta):
return type.__new__(cls, name, bases, attrs)
options = Options(
attrs.pop('Meta', None),
name=name,
description=None,
node=None,
# noinspection PyPep8Naming
def page_info_adapter(startCursor, endCursor, hasPreviousPage, hasNextPage):
"""Adapter for creating PageInfo instances"""
return PageInfo(
start_cursor=startCursor,
end_cursor=endCursor,
has_previous_page=hasPreviousPage,
has_next_page=hasNextPage,
)
options.interfaces = ()
options.local_fields = OrderedDict()
assert options.node, 'You have to provide a node in {}.Meta'.format(cls.__name__)
assert issubclass(options.node, (Scalar, Enum, ObjectType, Interface, Union, NonNull)), (
'Received incompatible node "{}" for Connection {}.'
).format(options.node, name)
base_name = re.sub('Connection$', '', options.name) or options.node._meta.name
if not options.name:
options.name = '{}Connection'.format(base_name)
edge_class = attrs.pop('Edge', None)
class EdgeBase(AbstractType):
node = Field(options.node, description='The item at the end of the edge')
cursor = String(required=True, description='A cursor for use in pagination')
edge_name = '{}Edge'.format(base_name)
if edge_class and issubclass(edge_class, AbstractType):
edge = type(edge_name, (EdgeBase, edge_class, ObjectType, ), {})
else:
edge_attrs = props(edge_class) if edge_class else {}
edge = type(edge_name, (EdgeBase, ObjectType, ), edge_attrs)
class ConnectionBase(AbstractType):
page_info = Field(PageInfo, name='pageInfo', required=True)
edges = NonNull(List(edge))
bases = (ConnectionBase, ) + bases
attrs = dict(attrs, _meta=options, Edge=edge)
return ObjectTypeMeta.__new__(cls, name, bases, attrs)
class Connection(six.with_metaclass(ConnectionMeta, ObjectType)):
pass
class ConnectionOptions(ObjectTypeOptions):
node = None
class Connection(ObjectType):
class Meta:
abstract = True
@classmethod
def __init_subclass_with_meta__(
cls, node=None, name=None, strict_types=False, _meta=None, **options
):
if not _meta:
_meta = ConnectionOptions(cls)
assert node, f"You have to provide a node in {cls.__name__}.Meta"
assert isinstance(node, NonNull) or issubclass(
node, (Scalar, Enum, ObjectType, Interface, Union, NonNull)
), f'Received incompatible node "{node}" for Connection {cls.__name__}.'
base_name = re.sub("Connection$", "", name or cls.__name__) or node._meta.name
if not name:
name = f"{base_name}Connection"
options["name"] = name
_meta.node = node
if not _meta.fields:
_meta.fields = {}
if "page_info" not in _meta.fields:
_meta.fields["page_info"] = Field(
PageInfo,
name="pageInfo",
required=True,
description="Pagination data for this connection.",
)
if "edges" not in _meta.fields:
edge_class = get_edge_class(cls, node, base_name, strict_types) # type: ignore
cls.Edge = edge_class
_meta.fields["edges"] = Field(
NonNull(List(NonNull(edge_class) if strict_types else edge_class)),
description="Contains the nodes in this connection.",
)
return super(Connection, cls).__init_subclass_with_meta__(
_meta=_meta, **options
)
# noinspection PyPep8Naming
def connection_adapter(cls, edges, pageInfo):
"""Adapter for creating Connection instances"""
return cls(edges=edges, page_info=pageInfo)
class IterableConnectionField(Field):
def __init__(self, type, *args, **kwargs):
kwargs.setdefault('before', String())
kwargs.setdefault('after', String())
kwargs.setdefault('first', Int())
kwargs.setdefault('last', Int())
super(IterableConnectionField, self).__init__(
type,
*args,
**kwargs
)
def __init__(self, type_, *args, **kwargs):
kwargs.setdefault("before", String())
kwargs.setdefault("after", String())
kwargs.setdefault("first", Int())
kwargs.setdefault("last", Int())
super(IterableConnectionField, self).__init__(type_, *args, **kwargs)
@property
def type(self):
type = super(IterableConnectionField, self).type
if is_node(type):
connection_type = type.Connection
else:
connection_type = type
assert issubclass(connection_type, Connection), (
'{} type have to be a subclass of Connection. Received "{}".'
).format(self.__class__.__name__, connection_type)
return connection_type
type_ = super(IterableConnectionField, self).type
connection_type = type_
if isinstance(type_, NonNull):
connection_type = type_.of_type
if is_node(connection_type):
raise Exception(
"ConnectionFields now need a explicit ConnectionType for Nodes.\n"
"Read more: https://github.com/graphql-python/graphene/blob/v2.0.0/UPGRADE-v2.0.md#node-connections"
)
assert issubclass(
connection_type, Connection
), f'{self.__class__.__name__} type has to be a subclass of Connection. Received "{connection_type}".'
return type_
@classmethod
def resolve_connection(cls, connection_type, args, resolved):
@ -124,31 +169,31 @@ class IterableConnectionField(Field):
return resolved
assert isinstance(resolved, Iterable), (
'Resolved value from the connection field have to be iterable or instance of {}. '
'Received "{}"'
).format(connection_type, resolved)
connection = connection_from_list(
f"Resolved value from the connection field has to be an iterable or instance of {connection_type}. "
f'Received "{resolved}"'
)
connection = connection_from_array(
resolved,
args,
connection_type=connection_type,
connection_type=partial(connection_adapter, connection_type),
edge_type=connection_type.Edge,
pageinfo_type=PageInfo
page_info_type=page_info_adapter,
)
connection.iterable = resolved
return connection
@classmethod
def connection_resolver(cls, resolver, connection_type, root, args, context, info):
resolved = resolver(root, args, context, info)
def connection_resolver(cls, resolver, connection_type, root, info, **args):
resolved = resolver(root, info, **args)
if isinstance(connection_type, NonNull):
connection_type = connection_type.of_type
on_resolve = partial(cls.resolve_connection, connection_type, args)
if is_thenable(resolved):
return Promise.resolve(resolved).then(on_resolve)
return maybe_thenable(resolved, on_resolve)
return on_resolve(resolved)
def get_resolver(self, parent_resolver):
resolver = super(IterableConnectionField, self).get_resolver(parent_resolver)
def wrap_resolve(self, parent_resolver):
resolver = super(IterableConnectionField, self).wrap_resolve(parent_resolver)
return partial(self.connection_resolver, resolver, self.type)

87
graphene/relay/id_type.py Normal file
View File

@ -0,0 +1,87 @@
from graphql_relay import from_global_id, to_global_id
from ..types import ID, UUID
from ..types.base import BaseType
from typing import Type
class BaseGlobalIDType:
"""
Base class that define the required attributes/method for a type.
"""
graphene_type: Type[BaseType] = ID
@classmethod
def resolve_global_id(cls, info, global_id):
# return _type, _id
raise NotImplementedError
@classmethod
def to_global_id(cls, _type, _id):
# return _id
raise NotImplementedError
class DefaultGlobalIDType(BaseGlobalIDType):
"""
Default global ID type: base64 encoded version of "<node type name>: <node id>".
"""
graphene_type = ID
@classmethod
def resolve_global_id(cls, info, global_id):
try:
_type, _id = from_global_id(global_id)
if not _type:
raise ValueError("Invalid Global ID")
return _type, _id
except Exception as e:
raise Exception(
f'Unable to parse global ID "{global_id}". '
'Make sure it is a base64 encoded string in the format: "TypeName:id". '
f"Exception message: {e}"
)
@classmethod
def to_global_id(cls, _type, _id):
return to_global_id(_type, _id)
class SimpleGlobalIDType(BaseGlobalIDType):
"""
Simple global ID type: simply the id of the object.
To be used carefully as the user is responsible for ensuring that the IDs are indeed global
(otherwise it could cause request caching issues).
"""
graphene_type = ID
@classmethod
def resolve_global_id(cls, info, global_id):
_type = info.return_type.graphene_type._meta.name
return _type, global_id
@classmethod
def to_global_id(cls, _type, _id):
return _id
class UUIDGlobalIDType(BaseGlobalIDType):
"""
UUID global ID type.
By definition UUID are global so they are used as they are.
"""
graphene_type = UUID
@classmethod
def resolve_global_id(cls, info, global_id):
_type = info.return_type.graphene_type._meta.name
return _type, global_id
@classmethod
def to_global_id(cls, _type, _id):
return _id

View File

@ -1,64 +1,66 @@
import re
from functools import partial
import six
from promise import Promise
from ..types import AbstractType, Argument, Field, InputObjectType, String
from ..types.objecttype import ObjectType, ObjectTypeMeta
from ..utils.is_base_type import is_base_type
from ..utils.props import props
from ..types import Field, InputObjectType, String
from ..types.mutation import Mutation
from ..utils.thenables import maybe_thenable
class ClientIDMutationMeta(ObjectTypeMeta):
def __new__(cls, name, bases, attrs):
# Also ensure initialization is only performed for subclasses of
# Mutation
if not is_base_type(bases, ClientIDMutationMeta):
return type.__new__(cls, name, bases, attrs)
input_class = attrs.pop('Input', None)
base_name = re.sub('Payload$', '', name)
if 'client_mutation_id' not in attrs:
attrs['client_mutation_id'] = String(name='clientMutationId')
cls = ObjectTypeMeta.__new__(cls, '{}Payload'.format(base_name), bases, attrs)
mutate_and_get_payload = getattr(cls, 'mutate_and_get_payload', None)
if cls.mutate and cls.mutate.__func__ == ClientIDMutation.mutate.__func__:
assert mutate_and_get_payload, (
"{}.mutate_and_get_payload method is required"
" in a ClientIDMutation."
).format(name)
input_attrs = {}
bases = ()
if not input_class:
input_attrs = {}
elif not issubclass(input_class, AbstractType):
input_attrs = props(input_class)
else:
bases += (input_class, )
input_attrs['client_mutation_id'] = String(name='clientMutationId')
cls.Input = type('{}Input'.format(base_name), bases + (InputObjectType,), input_attrs)
cls.Field = partial(Field, cls, resolver=cls.mutate, input=Argument(cls.Input, required=True))
return cls
class ClientIDMutation(six.with_metaclass(ClientIDMutationMeta, ObjectType)):
class ClientIDMutation(Mutation):
class Meta:
abstract = True
@classmethod
def mutate(cls, root, args, context, info):
input = args.get('input')
def __init_subclass_with_meta__(
cls, output=None, input_fields=None, arguments=None, name=None, **options
):
input_class = getattr(cls, "Input", None)
base_name = re.sub("Payload$", "", name or cls.__name__)
assert not output, "Can't specify any output"
assert not arguments, "Can't specify any arguments"
bases = (InputObjectType,)
if input_class:
bases += (input_class,)
if not input_fields:
input_fields = {}
cls.Input = type(
f"{base_name}Input",
bases,
dict(input_fields, client_mutation_id=String(name="clientMutationId")),
)
arguments = dict(
input=cls.Input(required=True)
# 'client_mutation_id': String(name='clientMutationId')
)
mutate_and_get_payload = getattr(cls, "mutate_and_get_payload", None)
if cls.mutate and cls.mutate.__func__ == ClientIDMutation.mutate.__func__:
assert mutate_and_get_payload, (
f"{name or cls.__name__}.mutate_and_get_payload method is required"
" in a ClientIDMutation."
)
if not name:
name = f"{base_name}Payload"
super(ClientIDMutation, cls).__init_subclass_with_meta__(
output=None, arguments=arguments, name=name, **options
)
cls._meta.fields["client_mutation_id"] = Field(String, name="clientMutationId")
@classmethod
def mutate(cls, root, info, input):
def on_resolve(payload):
try:
payload.client_mutation_id = input.get('clientMutationId')
except:
raise Exception((
'Cannot set client_mutation_id in the payload object {}'
).format(repr(payload)))
payload.client_mutation_id = input.get("client_mutation_id")
except Exception:
raise Exception(
f"Cannot set client_mutation_id in the payload object {repr(payload)}"
)
return payload
return Promise.resolve(
cls.mutate_and_get_payload(input, context, info)
).then(on_resolve)
result = cls.mutate_and_get_payload(root, info, **input)
return maybe_thenable(result, on_resolve)

View File

@ -1,127 +1,135 @@
from functools import partial
from inspect import isclass
import six
from graphql_relay import from_global_id, to_global_id
from ..types import ID, Field, Interface, ObjectType
from ..types import Field, Interface, ObjectType
from ..types.interface import InterfaceOptions
from ..types.utils import get_type
from ..types.interface import InterfaceMeta
from .id_type import BaseGlobalIDType, DefaultGlobalIDType
def is_node(objecttype):
'''
"""
Check if the given objecttype has Node as an interface
'''
"""
if not isclass(objecttype):
return False
if not issubclass(objecttype, ObjectType):
return False
for i in objecttype._meta.interfaces:
if issubclass(i, Node):
return True
return False
def get_default_connection(cls):
from .connection import Connection
assert issubclass(cls, ObjectType), (
'Can only get connection type on implemented Nodes.'
)
class Meta:
node = cls
return type('{}Connection'.format(cls.__name__), (Connection,), {'Meta': Meta})
return any(issubclass(i, Node) for i in objecttype._meta.interfaces)
class GlobalID(Field):
def __init__(self, node=None, parent_type=None, required=True, *args, **kwargs):
super(GlobalID, self).__init__(ID, required=required, *args, **kwargs)
def __init__(
self,
node=None,
parent_type=None,
required=True,
global_id_type=DefaultGlobalIDType,
*args,
**kwargs,
):
super(GlobalID, self).__init__(
global_id_type.graphene_type, required=required, *args, **kwargs
)
self.node = node or Node
self.parent_type_name = parent_type._meta.name if parent_type else None
@staticmethod
def id_resolver(parent_resolver, node, root, args, context, info, parent_type_name=None):
type_id = parent_resolver(root, args, context, info)
def id_resolver(parent_resolver, node, root, info, parent_type_name=None, **args):
type_id = parent_resolver(root, info, **args)
parent_type_name = parent_type_name or info.parent_type.name
return node.to_global_id(parent_type_name, type_id) # root._meta.name
def get_resolver(self, parent_resolver):
return partial(self.id_resolver, parent_resolver, self.node, parent_type_name=self.parent_type_name)
class NodeMeta(InterfaceMeta):
def __new__(cls, name, bases, attrs):
cls = InterfaceMeta.__new__(cls, name, bases, attrs)
cls._meta.fields['id'] = GlobalID(cls, description='The ID of the object.')
return cls
def wrap_resolve(self, parent_resolver):
return partial(
self.id_resolver,
parent_resolver,
self.node,
parent_type_name=self.parent_type_name,
)
class NodeField(Field):
def __init__(self, node, type=False, deprecation_reason=None,
name=None, **kwargs):
assert issubclass(node, Node), 'NodeField can only operate in Nodes'
def __init__(self, node, type_=False, **kwargs):
assert issubclass(node, Node), "NodeField can only operate in Nodes"
self.node_type = node
self.field_type = type
self.field_type = type_
global_id_type = node._meta.global_id_type
super(NodeField, self).__init__(
# If we don's specify a type, the field type will be the node interface
type or node,
description='The ID of the object',
id=ID(required=True)
# If we don't specify a type, the field type will be the node interface
type_ or node,
id=global_id_type.graphene_type(
required=True, description="The ID of the object"
),
**kwargs,
)
def get_resolver(self, parent_resolver):
return partial(self.node_type.node_resolver, only_type=get_type(self.field_type))
def wrap_resolve(self, parent_resolver):
return partial(self.node_type.node_resolver, get_type(self.field_type))
class Node(six.with_metaclass(NodeMeta, Interface)):
'''An object with an ID'''
class AbstractNode(Interface):
class Meta:
abstract = True
@classmethod
def __init_subclass_with_meta__(cls, global_id_type=DefaultGlobalIDType, **options):
assert issubclass(
global_id_type, BaseGlobalIDType
), "Custom ID type need to be implemented as a subclass of BaseGlobalIDType."
_meta = InterfaceOptions(cls)
_meta.global_id_type = global_id_type
_meta.fields = {
"id": GlobalID(
cls, global_id_type=global_id_type, description="The ID of the object"
)
}
super(AbstractNode, cls).__init_subclass_with_meta__(_meta=_meta, **options)
@classmethod
def resolve_global_id(cls, info, global_id):
return cls._meta.global_id_type.resolve_global_id(info, global_id)
class Node(AbstractNode):
"""An object with an ID"""
@classmethod
def Field(cls, *args, **kwargs): # noqa: N802
return NodeField(cls, *args, **kwargs)
@classmethod
def node_resolver(cls, root, args, context, info, only_type=None):
return cls.get_node_from_global_id(args.get('id'), context, info, only_type)
def node_resolver(cls, only_type, root, info, id):
return cls.get_node_from_global_id(info, id, only_type=only_type)
@classmethod
def get_node_from_global_id(cls, global_id, context, info, only_type=None):
try:
_type, _id = cls.from_global_id(global_id)
graphene_type = info.schema.get_type(_type).graphene_type
except:
return None
def get_node_from_global_id(cls, info, global_id, only_type=None):
_type, _id = cls.resolve_global_id(info, global_id)
graphene_type = info.schema.get_type(_type)
if graphene_type is None:
raise Exception(f'Relay Node "{_type}" not found in schema')
graphene_type = graphene_type.graphene_type
if only_type:
assert graphene_type == only_type, (
'Must receive an {} id.'
).format(graphene_type._meta.name)
assert (
graphene_type == only_type
), f"Must receive a {only_type._meta.name} id."
# We make sure the ObjectType implements the "Node" interface
if cls not in graphene_type._meta.interfaces:
return None
raise Exception(
f'ObjectType "{_type}" does not implement the "{cls}" interface.'
)
get_node = getattr(graphene_type, 'get_node', None)
get_node = getattr(graphene_type, "get_node", None)
if get_node:
return get_node(_id, context, info)
return get_node(info, _id)
@classmethod
def from_global_id(cls, global_id):
return from_global_id(global_id)
@classmethod
def to_global_id(cls, type, id):
return to_global_id(type, id)
@classmethod
def implements(cls, objecttype):
get_connection = getattr(objecttype, 'get_connection', None)
if not get_connection:
get_connection = partial(get_default_connection, objecttype)
objecttype.Connection = get_connection()
def to_global_id(cls, type_, id):
return cls._meta.global_id_type.to_global_id(type_, id)

View File

@ -1,13 +1,22 @@
import re
from ...types import AbstractType, Field, List, NonNull, ObjectType, String, Argument, Int
from ..connection import Connection, PageInfo, ConnectionField
from pytest import raises
from ...types import Argument, Field, Int, List, NonNull, ObjectType, Schema, String
from ..connection import (
Connection,
ConnectionField,
PageInfo,
ConnectionOptions,
get_edge_class,
)
from ..node import Node
class MyObject(ObjectType):
class Meta:
interfaces = [Node]
field = String()
@ -21,11 +30,11 @@ def test_connection():
class Edge:
other = String()
assert MyObjectConnection._meta.name == 'MyObjectConnection'
assert MyObjectConnection._meta.name == "MyObjectConnection"
fields = MyObjectConnection._meta.fields
assert list(fields.keys()) == ['page_info', 'edges', 'extra']
edge_field = fields['edges']
pageinfo_field = fields['page_info']
assert list(fields) == ["page_info", "edges", "extra"]
edge_field = fields["edges"]
pageinfo_field = fields["page_info"]
assert isinstance(edge_field, Field)
assert isinstance(edge_field.type, NonNull)
@ -38,22 +47,139 @@ def test_connection():
def test_connection_inherit_abstracttype():
class BaseConnection(AbstractType):
class BaseConnection:
extra = String()
class MyObjectConnection(BaseConnection, Connection):
class Meta:
node = MyObject
assert MyObjectConnection._meta.name == 'MyObjectConnection'
assert MyObjectConnection._meta.name == "MyObjectConnection"
fields = MyObjectConnection._meta.fields
assert list(fields.keys()) == ['page_info', 'edges', 'extra']
assert list(fields) == ["page_info", "edges", "extra"]
def test_connection_extra_abstract_fields():
class ConnectionWithNodes(Connection):
class Meta:
abstract = True
@classmethod
def __init_subclass_with_meta__(cls, node=None, name=None, **options):
_meta = ConnectionOptions(cls)
_meta.fields = {
"nodes": Field(
NonNull(List(node)),
description="Contains all the nodes in this connection.",
),
}
return super(ConnectionWithNodes, cls).__init_subclass_with_meta__(
node=node, name=name, _meta=_meta, **options
)
class MyObjectConnection(ConnectionWithNodes):
class Meta:
node = MyObject
class Edge:
other = String()
assert MyObjectConnection._meta.name == "MyObjectConnection"
fields = MyObjectConnection._meta.fields
assert list(fields) == ["nodes", "page_info", "edges"]
edge_field = fields["edges"]
pageinfo_field = fields["page_info"]
nodes_field = fields["nodes"]
assert isinstance(edge_field, Field)
assert isinstance(edge_field.type, NonNull)
assert isinstance(edge_field.type.of_type, List)
assert edge_field.type.of_type.of_type == MyObjectConnection.Edge
assert isinstance(pageinfo_field, Field)
assert isinstance(pageinfo_field.type, NonNull)
assert pageinfo_field.type.of_type == PageInfo
assert isinstance(nodes_field, Field)
assert isinstance(nodes_field.type, NonNull)
assert isinstance(nodes_field.type.of_type, List)
assert nodes_field.type.of_type.of_type == MyObject
def test_connection_override_fields():
class ConnectionWithNodes(Connection):
class Meta:
abstract = True
@classmethod
def __init_subclass_with_meta__(cls, node=None, name=None, **options):
_meta = ConnectionOptions(cls)
base_name = (
re.sub("Connection$", "", name or cls.__name__) or node._meta.name
)
edge_class = get_edge_class(cls, node, base_name)
_meta.fields = {
"page_info": Field(
NonNull(
PageInfo,
name="pageInfo",
required=True,
description="Pagination data for this connection.",
)
),
"edges": Field(
NonNull(List(NonNull(edge_class))),
description="Contains the nodes in this connection.",
),
}
return super(ConnectionWithNodes, cls).__init_subclass_with_meta__(
node=node, name=name, _meta=_meta, **options
)
class MyObjectConnection(ConnectionWithNodes):
class Meta:
node = MyObject
assert MyObjectConnection._meta.name == "MyObjectConnection"
fields = MyObjectConnection._meta.fields
assert list(fields) == ["page_info", "edges"]
edge_field = fields["edges"]
pageinfo_field = fields["page_info"]
assert isinstance(edge_field, Field)
assert isinstance(edge_field.type, NonNull)
assert isinstance(edge_field.type.of_type, List)
assert isinstance(edge_field.type.of_type.of_type, NonNull)
assert edge_field.type.of_type.of_type.of_type.__name__ == "MyObjectEdge"
# This page info is NonNull
assert isinstance(pageinfo_field, Field)
assert isinstance(edge_field.type, NonNull)
assert pageinfo_field.type.of_type == PageInfo
def test_connection_name():
custom_name = "MyObjectCustomNameConnection"
class BaseConnection:
extra = String()
class MyObjectConnection(BaseConnection, Connection):
class Meta:
node = MyObject
name = custom_name
assert MyObjectConnection._meta.name == custom_name
def test_edge():
class MyObjectConnection(Connection):
class Meta:
node = MyObject
@ -61,23 +187,22 @@ def test_edge():
other = String()
Edge = MyObjectConnection.Edge
assert Edge._meta.name == 'MyObjectEdge'
assert Edge._meta.name == "MyObjectEdge"
edge_fields = Edge._meta.fields
assert list(edge_fields.keys()) == ['node', 'cursor', 'other']
assert list(edge_fields) == ["node", "cursor", "other"]
assert isinstance(edge_fields['node'], Field)
assert edge_fields['node'].type == MyObject
assert isinstance(edge_fields["node"], Field)
assert edge_fields["node"].type == MyObject
assert isinstance(edge_fields['other'], Field)
assert edge_fields['other'].type == String
assert isinstance(edge_fields["other"], Field)
assert edge_fields["other"].type == String
def test_edge_with_bases():
class BaseEdge(AbstractType):
class BaseEdge:
extra = String()
class MyObjectConnection(Connection):
class Meta:
node = MyObject
@ -85,31 +210,37 @@ def test_edge_with_bases():
other = String()
Edge = MyObjectConnection.Edge
assert Edge._meta.name == 'MyObjectEdge'
assert Edge._meta.name == "MyObjectEdge"
edge_fields = Edge._meta.fields
assert list(edge_fields.keys()) == ['node', 'cursor', 'extra', 'other']
assert list(edge_fields) == ["node", "cursor", "extra", "other"]
assert isinstance(edge_fields['node'], Field)
assert edge_fields['node'].type == MyObject
assert isinstance(edge_fields["node"], Field)
assert edge_fields["node"].type == MyObject
assert isinstance(edge_fields['other'], Field)
assert edge_fields['other'].type == String
assert isinstance(edge_fields["other"], Field)
assert edge_fields["other"].type == String
def test_edge_on_node():
Edge = MyObject.Connection.Edge
assert Edge._meta.name == 'MyObjectEdge'
edge_fields = Edge._meta.fields
assert list(edge_fields.keys()) == ['node', 'cursor']
def test_edge_with_nonnull_node():
class MyObjectConnection(Connection):
class Meta:
node = NonNull(MyObject)
assert isinstance(edge_fields['node'], Field)
assert edge_fields['node'].type == MyObject
edge_fields = MyObjectConnection.Edge._meta.fields
assert isinstance(edge_fields["node"], Field)
assert isinstance(edge_fields["node"].type, NonNull)
assert edge_fields["node"].type.of_type == MyObject
def test_pageinfo():
assert PageInfo._meta.name == 'PageInfo'
assert PageInfo._meta.name == "PageInfo"
fields = PageInfo._meta.fields
assert list(fields.keys()) == ['has_next_page', 'has_previous_page', 'start_cursor', 'end_cursor']
assert list(fields) == [
"has_next_page",
"has_previous_page",
"start_cursor",
"end_cursor",
]
def test_connectionfield():
@ -119,23 +250,69 @@ def test_connectionfield():
field = ConnectionField(MyObjectConnection)
assert field.args == {
'before': Argument(String),
'after': Argument(String),
'first': Argument(Int),
'last': Argument(Int),
"before": Argument(String),
"after": Argument(String),
"first": Argument(Int),
"last": Argument(Int),
}
def test_connectionfield_node_deprecated():
field = ConnectionField(MyObject)
with raises(Exception) as exc_info:
field.type
assert "ConnectionFields now need a explicit ConnectionType for Nodes." in str(
exc_info.value
)
def test_connectionfield_custom_args():
class MyObjectConnection(Connection):
class Meta:
node = MyObject
field = ConnectionField(MyObjectConnection, before=String(required=True), extra=String())
field = ConnectionField(
MyObjectConnection, before=String(required=True), extra=String()
)
assert field.args == {
'before': Argument(NonNull(String)),
'after': Argument(String),
'first': Argument(Int),
'last': Argument(Int),
'extra': Argument(String),
"before": Argument(NonNull(String)),
"after": Argument(String),
"first": Argument(Int),
"last": Argument(Int),
"extra": Argument(String),
}
def test_connectionfield_required():
class MyObjectConnection(Connection):
class Meta:
node = MyObject
class Query(ObjectType):
test_connection = ConnectionField(MyObjectConnection, required=True)
def resolve_test_connection(root, info, **args):
return []
schema = Schema(query=Query)
executed = schema.execute("{ testConnection { edges { cursor } } }")
assert not executed.errors
assert executed.data == {"testConnection": {"edges": []}}
def test_connectionfield_strict_types():
class MyObjectConnection(Connection):
class Meta:
node = MyObject
strict_types = True
connection_field = ConnectionField(MyObjectConnection)
edges_field_type = connection_field.type._meta.fields["edges"].type
assert isinstance(edges_field_type, NonNull)
edges_list_element_type = edges_field_type.of_type.of_type
assert isinstance(edges_list_element_type, NonNull)
node_field = edges_list_element_type.of_type._meta.fields["node"]
assert isinstance(node_field.type, NonNull)

View File

@ -0,0 +1,121 @@
from pytest import mark
from graphql_relay.utils import base64
from graphene.types import ObjectType, Schema, String
from graphene.relay.connection import Connection, ConnectionField, PageInfo
from graphene.relay.node import Node
letter_chars = ["A", "B", "C", "D", "E"]
class Letter(ObjectType):
class Meta:
interfaces = (Node,)
letter = String()
class LetterConnection(Connection):
class Meta:
node = Letter
class Query(ObjectType):
letters = ConnectionField(LetterConnection)
connection_letters = ConnectionField(LetterConnection)
async_letters = ConnectionField(LetterConnection)
node = Node.Field()
def resolve_letters(self, info, **args):
return list(letters.values())
async def resolve_async_letters(self, info, **args):
return list(letters.values())
def resolve_connection_letters(self, info, **args):
return LetterConnection(
page_info=PageInfo(has_next_page=True, has_previous_page=False),
edges=[
LetterConnection.Edge(node=Letter(id=0, letter="A"), cursor="a-cursor")
],
)
schema = Schema(Query)
letters = {letter: Letter(id=i, letter=letter) for i, letter in enumerate(letter_chars)}
def edges(selected_letters):
return [
{
"node": {"id": base64("Letter:%s" % letter.id), "letter": letter.letter},
"cursor": base64("arrayconnection:%s" % letter.id),
}
for letter in [letters[i] for i in selected_letters]
]
def cursor_for(ltr):
letter = letters[ltr]
return base64("arrayconnection:%s" % letter.id)
def execute(args=""):
if args:
args = "(" + args + ")"
return schema.execute(
"""
{
letters%s {
edges {
node {
id
letter
}
cursor
}
pageInfo {
hasPreviousPage
hasNextPage
startCursor
endCursor
}
}
}
"""
% args
)
@mark.asyncio
async def test_connection_async():
result = await schema.execute_async(
"""
{
asyncLetters(first:1) {
edges {
node {
id
letter
}
}
pageInfo {
hasPreviousPage
hasNextPage
}
}
}
"""
)
assert not result.errors
assert result.data == {
"asyncLetters": {
"edges": [{"node": {"id": "TGV0dGVyOjA=", "letter": "A"}}],
"pageInfo": {"hasPreviousPage": False, "hasNextPage": True},
}
}

View File

@ -1,82 +1,73 @@
from collections import OrderedDict
from pytest import mark
from graphql_relay.utils import base64
from promise import Promise
from ...types import ObjectType, Schema, String
from ..connection import ConnectionField, PageInfo
from ..connection import Connection, ConnectionField, PageInfo
from ..node import Node
letter_chars = ['A', 'B', 'C', 'D', 'E']
letter_chars = ["A", "B", "C", "D", "E"]
class Letter(ObjectType):
class Meta:
interfaces = (Node,)
letter = String()
class LetterConnection(Connection):
class Meta:
node = Letter
class Query(ObjectType):
letters = ConnectionField(Letter)
connection_letters = ConnectionField(Letter)
promise_letters = ConnectionField(Letter)
letters = ConnectionField(LetterConnection)
connection_letters = ConnectionField(LetterConnection)
async_letters = ConnectionField(LetterConnection)
node = Node.Field()
def resolve_letters(self, args, context, info):
def resolve_letters(self, info, **args):
return list(letters.values())
def resolve_promise_letters(self, args, context, info):
return Promise.resolve(list(letters.values()))
async def resolve_async_letters(self, info, **args):
return list(letters.values())
def resolve_connection_letters(self, args, context, info):
return Letter.Connection(
page_info=PageInfo(
has_next_page=True,
has_previous_page=False
),
def resolve_connection_letters(self, info, **args):
return LetterConnection(
page_info=PageInfo(has_next_page=True, has_previous_page=False),
edges=[
Letter.Connection.Edge(
node=Letter(id=0, letter='A'),
cursor='a-cursor'
),
]
LetterConnection.Edge(node=Letter(id=0, letter="A"), cursor="a-cursor")
],
)
schema = Schema(Query)
letters = OrderedDict()
for i, letter in enumerate(letter_chars):
l = Letter(id=i, letter=letter)
letters[letter] = l
letters = {letter: Letter(id=i, letter=letter) for i, letter in enumerate(letter_chars)}
def edges(selected_letters):
return [
{
'node': {
'id': base64('Letter:%s' % l.id),
'letter': l.letter
},
'cursor': base64('arrayconnection:%s' % l.id)
"node": {"id": base64("Letter:%s" % letter.id), "letter": letter.letter},
"cursor": base64("arrayconnection:%s" % letter.id),
}
for l in [letters[i] for i in selected_letters]
for letter in [letters[i] for i in selected_letters]
]
def cursor_for(ltr):
l = letters[ltr]
return base64('arrayconnection:%s' % l.id)
letter = letters[ltr]
return base64("arrayconnection:%s" % letter.id)
def execute(args=''):
async def execute(args=""):
if args:
args = '(' + args + ')'
return schema.execute('''
args = "(" + args + ")"
return await schema.execute_async(
"""
{
letters%s {
edges {
@ -94,112 +85,148 @@ def execute(args=''):
}
}
}
''' % args)
"""
% args
)
def check(args, letters, has_previous_page=False, has_next_page=False):
result = execute(args)
async def check(args, letters, has_previous_page=False, has_next_page=False):
result = await execute(args)
expected_edges = edges(letters)
expected_page_info = {
'hasPreviousPage': has_previous_page,
'hasNextPage': has_next_page,
'endCursor': expected_edges[-1]['cursor'] if expected_edges else None,
'startCursor': expected_edges[0]['cursor'] if expected_edges else None
"hasPreviousPage": has_previous_page,
"hasNextPage": has_next_page,
"endCursor": expected_edges[-1]["cursor"] if expected_edges else None,
"startCursor": expected_edges[0]["cursor"] if expected_edges else None,
}
assert not result.errors
assert result.data == {
'letters': {
'edges': expected_edges,
'pageInfo': expected_page_info
}
"letters": {"edges": expected_edges, "pageInfo": expected_page_info}
}
def test_returns_all_elements_without_filters():
check('', 'ABCDE')
@mark.asyncio
async def test_returns_all_elements_without_filters():
await check("", "ABCDE")
def test_respects_a_smaller_first():
check('first: 2', 'AB', has_next_page=True)
@mark.asyncio
async def test_respects_a_smaller_first():
await check("first: 2", "AB", has_next_page=True)
def test_respects_an_overly_large_first():
check('first: 10', 'ABCDE')
@mark.asyncio
async def test_respects_an_overly_large_first():
await check("first: 10", "ABCDE")
def test_respects_a_smaller_last():
check('last: 2', 'DE', has_previous_page=True)
@mark.asyncio
async def test_respects_a_smaller_last():
await check("last: 2", "DE", has_previous_page=True)
def test_respects_an_overly_large_last():
check('last: 10', 'ABCDE')
@mark.asyncio
async def test_respects_an_overly_large_last():
await check("last: 10", "ABCDE")
def test_respects_first_and_after():
check('first: 2, after: "{}"'.format(cursor_for('B')), 'CD', has_next_page=True)
@mark.asyncio
async def test_respects_first_and_after():
await check(f'first: 2, after: "{cursor_for("B")}"', "CD", has_next_page=True)
def test_respects_first_and_after_with_long_first():
check('first: 10, after: "{}"'.format(cursor_for('B')), 'CDE')
@mark.asyncio
async def test_respects_first_and_after_with_long_first():
await check(f'first: 10, after: "{cursor_for("B")}"', "CDE")
def test_respects_last_and_before():
check('last: 2, before: "{}"'.format(cursor_for('D')), 'BC', has_previous_page=True)
@mark.asyncio
async def test_respects_last_and_before():
await check(f'last: 2, before: "{cursor_for("D")}"', "BC", has_previous_page=True)
def test_respects_last_and_before_with_long_last():
check('last: 10, before: "{}"'.format(cursor_for('D')), 'ABC')
@mark.asyncio
async def test_respects_last_and_before_with_long_last():
await check(f'last: 10, before: "{cursor_for("D")}"', "ABC")
def test_respects_first_and_after_and_before_too_few():
check('first: 2, after: "{}", before: "{}"'.format(cursor_for('A'), cursor_for('E')), 'BC', has_next_page=True)
@mark.asyncio
async def test_respects_first_and_after_and_before_too_few():
await check(
f'first: 2, after: "{cursor_for("A")}", before: "{cursor_for("E")}"',
"BC",
has_next_page=True,
)
def test_respects_first_and_after_and_before_too_many():
check('first: 4, after: "{}", before: "{}"'.format(cursor_for('A'), cursor_for('E')), 'BCD')
@mark.asyncio
async def test_respects_first_and_after_and_before_too_many():
await check(
f'first: 4, after: "{cursor_for("A")}", before: "{cursor_for("E")}"', "BCD"
)
def test_respects_first_and_after_and_before_exactly_right():
check('first: 3, after: "{}", before: "{}"'.format(cursor_for('A'), cursor_for('E')), "BCD")
@mark.asyncio
async def test_respects_first_and_after_and_before_exactly_right():
await check(
f'first: 3, after: "{cursor_for("A")}", before: "{cursor_for("E")}"', "BCD"
)
def test_respects_last_and_after_and_before_too_few():
check('last: 2, after: "{}", before: "{}"'.format(cursor_for('A'), cursor_for('E')), 'CD', has_previous_page=True)
@mark.asyncio
async def test_respects_last_and_after_and_before_too_few():
await check(
f'last: 2, after: "{cursor_for("A")}", before: "{cursor_for("E")}"',
"CD",
has_previous_page=True,
)
def test_respects_last_and_after_and_before_too_many():
check('last: 4, after: "{}", before: "{}"'.format(cursor_for('A'), cursor_for('E')), 'BCD')
@mark.asyncio
async def test_respects_last_and_after_and_before_too_many():
await check(
f'last: 4, after: "{cursor_for("A")}", before: "{cursor_for("E")}"', "BCD"
)
def test_respects_last_and_after_and_before_exactly_right():
check('last: 3, after: "{}", before: "{}"'.format(cursor_for('A'), cursor_for('E')), 'BCD')
@mark.asyncio
async def test_respects_last_and_after_and_before_exactly_right():
await check(
f'last: 3, after: "{cursor_for("A")}", before: "{cursor_for("E")}"', "BCD"
)
def test_returns_no_elements_if_first_is_0():
check('first: 0', '', has_next_page=True)
@mark.asyncio
async def test_returns_no_elements_if_first_is_0():
await check("first: 0", "", has_next_page=True)
def test_returns_all_elements_if_cursors_are_invalid():
check('before: "invalid" after: "invalid"', 'ABCDE')
@mark.asyncio
async def test_returns_all_elements_if_cursors_are_invalid():
await check('before: "invalid" after: "invalid"', "ABCDE")
def test_returns_all_elements_if_cursors_are_on_the_outside():
check(
'before: "{}" after: "{}"'.format(
base64(
'arrayconnection:%s' % 6),
base64(
'arrayconnection:%s' % -1)),
'ABCDE')
@mark.asyncio
async def test_returns_all_elements_if_cursors_are_on_the_outside():
await check(
f'before: "{base64("arrayconnection:%s" % 6)}" after: "{base64("arrayconnection:%s" % -1)}"',
"ABCDE",
)
def test_returns_no_elements_if_cursors_cross():
check('before: "{}" after: "{}"'.format(base64('arrayconnection:%s' % 2), base64('arrayconnection:%s' % 4)), '')
@mark.asyncio
async def test_returns_no_elements_if_cursors_cross():
await check(
f'before: "{base64("arrayconnection:%s" % 2)}" after: "{base64("arrayconnection:%s" % 4)}"',
"",
)
def test_connection_type_nodes():
result = schema.execute('''
@mark.asyncio
async def test_connection_type_nodes():
result = await schema.execute_async(
"""
{
connectionLetters {
edges {
@ -215,30 +242,26 @@ def test_connection_type_nodes():
}
}
}
''')
"""
)
assert not result.errors
assert result.data == {
'connectionLetters': {
'edges': [{
'node': {
'id': 'TGV0dGVyOjA=',
'letter': 'A',
},
'cursor': 'a-cursor',
}],
'pageInfo': {
'hasPreviousPage': False,
'hasNextPage': True,
}
"connectionLetters": {
"edges": [
{"node": {"id": "TGV0dGVyOjA=", "letter": "A"}, "cursor": "a-cursor"}
],
"pageInfo": {"hasPreviousPage": False, "hasNextPage": True},
}
}
def test_connection_promise():
result = schema.execute('''
@mark.asyncio
async def test_connection_async():
result = await schema.execute_async(
"""
{
promiseLetters(first:1) {
asyncLetters(first:1) {
edges {
node {
id
@ -251,20 +274,13 @@ def test_connection_promise():
}
}
}
''')
"""
)
assert not result.errors
assert result.data == {
'promiseLetters': {
'edges': [{
'node': {
'id': 'TGV0dGVyOjA=',
'letter': 'A',
},
}],
'pageInfo': {
'hasPreviousPage': False,
'hasNextPage': True,
}
"asyncLetters": {
"edges": [{"node": {"id": "TGV0dGVyOjA=", "letter": "A"}}],
"pageInfo": {"hasPreviousPage": False, "hasNextPage": True},
}
}

View File

@ -0,0 +1,325 @@
import re
from uuid import uuid4
from graphql import graphql_sync
from ..id_type import BaseGlobalIDType, SimpleGlobalIDType, UUIDGlobalIDType
from ..node import Node
from ...types import Int, ObjectType, Schema, String
class TestUUIDGlobalID:
def setup_method(self):
self.user_list = [
{"id": uuid4(), "name": "First"},
{"id": uuid4(), "name": "Second"},
{"id": uuid4(), "name": "Third"},
{"id": uuid4(), "name": "Fourth"},
]
self.users = {user["id"]: user for user in self.user_list}
class CustomNode(Node):
class Meta:
global_id_type = UUIDGlobalIDType
class User(ObjectType):
class Meta:
interfaces = [CustomNode]
name = String()
@classmethod
def get_node(cls, _type, _id):
return self.users[_id]
class RootQuery(ObjectType):
user = CustomNode.Field(User)
self.schema = Schema(query=RootQuery, types=[User])
self.graphql_schema = self.schema.graphql_schema
def test_str_schema_correct(self):
"""
Check that the schema has the expected and custom node interface and user type and that they both use UUIDs
"""
parsed = re.findall(r"(.+) \{\n\s*([\w\W]*?)\n\}", str(self.schema))
types = [t for t, f in parsed]
fields = [f for t, f in parsed]
custom_node_interface = "interface CustomNode"
assert custom_node_interface in types
assert (
'"""The ID of the object"""\n id: UUID!'
== fields[types.index(custom_node_interface)]
)
user_type = "type User implements CustomNode"
assert user_type in types
assert (
'"""The ID of the object"""\n id: UUID!\n name: String'
== fields[types.index(user_type)]
)
def test_get_by_id(self):
query = """query userById($id: UUID!) {
user(id: $id) {
id
name
}
}"""
# UUID need to be converted to string for serialization
result = graphql_sync(
self.graphql_schema,
query,
variable_values={"id": str(self.user_list[0]["id"])},
)
assert not result.errors
assert result.data["user"]["id"] == str(self.user_list[0]["id"])
assert result.data["user"]["name"] == self.user_list[0]["name"]
class TestSimpleGlobalID:
def setup_method(self):
self.user_list = [
{"id": "my global primary key in clear 1", "name": "First"},
{"id": "my global primary key in clear 2", "name": "Second"},
{"id": "my global primary key in clear 3", "name": "Third"},
{"id": "my global primary key in clear 4", "name": "Fourth"},
]
self.users = {user["id"]: user for user in self.user_list}
class CustomNode(Node):
class Meta:
global_id_type = SimpleGlobalIDType
class User(ObjectType):
class Meta:
interfaces = [CustomNode]
name = String()
@classmethod
def get_node(cls, _type, _id):
return self.users[_id]
class RootQuery(ObjectType):
user = CustomNode.Field(User)
self.schema = Schema(query=RootQuery, types=[User])
self.graphql_schema = self.schema.graphql_schema
def test_str_schema_correct(self):
"""
Check that the schema has the expected and custom node interface and user type and that they both use UUIDs
"""
parsed = re.findall(r"(.+) \{\n\s*([\w\W]*?)\n\}", str(self.schema))
types = [t for t, f in parsed]
fields = [f for t, f in parsed]
custom_node_interface = "interface CustomNode"
assert custom_node_interface in types
assert (
'"""The ID of the object"""\n id: ID!'
== fields[types.index(custom_node_interface)]
)
user_type = "type User implements CustomNode"
assert user_type in types
assert (
'"""The ID of the object"""\n id: ID!\n name: String'
== fields[types.index(user_type)]
)
def test_get_by_id(self):
query = """query {
user(id: "my global primary key in clear 3") {
id
name
}
}"""
result = graphql_sync(self.graphql_schema, query)
assert not result.errors
assert result.data["user"]["id"] == self.user_list[2]["id"]
assert result.data["user"]["name"] == self.user_list[2]["name"]
class TestCustomGlobalID:
def setup_method(self):
self.user_list = [
{"id": 1, "name": "First"},
{"id": 2, "name": "Second"},
{"id": 3, "name": "Third"},
{"id": 4, "name": "Fourth"},
]
self.users = {user["id"]: user for user in self.user_list}
class CustomGlobalIDType(BaseGlobalIDType):
"""
Global id that is simply and integer in clear.
"""
graphene_type = Int
@classmethod
def resolve_global_id(cls, info, global_id):
_type = info.return_type.graphene_type._meta.name
return _type, global_id
@classmethod
def to_global_id(cls, _type, _id):
return _id
class CustomNode(Node):
class Meta:
global_id_type = CustomGlobalIDType
class User(ObjectType):
class Meta:
interfaces = [CustomNode]
name = String()
@classmethod
def get_node(cls, _type, _id):
return self.users[_id]
class RootQuery(ObjectType):
user = CustomNode.Field(User)
self.schema = Schema(query=RootQuery, types=[User])
self.graphql_schema = self.schema.graphql_schema
def test_str_schema_correct(self):
"""
Check that the schema has the expected and custom node interface and user type and that they both use UUIDs
"""
parsed = re.findall(r"(.+) \{\n\s*([\w\W]*?)\n\}", str(self.schema))
types = [t for t, f in parsed]
fields = [f for t, f in parsed]
custom_node_interface = "interface CustomNode"
assert custom_node_interface in types
assert (
'"""The ID of the object"""\n id: Int!'
== fields[types.index(custom_node_interface)]
)
user_type = "type User implements CustomNode"
assert user_type in types
assert (
'"""The ID of the object"""\n id: Int!\n name: String'
== fields[types.index(user_type)]
)
def test_get_by_id(self):
query = """query {
user(id: 2) {
id
name
}
}"""
result = graphql_sync(self.graphql_schema, query)
assert not result.errors
assert result.data["user"]["id"] == self.user_list[1]["id"]
assert result.data["user"]["name"] == self.user_list[1]["name"]
class TestIncompleteCustomGlobalID:
def setup_method(self):
self.user_list = [
{"id": 1, "name": "First"},
{"id": 2, "name": "Second"},
{"id": 3, "name": "Third"},
{"id": 4, "name": "Fourth"},
]
self.users = {user["id"]: user for user in self.user_list}
def test_must_define_to_global_id(self):
"""
Test that if the `to_global_id` method is not defined, we can query the object, but we can't request its ID.
"""
class CustomGlobalIDType(BaseGlobalIDType):
graphene_type = Int
@classmethod
def resolve_global_id(cls, info, global_id):
_type = info.return_type.graphene_type._meta.name
return _type, global_id
class CustomNode(Node):
class Meta:
global_id_type = CustomGlobalIDType
class User(ObjectType):
class Meta:
interfaces = [CustomNode]
name = String()
@classmethod
def get_node(cls, _type, _id):
return self.users[_id]
class RootQuery(ObjectType):
user = CustomNode.Field(User)
self.schema = Schema(query=RootQuery, types=[User])
self.graphql_schema = self.schema.graphql_schema
query = """query {
user(id: 2) {
name
}
}"""
result = graphql_sync(self.graphql_schema, query)
assert not result.errors
assert result.data["user"]["name"] == self.user_list[1]["name"]
query = """query {
user(id: 2) {
id
name
}
}"""
result = graphql_sync(self.graphql_schema, query)
assert result.errors is not None
assert len(result.errors) == 1
assert result.errors[0].path == ["user", "id"]
def test_must_define_resolve_global_id(self):
"""
Test that if the `resolve_global_id` method is not defined, we can't query the object by ID.
"""
class CustomGlobalIDType(BaseGlobalIDType):
graphene_type = Int
@classmethod
def to_global_id(cls, _type, _id):
return _id
class CustomNode(Node):
class Meta:
global_id_type = CustomGlobalIDType
class User(ObjectType):
class Meta:
interfaces = [CustomNode]
name = String()
@classmethod
def get_node(cls, _type, _id):
return self.users[_id]
class RootQuery(ObjectType):
user = CustomNode.Field(User)
self.schema = Schema(query=RootQuery, types=[User])
self.graphql_schema = self.schema.graphql_schema
query = """query {
user(id: 2) {
id
name
}
}"""
result = graphql_sync(self.graphql_schema, query)
assert result.errors is not None
assert len(result.errors) == 1
assert result.errors[0].path == ["user"]

View File

@ -1,25 +1,23 @@
from graphql_relay import to_global_id
from ..node import Node, GlobalID
from ...types import NonNull, ID, ObjectType, String
from ...types import ID, NonNull, ObjectType, String
from ...types.definitions import GrapheneObjectType
from ..node import GlobalID, Node
class CustomNode(Node):
class Meta:
name = 'Node'
name = "Node"
class User(ObjectType):
class Meta:
interfaces = [CustomNode]
name = String()
class Info(object):
class Info:
def __init__(self, parent_type):
self.parent_type = GrapheneObjectType(
graphene_type=parent_type,
@ -27,7 +25,7 @@ class Info(object):
description=parent_type._meta.description,
fields=None,
is_type_of=parent_type.is_type_of,
interfaces=None
interfaces=None,
)
@ -45,16 +43,16 @@ def test_global_id_allows_overriding_of_node_and_required():
def test_global_id_defaults_to_info_parent_type():
my_id = '1'
my_id = "1"
gid = GlobalID()
id_resolver = gid.get_resolver(lambda *_: my_id)
my_global_id = id_resolver(None, None, None, Info(User))
id_resolver = gid.wrap_resolve(lambda *_: my_id)
my_global_id = id_resolver(None, Info(User))
assert my_global_id == to_global_id(User._meta.name, my_id)
def test_global_id_allows_setting_customer_parent_type():
my_id = '1'
my_id = "1"
gid = GlobalID(parent_type=User)
id_resolver = gid.get_resolver(lambda *_: my_id)
my_global_id = id_resolver(None, None, None, None)
id_resolver = gid.wrap_resolve(lambda *_: my_id)
my_global_id = id_resolver(None, None)
assert my_global_id == to_global_id(User._meta.name, my_id)

View File

@ -1,52 +1,92 @@
import pytest
from pytest import mark, raises
from ...types import (AbstractType, Argument, Field, InputField,
InputObjectType, NonNull, ObjectType, Schema)
from ...types import (
ID,
Argument,
Field,
InputField,
InputObjectType,
NonNull,
ObjectType,
Schema,
)
from ...types.scalars import String
from ..mutation import ClientIDMutation
from ..node import Node
class SharedFields(AbstractType):
class SharedFields:
shared = String()
class MyNode(ObjectType):
class Meta:
interfaces = (Node, )
# class Meta:
# interfaces = (Node, )
id = ID()
name = String()
class SaySomething(ClientIDMutation):
class Input:
what = String()
phrase = String()
@staticmethod
def mutate_and_get_payload(args, context, info):
what = args.get('what')
def mutate_and_get_payload(self, info, what, client_mutation_id=None):
return SaySomething(phrase=str(what))
class OtherMutation(ClientIDMutation):
class FixedSaySomething:
__slots__ = ("phrase",)
def __init__(self, phrase):
self.phrase = phrase
class SaySomethingFixed(ClientIDMutation):
class Input:
what = String()
phrase = String()
@staticmethod
def mutate_and_get_payload(self, info, what, client_mutation_id=None):
return FixedSaySomething(phrase=str(what))
class SaySomethingAsync(ClientIDMutation):
class Input:
what = String()
phrase = String()
@staticmethod
async def mutate_and_get_payload(self, info, what, client_mutation_id=None):
return SaySomething(phrase=str(what))
# MyEdge = MyNode.Connection.Edge
class MyEdge(ObjectType):
node = Field(MyNode)
cursor = String()
class OtherMutation(ClientIDMutation):
class Input(SharedFields):
additional_field = String()
name = String()
my_node_edge = Field(MyNode.Connection.Edge)
my_node_edge = Field(MyEdge)
@classmethod
def mutate_and_get_payload(cls, args, context, info):
shared = args.get('shared', '')
additionalField = args.get('additionalField', '')
edge_type = MyNode.Connection.Edge
return OtherMutation(name=shared + additionalField,
my_node_edge=edge_type(
cursor='1', node=MyNode(name='name')))
@staticmethod
def mutate_and_get_payload(
self, info, shared="", additional_field="", client_mutation_id=None
):
edge_type = MyEdge
return OtherMutation(
name=shared + additional_field,
my_node_edge=edge_type(cursor="1", node=MyNode(name="name")),
)
class RootQuery(ObjectType):
@ -55,81 +95,112 @@ class RootQuery(ObjectType):
class Mutation(ObjectType):
say = SaySomething.Field()
say_fixed = SaySomethingFixed.Field()
say_async = SaySomethingAsync.Field()
other = OtherMutation.Field()
schema = Schema(query=RootQuery, mutation=Mutation)
def test_no_mutate_and_get_payload():
with pytest.raises(AssertionError) as excinfo:
with raises(AssertionError) as excinfo:
class MyMutation(ClientIDMutation):
pass
assert "MyMutation.mutate_and_get_payload method is required in a ClientIDMutation." == str(
excinfo.value)
assert (
"MyMutation.mutate_and_get_payload method is required in a ClientIDMutation."
== str(excinfo.value)
)
def test_mutation():
fields = SaySomething._meta.fields
assert list(fields.keys()) == ['phrase', 'client_mutation_id']
assert isinstance(fields['phrase'], Field)
assert list(fields) == ["phrase", "client_mutation_id"]
assert SaySomething._meta.name == "SaySomethingPayload"
assert isinstance(fields["phrase"], Field)
field = SaySomething.Field()
assert field.type == SaySomething
assert list(field.args.keys()) == ['input']
assert isinstance(field.args['input'], Argument)
assert isinstance(field.args['input'].type, NonNull)
assert field.args['input'].type.of_type == SaySomething.Input
assert isinstance(fields['client_mutation_id'], Field)
assert fields['client_mutation_id'].name == 'clientMutationId'
assert fields['client_mutation_id'].type == String
assert list(field.args) == ["input"]
assert isinstance(field.args["input"], Argument)
assert isinstance(field.args["input"].type, NonNull)
assert field.args["input"].type.of_type == SaySomething.Input
assert isinstance(fields["client_mutation_id"], Field)
assert fields["client_mutation_id"].name == "clientMutationId"
assert fields["client_mutation_id"].type == String
def test_mutation_input():
Input = SaySomething.Input
assert issubclass(Input, InputObjectType)
fields = Input._meta.fields
assert list(fields.keys()) == ['what', 'client_mutation_id']
assert isinstance(fields['what'], InputField)
assert fields['what'].type == String
assert isinstance(fields['client_mutation_id'], InputField)
assert fields['client_mutation_id'].type == String
assert list(fields) == ["what", "client_mutation_id"]
assert isinstance(fields["what"], InputField)
assert fields["what"].type == String
assert isinstance(fields["client_mutation_id"], InputField)
assert fields["client_mutation_id"].type == String
def test_subclassed_mutation():
fields = OtherMutation._meta.fields
assert list(fields.keys()) == ['name', 'my_node_edge', 'client_mutation_id']
assert isinstance(fields['name'], Field)
assert list(fields) == ["name", "my_node_edge", "client_mutation_id"]
assert isinstance(fields["name"], Field)
field = OtherMutation.Field()
assert field.type == OtherMutation
assert list(field.args.keys()) == ['input']
assert isinstance(field.args['input'], Argument)
assert isinstance(field.args['input'].type, NonNull)
assert field.args['input'].type.of_type == OtherMutation.Input
assert list(field.args) == ["input"]
assert isinstance(field.args["input"], Argument)
assert isinstance(field.args["input"].type, NonNull)
assert field.args["input"].type.of_type == OtherMutation.Input
def test_subclassed_mutation_input():
Input = OtherMutation.Input
assert issubclass(Input, InputObjectType)
fields = Input._meta.fields
assert list(fields.keys()) == ['shared', 'additional_field', 'client_mutation_id']
assert isinstance(fields['shared'], InputField)
assert fields['shared'].type == String
assert isinstance(fields['additional_field'], InputField)
assert fields['additional_field'].type == String
assert isinstance(fields['client_mutation_id'], InputField)
assert fields['client_mutation_id'].type == String
assert list(fields) == ["shared", "additional_field", "client_mutation_id"]
assert isinstance(fields["shared"], InputField)
assert fields["shared"].type == String
assert isinstance(fields["additional_field"], InputField)
assert fields["additional_field"].type == String
assert isinstance(fields["client_mutation_id"], InputField)
assert fields["client_mutation_id"].type == String
# def test_node_query():
# executed = schema.execute(
# 'mutation a { say(input: {what:"hello", clientMutationId:"1"}) { phrase } }'
# )
# assert not executed.errors
# assert executed.data == {'say': {'phrase': 'hello'}}
def test_node_query():
executed = schema.execute(
'mutation a { say(input: {what:"hello", clientMutationId:"1"}) { phrase } }'
)
assert not executed.errors
assert executed.data == {"say": {"phrase": "hello"}}
def test_node_query_fixed():
executed = schema.execute(
'mutation a { sayFixed(input: {what:"hello", clientMutationId:"1"}) { phrase } }'
)
assert "Cannot set client_mutation_id in the payload object" in str(
executed.errors[0]
)
@mark.asyncio
async def test_node_query_async():
executed = await schema.execute_async(
'mutation a { sayAsync(input: {what:"hello", clientMutationId:"1"}) { phrase } }'
)
assert not executed.errors
assert executed.data == {"sayAsync": {"phrase": "hello"}}
def test_edge_query():
executed = schema.execute(
'mutation a { other(input: {clientMutationId:"1"}) { clientMutationId, myNodeEdge { cursor node { name }} } }'
)
assert not executed.errors
assert dict(executed.data) == {'other': {'clientMutationId': '1', 'myNodeEdge': {'cursor': '1', 'node': {'name': 'name'}}}}
assert dict(executed.data) == {
"other": {
"clientMutationId": "1",
"myNodeEdge": {"cursor": "1", "node": {"name": "name"}},
}
}

View File

@ -0,0 +1,90 @@
from pytest import mark
from graphene.types import ID, Field, ObjectType, Schema
from graphene.types.scalars import String
from graphene.relay.mutation import ClientIDMutation
from graphene.test import Client
class SharedFields(object):
shared = String()
class MyNode(ObjectType):
# class Meta:
# interfaces = (Node, )
id = ID()
name = String()
class SaySomethingAsync(ClientIDMutation):
class Input:
what = String()
phrase = String()
@staticmethod
async def mutate_and_get_payload(self, info, what, client_mutation_id=None):
return SaySomethingAsync(phrase=str(what))
# MyEdge = MyNode.Connection.Edge
class MyEdge(ObjectType):
node = Field(MyNode)
cursor = String()
class OtherMutation(ClientIDMutation):
class Input(SharedFields):
additional_field = String()
name = String()
my_node_edge = Field(MyEdge)
@staticmethod
def mutate_and_get_payload(
self, info, shared="", additional_field="", client_mutation_id=None
):
edge_type = MyEdge
return OtherMutation(
name=shared + additional_field,
my_node_edge=edge_type(cursor="1", node=MyNode(name="name")),
)
class RootQuery(ObjectType):
something = String()
class Mutation(ObjectType):
say_promise = SaySomethingAsync.Field()
other = OtherMutation.Field()
schema = Schema(query=RootQuery, mutation=Mutation)
client = Client(schema)
@mark.asyncio
async def test_node_query_promise():
executed = await client.execute_async(
'mutation a { sayPromise(input: {what:"hello", clientMutationId:"1"}) { phrase } }'
)
assert isinstance(executed, dict)
assert "errors" not in executed
assert executed["data"] == {"sayPromise": {"phrase": "hello"}}
@mark.asyncio
async def test_edge_query():
executed = await client.execute_async(
'mutation a { other(input: {clientMutationId:"1"}) { clientMutationId, myNodeEdge { cursor node { name }} } }'
)
assert isinstance(executed, dict)
assert "errors" not in executed
assert executed["data"] == {
"other": {
"clientMutationId": "1",
"myNodeEdge": {"cursor": "1", "node": {"name": "name"}},
}
}

View File

@ -1,29 +1,28 @@
from collections import OrderedDict
import re
from textwrap import dedent
from graphql_relay import to_global_id
from ...types import AbstractType, ObjectType, Schema, String
from ..connection import Connection
from ..node import Node
from ...types import ObjectType, Schema, String
from ..node import Node, is_node
class SharedNodeFields(AbstractType):
class SharedNodeFields:
shared = String()
something_else = String()
def resolve_something_else(*_):
return '----'
return "----"
class MyNode(ObjectType):
class Meta:
interfaces = (Node,)
name = String()
@staticmethod
def get_node(id, *_):
def get_node(info, id):
return MyNode(name=str(id))
@ -34,10 +33,10 @@ class MyOtherNode(SharedNodeFields, ObjectType):
interfaces = (Node,)
def resolve_extra_field(self, *_):
return 'extra field info.'
return "extra field info."
@staticmethod
def get_node(id, *_):
def get_node(info, id):
return MyOtherNode(shared=str(id))
@ -47,20 +46,15 @@ class RootQuery(ObjectType):
only_node = Node.Field(MyNode)
only_node_lazy = Node.Field(lambda: MyNode)
schema = Schema(query=RootQuery, types=[MyNode, MyOtherNode])
def test_node_good():
assert 'id' in MyNode._meta.fields
def test_node_get_connection():
connection = MyNode.Connection
assert issubclass(connection, Connection)
def test_node_get_connection_dont_duplicate():
assert MyNode.Connection == MyNode.Connection
assert "id" in MyNode._meta.fields
assert is_node(MyNode)
assert not is_node(object)
assert not is_node("node")
def test_node_query():
@ -68,24 +62,52 @@ def test_node_query():
'{ node(id:"%s") { ... on MyNode { name } } }' % Node.to_global_id("MyNode", 1)
)
assert not executed.errors
assert executed.data == {'node': {'name': '1'}}
assert executed.data == {"node": {"name": "1"}}
def test_subclassed_node_query():
executed = schema.execute(
'{ node(id:"%s") { ... on MyOtherNode { shared, extraField, somethingElse } } }' %
to_global_id("MyOtherNode", 1))
'{ node(id:"%s") { ... on MyOtherNode { shared, extraField, somethingElse } } }'
% to_global_id("MyOtherNode", 1)
)
assert not executed.errors
assert executed.data == OrderedDict({'node': OrderedDict(
[('shared', '1'), ('extraField', 'extra field info.'), ('somethingElse', '----')])})
assert executed.data == {
"node": {
"shared": "1",
"extraField": "extra field info.",
"somethingElse": "----",
}
}
def test_node_requesting_non_node():
executed = schema.execute(
'{ node(id:"%s") { __typename } } ' % Node.to_global_id("RootQuery", 1)
)
assert executed.errors
assert re.match(
r"ObjectType .* does not implement the .* interface.",
executed.errors[0].message,
)
assert executed.data == {"node": None}
def test_node_requesting_unknown_type():
executed = schema.execute(
'{ node(id:"%s") { __typename } } ' % Node.to_global_id("UnknownType", 1)
)
assert executed.errors
assert re.match(r"Relay Node .* not found in schema", executed.errors[0].message)
assert executed.data == {"node": None}
def test_node_query_incorrect_id():
executed = schema.execute(
'{ node(id:"%s") { ... on MyNode { name } } }' % "something:2"
)
assert not executed.errors
assert executed.data == {'node': None}
assert executed.errors
assert re.match(r"Unable to parse global ID .*", executed.errors[0].message)
assert executed.data == {"node": None}
def test_node_field():
@ -100,66 +122,98 @@ def test_node_field_custom():
assert node_field.node_type == Node
def test_node_field_args():
field_args = {
"name": "my_custom_name",
"description": "my_custom_description",
"deprecation_reason": "my_custom_deprecation_reason",
}
node_field = Node.Field(**field_args)
for field_arg, value in field_args.items():
assert getattr(node_field, field_arg) == value
def test_node_field_only_type():
executed = schema.execute(
'{ onlyNode(id:"%s") { __typename, name } } ' % Node.to_global_id("MyNode", 1)
)
assert not executed.errors
assert executed.data == {'onlyNode': {'__typename': 'MyNode', 'name': '1'}}
assert executed.data == {"onlyNode": {"__typename": "MyNode", "name": "1"}}
def test_node_field_only_type_wrong():
executed = schema.execute(
'{ onlyNode(id:"%s") { __typename, name } } ' % Node.to_global_id("MyOtherNode", 1)
'{ onlyNode(id:"%s") { __typename, name } } '
% Node.to_global_id("MyOtherNode", 1)
)
assert len(executed.errors) == 1
assert str(executed.errors[0]) == 'Must receive an MyOtherNode id.'
assert executed.data == { 'onlyNode': None }
assert str(executed.errors[0]).startswith("Must receive a MyNode id.")
assert executed.data == {"onlyNode": None}
def test_node_field_only_lazy_type():
executed = schema.execute(
'{ onlyNodeLazy(id:"%s") { __typename, name } } ' % Node.to_global_id("MyNode", 1)
'{ onlyNodeLazy(id:"%s") { __typename, name } } '
% Node.to_global_id("MyNode", 1)
)
assert not executed.errors
assert executed.data == {'onlyNodeLazy': {'__typename': 'MyNode', 'name': '1'}}
assert executed.data == {"onlyNodeLazy": {"__typename": "MyNode", "name": "1"}}
def test_node_field_only_lazy_type_wrong():
executed = schema.execute(
'{ onlyNodeLazy(id:"%s") { __typename, name } } ' % Node.to_global_id("MyOtherNode", 1)
'{ onlyNodeLazy(id:"%s") { __typename, name } } '
% Node.to_global_id("MyOtherNode", 1)
)
assert len(executed.errors) == 1
assert str(executed.errors[0]) == 'Must receive an MyOtherNode id.'
assert executed.data == { 'onlyNodeLazy': None }
assert str(executed.errors[0]).startswith("Must receive a MyNode id.")
assert executed.data == {"onlyNodeLazy": None}
def test_str_schema():
assert str(schema) == """
assert (
str(schema).strip()
== dedent(
'''
schema {
query: RootQuery
}
type MyNode implements Node {
"""The ID of the object"""
id: ID!
name: String
}
"""An object with an ID"""
interface Node {
"""The ID of the object"""
id: ID!
}
type MyOtherNode implements Node {
"""The ID of the object"""
id: ID!
shared: String
somethingElse: String
extraField: String
}
interface Node {
id: ID!
}
type RootQuery {
first: String
node(id: ID!): Node
onlyNode(id: ID!): MyNode
onlyNodeLazy(id: ID!): MyNode
node(
"""The ID of the object"""
id: ID!
): Node
onlyNode(
"""The ID of the object"""
id: ID!
): MyNode
onlyNodeLazy(
"""The ID of the object"""
id: ID!
): MyNode
}
""".lstrip()
'''
).strip()
)

View File

@ -1,4 +1,6 @@
from graphql import graphql
from textwrap import dedent
from graphql import graphql_sync
from ...types import Interface, ObjectType, Schema
from ...types.scalars import Int, String
@ -6,17 +8,16 @@ from ..node import Node
class CustomNode(Node):
class Meta:
name = 'Node'
name = "Node"
@staticmethod
def to_global_id(type, id):
def to_global_id(type_, id):
return id
@staticmethod
def get_node_from_global_id(id, context, info, only_type=None):
assert info.schema == schema
def get_node_from_global_id(info, id, only_type=None):
assert info.schema is graphql_schema
if id in user_data:
return user_data.get(id)
else:
@ -24,106 +25,110 @@ class CustomNode(Node):
class BasePhoto(Interface):
width = Int()
width = Int(description="The width of the photo in pixels")
class User(ObjectType):
class Meta:
interfaces = [CustomNode]
name = String()
name = String(description="The full name of the user")
class Photo(ObjectType):
class Meta:
interfaces = [CustomNode, BasePhoto]
user_data = {
'1': User(id='1', name='John Doe'),
'2': User(id='2', name='Jane Smith'),
}
user_data = {"1": User(id="1", name="John Doe"), "2": User(id="2", name="Jane Smith")}
photo_data = {
'3': Photo(id='3', width=300),
'4': Photo(id='4', width=400),
}
photo_data = {"3": Photo(id="3", width=300), "4": Photo(id="4", width=400)}
class RootQuery(ObjectType):
node = CustomNode.Field()
schema = Schema(query=RootQuery, types=[User, Photo])
graphql_schema = schema.graphql_schema
def test_str_schema_correct():
assert str(schema) == '''schema {
assert (
str(schema).strip()
== dedent(
'''
schema {
query: RootQuery
}
interface BasePhoto {
width: Int
type User implements Node {
"""The ID of the object"""
id: ID!
"""The full name of the user"""
name: String
}
interface Node {
"""The ID of the object"""
id: ID!
}
type Photo implements Node, BasePhoto {
type Photo implements Node & BasePhoto {
"""The ID of the object"""
id: ID!
"""The width of the photo in pixels"""
width: Int
}
interface BasePhoto {
"""The width of the photo in pixels"""
width: Int
}
type RootQuery {
node(id: ID!): Node
}
type User implements Node {
node(
"""The ID of the object"""
id: ID!
name: String
): Node
}
'''
).strip()
)
def test_gets_the_correct_id_for_users():
query = '''
query = """
{
node(id: "1") {
id
}
}
'''
expected = {
'node': {
'id': '1',
}
}
result = graphql(schema, query)
"""
expected = {"node": {"id": "1"}}
result = graphql_sync(graphql_schema, query)
assert not result.errors
assert result.data == expected
def test_gets_the_correct_id_for_photos():
query = '''
query = """
{
node(id: "4") {
id
}
}
'''
expected = {
'node': {
'id': '4',
}
}
result = graphql(schema, query)
"""
expected = {"node": {"id": "4"}}
result = graphql_sync(graphql_schema, query)
assert not result.errors
assert result.data == expected
def test_gets_the_correct_name_for_users():
query = '''
query = """
{
node(id: "1") {
id
@ -132,20 +137,15 @@ def test_gets_the_correct_name_for_users():
}
}
}
'''
expected = {
'node': {
'id': '1',
'name': 'John Doe'
}
}
result = graphql(schema, query)
"""
expected = {"node": {"id": "1", "name": "John Doe"}}
result = graphql_sync(graphql_schema, query)
assert not result.errors
assert result.data == expected
def test_gets_the_correct_width_for_photos():
query = '''
query = """
{
node(id: "4") {
id
@ -154,60 +154,45 @@ def test_gets_the_correct_width_for_photos():
}
}
}
'''
expected = {
'node': {
'id': '4',
'width': 400
}
}
result = graphql(schema, query)
"""
expected = {"node": {"id": "4", "width": 400}}
result = graphql_sync(graphql_schema, query)
assert not result.errors
assert result.data == expected
def test_gets_the_correct_typename_for_users():
query = '''
query = """
{
node(id: "1") {
id
__typename
}
}
'''
expected = {
'node': {
'id': '1',
'__typename': 'User'
}
}
result = graphql(schema, query)
"""
expected = {"node": {"id": "1", "__typename": "User"}}
result = graphql_sync(graphql_schema, query)
assert not result.errors
assert result.data == expected
def test_gets_the_correct_typename_for_photos():
query = '''
query = """
{
node(id: "4") {
id
__typename
}
}
'''
expected = {
'node': {
'id': '4',
'__typename': 'Photo'
}
}
result = graphql(schema, query)
"""
expected = {"node": {"id": "4", "__typename": "Photo"}}
result = graphql_sync(graphql_schema, query)
assert not result.errors
assert result.data == expected
def test_ignores_photo_fragments_on_user():
query = '''
query = """
{
node(id: "1") {
id
@ -216,35 +201,29 @@ def test_ignores_photo_fragments_on_user():
}
}
}
'''
expected = {
'node': {
'id': '1',
}
}
result = graphql(schema, query)
"""
expected = {"node": {"id": "1"}}
result = graphql_sync(graphql_schema, query)
assert not result.errors
assert result.data == expected
def test_returns_null_for_bad_ids():
query = '''
query = """
{
node(id: "5") {
id
}
}
'''
expected = {
'node': None
}
result = graphql(schema, query)
"""
expected = {"node": None}
result = graphql_sync(graphql_schema, query)
assert not result.errors
assert result.data == expected
def test_have_correct_node_interface():
query = '''
query = """
{
__type(name: "Node") {
name
@ -261,32 +240,29 @@ def test_have_correct_node_interface():
}
}
}
'''
"""
expected = {
'__type': {
'name': 'Node',
'kind': 'INTERFACE',
'fields': [
"__type": {
"name": "Node",
"kind": "INTERFACE",
"fields": [
{
'name': 'id',
'type': {
'kind': 'NON_NULL',
'ofType': {
'name': 'ID',
'kind': 'SCALAR'
"name": "id",
"type": {
"kind": "NON_NULL",
"ofType": {"name": "ID", "kind": "SCALAR"},
},
}
],
}
}
}
]
}
}
result = graphql(schema, query)
result = graphql_sync(graphql_schema, query)
assert not result.errors
assert result.data == expected
def test_has_correct_node_root_field():
query = '''
query = """
{
__schema {
queryType {
@ -310,34 +286,28 @@ def test_has_correct_node_root_field():
}
}
}
'''
"""
expected = {
'__schema': {
'queryType': {
'fields': [
"__schema": {
"queryType": {
"fields": [
{
'name': 'node',
'type': {
'name': 'Node',
'kind': 'INTERFACE'
"name": "node",
"type": {"name": "Node", "kind": "INTERFACE"},
"args": [
{
"name": "id",
"type": {
"kind": "NON_NULL",
"ofType": {"name": "ID", "kind": "SCALAR"},
},
'args': [
{
'name': 'id',
'type': {
'kind': 'NON_NULL',
'ofType': {
'name': 'ID',
'kind': 'SCALAR'
}
}
],
}
]
}
]
}
}
}
result = graphql(schema, query)
result = graphql_sync(graphql_schema, query)
assert not result.errors
assert result.data == expected

View File

@ -1,5 +1,3 @@
import six
from graphql.error import format_error as format_graphql_error
from graphql.error import GraphQLError
from graphene.types.schema import Schema
@ -7,33 +5,35 @@ from graphene.types.schema import Schema
def default_format_error(error):
if isinstance(error, GraphQLError):
return format_graphql_error(error)
return {'message': six.text_type(error)}
return error.formatted
return {"message": str(error)}
def format_execution_result(execution_result, format_error):
if execution_result:
response = {}
if execution_result.errors:
response['errors'] = [format_error(e) for e in execution_result.errors]
if not execution_result.invalid:
response['data'] = execution_result.data
response["errors"] = [format_error(e) for e in execution_result.errors]
response["data"] = execution_result.data
return response
class Client(object):
class Client:
def __init__(self, schema, format_error=None, **execute_options):
assert isinstance(schema, Schema)
self.schema = schema
self.execute_options = execute_options
self.format_error = format_error or default_format_error
def format_result(self, result):
return format_execution_result(result, self.format_error)
def execute(self, *args, **kwargs):
return format_execution_result(
self.schema.execute(*args, **dict(self.execute_options, **kwargs)),
self.format_error
executed = self.schema.execute(*args, **dict(self.execute_options, **kwargs))
return self.format_result(executed)
async def execute_async(self, *args, **kwargs):
executed = await self.schema.execute_async(
*args, **dict(self.execute_options, **kwargs)
)
return self.format_result(executed)

View File

@ -0,0 +1,41 @@
# https://github.com/graphql-python/graphene/issues/1293
from datetime import datetime, timezone
import graphene
from graphql.utilities import print_schema
class Filters(graphene.InputObjectType):
datetime_after = graphene.DateTime(
required=False,
default_value=datetime.fromtimestamp(1434549820.776, timezone.utc),
)
datetime_before = graphene.DateTime(
required=False,
default_value=datetime.fromtimestamp(1444549820.776, timezone.utc),
)
class SetDatetime(graphene.Mutation):
class Arguments:
filters = Filters(required=True)
ok = graphene.Boolean()
def mutate(root, info, filters):
return SetDatetime(ok=True)
class Query(graphene.ObjectType):
goodbye = graphene.String()
class Mutations(graphene.ObjectType):
set_datetime = SetDatetime.Field()
def test_schema_printable_with_default_datetime_value():
schema = graphene.Schema(query=Query, mutation=Mutations)
schema_str = print_schema(schema.graphql_schema)
assert schema_str, "empty schema printed"

View File

@ -0,0 +1,36 @@
from ...types import ObjectType, Schema, String, NonNull
class Query(ObjectType):
hello = String(input=NonNull(String))
def resolve_hello(self, info, input):
if input == "nothing":
return None
return f"Hello {input}!"
schema = Schema(query=Query)
def test_required_input_provided():
"""
Test that a required argument works when provided.
"""
input_value = "Potato"
result = schema.execute('{ hello(input: "%s") }' % input_value)
assert not result.errors
assert result.data == {"hello": "Hello Potato!"}
def test_required_input_missing():
"""
Test that a required argument raised an error if not provided.
"""
result = schema.execute("{ hello }")
assert result.errors
assert len(result.errors) == 1
assert (
result.errors[0].message
== "Field 'hello' argument 'input' of type 'String!' is required, but it was not provided."
)

View File

@ -0,0 +1,53 @@
import pytest
from ...types.base64 import Base64
from ...types.datetime import Date, DateTime
from ...types.decimal import Decimal
from ...types.generic import GenericScalar
from ...types.json import JSONString
from ...types.objecttype import ObjectType
from ...types.scalars import ID, BigInt, Boolean, Float, Int, String
from ...types.schema import Schema
from ...types.uuid import UUID
@pytest.mark.parametrize(
"input_type,input_value",
[
(Date, '"2022-02-02"'),
(GenericScalar, '"foo"'),
(Int, "1"),
(BigInt, "12345678901234567890"),
(Float, "1.1"),
(String, '"foo"'),
(Boolean, "true"),
(ID, "1"),
(DateTime, '"2022-02-02T11:11:11"'),
(UUID, '"cbebbc62-758e-4f75-a890-bc73b5017d81"'),
(Decimal, '"1.1"'),
(JSONString, '"{\\"key\\":\\"foo\\",\\"value\\":\\"bar\\"}"'),
(Base64, '"Q2hlbG8gd29ycmxkCg=="'),
],
)
def test_parse_literal_with_variables(input_type, input_value):
# input_b needs to be evaluated as literal while the variable dict for
# input_a is passed along.
class Query(ObjectType):
generic = GenericScalar(input_a=GenericScalar(), input_b=input_type())
def resolve_generic(self, info, input_a=None, input_b=None):
return input
schema = Schema(query=Query)
query = f"""
query Test($a: GenericScalar){{
generic(inputA: $a, inputB: {input_value})
}}
"""
result = schema.execute(
query,
variables={"a": "bar"},
)
assert not result.errors

View File

@ -1,11 +1,12 @@
# https://github.com/graphql-python/graphene/issues/313
import graphene
from graphene import resolve_only_args
class Query(graphene.ObjectType):
rand = graphene.String()
class Success(graphene.ObjectType):
yeah = graphene.String()
@ -20,14 +21,13 @@ class CreatePostResult(graphene.Union):
class CreatePost(graphene.Mutation):
class Input:
class Arguments:
text = graphene.String(required=True)
result = graphene.Field(CreatePostResult)
@resolve_only_args
def mutate(self, text):
result = Success(yeah='yeah')
def mutate(self, info, text):
result = Success(yeah="yeah")
return CreatePost(result=result)
@ -35,10 +35,12 @@ class CreatePost(graphene.Mutation):
class Mutations(graphene.ObjectType):
create_post = CreatePost.Field()
# tests.py
def test_create_post():
query_string = '''
query_string = """
mutation {
createPost(text: "Try this out") {
result {
@ -46,10 +48,10 @@ def test_create_post():
}
}
}
'''
"""
schema = graphene.Schema(query=Query, mutation=Mutations)
result = schema.execute(query_string)
assert not result.errors
assert result.data['createPost']['result']['__typename'] == 'Success'
assert result.data["createPost"]["result"]["__typename"] == "Success"

View File

@ -1,24 +1,33 @@
# https://github.com/graphql-python/graphene/issues/356
import pytest
from pytest import raises
import graphene
from graphene import relay
class SomeTypeOne(graphene.ObjectType):
pass
class SomeTypeTwo(graphene.ObjectType):
pass
class MyUnion(graphene.Union):
class Meta:
types = (SomeTypeOne, SomeTypeTwo)
def test_issue():
with pytest.raises(Exception) as exc_info:
class Query(graphene.ObjectType):
things = relay.ConnectionField(MyUnion)
schema = graphene.Schema(query=Query)
with raises(Exception) as exc_info:
graphene.Schema(query=Query)
assert str(exc_info.value) == 'IterableConnectionField type have to be a subclass of Connection. Received "MyUnion".'
assert str(exc_info.value) == (
"Query fields cannot be resolved."
" IterableConnectionField type has to be a subclass of Connection."
' Received "MyUnion".'
)

View File

@ -1,53 +1,117 @@
# https://github.com/graphql-python/graphene/issues/425
import six
# Adapted for Graphene 2.0
from graphene.utils.is_base_type import is_base_type
from graphene.types.enum import Enum, EnumOptions
from graphene.types.inputobjecttype import InputObjectType
from graphene.types.objecttype import ObjectType, ObjectTypeOptions
from graphene.types.objecttype import ObjectTypeMeta, ObjectType
from graphene.types.options import Options
class SpecialObjectTypeMeta(ObjectTypeMeta):
# ObjectType
class SpecialOptions(ObjectTypeOptions):
other_attr = None
@staticmethod
def __new__(cls, name, bases, attrs):
# Also ensure initialization is only performed for subclasses of
# DjangoObjectType
if not is_base_type(bases, SpecialObjectTypeMeta):
return type.__new__(cls, name, bases, attrs)
options = Options(
attrs.pop('Meta', None),
other_attr='default',
class SpecialObjectType(ObjectType):
@classmethod
def __init_subclass_with_meta__(cls, other_attr="default", **options):
_meta = SpecialOptions(cls)
_meta.other_attr = other_attr
super(SpecialObjectType, cls).__init_subclass_with_meta__(
_meta=_meta, **options
)
cls = ObjectTypeMeta.__new__(cls, name, bases, dict(attrs, _meta=options))
assert cls._meta is options
return cls
class SpecialObjectType(six.with_metaclass(SpecialObjectTypeMeta, ObjectType)):
pass
def test_special_objecttype_could_be_subclassed():
class MyType(SpecialObjectType):
class Meta:
other_attr = 'yeah!'
other_attr = "yeah!"
assert MyType._meta.other_attr == 'yeah!'
assert MyType._meta.other_attr == "yeah!"
def test_special_objecttype_could_be_subclassed_default():
class MyType(SpecialObjectType):
pass
assert MyType._meta.other_attr == 'default'
assert MyType._meta.other_attr == "default"
def test_special_objecttype_inherit_meta_options():
class MyType(SpecialObjectType):
pass
assert MyType._meta.name == 'MyType'
assert MyType._meta.default_resolver == None
assert MyType._meta.name == "MyType"
assert MyType._meta.default_resolver is None
assert MyType._meta.interfaces == ()
# InputObjectType
class SpecialInputObjectTypeOptions(ObjectTypeOptions):
other_attr = None
class SpecialInputObjectType(InputObjectType):
@classmethod
def __init_subclass_with_meta__(cls, other_attr="default", **options):
_meta = SpecialInputObjectTypeOptions(cls)
_meta.other_attr = other_attr
super(SpecialInputObjectType, cls).__init_subclass_with_meta__(
_meta=_meta, **options
)
def test_special_inputobjecttype_could_be_subclassed():
class MyInputObjectType(SpecialInputObjectType):
class Meta:
other_attr = "yeah!"
assert MyInputObjectType._meta.other_attr == "yeah!"
def test_special_inputobjecttype_could_be_subclassed_default():
class MyInputObjectType(SpecialInputObjectType):
pass
assert MyInputObjectType._meta.other_attr == "default"
def test_special_inputobjecttype_inherit_meta_options():
class MyInputObjectType(SpecialInputObjectType):
pass
assert MyInputObjectType._meta.name == "MyInputObjectType"
# Enum
class SpecialEnumOptions(EnumOptions):
other_attr = None
class SpecialEnum(Enum):
@classmethod
def __init_subclass_with_meta__(cls, other_attr="default", **options):
_meta = SpecialEnumOptions(cls)
_meta.other_attr = other_attr
super(SpecialEnum, cls).__init_subclass_with_meta__(_meta=_meta, **options)
def test_special_enum_could_be_subclassed():
class MyEnum(SpecialEnum):
class Meta:
other_attr = "yeah!"
assert MyEnum._meta.other_attr == "yeah!"
def test_special_enum_could_be_subclassed_default():
class MyEnum(SpecialEnum):
pass
assert MyEnum._meta.other_attr == "default"
def test_special_enum_inherit_meta_options():
class MyEnum(SpecialEnum):
pass
assert MyEnum._meta.name == "MyEnum"

View File

@ -0,0 +1,24 @@
# https://github.com/graphql-python/graphene/issues/313
import graphene
class Query(graphene.ObjectType):
some_field = graphene.String(from_=graphene.String(name="from"))
def resolve_some_field(self, info, from_=None):
return from_
def test_issue():
query_string = """
query myQuery {
someField(from: "Oh")
}
"""
schema = graphene.Schema(query=Query)
result = schema.execute(query_string)
assert not result.errors
assert result.data["someField"] == "Oh"

View File

@ -0,0 +1,44 @@
# https://github.com/graphql-python/graphene/issues/720
# InputObjectTypes overwrite the "fields" attribute of the provided
# _meta object, so even if dynamic fields are provided with a standard
# InputObjectTypeOptions, they are ignored.
import graphene
class MyInputClass(graphene.InputObjectType):
@classmethod
def __init_subclass_with_meta__(
cls, container=None, _meta=None, fields=None, **options
):
if _meta is None:
_meta = graphene.types.inputobjecttype.InputObjectTypeOptions(cls)
_meta.fields = fields
super(MyInputClass, cls).__init_subclass_with_meta__(
container=container, _meta=_meta, **options
)
class MyInput(MyInputClass):
class Meta:
fields = dict(x=graphene.Field(graphene.Int))
class Query(graphene.ObjectType):
myField = graphene.Field(graphene.String, input=graphene.Argument(MyInput))
def resolve_myField(parent, info, input):
return "ok"
def test_issue():
query_string = """
query myQuery {
myField(input: {x: 1})
}
"""
schema = graphene.Schema(query=Query)
result = schema.execute(query_string)
assert not result.errors

View File

@ -0,0 +1,27 @@
import pickle
from ...types.enum import Enum
class PickleEnum(Enum):
# is defined outside of test because pickle unable to dump class inside ot pytest function
A = "a"
B = 1
def test_enums_pickling():
a = PickleEnum.A
pickled = pickle.dumps(a)
restored = pickle.loads(pickled)
assert type(a) is type(restored)
assert a == restored
assert a.value == restored.value
assert a.name == restored.name
b = PickleEnum.B
pickled = pickle.dumps(b)
restored = pickle.loads(pickled)
assert type(a) is type(restored)
assert b == restored
assert b.value == restored.value
assert b.name == restored.name

View File

@ -0,0 +1,8 @@
import graphene
def test_issue():
options = {"description": "This my enum", "deprecation_reason": "For the funs"}
new_enum = graphene.Enum("MyEnum", [("some", "data")], **options)
assert new_enum._meta.description == options["description"]
assert new_enum._meta.deprecation_reason == options["deprecation_reason"]

View File

@ -1,40 +1,53 @@
# flake8: noqa
from graphql import GraphQLResolveInfo as ResolveInfo
from .objecttype import ObjectType
from .abstracttype import AbstractType
from .interface import Interface
from .mutation import Mutation
from .scalars import Scalar, String, ID, Int, Float, Boolean
from .schema import Schema
from .structures import List, NonNull
from .argument import Argument
from .base64 import Base64
from .context import Context
from .datetime import Date, DateTime, Time
from .decimal import Decimal
from .dynamic import Dynamic
from .enum import Enum
from .field import Field
from .inputfield import InputField
from .argument import Argument
from .inputobjecttype import InputObjectType
from .dynamic import Dynamic
from .interface import Interface
from .json import JSONString
from .mutation import Mutation
from .objecttype import ObjectType
from .scalars import ID, BigInt, Boolean, Float, Int, Scalar, String
from .schema import Schema
from .structures import List, NonNull
from .union import Union
from .uuid import UUID
__all__ = [
'AbstractType',
'ObjectType',
'InputObjectType',
'Interface',
'Mutation',
'Enum',
'Field',
'InputField',
'Schema',
'Scalar',
'String',
'ID',
'Int',
'Float',
'Boolean',
'List',
'NonNull',
'Argument',
'Dynamic',
'Union',
"Argument",
"Base64",
"BigInt",
"Boolean",
"Context",
"Date",
"DateTime",
"Decimal",
"Dynamic",
"Enum",
"Field",
"Float",
"ID",
"InputField",
"InputObjectType",
"Int",
"Interface",
"JSONString",
"List",
"Mutation",
"NonNull",
"ObjectType",
"ResolveInfo",
"Scalar",
"Schema",
"String",
"Time",
"UUID",
"Union",
]

View File

@ -1,41 +0,0 @@
import six
from ..utils.is_base_type import is_base_type
from .options import Options
from .utils import get_base_fields, merge, yank_fields_from_attrs
class AbstractTypeMeta(type):
'''
AbstractType Definition
When we want to share fields across multiple types, like a Interface,
a ObjectType and a Input ObjectType we can use AbstractTypes for defining
our fields that the other types will inherit from.
'''
def __new__(cls, name, bases, attrs):
# Also ensure initialization is only performed for subclasses of
# AbstractType
if not is_base_type(bases, AbstractTypeMeta):
return type.__new__(cls, name, bases, attrs)
for base in bases:
if not issubclass(base, AbstractType) and issubclass(type(base), AbstractTypeMeta):
# raise Exception('You can only extend AbstractTypes after the base definition.')
return type.__new__(cls, name, bases, attrs)
base_fields = get_base_fields(bases, _as=None)
fields = yank_fields_from_attrs(attrs, _as=None)
options = Options(
fields=merge(base_fields, fields)
)
cls = type.__new__(cls, name, bases, dict(attrs, _meta=options))
return cls
class AbstractType(six.with_metaclass(AbstractTypeMeta)):
pass

View File

@ -1,30 +1,82 @@
from collections import OrderedDict
from itertools import chain
from graphql import Undefined
from .dynamic import Dynamic
from .mountedtype import MountedType
from .structures import NonNull
from .dynamic import Dynamic
from .utils import get_type
class Argument(MountedType):
"""
Makes an Argument available on a Field in the GraphQL schema.
def __init__(self, type, default_value=None, description=None, name=None, required=False, _creation_counter=None):
Arguments will be parsed and provided to resolver methods for fields as keyword arguments.
All ``arg`` and ``**extra_args`` for a ``graphene.Field`` are implicitly mounted as Argument
using the below parameters.
.. code:: python
from graphene import String, Boolean, Argument
age = String(
# Boolean implicitly mounted as Argument
dog_years=Boolean(description="convert to dog years"),
# Boolean explicitly mounted as Argument
decades=Argument(Boolean, default_value=False),
)
args:
type (class for a graphene.UnmountedType): must be a class (not an instance) of an
unmounted graphene type (ex. scalar or object) which is used for the type of this
argument in the GraphQL schema.
required (optional, bool): indicates this argument as not null in the graphql schema. Same behavior
as graphene.NonNull. Default False.
name (optional, str): the name of the GraphQL argument. Defaults to parameter name.
description (optional, str): the description of the GraphQL argument in the schema.
default_value (optional, Any): The value to be provided if the user does not set this argument in
the operation.
deprecation_reason (optional, str): Setting this value indicates that the argument is
depreciated and may provide instruction or reason on how for clients to proceed. Cannot be
set if the argument is required (see spec).
"""
def __init__(
self,
type_,
default_value=Undefined,
deprecation_reason=None,
description=None,
name=None,
required=False,
_creation_counter=None,
):
super(Argument, self).__init__(_creation_counter=_creation_counter)
if required:
type = NonNull(type)
assert (
deprecation_reason is None
), f"Argument {name} is required, cannot deprecate it."
type_ = NonNull(type_)
self.name = name
self.type = type
self._type = type_
self.default_value = default_value
self.description = description
self.deprecation_reason = deprecation_reason
@property
def type(self):
return get_type(self._type)
def __eq__(self, other):
return isinstance(other, Argument) and (
self.name == other.name,
self.type == other.type,
self.default_value == other.default_value,
self.description == other.description
self.name == other.name
and self.type == other.type
and self.default_value == other.default_value
and self.description == other.description
and self.deprecation_reason == other.deprecation_reason
)
@ -32,12 +84,13 @@ def to_arguments(args, extra_args=None):
from .unmountedtype import UnmountedType
from .field import Field
from .inputfield import InputField
if extra_args:
extra_args = sorted(extra_args.items(), key=lambda f: f[1])
else:
extra_args = []
iter_arguments = chain(args.items(), extra_args)
arguments = OrderedDict()
arguments = {}
for default_name, arg in iter_arguments:
if isinstance(arg, Dynamic):
arg = arg.get_type()
@ -50,17 +103,18 @@ def to_arguments(args, extra_args=None):
arg = Argument.mounted(arg)
if isinstance(arg, (InputField, Field)):
raise ValueError('Expected {} to be Argument, but received {}. Try using Argument({}).'.format(
default_name,
type(arg).__name__,
arg.type
))
raise ValueError(
f"Expected {default_name} to be Argument, "
f"but received {type(arg).__name__}. Try using Argument({arg.type})."
)
if not isinstance(arg, Argument):
raise ValueError('Unknown argument "{}".'.format(default_name))
raise ValueError(f'Unknown argument "{default_name}".')
arg_name = default_name or arg.name
assert arg_name not in arguments, 'More than one Argument have same name "{}".'.format(arg_name)
assert (
arg_name not in arguments
), f'More than one Argument have same name "{arg_name}".'
arguments[arg_name] = arg
return arguments

48
graphene/types/base.py Normal file
View File

@ -0,0 +1,48 @@
from typing import Type, Optional
from ..utils.subclass_with_meta import SubclassWithMeta, SubclassWithMeta_Meta
from ..utils.trim_docstring import trim_docstring
class BaseOptions:
name: Optional[str] = None
description: Optional[str] = None
_frozen: bool = False
def __init__(self, class_type: Type):
self.class_type: Type = class_type
def freeze(self):
self._frozen = True
def __setattr__(self, name, value):
if not self._frozen:
super(BaseOptions, self).__setattr__(name, value)
else:
raise Exception(f"Can't modify frozen Options {self}")
def __repr__(self):
return f"<{self.__class__.__name__} name={repr(self.name)}>"
BaseTypeMeta = SubclassWithMeta_Meta
class BaseType(SubclassWithMeta):
@classmethod
def create_type(cls, class_name, **options):
return type(class_name, (cls,), {"Meta": options})
@classmethod
def __init_subclass_with_meta__(
cls, name=None, description=None, _meta=None, **_kwargs
):
assert "_meta" not in cls.__dict__, "Can't assign meta directly"
if not _meta:
return
_meta.name = name or cls.__name__
_meta.description = description or trim_docstring(cls.__doc__)
_meta.freeze()
cls._meta = _meta
super(BaseType, cls).__init_subclass_with_meta__()

43
graphene/types/base64.py Normal file
View File

@ -0,0 +1,43 @@
from binascii import Error as _Error
from base64 import b64decode, b64encode
from graphql.error import GraphQLError
from graphql.language import StringValueNode, print_ast
from .scalars import Scalar
class Base64(Scalar):
"""
The `Base64` scalar type represents a base64-encoded String.
"""
@staticmethod
def serialize(value):
if not isinstance(value, bytes):
if isinstance(value, str):
value = value.encode("utf-8")
else:
value = str(value).encode("utf-8")
return b64encode(value).decode("utf-8")
@classmethod
def parse_literal(cls, node, _variables=None):
if not isinstance(node, StringValueNode):
raise GraphQLError(
f"Base64 cannot represent non-string value: {print_ast(node)}"
)
return cls.parse_value(node.value)
@staticmethod
def parse_value(value):
if not isinstance(value, bytes):
if not isinstance(value, str):
raise GraphQLError(
f"Base64 cannot represent non-string value: {repr(value)}"
)
value = value.encode("utf-8")
try:
return b64decode(value, validate=True).decode("utf-8")
except _Error:
raise GraphQLError(f"Base64 cannot decode value: {repr(value)}")

25
graphene/types/context.py Normal file
View File

@ -0,0 +1,25 @@
class Context:
"""
Context can be used to make a convenient container for attributes to provide
for execution for resolvers of a GraphQL operation like a query.
.. code:: python
from graphene import Context
context = Context(loaders=build_dataloaders(), request=my_web_request)
schema.execute('{ hello(name: "world") }', context=context)
def resolve_hello(parent, info, name):
info.context.request # value set in Context
info.context.loaders # value set in Context
# ...
args:
**params (Dict[str, Any]): values to make available on Context instance as attributes.
"""
def __init__(self, **params):
for key, value in params.items():
setattr(self, key, value)

View File

@ -1,65 +1,111 @@
from __future__ import absolute_import
import datetime
from graphql.language import ast
from dateutil.parser import isoparse
from graphql.error import GraphQLError
from graphql.language import StringValueNode, print_ast
from .scalars import Scalar
try:
import iso8601
except:
raise ImportError(
"iso8601 package is required for DateTime Scalar.\n"
"You can install it using: pip install iso8601."
)
class DateTime(Scalar):
'''
The `DateTime` scalar type represents a DateTime
class Date(Scalar):
"""
The `Date` scalar type represents a Date
value as specified by
[iso8601](https://en.wikipedia.org/wiki/ISO_8601).
'''
"""
@staticmethod
def serialize(dt):
assert isinstance(dt, (datetime.datetime, datetime.date)), (
'Received not compatible datetime "{}"'.format(repr(dt))
)
return dt.isoformat()
def serialize(date):
if isinstance(date, datetime.datetime):
date = date.date()
if not isinstance(date, datetime.date):
raise GraphQLError(f"Date cannot represent value: {repr(date)}")
return date.isoformat()
@classmethod
def parse_literal(cls, node):
if isinstance(node, ast.StringValue):
def parse_literal(cls, node, _variables=None):
if not isinstance(node, StringValueNode):
raise GraphQLError(
f"Date cannot represent non-string value: {print_ast(node)}"
)
return cls.parse_value(node.value)
@staticmethod
def parse_value(value):
return iso8601.parse_date(value)
if isinstance(value, datetime.date):
return value
if not isinstance(value, str):
raise GraphQLError(f"Date cannot represent non-string value: {repr(value)}")
try:
return datetime.date.fromisoformat(value)
except ValueError:
raise GraphQLError(f"Date cannot represent value: {repr(value)}")
class DateTime(Scalar):
"""
The `DateTime` scalar type represents a DateTime
value as specified by
[iso8601](https://en.wikipedia.org/wiki/ISO_8601).
"""
@staticmethod
def serialize(dt):
if not isinstance(dt, (datetime.datetime, datetime.date)):
raise GraphQLError(f"DateTime cannot represent value: {repr(dt)}")
return dt.isoformat()
@classmethod
def parse_literal(cls, node, _variables=None):
if not isinstance(node, StringValueNode):
raise GraphQLError(
f"DateTime cannot represent non-string value: {print_ast(node)}"
)
return cls.parse_value(node.value)
@staticmethod
def parse_value(value):
if isinstance(value, datetime.datetime):
return value
if not isinstance(value, str):
raise GraphQLError(
f"DateTime cannot represent non-string value: {repr(value)}"
)
try:
return isoparse(value)
except ValueError:
raise GraphQLError(f"DateTime cannot represent value: {repr(value)}")
class Time(Scalar):
'''
"""
The `Time` scalar type represents a Time value as
specified by
[iso8601](https://en.wikipedia.org/wiki/ISO_8601).
'''
epoch_date = '1970-01-01'
"""
@staticmethod
def serialize(time):
assert isinstance(time, datetime.time), (
'Received not compatible time "{}"'.format(repr(time))
)
if not isinstance(time, datetime.time):
raise GraphQLError(f"Time cannot represent value: {repr(time)}")
return time.isoformat()
@classmethod
def parse_literal(cls, node):
if isinstance(node, ast.StringValue):
def parse_literal(cls, node, _variables=None):
if not isinstance(node, StringValueNode):
raise GraphQLError(
f"Time cannot represent non-string value: {print_ast(node)}"
)
return cls.parse_value(node.value)
@classmethod
def parse_value(cls, value):
dt = iso8601.parse_date('{}T{}'.format(cls.epoch_date, value))
return datetime.time(dt.hour, dt.minute, dt.second, dt.microsecond, dt.tzinfo)
if isinstance(value, datetime.time):
return value
if not isinstance(value, str):
raise GraphQLError(f"Time cannot represent non-string value: {repr(value)}")
try:
return datetime.time.fromisoformat(value)
except ValueError:
raise GraphQLError(f"Time cannot represent value: {repr(value)}")

Some files were not shown because too many files have changed in this diff Show More