From 8dc4becb6bb57da764f0f848c1e9e63d2de51364 Mon Sep 17 00:00:00 2001 From: Tyler Matteson Date: Tue, 27 Oct 2026 11:14:28 -0400 Subject: [PATCH] feat: support australian addresses --- README.rst | 2 +- pyap/api.py | 4 +- pyap/parser.py | 2 +- pyap/source_AU/__init__.py | 0 pyap/source_AU/data.py | 752 +++++++++++++++++++++++++++++++++++++ pyap/source_US/data.py | 22 +- pyap/utils.py | 20 + tests/test_parser.py | 8 + tests/test_parser_au.py | 280 ++++++++++++++ tox.ini | 26 +- 10 files changed, 1079 insertions(+), 37 deletions(-) create mode 100644 pyap/source_AU/__init__.py create mode 100644 pyap/source_AU/data.py create mode 100644 tests/test_parser_au.py diff --git a/README.rst b/README.rst index ffa3639..b2915a6 100644 --- a/README.rst +++ b/README.rst @@ -3,7 +3,7 @@ Pyap2: Python address parser Pyap2 is a maintained fork of Pyap, a regex-based python library for -detecting and parsing addresses. Currently it supports US πŸ‡ΊπŸ‡Έ, Canadian πŸ‡¨πŸ‡¦ and British πŸ‡¬πŸ‡§ addresses. +detecting and parsing addresses. Currently it supports US πŸ‡ΊπŸ‡Έ, Canadian πŸ‡¨πŸ‡¦, British πŸ‡¬πŸ‡§ and Australian πŸ‡¦πŸ‡Ί addresses. .. code-block:: python diff --git a/pyap/api.py b/pyap/api.py index 8830ba9..5aac7d0 100644 --- a/pyap/api.py +++ b/pyap/api.py @@ -16,7 +16,7 @@ from . import address -def parse(some_text: str, country: Literal["US", "GB", "CA"]) -> List[address.Address]: +def parse(some_text: str, country: Literal["US", "GB", "CA", "AU"]) -> List[address.Address]: """Creates request to AddressParser and returns list of Address objects """ @@ -25,7 +25,7 @@ def parse(some_text: str, country: Literal["US", "GB", "CA"]) -> List[address.Ad def parse_single_street( - some_text: str, country: Literal["US", "GB", "CA"] + some_text: str, country: Literal["US", "GB", "CA", "AU"] ) -> List[address.Address]: ap = parser.AddressParser(country) return ap.parse_single_street(some_text) diff --git a/pyap/parser.py b/pyap/parser.py index 2e05bef..bdae31c 100644 --- a/pyap/parser.py +++ b/pyap/parser.py @@ -21,7 +21,7 @@ class AddressParser: - def __init__(self, country: Literal["US", "CA", "GB"]): + def __init__(self, country: Literal["US", "CA", "GB", "AU"]): """Initialize with custom arguments""" self.country = country.upper() diff --git a/pyap/source_AU/__init__.py b/pyap/source_AU/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pyap/source_AU/data.py b/pyap/source_AU/data.py new file mode 100644 index 0000000..eb0b072 --- /dev/null +++ b/pyap/source_AU/data.py @@ -0,0 +1,752 @@ +# -*- coding: utf-8 -*- + +""" + pyap.source_AU.data + ~~~~~~~~~~~~~~~~~~~ + + Regular expression definitions for detecting Australian addresses. + + Street type vocabulary derived from Geoscape G-NAF (STREET_TYPE_AUT), + data.gov.au, CC BY 4.0. + + :copyright: (c) 2015 by Vladimir Goncharov. + :license: MIT, see LICENSE for more details. +""" + +from pyap.utils import str_list_to_upper_lower_regex, street_type_list_to_regex + +part_divider = r"(?:[\,\ \.\-]{0,3}[\,\n][\,\ \.\-]{0,3})" +space_pattern = r"(?:[\ \t]{1,3})" + +# Geoscape G-NAF STREET_TYPE_AUT (names and abbreviations); directions omitted. +street_type_list = [ + "ACCESS", + "ACCS", + "ACRE", + "AIRWALK", + "ALLEY", + "ALLEYWAY", + "ALLY", + "ALWY", + "AMBL", + "AMBLE", + "ANNEX", + "APP", + "APPROACH", + "ARC", + "ARCADE", + "ARTERIAL", + "ARTERY", + "ARTL", + "ARTY", + "AV", + "AVENUE", + "AWLK", + "BA", + "BANAN", + "BANK", + "BAY", + "BCH", + "BDGE", + "BDWY", + "BDY", + "BEACH", + "BEND", + "BIDI", + "BOARDWALK", + "BOULEVARD", + "BOULEVARDE", + "BOUNDARY", + "BOWL", + "BR", + "BRACE", + "BRAE", + "BRAN", + "BRANCH", + "BRDWLK", + "BREAK", + "BRET", + "BRETT", + "BRIDGE", + "BRK", + "BROADWALK", + "BROADWAY", + "BROW", + "BSWY", + "BULL", + "BUSWAY", + "BVD", + "BVDE", + "BWLK", + "BYPA", + "BYPASS", + "BYWAY", + "BYWY", + "CAUSEWAY", + "CCT", + "CENTRE", + "CENTREWAY", + "CH", + "CHASE", + "CIR", + "CIRCLE", + "CIRCLET", + "CIRCUIT", + "CIRCUS", + "CL", + "CLDE", + "CLOSE", + "CLR", + "CLT", + "CLUSTER", + "CMMN", + "CMMNS", + "CNCD", + "CNR", + "CNTN", + "CNWY", + "COLONNADE", + "COMMON", + "COMMONS", + "CON", + "CONCORD", + "CONCOURSE", + "CONNECTION", + "CONNECTOR", + "CONR", + "COPSE", + "CORNER", + "CORSO", + "COURSE", + "COURT", + "COURTYARD", + "COVE", + "CPS", + "CR", + "CRCS", + "CRESCENT", + "CREST", + "CRF", + "CRIEF", + "CRK", + "CROOK", + "CROSS", + "CROSSING", + "CROSSOVER", + "CRSE", + "CRSG", + "CRSS", + "CRST", + "CRUISEWAY", + "CRVR", + "CSAC", + "CSO", + "CSWY", + "CT", + "CTR", + "CTYD", + "CUL-DE-SAC", + "CUT", + "CUTT", + "CUTTING", + "CUWY", + "DALE", + "DASH", + "DE", + "DELL", + "DENE", + "DEVIATION", + "DIP", + "DISTRIBUTOR", + "DIV", + "DIVIDE", + "DOCK", + "DOM", + "DOMAIN", + "DOWN", + "DOWNS", + "DR", + "DRIVE", + "DRIVEWAY", + "DSTR", + "DVWY", + "DWNS", + "EASEMENT", + "EDGE", + "ELB", + "ELBOW", + "END", + "ENT", + "ENTRANCE", + "ESMT", + "ESP", + "ESPLANADE", + "EST", + "ESTATE", + "EXP", + "EXPRESSWAY", + "EXTENSION", + "EXTN", + "FAIRWAY", + "FAWY", + "FBRK", + "FIREBREAK", + "FIRELINE", + "FIRETRACK", + "FIRETRAIL", + "FITR", + "FLAT", + "FLATS", + "FLNE", + "FLTS", + "FOLLOW", + "FOLW", + "FOOTWAY", + "FORD", + "FORESHORE", + "FORK", + "FORM", + "FORMATION", + "FREEWAY", + "FRNT", + "FRONT", + "FRONTAGE", + "FRTG", + "FSHR", + "FTRK", + "FTWY", + "FWY", + "GAP", + "GARDEN", + "GARDENS", + "GATE", + "GATEWAY", + "GDN", + "GDNS", + "GLADE", + "GLDE", + "GLEN", + "GLY", + "GR", + "GRA", + "GRANGE", + "GREEN", + "GRN", + "GROVE", + "GTE", + "GULLY", + "GWY", + "HARBOUR", + "HAVEN", + "HEATH", + "HEIGHTS", + "HIGHROAD", + "HIGHWAY", + "HIKE", + "HILL", + "HILLS", + "HIRD", + "HLLW", + "HOLLOW", + "HRBR", + "HTH", + "HTS", + "HUB", + "HVN", + "HWY", + "ID", + "INLET", + "INLT", + "INTERCHANGE", + "INTG", + "ISLAND", + "JNC", + "JUNCTION", + "KEY", + "KEYS", + "KNOL", + "KNOLL", + "LADDER", + "LADR", + "LANDING", + "LANE", + "LANEWAY", + "LDG", + "LEAD", + "LEADER", + "LEDR", + "LINE", + "LINK", + "LINKWAY", + "LKT", + "LNKWAY", + "LNWY", + "LOOKOUT", + "LOOP", + "LYNN", + "LYNNE", + "MALL", + "MANOR", + "MANR", + "MART", + "MAZE", + "MEAD", + "MEANDER", + "MEW", + "MEWS", + "MILE", + "MNDR", + "MOTORWAY", + "MTWY", + "MZ", + "NEST", + "NOOK", + "NORTH", + "NTH", + "OTLK", + "OTLT", + "OUTLET", + "OUTLOOK", + "OVAL", + "PALMS", + "PARADE", + "PARADISE", + "PARK", + "PARKWAY", + "PASS", + "PASSAGE", + "PATH", + "PATHWAY", + "PDE", + "PENINSULA", + "PERCH", + "PIAZ", + "PIAZZA", + "PKT", + "PL", + "PLACE", + "PLAZA", + "PLMS", + "PLZA", + "PNT", + "POCKET", + "POINT", + "PORT", + "PRCH", + "PRDS", + "PREC", + "PRECINCT", + "PROM", + "PROMENADE", + "PRST", + "PSGE", + "PSLA", + "PURSUIT", + "PWAY", + "PWY", + "QDRT", + "QUAD", + "QUADRANT", + "QUAY", + "QUAYS", + "QY", + "QYS", + "RAMBLE", + "RAMP", + "RANGE", + "RCH", + "RD", + "RDGE", + "RDS", + "RDWY", + "REACH", + "REEF", + "RES", + "RESERVE", + "REST", + "RETREAT", + "RETURN", + "RIDE", + "RIDGE", + "RIGHT OF WAY", + "RING", + "RISE", + "RISING", + "RIVER", + "RMBL", + "RND", + "RNGE", + "ROAD", + "ROADS", + "ROADWAY", + "ROFW", + "ROTARY", + "ROUND", + "ROUTE", + "ROW", + "ROWE", + "RSNG", + "RTE", + "RTN", + "RTT", + "RTY", + "RUE", + "RUN", + "RVR", + "SBWY", + "SDNG", + "SERVICEWAY", + "SHUN", + "SHUNT", + "SIDING", + "SKLN", + "SKYLINE", + "SLOPE", + "SLPE", + "SOUTH", + "SPUR", + "SQ", + "SQUARE", + "ST", + "STAI", + "STEPS", + "STH", + "STPS", + "STRA", + "STRAIGHT", + "STRAIT", + "STRAND", + "STREET", + "STRIP", + "STRP", + "STRT", + "SUBWAY", + "SVWY", + "TARN", + "TCE", + "TERRACE", + "THFR", + "THOROUGHFARE", + "THROUGHWAY", + "THRU", + "TKWY", + "TLWY", + "TMWY", + "TOLLWAY", + "TOP", + "TOR", + "TRACK", + "TRAIL", + "TRAMWAY", + "TRAVERSE", + "TRIANGLE", + "TRK", + "TRL", + "TRUNKWAY", + "TUNL", + "TUNNEL", + "TURN", + "TVSE", + "TWIST", + "UNDERPASS", + "UPAS", + "VALE", + "VALLEY", + "VERGE", + "VIAD", + "VIADUCT", + "VIEW", + "VIEWS", + "VILLA", + "VILLAGE", + "VILLAS", + "VISTA", + "VLGE", + "VLLA", + "VLLS", + "VLLY", + "VSTA", + "VUE", + "VWS", + "WADE", + "WALK", + "WALKWAY", + "WATERS", + "WATERWAY", + "WAY", + "WD", + "WDS", + "WHARF", + "WHRF", + "WKWY", + "WOOD", + "WOODS", + "WTRS", + "WTWY", + "WYND", + "YARD", + "YARDS", + +] + +street_type_re = street_type_list_to_regex(street_type_list) + +street_number = r""" + (?P + (?:(? + (?(street_number) + [a-zA-Z0-9][a-zA-Z0-9 \.\-\']{0,40}? + | + [a-zA-Z][a-zA-Z \.\-\']{0,40}? + ) + ) +""" + +street_type = r""" + (?P + {street_type_re} + ) +""".format( + street_type_re=street_type_re, +) + +post_direction = r""" + (?P + (?: + N(?:orth)?\b|E(?:ast)?\b|S(?:outh)?\b|W(?:est)?\b| + NE\b|NW\b|SE\b|SW\b| + [Uu]pper\b|[Ll]ower\b|[Ee]xtension\b|[Ii]nner\b|[Oo]uter\b + ) + ) +""" + +unit_slash = r""" + (?P + [A-Za-z]?\d{1,5}[A-Za-z]? + ) + \ ?/\ ? +""" + +flat_type_words = [ + "Unit", + "U", + "Shop", + "Suite", + "Se", + "Ste", + "Apartment", + "Apt", + "Flat", + "Villa", + "Townhouse", + "Lot", + "Site", + "Office", + "Room", + "Factory", + "Studio", + "Stu", + "Warehouse", + "Whse", + "Townhouse", + "Tnhs", + "Duplex", + "Dupl", + "Kiosk", + "Ksk", + "Cottage", + "Ctge", + "Garage", + "Grge", + "Shed", + "Section", + "Sec", +] + +flat_type_tokens = sorted( + {word for word in flat_type_words if word != "U"}, + key=len, + reverse=True, +) +flat_types_re = str_list_to_upper_lower_regex(flat_type_tokens) + r"|\bU\b" + +occupancy = r""" + (?P + (?: + {flat_types} + ) + {space}? + [A-Za-z\#\-\&\d]{{1,7}} + {space}? + ) +""".format( + flat_types=flat_types_re, + space=space_pattern, +) + +floor = r""" + (?P + (?: + (?:[Ll]|[Ll]evel|[Ll]vl|[Ff]|[Ff]loor|[Gg]|[Gg]round| + [Bb]|[Bb]asement|[Ll][Gg]|[Uu][Gg]|[Mm]|[Mm]ezzanine) + {space}? + \d+[A-Za-z]{{0,2}} + ) + {space}? + ) +""".format( + space=space_pattern, +) + +building = r""" + (?P + (?: + [Bb][Uu][Ii][Ll][Dd][Ii][Nn][Gg]|[Bb][Ll][Dd][Gg] + ) + {space} + \d{{0,4}}[A-Za-z]? + {space}? + ) +""".format( + space=space_pattern, +) + +po_box = r""" + (?P + (?: + (?:[Gg]\.?[Pp]\.?[Oo]\.?|[Pp]\.?[Oo]\.?)\ ?[Bb][Oo][Xx] + |[Ll]ocked\ [Bb]ag + |[Pp]rivate\ [Bb]ag + |RMB|RSD|CMB + ) + {space}? + \d+ + ) +""".format( + space=space_pattern, +) + +street_body = r""" + (?P + (?: + {floor} {part_divider}? + )? + (?: + {occupancy} {part_divider}? + )? + (?: + {building} {part_divider}? + )? + ) + (?P + (?: + {unit_slash} + )? + (?: + {street_number}{space} + )? + {street_name}{space}? + (?:{street_type}{space}?)? + (?:{post_direction})? + ) +""".format( + street_number=street_number, + street_name=street_name, + street_type=street_type, + post_direction=post_direction, + unit_slash=unit_slash, + floor=floor, + building=building, + occupancy=occupancy, + part_divider=part_divider, + space=space_pattern, +) + +full_street = r""" + (?P + (?: + {po_box} + | + {street_body} + ) + ) +""".format( + po_box=po_box, + street_body=street_body, +) + +region1 = r""" + (?P + (?: + NSW\b|[Nn][Ss][Ww]\b| + VIC\b|[Vv][Ii][Cc]\b| + QLD\b|[Qq][Ll][Dd]\b| + TAS\b|[Tt][Aa][Ss]\b| + SA\b|WA\b|NT\b|ACT\b| + [Nn][Ee][Ww]\ [Ss][Oo][Uu][Tt][Hh]\ [Ww][Aa][Ll][Ee][Ss]| + [Vv][Ii][Cc][Tt][Oo][Rr][Ii][Aa]| + [Qq][Uu][Ee][Ee][Nn][Ss][Ll][Aa][Nn][Dd]| + [Ss][Oo][Uu][Tt][Hh]\ [Aa][Uu][Ss][Tt][Rr][Aa][Ll][Ii][Aa]| + [Ww][Ee][Ss][Tt][Ee][Rr][Nn]\ [Aa][Uu][Ss][Tt][Rr][Aa][Ll][Ii][Aa]| + [Tt][Aa][Ss][Mm][Aa][Nn][Ii][Aa]| + [Nn][Oo][Rr][Tt][Hh][Ee][Rr][Nn]\ [Tt][Ee][Rr][Rr][Ii][Tt][Oo][Rr][Yy]| + [Aa][Uu][Ss][Tt][Rr][Aa][Ll][Ii][Aa][Nn]\ [Cc][Aa][Pp][Ii][Tt][Aa][Ll]\ [Tt][Ee][Rr][Rr][Ii][Tt][Oo][Rr][Yy] + ) + ) +""" + +city = r""" + (?P + [A-Za-z0-9][A-Za-z0-9 \.\-\']*(?:\ [A-Za-z0-9][A-Za-z0-9 \.\-\']*)* + ) +""" + +postal_code = r""" + (?P + (?:(? + [Aa][Uu][Ss][Tt][Rr][Aa][Ll][Ii][Aa]| + [Aa][Uu][Ss]\b| + [Aa][Uu][Ss][Tt][Rr][Aa][Ll][Ii][Aa][Nn]| + (? + {full_street} + {address_tail_divider} + {city} + {address_tail_divider}? + {region1} + (?:{space}?{postal_code})? + (?:{part_divider}{country})? + ) +""".format( + full_street=full_street, + address_tail_divider=address_tail_divider, + part_divider=part_divider, + city=city, + region1=region1, + postal_code=postal_code, + country=country, + space=space_pattern, +) diff --git a/pyap/source_US/data.py b/pyap/source_US/data.py index adb8d88..9de9d55 100644 --- a/pyap/source_US/data.py +++ b/pyap/source_US/data.py @@ -14,19 +14,10 @@ :license: MIT, see LICENSE for more details. """ -import string from typing import List from typing import Optional - -def str_list_to_upper_lower_regex(str_list: List[str]) -> str: - regex = "|".join(set(str_list)).lower() - for letter in string.ascii_lowercase: - regex = regex.replace( - letter, "[{upper}{lower}]".format(upper=letter.upper(), lower=letter) - ) - - return regex +from pyap.utils import str_list_to_upper_lower_regex, street_type_list_to_regex space_div = r"(?:[\,\ ]{1,2}|$)" @@ -916,17 +907,6 @@ def numbered_or_typeless_street_name(idx: str) -> str: ] -def street_type_list_to_regex(street_type_list: list[str]) -> str: - """Converts a list of street types into a regex""" - street_types = str_list_to_upper_lower_regex(street_type_list) - - # Use \b to check that there are word boundaries before and after the street type - # Optionally match zero to two of " ", ",", or "." after the street name - street_types = street_types.replace("|", r"\b|\b") - street_types = r"\b" + r"(?:" + street_types + r")" + r"\b\.?" - return street_types - - street_types_re = street_type_list_to_regex(street_type_list) street_types_with_interstate_re = rf"{street_types_re}|{interstate_street_type}" diff --git a/pyap/utils.py b/pyap/utils.py index 81c4a72..87caca4 100644 --- a/pyap/utils.py +++ b/pyap/utils.py @@ -11,11 +11,31 @@ """ import re +import string from typing import Any, List, Union DEFAULT_FLAGS = re.VERBOSE | re.UNICODE +def str_list_to_upper_lower_regex(str_list: List[str]) -> str: + regex = "|".join(set(str_list)).lower() + for letter in string.ascii_lowercase: + regex = regex.replace( + letter, "[{upper}{lower}]".format(upper=letter.upper(), lower=letter) + ) + + return regex + + +def street_type_list_to_regex(street_type_list: list[str]) -> str: + """Converts a list of street types into a regex.""" + street_types = str_list_to_upper_lower_regex(street_type_list) + + street_types = street_types.replace("|", r"\b|\b") + street_types = r"\b" + r"(?:" + street_types + r")" + r"\b\.?" + return street_types + + def match( regex: Union[str, re.Pattern[str]], string: str, flags: re.RegexFlag = DEFAULT_FLAGS ) -> Union[re.Match[str], None]: diff --git a/tests/test_parser.py b/tests/test_parser.py index e7da9e8..7f1eca3 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -27,6 +27,14 @@ def test_api_parse_canada(): ) +def test_api_parse_australia(): + test_address = "xxx 2/45 Smith St, Newtown NSW 2042 xxx" + addresses = parse(test_address, country="AU") + assert ( + str(addresses[0].full_address) == "2/45 Smith St, Newtown NSW 2042" + ) + + def test_api_parse_single_street(): test_address = "255 SOUTH STREET" addresses = parse_single_street(test_address, country="US") diff --git a/tests/test_parser_au.py b/tests/test_parser_au.py new file mode 100644 index 0000000..3acb3b3 --- /dev/null +++ b/tests/test_parser_au.py @@ -0,0 +1,280 @@ +# pyright: reportPrivateUsage=false +# -*- coding: utf-8 -*- + +""" +Tests for Australian address parser. + +Positive address examples informed by Geoscape G-NAF (data.gov.au, CC BY 4.0) +and the ACNC Register of Australian Charities (data.gov.au, CC BY 3.0 AU). +""" + +import re +import pytest +import itertools +from pyap import parser, utils, parse +import pyap.source_AU.data as data_au + + +def execute_matching_test(input, expected, pattern): + match = utils.match(pattern, input, re.VERBOSE) + is_found = match is not None + if expected: + assert is_found == expected + assert match.group(0) == input # type: ignore + else: + assert (is_found == expected) or (match.group(0) != input) # type: ignore + + +unit_slash_pattern = r"(?P{})".format(data_au.unit_slash) + + +@pytest.mark.parametrize( + "input,expected", + [ + ("12", True), + ("12A", True), + ("12-14", True), + ("12345", True), + ("123456", False), + ], +) +def test_street_number(input, expected): + execute_matching_test(input, expected, data_au.street_number) + + +@pytest.mark.parametrize( + "input,expected", + [ + ("2/", True), + ("12A/", True), + ("2/ ", True), + ], +) +def test_unit_slash(input, expected): + execute_matching_test(input, expected, unit_slash_pattern) + + +@pytest.mark.parametrize( + "input,expected", + [ + ("St", True), + ("Street", True), + ("Tce", True), + ("Terrace", True), + ("Hwy", True), + ("Highway", True), + ("Pde", True), + ("Cct", True), + ("Rd", True), + ("Xyz", False), + ], +) +def test_street_type(input, expected): + execute_matching_test(input, expected, data_au.street_type) + + +@pytest.mark.parametrize( + "input,expected", + [ + ("Unit 5", True), + ("U 804", True), + ("Shop 3", True), + ("Suite 11", True), + ("Lot 12", True), + ("Apt 2B", True), + ], +) +def test_occupancy(input, expected): + execute_matching_test(input, expected, data_au.occupancy) + + +@pytest.mark.parametrize( + "input,expected", + [ + ("Level 15", True), + ("L 5", True), + ("L5", True), + ("Floor 3", True), + ], +) +def test_floor(input, expected): + execute_matching_test(input, expected, data_au.floor) + + +@pytest.mark.parametrize( + "input,expected", + [ + ("Po Box 3122", True), + ("P.O. Box 201", True), + ("GPO Box 1234", True), + ("Locked Bag 42", True), + ("Private Bag 100", True), + ("RMB 1234", True), + ("RSD 5678", True), + ], +) +def test_po_box(input, expected): + execute_matching_test(input, expected, data_au.po_box) + + +@pytest.mark.parametrize( + "input,expected", + [ + ("N", True), + ("NE", True), + ("Upper", True), + ("Lower", True), + ], +) +def test_post_direction(input, expected): + execute_matching_test(input, expected, data_au.post_direction) + + +@pytest.mark.parametrize( + "input,expected", + [ + ("NSW", True), + ("Vic", True), + ("QLD", True), + ("Qld", True), + ("WA", True), + ("ACT", True), + ("New South Wales", True), + ("Western Australia", True), + ("wa", False), + ("sa", False), + ("nt", False), + ], +) +def test_region1(input, expected): + execute_matching_test(input, expected, data_au.region1) + + +@pytest.mark.parametrize( + "input,expected", + [ + ("2042", True), + ("204", False), + ("20422", False), + ("12042", False), + ], +) +def test_postal_code(input, expected): + match = utils.match(data_au.postal_code, input, re.VERBOSE) + if expected: + assert match is not None + assert match.group("postal_code") == input.strip() # type: ignore + else: + assert match is None or match.group("postal_code") != input.strip() # type: ignore + + +@pytest.mark.parametrize( + "input,expected", + [ + ("Australia", True), + ("AUS", True), + ("AU", True), + ("Austria", False), + ], +) +def test_country(input, expected): + execute_matching_test(input, expected, data_au.country) + + +@pytest.mark.parametrize( + "input,expected", + [ + ("2/45 Smith St, Newtown NSW 2042", True), + ("433 Princes Hwy, Officer VIC 3809", True), + ("Po Box 3122, West Tamworth NSW 2340", True), + ("P.O. Box 201, Bendigo VIC 3552", True), + ("Level 15, 216 St Georges Tce, Perth WA 6000", True), + ("1/28 Breynia Street, Figtree NSW 2525", True), + ("12-14 Mcnicol St, Geelong West VIC 3218", True), + ("1a Park Bvd, Ferntree Gully VIC 3156", True), + ("500 Pacific Hwy, Belmont NSW 2280", True), + ("101 Tram Road, Doncaster VIC 3108", True), + ("292 Canterbury Rd, Surrey Hills VIC 3127", True), + ("20 Milan Tce, Stirling SA 5152", True), + ("55 Clyde St, Mollymook Beach NSW 2539", True), + ("49 Kildare Rd, Blacktown NSW 2148", True), + ("3126 Geelong-bacchus Marsh Rd, Balliang East VIC 3340", True), + ("478 George St, Sydney NSW 2000", True), + ("213-217 North Road, Albany WA 6330", True), + ("614 Burbridge Rd, West Beach SA 5024", True), + ("2 Bingar Street, Yenda NSW 2681", True), + ("14 Xuereb St, Tallawong NSW 2762", True), + ("216 St Georges Tce Perth WA 6000", True), + ("45 Smith St, Newtown NSW", True), + ("1 Main St, Brisbane QLD 4000, Australia", True), + ("(02) 9555 1234", False), + ("0412 345 678", False), + ("12 345 678 901", False), + ("45 Smith St, Newtown", False), + ("He moved to NSW in 2020", False), + ("wa s good day", False), + ], +) +def test_full_address(input, expected): + execute_matching_test(input, expected, data_au.full_address) + + +def test_full_address_parts(): + example_addresses = [ + { + "full_address": "2/45 Smith St, Newtown NSW 2042", + "occupancy": "2", + "street_number": "45", + "street_name": "Smith", + "street_type": "St", + "city": "Newtown", + "region1": "NSW", + "postal_code": "2042", + }, + { + "full_address": "Level 15, 216 St Georges Tce, Perth WA 6000", + "floor": "Level 15", + "street_number": "216", + "street_name": "St Georges", + "street_type": "Tce", + "city": "Perth", + "region1": "WA", + "postal_code": "6000", + }, + { + "full_address": "Po Box 3122, West Tamworth NSW 2340", + "po_box": "Po Box 3122", + "city": "West Tamworth", + "region1": "NSW", + "postal_code": "2340", + }, + ] + filler_text = ( + "This is filler text that can be inserted both before and after addresses" + ) + punctuation = ["\n", ", ", ". ", " "] + + for address_parts in example_addresses: + for filler_before, filler_after in itertools.product( + [False, True], [False, True] + ): + for join_string in punctuation: + filler_text_before = ( + (filler_text + join_string) if filler_before else "" + ) + filler_text_after = (join_string + filler_text) if filler_after else "" + address_text = ( + filler_text_before + + address_parts["full_address"] + + filler_text_after + ) + + parsed = parse(address_text, country="AU") + assert len(parsed) == 1 + for k, v in address_parts.items(): + if k == "full_address": + assert parsed[ + 0 + ].full_address == parser.AddressParser._normalize_string(v) + else: + assert parsed[0].__getattribute__(k) == v diff --git a/tox.ini b/tox.ini index f81fa40..bfc39aa 100644 --- a/tox.ini +++ b/tox.ini @@ -1,16 +1,18 @@ -# Tox (http://tox.testrun.org/) is a tool for running tests -# in multiple virtualenvs. This configuration file will run the -# test suite on all supported python versions. To use it, "pip install tox" -# and then run "tox" from this directory. +# Run the test suite in an isolated env (optional; Poetry is the primary workflow). +# From the repo root: tox +# Matches CI test command: poetry run task test -> pytest tests -vvv [tox] -envlist = py27, py38 +envlist = py39 +requires = tox>=4 +skip_missing_interpreters = true [testenv] -commands = py.test \ - test_parser.py \ - test_parser_ca.py \ - test_parser_us.py \ - test_parser_gb.py -deps = - pytest +description = pytest against tests/ (package installed editable via Poetry) +skip_install = true +allowlist_externals = + poetry +commands_pre = + poetry install --no-interaction --sync +commands = + poetry run pytest tests -vvv