Skip to content

Commit e775962

Browse files
authored
fix docs example (#5)
1 parent 14f9c1a commit e775962

File tree

12 files changed

+216
-38
lines changed

12 files changed

+216
-38
lines changed

README.rst

+2-2
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@
2424
:target: https://pypi.python.org/pypi/graphql-dsl
2525
:alt: Latest PyPI Release
2626

27-
Compose GraphQL queries by defining Python types
28-
================================================
27+
Compose GraphQL queries by composing Python types
28+
=================================================
2929

3030
.. code-block:: bash
3131

docs/index.rst

+1-1
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ Now we are able to call the service and receive the typed result from it:
137137
}
138138
)
139139
140-
data = compiled_query.get_result(response)
140+
data = compiled_query.get_result(response.json())
141141
assert isinstance(data, Query)
142142
143143
# will print AD, AE, AF, AG, AI, AL, AM, AO, ...

graphql_dsl/cli/__init__.py

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import argparse
2+
import sys
3+
4+
from pkg_resources import get_distribution
5+
6+
from . import gen
7+
from ..info import DISTRIBUTION_NAME
8+
9+
10+
def main(args=None, in_channel=sys.stdin, out_channel=sys.stdout):
11+
parser = argparse.ArgumentParser(description='GraphQL DSL')
12+
parser.add_argument('-V', '--version', action='version',
13+
version=f'{DISTRIBUTION_NAME} {get_distribution(DISTRIBUTION_NAME).version}')
14+
subparsers = parser.add_subparsers(title='sub-commands',
15+
description='valid sub-commands',
16+
help='additional help',
17+
dest='sub-command')
18+
# make subparsers required (see http://stackoverflow.com/a/23354355/458106)
19+
subparsers.required = True
20+
21+
# $ <cmd> gen
22+
# ---------------------------
23+
gen.setup(subparsers)
24+
25+
# Parse arguments and config
26+
# --------------------------
27+
if args is None:
28+
args = sys.argv[1:]
29+
args = parser.parse_args(args)
30+
31+
# Set up and run
32+
# --------------
33+
args.run_cmd(args, in_channel=in_channel, out_channel=out_channel)

graphql_dsl/cli/gen.py

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import argparse
2+
import json
3+
import sys
4+
from pathlib import Path
5+
from typing import Mapping
6+
from itertools import islice
7+
8+
from .. import parser
9+
10+
11+
def is_empty_dir(p: Path) -> bool:
12+
return p.is_dir() and not bool(list(islice(p.iterdir(), 1)))
13+
14+
15+
def setup(subparsers: argparse._SubParsersAction) -> argparse.ArgumentParser:
16+
sub = subparsers.add_parser('gen', help='Generate Python definitions from GraphQL schema source.')
17+
sub.add_argument('-s', '--source', help="Path to a GraphQL schema file. "
18+
"If not specified, then the data will be read from stdin.")
19+
sub.add_argument('-o', '--out-dir', required=True,
20+
help="Output directory that will contain a newly generated Python client.")
21+
sub.add_argument('-n', '--name', required=True,
22+
help="Name of a newly generated Python client (package name).")
23+
sub.add_argument('-f', '--force-overwrite', required=False, action='store_true',
24+
help="Overwrite existing files and directories if they already exist"
25+
)
26+
sub.set_defaults(run_cmd=main)
27+
return sub
28+
29+
30+
def main(args: argparse.Namespace, in_channel=sys.stdin, out_channel=sys.stdout) -> None:
31+
""" $ <cmd-prefix> gen <source> <target>
32+
"""
33+
try:
34+
src = Path(args.source).read_text()
35+
except TypeError:
36+
# source is None, read from stdin
37+
src = in_channel.read()
38+
39+
spec = parser.parse(src)
40+
41+
42+
out_channel.write('Successfully parsed.\n')
43+

graphql_dsl/dsl.py

+32-5
Original file line numberDiff line numberDiff line change
@@ -58,15 +58,26 @@ class Binding(NamedTuple):
5858
expr_field: FieldReference
5959
variable_alias: Optional[str] = None
6060

61-
def __and__(self, other: 'Binding') -> 'BindComb':
62-
return BindComb(pvector([self, other]))
61+
def __and__(self, other: Union['Binding', 'BindComb']) -> 'BindComb':
62+
if isinstance(other, BindComb):
63+
return BindComb(pvector([self]).extend(other.bindings))
64+
elif isinstance(other, Binding):
65+
return BindComb(pvector([self, other]))
66+
raise NotImplementedError('Binding???')
6367

6468

6569
class BindComb(NamedTuple):
6670
""" Binding combinator
6771
"""
6872
bindings: PVector[Binding] = pvector()
6973

74+
def __and__(self, other: Union['Binding', 'BindComb']) -> 'BindComb':
75+
if isinstance(other, BindComb):
76+
return self._replace(bindings=self.bindings.extend(other.bindings))
77+
elif isinstance(other, Binding):
78+
return self._replace(bindings=self.bindings.append(other))
79+
raise NotImplementedError('BindComb???')
80+
7081

7182
class Unit(NamedTuple):
7283
pass
@@ -121,7 +132,16 @@ def prepare_bindings(self, expr: Expr) -> Mapping[str, Iterable[ResolvedBinding]
121132
rv = defaultdict(list)
122133
for binding in expr.bindings:
123134
resolved_input = self.resolve_binding(expr.input, binding.input_field, binding.variable_alias)
124-
resolved_expr = self.resolve_binding(expr.query, binding.expr_field, binding.variable_alias)
135+
try:
136+
resolved_expr = self.resolve_binding(expr.query, binding.expr_field, binding.variable_alias)
137+
except AttrNotFound:
138+
for typ in self.typer.memo.keys():
139+
try:
140+
resolved_expr = self.resolve_binding(typ, binding.expr_field, binding.variable_alias)
141+
except (AttributeError, TypeError, AttrNotFound):
142+
continue
143+
else:
144+
break
125145
rv[resolved_expr.attr_name].append(resolved_input)
126146
return rv
127147

@@ -134,7 +154,7 @@ def resolve_binding(self, typ: Type[Any], field: FieldReference, alias: Optional
134154
variable_python_type = field_type
135155
break
136156
else:
137-
raise TypeError(f"Couldn't find a field of alias \"{alias}\" in {typ}")
157+
raise AttrNotFound(f"Couldn't find a field of alias \"{alias}\" in {typ}")
138158

139159
elif isinstance(field, tuple):
140160
# dataclass
@@ -147,7 +167,10 @@ def resolve_binding(self, typ: Type[Any], field: FieldReference, alias: Optional
147167
overrider = get_global_name_overrider(self.typer.overrides)
148168
attr_name = overrider(name)
149169
is_optional = type(None) in inner_type_boundaries(variable_python_type)
150-
type_name = GQL_SCALARS.get(variable_python_type, variable_python_type.__name__)
170+
try:
171+
type_name = GQL_SCALARS.get(variable_python_type, variable_python_type.__name__)
172+
except AttributeError:
173+
type_name = ''
151174
return ResolvedBinding(attr_name=overrider(name),
152175
input_attr_name=alias if alias else attr_name,
153176
type_name=type_name,
@@ -200,3 +223,7 @@ def AS(a: Binding, b: str) -> Binding:
200223

201224

202225
GQL = GraphQLQueryConstructor()
226+
227+
228+
class AttrNotFound(TypeError):
229+
pass

graphql_dsl/info.py

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
DISTRIBUTION_NAME = 'graphql-dsl'
2+
PACKAGE_NAME = 'graphql_dsl'

graphql_dsl/parser/__init__.py

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import graphql
2+
3+
4+
def parse(src: str) -> graphql.DocumentNode:
5+
schema = graphql.parse(src)
6+
schema.definitions[0].operation_types[0].type.name.value
7+
return schema

requirements/minimal.txt

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
1-
typeit>=0.27.1
1+
typeit>=3.9.1.8
2+
graphql-core>=3
23
infix==1.2

setup.py

+7-3
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@
2121
# ----------------------------
2222

2323
setup(name='graphql-dsl',
24-
version='0.1.3',
25-
description='GraphQL DSL',
24+
version='0.2.0',
25+
description='Compose GraphQL queries by composing Python types!',
2626
long_description=README,
2727
classifiers=[
2828
'Development Status :: 1 - Planning',
@@ -51,5 +51,9 @@
5151
test_suite='tests',
5252
tests_require=['pytest', 'coverage'],
5353
install_requires=requires,
54-
entry_points={}
54+
entry_points={
55+
'console_scripts': [
56+
'graphql-dsl = graphql_dsl.cli:main'
57+
],
58+
}
5559
)

shell.nix

+31-26
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,49 @@
1-
with (import (builtins.fetchTarball {
2-
# Descriptive name to make the store path easier to identify
3-
name = "graphql-dsl-python38";
4-
# Commit hash for nixos-unstable as of 2019-10-27
5-
url = https://github.com/NixOS/nixpkgs-channels/archive/f601ab37c2fb7e5f65989a92df383bcd6942567a.tar.gz;
6-
# Hash obtained using `nix-prefetch-url --unpack <url>`
7-
sha256 = "0ikhcmcc29iiaqjv5r91ncgxny2z67bjzkppd3wr1yx44sv7v69s";
8-
}) {});
1+
{
2+
pkgs ? import (builtins.fetchTarball {
3+
# https://nixos.wiki/wiki/FAQ/Pinning_Nixpkgs
4+
# Descriptive name to make the store path easier to identify
5+
name = "nixpkgs-unstable-2021-01-20";
6+
url = https://github.com/NixOS/nixpkgs/archive/92c884dfd7140a6c3e6c717cf8990f7a78524331.tar.gz;
7+
# hash obtained with `nix-prefetch-url --unpack <archive>`
8+
sha256 = "0wk2jg2q5q31wcynknrp9v4wc4pj3iz3k7qlxvfh7gkpd8vq33aa";
9+
}) {}
10+
, pyVersion ? "39"
11+
, isDevEnv ? true
12+
}:
913

10-
let macOsDeps = with pkgs; stdenv.lib.optionals stdenv.isDarwin [
11-
darwin.apple_sdk.frameworks.CoreServices
12-
darwin.apple_sdk.frameworks.ApplicationServices
13-
];
14+
let
15+
macOsDeps = with pkgs; stdenv.lib.optionals stdenv.isDarwin [
16+
darwin.apple_sdk.frameworks.CoreServices
17+
darwin.apple_sdk.frameworks.ApplicationServices
18+
];
19+
python = pkgs."python${pyVersion}Full";
20+
pythonPkgs = pkgs."python${pyVersion}Packages";
21+
devLibs = if isDevEnv then [ pythonPkgs.twine pythonPkgs.wheel ] else [ pythonPkgs.coveralls ];
1422

1523
in
1624

1725
# Make a new "derivation" that represents our shell
18-
stdenv.mkDerivation {
19-
name = "graphql-dsl38";
26+
pkgs.stdenv.mkDerivation {
27+
name = "graphql-dsl39";
2028

2129
# The packages in the `buildInputs` list will be added to the PATH in our shell
2230
# Python-specific guide:
2331
# https://github.com/NixOS/nixpkgs/blob/master/doc/languages-frameworks/python.section.md
24-
buildInputs = [
32+
buildInputs = with pkgs; [
2533
# see https://nixos.org/nixos/packages.html
2634
# Python distribution
27-
cookiecutter
28-
python38Full
29-
python38Packages.virtualenv
30-
python38Packages.wheel
31-
python38Packages.twine
35+
python
36+
pythonPkgs.virtualenv
37+
pythonPkgs.wheel
38+
pythonPkgs.twine
3239
taglib
3340
ncurses
3441
libxml2
3542
libxslt
3643
libzip
3744
zlib
38-
libressl
45+
openssl
3946

40-
libuv
41-
postgresql
4247
# root CA certificates
4348
cacert
4449
which
@@ -49,13 +54,13 @@ stdenv.mkDerivation {
4954
# to allow package installs from PyPI
5055
export SOURCE_DATE_EPOCH=$(date +%s)
5156
52-
VENV_DIR=$PWD/.venv
57+
export VENV_DIR="$PWD/.venv${pyVersion}"
5358
5459
export PATH=$VENV_DIR/bin:$PATH
5560
export PYTHONPATH=""
5661
export LANG=en_US.UTF-8
5762
58-
export PIP_CACHE_DIR=$PWD/.local/pip-cache
63+
export PIP_CACHE_DIR="$PWD/.local/pip-cache${pyVersion}"
5964
6065
# Setup virtualenv
6166
if [ ! -d $VENV_DIR ]; then
@@ -67,6 +72,6 @@ stdenv.mkDerivation {
6772
6873
# Dirty fix for Linux systems
6974
# https://nixos.wiki/wiki/Packaging/Quirks_and_Caveats
70-
export LD_LIBRARY_PATH=${stdenv.cc.cc.lib}/lib/:$LD_LIBRARY_PATH
75+
export LD_LIBRARY_PATH=${pkgs.stdenv.cc.cc.lib}/lib/:$LD_LIBRARY_PATH
7176
'';
7277
}

tests/examples/__init__.py

Whitespace-only changes.

tests/examples/test_github_example.py

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
from typing import NamedTuple, Sequence
2+
from graphql_dsl import *
3+
4+
5+
def test_github_query_example():
6+
""" Composes the following example from
7+
query ListIssues($owner: String!, $name: String!) {
8+
repository(owner: $owner, name: $name) {
9+
issues(first: 100) {
10+
nodes {
11+
number
12+
title
13+
}
14+
pageInfo {
15+
hasNextPage
16+
endCursor
17+
}
18+
}
19+
}
20+
}
21+
"""
22+
result = """
23+
query ListIssues($owner:String!,$name:String!,$numFirstIssues:Int!){repository(owner:$owner,name:$name){issues(first:$numFirstIssues){nodes{number title}pageInfo{hasNextPage endCursor}}}}\
24+
"""
25+
26+
class Node(NamedTuple):
27+
number: int
28+
title: str
29+
30+
class PageInfo(NamedTuple):
31+
hasNextPage: bool
32+
endCursor: str
33+
34+
class Issue(NamedTuple):
35+
nodes: Sequence[Node]
36+
pageInfo: PageInfo
37+
38+
class Repository(NamedTuple):
39+
issues: Sequence[Issue]
40+
41+
class ListIssues(NamedTuple):
42+
repository: Repository
43+
44+
class Input(NamedTuple):
45+
owner: str
46+
name: str
47+
numFirstIssues: int
48+
49+
q = GQL( QUERY | ListIssues
50+
| WITH | Input
51+
| PASS | Input.owner * TO * ListIssues.repository
52+
& Input.name * TO * ListIssues.repository
53+
& Input.numFirstIssues * TO * Repository.issues * AS * 'first'
54+
)
55+
56+
assert q.query == result.strip()

0 commit comments

Comments
 (0)