From 9e8055f5608622f0e48ddf9f701adccf6ab2d8be Mon Sep 17 00:00:00 2001 From: Daniel McClanahan <1305167+cosmicexplorer@users.noreply.github.com> Date: Thu, 26 Apr 2018 00:28:44 -0700 Subject: [PATCH] remove @typed_data and move type checking support to datatype() --- src/python/pants/util/objects.py | 327 +++++------------- .../engine/test_isolated_process.py | 55 +-- tests/python/pants_test/util/test_objects.py | 263 ++++++-------- 3 files changed, 217 insertions(+), 428 deletions(-) diff --git a/src/python/pants/util/objects.py b/src/python/pants/util/objects.py index 0ffabd69096d..7ed1bcb3768b 100644 --- a/src/python/pants/util/objects.py +++ b/src/python/pants/util/objects.py @@ -5,8 +5,6 @@ from __future__ import (absolute_import, division, generators, nested_scopes, print_function, unicode_literals, with_statement) -import inspect -import re import sys from abc import abstractmethod from collections import OrderedDict, namedtuple @@ -17,11 +15,53 @@ from pants.util.meta import AbstractClass -def datatype(*args, **kwargs): +def datatype(name, field_decls, **kwargs): """A wrapper for `namedtuple` that accounts for the type of the object in equality.""" - class DataType(namedtuple(*args, **kwargs)): + field_names = [] + fields_with_constraints = OrderedDict() + invalid_decl_errs = [] + for maybe_decl in field_decls: + # ('field_name', type) + if isinstance(maybe_decl, tuple): + field_name, field_type = maybe_decl + fields_with_constraints[field_name] = Exactly(field_type) + else: + # interpret it as a field name without a type to check + field_name = maybe_decl + # namedtuple() already checks field uniqueness + field_names.append(field_name) + + namedtuple_cls = namedtuple( + name, + # '_anonymous_namedtuple_subclass', + field_names, **kwargs) + + class DataType(namedtuple_cls): + # TODO: remove this? namedtuple already does this __slots__ = () + @classmethod + def make_type_error(cls, msg): + return TypeCheckError(cls.__name__, msg) + + def __new__(cls, *args, **kwargs): + this_object = super(DataType, cls).__new__(cls, *args, **kwargs) + + # TODO(cosmicexplorer): Make this kind of exception pattern (filter for + # errors then display them all at once) more ergonomic. + type_failure_msgs = [] + for field_name, field_constraint in fields_with_constraints.items(): + field_value = getattr(this_object, field_name) + try: + field_constraint.validate_satisfied_by(field_value) + except TypeConstraintError as e: + type_failure_msgs.append( + "field '{}' was invalid: {}".format(field_name, e)) + if type_failure_msgs: + raise cls.make_type_error('\n'.join(type_failure_msgs)) + + return this_object + def __eq__(self, other): if self is other: return True @@ -40,20 +80,55 @@ def __ne__(self, other): def __iter__(self): raise TypeError("'{}' object is not iterable".format(type(self).__name__)) + def _super_iter(self): + return super(DataType, self).__iter__() + def _asdict(self): '''Return a new OrderedDict which maps field names to their values''' - return OrderedDict(zip(self._fields, super(DataType, self).__iter__())) + return OrderedDict(zip(self._fields, self._super_iter())) def _replace(_self, **kwds): '''Return a new datatype object replacing specified fields with new values''' - result = _self._make(map(kwds.pop, _self._fields, super(DataType, _self).__iter__())) + result = _self._make(map(kwds.pop, _self._fields, _self._super_iter())) if kwds: raise ValueError('Got unexpected field names: %r' % kwds.keys()) return result def __getnewargs__(self): '''Return self as a plain tuple. Used by copy and pickle.''' - return tuple(super(DataType, self).__iter__()) + return tuple(self._super_iter()) + + def __repr__(self): + args_formatted = [] + for field_name in field_names: + field_value = getattr(self, field_name) + args_formatted.append("{}={!r}".format(field_name, field_value)) + return '{class_name}({args_joined})'.format( + class_name=type(self).__name__, + args_joined=', '.join(args_formatted)) + + def __str__(self): + elements_formatted = [] + for field_name in field_names: + constraint_for_field = fields_with_constraints.get(field_name, None) + field_value = getattr(self, field_name) + if not constraint_for_field: + elements_formatted.append( + "{field_name}={field_value}" + .format(field_name=field_name, + field_value=field_value)) + else: + elements_formatted.append( + "{field_name}<{type_constraint}>={field_value}" + .format(field_name=field_name, + type_constraint=constraint_for_field, + field_value=field_value)) + return '{class_name}({typed_tagged_elements})'.format( + class_name=type(self).__name__, + typed_tagged_elements=', '.join(elements_formatted)) + + # TODO: remove! + DataType.__name__ = str(name) return DataType @@ -146,7 +221,7 @@ def validate_satisfied_by(self, obj): return obj raise TypeConstraintError( - "value {!r} (with type {!r}) does not satisfy this type constraint: {!r}." + "value {!r} (with type {!r}) must satisfy this type constraint: {!r}." .format(obj, type(obj).__name__, self)) def __hash__(self): @@ -212,238 +287,12 @@ def satisfied_by_type(self, obj_type): return issubclass(obj_type, self._types) -class FieldType(Exactly): - """A TypeConstraint which matches exactly one type, with a name string. - - Create using `FieldType.create_from_type(cls)` to generate a FieldType with a - predictable, unique name string from the class `cls` which is used in - `typed_datatype()` to generate property names passed in to `datatype()`. - """ - - class FieldTypeConstructionError(Exception): - """Raised on invalid arguments on creation.""" - - class FieldTypeNameError(FieldTypeConstructionError): - """Raised if a type object has an invalid name.""" - - CAMEL_CASE_TYPE_NAME = re.compile('\A([A-Z][a-z]*)+\Z') - CAMEL_CASE_SPLIT_PATTERN = re.compile('[A-Z][a-z]*') - - LOWER_CASE_TYPE_NAME = re.compile('\A[a-z]+\Z') - - @classmethod - def _transform_type_field_name(cls, type_name): - if cls.LOWER_CASE_TYPE_NAME.match(type_name): - # double underscore here ensures no clash with camel-case type names - return 'primitive__{}'.format(type_name) - - if cls.CAMEL_CASE_TYPE_NAME.match(type_name): - split_by_camel_downcased = [] - for m in cls.CAMEL_CASE_SPLIT_PATTERN.finditer(type_name): - camel_group = m.group(0) - downcased = camel_group.lower() - split_by_camel_downcased.append(downcased) - return '_'.join(split_by_camel_downcased) - - raise cls.FieldTypeNameError( - "Type name {!r} must be camel-cased with an initial capital, " - "or all lowercase. Only ASCII alphabetical characters are allowed." - .format(type_name)) - - def __init__(self, single_type, field_name): - if not isinstance(single_type, type): - raise self.FieldTypeConstructionError( - "single_type is not a type: was {!r} (type {!r})." - .format(single_type, type(single_type).__name__)) - if not isinstance(field_name, str): - raise self.FieldTypeConstructionError( - "field_name is not a str: was {!r} (type {!r})." - .format(field_name, type(field_name).__name__)) - - super(FieldType, self).__init__(single_type) - - self._field_name = field_name - - @property - def field_name(self): - return self._field_name - - @property - def field_type(self): - return self.types[0] - - def validate_satisfies_field(self, obj): - """Return `obj` if it satisfies this type constraint, or raise. - - Use this method over `self.validate_satisfied_by()` to provide a more - specific error message for the FieldType class. - - :raises: `TypeConstraintError` if the given object does not satisfy this - type constraint. - """ - if self.satisfied_by(obj): - return obj - - raise TypeConstraintError( - "value {!r} (with type {!r}) must be an instance of type {!r}." - .format(obj, type(obj).__name__, self.field_type.__name__)) - - def __repr__(self): - fmt_str = 'FieldType({field_type}, {field_name!r})' - return fmt_str.format(field_type=self.field_type.__name__, - field_name=self.field_name) - - @classmethod - def create_from_type(cls, type_obj): - """Generate a FieldType with a predictable name string from the given type. - - The field name will be a deterministic and unique string generated from the - type `type_obj`. `type_obj` must have a camel-cased name (e.g. MyType) or - all-lowercased name (e.g. int). - """ - if not isinstance(type_obj, type): - raise cls.FieldTypeConstructionError( - "type_obj is not a type: was {!r} (type {!r})" - .format(type_obj, type(type_obj).__name__)) - transformed_type_name = cls._transform_type_field_name(type_obj.__name__) - return cls(type_obj, str(transformed_type_name)) - - -def typed_datatype(type_name, field_decls): - """A wrapper over namedtuple which accepts a dict of field names and types. - - This can be used to very concisely define classes which have fields that are - type-checked at construction. - """ - - type_name = str(type_name) - - if not isinstance(field_decls, tuple): - raise TypedDatatypeClassConstructionError( - type_name, - "field_decls is not a tuple: {!r}".format(field_decls)) - if field_decls is (): - raise TypedDatatypeClassConstructionError( - type_name, - "no fields were declared") - - # TODO(cosmicexplorer): Make this kind of exception pattern (filter for errors - # then display them all at once) more ergonomic. - type_constraints = OrderedSet() - invalid_decl_errs = [] - for maybe_decl in field_decls: - try: - field_constraint = FieldType.create_from_type(maybe_decl) - except FieldType.FieldTypeConstructionError as e: - invalid_decl_errs.append(str(e)) - continue - - if field_constraint in type_constraints: - invalid_decl_errs.append( - "type {!r} was already used as a field" - .format(field_constraint.field_type.__name__)) - else: - type_constraints.add(field_constraint) - if invalid_decl_errs: - raise TypedDatatypeClassConstructionError( - type_name, - "invalid field declarations:\n{}".format('\n'.join(invalid_decl_errs))) - - # This is a tuple of FieldType instances for the arguments given. - field_type_tuple = tuple(type_constraints) - # This is a tuple of type names, for use in error messages. - type_names_joined = "({})".format( - ' '.join("{},".format(f.field_type.__name__) for f in field_type_tuple)) - - datatype_cls = datatype(type_name, [t.field_name for t in field_type_tuple]) - - class TypedDatatype(datatype_cls): - - def __new__(cls, *args, **kwargs): - if kwargs: - raise TypedDatatypeInstanceConstructionError( - type_name, - """typed_datatype() subclasses can only be constructed with positional arguments! The class {class_name} requires {field_types} as arguments. -The args provided were: {args!r}. -The kwargs provided were: {kwargs!r}.""" - .format(class_name=cls.__name__, - field_types=type_names_joined, - args=args, - kwargs=kwargs)) - - if len(args) != len(field_type_tuple): - raise TypedDatatypeInstanceConstructionError( - type_name, - """{num_args} args were provided, but expected {expected_num_args}: {field_types}. -The args provided were: {args!r}.""" - .format(num_args=len(args), - expected_num_args=len(field_type_tuple), - field_types=type_names_joined, - args=args)) - - type_failure_msgs = [] - for field_idx, field_value in enumerate(args): - constraint_for_field = field_type_tuple[field_idx] - try: - constraint_for_field.validate_satisfies_field(field_value) - except TypeConstraintError as e: - type_failure_msgs.append( - "field '{}' was invalid: {}" - .format(constraint_for_field.field_name, e)) - if type_failure_msgs: - raise TypeCheckError(type_name, '\n'.join(type_failure_msgs)) - - return super(TypedDatatype, cls).__new__(cls, *args) - - def __repr__(self): - formatted_args = [repr(arg) for arg in self.__getnewargs__()] - return '{class_name}({args_joined})'.format( - class_name=type(self).__name__, - args_joined=', '.join(formatted_args)) - - def __str__(self): - arg_tuple = self.__getnewargs__() - elements_formatted = [] - for field_idx, field_value in enumerate(arg_tuple): - constraint_for_field = field_type_tuple[field_idx] - elements_formatted.append("{field_name}<{type_name}>={arg}".format( - field_name=constraint_for_field.field_name, - type_name=constraint_for_field.field_type.__name__, - arg=field_value)) - return '{class_name}({typed_tagged_elements})'.format( - class_name=type(self).__name__, - typed_tagged_elements=', '.join(elements_formatted)) - - @classmethod - def make_type_error(cls, msg): - return TypeCheckError(cls.__name__, msg) - - return TypedDatatype - - -# @typed_data(int, str) -# class MyTypedData(SomeMixin): -# # source code... -# -# | -# | -# V -# -# class MyTypedData(typed_datatype('MyTypedData', (int, str)), SomeMixin): -# # source code... -def typed_data(*fields): - - def from_class(cls): - if not inspect.isclass(cls): - raise ValueError("The @typed_data() decorator must be applied " - "innermost of all decorators.") - - typed_base = typed_datatype(cls.__name__, tuple(fields)) - all_bases = (typed_base,) + cls.__bases__ - - return type(cls.__name__, all_bases, dict(cls.__dict__)) +# def typed_datatype(type_name, field_decls): +# """A wrapper over namedtuple which accepts a dict of field names and types. - return from_class +# This can be used to very concisely define classes which have fields that are +# type-checked at construction. +# """ class Collection(object): diff --git a/tests/python/pants_test/engine/test_isolated_process.py b/tests/python/pants_test/engine/test_isolated_process.py index e8f0c4e40ead..670004bb532a 100644 --- a/tests/python/pants_test/engine/test_isolated_process.py +++ b/tests/python/pants_test/engine/test_isolated_process.py @@ -14,21 +14,20 @@ create_process_rules) from pants.engine.rules import RootRule, rule from pants.engine.selectors import Get, Select -from pants.util.objects import TypeCheckError, typed_data +from pants.util.objects import TypeCheckError, datatype from pants_test.engine.scheduler_test_base import SchedulerTestBase -@typed_data(str) -class Concatted: pass +class Concatted(datatype('Concatted', [('value', str)])): + pass -@typed_data(str) -class BinaryLocation: +class BinaryLocation(datatype('BinaryLocation', [('bin_path', str)])): def __new__(cls, *args, **kwargs): this_object = super(BinaryLocation, cls).__new__(cls, *args, **kwargs) - bin_path = this_object.primitive__str + bin_path = this_object.bin_path if os.path.isfile(bin_path) and os.access(bin_path, os.X_OK): return this_object @@ -38,8 +37,7 @@ def __new__(cls, *args, **kwargs): "path {} does not name an existing executable file.".format(bin_path)) -@typed_data(BinaryLocation) -class ShellCat: +class ShellCat(datatype('ShellCat', [('binary_location', BinaryLocation)])): """Wrapper class to show an example of using an auxiliary class (which wraps an executable) to generate an argv instead of doing it all in CatExecutionRequest. This can be used to encapsulate operations such as @@ -50,7 +48,7 @@ class ShellCat: @property def bin_path(self): - return self.binary_location.primitive__str + return self.binary_location.bin_path def argv_from_snapshot(self, snapshot): cat_file_paths = [f.path for f in snapshot.files] @@ -64,8 +62,11 @@ def argv_from_snapshot(self, snapshot): return (self.bin_path,) + tuple(cat_file_paths) -@typed_data(ShellCat, PathGlobs) -class CatExecutionRequest: pass +class CatExecutionRequest(datatype('CatExecutionRequest', [ + ('shell_cat', ShellCat), + ('path_globs', PathGlobs), +])): + pass @rule(ExecuteProcessRequest, [Select(CatExecutionRequest)]) @@ -98,12 +99,13 @@ def create_cat_stdout_rules(): ] -@typed_data(BinaryLocation) -class JavacVersionExecutionRequest: +class JavacVersionExecutionRequest(datatype('JavacVersionExecutionRequest', [ + ('binary_location', BinaryLocation), +])): @property def bin_path(self): - return self.binary_location.primitive__str + return self.binary_location.bin_path def gen_argv(self): return (self.bin_path, '-version',) @@ -116,8 +118,8 @@ def process_request_from_javac_version(javac_version_exe_req): env=tuple()) -@typed_data(str) -class JavacVersionOutput: pass +class JavacVersionOutput(datatype('JavacVersionOutput', [('value', str)])): + pass class ProcessExecutionFailure(Exception): @@ -164,8 +166,7 @@ def get_javac_version_output(javac_version_command): yield JavacVersionOutput(str(javac_version_proc_result.stderr)) -@typed_data(PathGlobs) -class JavacSources: +class JavacSources(datatype('JavacSources', [('path_globs', PathGlobs)])): """PathGlobs wrapper for Java source files to show an example of making a custom type to wrap generic types such as PathGlobs to add usage context. @@ -174,12 +175,14 @@ class JavacSources: """ -@typed_data(BinaryLocation, JavacSources) -class JavacCompileRequest: +class JavacCompileRequest(datatype('JavacCompileRequest', [ + ('binary_location', BinaryLocation), + ('javac_sources', JavacSources), +])): @property def bin_path(self): - return self.binary_location.primitive__str + return self.binary_location.bin_path def argv_from_source_snapshot(self, snapshot): snapshot_file_paths = [f.path for f in snapshot.files] @@ -268,7 +271,7 @@ def test_integration_concat_with_snapshots_stdout(self): self.assertEqual( repr(cat_exe_req), - str("CatExecutionRequest(ShellCat(BinaryLocation('/bin/cat')), PathGlobs(include=(u'fs_test/a/b/*',), exclude=()))")) + str("CatExecutionRequest(shell_cat=ShellCat(binary_location=BinaryLocation(bin_path='/bin/cat')), path_globs=PathGlobs(include=(u'fs_test/a/b/*',), exclude=()))")) results = self.execute(scheduler, Concatted, cat_exe_req) self.assertEqual(1, len(results)) @@ -286,12 +289,12 @@ def test_javac_version_example(self): self.assertEqual( repr(request), - "JavacVersionExecutionRequest(BinaryLocation('/usr/bin/javac'))") + "JavacVersionExecutionRequest(binary_location=BinaryLocation(bin_path='/usr/bin/javac'))") results = self.execute(scheduler, JavacVersionOutput, request) self.assertEqual(1, len(results)) javac_version_output = results[0] - self.assertIn('javac', javac_version_output.primitive__str) + self.assertIn('javac', javac_version_output.value) def test_javac_compilation_example_success(self): scheduler = self.mk_scheduler_in_example_fs(create_javac_compile_rules()) @@ -304,7 +307,7 @@ def test_javac_compilation_example_success(self): self.assertEqual( repr(request), - "JavacCompileRequest(BinaryLocation('/usr/bin/javac'), JavacSources(PathGlobs(include=(u'scheduler_inputs/src/java/simple/Simple.java',), exclude=())))") + "JavacCompileRequest(binary_location=BinaryLocation(bin_path='/usr/bin/javac'), javac_sources=JavacSources(path_globs=PathGlobs(include=(u'scheduler_inputs/src/java/simple/Simple.java',), exclude=())))") results = self.execute(scheduler, JavacCompileResult, request) self.assertEqual(1, len(results)) @@ -322,7 +325,7 @@ def test_javac_compilation_example_failure(self): self.assertEqual( repr(request), - "JavacCompileRequest(BinaryLocation('/usr/bin/javac'), JavacSources(PathGlobs(include=(u'scheduler_inputs/src/java/simple/Broken.java',), exclude=())))") + "JavacCompileRequest(binary_location=BinaryLocation(bin_path='/usr/bin/javac'), javac_sources=JavacSources(path_globs=PathGlobs(include=(u'scheduler_inputs/src/java/simple/Broken.java',), exclude=())))") with self.assertRaises(ProcessExecutionFailure) as cm: self.execute_raising_throw(scheduler, JavacCompileResult, request) diff --git a/tests/python/pants_test/util/test_objects.py b/tests/python/pants_test/util/test_objects.py index 03f68870b8a5..43dcc8fd9e76 100644 --- a/tests/python/pants_test/util/test_objects.py +++ b/tests/python/pants_test/util/test_objects.py @@ -9,10 +9,9 @@ import pickle from abc import abstractmethod -from pants.util.objects import (Exactly, FieldType, SubclassesOf, SuperclassesOf, TypeCheckError, +from pants.util.objects import (Exactly, SubclassesOf, SuperclassesOf, TypeCheckError, TypeConstraintError, TypedDatatypeClassConstructionError, - TypedDatatypeInstanceConstructionError, datatype, typed_data, - typed_datatype) + TypedDatatypeInstanceConstructionError, datatype) from pants_test.base_test import BaseTest @@ -125,8 +124,7 @@ class AbsClass(object): pass -@typed_data(int) -class SomeTypedDatatype: pass +class SomeTypedDatatype(datatype('SomeTypedDatatype', [('val', int)])): pass class SomeMixin(object): @@ -138,24 +136,37 @@ def stripped(self): return self.as_str().strip() -@typed_data(str) -class TypedWithMixin(SomeMixin): - """Example of using `@typed_data()` with a mixin.""" +class TypedWithMixin(datatype('TypedWithMixin', [('val', str)]), SomeMixin): + """Example of using `datatype()` with a mixin.""" def as_str(self): - return self.primitive__str + return self.val -class AnotherTypedDatatype(typed_datatype('AnotherTypedDatatype', (str, list))): - """This is an example of successfully using `typed_datatype()` without `@typed_data()`.""" +class AnotherTypedDatatype(datatype('AnotherTypedDatatype', [ + ('string', str), + ('elements', list), +])): + pass + +class YetAnotherNamedTypedDatatype(datatype('YetAnotherNamedTypedDatatype', [ + ('a_string', str), + ('an_int', int), +])): + pass -@typed_data(str, int) -class YetAnotherNamedTypedDatatype: pass +class MixedTyping(datatype('MixedTyping', [ + 'value', + ('name', str), +])): + pass -@typed_data(int) -class NonNegativeInt: + +class NonNegativeInt(datatype('NonNegativeInt', [ + ('an_int', int), +])): """Example of overriding __new__() to perform deeper argument checking.""" # NB: TypedDatatype.__new__() will raise if any kwargs are provided, but @@ -165,7 +176,7 @@ def __new__(cls, *args, **kwargs): # Call the superclass ctor first to ensure the type is correct. this_object = super(NonNegativeInt, cls).__new__(cls, *args, **kwargs) - value = this_object.primitive__int + value = this_object.an_int if value < 0: raise cls.make_type_error("value is negative: {!r}.".format(value)) @@ -173,8 +184,10 @@ def __new__(cls, *args, **kwargs): return this_object -@typed_data(NonNegativeInt) -class CamelCaseWrapper: pass +class CamelCaseWrapper(datatype('CamelCaseWrapper', [ + ('nonneg_int', NonNegativeInt), +])): + pass class ReturnsNotImplemented(object): @@ -212,8 +225,7 @@ def test_repr(self): class Foo(datatype('F', ['val']), AbsClass): pass - # Maybe this should be 'Foo(val=1)'? - self.assertEqual('F(val=1)', repr(Foo(1))) + self.assertEqual('Foo(val=1)', repr(Foo(1))) def test_not_iterable(self): bar = datatype('Bar', ['val']) @@ -279,211 +291,136 @@ def test_unexpect_kwarg(self): bar(other=1) -class FieldTypeTest(BaseTest): - def test_field_type_validation(self): - str_field = FieldType.create_from_type(str) - self.assertEqual(repr(str_field), "FieldType(str, 'primitive__str')") - - self.assertEqual(str('asdf'), - str_field.validate_satisfies_field(str('asdf'))) - - with self.assertRaises(TypeConstraintError) as cm: - str_field.validate_satisfies_field(3) - expected_msg = ( - "value 3 (with type 'int') must be an instance of type 'str'.") - self.assertEqual(str(cm.exception), str(expected_msg)) - - nonneg_int_field = FieldType.create_from_type(NonNegativeInt) - self.assertEqual(repr(nonneg_int_field), - "FieldType(NonNegativeInt, 'non_negative_int')") - - self.assertEqual( - NonNegativeInt(45), - nonneg_int_field.validate_satisfies_field(NonNegativeInt(45))) - - # test that camel-cased versions of primitive type names are given the - # correct field name. - class Int(int): pass - int_wrapper_field = FieldType.create_from_type(Int) - self.assertEqual(repr(int_wrapper_field), "FieldType(Int, 'int')") - self.assertEqual( - 45, - int_wrapper_field.validate_satisfies_field(Int(45))) - - with self.assertRaises(TypeConstraintError) as cm: - nonneg_int_field.validate_satisfies_field(-3) - expected_msg = ("value -3 (with type 'int') must be an instance " - "of type 'NonNegativeInt'.") - self.assertEqual(str(cm.exception), str(expected_msg)) - - def test_field_type_creation_errors(self): - class invalid_class_name(object): pass - with self.assertRaises(FieldType.FieldTypeNameError) as cm: - FieldType.create_from_type(invalid_class_name) - expected_msg = ( - "Type name 'invalid_class_name' must be camel-cased " - "with an initial capital, or all lowercase. Only ASCII alphabetical " - "characters are allowed.") - self.assertEqual(str(cm.exception), str(expected_msg)) - - with self.assertRaises(FieldType.FieldTypeConstructionError) as cm: - FieldType(3, 'asdf') - expected_msg = "single_type is not a type: was 3 (type 'int')." - self.assertEqual(str(cm.exception), str(expected_msg)) - - with self.assertRaises(FieldType.FieldTypeConstructionError) as cm: - FieldType(int, 45) - expected_msg = "field_name is not a str: was 45 (type 'int')." - self.assertEqual(str(cm.exception), str(expected_msg)) - - class TypedDatatypeTest(BaseTest): def test_class_construction_errors(self): - # NB: typed_datatype subclasses declared at top level are the success cases + # NB: datatype subclasses declared at top level are the success cases # here by not failing on import. # If the type_name can't be converted into a suitable identifier, throw a # ValueError. with self.assertRaises(ValueError) as cm: - class NonStrType(typed_datatype(3, (int,))): pass + class NonStrType(datatype(3, [int,])): pass expected_msg = "Type names and field names cannot start with a number: '3'" self.assertEqual(str(cm.exception), str(expected_msg)) # This raises a TypeError because it doesn't provide a required argument. with self.assertRaises(TypeError) as cm: - class NoFields(typed_datatype('NoFields')): pass - expected_msg = "typed_datatype() takes exactly 2 arguments (1 given)" + class NoFields(datatype('NoFields')): pass + expected_msg = "datatype() takes exactly 2 arguments (1 given)" self.assertEqual(str(cm.exception), str(expected_msg)) - with self.assertRaises(TypedDatatypeClassConstructionError) as cm: - class NonTupleFields(typed_datatype('NonTupleFields', [str])): pass + with self.assertRaises(ValueError) as cm: + class NonTupleFields(datatype('NonTupleFields', [str])): pass expected_msg = ( - "error: while trying to generate typed datatype NonTupleFields: " - "field_decls is not a tuple: []") + "Type names and field names can only contain alphanumeric characters " + "and underscores: \"\"") self.assertEqual(str(cm.exception), str(expected_msg)) - with self.assertRaises(TypedDatatypeClassConstructionError) as cm: - class EmptyTupleFields(typed_datatype('EmptyTupleFields', ())): pass - expected_msg = ( - "error: while trying to generate typed datatype EmptyTupleFields: " - "no fields were declared") + with self.assertRaises(ValueError) as cm: + class NonTypeFields(datatype('NonTypeFields', (3,))): pass + expected_msg = "Type names and field names cannot start with a number: '3'" self.assertEqual(str(cm.exception), str(expected_msg)) - with self.assertRaises(TypedDatatypeClassConstructionError) as cm: - class NonTypeFields(typed_datatype('NonTypeFields', (3,))): pass - expected_msg = ( - "error: while trying to generate typed datatype NonTypeFields: " - "invalid field declarations:\n" - "type_obj is not a type: was 3 (type 'int')") + with self.assertRaises(TypeError) as cm: + class NonTypeTypes(datatype('NonTypeTypes', [('field', 4)])): pass + expected_msg = "Supplied types must be types. (4,)" self.assertEqual(str(cm.exception), str(expected_msg)) - with self.assertRaises(TypedDatatypeClassConstructionError) as cm: - class MultipleOfSameType(typed_datatype( - 'MultipleOfSameType', (int, str, int))): + with self.assertRaises(ValueError) as cm: + class MultipleSameName(datatype('MultipleSameName', [ + 'field_a', + 'field_b', + 'field_a', + ])): pass - expected_msg = ( - "error: while trying to generate typed datatype MultipleOfSameType: " - "invalid field declarations:\n" - "type 'int' was already used as a field") - self.assertEqual(str(cm.exception), str(expected_msg)) - - def test_decorator_construction_errors(self): - with self.assertRaises(TypedDatatypeClassConstructionError) as cm: - @typed_data(str('hm')) - class NonTypeFieldDecorated: pass - expected_msg = ( - "error: while trying to generate typed datatype NonTypeFieldDecorated: " - "invalid field declarations:\n" - "type_obj is not a type: was 'hm' (type 'str')") + expected_msg = "Encountered duplicate field name: 'field_a'" self.assertEqual(str(cm.exception), str(expected_msg)) with self.assertRaises(ValueError) as cm: - @typed_data(int) - def some_fun(): pass - expected_msg = ("The @typed_data() decorator must be applied " - "innermost of all decorators.") + class MultipleSameNameWithType(datatype( + 'MultipleSameNameWithType', [ + 'field_a', + ('field_a', int), + ])): + pass + expected_msg = "Encountered duplicate field name: 'field_a'" self.assertEqual(str(cm.exception), str(expected_msg)) def test_instance_construction_by_repr(self): some_val = SomeTypedDatatype(3) - self.assertEqual(3, some_val.primitive__int) - self.assertEqual(repr(some_val), "SomeTypedDatatype(3)") - self.assertEqual(str(some_val), "SomeTypedDatatype(primitive__int=3)") + self.assertEqual(3, some_val.val) + self.assertEqual(repr(some_val), "SomeTypedDatatype(val=3)") + self.assertEqual(str(some_val), "SomeTypedDatatype(val<=int>=3)") some_object = YetAnotherNamedTypedDatatype(str('asdf'), 45) - self.assertEqual(some_object.primitive__str, 'asdf') - self.assertEqual(some_object.primitive__int, 45) + self.assertEqual(some_object.a_string, 'asdf') + self.assertEqual(some_object.an_int, 45) self.assertEqual(repr(some_object), - "YetAnotherNamedTypedDatatype('asdf', 45)") + "YetAnotherNamedTypedDatatype(a_string='asdf', an_int=45)") self.assertEqual( str(some_object), - str("YetAnotherNamedTypedDatatype(primitive__str=asdf, primitive__int=45)")) + str("YetAnotherNamedTypedDatatype(a_string<=str>=asdf, an_int<=int>=45)")) - some_nonneg_int = NonNegativeInt(3) - self.assertEqual(3, some_nonneg_int.primitive__int) - self.assertEqual(repr(some_nonneg_int), "NonNegativeInt(3)") - self.assertEqual(str(some_nonneg_int), - "NonNegativeInt(primitive__int=3)") + some_nonneg_int = NonNegativeInt(an_int=3) + self.assertEqual(3, some_nonneg_int.an_int) + self.assertEqual(repr(some_nonneg_int), "NonNegativeInt(an_int=3)") + self.assertEqual(str(some_nonneg_int), "NonNegativeInt(an_int<=int>=3)") wrapped_nonneg_int = CamelCaseWrapper(NonNegativeInt(45)) # test attribute naming for camel-cased types - self.assertEqual(45, wrapped_nonneg_int.non_negative_int.primitive__int) + self.assertEqual(45, wrapped_nonneg_int.nonneg_int.an_int) # test that repr() is called inside repr(), and str() inside str() self.assertEqual(repr(wrapped_nonneg_int), - "CamelCaseWrapper(NonNegativeInt(45))") + "CamelCaseWrapper(nonneg_int=NonNegativeInt(an_int=45))") self.assertEqual( str(wrapped_nonneg_int), - str("CamelCaseWrapper(non_negative_int=NonNegativeInt(primitive__int=45))")) + str("CamelCaseWrapper(nonneg_int<=NonNegativeInt>=NonNegativeInt(an_int<=int>=45))")) + + mixed_type_obj = MixedTyping(value=3, name=str('asdf')) + self.assertEqual(3, mixed_type_obj.value) + self.assertEqual(repr(mixed_type_obj), + str("MixedTyping(value=3, name='asdf')")) + self.assertEqual(str(mixed_type_obj), + str("MixedTyping(value=3, name<=str>=asdf)")) def test_mixin_type_construction(self): obj_with_mixin = TypedWithMixin(str(' asdf ')) - self.assertEqual(repr(obj_with_mixin), "TypedWithMixin(' asdf ')") + self.assertEqual(repr(obj_with_mixin), "TypedWithMixin(val=' asdf ')") self.assertEqual(str(obj_with_mixin), - "TypedWithMixin(primitive__str= asdf )") + "TypedWithMixin(val<=str>= asdf )") self.assertEqual(obj_with_mixin.as_str(), ' asdf ') self.assertEqual(obj_with_mixin.stripped(), 'asdf') def test_instance_construction_errors(self): - with self.assertRaises(TypedDatatypeInstanceConstructionError) as cm: - SomeTypedDatatype(primitive__int=3) - expected_msg = ( - """error: in constructor of type SomeTypedDatatype: typed_datatype() subclasses can only be constructed with positional arguments! The class SomeTypedDatatype requires (int,) as arguments. -The args provided were: (). -The kwargs provided were: {'primitive__int': 3}.""") + with self.assertRaises(TypeError) as cm: + SomeTypedDatatype(something=3) + expected_msg = "__new__() got an unexpected keyword argument 'something'" self.assertEqual(str(cm.exception), str(expected_msg)) # not providing all the fields - with self.assertRaises(TypedDatatypeInstanceConstructionError) as cm: + with self.assertRaises(TypeError) as cm: SomeTypedDatatype() - expected_msg = ( - """error: in constructor of type SomeTypedDatatype: 0 args were provided, but expected 1: (int,). -The args provided were: ().""") + expected_msg = "__new__() takes exactly 2 arguments (1 given)" self.assertEqual(str(cm.exception), str(expected_msg)) # unrecognized fields - with self.assertRaises(TypedDatatypeInstanceConstructionError) as cm: + with self.assertRaises(TypeError) as cm: SomeTypedDatatype(3, 4) - expected_msg = ( - """error: in constructor of type SomeTypedDatatype: 2 args were provided, but expected 1: (int,). -The args provided were: (3, 4).""") + expected_msg = "__new__() takes exactly 2 arguments (3 given)" self.assertEqual(str(cm.exception), str(expected_msg)) with self.assertRaises(TypedDatatypeInstanceConstructionError) as cm: - CamelCaseWrapper(non_negative_int=3) + CamelCaseWrapper(nonneg_int=3) expected_msg = ( - """error: in constructor of type CamelCaseWrapper: typed_datatype() subclasses can only be constructed with positional arguments! The class CamelCaseWrapper requires (NonNegativeInt,) as arguments. -The args provided were: (). -The kwargs provided were: {'non_negative_int': 3}.""") + """error: in constructor of type CamelCaseWrapper: type check error: +field 'nonneg_int' was invalid: value 3 (with type 'int') must satisfy this type constraint: Exactly(NonNegativeInt).""") self.assertEqual(str(cm.exception), str(expected_msg)) # test that kwargs with keywords that aren't field names fail the same way - with self.assertRaises(TypedDatatypeInstanceConstructionError) as cm: + with self.assertRaises(TypeError) as cm: CamelCaseWrapper(4, a=3) - expected_msg = ( - """error: in constructor of type CamelCaseWrapper: typed_datatype() subclasses can only be constructed with positional arguments! The class CamelCaseWrapper requires (NonNegativeInt,) as arguments. -The args provided were: (4,). -The kwargs provided were: {'a': 3}.""") + expected_msg = "__new__() got an unexpected keyword argument 'a'" self.assertEqual(str(cm.exception), str(expected_msg)) def test_type_check_errors(self): @@ -492,7 +429,7 @@ def test_type_check_errors(self): SomeTypedDatatype([]) expected_msg = ( """error: in constructor of type SomeTypedDatatype: type check error: -field 'primitive__int' was invalid: value [] (with type 'list') must be an instance of type 'int'.""") +field 'val' was invalid: value [] (with type 'list') must satisfy this type constraint: Exactly(int).""") self.assertEqual(str(cm.exception), str(expected_msg)) # type checking failure with multiple arguments (one is correct) @@ -500,7 +437,7 @@ def test_type_check_errors(self): AnotherTypedDatatype(str('correct'), str('should be list')) expected_msg = ( """error: in constructor of type AnotherTypedDatatype: type check error: -field 'primitive__list' was invalid: value 'should be list' (with type 'str') must be an instance of type 'list'.""") +field 'elements' was invalid: value 'should be list' (with type 'str') must satisfy this type constraint: Exactly(list).""") self.assertEqual(str(cm.exception), str(expected_msg)) # type checking failure on both arguments @@ -508,15 +445,15 @@ def test_type_check_errors(self): AnotherTypedDatatype(3, str('should be list')) expected_msg = ( """error: in constructor of type AnotherTypedDatatype: type check error: -field 'primitive__str' was invalid: value 3 (with type 'int') must be an instance of type 'str'. -field 'primitive__list' was invalid: value 'should be list' (with type 'str') must be an instance of type 'list'.""") +field 'string' was invalid: value 3 (with type 'int') must satisfy this type constraint: Exactly(str). +field 'elements' was invalid: value 'should be list' (with type 'str') must satisfy this type constraint: Exactly(list).""") self.assertEqual(str(cm.exception), str(expected_msg)) with self.assertRaises(TypeCheckError) as cm: NonNegativeInt(str('asdf')) expected_msg = ( """error: in constructor of type NonNegativeInt: type check error: -field 'primitive__int' was invalid: value 'asdf' (with type 'str') must be an instance of type 'int'.""") +field 'an_int' was invalid: value 'asdf' (with type 'str') must satisfy this type constraint: Exactly(int).""") self.assertEqual(str(cm.exception), str(expected_msg)) with self.assertRaises(TypeCheckError) as cm: