This is an automated email from the ASF dual-hosted git repository.

kojiromike pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/avro.git


The following commit(s) were added to refs/heads/master by this push:
     new 1d55d10  AVRO-2611: Use warnings for invalid logical types. (#708)
1d55d10 is described below

commit 1d55d10ed9200084d2603f2a6b136aede1f530b2
Author: RyanSkraba <[email protected]>
AuthorDate: Wed Nov 20 03:03:41 2019 +0100

    AVRO-2611: Use warnings for invalid logical types. (#708)
    
    * AVRO-2611: Use warnings for invalid logical types.
    
    AVRO-2611: Add unit tests and restore warnings.
    
    AVRO-2611: Add warnings to unit tests.
    
    AVRO-2611: Update messages for missing precision.
    
    * AVRO-2611: Typo before knowing the scale type.
    
    * AVRO-2611: Clean up docstrings and format.
    
    Fixes after code review.
    
    * Simplify list sort.
    
    * Update precision attribute to explicit fp number.
---
 lang/py/src/avro/schema.py  |  65 +++++++++++++++++----------
 lang/py/test/test_schema.py | 107 ++++++++++++++++++++++++++++++++++----------
 2 files changed, 124 insertions(+), 48 deletions(-)

diff --git a/lang/py/src/avro/schema.py b/lang/py/src/avro/schema.py
index 6e3ebfa..60a826d 100644
--- a/lang/py/src/avro/schema.py
+++ b/lang/py/src/avro/schema.py
@@ -43,6 +43,7 @@ from __future__ import absolute_import, division, 
print_function
 import json
 import math
 import sys
+import warnings
 
 from avro import constants
 
@@ -112,6 +113,12 @@ class AvroException(Exception):
 class SchemaParseException(AvroException):
   pass
 
+class AvroWarning(UserWarning):
+  """Base class for warnings."""
+
+class IgnoredLogicalType(AvroWarning):
+  """Warnings for unknown or invalid logical types."""
+
 #
 # Base Classes
 #
@@ -337,19 +344,20 @@ class LogicalSchema(object):
 class DecimalLogicalSchema(LogicalSchema):
   def __init__(self, precision, scale=0, max_precision=0):
     if not isinstance(precision, int) or precision <= 0:
-      raise SchemaParseException("""Precision is required for logical type
-                                DECIMAL and must be a positive integer but
-                                is %s.""" % precision)
-    elif precision > max_precision:
-      raise SchemaParseException("Cannot store precision digits. Max is %s"
-                                 %(max_precision))
+      raise IgnoredLogicalType(
+          "Invalid decimal precision {}. Must be a positive 
integer.".format(precision))
+
+    if precision > max_precision:
+      raise IgnoredLogicalType(
+          "Invalid decimal precision {}. Max is {}.".format(precision, 
max_precision))
 
     if not isinstance(scale, int) or scale < 0:
-      raise SchemaParseException("Scale %s must be a positive Integer." % 
scale)
+      raise IgnoredLogicalType(
+          "Invalid decimal scale {}. Must be a positive 
integer.".format(scale))
 
-    elif scale > precision:
-      raise SchemaParseException("Invalid DECIMAL scale %s. Cannot be greater 
than precision %s"
-                                 %(scale, precision))
+    if scale > precision:
+      raise IgnoredLogicalType("Invalid decimal scale {}. Cannot be greater 
than precision {}."
+                                 .format(scale, precision))
 
     super(DecimalLogicalSchema, self).__init__('decimal')
 
@@ -506,7 +514,7 @@ class FixedSchema(NamedSchema):
 
 class FixedDecimalSchema(FixedSchema, DecimalLogicalSchema):
   def __init__(self, size, name, precision, scale=0, namespace=None, 
names=None, other_props=None):
-    max_precision = math.floor(math.log10(2) * (8 * size - 1))
+    max_precision = int(math.floor(math.log10(2) * (8 * size - 1)))
     DecimalLogicalSchema.__init__(self, precision, scale, max_precision)
     FixedSchema.__init__(self, name, namespace, size, names, other_props)
     self.set_prop('precision', precision)
@@ -887,20 +895,29 @@ def make_bytes_decimal_schema(other_props):
 def make_logical_schema(logical_type, type_, other_props):
   """Map the logical types to the appropriate literal type and schema class."""
   logical_types = {
-    constants.DATE: ('int', DateSchema),
-    # Fixed decimal schema is handled before we get here.
-    constants.DECIMAL: ('bytes', make_bytes_decimal_schema),
-    constants.TIMESTAMP_MICROS: ('long', TimestampMicrosSchema),
-    constants.TIMESTAMP_MILLIS: ('long', TimestampMillisSchema),
-    constants.TIME_MICROS: ('long', TimeMicrosSchema),
-    constants.TIME_MILLIS: ('int', TimeMillisSchema),
+    (constants.DATE, 'int'): DateSchema,
+    (constants.DECIMAL, 'bytes'): make_bytes_decimal_schema,
+    # The fixed decimal schema is handled later by returning None now.
+    (constants.DECIMAL, 'fixed'): lambda x: None,
+    (constants.TIMESTAMP_MICROS, 'long'): TimestampMicrosSchema,
+    (constants.TIMESTAMP_MILLIS, 'long'): TimestampMillisSchema,
+    (constants.TIME_MICROS, 'long'): TimeMicrosSchema,
+    (constants.TIME_MILLIS, 'int'): TimeMillisSchema,
   }
-  literal_type, schema_type = logical_types.get(logical_type, (None, None))
   try:
-    if literal_type == type_:
+    schema_type = logical_types.get((logical_type, type_), None)
+    if schema_type is not None:
       return schema_type(other_props)
-  except SchemaParseException:
-    pass
+
+    expected_types = sorted(literal_type for lt, literal_type in logical_types 
if lt == logical_type)
+    if expected_types:
+      warnings.warn(
+          IgnoredLogicalType("Logical type {} requires literal type {}, not 
{}.".format(
+              logical_type, "/".join(expected_types), type_)))
+    else:
+      warnings.warn(IgnoredLogicalType("Unknown {}, using 
{}.".format(logical_type, type_)))
+  except IgnoredLogicalType as warning:
+    warnings.warn(warning)
   return None
 
 def make_avsc_object(json_data, names=None):
@@ -931,8 +948,8 @@ def make_avsc_object(json_data, names=None):
           scale = 0 if json_data.get('scale') is None else 
json_data.get('scale')
           try:
             return FixedDecimalSchema(size, name, precision, scale, namespace, 
names, other_props)
-          except (AvroException, SchemaParseException):
-            pass
+          except IgnoredLogicalType as warning:
+            warnings.warn(warning)
         return FixedSchema(name, namespace, size, names, other_props)
       elif type == 'enum':
         symbols = json_data.get('symbols')
diff --git a/lang/py/test/test_schema.py b/lang/py/test/test_schema.py
index 6186ca2..ea6779d 100644
--- a/lang/py/test/test_schema.py
+++ b/lang/py/test/test_schema.py
@@ -23,21 +23,22 @@ from __future__ import absolute_import, division, 
print_function
 
 import json
 import unittest
+import warnings
 
 import set_avro_test_path
 from avro import schema
-from avro.schema import AvroException, SchemaParseException
 
 
 class TestSchema(object):
   """A proxy for a schema string that provides useful test metadata."""
 
-  def __init__(self, data, name='', comment=''):
+  def __init__(self, data, name='', comment='', warnings=None):
     if not isinstance(data, basestring):
       data = json.dumps(data)
     self.data = data
     self.name = name or data  # default to data for name
     self.comment = comment
+    self.warnings = warnings
 
   def parse(self):
     return schema.parse(str(self))
@@ -217,23 +218,72 @@ TIMESTAMPMICROS_LOGICAL_TYPE = [
 ]
 
 IGNORED_LOGICAL_TYPE = [
-  ValidTestSchema({"type": "string", "logicalType": "uuid"}),
-  ValidTestSchema({"type": "string", "logicalType": "unknown-logical-type"}),
-  ValidTestSchema({"type": "bytes", "logicalType": "decimal", "precision": 2, 
"scale": -2}),
-  ValidTestSchema({"type": "bytes", "logicalType": "decimal", "precision": -2, 
"scale": 2}),
-  ValidTestSchema({"type": "bytes", "logicalType": "decimal", "precision": 2, 
"scale": 3}),
-  ValidTestSchema({"type": "fixed", "logicalType": "decimal", "name": 
"TestIgnored", "precision": -10, "scale": 2, "size": 5}),
-  ValidTestSchema({"type": "fixed", "logicalType": "decimal", "name": 
"TestIgnored2", "precision": 2, "scale": 3, "size": 2}),
-  ValidTestSchema({"type": "int", "logicalType": "date1"}),
-  ValidTestSchema({"type": "long", "logicalType": "date"}),
-  ValidTestSchema({"type": "int", "logicalType": "time-milis"}),
-  ValidTestSchema({"type": "long", "logicalType": "time-millis"}),
-  ValidTestSchema({"type": "long", "logicalType": "time-micro"}),
-  ValidTestSchema({"type": "int", "logicalType": "time-micros"}),
-  ValidTestSchema({"type": "long", "logicalType": "timestamp-milis"}),
-  ValidTestSchema({"type": "int", "logicalType": "timestamp-millis"}),
-  ValidTestSchema({"type": "long", "logicalType": "timestamp-micro"}),
-  ValidTestSchema({"type": "int", "logicalType": "timestamp-micros"})
+  ValidTestSchema(
+    {"type": "string", "logicalType": "uuid"},
+    warnings=[schema.IgnoredLogicalType('Unknown uuid, using string.')]),
+  ValidTestSchema(
+    {"type": "string", "logicalType": "unknown-logical-type"},
+    warnings=[schema.IgnoredLogicalType('Unknown unknown-logical-type, using 
string.')]),
+  ValidTestSchema(
+    {"type": "bytes", "logicalType": "decimal", "scale": 0},
+    warnings=[schema.IgnoredLogicalType('Invalid decimal precision None. Must 
be a positive integer.')]),
+  ValidTestSchema(
+    {"type": "bytes", "logicalType": "decimal", "precision": 2.4, "scale": 0},
+    warnings=[schema.IgnoredLogicalType('Invalid decimal precision 2.4. Must 
be a positive integer.')]),
+  ValidTestSchema(
+    {"type": "bytes", "logicalType": "decimal", "precision": 2, "scale": -2},
+    warnings=[schema.IgnoredLogicalType('Invalid decimal scale -2. Must be a 
positive integer.')]),
+  ValidTestSchema(
+    {"type": "bytes", "logicalType": "decimal", "precision": -2, "scale": 2},
+    warnings=[schema.IgnoredLogicalType('Invalid decimal precision -2. Must be 
a positive integer.')]),
+  ValidTestSchema(
+    {"type": "bytes", "logicalType": "decimal", "precision": 2, "scale": 3},
+    warnings=[schema.IgnoredLogicalType('Invalid decimal scale 3. Cannot be 
greater than precision 2.')]),
+  ValidTestSchema(
+    {"type": "fixed", "logicalType": "decimal", "name": "TestIgnored", 
"precision": -10, "scale": 2, "size": 5},
+    warnings=[schema.IgnoredLogicalType('Invalid decimal precision -10. Must 
be a positive integer.')]),
+  ValidTestSchema(
+    {"type": "fixed", "logicalType": "decimal", "name": "TestIgnored", 
"scale": 2, "size": 5},
+    warnings=[schema.IgnoredLogicalType('Invalid decimal precision None. Must 
be a positive integer.')]),
+  ValidTestSchema(
+    {"type": "fixed", "logicalType": "decimal", "name": "TestIgnored", 
"precision": 2, "scale": 3, "size": 2},
+    warnings=[schema.IgnoredLogicalType('Invalid decimal scale 3. Cannot be 
greater than precision 2.')]),
+  ValidTestSchema(
+    {"type": "fixed", "logicalType": "decimal", "name": "TestIgnored", 
"precision": 311, "size": 129},
+    warnings=[schema.IgnoredLogicalType('Invalid decimal precision 311. Max is 
310.')]),
+  ValidTestSchema(
+    {"type": "float", "logicalType": "decimal", "precision": 2, "scale": 0},
+    warnings=[schema.IgnoredLogicalType('Logical type decimal requires literal 
type bytes/fixed, not float.')]),
+  ValidTestSchema(
+    {"type": "int", "logicalType": "date1"},
+    warnings=[schema.IgnoredLogicalType('Unknown date1, using int.')]),
+  ValidTestSchema(
+    {"type": "long", "logicalType": "date"},
+    warnings=[schema.IgnoredLogicalType('Logical type date requires literal 
type int, not long.')]),
+  ValidTestSchema(
+    {"type": "int", "logicalType": "time-milis"},
+    warnings=[schema.IgnoredLogicalType('Unknown time-milis, using int.')]),
+  ValidTestSchema(
+    {"type": "long", "logicalType": "time-millis"},
+    warnings=[schema.IgnoredLogicalType('Logical type time-millis requires 
literal type int, not long.')]),
+  ValidTestSchema(
+    {"type": "long", "logicalType": "time-micro"},
+    warnings=[schema.IgnoredLogicalType('Unknown time-micro, using long.')]),
+  ValidTestSchema(
+    {"type": "int", "logicalType": "time-micros"},
+    warnings=[schema.IgnoredLogicalType('Logical type time-micros requires 
literal type long, not int.')]),
+  ValidTestSchema(
+    {"type": "long", "logicalType": "timestamp-milis"},
+    warnings=[schema.IgnoredLogicalType('Unknown timestamp-milis, using 
long.')]),
+  ValidTestSchema(
+    {"type": "int", "logicalType": "timestamp-millis"},
+    warnings=[schema.IgnoredLogicalType('Logical type timestamp-millis 
requires literal type long, not int.')]),
+  ValidTestSchema(
+    {"type": "long", "logicalType": "timestamp-micro"},
+    warnings=[schema.IgnoredLogicalType('Unknown timestamp-micro, using 
long.')]),
+  ValidTestSchema(
+    {"type": "int", "logicalType": "timestamp-micros"},
+    warnings=[schema.IgnoredLogicalType('Logical type timestamp-micros 
requires literal type long, not int.')])
 ]
 
 EXAMPLES = PRIMITIVE_EXAMPLES
@@ -397,13 +447,22 @@ class SchemaParseTestCase(unittest.TestCase):
     super(SchemaParseTestCase, self).__init__(
         'parse_valid' if test_schema.valid else 'parse_invalid')
     self.test_schema = test_schema
+    # Never hide repeated warnings when running this test case.
+    warnings.simplefilter("always")
 
   def parse_valid(self):
-    """Parsing a valid schema should not error."""
-    try:
-      self.test_schema.parse()
-    except (schema.AvroException, schema.SchemaParseException):
-      self.fail("Valid schema failed to parse: {!s}".format(self.test_schema))
+    """Parsing a valid schema should not error, but may contain warnings."""
+    with warnings.catch_warnings(record=True) as actual_warnings:
+      try:
+        self.test_schema.parse()
+      except (schema.AvroException, schema.SchemaParseException):
+        self.fail("Valid schema failed to parse: 
{!s}".format(self.test_schema))
+      actual_messages = [str(wmsg.message) for wmsg in actual_warnings]
+      if self.test_schema.warnings:
+        expected_messages = [str(w) for w in self.test_schema.warnings]
+        self.assertItemsEqual(actual_messages, expected_messages)
+      else:
+        self.assertEqual(actual_messages, [])
 
   def parse_invalid(self):
     """Parsing an invalid schema should error."""

Reply via email to