Skip to content

Commit

Permalink
refactor TypeConstraint to allow parameterized types such as collections
Browse files Browse the repository at this point in the history
  • Loading branch information
cosmicexplorer committed Jan 28, 2019
1 parent 6004b76 commit 6bb9993
Show file tree
Hide file tree
Showing 2 changed files with 108 additions and 61 deletions.
159 changes: 108 additions & 51 deletions src/python/pants/util/objects.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@

from twitter.common.collections import OrderedSet

from pants.util.collections_abc_backport import OrderedDict
from pants.util.collections_abc_backport import Iterable, OrderedDict
from pants.util.memo import memoized_classproperty
from pants.util.meta import AbstractClass
from pants.util.meta import AbstractClass, classproperty


def datatype(field_decls, superclass_name=None, **kwargs):
Expand Down Expand Up @@ -264,101 +264,124 @@ def __init__(self, type_name, msg, *args, **kwargs):
type_name, formatted_msg, *args, **kwargs)


# TODO: make these members of the `TypeConstraint` class!
class TypeConstraintError(TypeError):
"""Indicates a :class:`TypeConstraint` violation."""


class TypeConstraint(AbstractClass):
"""Represents a type constraint.
Not intended for direct use; instead, use one of :class:`SuperclassesOf`, :class:`Exact` or
Not intended for direct use; instead, use one of :class:`SuperclassesOf`, :class:`Exactly` or
:class:`SubclassesOf`.
"""

def __init__(self, *types, **kwargs):
def __init__(self, variance_symbol, wrapper_type, description):
"""Creates a type constraint centered around the given types.
The type constraint is satisfied as a whole if satisfied for at least one of the given types.
:param type *types: The focus of this type constraint.
:param str description: A description for this constraint if the list of types is too long.
"""
if not types:
raise ValueError('Must supply at least one type')
if any(not isinstance(t, type) for t in types):
raise TypeError('Supplied types must be types. {!r}'.format(types))

# NB: `types` is converted to tuple here because self.types's docstring says
# it returns a tuple. Does it matter what type this field is?
self._types = tuple(types)
self._desc = kwargs.get('description', None)

@property
def types(self):
"""Return the subject types of this type constraint.
:type: tuple of type
"""
return self._types

def satisfied_by(self, obj):
"""Return `True` if the given object satisfies this type constraint.
:rtype: bool
"""
return self.satisfied_by_type(type(obj))
assert(variance_symbol)
assert(wrapper_type is None or isinstance(wrapper_type, type))
self._variance_symbol = variance_symbol
self._wrapper_type = wrapper_type
self._description = description

@abstractmethod
def satisfied_by_type(self, obj_type):
def satisfied_by(self, obj):
"""Return `True` if the given object satisfies this type constraint.
:rtype: bool
"""

# NB: we currently use the return value and drop the input in all usages of this method, to allow
# for the possibility of modifying the returned value in the future.
def validate_satisfied_by(self, obj):
"""Return `obj` if the object satisfies this type constraint, or raise.
# TODO: consider disallowing overriding this too?
:raises: `TypeConstraintError` if `obj` does not satisfy the constraint.
"""

if self.satisfied_by(obj):
if self._wrapper_type:
return self._wrapper_type(obj)
return obj

raise TypeConstraintError(
"value {!r} (with type {!r}) must satisfy this type constraint: {!r}."
.format(obj, type(obj).__name__, self))

def __hash__(self):
return hash((type(self), self._types))

def __eq__(self, other):
return type(self) == type(other) and self._types == other._types

def __ne__(self, other):
return not (self == other)

def __str__(self):
if self._desc:
constrained_type = '({})'.format(self._desc)
return '{}{}'.format(self._variance_symbol, self._description)


class BasicTypeConstraint(TypeConstraint):
"""???"""

@classproperty
def _variance_symbol(cls):
"""???"""
raise NotImplementedError('???')

def __init__(self, *types):
"""Creates a type constraint centered around the given types.
The type constraint is satisfied as a whole if satisfied for at least one of the given types.
:param type *types: The focus of this type constraint.
:param str description: A description for this constraint if the list of types is too long.
"""

if not types:
raise ValueError('Must supply at least one type')
if any(not isinstance(t, type) for t in types):
raise TypeError('Supplied types must be types. {!r}'.format(types))

if len(types) == 1:
constrained_type = types[0].__name__
else:
if len(self._types) == 1:
constrained_type = self._types[0].__name__
else:
constrained_type = '({})'.format(', '.join(t.__name__ for t in self._types))
return '{variance_symbol}{constrained_type}'.format(variance_symbol=self._variance_symbol,
constrained_type=constrained_type)
constrained_type = '({})'.format(', '.join(t.__name__ for t in types))

super(BasicTypeConstraint, self).__init__(
variance_symbol=self._variance_symbol,
wrapper_type=None,
description=constrained_type)

# NB: This is made into a tuple so that we can use self._types in issubclass() and others!
self._types = tuple(types)

@abstractmethod
def satisfied_by_type(self, obj_type):
"""Return `True` if the given object satisfies this type constraint.
:rtype: bool
"""

def satisfied_by(self, obj):
return self.satisfied_by_type(type(obj))

def __hash__(self):
return hash((type(self), self._types))

def __eq__(self, other):
return type(self) == type(other) and self._types == other._types

def __repr__(self):
if self._desc:
constrained_type = self._desc
else:
constrained_type = ', '.join(t.__name__ for t in self._types)
constrained_type = ', '.join(t.__name__ for t in self._types)
return ('{type_constraint_type}({constrained_type})'
.format(type_constraint_type=type(self).__name__,
constrained_type=constrained_type))
constrained_type=constrained_type))


class SuperclassesOf(TypeConstraint):
class SuperclassesOf(BasicTypeConstraint):
"""Objects of the exact type as well as any super-types are allowed."""

_variance_symbol = '-'
Expand All @@ -367,7 +390,7 @@ def satisfied_by_type(self, obj_type):
return any(issubclass(t, obj_type) for t in self._types)


class Exactly(TypeConstraint):
class Exactly(BasicTypeConstraint):
"""Only objects of the exact type are allowed."""

_variance_symbol = '='
Expand All @@ -382,10 +405,44 @@ def graph_str(self):
return repr(self)


class SubclassesOf(TypeConstraint):
class SubclassesOf(BasicTypeConstraint):
"""Objects of the exact type as well as any sub-types are allowed."""

_variance_symbol = '+'

def satisfied_by_type(self, obj_type):
return issubclass(obj_type, self._types)


class TypedCollection(TypeConstraint):

@classmethod
def _generate_variance_symbol(cls, constraint):
return '[{}]'.format(constraint._variance_symbol)

def __init__(self, constraint, wrapper_type=tuple):

assert(isinstance(constraint, BasicTypeConstraint))
self._constraint = constraint

super(TypedCollection, self).__init__(
variance_symbol=self._generate_variance_symbol(constraint),
wrapper_type=wrapper_type,
description=constraint._description)

def satisfied_by(self, obj):
if isinstance(obj, Iterable):
return all(self._constraint.satisfied_by(el) for el in obj)
return False

def __hash__(self):
return hash((type(self), self._constraint, self._wrapper_type))

def __eq__(self, other):
return type(self) == type(other) and self._constraint == other._constraint

def __repr__(self):
return ('{type_constraint_type}({constraint!r}, wrapper_type={wrapper_type!r})'
.format(type_constraint_type=type(self).__name__,
constraint=self._constraint,
wrapper_type=self._wrapper_type))
10 changes: 0 additions & 10 deletions tests/python/pants_test/util/test_objects.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,15 +38,13 @@ def test_none(self):

def test_single(self):
superclasses_of_b = SuperclassesOf(self.B)
self.assertEqual((self.B,), superclasses_of_b.types)
self.assertTrue(superclasses_of_b.satisfied_by(self.A()))
self.assertTrue(superclasses_of_b.satisfied_by(self.B()))
self.assertFalse(superclasses_of_b.satisfied_by(self.BPrime()))
self.assertFalse(superclasses_of_b.satisfied_by(self.C()))

def test_multiple(self):
superclasses_of_a_or_b = SuperclassesOf(self.A, self.B)
self.assertEqual((self.A, self.B), superclasses_of_a_or_b.types)
self.assertTrue(superclasses_of_a_or_b.satisfied_by(self.A()))
self.assertTrue(superclasses_of_a_or_b.satisfied_by(self.B()))
self.assertFalse(superclasses_of_a_or_b.satisfied_by(self.BPrime()))
Expand All @@ -60,15 +58,13 @@ def test_none(self):

def test_single(self):
exactly_b = Exactly(self.B)
self.assertEqual((self.B,), exactly_b.types)
self.assertFalse(exactly_b.satisfied_by(self.A()))
self.assertTrue(exactly_b.satisfied_by(self.B()))
self.assertFalse(exactly_b.satisfied_by(self.BPrime()))
self.assertFalse(exactly_b.satisfied_by(self.C()))

def test_multiple(self):
exactly_a_or_b = Exactly(self.A, self.B)
self.assertEqual((self.A, self.B), exactly_a_or_b.types)
self.assertTrue(exactly_a_or_b.satisfied_by(self.A()))
self.assertTrue(exactly_a_or_b.satisfied_by(self.B()))
self.assertFalse(exactly_a_or_b.satisfied_by(self.BPrime()))
Expand All @@ -79,10 +75,6 @@ def test_disallows_unsplatted_lists(self):
Exactly([1])

def test_str_and_repr(self):
exactly_b_types = Exactly(self.B, description='B types')
self.assertEqual("=(B types)", str(exactly_b_types))
self.assertEqual("Exactly(B types)", repr(exactly_b_types))

exactly_b = Exactly(self.B)
self.assertEqual("=B", str(exactly_b))
self.assertEqual("Exactly(B)", repr(exactly_b))
Expand All @@ -103,15 +95,13 @@ def test_none(self):

def test_single(self):
subclasses_of_b = SubclassesOf(self.B)
self.assertEqual((self.B,), subclasses_of_b.types)
self.assertFalse(subclasses_of_b.satisfied_by(self.A()))
self.assertTrue(subclasses_of_b.satisfied_by(self.B()))
self.assertFalse(subclasses_of_b.satisfied_by(self.BPrime()))
self.assertTrue(subclasses_of_b.satisfied_by(self.C()))

def test_multiple(self):
subclasses_of_b_or_c = SubclassesOf(self.B, self.C)
self.assertEqual((self.B, self.C), subclasses_of_b_or_c.types)
self.assertTrue(subclasses_of_b_or_c.satisfied_by(self.B()))
self.assertTrue(subclasses_of_b_or_c.satisfied_by(self.C()))
self.assertFalse(subclasses_of_b_or_c.satisfied_by(self.BPrime()))
Expand Down

0 comments on commit 6bb9993

Please sign in to comment.