From e045859557a0d97566d56b2e4c7790f97095a180 Mon Sep 17 00:00:00 2001 From: Matt Bogosian Date: Tue, 28 Feb 2017 15:09:47 -0800 Subject: [PATCH] Release v7.2.1 - Bump version. - Update spec and stone. - Make update_version.sh more portable. - Update .gitignore. --- .gitignore | 1 + dropbox/dropbox.py | 2 +- dropbox/stone_base.py | 6 +- dropbox/stone_serializers.py | 624 ++++++++++++++++++++++------------- dropbox/stone_validators.py | 28 +- spec | 2 +- stone | 2 +- update_version.sh | 2 +- 8 files changed, 430 insertions(+), 237 deletions(-) diff --git a/.gitignore b/.gitignore index 01109fa5..702cc486 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ __pycache__/ +.cache/ *.egg *.egg-info/ *.pyc diff --git a/dropbox/dropbox.py b/dropbox/dropbox.py index 85638f5c..db7e1d02 100644 --- a/dropbox/dropbox.py +++ b/dropbox/dropbox.py @@ -4,7 +4,7 @@ 'create_session', ] -__version__ = '7.2.0' +__version__ = '7.2.1' import contextlib import json diff --git a/dropbox/stone_base.py b/dropbox/stone_base.py index 4c7af725..eeb248a9 100644 --- a/dropbox/stone_base.py +++ b/dropbox/stone_base.py @@ -15,12 +15,16 @@ # This makes testing this file directly (outside of a package) easier. import stone_validators as bv # type: ignore +_MYPY = False +if _MYPY: + import typing # noqa: F401 # pylint: disable=import-error,unused-import,useless-suppression -class Union(object): +class Union(object): # TODO(kelkabany): Possible optimization is to remove _value if a # union is composed of only symbols. __slots__ = ['_tag', '_value'] + _tagmap = {} # type: typing.Dict[typing.Text, bv.Validator] def __init__(self, tag, value=None): assert tag in self._tagmap, 'Invalid tag %r.' % tag diff --git a/dropbox/stone_serializers.py b/dropbox/stone_serializers.py index 23bd6657..6d61ab9c 100644 --- a/dropbox/stone_serializers.py +++ b/dropbox/stone_serializers.py @@ -17,18 +17,348 @@ import datetime import functools import json +import re import six +import time try: + from . import stone_base as bb # noqa: F401 # pylint: disable=unused-import from . import stone_validators as bv except (SystemError, ValueError): # Catch errors raised when importing a relative module when not in a package. # This makes testing this file directly (outside of a package) easier. + import stone_validators as bb # type: ignore # noqa: F401 # pylint: disable=unused-import import stone_validators as bv # type: ignore +_MYPY = False +if _MYPY: + import typing # noqa: F401 # pylint: disable=import-error,unused-import,useless-suppression + + +# ------------------------------------------------------------------------ +class StoneEncoderInterface(object): + """ + Interface defining a stone object encoder. + """ + + def encode(self, validator, value): + # type: (bv.Validator, typing.Any) -> typing.Any + """ + Validate ``value`` using ``validator`` and return the encoding. + + Args: + validator: the ``stone_validators.Validator`` used to validate + ``value`` + value: the object to encode + + Returns: + The encoded object. This is implementation-defined. + + Raises: + stone_validators.ValidationError: Raised if ``value`` (or one + of its sub-values). + """ + raise NotImplementedError + +# ------------------------------------------------------------------------ +class StoneSerializerBase(StoneEncoderInterface): + + def __init__(self, alias_validators=None): + # type: (typing.Mapping[bv.Validator, typing.Callable[[typing.Any], None]]) -> None + """ + Constructor, `obviously + `. + + Args: + alias_validators (``typing.Mapping``, optional): A mapping of + custom validation callables in the format + ``{stone_validators.Validator: + typing.Callable[[typing.Any], None], ...}``. These callables must + raise a ``stone_validators.ValidationError`` on failure. + Defaults to ``None``. + """ + self._alias_validators = {} # type: typing.Dict[bv.Validator, typing.Callable[[typing.Any], None]] # noqa: E501 + + if alias_validators is not None: + self._alias_validators.update(alias_validators) + + @property + def alias_validators(self): + """ + A ``typing.Mapping`` of custom validation callables in the format + ``{stone_validators.Validator: typing.Callable[typing.Any], + ...}``. + """ + return self._alias_validators + + def encode(self, validator, value): + return self.encode_sub(validator, value) + + def encode_sub(self, validator, value): + # type: (bv.Validator, typing.Any) -> typing.Any + """ + Callback intended to be called by other ``encode`` methods to + delegate encoding of sub-values. Arguments have the same semantics + as with the ``encode`` method. + """ + if isinstance(validator, bv.List): + # Because Lists are mutable, we always validate them during + # serialization + validate_f = validator.validate + encode_f = self.encode_list + elif isinstance(validator, bv.Nullable): + validate_f = validator.validate + encode_f = self.encode_nullable + elif isinstance(validator, bv.Primitive): + validate_f = validator.validate + encode_f = self.encode_primitive + elif isinstance(validator, bv.Struct): + if isinstance(validator, bv.StructTree): + validate_f = validator.validate + encode_f = self.encode_struct_tree + else: + # Fields are already validated on assignment + validate_f = validator.validate_type_only + encode_f = self.encode_struct + elif isinstance(validator, bv.Union): + # Fields are already validated on assignment + validate_f = validator.validate_type_only + encode_f = self.encode_union + else: + raise bv.ValidationError('Unsupported data type {}'.format(type(validator).__name__)) + + validate_f(value) + + return encode_f(validator, value) + + def encode_list(self, validator, value): + # type: (bv.List, typing.Any) -> typing.Any + """ + Callback for serializing a ``stone_validators.List``. Arguments + have the same semantics as with the ``encode`` method. + """ + raise NotImplementedError + + def encode_nullable(self, validator, value): + # type: (bv.Nullable, typing.Any) -> typing.Any + """ + Callback for serializing a ``stone_validators.Nullable``. + Arguments have the same semantics as with the ``encode`` method. + """ + raise NotImplementedError + + def encode_primitive(self, validator, value): + # type: (bv.Primitive, typing.Any) -> typing.Any + """ + Callback for serializing a ``stone_validators.Primitive``. + Arguments have the same semantics as with the ``encode`` method. + """ + raise NotImplementedError + + def encode_struct(self, validator, value): + # type: (bv.Struct, typing.Any) -> typing.Any + """ + Callback for serializing a ``stone_validators.Struct``. Arguments + have the same semantics as with the ``encode`` method. + """ + raise NotImplementedError + + def encode_struct_tree(self, validator, value): + # type: (bv.StructTree, typing.Any) -> typing.Any + """ + Callback for serializing a ``stone_validators.StructTree``. + Arguments have the same semantics as with the ``encode`` method. + """ + raise NotImplementedError + + def encode_union(self, validator, value): + # type: (bv.Union, bb.Union) -> typing.Any + """ + Callback for serializing a ``stone_validators.Union``. Arguments + have the same semantics as with the ``encode`` method. + """ + raise NotImplementedError + +# ------------------------------------------------------------------------ +class StoneToPythonPrimitiveSerializer(StoneSerializerBase): + + def __init__(self, alias_validators=None, for_msgpack=False, old_style=False): + # type: (typing.Mapping[bv.Validator, typing.Callable[[typing.Any], None]], bool, bool) -> None # noqa: E501 + """ + Args: + alias_validators (``typing.Mapping``, optional): Passed + to ``StoneSerializer.__init__``. Defaults to ``None``. + for_msgpack (bool, optional): See the like-named property. + Defaults to ``False``. + old_style (bool, optional): See the like-named property. + Defaults to ``False``. + """ + super(StoneToPythonPrimitiveSerializer, self).__init__(alias_validators=alias_validators) + self._for_msgpack = for_msgpack + self._old_style = old_style + + @property + def for_msgpack(self): + """ + EXPERIMENTAL: A flag associated with the serializer indicating + whether objects produced by the ``encode`` method should be + encoded for msgpack. + + """ + return self._for_msgpack + + @property + def old_style(self): + """ + A flag associated with the serializer indicating whether objects + produced by the ``encode`` method should be encoded according to + Dropbox's old or new API styles. + """ + return self._old_style + + def encode_list(self, validator, value): + validated_value = validator.validate(value) + + return [self.encode_sub(validator.item_validator, value_item) for value_item in + validated_value] + + def encode_nullable(self, validator, value): + if value is None: + return None + + return self.encode_sub(validator.validator, value) + + def encode_primitive(self, validator, value): + if validator in self.alias_validators: + self.alias_validators[validator](value) + + if isinstance(validator, bv.Void): + return None + elif isinstance(validator, bv.Timestamp): + return _strftime(value, validator.format) + elif isinstance(validator, bv.Bytes): + if self.for_msgpack: + return value + else: + return base64.b64encode(value).decode('ascii') + elif isinstance(validator, bv.Integer) \ + and isinstance(value, bool): + # bool is sub-class of int so it passes Integer validation, + # but we want the bool to be encoded as ``0`` or ``1``, rather + # than ``False`` or ``True``, respectively + return int(value) + else: + return value + + def encode_struct(self, validator, value): + # Skip validation of fields with primitive data types because + # they've already been validated on assignment + d = collections.OrderedDict() # type: typing.Dict[str, typing.Any] + + for field_name, field_validator in validator.definition._all_fields_: + try: + field_value = getattr(value, field_name) + except AttributeError as exc: + raise bv.ValidationError(exc.args[0]) + + presence_key = '_%s_present' % field_name + + if field_value is not None \ + and getattr(value, presence_key): + # Only serialize struct fields that have been explicitly + # set, even if there is a default + try: + d[field_name] = self.encode_sub(field_validator, field_value) + except bv.ValidationError as exc: + exc.add_parent(field_name) + + raise + return d + + def encode_struct_tree(self, validator, value): + assert type(value) in validator.definition._pytype_to_tag_and_subtype_, \ + '%r is not a serializable subtype of %r.' % (type(value), validator.definition) + + tags, subtype = validator.definition._pytype_to_tag_and_subtype_[type(value)] + + assert len(tags) == 1, tags + assert not isinstance(subtype, bv.StructTree), \ + 'Cannot serialize type %r because it enumerates subtypes.' % subtype.definition + + if self.old_style: + d = { + tags[0]: self.encode_struct(subtype, value), + } + else: + d = collections.OrderedDict() + d['.tag'] = tags[0] + d.update(self.encode_struct(subtype, value)) + + return d + + def encode_union(self, validator, value): + if value._tag is None: + raise bv.ValidationError('no tag set') + + field_validator = validator.definition._tagmap[value._tag] + is_none = isinstance(field_validator, bv.Void) \ + or (isinstance(field_validator, bv.Nullable) + and value._value is None) + + def encode_sub(sub_validator, sub_value, parent_tag): + try: + encoded_val = self.encode_sub(sub_validator, sub_value) + except bv.ValidationError as exc: + exc.add_parent(parent_tag) + + raise + else: + return encoded_val + + if self.old_style: + if field_validator is None: + return value._tag + elif is_none: + return value._tag + else: + encoded_val = encode_sub(field_validator, value._value, value._tag) + + return {value._tag: encoded_val} + elif is_none: + return {'.tag': value._tag} + else: + encoded_val = encode_sub(field_validator, value._value, value._tag) + + if isinstance(field_validator, bv.Nullable): + # We've already checked for the null case above, + # so now we're only interested in what the + # wrapped validator is + field_validator = field_validator.validator + + if isinstance(field_validator, bv.Struct) \ + and not isinstance(field_validator, bv.StructTree): + d = collections.OrderedDict() # type: typing.Dict[str, typing.Any] + d['.tag'] = value._tag + d.update(encoded_val) + + return d + else: + return collections.OrderedDict(( + ('.tag', value._tag), + (value._tag, encoded_val), + )) + +# ------------------------------------------------------------------------ +class StoneToJsonSerializer(StoneToPythonPrimitiveSerializer): + + def encode(self, validator, value): + return json.dumps(super(StoneToJsonSerializer, self).encode(validator, value)) # -------------------------------------------------------------- # JSON Encoder +# +# These interfaces are preserved for backward compatibility and symmetry with deserialization +# functions. def json_encode(data_type, obj, alias_validators=None, old_style=False): """Encodes an object into JSON based on its type. @@ -81,10 +411,9 @@ def json_encode(data_type, obj, alias_validators=None, old_style=False): > JsonEncoder.encode(um) "{'update': {'path': 'a/b/c', 'rev': '1234'}}" """ - return json.dumps( - json_compat_obj_encode( - data_type, obj, alias_validators, old_style)) - + for_msgpack = False + serializer = StoneToJsonSerializer(alias_validators, for_msgpack, old_style) + return serializer.encode(data_type, obj) def json_compat_obj_encode( data_type, obj, alias_validators=None, old_style=False, @@ -101,223 +430,8 @@ def json_compat_obj_encode( See json_encode() for additional information about validation. """ - if isinstance(data_type, (bv.Struct, bv.Union)): - # Only validate the type because fields are validated on assignment. - data_type.validate_type_only(obj) - else: - data_type.validate(obj) - return _json_compat_obj_encode_helper( - data_type, obj, alias_validators, old_style, for_msgpack) - - -def _json_compat_obj_encode_helper( - data_type, obj, alias_validators, old_style, for_msgpack): - """ - See json_encode() for argument descriptions. - """ - if isinstance(data_type, bv.List): - return _encode_list( - data_type, obj, alias_validators, old_style=old_style, - for_msgpack=for_msgpack) - elif isinstance(data_type, bv.Nullable): - return _encode_nullable( - data_type, obj, alias_validators, old_style=old_style, - for_msgpack=for_msgpack) - elif isinstance(data_type, bv.Primitive): - return _make_json_friendly( - data_type, obj, alias_validators, for_msgpack=for_msgpack) - elif isinstance(data_type, bv.StructTree): - return _encode_struct_tree( - data_type, obj, alias_validators, old_style=old_style, - for_msgpack=for_msgpack) - elif isinstance(data_type, bv.Struct): - return _encode_struct( - data_type, obj, alias_validators, old_style=old_style, - for_msgpack=for_msgpack) - elif isinstance(data_type, bv.Union): - if old_style: - return _encode_union_old( - data_type, obj, alias_validators, for_msgpack=for_msgpack) - else: - return _encode_union( - data_type, obj, alias_validators, for_msgpack=for_msgpack) - else: - raise AssertionError('Unsupported data type %r' % - type(data_type).__name__) - - -def _encode_list(data_type, obj, alias_validators, old_style, for_msgpack): - """ - The data_type argument must be a List. - See json_encode() for argument descriptions. - """ - # Because Lists are mutable, we always validate them during serialization. - obj = data_type.validate(obj) - return [ - _json_compat_obj_encode_helper( - data_type.item_validator, item, alias_validators, old_style, for_msgpack) - for item in obj - ] - - -def _encode_nullable(data_type, obj, alias_validators, old_style, for_msgpack): - """ - The data_type argument must be a Nullable. - See json_encode() for argument descriptions. - """ - if obj is not None: - return _json_compat_obj_encode_helper( - data_type.validator, obj, alias_validators, old_style, for_msgpack) - else: - return None - - -def _encode_struct(data_type, obj, alias_validators, old_style, for_msgpack): - """ - The data_type argument must be a Struct or StructTree. - See json_encode() for argument descriptions. - """ - # We skip validation of fields with primitive data types in structs and - # unions because they've already been validated on assignment. - d = collections.OrderedDict() - for field_name, field_data_type in data_type.definition._all_fields_: - try: - val = getattr(obj, field_name) - except AttributeError as e: - raise bv.ValidationError(e.args[0]) - presence_key = '_%s_present' % field_name - if val is not None and getattr(obj, presence_key): - # This check makes sure that we don't serialize absent struct - # fields as null, even if there is a default. - try: - d[field_name] = _json_compat_obj_encode_helper( - field_data_type, val, alias_validators, old_style, - for_msgpack) - except bv.ValidationError as e: - e.add_parent(field_name) - raise - return d - - -def _encode_union(data_type, obj, alias_validators, for_msgpack): - """ - The data_type argument must be a Union. - See json_encode() for argument descriptions. - """ - if obj._tag is None: - raise bv.ValidationError('no tag set') - field_data_type = data_type.definition._tagmap[obj._tag] - - if (isinstance(field_data_type, bv.Void) or - (isinstance(field_data_type, bv.Nullable) and obj._value is None)): - return {'.tag': obj._tag} - else: - try: - encoded_val = _json_compat_obj_encode_helper( - field_data_type, obj._value, alias_validators, False, - for_msgpack) - except bv.ValidationError as e: - e.add_parent(obj._tag) - raise - else: - if isinstance(field_data_type, bv.Nullable): - # We've already checked for the null case above, so now we're - # only interested in what the wrapped validator is. - field_data_type = field_data_type.validator - if (isinstance(field_data_type, bv.Struct) and - not isinstance(field_data_type, bv.StructTree)): - d = collections.OrderedDict() - d['.tag'] = obj._tag - d.update(encoded_val) - return d - else: - return collections.OrderedDict([ - ('.tag', obj._tag), - (obj._tag, encoded_val)]) - - -def _encode_union_old(data_type, obj, alias_validators, for_msgpack): - """ - The data_type argument must be a Union. - See json_encode() for argument descriptions. - """ - if obj._tag is None: - raise bv.ValidationError('no tag set') - field_data_type = data_type.definition._tagmap[obj._tag] - if field_data_type is None: - return obj._tag - else: - if (isinstance(field_data_type, bv.Void) or - (isinstance(field_data_type, bv.Nullable) and - obj._value is None)): - return obj._tag - else: - try: - encoded_val = _json_compat_obj_encode_helper( - field_data_type, obj._value, alias_validators, True, - for_msgpack) - except bv.ValidationError as e: - e.add_parent(obj._tag) - raise - else: - return {obj._tag: encoded_val} - - -def _encode_struct_tree( - data_type, obj, alias_validators, old_style, for_msgpack): - """ - Args: - data_type (StructTree) - as_root (bool): If a struct with enumerated subtypes is designated as a - root, then its fields including those that are inherited are - encoded in the outermost JSON object together. - - See json_encode() for other argument descriptions. - """ - assert type(obj) in data_type.definition._pytype_to_tag_and_subtype_, ( - '%r is not a serializable subtype of %r.' % - (type(obj), data_type.definition)) - tags, subtype = data_type.definition._pytype_to_tag_and_subtype_[type(obj)] - assert len(tags) == 1, tags - assert not isinstance(subtype, bv.StructTree), ( - 'Cannot serialize type %r because it enumerates subtypes.' % - subtype.definition) - if old_style: - return { - tags[0]: - _encode_struct( - subtype, obj, alias_validators, old_style, for_msgpack) - } - d = collections.OrderedDict() - d['.tag'] = tags[0] - d.update( - _encode_struct(subtype, obj, alias_validators, old_style, for_msgpack)) - return d - - -def _make_json_friendly(data_type, val, alias_validators, for_msgpack): - """ - Convert a primitive type to a Python type that can be serialized by the - json package. - """ - if alias_validators is not None and data_type in alias_validators: - alias_validators[data_type](val) - if isinstance(data_type, bv.Void): - return None - elif isinstance(data_type, bv.Timestamp): - return val.strftime(data_type.format) - elif isinstance(data_type, bv.Bytes): - if for_msgpack: - return val - else: - return base64.b64encode(val).decode('ascii') - elif isinstance(data_type, bv.Integer) and isinstance(val, bool): - # A bool is a subclass of an int so it passes Integer validation. But, - # we want the bool to be encoded as an Integer (1/0) rather than T/F. - return int(val) - else: - return val - + serializer = StoneToPythonPrimitiveSerializer(alias_validators, for_msgpack, old_style) + return serializer.encode(data_type, obj) # -------------------------------------------------------------- # JSON Decoder @@ -745,6 +859,74 @@ def _make_stone_friendly( alias_validators[data_type](ret) return ret +# Adapted from: +# http://code.activestate.com/recipes/306860-proleptic-gregorian-dates-and-strftime-before-1900/ +# Remove the unsupposed "%s" command. But don't do it if there's an odd +# number of %s before the s because those are all escaped. Can't simply +# remove the s because the result of %sY should be %Y if %s isn't +# supported, not the 4 digit year. +_ILLEGAL_S = re.compile(r'((^|[^%])(%%)*%s)') + +def _findall(text, substr): + # Also finds overlaps + sites = [] + i = 0 + + while 1: + j = text.find(substr, i) + + if j == -1: + break + + sites.append(j) + i = j + 1 + + return sites + +# Every 28 years the calendar repeats, except through century leap years +# where it's 6 years. But only if you're using the Gregorian calendar. ;) +def _strftime(dt, fmt): + try: + return dt.strftime(fmt) + except ValueError: + if not six.PY2 or dt.year > 1900: + raise + + if _ILLEGAL_S.search(fmt): + raise TypeError("This strftime implementation does not handle %s") + + year = dt.year + + # For every non-leap year century, advance by 6 years to get into the + # 28-year repeat cycle + delta = 2000 - year + off = 6 * (delta // 100 + delta // 400) + year = year + off + + # Move to around the year 2000 + year = year + ((2000 - year) // 28) * 28 + timetuple = dt.timetuple() + s1 = time.strftime(fmt, (year,) + timetuple[1:]) + sites1 = _findall(s1, str(year)) + + s2 = time.strftime(fmt, (year + 28,) + timetuple[1:]) + sites2 = _findall(s2, str(year + 28)) + + sites = [] + + for site in sites1: + if site in sites2: + sites.append(site) + + s = s1 + syear = '%4d' % (dt.year,) + + for site in sites: + s = s[:site] + syear + s[site + 4:] + + return s + + try: import msgpack except ImportError: diff --git a/dropbox/stone_validators.py b/dropbox/stone_validators.py index 59f8975a..716d0760 100644 --- a/dropbox/stone_validators.py +++ b/dropbox/stone_validators.py @@ -18,12 +18,16 @@ import numbers import re import six -from typing import Optional +_MYPY = False +if _MYPY: + import typing # noqa: F401 # pylint: disable=import-error,unused-import,useless-suppression + +# See if six.PY3: - _binary_types = (bytes, memoryview) + _binary_types = (bytes, memoryview) # noqa: E501,F821 # pylint: disable=undefined-variable,useless-suppression else: - _binary_types = (bytes, buffer) + _binary_types = (bytes, buffer) # noqa: E501,F821 # pylint: disable=undefined-variable,useless-suppression class ValidationError(Exception): @@ -106,6 +110,7 @@ def get_default(self): class Primitive(Validator): """A basic type that is defined by Stone.""" + # pylint: disable=abstract-method pass @@ -122,8 +127,8 @@ class Integer(Primitive): Do not use this class directly. Extend it and specify a 'minimum' and 'maximum' value as class variables for a more restrictive integer range. """ - minimum = None # type: Optional[numbers.Integral] - maximum = None # type: Optional[numbers.Integral] + minimum = None # type: typing.Optional[int] + maximum = None # type: typing.Optional[int] def __init__(self, min_value=None, max_value=None): """ @@ -184,8 +189,8 @@ class Real(Primitive): and 'maximum' value to enforce a range that's a subset of the Python float implementation. Python floats are doubles. """ - minimum = None # type: Optional[numbers.Real] - maximum = None # type: Optional[numbers.Real] + minimum = None # type: typing.Optional[float] + maximum = None # type: typing.Optional[float] def __init__(self, min_value=None, max_value=None): """ @@ -347,11 +352,11 @@ class Timestamp(Primitive): since a native Python datetime object is preferred. The format, however, can and should be used by serializers.""" - def __init__(self, format): - """format must be composed of format codes that the C standard (1989) + def __init__(self, fmt): + """fmt must be composed of format codes that the C standard (1989) supports, most notably in its strftime() function.""" - assert isinstance(format, six.text_type), 'format must be a string' - self.format = format + assert isinstance(fmt, six.text_type), 'format must be a string' + self.format = fmt def validate(self, val): if not isinstance(val, datetime.datetime): @@ -367,6 +372,7 @@ def validate(self, val): class Composite(Validator): """Validator for a type that builds on other primitive and composite types.""" + # pylint: disable=abstract-method pass diff --git a/spec b/spec index fee0a628..4483042b 160000 --- a/spec +++ b/spec @@ -1 +1 @@ -Subproject commit fee0a628da40e604c3051c5a322a43d40e510209 +Subproject commit 4483042b0016788742a1c4233a56e163e65b3121 diff --git a/stone b/stone index bc3e5e66..4ad88ea0 160000 --- a/stone +++ b/stone @@ -1 +1 @@ -Subproject commit bc3e5e668799e20c48c6495271e3ab9bc76353ff +Subproject commit 4ad88ea0f3db50075bf20bab9306feb6c699c3b1 diff --git a/update_version.sh b/update_version.sh index a491b917..ea32b79b 100755 --- a/update_version.sh +++ b/update_version.sh @@ -2,5 +2,5 @@ if [ -z $1 ]; then echo "error: $0 needs a version number as argument. Current version: `python -c 'import dropbox; print(dropbox.__version__)'`"; else - sed -i "s/^__version__.*/__version__ = '$1'/g" dropbox/dropbox.py + perl -pi -e "s/^__version__.*/__version__ = '$1'/g" dropbox/dropbox.py fi