mirror of
				https://github.com/python/cpython.git
				synced 2025-11-01 06:01:29 +00:00 
			
		
		
		
	bpo-32505: dataclasses: raise TypeError if a member variable is of type Field, but doesn't have a type annotation. (GH-6192)
If a dataclass has a member variable that's of type Field, but it doesn't have a type annotation, raise TypeError.
This commit is contained in:
		
							parent
							
								
									f757b72b25
								
							
						
					
					
						commit
						56970b8ce9
					
				
					 3 changed files with 66 additions and 17 deletions
				
			
		|  | @ -573,22 +573,6 @@ def _get_field(cls, a_name, a_type): | ||||||
|     return f |     return f | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def _find_fields(cls): |  | ||||||
|     # Return a list of Field objects, in order, for this class (and no |  | ||||||
|     #  base classes).  Fields are found from the class dict's |  | ||||||
|     #  __annotations__ (which is guaranteed to be ordered).  Default |  | ||||||
|     #  values are from class attributes, if a field has a default.  If |  | ||||||
|     #  the default value is a Field(), then it contains additional |  | ||||||
|     #  info beyond (and possibly including) the actual default value. |  | ||||||
|     #  Pseudo-fields ClassVars and InitVars are included, despite the |  | ||||||
|     #  fact that they're not real fields.  That's dealt with later. |  | ||||||
| 
 |  | ||||||
|     # If __annotations__ isn't present, then this class adds no new |  | ||||||
|     #  annotations. |  | ||||||
|     annotations = cls.__dict__.get('__annotations__', {}) |  | ||||||
|     return [_get_field(cls, name, type) for name, type in annotations.items()] |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| def _set_new_attribute(cls, name, value): | def _set_new_attribute(cls, name, value): | ||||||
|     # Never overwrites an existing attribute.  Returns True if the |     # Never overwrites an existing attribute.  Returns True if the | ||||||
|     #  attribute already exists. |     #  attribute already exists. | ||||||
|  | @ -663,10 +647,25 @@ def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen): | ||||||
|             if getattr(b, _PARAMS).frozen: |             if getattr(b, _PARAMS).frozen: | ||||||
|                 any_frozen_base = True |                 any_frozen_base = True | ||||||
| 
 | 
 | ||||||
|  |     # Annotations that are defined in this class (not in base | ||||||
|  |     #  classes).  If __annotations__ isn't present, then this class | ||||||
|  |     #  adds no new annotations.  We use this to compute fields that | ||||||
|  |     #  are added by this class. | ||||||
|  |     # Fields are found from cls_annotations, which is guaranteed to be | ||||||
|  |     #  ordered.  Default values are from class attributes, if a field | ||||||
|  |     #  has a default.  If the default value is a Field(), then it | ||||||
|  |     #  contains additional info beyond (and possibly including) the | ||||||
|  |     #  actual default value.  Pseudo-fields ClassVars and InitVars are | ||||||
|  |     #  included, despite the fact that they're not real fields. | ||||||
|  |     #  That's dealt with later. | ||||||
|  |     cls_annotations = cls.__dict__.get('__annotations__', {}) | ||||||
|  | 
 | ||||||
|     # Now find fields in our class.  While doing so, validate some |     # Now find fields in our class.  While doing so, validate some | ||||||
|     #  things, and set the default values (as class attributes) |     #  things, and set the default values (as class attributes) | ||||||
|     #  where we can. |     #  where we can. | ||||||
|     for f in _find_fields(cls): |     cls_fields = [_get_field(cls, name, type) | ||||||
|  |                   for name, type in cls_annotations.items()] | ||||||
|  |     for f in cls_fields: | ||||||
|         fields[f.name] = f |         fields[f.name] = f | ||||||
| 
 | 
 | ||||||
|         # If the class attribute (which is the default value for |         # If the class attribute (which is the default value for | ||||||
|  | @ -685,6 +684,11 @@ def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen): | ||||||
|             else: |             else: | ||||||
|                 setattr(cls, f.name, f.default) |                 setattr(cls, f.name, f.default) | ||||||
| 
 | 
 | ||||||
|  |     # Do we have any Field members that don't also have annotations? | ||||||
|  |     for name, value in cls.__dict__.items(): | ||||||
|  |         if isinstance(value, Field) and not name in cls_annotations: | ||||||
|  |             raise TypeError(f'{name!r} is a field but has no type annotation') | ||||||
|  | 
 | ||||||
|     # Check rules that apply if we are derived from any dataclasses. |     # Check rules that apply if we are derived from any dataclasses. | ||||||
|     if has_dataclass_bases: |     if has_dataclass_bases: | ||||||
|         # Raise an exception if any of our bases are frozen, but we're not. |         # Raise an exception if any of our bases are frozen, but we're not. | ||||||
|  |  | ||||||
|  | @ -24,6 +24,14 @@ class C: | ||||||
|         o = C() |         o = C() | ||||||
|         self.assertEqual(len(fields(C)), 0) |         self.assertEqual(len(fields(C)), 0) | ||||||
| 
 | 
 | ||||||
|  |     def test_no_fields_but_member_variable(self): | ||||||
|  |         @dataclass | ||||||
|  |         class C: | ||||||
|  |             i = 0 | ||||||
|  | 
 | ||||||
|  |         o = C() | ||||||
|  |         self.assertEqual(len(fields(C)), 0) | ||||||
|  | 
 | ||||||
|     def test_one_field_no_default(self): |     def test_one_field_no_default(self): | ||||||
|         @dataclass |         @dataclass | ||||||
|         class C: |         class C: | ||||||
|  | @ -1906,6 +1914,41 @@ def test_helper_make_dataclass_no_types(self): | ||||||
|                                              'z': 'typing.Any'}) |                                              'z': 'typing.Any'}) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | class TestFieldNoAnnotation(unittest.TestCase): | ||||||
|  |     def test_field_without_annotation(self): | ||||||
|  |         with self.assertRaisesRegex(TypeError, | ||||||
|  |                                     "'f' is a field but has no type annotation"): | ||||||
|  |             @dataclass | ||||||
|  |             class C: | ||||||
|  |                 f = field() | ||||||
|  | 
 | ||||||
|  |     def test_field_without_annotation_but_annotation_in_base(self): | ||||||
|  |         @dataclass | ||||||
|  |         class B: | ||||||
|  |             f: int | ||||||
|  | 
 | ||||||
|  |         with self.assertRaisesRegex(TypeError, | ||||||
|  |                                     "'f' is a field but has no type annotation"): | ||||||
|  |             # This is still an error: make sure we don't pick up the | ||||||
|  |             # type annotation in the base class. | ||||||
|  |             @dataclass | ||||||
|  |             class C(B): | ||||||
|  |                 f = field() | ||||||
|  | 
 | ||||||
|  |     def test_field_without_annotation_but_annotation_in_base_not_dataclass(self): | ||||||
|  |         # Same test, but with the base class not a dataclass. | ||||||
|  |         class B: | ||||||
|  |             f: int | ||||||
|  | 
 | ||||||
|  |         with self.assertRaisesRegex(TypeError, | ||||||
|  |                                     "'f' is a field but has no type annotation"): | ||||||
|  |             # This is still an error: make sure we don't pick up the | ||||||
|  |             # type annotation in the base class. | ||||||
|  |             @dataclass | ||||||
|  |             class C(B): | ||||||
|  |                 f = field() | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| class TestDocString(unittest.TestCase): | class TestDocString(unittest.TestCase): | ||||||
|     def assertDocStrEqual(self, a, b): |     def assertDocStrEqual(self, a, b): | ||||||
|         # Because 3.6 and 3.7 differ in how inspect.signature work |         # Because 3.6 and 3.7 differ in how inspect.signature work | ||||||
|  |  | ||||||
|  | @ -0,0 +1,2 @@ | ||||||
|  | Raise TypeError if a member variable of a dataclass is of type Field, but | ||||||
|  | doesn't have a type annotation. | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Eric V. Smith
						Eric V. Smith