def test_added_removed_directive(): no_directive = schema(""" type A { a: String } """) one_directive = schema(""" directive @somedir on FIELD_DEFINITION type A { a: String } """) diff = Schema(no_directive, one_directive).diff() assert diff and len(diff) == 1 assert diff[0].message == "Directive `@somedir` was added to use on `FIELD_DEFINITION`" assert diff[0].path == '@somedir' assert diff[0].criticality == Criticality.safe() diff = Schema(one_directive, no_directive).diff() assert diff and len(diff) == 1 assert diff[0].message == "Directive `@somedir` was removed" assert diff[0].path == '@somedir' assert diff[0].criticality == Criticality.breaking('Removing a directive may break clients that depend on them.') two_locations = schema(""" directive @somedir on FIELD_DEFINITION | QUERY type A { a: String } """) diff = Schema(no_directive, two_locations).diff() assert diff and len(diff) == 1 assert diff[0].message == "Directive `@somedir` was added to use on `FIELD_DEFINITION | QUERY`" assert diff[0].path == '@somedir' assert diff[0].criticality == Criticality.safe()
def test_directive_argument_description_changed(): no_desc = schema(""" directive @limit( number: Int ) on FIELD_DEFINITION type A { a: String } """) a_desc = schema(""" directive @limit( "number limit" number: Int ) on FIELD_DEFINITION type A { a: String } """) other_desc = schema(""" directive @limit( "field limit" number: Int ) on FIELD_DEFINITION type A { a: String } """) diff = Schema(no_desc, a_desc).diff() assert diff and len(diff) == 1 expected_message = ( "Description for argument `number` on `@limit` directive changed from `None` to `number limit`" ) assert diff[0].message == expected_message assert diff[0].path == '@limit' assert diff[0].criticality == Criticality.safe() diff = Schema(a_desc, other_desc).diff() assert diff and len(diff) == 1 expected_message = ( "Description for argument `number` on `@limit` directive changed from `number limit` to `field limit`" ) assert diff[0].message == expected_message assert diff[0].path == '@limit' assert diff[0].criticality == Criticality.safe() diff = Schema(other_desc, no_desc).diff() assert diff and len(diff) == 1 expected_message = ( "Description for argument `number` on `@limit` directive changed from `field limit` to `None`" ) assert diff[0].message == expected_message assert diff[0].path == '@limit' assert diff[0].criticality == Criticality.safe()
def test_directive_argument_changes(): name_arg = schema(""" directive @somedir(name: String) on FIELD_DEFINITION type A { a: String } """) id_arg = schema(""" directive @somedir(id: ID) on FIELD_DEFINITION type A { a: String } """) diff = Schema(name_arg, id_arg).diff() assert diff and len(diff) == 2 expected_message = ( 'Removed argument `name: String` from `@somedir` directive' "Added argument `id: ID` to `@somedir` directive" ) for change in diff: assert change.message in expected_message assert change.path == '@somedir' assert change.criticality == Criticality.breaking( 'Removing a directive argument will break existing usages of the argument' ) if 'Removed' in change.message else Criticality.safe()
def test_deprecated_enum_value(): a = schema(""" type Query { a: Int } enum Letters { A B } """) b = schema(""" type Query { a: Int } enum Letters { A B @deprecated(reason: "Changed the alphabet") } """) diff = Schema(a, b).diff() assert len(diff) == 1 assert diff[ 0].message == "Enum value `B` was deprecated with reason `Changed the alphabet`" assert diff[0].path == 'Letters.B' assert diff[0].criticality == Criticality.safe( "A deprecated field can still be used by clients and will give them time to adapt their queries" )
def test_directive_location_added_and_removed(): one_location = schema(""" directive @somedir on FIELD_DEFINITION type A { a: String } """) two_locations = schema(""" directive @somedir on FIELD_DEFINITION | FIELD type A { a: String } """) diff = Schema(one_location, two_locations).diff() assert diff and len(diff) == 1 expected_message = ( "Directive locations of `@somedir` changed from `FIELD_DEFINITION` to `FIELD_DEFINITION | FIELD`" ) assert diff[0].message == expected_message assert diff[0].path == '@somedir' assert diff[0].criticality == Criticality.safe() diff = Schema(two_locations, one_location).diff() assert diff and len(diff) == 1 expected_message = ( "Directive locations of `@somedir` changed from `FIELD_DEFINITION | FIELD` to `FIELD_DEFINITION`" ) assert diff[0].message == expected_message assert diff[0].path == '@somedir' assert diff[0].criticality == Criticality.breaking( 'Removing a directive location will break any instance of its usage. Be sure no one uses it before removing it' )
def test_description_changed(): a = schema(''' type Query { a: Int } enum Letters { """My description""" A } ''') b = schema(''' type Query { a: Int } enum Letters { """My new description""" A } ''') diff = Schema(a, b).diff() assert len(diff) == 1 assert diff[0].message == ( "Description for enum value `A` changed from `My description` to `My new description`" ) assert diff[0].path == 'Letters.A' assert diff[0].criticality == Criticality.safe()
def test_deprecated_reason_changed(): a = schema(""" type Query { a: Int } enum Letters { A B @deprecated(reason: "a reason") } """) b = schema(""" type Query { a: Int } enum Letters { A B @deprecated(reason: "a new reason") } """) diff = Schema(a, b).diff() assert len(diff) == 1 assert diff[0].message == ( "Deprecation reason for enum value `B` changed from `a reason` to `a new reason`" ) assert diff[0].path == 'Letters.B' assert diff[0].criticality == Criticality.safe( "A deprecated field can still be used by clients and will give them time to adapt their queries" )
def test_input_field_added_field(): a = schema(""" input Recipe { ingredients: [String] } """) b = schema(""" input Recipe { ingredients: [String] love: Float } """) diff = Schema(a, b).diff() assert diff and len(diff) == 1 assert diff[0].message == ( "Input Field `love: Float` was added to input type `Recipe`") assert diff[0].path == 'Recipe.love' assert diff[0].criticality == Criticality.safe() diff = Schema(b, a).diff() assert diff and len(diff) == 1 assert diff[0].message == ( "Input Field `love` removed from input type `Recipe`") assert diff[0].path == 'Recipe.love' assert diff[0].criticality == Criticality.breaking( 'Removing an input field will break queries that use this input field.' )
def test_object_type_added_field(): a = schema(""" type MyType{ a: Int } """) b = schema(""" type MyType{ a: Int b: String! } """) diff = Schema(a, b).diff() assert diff and len(diff) == 1 assert diff[0].message == "Field `b` was added to object type `MyType`" assert diff[0].path == 'MyType.b' assert diff[0].criticality == Criticality.safe() diff = Schema(b, a).diff() assert diff and len(diff) == 1 assert diff[0].message == "Field `b` was removed from object type `MyType`" assert diff[0].path == 'MyType.b' assert diff[0].criticality == Criticality.breaking( 'Removing a field is a breaking change. It is preferred to deprecate the field before removing it.' )
def test_schema_added_type(): old_schema = schema(""" schema { query: Query } type Query { field: String! } """) new_schema = schema(""" schema { query: Query } type Query { field: String! } type AddedType { added: Int } """) diff = Schema(old_schema, new_schema).diff() assert diff and len(diff) == 1 # Type Int was also added but its ignored because its a primitive. assert diff[0].message == "Type `AddedType` was added" assert diff[0].criticality == Criticality.safe()
def test_directive_description_changed(): no_desc = schema(""" directive @my_directive on FIELD type A { a: String } """) with_desc = schema(''' """directive desc""" directive @my_directive on FIELD type A { a: String } ''') new_desc = schema(''' """new description""" directive @my_directive on FIELD type A { a: String } ''') diff = Schema(no_desc, with_desc).diff() assert diff and len(diff) == 1 expected_message = ( 'Description for directive `@my_directive` changed from `None` to `directive desc`' ) assert diff[0].message == expected_message assert diff[0].path == '@my_directive' assert diff[0].criticality == Criticality.safe() diff = Schema(with_desc, new_desc).diff() assert diff and len(diff) == 1 expected_message = ( 'Description for directive `@my_directive` changed from `directive desc` to `new description`' ) assert diff[0].message == expected_message assert diff[0].path == '@my_directive' assert diff[0].criticality == Criticality.safe() diff = Schema(with_desc, no_desc).diff() assert diff and len(diff) == 1 expected_message = ( 'Description for directive `@my_directive` changed from `directive desc` to `None`' ) assert diff[0].message == expected_message assert diff[0].path == '@my_directive' assert diff[0].criticality == Criticality.safe()
def __init__(self, parent_type, field, arg_name, old_arg, new_arg): super().__init__(parent_type, field, arg_name, old_arg, new_arg) self.criticality = (Criticality.safe( ) if is_safe_change_for_input_value( old_arg.type, new_arg.type ) else Criticality.breaking( "Changing the type of a field's argument can break existing queries that use this argument." ))
def __init__(self, directive, arg_name, arg_type): self.criticality = Criticality.safe( ) if not is_non_null_type(arg_type.type) else Criticality.breaking( "Adding a non nullable directive argument will break existing usages of the directive" ) self.directive = directive self.arg_name = arg_name self.arg_type = arg_type
def __init__(self, directive, old_locations, new_locations): self.directive = directive self.old_locations = old_locations self.new_locations = new_locations self.criticality = (Criticality.safe() if self._only_additions( ) else Criticality.breaking( "Removing a directive location will break any instance of its usage. " "Be sure no one uses it before removing it"))
def __init__(self, type_, field_name, old_field, new_field): self.criticality = Criticality.safe()\ if is_safe_type_change(old_field.type, new_field.type)\ else Criticality.breaking('Changing a field type will break queries that assume its type') self.type_ = type_ self.field_name = field_name self.old_field = old_field self.new_field = new_field
class NonBreakingChange(Change): criticality = Criticality.safe('this is a safe change') def message(self): return 'test message' def path(self): return 'test path'
class Testchange(Change): criticality = Criticality.safe('this is a safe change') @property def message(self): return 'test message' def path(self): return 'Query.path'
class InterfaceFieldDescriptionChanged(AbstractInterfanceChange): criticality = Criticality.safe() @property def message(self): return ( f"`{self.interface.name}.{self.field_name}` description changed " f"from `{self.old_field.description}` to `{self.new_field.description}`" )
def __init__(self, parent, field_name, argument_name, arg_type): self.criticality = Criticality.safe('Adding an optional argument is a safe change')\ if not is_non_null_type(arg_type.type)\ else Criticality.breaking("Adding a required argument to an existing field is a breaking " "change because it will break existing uses of this field") self.parent = parent self.field_name = field_name self.argument_name = argument_name self.arg_type = arg_type
class MyChange(Change): criticality = Criticality.safe('Hello') @property def message(self): return '' @property def path(self): return None
def __init__(self, input_object, field_name, field): self.criticality = ( Criticality.safe() if is_non_null_type(field.type) is False else Criticality.breaking(self.BREAKING_MSG) ) self.input_object = input_object self.field_name = field_name self.field = field
class FieldArgumentDescriptionChanged(FieldAbstractArgumentChange): criticality = Criticality.safe() @property def message(self): return ( f"Description for argument `{self.arg_name}` on field `{self.parent}.{self.field_name}` " f"changed from `{self.old_arg.description}` to `{self.new_arg.description}`" )
class UnexpectedChange(Change): criticality = Criticality.safe() @property def message(self): return "" @property def path(self): return ""
def __init__(self, input_, name, new_field, old_field): self.criticality = ( Criticality.safe() if is_safe_change_for_input_value(old_field.type, new_field.type) else Criticality.breaking( "Changing the type of an input field can break existing queries that use this field" ) ) self.input_ = input_ self.name = name self.new_field = new_field self.old_field = old_field
class AddedDirective(DirectiveChange): criticality = Criticality.safe() def __init__(self, directive, directive_locations): self.directive = directive self.directive_locations = directive_locations @property def message(self): locations = ' | '.join(loc.name for loc in self.directive_locations) return f"Directive `{self.directive}` was added to use on `{locations}`"
def __init__(self, directive, arg_name, old_type, new_type): self.criticality = (Criticality.breaking( "Changing the argument type is a breaking change" ) if not is_safe_change_for_input_value( old_type, new_type ) else Criticality.safe( "Changing an input field from non-null to null is considered non-breaking" )) self.directive = directive self.arg_name = arg_name self.old_type = old_type self.new_type = new_type
class DirectiveArgumentDescriptionChanged(DirectiveChange): criticality = Criticality.safe() def __init__(self, directive, arg_name, old_desc, new_desc): self.directive = directive self.arg_name = arg_name self.old_desc = old_desc self.new_desc = new_desc @property def message(self): return ( f"Description for argument `{self.arg_name}` on `{self.directive!s}` directive changed " f"from `{self.old_desc}` to `{self.new_desc}`")
def test_added_removed_arguments(): a = schema(""" type Football { skill: Float! } """) b = schema(""" type Football { skill(player: ID): Float! } """) diff = Schema(a, b).diff() assert diff and len(diff) == 1 assert diff[0].message == "Argument `player: ID` added to `Football.skill`" assert diff[0].path == 'Football.skill' assert diff[0].criticality == Criticality.safe( 'Adding an optional argument is a safe change') diff = Schema(b, a).diff() assert diff and len(diff) == 1 assert diff[0].message == "Removed argument `player` from `Football.skill`" assert diff[0].path == 'Football.skill' assert diff[0].criticality == Criticality.breaking( 'Removing a field argument will break queries that use this argument') c = schema(""" type Football { skill(player: ID, age: Int): Float! } """) diff = Schema(b, c).diff() assert diff and len(diff) == 1 assert diff[0].message == "Argument `age: Int` added to `Football.skill`" assert diff[0].path == 'Football.skill' assert diff[0].criticality == Criticality.safe( 'Adding an optional argument is a safe change')
class ObjectTypeFieldAdded(Change): criticality = Criticality.safe() def __init__(self, parent, field_name): self.parent = parent self.field_name = field_name self.description = parent.fields[field_name].description @property def message(self): return f"Field `{self.field_name}` was added to object type `{self.parent.name}`" @property def path(self): return f"{self.parent.name}.{self.field_name}"
def test_deprecation_reason_changed(): a = schema(''' type Query { b: Int! @deprecated(reason: "Not used") } ''') b = schema(''' type Query { b: Int! @deprecated(reason: "Some string") } ''') diff = Schema(a, b).diff() assert diff and len(diff) == 1 assert diff[ 0].message == "Deprecation reason on field `Query.b` changed from `Not used` to `Some string`" assert diff[0].path == 'Query.b' assert diff[0].criticality == Criticality.safe()