@@ -294,6 +294,7 @@ class Person(Model):
294294 "IntegerProperty" ,
295295 "FloatProperty" ,
296296 "BlobProperty" ,
297+ "CompressedTextProperty" ,
297298 "TextProperty" ,
298299 "StringProperty" ,
299300 "GeoPtProperty" ,
@@ -2558,6 +2559,129 @@ def _db_set_uncompressed_meaning(self, p):
25582559 raise exceptions .NoLongerImplementedError ()
25592560
25602561
2562+ class CompressedTextProperty (BlobProperty ):
2563+ """A version of :class:`TextProperty` which compresses values.
2564+
2565+ Values are stored as ``zlib`` compressed UTF-8 byte sequences rather than
2566+ as strings as in a regular :class:`TextProperty`. This class allows NDB to
2567+ support passing `compressed=True` to :class:`TextProperty`. It is not
2568+ necessary to instantiate this class directly.
2569+ """
2570+
2571+ __slots__ = ()
2572+
2573+ def __init__ (self , * args , ** kwargs ):
2574+ indexed = kwargs .pop ("indexed" , False )
2575+ if indexed :
2576+ raise NotImplementedError (
2577+ "A TextProperty cannot be indexed. Previously this was "
2578+ "allowed, but this usage is no longer supported."
2579+ )
2580+
2581+ kwargs ["compressed" ] = True
2582+ super (CompressedTextProperty , self ).__init__ (* args , ** kwargs )
2583+
2584+ def _constructor_info (self ):
2585+ """Helper for :meth:`__repr__`.
2586+
2587+ Yields:
2588+ Tuple[str, bool]: Pairs of argument name and a boolean indicating
2589+ if that argument is a keyword.
2590+ """
2591+ parent_init = super (CompressedTextProperty , self ).__init__
2592+ # inspect.signature not available in Python 2.7, so we use positional
2593+ # decorator combined with argspec instead.
2594+ argspec = getattr (
2595+ parent_init , "_argspec" , inspect .getargspec (parent_init )
2596+ )
2597+ positional = getattr (parent_init , "_positional_args" , 1 )
2598+ for index , name in enumerate (argspec .args ):
2599+ if name in ("self" , "indexed" , "compressed" ):
2600+ continue
2601+ yield name , index >= positional
2602+
2603+ @property
2604+ def _indexed (self ):
2605+ """bool: Indicates that the property is not indexed."""
2606+ return False
2607+
2608+ def _validate (self , value ):
2609+ """Validate a ``value`` before setting it.
2610+
2611+ Args:
2612+ value (Union[bytes, str]): The value to check.
2613+
2614+ Raises:
2615+ .BadValueError: If ``value`` is :class:`bytes`, but is not a valid
2616+ UTF-8 encoded string.
2617+ .BadValueError: If ``value`` is neither :class:`bytes` nor
2618+ :class:`str`.
2619+ .BadValueError: If the current property is indexed but the UTF-8
2620+ encoded value exceeds the maximum length (1500 bytes).
2621+ """
2622+ if not isinstance (value , six .text_type ):
2623+ # In Python 2.7, bytes is a synonym for str
2624+ if isinstance (value , bytes ):
2625+ try :
2626+ value = value .decode ("utf-8" )
2627+ except UnicodeError :
2628+ raise exceptions .BadValueError (
2629+ "Expected valid UTF-8, got {!r}" .format (value )
2630+ )
2631+ else :
2632+ raise exceptions .BadValueError (
2633+ "Expected string, got {!r}" .format (value )
2634+ )
2635+
2636+ def _to_base_type (self , value ):
2637+ """Convert a value to the "base" value type for this property.
2638+
2639+ Args:
2640+ value (Union[bytes, str]): The value to be converted.
2641+
2642+ Returns:
2643+ Optional[bytes]: The converted value. If ``value`` is a
2644+ :class:`str`, this will return the UTF-8 encoded bytes for it.
2645+ Otherwise, it will return :data:`None`.
2646+ """
2647+ if isinstance (value , six .text_type ):
2648+ return value .encode ("utf-8" )
2649+
2650+ def _from_base_type (self , value ):
2651+ """Convert a value from the "base" value type for this property.
2652+
2653+ .. note::
2654+
2655+ Older versions of ``ndb`` could write non-UTF-8 ``TEXT``
2656+ properties. This means that if ``value`` is :class:`bytes`, but is
2657+ not a valid UTF-8 encoded string, it can't (necessarily) be
2658+ rejected. But, :meth:`_validate` now rejects such values, so it's
2659+ not possible to write new non-UTF-8 ``TEXT`` properties.
2660+
2661+ Args:
2662+ value (Union[bytes, str]): The value to be converted.
2663+
2664+ Returns:
2665+ Optional[str]: The converted value. If ``value`` is a valid UTF-8
2666+ encoded :class:`bytes` string, this will return the decoded
2667+ :class:`str` corresponding to it. Otherwise, it will return
2668+ :data:`None`.
2669+ """
2670+ if isinstance (value , bytes ):
2671+ try :
2672+ return value .decode ("utf-8" )
2673+ except UnicodeError :
2674+ pass
2675+
2676+ def _db_set_uncompressed_meaning (self , p ):
2677+ """Helper for :meth:`_db_set_value`.
2678+
2679+ Raises:
2680+ NotImplementedError: Always. This method is virtual.
2681+ """
2682+ raise NotImplementedError
2683+
2684+
25612685class TextProperty (Property ):
25622686 """An unindexed property that contains UTF-8 encoded text values.
25632687
@@ -2578,10 +2702,37 @@ class Item(ndb.Model):
25782702 .. automethod:: _from_base_type
25792703 .. automethod:: _validate
25802704
2705+ Args:
2706+ name (str): The name of the property.
2707+ compressed (bool): Indicates if the value should be compressed (via
2708+ ``zlib``). An instance of :class:`CompressedTextProperty` will be
2709+ substituted if `True`.
2710+ indexed (bool): Indicates if the value should be indexed.
2711+ repeated (bool): Indicates if this property is repeated, i.e. contains
2712+ multiple values.
2713+ required (bool): Indicates if this property is required on the given
2714+ model type.
2715+ default (Any): The default value for this property.
2716+ choices (Iterable[Any]): A container of allowed values for this
2717+ property.
2718+ validator (Callable[[~google.cloud.ndb.model.Property, Any], bool]): A
2719+ validator to be used to check values.
2720+ verbose_name (str): A longer, user-friendly name for this property.
2721+ write_empty_list (bool): Indicates if an empty list should be written
2722+ to the datastore.
2723+
25812724 Raises:
25822725 NotImplementedError: If ``indexed=True`` is provided.
25832726 """
25842727
2728+ def __new__ (cls , * args , ** kwargs ):
2729+ # If "compressed" is True, substitute CompressedTextProperty
2730+ compressed = kwargs .get ("compressed" , False )
2731+ if compressed :
2732+ return CompressedTextProperty (* args , ** kwargs )
2733+
2734+ return super (TextProperty , cls ).__new__ (cls )
2735+
25852736 def __init__ (self , * args , ** kwargs ):
25862737 indexed = kwargs .pop ("indexed" , False )
25872738 if indexed :
0 commit comments