diff --git a/src/gqlspection/GQLArg.py b/src/gqlspection/GQLArg.py index a8d7d36..f4f5650 100644 --- a/src/gqlspection/GQLArg.py +++ b/src/gqlspection/GQLArg.py @@ -1,9 +1,11 @@ # coding: utf-8 from __future__ import unicode_literals from gqlspection.six import ensure_text, text_type +from gqlspection.utils.tools import cache_str_repr import gqlspection +@cache_str_repr class GQLArg(object): name = '' kind = None diff --git a/src/gqlspection/GQLEnum.py b/src/gqlspection/GQLEnum.py index 5603ae8..88ca2d4 100644 --- a/src/gqlspection/GQLEnum.py +++ b/src/gqlspection/GQLEnum.py @@ -1,8 +1,10 @@ # coding: utf-8 from __future__ import unicode_literals from gqlspection.six import ensure_text +from gqlspection.utils.tools import cache_str_repr +@cache_str_repr class GQLEnum(object): name = '' description = '' diff --git a/src/gqlspection/GQLSubQuery.py b/src/gqlspection/GQLSubQuery.py index f0d9624..7b9ba52 100644 --- a/src/gqlspection/GQLSubQuery.py +++ b/src/gqlspection/GQLSubQuery.py @@ -1,10 +1,15 @@ # coding: utf-8 from __future__ import unicode_literals from gqlspection.six import python_2_unicode_compatible, text_type -from gqlspection.utils import pad_string, format_comment +from gqlspection.utils import format_comment from gqlspection.GQLField import GQLField from gqlspection.GQLTypeProxy import GQLTypeProxy -import copy + +import sys +if sys.platform.startswith('java'): + from multiprocessing.dummy import Pool as ThreadPool +else: + from multiprocessing import Pool as ThreadPool # TODO: Figure out if it's better to split this monstrosity in two classes @@ -91,7 +96,7 @@ def schedule_close_brace(self): self.stack.append(None) def schedule(self, items): - self.stack.extend(reversed(items)) + self.stack.extend(items[::-1]) @python_2_unicode_compatible @@ -210,24 +215,30 @@ def description(self): return self._format_comment(self._description) - def subquery(self, field): + def subquery(self, field, depth=None): """Create a subquery for the given field.""" + if depth is None: + depth = self.depth + 1 + if isinstance(field, GQLField): - return GQLSubQuery(field, indent=self.indent, max_depth=self.max_depth, current_depth=self.depth + 1) + return GQLSubQuery(field, indent=self.indent, max_depth=self.max_depth, current_depth=depth) elif isinstance(field, GQLTypeProxy): - return GQLSubQuery(self.field, indent=self.indent, max_depth=self.max_depth, current_depth=self.depth + 1, union=field) + return GQLSubQuery(self.field, indent=self.indent, max_depth=self.max_depth, current_depth=depth, union=field) - def to_string(self, pad=4): + def to_string(self, pad=4, query=None): """Generate a string representation of the GraphQL query, iteratively.""" - self.builder = QueryBuilder(self, pad) + if query is None: + query = self - for query in self.builder: + builder = QueryBuilder(query, pad) + + for query in builder: # Union handling is a bit hacky, but it works if query.union: - self.builder.add_line('... on ' + query.union.name) - self.builder.open_brace() - self.builder.add_line('__typename') - self.builder.schedule([query.subquery(field) for field in query.union.fields]) + builder.add_line('... on ' + query.union.name) + builder.open_brace() + builder.add_line('__typename') + builder.schedule([query.subquery(field) for field in query.union.fields]) continue # Print out description and field name @@ -235,16 +246,16 @@ def to_string(self, pad=4): if len(description_lines) == 1 and query.field.type.kind.is_leaf: # Inline comment for a leaf type, like a scalar or enum, assuming description fits in a single line - self.builder.add_line(query.name_and_args + ' ' + query.description) + builder.add_line(query.name_and_args + ' ' + query.description) else: # Multiline comment precedes the field name - self.builder.add_lines(description_lines) - self.builder.add_field(query.name_and_args) + builder.add_lines(description_lines) + builder.add_field(query.name_and_args) # A complicated type, like an object or union - open a curly brace and iterate over subfields if not query.field.type.kind.is_leaf: # Opens a curly brace and schedules a closing brace to be printed after all subfields are processed - self.builder.open_brace() + builder.open_brace() if query.depth_limit_reached: # If we reached the depth limit, don't add any subfields @@ -252,11 +263,70 @@ def to_string(self, pad=4): # Add subfields if query.field.kind.kind == 'UNION': - self.builder.schedule([query.subquery(union) for union in query.field.type.unions]) + builder.schedule([query.subquery(union) for union in query.field.type.unions]) else: #if query.field.kind.kind == 'OBJECT': - self.builder.schedule([query.subquery(field) for field in query.field.type.fields]) + builder.schedule([query.subquery(field) for field in query.field.type.fields]) + return builder.build() + + def to_string2(self, pad=None, num_threads=4): + print("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!") + pool = ThreadPool(num_threads) + queries = [self.subquery(field, depth=1) for field in self.field.type.fields] + results = pool.map(self.to_string_naive, queries) + pool.close() + pool.join() + return '\n'.join(results) + + + def to_string_naive(self, query=None, pad=4): + print("NAIVE") + print("NAIVE") + print("NAIVE") + print("NAIVE") + print("NAIVE") + print("NAIVE") + """Generate a string representation of the GraphQL query, iteratively.""" + if query is None: + query = self + builder = QueryBuilder(query, pad) + + for query in builder: + # Union handling is a bit hacky, but it works + if query.union: + builder.add_line('... on ' + query.union.name) + builder.open_brace() + builder.add_line('__typename') + builder.schedule([query.subquery(field) for field in query.union.fields]) + continue - return self.builder.build() + # Print out description and field name + description_lines = query.description.splitlines() + + if len(description_lines) == 1 and query.field.type.kind.is_leaf: + # Inline comment for a leaf type, like a scalar or enum, assuming description fits in a single line + builder.add_line(query.name_and_args + ' ' + query.description) + else: + # Multiline comment precedes the field name + builder.add_lines(description_lines) + builder.add_field(query.name_and_args) + + # A complicated type, like an object or union - open a curly brace and iterate over subfields + if not query.field.type.kind.is_leaf: + # Opens a curly brace and schedules a closing brace to be printed after all subfields are processed + builder.open_brace() + + if query.depth_limit_reached: + # If we reached the depth limit, don't add any subfields + continue + + # Add subfields + if query.field.kind.kind == 'UNION': + builder.schedule([query.subquery(union) for union in query.field.type.unions]) + + else: + #if query.field.kind.kind == 'OBJECT': + builder.schedule([query.subquery(field) for field in query.field.type.fields]) + return builder.build() diff --git a/src/gqlspection/GQLTypeKind.py b/src/gqlspection/GQLTypeKind.py index 2ae75c2..44695a8 100644 --- a/src/gqlspection/GQLTypeKind.py +++ b/src/gqlspection/GQLTypeKind.py @@ -54,6 +54,12 @@ def __init__(self, name, kind, modifiers=None): self.name = ensure_text(name) self.kind = kind self.modifiers = modifiers or [] + self._is_builtin_scalar = None + self._is_leaf = None + self._is_final = None + self.isbs = None + self.isl = None + self.isf = None @staticmethod def from_json(typedef): @@ -87,12 +93,30 @@ def __repr__(self): @property def is_builtin_scalar(self): - return self.name in self.builtin_scalars + value = self._is_builtin_scalar + if value is not None: + return value + + value = self.name in self.builtin_scalars + self._is_builtin_scalar = value + return value @property def is_leaf(self): - return self.kind in self.leaf_types + value = self._is_leaf + if value is not None: + return value + + value = self.kind in self.leaf_types + self._is_leaf = value + return value @property def is_final(self): - return self.is_leaf or self.is_builtin_scalar + value = self._is_final + if value is not None: + return value + + value = self.is_leaf or self.is_builtin_scalar + self._is_final = value + return value diff --git a/src/gqlspection/GQLTypeProxy.py b/src/gqlspection/GQLTypeProxy.py index 1bbfeb7..088dd8c 100644 --- a/src/gqlspection/GQLTypeProxy.py +++ b/src/gqlspection/GQLTypeProxy.py @@ -2,52 +2,25 @@ from __future__ import unicode_literals from gqlspection import log from gqlspection.six import ensure_text -import gqlspection - class GQLTypeProxy(object): - name = '' - schema = None - _upstream = None - max_depth = 4 - + """A proxy for GQLType that allows for lazy loading of nested objects.""" def __init__(self, name, schema): # type: (str, GQLSchema) -> None self.name = ensure_text(name) self.schema = schema - - @property - def upstream(self): - # use cached value if present - if self._upstream: - return self._upstream - - if self.name in self.schema.types: - self._upstream = self.schema.types[self.name] - return self._upstream - else: - # TODO: expose this somehow through cli - if 'DEBUGGER' in globals(): - import pdb - pdb.set_trace() - log.debug("Found an unknown type: '%s'. At this time following types are present in schema:", self.name) - for t in self.schema.types: - log.debug(" %s(%s) [%s]" % (type(t.name), t.name, t.kind.kind)) - - raise Exception("GQLTypeProxy: type '%s' not defined" % self.name) - - def _proxy_getattr(self, item, levels): - if levels >= self.max_depth: - raise Exception("GQLTypeProxy: reached the recursion limit!") - return getattr(self, item) + self.upstream = None def __getattr__(self, item): - proxy = getattr(self.upstream, '_proxy_getattr', None) - if proxy: - # nested object detected, pass execution to proxy - return proxy(item, 0) - - return getattr(self.upstream, item) - - def __dir__(self): - return super(gqlspection.GQLType, self.upstream).__dir__() + # Once the attribute is accessed, we load the actual object from the schema and replace attribute with strong reference + upstream = self.upstream + if upstream is None: + try: + upstream = self.schema.types[self.name] + self.upstream = upstream + except KeyError: + raise AttributeError("GQLTypeProxy: type '%s' not defined" % self.name) + upstream_item = getattr(upstream, item) + + setattr(self, item, upstream_item) # Setting attribute to the proxy + return upstream_item diff --git a/src/gqlspection/utils/tools.py b/src/gqlspection/utils/tools.py index f929dd7..fa22c0c 100644 --- a/src/gqlspection/utils/tools.py +++ b/src/gqlspection/utils/tools.py @@ -141,3 +141,23 @@ def query_introspection(url, headers=None, include_metadata=False, request_fn=No # None of the introspection queries were successful log.warning("Introspection seems disabled for this endpoint: '%s'.", url) raise Exception("Introspection seems disabled for this endpoint: '%s'." % url) + + +def cache_str_repr(cls): + """A class decorator that caches the __str__ and __repr__ methods.""" + + def cached_method(method_func, cache_attribute): + """Inner decorator to apply caching to a method.""" + def wrapper(self): + cache_value = getattr(self, cache_attribute, None) + if cache_value is None: + cache_value = method_func(self) + setattr(self, cache_attribute, cache_value) + return cache_value + return wrapper + + # Apply caching to the __str__ and __repr__ methods + cls.__str__ = cached_method(cls.__str__, "__cachedstrings__str__") + cls.__repr__ = cached_method(cls.__repr__, "__cachedstrings__repr__") + + return cls