|
1 | 1 | import copy |
2 | 2 | import pickle |
3 | 3 | import re |
| 4 | +import warnings |
4 | 5 |
|
5 | 6 | import pytest |
6 | 7 |
|
@@ -215,18 +216,22 @@ def test_deepcopy_default_name_preserves_singleton_identity(self) -> None: |
215 | 216 | self.assertEqual(dup.last, hn.last) |
216 | 217 |
|
217 | 218 | def test_comparison(self) -> None: |
| 219 | + # deprecated behavior (#223/#224), still supported until 2.0: |
| 220 | + # deprecated_call() asserts the warning fires while silencing it |
218 | 221 | hn1 = HumanName("Doe-Ray, Dr. John P., CLU, CFP, LUTC") |
219 | 222 | hn2 = HumanName("Dr. John P. Doe-Ray, CLU, CFP, LUTC") |
220 | | - self.assertTrue(hn1 == hn2) |
221 | | - self.assertIsNot(hn1, hn2) |
222 | | - self.assertTrue(hn1 == "Dr. John P. Doe-Ray CLU, CFP, LUTC") |
| 223 | + with pytest.deprecated_call(): |
| 224 | + self.assertTrue(hn1 == hn2) |
| 225 | + self.assertIsNot(hn1, hn2) |
| 226 | + self.assertTrue(hn1 == "Dr. John P. Doe-Ray CLU, CFP, LUTC") |
223 | 227 | hn1 = HumanName("Doe, Dr. John P., CLU, CFP, LUTC") |
224 | 228 | hn2 = HumanName("Dr. John P. Doe-Ray, CLU, CFP, LUTC") |
225 | | - self.assertTrue(not hn1 == hn2) |
226 | | - self.assertTrue(not hn1 == 0) |
227 | | - self.assertTrue(not hn1 == "test") |
228 | | - self.assertTrue(not hn1 == ["test"]) |
229 | | - self.assertTrue(not hn1 == {"test": hn2}) |
| 229 | + with pytest.deprecated_call(): |
| 230 | + self.assertTrue(not hn1 == hn2) |
| 231 | + self.assertTrue(not hn1 == 0) |
| 232 | + self.assertTrue(not hn1 == "test") |
| 233 | + self.assertTrue(not hn1 == ["test"]) |
| 234 | + self.assertTrue(not hn1 == {"test": hn2}) |
230 | 235 |
|
231 | 236 | def test_assignment_to_full_name(self) -> None: |
232 | 237 | hn = HumanName("John A. Kenneth Doe, Jr.") |
@@ -275,30 +280,147 @@ def test_assign_list_to_attribute(self) -> None: |
275 | 280 | self.m(hn.suffix, "test", hn) |
276 | 281 |
|
277 | 282 | def test_comparison_case_insensitive(self) -> None: |
| 283 | + # deprecated behavior (#223/#224), still supported until 2.0 |
278 | 284 | hn1 = HumanName("Doe-Ray, Dr. John P., CLU, CFP, LUTC") |
279 | 285 | hn2 = HumanName("dr. john p. doe-Ray, CLU, CFP, LUTC") |
280 | | - self.assertTrue(hn1 == hn2) |
281 | | - self.assertIsNot(hn1, hn2) |
282 | | - self.assertTrue(hn1 == "Dr. John P. Doe-ray clu, CFP, LUTC") |
| 286 | + with pytest.deprecated_call(): |
| 287 | + self.assertTrue(hn1 == hn2) |
| 288 | + self.assertIsNot(hn1, hn2) |
| 289 | + self.assertTrue(hn1 == "Dr. John P. Doe-ray clu, CFP, LUTC") |
283 | 290 |
|
284 | 291 | def test_hash_matches_case_insensitive_equality(self) -> None: |
| 292 | + # deprecated behavior (#223/#224), still supported until 2.0. |
285 | 293 | # __eq__ compares lowercased strings, so __hash__ must too: |
286 | 294 | # equal objects are required to have equal hashes. |
287 | 295 | hn1 = HumanName("Doe-Ray, Dr. John P., CLU, CFP, LUTC") |
288 | 296 | hn2 = HumanName("dr. john p. doe-Ray, CLU, CFP, LUTC") |
289 | | - self.assertEqual(hn1, hn2) |
290 | | - self.assertEqual(hash(hn1), hash(hn2)) |
291 | | - self.assertEqual(len({hn1, hn2}), 1) |
292 | | - # __eq__ also accepts plain strings, so hashing str(self).lower() |
293 | | - # specifically (not e.g. an attribute tuple) is what lets strings and |
294 | | - # HumanName instances interoperate in sets and dicts |
295 | | - hn = HumanName("John Smith") |
296 | | - self.assertEqual(hash(hn), hash("john smith")) |
297 | | - self.assertIn("john smith", {hn}) |
| 297 | + with pytest.deprecated_call(): |
| 298 | + self.assertEqual(hn1, hn2) |
| 299 | + self.assertEqual(hash(hn1), hash(hn2)) |
| 300 | + self.assertEqual(len({hn1, hn2}), 1) |
| 301 | + # __eq__ also accepts plain strings, so hashing str(self).lower() |
| 302 | + # specifically (not e.g. an attribute tuple) is what lets strings |
| 303 | + # and HumanName instances interoperate in sets and dicts |
| 304 | + hn = HumanName("John Smith") |
| 305 | + self.assertEqual(hash(hn), hash("john smith")) |
| 306 | + self.assertIn("john smith", {hn}) |
298 | 307 |
|
299 | 308 | def test_not_equal_operator(self) -> None: |
300 | | - self.assertTrue(HumanName("John Smith") != HumanName("Jane Smith")) |
301 | | - self.assertFalse(HumanName("John Smith") != HumanName("john smith")) |
| 309 | + # deprecated behavior (#223/#224), still supported until 2.0; |
| 310 | + # != routes through __eq__, so it warns too |
| 311 | + with pytest.deprecated_call(): |
| 312 | + self.assertTrue(HumanName("John Smith") != HumanName("Jane Smith")) |
| 313 | + self.assertFalse(HumanName("John Smith") != HumanName("john smith")) |
| 314 | + |
| 315 | + def test_comparison_key_components(self) -> None: |
| 316 | + hn = HumanName("Dr. Juan Q. Xavier de la Vega III") |
| 317 | + self.assertEqual( |
| 318 | + hn.comparison_key(), |
| 319 | + ('dr.', 'juan', 'q. xavier', 'de la vega', 'iii', '', '')) |
| 320 | + |
| 321 | + def test_comparison_key_case_insensitive_across_formats(self) -> None: |
| 322 | + hn1 = HumanName("Dr. Juan Q. Xavier de la Vega III") |
| 323 | + hn2 = HumanName("de la vega, dr. juan Q. xavier III") |
| 324 | + self.assertEqual(hn1.comparison_key(), hn2.comparison_key()) |
| 325 | + |
| 326 | + def test_comparison_key_independent_of_string_format(self) -> None: |
| 327 | + # unlike ==, which compares str(self) and so changes meaning when |
| 328 | + # display config changes, the key is built from the parsed lists |
| 329 | + hn1 = HumanName("John Smith") |
| 330 | + hn2 = HumanName("John Smith", string_format="{last}") |
| 331 | + self.assertEqual(hn1.comparison_key(), hn2.comparison_key()) |
| 332 | + |
| 333 | + def test_comparison_key_includes_maiden(self) -> None: |
| 334 | + # maiden isn't in the default string_format, so == can't see it; |
| 335 | + # the key includes all seven members |
| 336 | + hn1 = HumanName(first="Jenny", last="Baker", maiden="Johnson") |
| 337 | + hn2 = HumanName(first="Jenny", last="Baker") |
| 338 | + self.assertNotEqual(hn1.comparison_key(), hn2.comparison_key()) |
| 339 | + |
| 340 | + def test_comparison_key_usable_for_dedup(self) -> None: |
| 341 | + names = [HumanName("John Smith"), HumanName("Smith, John"), |
| 342 | + HumanName("JOHN SMITH"), HumanName("Jane Smith")] |
| 343 | + unique = {n.comparison_key(): n for n in names} |
| 344 | + self.assertEqual(len(unique), 2) |
| 345 | + |
| 346 | + def test_matches_str_is_semantic_not_textual(self) -> None: |
| 347 | + # any written form of the same name matches, unlike == which only |
| 348 | + # matches strings that render exactly like str(self) |
| 349 | + hn = HumanName("Dr. Juan Q. Xavier de la Vega III") |
| 350 | + self.assertTrue(hn.matches("de la vega, dr. juan Q. xavier III")) |
| 351 | + self.assertTrue(hn.matches("Dr. Juan Q. Xavier de la Vega III")) |
| 352 | + self.assertFalse(hn.matches("Juan de la Vega")) |
| 353 | + |
| 354 | + def test_matches_humanname_operand(self) -> None: |
| 355 | + hn = HumanName("John Smith") |
| 356 | + self.assertTrue(hn.matches(HumanName("JOHN SMITH"))) |
| 357 | + self.assertFalse(hn.matches(HumanName("Jane Smith"))) |
| 358 | + |
| 359 | + def test_matches_parses_str_with_instance_constants(self) -> None: |
| 360 | + # the custom title must not be a default one ('chancellor' is!), or |
| 361 | + # this passes without the self.C parse path ever mattering |
| 362 | + c = Constants() |
| 363 | + c.titles.add('zephyrmark') |
| 364 | + self.assertNotIn('zephyrmark', CONSTANTS.titles) |
| 365 | + hn = HumanName("Zephyrmark Jane Smith", constants=c) |
| 366 | + # the str operand is parsed with self.C, so the custom title is |
| 367 | + # recognized in the comma form; parsed with the shared CONSTANTS, |
| 368 | + # 'zephyrmark' would land in first/middle and the keys would differ |
| 369 | + self.assertTrue(hn.matches("smith, zephyrmark jane")) |
| 370 | + |
| 371 | + def test_matches_humanname_operand_keeps_its_own_parse(self) -> None: |
| 372 | + # asymmetry, pinned deliberately: a str operand is reparsed with |
| 373 | + # self.C, but a HumanName operand is compared as already parsed -- |
| 374 | + # its own constants determined its components |
| 375 | + c = Constants() |
| 376 | + c.titles.add('zephyrmark') |
| 377 | + with_title = HumanName("Zephyrmark Jane Smith", constants=c) |
| 378 | + default_parse = HumanName("Zephyrmark Jane Smith") |
| 379 | + self.assertTrue(with_title.matches("Zephyrmark Jane Smith")) |
| 380 | + self.assertFalse(with_title.matches(default_parse)) |
| 381 | + |
| 382 | + def test_empty_parses_share_a_comparison_key(self) -> None: |
| 383 | + # documented caveat: empty/unparsable input collapses to the |
| 384 | + # all-empty key, so such names match each other and collide in |
| 385 | + # dedup; screen with len(name) == 0 first |
| 386 | + self.assertTrue(HumanName("").matches(HumanName(","))) |
| 387 | + self.assertEqual(HumanName("()").comparison_key(), |
| 388 | + HumanName("").comparison_key()) |
| 389 | + |
| 390 | + def test_matches_non_ascii_case_insensitive(self) -> None: |
| 391 | + hn = HumanName("JOSÉ GARCÍA") |
| 392 | + self.assertTrue(hn.matches("José García")) |
| 393 | + |
| 394 | + def test_matches_rejects_other_types(self) -> None: |
| 395 | + hn = HumanName("John Smith") |
| 396 | + for bad in (None, 42, b"John Smith", ["John Smith"]): |
| 397 | + with pytest.raises(TypeError, match="str or HumanName"): |
| 398 | + hn.matches(bad) # type: ignore[arg-type] |
| 399 | + |
| 400 | + def test_eq_emits_deprecation_warning(self) -> None: |
| 401 | + # behavior is unchanged until 2.0 (#223); 1.3.0 only warns |
| 402 | + hn1 = HumanName("John Smith") |
| 403 | + hn2 = HumanName("john smith") |
| 404 | + with pytest.deprecated_call(match="matches"): |
| 405 | + result = hn1 == hn2 |
| 406 | + self.assertTrue(result) |
| 407 | + |
| 408 | + def test_hash_emits_deprecation_warning(self) -> None: |
| 409 | + hn = HumanName("John Smith") |
| 410 | + with pytest.deprecated_call(match="comparison_key"): |
| 411 | + result = hash(hn) |
| 412 | + self.assertEqual(result, hash("john smith")) |
| 413 | + |
| 414 | + def test_new_comparison_api_does_not_warn(self) -> None: |
| 415 | + # the replacements must be adoptable before 2.0 without tripping |
| 416 | + # -W error test suites; matches(str) parses, so this also covers |
| 417 | + # the parse path |
| 418 | + hn = HumanName("John Smith") |
| 419 | + with warnings.catch_warnings(): |
| 420 | + warnings.simplefilter("error") |
| 421 | + hn.comparison_key() |
| 422 | + hn.matches("Smith, John") |
| 423 | + hn.matches(HumanName("John Smith")) |
302 | 424 |
|
303 | 425 | def test_unparsable_attribute_removed(self) -> None: |
304 | 426 | # Removed in 1.3.0: the guard that reported unparsable names was |
|
0 commit comments