Skip to content

Commit c64a9df

Browse files
committed
Merge branch 'dev' into python3
Conflicts: python/README python/phonenumbers/phonemetadata.py python/phonenumbers/phonenumber.py python/phonenumbers/util.py python/tests/phonenumberutiltest.py
2 parents ae4a162 + af8fb4b commit c64a9df

8 files changed

Lines changed: 131 additions & 33 deletions

File tree

python/README

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ Release Procedure
109109
- Ensure that HISTORY file is up-to-date.
110110
- Set the __version__ field in phonenumbers/__init__.py
111111
- Check that the list of symbols in phonenumbers/__init__.py __all__ is
112-
up to date.
112+
up to date. The allcheck.py script helps with this.
113113
- Optionally, force metadata regeneration:
114114
cd python && make metaclean alldata
115115
- Check that the unit tests all run successfully:
@@ -123,7 +123,7 @@ Release Procedure
123123
- Create a release-<version> branch and shift to it with:
124124
git checkout -b release-<version>
125125
- Push the branch to Github with:
126-
git push origin release-<version>
126+
git push <github-remote> release-<version>
127127
- Create a setup-tools packaged tarball dist/phonenumbers-<version>.tar.gz
128128
with:
129129
make sdist # runs setup.py sdist

python/buildmetadatafromxml.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@ def __init__(self, owning_xterr, xtag, national_prefix,
156156
self.io = None
157157
else:
158158
self.o = NumberFormat()
159+
self.o._mutable = True
159160
# Find the REQUIRED attribute
160161
self.o.pattern = xtag.attrib['pattern']
161162
# Find the IMPLIED attribute(s)
@@ -207,6 +208,7 @@ def __init__(self, owning_xterr, xtag, national_prefix,
207208
# If the intlFormat is set to "NA" the intlFormat should be ignored.
208209
self.io = NumberFormat(pattern=self.o.pattern,
209210
leading_digits_pattern=self.o.leading_digits_pattern)
211+
self.io._mutable = True
210212

211213
intl_format = _get_unique_child_value(xtag, "intlFormat")
212214
if intl_format is None:
@@ -231,6 +233,7 @@ class XPhoneNumberDesc(UnicodeMixin):
231233
def __init__(self, xtag,
232234
template=None, fill_na=True):
233235
self.o = PhoneNumberDesc()
236+
self.o._mutable = True
234237
self.o.national_number_pattern = None
235238
self.o.possible_number_pattern = None
236239
self.o.example_number = None
@@ -266,6 +269,7 @@ def __init__(self, xterritory):
266269
# Retrieve the REQUIRED attributes
267270
id = xterritory.attrib['id']
268271
self.o = PhoneMetadata(id, register=False)
272+
self.o._mutable = True
269273
self.o.country_code = int(xterritory.attrib['countryCode'])
270274
# Retrieve the IMPLIED attributes
271275
self.o.international_prefix = xterritory.get('internationalPrefix', None)

python/phonenumbers/geocoder.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,8 @@ def area_description_for_number(numobj, lang, script=None, region=None):
112112
number's area, or an empty string if no description is available."""
113113
e164_num = format_number(numobj, PhoneNumberFormat.E164)
114114
if not e164_num.startswith('+'): # pragma no cover
115+
# Can only hit this arm if there's an internal error in the rest of
116+
# the library
115117
raise Exception("Expect E164 number to start with +")
116118
for prefix_len in range(GEOCODE_LONGEST_PREFIX, 0, -1):
117119
prefix = e164_num[1:(1 + prefix_len)]

python/phonenumbers/phonemetadata.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,15 @@
1616
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1717
# See the License for the specific language governing permissions and
1818
# limitations under the License.
19-
from .util import UnicodeMixin, u, unicod, rpr, force_unicode
19+
from .util import UnicodeMixin, ImmutableMixin, mutating_method
20+
from .util import u, unicod, rpr, force_unicode
2021

2122
REGION_CODE_FOR_NON_GEO_ENTITY = u("001")
2223

2324

24-
class NumberFormat(UnicodeMixin):
25+
class NumberFormat(UnicodeMixin, ImmutableMixin):
2526
"""Representation of way that a phone number can be formatted for output"""
27+
@mutating_method
2628
def __init__(self,
2729
pattern=None,
2830
format=None,
@@ -140,9 +142,9 @@ def __unicode__(self):
140142
return result
141143

142144

143-
class PhoneNumberDesc(UnicodeMixin):
145+
class PhoneNumberDesc(UnicodeMixin, ImmutableMixin):
144146
"""Class representing the description of a set of phone numbers."""
145-
147+
@mutating_method
146148
def __init__(self,
147149
national_number_pattern=None,
148150
possible_number_pattern=None,
@@ -202,7 +204,7 @@ def __unicode__(self):
202204
return result
203205

204206

205-
class PhoneMetadata(UnicodeMixin):
207+
class PhoneMetadata(UnicodeMixin, ImmutableMixin):
206208
"""Class representing metadata for international telephone numbers for a region.
207209
208210
This class is hand created based on phonemetadata.proto. Please refer to that file
@@ -222,6 +224,7 @@ def metadata_for_region_or_calling_code(kls, country_calling_code, region_code):
222224
else:
223225
return kls.region_metadata.get(region_code, None)
224226

227+
@mutating_method
225228
def __init__(self,
226229
id,
227230
general_desc=None,

python/phonenumbers/phonenumber.py

Lines changed: 9 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@
1616
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1717
# See the License for the specific language governing permissions and
1818
# limitations under the License.
19-
from .util import UnicodeMixin, to_long, unicod, rpr, force_unicode
19+
from .util import UnicodeMixin, ImmutableMixin, mutating_method
20+
from .util import to_long, unicod, rpr, force_unicode
2021

2122

2223
class CountryCodeSource(object):
@@ -210,10 +211,8 @@ def __unicode__(self):
210211
return result
211212

212213

213-
class FrozenPhoneNumber(PhoneNumber):
214+
class FrozenPhoneNumber(PhoneNumber, ImmutableMixin):
214215
"""Immutable version of PhoneNumber"""
215-
_mutable = False
216-
217216
def __hash__(self):
218217
return hash((self.country_code,
219218
self.national_number,
@@ -223,25 +222,10 @@ def __hash__(self):
223222
self.country_code_source,
224223
self.preferred_domestic_carrier_code))
225224

226-
def __setattr__(self, name, value):
227-
if self._mutable or name == "_mutable":
228-
super(FrozenPhoneNumber, self).__setattr__(name, value)
229-
else:
230-
raise TypeError("Can't modify immutable instance")
231-
232-
def __delattr__(self, name):
233-
if self._mutable:
234-
super(FrozenPhoneNumber, self).__delattr__(name)
235-
else:
236-
raise TypeError("Can't modify immutable instance")
237-
225+
@mutating_method
238226
def __init__(self, *args, **kwargs):
239-
old_mutable = self._mutable
240-
self._mutable = True
241-
try:
242-
if len(kwargs) == 0 and len(args) == 1 and isinstance(args[0], PhoneNumber):
243-
super(FrozenPhoneNumber, self).__init__(**args[0].__dict__)
244-
else:
245-
super(FrozenPhoneNumber, self).__init__(*args, **kwargs)
246-
finally:
247-
self._mutable = old_mutable
227+
if len(kwargs) == 0 and len(args) == 1 and isinstance(args[0], PhoneNumber):
228+
# Copy constructor
229+
super(FrozenPhoneNumber, self).__init__(**args[0].__dict__)
230+
else:
231+
super(FrozenPhoneNumber, self).__init__(*args, **kwargs)

python/phonenumbers/phonenumberutil.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -752,6 +752,7 @@ def format_by_pattern(numobj, number_format, user_defined_formats):
752752
formatted_number = nsn
753753
else:
754754
num_format_copy = NumberFormat()
755+
num_format_copy._mutable = True
755756
# Before we do a replacement of the national prefix pattern $NP with
756757
# the national prefix, we need to copy the rule so that subsequent
757758
# replacements for different numbers have the appropriate national
@@ -1090,6 +1091,7 @@ def _format_original_allow_mods(numobj, region_calling_from):
10901091
return national_format
10911092
# Otherwise, we need to remove the national prefix from our output.
10921093
new_format_rule = NumberFormat()
1094+
new_format_rule._mutable = True
10931095
new_format_rule.merge_from(format_rule)
10941096
new_format_rule.national_prefix_formatting_rule = None
10951097
return format_by_pattern(numobj, PhoneNumberFormat.NATIONAL, [new_format_rule])
@@ -1198,6 +1200,7 @@ def format_out_of_country_keeping_alpha_chars(numobj, region_calling_from):
11981200
# If no pattern above is matched, we format the original input
11991201
return raw_input
12001202
new_format = NumberFormat()
1203+
new_format._mutable = True
12011204
new_format.merge_from(formatting_pattern)
12021205
# The first group is the first group of digits that the user
12031206
# wrote together.
@@ -2307,9 +2310,10 @@ def parse(number, region=None, keep_raw_input=False,
23072310
numobj.preferred_domestic_carrier_code = carrier_code
23082311
len_national_number = len(normalized_national_number)
23092312
if len_national_number < _MIN_LENGTH_FOR_NSN: # pragma no cover
2313+
# Check of _is_viable_phone_number() at the top of this function makes
2314+
# this effectively unhittable.
23102315
raise NumberParseException(NumberParseException.TOO_SHORT_NSN,
23112316
"The string supplied is too short to be a phone number.")
2312-
23132317
if len_national_number > _MAX_LENGTH_FOR_NSN:
23142318
raise NumberParseException(NumberParseException.TOO_LONG,
23152319
"The string supplied is too long to be a phone number.")

python/phonenumbers/util.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,37 @@ def force_unicode(s):
151151
else:
152152
return unicod(s)
153153

154+
155+
class ImmutableMixin(object):
156+
"""Mixin class to make objects of subclasses immutable"""
157+
_mutable = False
158+
159+
def __setattr__(self, name, value):
160+
if self._mutable or name == "_mutable":
161+
object.__setattr__(self, name, value)
162+
else:
163+
raise TypeError("Can't modify immutable instance")
164+
165+
def __delattr__(self, name):
166+
if self._mutable:
167+
object.__delattr__(self, name)
168+
else:
169+
raise TypeError("Can't modify immutable instance")
170+
171+
172+
def mutating_method(func):
173+
"""Decorator for methods that are allowed to modify immutable objects"""
174+
def wrapper(self, *__args, **__kwargs):
175+
old_mutable = self._mutable
176+
self._mutable = True
177+
try:
178+
# Call the wrapped function
179+
return func(self, *__args, **__kwargs)
180+
finally:
181+
self._mutable = old_mutable
182+
return wrapper
183+
184+
154185
if __name__ == '__main__': # pragma no cover
155186
import doctest
156187
doctest.testmod()

python/tests/phonenumberutiltest.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -685,6 +685,7 @@ def testFormatNumberForMobileDialing(self):
685685

686686
def testFormatByPattern(self):
687687
newNumFormat = NumberFormat(pattern="(\\d{3})(\\d{3})(\\d{4})", format="(\\1) \\2-\\3")
688+
newNumFormat._mutable = True
688689
newNumberFormats = [newNumFormat]
689690

690691
self.assertEqual("(650) 253-0000", phonenumbers.format_by_pattern(US_NUMBER, PhoneNumberFormat.NATIONAL,
@@ -1276,7 +1277,9 @@ def testExtractPossibleNumber(self):
12761277

12771278
def testMaybeStripNationalPrefix(self):
12781279
metadata = PhoneMetadata(id="Test", national_prefix_for_parsing="34", register=False)
1280+
metadata._mutable = True
12791281
metadata.general_desc = PhoneNumberDesc(national_number_pattern="\\d{4,8}")
1282+
metadata.general_desc._mutable = True
12801283
numberToStrip = "34356778"
12811284
strippedNumber = "356778"
12821285
cc, numberToStrip, rc = phonenumberutil._maybe_strip_national_prefix_carrier_code(numberToStrip, metadata)
@@ -2186,10 +2189,13 @@ def testIsAlphaNumber(self):
21862189
def testMetadataEquality(self):
21872190
# Python version extra tests for equality against other types
21882191
desc1 = PhoneNumberDesc(national_number_pattern="\\d{4,8}")
2192+
desc1._mutable = True
21892193
desc2 = PhoneNumberDesc(national_number_pattern="\\d{4,8}")
2194+
desc2._mutable = True
21902195
desc3 = PhoneNumberDesc(national_number_pattern="\\d{4,7}",
21912196
possible_number_pattern="\\d{7}",
21922197
example_number="1234567")
2198+
desc3._mutable = True
21932199
self.assertNotEqual(desc1, None)
21942200
self.assertNotEqual(desc1, "")
21952201
self.assertEqual(desc1, desc2)
@@ -2201,10 +2207,13 @@ def testMetadataEquality(self):
22012207
r"possible_number_pattern='\\d{7}', example_number='1234567')",
22022208
str(desc3))
22032209
nf1 = NumberFormat(pattern=r'\d{3}', format=r'\1', leading_digits_pattern=['1'])
2210+
nf1._mutable = True
22042211
nf2 = NumberFormat(pattern=r'\d{3}', format=r'\1', leading_digits_pattern=['1'])
2212+
nf2._mutable = True
22052213
nf3 = NumberFormat(pattern=r'\d{3}', format=r'\1', leading_digits_pattern=['2'],
22062214
national_prefix_formatting_rule='$NP',
22072215
domestic_carrier_code_formatting_rule='$NP')
2216+
nf3._mutable = True
22082217
self.assertEqual(nf1, nf2)
22092218
self.assertNotEqual(nf1, nf3)
22102219
self.assertNotEqual(nf1, None)
@@ -2216,15 +2225,72 @@ def testMetadataEquality(self):
22162225
self.assertNotEqual(nf1, nf3)
22172226

22182227
metadata1 = PhoneMetadata("XY", preferred_international_prefix=u('9123'), register=False)
2228+
metadata1._mutable = True
22192229
metadata2 = PhoneMetadata("XY", preferred_international_prefix=u('9123'), register=False)
2230+
metadata2._mutable = True
22202231
metadata3 = PhoneMetadata("XY", preferred_international_prefix=u('9100'), register=False)
2232+
metadata3._mutable = True
22212233
self.assertEqual(metadata1, metadata2)
22222234
self.assertNotEqual(metadata1, metadata3)
22232235
self.assertTrue(metadata1 != metadata3)
22242236
self.assertNotEqual(metadata1, None)
22252237
self.assertNotEqual(metadata1, "")
22262238
self.assertNotEqual(metadata1, 123)
22272239

2240+
def testFrozenPhoneNumberImmutable(self):
2241+
number = PhoneNumber(country_code=39, national_number=236618300L, italian_leading_zero=True)
2242+
frozen1 = FrozenPhoneNumber(country_code=39, national_number=236618300L, italian_leading_zero=True)
2243+
frozen2 = FrozenPhoneNumber(number)
2244+
self.assertEqual(number, frozen1)
2245+
self.assertEqual(frozen1, frozen2)
2246+
number.country_code = 999
2247+
self.assertNotEqual(number, frozen1)
2248+
try:
2249+
frozen1.country_code = 999
2250+
self.fail("Expected exception on __setattr__")
2251+
except TypeError:
2252+
pass
2253+
try:
2254+
del frozen2.country_code
2255+
self.fail("Expected exception on __delattr__")
2256+
except TypeError:
2257+
pass
2258+
2259+
def testMetadataImmutable(self):
2260+
desc = PhoneNumberDesc(national_number_pattern="\\d{4,8}")
2261+
nf = NumberFormat(pattern=r'\d{3}', format=r'\1', leading_digits_pattern=['1'])
2262+
metadata = PhoneMetadata("XY", preferred_international_prefix=u'9123', register=False)
2263+
try:
2264+
desc.national_number_pattern = ""
2265+
self.fail("Expected exception on __setattr__")
2266+
except TypeError:
2267+
pass
2268+
try:
2269+
del desc.national_number_pattern
2270+
self.fail("Expected exception on __delattr__")
2271+
except TypeError:
2272+
pass
2273+
try:
2274+
nf.pattern = ""
2275+
self.fail("Expected exception on __setattr__")
2276+
except TypeError:
2277+
pass
2278+
try:
2279+
del nf.pattern
2280+
self.fail("Expected exception on __delattr__")
2281+
except TypeError:
2282+
pass
2283+
try:
2284+
metadata.id = None
2285+
self.fail("Expected exception on __setattr__")
2286+
except TypeError:
2287+
pass
2288+
try:
2289+
del metadata.id
2290+
self.fail("Expected exception on __delattr__")
2291+
except TypeError:
2292+
pass
2293+
22282294
def testMetadataAsString(self):
22292295
# Python version extra tests for string conversions
22302296
metadata = PhoneMetadata.region_metadata["AU"]
@@ -2361,17 +2427,21 @@ def testCoverage(self):
23612427
# Temporarily insert invalid example number
23622428
metadata800 = PhoneMetadata.country_code_metadata[800]
23632429
saved_example = metadata800.general_desc.example_number
2430+
metadata800.general_desc._mutable = True
23642431
metadata800.general_desc.example_number = '01'
23652432
self.assertTrue(phonenumbers.example_number_for_non_geo_entity(800) is None)
23662433
metadata800.general_desc.example_number = saved_example
2434+
metadata800.general_desc._mutable = False
23672435

23682436
self.assertFalse(phonenumbers.phonenumberutil._raw_input_contains_national_prefix("077", "0", "JP"))
23692437

23702438
# Temporarily change formatting rule
23712439
metadataGB = PhoneMetadata.region_metadata["GB"]
23722440
saved_rule = metadataGB.number_format[0].national_prefix_formatting_rule
2441+
metadataGB.number_format[0]._mutable = True
23732442
metadataGB.number_format[0].national_prefix_formatting_rule = u('(\\1)')
23742443
numberWithoutNationalPrefixGB = phonenumbers.parse("2087654321", "GB", keep_raw_input=True)
23752444
self.assertEqual("(20) 8765 4321",
23762445
phonenumbers.format_in_original_format(numberWithoutNationalPrefixGB, "GB"))
23772446
metadataGB.number_format[0].national_prefix_formatting_rule = saved_rule
2447+
metadataGB.number_format[0]._mutable = False

0 commit comments

Comments
 (0)