Skip to content

Commit 0db02b3

Browse files
committed
Metadata should not be mutable
1 parent 0b65f58 commit 0db02b3

File tree

4 files changed

+70
-0
lines changed

4 files changed

+70
-0
lines changed

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/phonemetadata.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,16 @@
2323

2424
class NumberFormat(UnicodeMixin):
2525
"""Representation of way that a phone number can be formatted for output"""
26+
_mutable = False
27+
2628
def __init__(self,
2729
pattern=None,
2830
format=None,
2931
leading_digits_pattern=None,
3032
national_prefix_formatting_rule=None,
3133
national_prefix_optional_when_formatting=False,
3234
domestic_carrier_code_formatting_rule=None):
35+
self._mutable = True
3336
# pattern is a regex that is used to match the national (significant)
3437
# number. For example, the pattern "(20)(\d{4})(\d{4})" will match
3538
# number "2070313000", which is the national (significant) number for
@@ -98,6 +101,7 @@ def __init__(self,
98101
# formatted when format_with_carrier_code is called, if carrier codes
99102
# are used for a certain country.
100103
self.domestic_carrier_code_formatting_rule = domestic_carrier_code_formatting_rule # None or Unicode string
104+
self._mutable = False
101105

102106
def merge_from(self, other):
103107
"""Merge information from another NumberFormat object into this one."""
@@ -139,14 +143,28 @@ def __unicode__(self):
139143
result += u")"
140144
return result
141145

146+
def __setattr__(self, name, value): # pragma no cover
147+
if self._mutable or name == "_mutable":
148+
super(NumberFormat, self).__setattr__(name, value)
149+
else:
150+
raise TypeError("Can't modify immutable instance")
151+
152+
def __delattr__(self, name): # pragma no cover
153+
if self._mutable:
154+
super(NumberFormat, self).__delattr__(name)
155+
else:
156+
raise TypeError("Can't modify immutable instance")
157+
142158

143159
class PhoneNumberDesc(UnicodeMixin):
144160
"""Class representing the description of a set of phone numbers."""
161+
_mutable = False
145162

146163
def __init__(self,
147164
national_number_pattern=None,
148165
possible_number_pattern=None,
149166
example_number=None):
167+
self._mutable = True
150168
# The national_number_pattern is the pattern that a valid national
151169
# significant number would match. This specifies information such as
152170
# its total length and leading digits.
@@ -164,6 +182,7 @@ def __init__(self,
164182
# An example national significant number for the specific type. It
165183
# should not contain any formatting information.
166184
self.example_number = example_number # None or Unicode string
185+
self._mutable = False
167186

168187
def merge_from(self, other):
169188
"""Merge information from another PhoneNumberDesc object into this one."""
@@ -201,13 +220,26 @@ def __unicode__(self):
201220
result += u")"
202221
return result
203222

223+
def __setattr__(self, name, value): # pragma no cover
224+
if self._mutable or name == "_mutable":
225+
super(PhoneNumberDesc, self).__setattr__(name, value)
226+
else:
227+
raise TypeError("Can't modify immutable instance")
228+
229+
def __delattr__(self, name): # pragma no cover
230+
if self._mutable:
231+
super(PhoneNumberDesc, self).__delattr__(name)
232+
else:
233+
raise TypeError("Can't modify immutable instance")
234+
204235

205236
class PhoneMetadata(UnicodeMixin):
206237
"""Class representing metadata for international telephone numbers for a region.
207238
208239
This class is hand created based on phonemetadata.proto. Please refer to that file
209240
for detailed descriptions of the meaning of each field.
210241
"""
242+
_mutable = False
211243
region_metadata = {} # ISO 3166-1 alpha 2 => PhoneMetadata
212244
# A mapping from a country calling code for a non-geographical entity to
213245
# the PhoneMetadata for that country calling code. Examples of the country
@@ -250,6 +282,8 @@ def __init__(self,
250282
leading_digits=None,
251283
leading_zero_possible=False,
252284
register=True):
285+
self._mutable = True
286+
253287
# The general_desc contains information which is a superset of
254288
# descriptions for all types of phone numbers. If any element is
255289
# missing in the description of a specific type of number, the element
@@ -419,6 +453,7 @@ def __init__(self,
419453
raise Exception("Duplicate PhoneMetadata for %s (from %s:%s)" % (id, self.id, self.country_code))
420454
else:
421455
kls_map[id] = self
456+
self._mutable = False
422457

423458
def __eq__(self, other):
424459
if not isinstance(other, PhoneMetadata):
@@ -475,3 +510,15 @@ def __unicode__(self):
475510
result += ",\n leading_zero_possible=True"
476511
result += u")"
477512
return result
513+
514+
def __setattr__(self, name, value): # pragma no cover
515+
if self._mutable or name == "_mutable":
516+
super(PhoneMetadata, self).__setattr__(name, value)
517+
else:
518+
raise TypeError("Can't modify immutable instance")
519+
520+
def __delattr__(self, name): # pragma no cover
521+
if self._mutable:
522+
super(PhoneMetadata, self).__delattr__(name)
523+
else:
524+
raise TypeError("Can't modify immutable instance")

python/phonenumbers/phonenumberutil.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -750,6 +750,7 @@ def format_by_pattern(numobj, number_format, user_defined_formats):
750750
formatted_number = nsn
751751
else:
752752
num_format_copy = NumberFormat()
753+
num_format_copy._mutable = True
753754
# Before we do a replacement of the national prefix pattern $NP with
754755
# the national prefix, we need to copy the rule so that subsequent
755756
# replacements for different numbers have the appropriate national
@@ -1088,6 +1089,7 @@ def _format_original_allow_mods(numobj, region_calling_from):
10881089
return national_format
10891090
# Otherwise, we need to remove the national prefix from our output.
10901091
new_format_rule = NumberFormat()
1092+
new_format_rule._mutable = True
10911093
new_format_rule.merge_from(format_rule)
10921094
new_format_rule.national_prefix_formatting_rule = None
10931095
return format_by_pattern(numobj, PhoneNumberFormat.NATIONAL, [new_format_rule])
@@ -1196,6 +1198,7 @@ def format_out_of_country_keeping_alpha_chars(numobj, region_calling_from):
11961198
# If no pattern above is matched, we format the original input
11971199
return raw_input
11981200
new_format = NumberFormat()
1201+
new_format._mutable = True
11991202
new_format.merge_from(formatting_pattern)
12001203
# The first group is the first group of digits that the user
12011204
# wrote together.

python/tests/phonenumberutiltest.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -693,6 +693,7 @@ def testFormatNumberForMobileDialing(self):
693693

694694
def testFormatByPattern(self):
695695
newNumFormat = NumberFormat(pattern="(\\d{3})(\\d{3})(\\d{4})", format="(\\1) \\2-\\3")
696+
newNumFormat._mutable = True
696697
newNumberFormats = [newNumFormat]
697698

698699
self.assertEqual("(650) 253-0000", phonenumbers.format_by_pattern(US_NUMBER, PhoneNumberFormat.NATIONAL,
@@ -1284,7 +1285,9 @@ def testExtractPossibleNumber(self):
12841285

12851286
def testMaybeStripNationalPrefix(self):
12861287
metadata = PhoneMetadata(id="Test", national_prefix_for_parsing="34", register=False)
1288+
metadata._mutable = True
12871289
metadata.general_desc = PhoneNumberDesc(national_number_pattern="\\d{4,8}")
1290+
metadata.general_desc._mutable = True
12881291
numberToStrip = "34356778"
12891292
strippedNumber = "356778"
12901293
cc, numberToStrip, rc = phonenumberutil._maybe_strip_national_prefix_carrier_code(numberToStrip, metadata)
@@ -2166,10 +2169,13 @@ def testIsAlphaNumber(self):
21662169
def testMetadataEquality(self):
21672170
# Python version extra tests for equality against other types
21682171
desc1 = PhoneNumberDesc(national_number_pattern="\\d{4,8}")
2172+
desc1._mutable = True
21692173
desc2 = PhoneNumberDesc(national_number_pattern="\\d{4,8}")
2174+
desc2._mutable = True
21702175
desc3 = PhoneNumberDesc(national_number_pattern="\\d{4,7}",
21712176
possible_number_pattern="\\d{7}",
21722177
example_number="1234567")
2178+
desc3._mutable = True
21732179
self.assertNotEqual(desc1, None)
21742180
self.assertNotEqual(desc1, "")
21752181
self.assertEqual(desc1, desc2)
@@ -2181,10 +2187,13 @@ def testMetadataEquality(self):
21812187
r"possible_number_pattern='\\d{7}', example_number='1234567')",
21822188
str(desc3))
21832189
nf1 = NumberFormat(pattern=r'\d{3}', format=r'\1', leading_digits_pattern=['1'])
2190+
nf1._mutable = True
21842191
nf2 = NumberFormat(pattern=r'\d{3}', format=r'\1', leading_digits_pattern=['1'])
2192+
nf2._mutable = True
21852193
nf3 = NumberFormat(pattern=r'\d{3}', format=r'\1', leading_digits_pattern=['2'],
21862194
national_prefix_formatting_rule='$NP',
21872195
domestic_carrier_code_formatting_rule='$NP')
2196+
nf3._mutable = True
21882197
self.assertEqual(nf1, nf2)
21892198
self.assertNotEqual(nf1, nf3)
21902199
self.assertNotEqual(nf1, None)
@@ -2196,8 +2205,11 @@ def testMetadataEquality(self):
21962205
self.assertNotEqual(nf1, nf3)
21972206

21982207
metadata1 = PhoneMetadata("XY", preferred_international_prefix=u'9123', register=False)
2208+
metadata1._mutable = True
21992209
metadata2 = PhoneMetadata("XY", preferred_international_prefix=u'9123', register=False)
2210+
metadata2._mutable = True
22002211
metadata3 = PhoneMetadata("XY", preferred_international_prefix=u'9100', register=False)
2212+
metadata3._mutable = True
22012213
self.assertEqual(metadata1, metadata2)
22022214
self.assertNotEqual(metadata1, metadata3)
22032215
self.assertTrue(metadata1 != metadata3)
@@ -2341,17 +2353,21 @@ def testCoverage(self):
23412353
# Temporarily insert invalid example number
23422354
metadata800 = PhoneMetadata.country_code_metadata[800]
23432355
saved_example = metadata800.general_desc.example_number
2356+
metadata800.general_desc._mutable = True
23442357
metadata800.general_desc.example_number = '01'
23452358
self.assertTrue(phonenumbers.example_number_for_non_geo_entity(800) is None)
23462359
metadata800.general_desc.example_number = saved_example
2360+
metadata800.general_desc._mutable = False
23472361

23482362
self.assertFalse(phonenumbers.phonenumberutil._raw_input_contains_national_prefix("077", "0", "JP"))
23492363

23502364
# Temporarily change formatting rule
23512365
metadataGB = PhoneMetadata.region_metadata["GB"]
23522366
saved_rule = metadataGB.number_format[0].national_prefix_formatting_rule
2367+
metadataGB.number_format[0]._mutable = True
23532368
metadataGB.number_format[0].national_prefix_formatting_rule = u'(\\1)'
23542369
numberWithoutNationalPrefixGB = phonenumbers.parse("2087654321", "GB", keep_raw_input=True)
23552370
self.assertEqual("(20) 8765 4321",
23562371
phonenumbers.format_in_original_format(numberWithoutNationalPrefixGB, "GB"))
23572372
metadataGB.number_format[0].national_prefix_formatting_rule = saved_rule
2373+
metadataGB.number_format[0]._mutable = False

0 commit comments

Comments
 (0)