diff --git a/.github/workflows/typing.yml b/.github/workflows/typing.yml new file mode 100644 index 000000000..5d5e89dc9 --- /dev/null +++ b/.github/workflows/typing.yml @@ -0,0 +1,45 @@ +name: Test type stubs +on: [push, pull_request] + +jobs: + stubtest: + name: Run stubtest + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Set up latest Python 3 + uses: actions/setup-python@v2 + with: + python-version: 3 + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install mypy types-protobuf + + - name: Run mypy.stubtest + run: | + cd python + python run_stubtest.py + + mypy: + name: Run mypy on stubs + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Set up latest Python 3 + uses: actions/setup-python@v2 + with: + python-version: 3 + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install mypy types-protobuf + + - name: Run mypy + run: | + cd python + mypy --exclude pb2/ -p phonenumbers \ No newline at end of file diff --git a/README.md b/README.md index 2b329ee97..2b22d68f7 100644 --- a/README.md +++ b/README.md @@ -210,6 +210,16 @@ load of metadata will not cause a pause or memory exhaustion): The `phonenumberslite` version of the package does not include the geocoding, carrier and timezone metadata, which can be useful if you have problems installing the main `phonenumbers` package due to space/memory limitations. +Static Typing +------------- + +The library includes a set of type [stub files](https://www.python.org/dev/peps/pep-0484/#stub-files) to support static +type checking by library users. These stub files signal the types that should be used, and may also be of use in IDEs +which have integrated type checking functionalities. + +These files are written for Python 3, and as such type checking the library with these stubs on Python 2.5-2.7 is +unsupported. + Project Layout -------------- diff --git a/python/MANIFEST.in b/python/MANIFEST.in index 85595092e..2f9440b9b 100644 --- a/python/MANIFEST.in +++ b/python/MANIFEST.in @@ -1,4 +1,3 @@ -recursive-include tests *.py include HISTORY.md include LICENSE include testwrapper.py diff --git a/python/phonenumbers/__init__.pyi b/python/phonenumbers/__init__.pyi new file mode 100644 index 000000000..73aadca80 --- /dev/null +++ b/python/phonenumbers/__init__.pyi @@ -0,0 +1,82 @@ +from .asyoutypeformatter import AsYouTypeFormatter as AsYouTypeFormatter +from .phonemetadata import NumberFormat as NumberFormat +from .phonemetadata import PhoneMetadata as PhoneMetadata +from .phonemetadata import PhoneNumberDesc as PhoneNumberDesc +from .phonemetadata import REGION_CODE_FOR_NON_GEO_ENTITY as REGION_CODE_FOR_NON_GEO_ENTITY +from .phonenumber import CountryCodeSource as CountryCodeSource +from .phonenumber import FrozenPhoneNumber as FrozenPhoneNumber +from .phonenumber import PhoneNumber as PhoneNumber +from .phonenumbermatcher import Leniency as Leniency +from .phonenumbermatcher import PhoneNumberMatch as PhoneNumberMatch +from .phonenumbermatcher import PhoneNumberMatcher as PhoneNumberMatcher +from .phonenumberutil import can_be_internationally_dialled as can_be_internationally_dialled +from .phonenumberutil import convert_alpha_characters_in_number as convert_alpha_characters_in_number +from .phonenumberutil import country_code_for_region as country_code_for_region +from .phonenumberutil import country_code_for_valid_region as country_code_for_valid_region +from .phonenumberutil import COUNTRY_CODE_TO_REGION_CODE as COUNTRY_CODE_TO_REGION_CODE +from .phonenumberutil import COUNTRY_CODES_FOR_NON_GEO_REGIONS as COUNTRY_CODES_FOR_NON_GEO_REGIONS +from .phonenumberutil import country_mobile_token as country_mobile_token +from .phonenumberutil import example_number as example_number +from .phonenumberutil import example_number_for_non_geo_entity as example_number_for_non_geo_entity +from .phonenumberutil import example_number_for_type as example_number_for_type +from .phonenumberutil import format_by_pattern as format_by_pattern +from .phonenumberutil import format_in_original_format as format_in_original_format +from .phonenumberutil import format_national_number_with_carrier_code as format_national_number_with_carrier_code +from .phonenumberutil import format_national_number_with_preferred_carrier_code as format_national_number_with_preferred_carrier_code +from .phonenumberutil import format_number as format_number +from .phonenumberutil import format_number_for_mobile_dialing as format_number_for_mobile_dialing +from .phonenumberutil import format_out_of_country_calling_number as format_out_of_country_calling_number +from .phonenumberutil import format_out_of_country_keeping_alpha_chars as format_out_of_country_keeping_alpha_chars +from .phonenumberutil import invalid_example_number as invalid_example_number +from .phonenumberutil import is_alpha_number as is_alpha_number +from .phonenumberutil import is_mobile_number_portable_region as is_mobile_number_portable_region +from .phonenumberutil import is_nanpa_country as is_nanpa_country +from .phonenumberutil import is_number_geographical as is_number_geographical +from .phonenumberutil import is_number_match as is_number_match +from .phonenumberutil import is_number_type_geographical as is_number_type_geographical +from .phonenumberutil import is_possible_number as is_possible_number +from .phonenumberutil import is_possible_number_for_type as is_possible_number_for_type +from .phonenumberutil import is_possible_number_for_type_with_reason as is_possible_number_for_type_with_reason +from .phonenumberutil import is_possible_number_string as is_possible_number_string +from .phonenumberutil import is_possible_number_with_reason as is_possible_number_with_reason +from .phonenumberutil import is_valid_number as is_valid_number +from .phonenumberutil import is_valid_number_for_region as is_valid_number_for_region +from .phonenumberutil import length_of_geographical_area_code as length_of_geographical_area_code +from .phonenumberutil import length_of_national_destination_code as length_of_national_destination_code +from .phonenumberutil import MatchType as MatchType +from .phonenumberutil import national_significant_number as national_significant_number +from .phonenumberutil import ndd_prefix_for_region as ndd_prefix_for_region +from .phonenumberutil import NON_DIGITS_PATTERN as NON_DIGITS_PATTERN +from .phonenumberutil import normalize_diallable_chars_only as normalize_diallable_chars_only +from .phonenumberutil import normalize_digits_only as normalize_digits_only +from .phonenumberutil import number_type as number_type +from .phonenumberutil import NumberParseException as NumberParseException +from .phonenumberutil import parse as parse +from .phonenumberutil import PhoneNumberFormat as PhoneNumberFormat +from .phonenumberutil import PhoneNumberType as PhoneNumberType +from .phonenumberutil import region_code_for_country_code as region_code_for_country_code +from .phonenumberutil import region_code_for_number as region_code_for_number +from .phonenumberutil import region_codes_for_country_code as region_codes_for_country_code +from .phonenumberutil import supported_calling_codes as supported_calling_codes +from .phonenumberutil import SUPPORTED_REGIONS as SUPPORTED_REGIONS +from .phonenumberutil import supported_types_for_non_geo_entity as supported_types_for_non_geo_entity +from .phonenumberutil import supported_types_for_region as supported_types_for_region +from .phonenumberutil import truncate_too_long_number as truncate_too_long_number +from .phonenumberutil import UNKNOWN_REGION as UNKNOWN_REGION +from .phonenumberutil import ValidationResult as ValidationResult +from .shortnumberinfo import connects_to_emergency_number as connects_to_emergency_number +from .shortnumberinfo import expected_cost as expected_cost +from .shortnumberinfo import expected_cost_for_region as expected_cost_for_region +from .shortnumberinfo import is_carrier_specific as is_carrier_specific +from .shortnumberinfo import is_carrier_specific_for_region as is_carrier_specific_for_region +from .shortnumberinfo import is_emergency_number as is_emergency_number +from .shortnumberinfo import is_possible_short_number as is_possible_short_number +from .shortnumberinfo import is_possible_short_number_for_region as is_possible_short_number_for_region +from .shortnumberinfo import is_sms_service_for_region as is_sms_service_for_region +from .shortnumberinfo import is_valid_short_number as is_valid_short_number +from .shortnumberinfo import is_valid_short_number_for_region as is_valid_short_number_for_region +from .shortnumberinfo import ShortNumberCost as ShortNumberCost +from .shortnumberinfo import SUPPORTED_SHORT_REGIONS as SUPPORTED_SHORT_REGIONS + +__version__: str +__all__: list[str] diff --git a/python/phonenumbers/asyoutypeformatter.pyi b/python/phonenumbers/asyoutypeformatter.pyi new file mode 100644 index 000000000..138e9f317 --- /dev/null +++ b/python/phonenumbers/asyoutypeformatter.pyi @@ -0,0 +1,59 @@ +from re import Pattern + +from .phonemetadata import NumberFormat +from .phonemetadata import PhoneMetadata + +_SEPARATOR_BEFORE_NATIONAL_NUMBER: str +_EMPTY_METADATA: PhoneMetadata +_NATIONAL_PREFIX_SEPARATORS_PATTERN: Pattern[str] +_ELIGIBLE_FORMAT_PATTERN: Pattern[str] +_MIN_LEADING_DIGITS_LENGTH: int +_DIGIT_PLACEHOLDER: str +_DIGIT_PATTERN: Pattern[str] + +def _get_metadata_for_region(region_code: str) -> PhoneMetadata: ... + +class AsYouTypeFormatter: + _default_country: str + _current_metadata: PhoneMetadata + _default_metadata: PhoneMetadata + _current_output: str + _accrued_input: str + _accrued_input_without_formatting: str + _formatting_template: str + _last_match_position: int + _current_formatting_pattern: str + _prefix_before_national_number: str + _should_add_space_after_national_prefix: bool + _extracted_national_prefix: str + _national_number: str + _able_to_format: bool + _input_has_formatting: bool + _position_to_remember: int + _original_position: int + _is_complete_number: bool + _is_expecting_country_calling_code: bool + _possible_formats: list[NumberFormat] + def __init__(self, region_code: str) -> None: ... + def _maybe_create_new_template(self) -> bool: ... + def _get_available_formats(self, leading_digits: str) -> None: ... + def _narrow_down_possible_formats(self, leading_digits: str) -> None: ... + def _create_formatting_template(self, num_format: NumberFormat) -> bool: ... + def _get_formatting_template(self, number_pattern: str, number_format: str) -> str: ... + def _clear(self) -> None: ... + def clear(self) -> None: ... + def input_digit(self, next_char: str, remember_position: bool = ...) -> str: ... + def _attempt_to_choose_pattern_with_prefix_extracted(self) -> str: ... + def _able_to_extract_longer_ndd(self) -> bool: ... + def _is_digit_or_leading_plus_sign(self, next_char: str) -> bool: ... + def _attempt_to_format_accrued_digits(self) -> str: ... + def get_remembered_position(self) -> int: ... + def _append_national_number(self, national_number: str) -> str: ... + def _attempt_to_choose_formatting_pattern(self) -> str: ... + def _input_accrued_national_number(self) -> str: ... + def _is_nanpa_number_with_national_prefix(self) -> bool: ... + def _remove_national_prefix_from_national_number(self) -> str: ... + def _attempt_to_extract_idd(self) -> bool: ... + def _attempt_to_extract_ccc(self) -> bool: ... + def _normalize_and_accrue_digits_and_plus_sign(self, next_char: str, remember_position: bool) -> str: ... + def _input_digit_helper(self, next_char: str) -> str: ... diff --git a/python/phonenumbers/carrier.pyi b/python/phonenumbers/carrier.pyi new file mode 100644 index 000000000..fc2947b74 --- /dev/null +++ b/python/phonenumbers/carrier.pyi @@ -0,0 +1,8 @@ +from .phonenumber import PhoneNumber + +__all__: list[str] + +def name_for_valid_number(numobj: PhoneNumber, lang: str, script: str | None = ..., region: str | None = ...) -> str: ... +def name_for_number(numobj: PhoneNumber, lang: str, script: str | None = ..., region: str | None = ...) -> str: ... +def safe_display_name(numobj: PhoneNumber, lang: str, script: str | None = ..., region: str | None = ...) -> str: ... +def _is_mobile(ntype: int) -> bool: ... diff --git a/python/phonenumbers/carrierdata/__init__.pyi b/python/phonenumbers/carrierdata/__init__.pyi new file mode 100644 index 000000000..e47cb065e --- /dev/null +++ b/python/phonenumbers/carrierdata/__init__.pyi @@ -0,0 +1,2 @@ +CARRIER_DATA: dict[str, dict[str, str]] +CARRIER_LONGEST_PREFIX: int diff --git a/python/phonenumbers/data/__init__.pyi b/python/phonenumbers/data/__init__.pyi new file mode 100644 index 000000000..0f69d5c65 --- /dev/null +++ b/python/phonenumbers/data/__init__.pyi @@ -0,0 +1,9 @@ +from ..phonemetadata import NumberFormat + +_AVAILABLE_REGION_CODES: list[str] +_AVAILABLE_NONGEO_COUNTRY_CODES: list[int] + +def _load_region(code: str | int) -> None: ... + +_ALT_NUMBER_FORMATS: dict[int, list[NumberFormat]] +_COUNTRY_CODE_TO_REGION_CODE: dict[int, tuple[str, ...]] diff --git a/python/phonenumbers/geocoder.pyi b/python/phonenumbers/geocoder.pyi new file mode 100644 index 000000000..949131be2 --- /dev/null +++ b/python/phonenumbers/geocoder.pyi @@ -0,0 +1,8 @@ +from .phonenumber import PhoneNumber + +__all__: list[str] + +def country_name_for_number(numobj: PhoneNumber, lang: str, script: str | None = ..., region: str | None = ...) -> str: ... +def _region_display_name(region_code: str, lang: str, script: str | None = ..., region: str | None = ...) -> str: ... +def description_for_valid_number(numobj: PhoneNumber, lang: str, script: str | None = ..., region: str | None = ...) -> str: ... +def description_for_number(numobj: PhoneNumber, lang: str, script: str | None = ..., region: str | None = ...) -> str: ... diff --git a/python/phonenumbers/geodata/__init__.pyi b/python/phonenumbers/geodata/__init__.pyi new file mode 100644 index 000000000..0ddf83d19 --- /dev/null +++ b/python/phonenumbers/geodata/__init__.pyi @@ -0,0 +1,2 @@ +GEOCODE_DATA: dict[str, dict[str, str]] +GEOCODE_LONGEST_PREFIX: int diff --git a/python/phonenumbers/geodata/locale.pyi b/python/phonenumbers/geodata/locale.pyi new file mode 100644 index 000000000..11391e542 --- /dev/null +++ b/python/phonenumbers/geodata/locale.pyi @@ -0,0 +1 @@ +LOCALE_DATA: dict[str, dict[str, str]] diff --git a/python/phonenumbers/phonemetadata.pyi b/python/phonenumbers/phonemetadata.pyi new file mode 100644 index 000000000..faf60aef0 --- /dev/null +++ b/python/phonenumbers/phonemetadata.pyi @@ -0,0 +1,147 @@ +from collections.abc import Callable +import threading + +from .util import ImmutableMixin +from .util import UnicodeMixin + +REGION_CODE_FOR_NON_GEO_ENTITY: str + +class NumberFormat(UnicodeMixin, ImmutableMixin): + pattern: str | None + format: str | None + leading_digits_pattern: list[str] + national_prefix_formatting_rule: str | None + national_prefix_optional_when_formatting: bool | None + domestic_carrier_code_formatting_rule: str | None + def __init__( + self, + pattern: str | None = ..., + format: str | None = ..., + leading_digits_pattern: list[str] | None = ..., + national_prefix_formatting_rule: str | None = ..., + national_prefix_optional_when_formatting: bool | None = ..., + domestic_carrier_code_formatting_rule: str | None = ..., + ) -> None: ... + def merge_from(self, other: NumberFormat) -> None: ... + def __eq__(self, other: object) -> bool: ... + def __ne__(self, other: object) -> bool: ... + def __repr__(self) -> str: ... + def __unicode__(self) -> str: ... + +class PhoneNumberDesc(UnicodeMixin, ImmutableMixin): + national_number_pattern: str | None + example_number: str | None + possible_length: tuple[int, ...] + possible_length_local_only: tuple[int, ...] + def __init__( + self, + national_number_pattern: str | None = ..., + example_number: str | None = ..., + possible_length: tuple[int, ...] | None = ..., + possible_length_local_only: tuple[int, ...] | None = ..., + ) -> None: ... + def merge_from(self, other: PhoneNumberDesc) -> None: ... + def __eq__(self, other: object) -> bool: ... + def __ne__(self, other: object) -> bool: ... + def __repr__(self) -> str: ... + def __unicode__(self) -> str: ... + +def _same_pattern(left: PhoneNumberDesc | None, right: PhoneNumberDesc | None) -> bool: ... + +class PhoneMetadata(UnicodeMixin, ImmutableMixin): + _metadata_lock: threading.Lock + _region_available: dict[str, Callable[[str], None] | None] + _short_region_available: dict[str, Callable[[str], None] | None] + _country_code_available: dict[int, Callable[[int], None] | None] + _region_metadata: dict[str, PhoneMetadata] + _short_region_metadata: dict[str, PhoneMetadata] + _country_code_metadata: dict[int, PhoneMetadata] + general_desc: PhoneNumberDesc | None + fixed_line: PhoneNumberDesc | None + mobile: PhoneNumberDesc | None + toll_free: PhoneNumberDesc | None + premium_rate: PhoneNumberDesc | None + shared_cost: PhoneNumberDesc | None + personal_number: PhoneNumberDesc | None + voip: PhoneNumberDesc | None + pager: PhoneNumberDesc | None + uan: PhoneNumberDesc | None + emergency: PhoneNumberDesc | None + voicemail: PhoneNumberDesc | None + short_code: PhoneNumberDesc | None + standard_rate: PhoneNumberDesc | None + carrier_specific: PhoneNumberDesc | None + sms_services: PhoneNumberDesc | None + no_international_dialling: PhoneNumberDesc | None + id: str + country_code: int | None + international_prefix: str | None + preferred_international_prefix: str | None + national_prefix: str | None + preferred_extn_prefix: str | None + national_prefix_for_parsing: str | None + national_prefix_transform_rule: str | None + same_mobile_and_fixed_line_pattern: bool + number_format: list[NumberFormat] + intl_number_format: list[NumberFormat] + main_country_for_code: bool + leading_digits: str | None + leading_zero_possible: bool + mobile_number_portable_region: bool + short_data: bool + @classmethod + def metadata_for_region(cls, region_code: str, default: PhoneMetadata | None = ...) -> PhoneMetadata | None: ... + @classmethod + def short_metadata_for_region(cls, region_code: str, default: PhoneMetadata | None = ...) -> PhoneMetadata | None: ... + @classmethod + def metadata_for_nongeo_region(cls, country_code: int, default: PhoneMetadata | None = ...) -> PhoneMetadata | None: ... + @classmethod + def metadata_for_region_or_calling_code(cls, country_calling_code: int, region_code: str) -> PhoneMetadata | None: ... + @classmethod + def register_region_loader(cls, region_code: str, loader: Callable[[str], None]) -> None: ... + @classmethod + def register_short_region_loader(cls, region_code: str, loader: Callable[[str], None]) -> None: ... + @classmethod + def register_nongeo_region_loader(cls, country_code: int, loader: Callable[[int], None]) -> None: ... + @classmethod + def load_all(cls) -> None: ... + def __init__( + self, + id: str, + general_desc: PhoneNumberDesc | None = ..., + fixed_line: PhoneNumberDesc | None = ..., + mobile: PhoneNumberDesc | None = ..., + toll_free: PhoneNumberDesc | None = ..., + premium_rate: PhoneNumberDesc | None = ..., + shared_cost: PhoneNumberDesc | None = ..., + personal_number: PhoneNumberDesc | None = ..., + voip: PhoneNumberDesc | None = ..., + pager: PhoneNumberDesc | None = ..., + uan: PhoneNumberDesc | None = ..., + emergency: PhoneNumberDesc | None = ..., + voicemail: PhoneNumberDesc | None = ..., + short_code: PhoneNumberDesc | None = ..., + standard_rate: PhoneNumberDesc | None = ..., + carrier_specific: PhoneNumberDesc | None = ..., + sms_services: PhoneNumberDesc | None = ..., + no_international_dialling: PhoneNumberDesc | None = ..., + country_code: int | None = ..., + international_prefix: str | None = ..., + preferred_international_prefix: str | None = ..., + national_prefix: str | None = ..., + preferred_extn_prefix: str | None = ..., + national_prefix_for_parsing: str | None = ..., + national_prefix_transform_rule: str | None = ..., + number_format: list[NumberFormat] | None = ..., + intl_number_format: list[NumberFormat] | None = ..., + main_country_for_code: bool = ..., + leading_digits: str | None = ..., + leading_zero_possible: bool = ..., + mobile_number_portable_region: bool = ..., + short_data: bool = ..., + register: bool = ..., + ) -> None: ... + def __eq__(self, other: object) -> bool: ... + def __ne__(self, other: object) -> bool: ... + def __repr__(self) -> str: ... + def __unicode__(self) -> str: ... diff --git a/python/phonenumbers/phonenumber.pyi b/python/phonenumbers/phonenumber.pyi new file mode 100644 index 000000000..668ec6396 --- /dev/null +++ b/python/phonenumbers/phonenumber.pyi @@ -0,0 +1,55 @@ +from typing import overload + +from .util import ImmutableMixin +from .util import UnicodeMixin + +class CountryCodeSource: + UNSPECIFIED: int + FROM_NUMBER_WITH_PLUS_SIGN: int + FROM_NUMBER_WITH_IDD: int + FROM_NUMBER_WITHOUT_PLUS_SIGN: int + FROM_DEFAULT_COUNTRY: int + +class PhoneNumber(UnicodeMixin): + country_code: int | None + national_number: int | None + extension: str | None + italian_leading_zero: bool | None + number_of_leading_zeros: int | None + raw_input: str | None + country_code_source: int + preferred_domestic_carrier_code: str | None + def __init__( + self, + country_code: int | None = ..., + national_number: int | None = ..., + extension: str | None = ..., + italian_leading_zero: bool | None = ..., + number_of_leading_zeros: int | None = ..., + raw_input: str | None = ..., + country_code_source: int = ..., + preferred_domestic_carrier_code: str | None = ..., + ) -> None: ... + def clear(self) -> None: ... + def merge_from(self, other: PhoneNumber) -> None: ... + def __eq__(self, other: object) -> bool: ... + def __ne__(self, other: object) -> bool: ... + def __repr__(self) -> str: ... + def __unicode__(self) -> str: ... + +class FrozenPhoneNumber(PhoneNumber, ImmutableMixin): + @overload + def __init__(self, numobj: PhoneNumber) -> None: ... + @overload + def __init__( + self, + country_code: int | None = ..., + national_number: int | None = ..., + extension: str | None = ..., + italian_leading_zero: bool | None = ..., + number_of_leading_zeros: int | None = ..., + raw_input: str | None = ..., + country_code_source: int = ..., + preferred_domestic_carrier_code: str | None = ..., + ) -> None: ... + def __hash__(self) -> int: ... diff --git a/python/phonenumbers/phonenumbermatcher.pyi b/python/phonenumbers/phonenumbermatcher.pyi new file mode 100644 index 000000000..2eb8e4a01 --- /dev/null +++ b/python/phonenumbers/phonenumbermatcher.pyi @@ -0,0 +1,84 @@ +from collections.abc import Callable +from collections.abc import Iterator +from re import Pattern + +from .phonemetadata import NumberFormat +from .phonenumber import PhoneNumber +from .util import UnicodeMixin + +def _limit(lower: int, upper: int) -> str: ... + +_OPENING_PARENS: str +_CLOSING_PARENS: str +_NON_PARENS: str +_BRACKET_PAIR_LIMIT: str +_MATCHING_BRACKETS: Pattern[str] +_LEAD_LIMIT: str +_PUNCTUATION_LIMIT: str +_DIGIT_BLOCK_LIMIT: int +_BLOCK_LIMIT: str +_PUNCTUATION: str +_DIGIT_SEQUENCE: str +_LEAD_CLASS_CHARS: str +_LEAD_CLASS: str +_LEAD_PATTERN: Pattern[str] +_PATTERN: Pattern[str] +_PUB_PAGES: Pattern[str] +_SLASH_SEPARATED_DATES: Pattern[str] +_TIME_STAMPS: Pattern[str] +_TIME_STAMPS_SUFFIX: Pattern[str] +_INNER_MATCHES: tuple[Pattern[str], ...] + +class Leniency: + POSSIBLE: int + VALID: int + STRICT_GROUPING: int + EXACT_GROUPING: int + +def _verify(leniency: int, numobj: PhoneNumber, candidate: str, matcher: PhoneNumberMatcher) -> bool: ... +def _verify_strict_grouping(numobj: PhoneNumber, candidate: str, matcher: PhoneNumberMatcher) -> bool: ... +def _all_number_groups_remain_grouped(numobj: PhoneNumber, normalized_candidate: str, formatted_number_groups: list[str]) -> bool: ... +def _verify_exact_grouping(numobj: PhoneNumber, candidate: str, matcher: PhoneNumberMatcher) -> bool: ... +def _all_number_groups_are_exactly_present(numobj: PhoneNumber, normalized_candidate: str, formatted_number_groups: list[str]) -> bool: ... +def _get_national_number_groups_without_pattern(numobj: PhoneNumber) -> list[str]: ... +def _get_national_number_groups(numobj: PhoneNumber, formatting_pattern: NumberFormat) -> list[str]: ... +def _contains_more_than_one_slash_in_national_number(numobj: PhoneNumber, candidate: str) -> bool: ... +def _contains_only_valid_x_chars(numobj: PhoneNumber, candidate: str) -> bool: ... +def _is_national_prefix_present_if_required(numobj: PhoneNumber) -> bool: ... + +class PhoneNumberMatcher: + _NOT_READY: int + _READY: int + _DONE: int + text: str + preferred_region: str | None + leniency: int + _max_tries: int + _state: int + _last_match: PhoneNumberMatch | None + _search_index: int + def __init__(self, text: str | None, region: str | None, leniency: int = ..., max_tries: int = ...) -> None: ... + def _find(self, index: int) -> PhoneNumberMatch | None: ... + def _trim_after_first_match(self, pattern: Pattern[str], candidate: str) -> str: ... + @classmethod + def _is_latin_letter(cls, letter: str) -> bool: ... + @classmethod + def _is_invalid_punctuation_symbol(cls, character: str) -> bool: ... + def _extract_match(self, candidate: str, offset: int) -> PhoneNumberMatch | None: ... + def _extract_inner_match(self, candidate: str, offset: int) -> PhoneNumberMatch | None: ... + def _parse_and_verify(self, candidate: str, offset: int) -> PhoneNumberMatch | None: ... + def _check_number_grouping_is_valid(self, numobj: PhoneNumber, candidate: str, checker: Callable[[PhoneNumber, str, list[str]], bool]) -> bool: ... + def has_next(self) -> bool: ... + def next(self) -> PhoneNumberMatch: ... + def __iter__(self) -> Iterator[PhoneNumberMatch]: ... + +class PhoneNumberMatch(UnicodeMixin): + start: int + raw_string: str + end: int + number: PhoneNumber + def __init__(self, start: int, raw_string: str, numobj: PhoneNumber) -> None: ... + def __eq__(self, other: object) -> bool: ... + def __ne__(self, other: object) -> bool: ... + def __repr__(self) -> str: ... + def __unicode__(self) -> str: ... diff --git a/python/phonenumbers/phonenumberutil.pyi b/python/phonenumbers/phonenumberutil.pyi new file mode 100644 index 000000000..ced1196ba --- /dev/null +++ b/python/phonenumbers/phonenumberutil.pyi @@ -0,0 +1,205 @@ +from re import Pattern +from re import RegexFlag + +from .phonemetadata import NumberFormat +from .phonemetadata import PhoneMetadata +from .phonemetadata import PhoneNumberDesc +from .phonenumber import PhoneNumber +from .util import UnicodeMixin + +COUNTRY_CODE_TO_REGION_CODE: dict[int, tuple[str, ...]] +_REGEX_FLAGS: RegexFlag +_MIN_LENGTH_FOR_NSN: int +_MAX_LENGTH_FOR_NSN: int +_MAX_LENGTH_COUNTRY_CODE: int +_MAX_INPUT_STRING_LENGTH: int +UNKNOWN_REGION: str +_NANPA_COUNTRY_CODE: int +_COLOMBIA_MOBILE_TO_FIXED_LINE_PREFIX: str +_MOBILE_TOKEN_MAPPINGS: dict[int, str] +_GEO_MOBILE_COUNTRIES_WITHOUT_MOBILE_AREA_CODES: frozenset[int] +_GEO_MOBILE_COUNTRIES: frozenset[int] +_PLUS_SIGN: str +_STAR_SIGN: str +_RFC3966_EXTN_PREFIX: str +_RFC3966_PREFIX: str +_RFC3966_PHONE_CONTEXT: str +_RFC3966_ISDN_SUBADDRESS: str +_ASCII_DIGITS_MAP: dict[str, str] +_ALPHA_MAPPINGS: dict[str, str] +_ALPHA_PHONE_MAPPINGS: dict[str, str] +_DIALLABLE_CHAR_MAPPINGS: dict[str, str] +_ALL_PLUS_NUMBER_GROUPING_SYMBOLS: dict[str, str] +_SINGLE_INTERNATIONAL_PREFIX: Pattern[str] +_VALID_PUNCTUATION: str +_DIGITS: str +_VALID_ALPHA: str +_PLUS_CHARS: str +_PLUS_CHARS_PATTERN: Pattern[str] +_SEPARATOR_PATTERN: Pattern[str] +_CAPTURING_DIGIT_PATTERN: Pattern[str] +_VALID_START_CHAR: str +_VALID_START_CHAR_PATTERN: Pattern[str] +_SECOND_NUMBER_START: str +_SECOND_NUMBER_START_PATTERN: Pattern[str] +_UNWANTED_END_CHARS: str +_UNWANTED_END_CHAR_PATTERN: Pattern[str] +_VALID_ALPHA_PHONE_PATTERN: Pattern[str] +_VALID_PHONE_NUMBER: str +_DEFAULT_EXTN_PREFIX: str + +def _extn_digits(max_length: int) -> str: ... +def _create_extn_pattern(for_parsing: bool) -> str: ... + +_EXTN_PATTERNS_FOR_PARSING: str +_EXTN_PATTERNS_FOR_MATCHING: str +_EXTN_PATTERN: Pattern[str] +_VALID_PHONE_NUMBER_PATTERN: Pattern[str] +NON_DIGITS_PATTERN: Pattern[str] +_FIRST_GROUP_PATTERN: Pattern[str] +_NP_STRING: str +_FG_STRING: str +_CC_STRING: str +_FIRST_GROUP_ONLY_PREFIX_PATTERN: Pattern[str] + +class PhoneNumberFormat: + E164: int + INTERNATIONAL: int + NATIONAL: int + RFC3966: int + +class PhoneNumberType: + FIXED_LINE: int + MOBILE: int + FIXED_LINE_OR_MOBILE: int + TOLL_FREE: int + PREMIUM_RATE: int + SHARED_COST: int + VOIP: int + PERSONAL_NUMBER: int + PAGER: int + UAN: int + VOICEMAIL: int + UNKNOWN: int + @classmethod + def values(cls) -> tuple[int, ...]: ... + +class MatchType: + NOT_A_NUMBER: int + NO_MATCH: int + SHORT_NSN_MATCH: int + NSN_MATCH: int + EXACT_MATCH: int + +class ValidationResult: + IS_POSSIBLE: int + IS_POSSIBLE_LOCAL_ONLY: int + INVALID_COUNTRY_CODE: int + TOO_SHORT: int + INVALID_LENGTH: int + TOO_LONG: int + +SUPPORTED_REGIONS: set[str] +COUNTRY_CODES_FOR_NON_GEO_REGIONS: set[int] +_NANPA_REGIONS: set[str] + +def _regenerate_derived_data() -> None: ... +def _copy_number_format(other: NumberFormat) -> NumberFormat: ... +def _extract_possible_number(number: str) -> str: ... +def _is_viable_phone_number(number: str) -> bool: ... +def _normalize(number: str) -> str: ... +def normalize_digits_only(number: str, keep_non_digits: bool = ...) -> str: ... +def normalize_diallable_chars_only(number: str) -> str: ... +def convert_alpha_characters_in_number(number: str) -> str: ... +def length_of_geographical_area_code(numobj: PhoneNumber) -> int: ... +def length_of_national_destination_code(numobj: PhoneNumber) -> int: ... +def country_mobile_token(country_code: int) -> str: ... +def _normalize_helper(number: str, replacements: dict[str, str], remove_non_matches: bool) -> str: ... +def supported_calling_codes() -> set[int]: ... +def _desc_has_possible_number_data(desc: PhoneNumberDesc | None) -> bool: ... +def _desc_has_data(desc: PhoneNumberDesc | None) -> bool: ... +def _supported_types_for_metadata(metadata: PhoneMetadata) -> set[int]: ... +def supported_types_for_region(region_code: str) -> set[int]: ... +def supported_types_for_non_geo_entity(country_code: int) -> set[int]: ... +def _formatting_rule_has_first_group_only(national_prefix_formatting_rule: str | None) -> bool: ... +def is_number_geographical(numobj: PhoneNumber) -> bool: ... +def is_number_type_geographical(num_type: int, country_code: int) -> bool: ... +def _is_valid_region_code(region_code: str | None) -> bool: ... +def _has_valid_country_calling_code(country_calling_code: int) -> bool: ... +def format_number(numobj: PhoneNumber, num_format: int) -> str: ... +def format_by_pattern(numobj: PhoneNumber, number_format: int, user_defined_formats: list[NumberFormat]) -> str: ... +def format_national_number_with_carrier_code(numobj: PhoneNumber, carrier_code: str) -> str: ... +def format_national_number_with_preferred_carrier_code(numobj: PhoneNumber, fallback_carrier_code: str) -> str: ... +def format_number_for_mobile_dialing(numobj: PhoneNumber, region_calling_from: str, with_formatting: bool) -> str: ... +def format_out_of_country_calling_number(numobj: PhoneNumber, region_calling_from: str) -> str: ... +def format_in_original_format(numobj: PhoneNumber, region_calling_from: str) -> str: ... +def _format_original_allow_mods(numobj: PhoneNumber, region_calling_from: str) -> str: ... +def _raw_input_contains_national_prefix(raw_input: str, national_prefix: str, region_code: str) -> bool: ... +def _has_formatting_pattern_for_number(numobj: PhoneNumber) -> bool: ... +def format_out_of_country_keeping_alpha_chars(numobj: PhoneNumber, region_calling_from: str) -> str: ... +def national_significant_number(numobj: PhoneNumber) -> str: ... +def _prefix_number_with_country_calling_code(country_code: int, num_format: int, formatted_number: str) -> str: ... +def _format_nsn(number: str, metadata: PhoneMetadata, num_format: int, carrier_code: str | None = ...) -> str: ... +def _choose_formatting_pattern_for_number(available_formats: list[NumberFormat], national_number: str) -> NumberFormat | None: ... +def _format_nsn_using_pattern(national_number: str, formatting_pattern: NumberFormat, number_format: int, carrier_code: str | None = ...) -> str: ... +def example_number(region_code: str) -> PhoneNumber | None: ... +def invalid_example_number(region_code: str) -> PhoneNumber | None: ... +def example_number_for_type(region_code: str | None, num_type: int) -> PhoneNumber | None: ... +def _example_number_anywhere_for_type(num_type: int) -> PhoneNumber | None: ... +def example_number_for_non_geo_entity(country_calling_code: int) -> PhoneNumber | None: ... +def _maybe_append_formatted_extension(numobj: PhoneNumber, metadata: PhoneMetadata, num_format: int, number: str) -> str: ... +def _number_desc_by_type(metadata: PhoneMetadata, num_type: int) -> PhoneNumberDesc | None: ... +def number_type(numobj: PhoneNumber) -> int: ... +def _number_type_helper(national_number: str, metadata: PhoneMetadata) -> int: ... +def _is_number_matching_desc(national_number: str, number_desc: PhoneNumberDesc | None) -> bool: ... +def is_valid_number(numobj: PhoneNumber) -> bool: ... +def is_valid_number_for_region(numobj: PhoneNumber, region_code: str) -> bool: ... +def region_code_for_number(numobj: PhoneNumber) -> str | None: ... +def _region_code_for_number_from_list(numobj: PhoneNumber, regions: tuple[str, ...]) -> str | None: ... +def region_code_for_country_code(country_code: int) -> str: ... +def region_codes_for_country_code(country_code: int) -> tuple[str, ...]: ... +def country_code_for_region(region_code: str) -> int: ... +def country_code_for_valid_region(region_code: str) -> int: ... +def ndd_prefix_for_region(region_code: str, strip_non_digits: bool) -> str | None: ... +def is_nanpa_country(region_code: str) -> bool: ... +def is_alpha_number(number: str) -> bool: ... +def is_possible_number(numobj: PhoneNumber) -> bool: ... +def is_possible_number_for_type(numobj: PhoneNumber, numtype: int) -> bool: ... +def _test_number_length(national_number: str, metadata: PhoneMetadata, numtype: int = ...) -> int: ... +def is_possible_number_with_reason(numobj: PhoneNumber) -> int: ... +def is_possible_number_for_type_with_reason(numobj: PhoneNumber, numtype: int) -> int: ... +def is_possible_number_string(number: str, region_dialing_from: str) -> bool: ... +def truncate_too_long_number(numobj: PhoneNumber) -> bool: ... +def _extract_country_code(number: str) -> tuple[int, str]: ... +def _maybe_extract_country_code(number: str, metadata: PhoneMetadata, keep_raw_input: bool, numobj: PhoneNumber) -> tuple[int, str]: ... +def _parse_prefix_as_idd(idd_pattern: Pattern[str], number: str) -> tuple[bool, str]: ... +def _maybe_strip_i18n_prefix_and_normalize(number: str, possible_idd_prefix: str) -> tuple[int, str]: ... +def _maybe_strip_national_prefix_carrier_code(number: str, metadata: PhoneMetadata) -> tuple[str, str, bool]: ... +def _maybe_strip_extension(number: str) -> tuple[str, str]: ... +def _check_region_for_parsing(number: str | None, default_region: str | None) -> bool: ... +def _set_italian_leading_zeros_for_phone_number(national_number: str, numobj: PhoneNumber) -> None: ... +def parse(number: str, region: str | None = ..., keep_raw_input: bool = ..., numobj: PhoneNumber | None = ..., _check_region: bool = ...) -> PhoneNumber: ... +def _build_national_number_for_parsing(number: str) -> str: ... +def _copy_core_fields_only(inobj: PhoneNumber) -> PhoneNumber: ... +def _is_number_match_OO(numobj1_in: PhoneNumber, numobj2_in: PhoneNumber) -> int: ... +def _is_national_number_suffix_of_other(numobj1: PhoneNumber, numobj2: PhoneNumber) -> bool: ... +def _is_number_match_SS(number1: str, number2: str) -> int: ... +def _is_number_match_OS(numobj1: PhoneNumber, number2: str) -> int: ... +def is_number_match(num1: PhoneNumber | str, num2: PhoneNumber | str) -> int: ... +def can_be_internationally_dialled(numobj: PhoneNumber) -> bool: ... +def is_mobile_number_portable_region(region_code: str) -> bool: ... + +class NumberParseException(UnicodeMixin, Exception): + INVALID_COUNTRY_CODE: int + NOT_A_NUMBER: int + TOO_SHORT_AFTER_IDD: int + TOO_SHORT_NSN: int + TOO_LONG: int + error_type: int + _msg: str + def __init__(self, error_type: int, msg: str) -> None: ... + def __reduce__(self) -> tuple[type[NumberParseException], tuple[int, str]]: ... + def __unicode__(self) -> str: ... + +def _match_national_number(number: str, number_desc: PhoneNumberDesc | None, allow_prefix_match: bool) -> bool: ... +def _match(number: str, pattern: Pattern[str], allow_prefix_match: bool) -> bool: ... diff --git a/python/phonenumbers/prefix.pyi b/python/phonenumbers/prefix.pyi new file mode 100644 index 000000000..85c791e5f --- /dev/null +++ b/python/phonenumbers/prefix.pyi @@ -0,0 +1,15 @@ +from .phonenumber import PhoneNumber + +_LOCALE_NORMALIZATION_MAP: dict[str, str] + +def _may_fall_back_to_english(lang: str) -> bool: ... +def _full_locale(lang: str, script: str | None, region: str | None) -> str: ... +def _find_lang(langdict: dict[str, str], lang: str, script: str | None, region: str | None) -> str | None: ... +def _prefix_description_for_number( + data: dict[str, dict[str, str]], + longest_prefix: int, + numobj: PhoneNumber, + lang: str, + script: str | None = ..., + region: str | None = ..., +) -> str: ... diff --git a/python/phonenumbers/py.typed b/python/phonenumbers/py.typed new file mode 100644 index 000000000..e69de29bb diff --git a/python/phonenumbers/re_util.pyi b/python/phonenumbers/re_util.pyi new file mode 100644 index 000000000..3f4efaa6d --- /dev/null +++ b/python/phonenumbers/re_util.pyi @@ -0,0 +1,4 @@ +from re import Match +from re import Pattern + +def fullmatch(pattern: Pattern[str], string: str, flags: int = ...) -> Match[str] | None: ... diff --git a/python/phonenumbers/shortdata/__init__.pyi b/python/phonenumbers/shortdata/__init__.pyi new file mode 100644 index 000000000..164b9f8a1 --- /dev/null +++ b/python/phonenumbers/shortdata/__init__.pyi @@ -0,0 +1,3 @@ +_AVAILABLE_REGION_CODES: list[str] + +def _load_region(code: str) -> None: ... diff --git a/python/phonenumbers/shortnumberinfo.pyi b/python/phonenumbers/shortnumberinfo.pyi new file mode 100644 index 000000000..f3e66e99c --- /dev/null +++ b/python/phonenumbers/shortnumberinfo.pyi @@ -0,0 +1,29 @@ +from .phonemetadata import PhoneNumberDesc +from .phonenumber import PhoneNumber + +SUPPORTED_SHORT_REGIONS: list[str] +_REGIONS_WHERE_EMERGENCY_NUMBERS_MUST_BE_EXACT: set[str] + +class ShortNumberCost: + TOLL_FREE: int + STANDARD_RATE: int + PREMIUM_RATE: int + UNKNOWN_COST: int + +def _region_dialing_from_matches_number(numobj: PhoneNumber, region_dialing_from: str) -> bool: ... +def is_possible_short_number_for_region(short_numobj: PhoneNumber, region_dialing_from: str) -> bool: ... +def is_possible_short_number(numobj: PhoneNumber) -> bool: ... +def is_valid_short_number_for_region(short_numobj: PhoneNumber, region_dialing_from: str) -> bool: ... +def is_valid_short_number(numobj: PhoneNumber) -> bool: ... +def expected_cost_for_region(short_numobj: PhoneNumber, region_dialing_from: str) -> int: ... +def expected_cost(numobj: PhoneNumber) -> int: ... +def _region_code_for_short_number_from_region_list(numobj: PhoneNumber, region_codes: tuple[str, ...]) -> str | None: ... +def _example_short_number(region_code: str) -> str: ... +def _example_short_number_for_cost(region_code: str, cost: int) -> str: ... +def connects_to_emergency_number(number: str, region_code: str) -> bool: ... +def is_emergency_number(number: str, region_code: str) -> bool: ... +def _matches_emergency_number_helper(number: str, region_code: str, allow_prefix_match: bool) -> bool: ... +def is_carrier_specific(numobj: PhoneNumber) -> bool: ... +def is_carrier_specific_for_region(numobj: PhoneNumber, region_dialing_from: str) -> bool: ... +def is_sms_service_for_region(numobj: PhoneNumber, region_dialing_from: str) -> bool: ... +def _matches_possible_number_and_national_number(number: str, number_desc: PhoneNumberDesc | None) -> bool: ... diff --git a/python/phonenumbers/timezone.pyi b/python/phonenumbers/timezone.pyi new file mode 100644 index 000000000..675873abe --- /dev/null +++ b/python/phonenumbers/timezone.pyi @@ -0,0 +1,9 @@ +from .phonenumber import PhoneNumber + +__all__: list[str] +UNKNOWN_TIMEZONE: str +_UNKNOWN_TIME_ZONE_LIST: tuple[str, ...] + +def time_zones_for_geographical_number(numobj: PhoneNumber) -> tuple[str, ...]: ... +def time_zones_for_number(numobj: PhoneNumber) -> tuple[str, ...]: ... +def _country_level_time_zones_for_number(numobj: PhoneNumber) -> tuple[str, ...]: ... diff --git a/python/phonenumbers/tzdata/__init__.pyi b/python/phonenumbers/tzdata/__init__.pyi new file mode 100644 index 000000000..45d621a7a --- /dev/null +++ b/python/phonenumbers/tzdata/__init__.pyi @@ -0,0 +1,2 @@ +TIMEZONE_DATA: dict[str, tuple[str, ...]] +TIMEZONE_LONGEST_PREFIX: int diff --git a/python/phonenumbers/unicode_util.pyi b/python/phonenumbers/unicode_util.pyi new file mode 100644 index 000000000..60af698cf --- /dev/null +++ b/python/phonenumbers/unicode_util.pyi @@ -0,0 +1,271 @@ +from .util import UnicodeMixin + +class Category: + LETTER: str + UPPERCASE_LETTER: str + LOWERCASE_LETTER: str + TITLECASE_LETTER: str + MODIFIER_LETTER: str + OTHER_LETTER: str + MARK: str + NON_SPACING_MARK: str + SPACING_COMBINING_MARK: str + ENCLOSING_MARK: str + NUMBER: str + DECIMAL_DIGIT_NUMBER: str + LETTER_NUMBER: str + OTHER_NUMBER: str + SYMBOL: str + MATH_SYMBOL: str + CURRENCY_SYMBOL: str + MODIFIER_SYMBOL: str + OTHER_SYMBOL: str + PUNCTUATION: str + CONNECTOR_PUNCTUATION: str + DASH_PUNCTUATION: str + OPEN_PUNCTUATION: str + CLOSE_PUNCTUATION: str + INITIAL_PUNCTUATION: str + FINAL_PUNCTUATION: str + OTHER_PUNCTUATION: str + SEPARATOR: str + SPACE_SEPARATOR: str + LINE_SEPARATOR: str + PARAGRAPH_SEPARATOR: str + OTHER: str + CONTROL: str + FORMAT: str + SURROGATE: str + PRIVATE_USE: str + NOT_ASSIGNED: str + @classmethod + def get(cls, uni_char: str) -> str: ... + +def is_letter(uni_char: str) -> bool: ... + +class _BlockRange(UnicodeMixin): + start: int + end: int + def __init__(self, start: int, end: int, regdict: dict[int, _BlockRange] | None = ...) -> None: ... + def __eq__(self, other: object) -> bool: ... + def __ne__(self, other: object) -> bool: ... + def __hash__(self) -> int: ... + def __unicode__(self) -> str: ... + +class Block: + _RANGES: dict[int, _BlockRange] + _RANGE_KEYS: list[int] | None + BASIC_LATIN: _BlockRange + LATIN_1_SUPPLEMENT: _BlockRange + LATIN_EXTENDED_A: _BlockRange + LATIN_EXTENDED_B: _BlockRange + IPA_EXTENSIONS: _BlockRange + SPACING_MODIFIER_LETTERS: _BlockRange + COMBINING_DIACRITICAL_MARKS: _BlockRange + GREEK_AND_COPTIC: _BlockRange + CYRILLIC: _BlockRange + CYRILLIC_SUPPLEMENT: _BlockRange + ARMENIAN: _BlockRange + HEBREW: _BlockRange + ARABIC: _BlockRange + SYRIAC: _BlockRange + ARABIC_SUPPLEMENT: _BlockRange + THAANA: _BlockRange + NKO: _BlockRange + SAMARITAN: _BlockRange + MANDAIC: _BlockRange + DEVANAGARI: _BlockRange + BENGALI: _BlockRange + GURMUKHI: _BlockRange + GUJARATI: _BlockRange + ORIYA: _BlockRange + TAMIL: _BlockRange + TELUGU: _BlockRange + KANNADA: _BlockRange + MALAYALAM: _BlockRange + SINHALA: _BlockRange + THAI: _BlockRange + LAO: _BlockRange + TIBETAN: _BlockRange + MYANMAR: _BlockRange + GEORGIAN: _BlockRange + HANGUL_JAMO: _BlockRange + ETHIOPIC: _BlockRange + ETHIOPIC_SUPPLEMENT: _BlockRange + CHEROKEE: _BlockRange + UNIFIED_CANADIAN_ABORIGINAL_SYLLABICS: _BlockRange + OGHAM: _BlockRange + RUNIC: _BlockRange + TAGALOG: _BlockRange + HANUNOO: _BlockRange + BUHID: _BlockRange + TAGBANWA: _BlockRange + KHMER: _BlockRange + MONGOLIAN: _BlockRange + UNIFIED_CANADIAN_ABORIGINAL_SYLLABICS_EXTENDED: _BlockRange + LIMBU: _BlockRange + TAI_LE: _BlockRange + NEW_TAI_LUE: _BlockRange + KHMER_SYMBOLS: _BlockRange + BUGINESE: _BlockRange + TAI_THAM: _BlockRange + BALINESE: _BlockRange + SUNDANESE: _BlockRange + BATAK: _BlockRange + LEPCHA: _BlockRange + OL_CHIKI: _BlockRange + VEDIC_EXTENSIONS: _BlockRange + PHONETIC_EXTENSIONS: _BlockRange + PHONETIC_EXTENSIONS_SUPPLEMENT: _BlockRange + COMBINING_DIACRITICAL_MARKS_SUPPLEMENT: _BlockRange + LATIN_EXTENDED_ADDITIONAL: _BlockRange + GREEK_EXTENDED: _BlockRange + GENERAL_PUNCTUATION: _BlockRange + SUPERSCRIPTS_AND_SUBSCRIPTS: _BlockRange + CURRENCY_SYMBOLS: _BlockRange + COMBINING_DIACRITICAL_MARKS_FOR_SYMBOLS: _BlockRange + LETTERLIKE_SYMBOLS: _BlockRange + NUMBER_FORMS: _BlockRange + ARROWS: _BlockRange + MATHEMATICAL_OPERATORS: _BlockRange + MISCELLANEOUS_TECHNICAL: _BlockRange + CONTROL_PICTURES: _BlockRange + OPTICAL_CHARACTER_RECOGNITION: _BlockRange + ENCLOSED_ALPHANUMERICS: _BlockRange + BOX_DRAWING: _BlockRange + BLOCK_ELEMENTS: _BlockRange + GEOMETRIC_SHAPES: _BlockRange + MISCELLANEOUS_SYMBOLS: _BlockRange + DINGBATS: _BlockRange + MISCELLANEOUS_MATHEMATICAL_SYMBOLS_A: _BlockRange + SUPPLEMENTAL_ARROWS_A: _BlockRange + BRAILLE_PATTERNS: _BlockRange + SUPPLEMENTAL_ARROWS_B: _BlockRange + MISCELLANEOUS_MATHEMATICAL_SYMBOLS_B: _BlockRange + SUPPLEMENTAL_MATHEMATICAL_OPERATORS: _BlockRange + MISCELLANEOUS_SYMBOLS_AND_ARROWS: _BlockRange + GLAGOLITIC: _BlockRange + LATIN_EXTENDED_C: _BlockRange + COPTIC: _BlockRange + GEORGIAN_SUPPLEMENT: _BlockRange + TIFINAGH: _BlockRange + ETHIOPIC_EXTENDED: _BlockRange + CYRILLIC_EXTENDED_A: _BlockRange + SUPPLEMENTAL_PUNCTUATION: _BlockRange + CJK_RADICALS_SUPPLEMENT: _BlockRange + KANGXI_RADICALS: _BlockRange + IDEOGRAPHIC_DESCRIPTION_CHARACTERS: _BlockRange + CJK_SYMBOLS_AND_PUNCTUATION: _BlockRange + HIRAGANA: _BlockRange + KATAKANA: _BlockRange + BOPOMOFO: _BlockRange + HANGUL_COMPATIBILITY_JAMO: _BlockRange + KANBUN: _BlockRange + BOPOMOFO_EXTENDED: _BlockRange + CJK_STROKES: _BlockRange + KATAKANA_PHONETIC_EXTENSIONS: _BlockRange + ENCLOSED_CJK_LETTERS_AND_MONTHS: _BlockRange + CJK_COMPATIBILITY: _BlockRange + CJK_UNIFIED_IDEOGRAPHS_EXTENSION_A: _BlockRange + YIJING_HEXAGRAM_SYMBOLS: _BlockRange + CJK_UNIFIED_IDEOGRAPHS: _BlockRange + YI_SYLLABLES: _BlockRange + YI_RADICALS: _BlockRange + LISU: _BlockRange + VAI: _BlockRange + CYRILLIC_EXTENDED_B: _BlockRange + BAMUM: _BlockRange + MODIFIER_TONE_LETTERS: _BlockRange + LATIN_EXTENDED_D: _BlockRange + SYLOTI_NAGRI: _BlockRange + COMMON_INDIC_NUMBER_FORMS: _BlockRange + PHAGS_PA: _BlockRange + SAURASHTRA: _BlockRange + DEVANAGARI_EXTENDED: _BlockRange + KAYAH_LI: _BlockRange + REJANG: _BlockRange + HANGUL_JAMO_EXTENDED_A: _BlockRange + JAVANESE: _BlockRange + CHAM: _BlockRange + MYANMAR_EXTENDED_A: _BlockRange + TAI_VIET: _BlockRange + ETHIOPIC_EXTENDED_A: _BlockRange + MEETEI_MAYEK: _BlockRange + HANGUL_SYLLABLES: _BlockRange + HANGUL_JAMO_EXTENDED_B: _BlockRange + HIGH_SURROGATES: _BlockRange + HIGH_PRIVATE_USE_SURROGATES: _BlockRange + LOW_SURROGATES: _BlockRange + PRIVATE_USE_AREA: _BlockRange + CJK_COMPATIBILITY_IDEOGRAPHS: _BlockRange + ALPHABETIC_PRESENTATION_FORMS: _BlockRange + ARABIC_PRESENTATION_FORMS_A: _BlockRange + VARIATION_SELECTORS: _BlockRange + VERTICAL_FORMS: _BlockRange + COMBINING_HALF_MARKS: _BlockRange + CJK_COMPATIBILITY_FORMS: _BlockRange + SMALL_FORM_VARIANTS: _BlockRange + ARABIC_PRESENTATION_FORMS_B: _BlockRange + HALFWIDTH_AND_FULLWIDTH_FORMS: _BlockRange + SPECIALS: _BlockRange + LINEAR_B_SYLLABARY: _BlockRange + LINEAR_B_IDEOGRAMS: _BlockRange + AEGEAN_NUMBERS: _BlockRange + ANCIENT_GREEK_NUMBERS: _BlockRange + ANCIENT_SYMBOLS: _BlockRange + PHAISTOS_DISC: _BlockRange + LYCIAN: _BlockRange + CARIAN: _BlockRange + OLD_ITALIC: _BlockRange + GOTHIC: _BlockRange + UGARITIC: _BlockRange + OLD_PERSIAN: _BlockRange + DESERET: _BlockRange + SHAVIAN: _BlockRange + OSMANYA: _BlockRange + CYPRIOT_SYLLABARY: _BlockRange + IMPERIAL_ARAMAIC: _BlockRange + PHOENICIAN: _BlockRange + LYDIAN: _BlockRange + KHAROSHTHI: _BlockRange + OLD_SOUTH_ARABIAN: _BlockRange + AVESTAN: _BlockRange + INSCRIPTIONAL_PARTHIAN: _BlockRange + INSCRIPTIONAL_PAHLAVI: _BlockRange + OLD_TURKIC: _BlockRange + RUMI_NUMERAL_SYMBOLS: _BlockRange + BRAHMI: _BlockRange + KAITHI: _BlockRange + CUNEIFORM: _BlockRange + CUNEIFORM_NUMBERS_AND_PUNCTUATION: _BlockRange + EGYPTIAN_HIEROGLYPHS: _BlockRange + BAMUM_SUPPLEMENT: _BlockRange + KANA_SUPPLEMENT: _BlockRange + BYZANTINE_MUSICAL_SYMBOLS: _BlockRange + MUSICAL_SYMBOLS: _BlockRange + ANCIENT_GREEK_MUSICAL_NOTATION: _BlockRange + TAI_XUAN_JING_SYMBOLS: _BlockRange + COUNTING_ROD_NUMERALS: _BlockRange + MATHEMATICAL_ALPHANUMERIC_SYMBOLS: _BlockRange + MAHJONG_TILES: _BlockRange + DOMINO_TILES: _BlockRange + PLAYING_CARDS: _BlockRange + ENCLOSED_ALPHANUMERIC_SUPPLEMENT: _BlockRange + ENCLOSED_IDEOGRAPHIC_SUPPLEMENT: _BlockRange + MISCELLANEOUS_SYMBOLS_AND_PICTOGRAPHS: _BlockRange + EMOTICONS: _BlockRange + TRANSPORT_AND_MAP_SYMBOLS: _BlockRange + ALCHEMICAL_SYMBOLS: _BlockRange + CJK_UNIFIED_IDEOGRAPHS_EXTENSION_B: _BlockRange + CJK_UNIFIED_IDEOGRAPHS_EXTENSION_C: _BlockRange + CJK_UNIFIED_IDEOGRAPHS_EXTENSION_D: _BlockRange + CJK_COMPATIBILITY_IDEOGRAPHS_SUPPLEMENT: _BlockRange + TAGS: _BlockRange + VARIATION_SELECTORS_SUPPLEMENT: _BlockRange + SUPPLEMENTARY_PRIVATE_USE_AREA_A: _BlockRange + SUPPLEMENTARY_PRIVATE_USE_AREA_B: _BlockRange + UNKNOWN: _BlockRange + @classmethod + def get(cls, uni_char: str) -> _BlockRange: ... + +def digit(uni_char: str, default_value: int | None = ...) -> int: ... diff --git a/python/phonenumbers/util.pyi b/python/phonenumbers/util.pyi new file mode 100644 index 000000000..0d3be4016 --- /dev/null +++ b/python/phonenumbers/util.pyi @@ -0,0 +1,47 @@ +from collections.abc import Callable +from typing import Any, overload + +from _typeshed import SupportsWrite + +unicod: type[str] +u: type[str] +to_long: type[int] + +@overload +def prnt( + *values: object, + sep: str | None = ..., + end: str | None = ..., + file: SupportsWrite[str] | None = ..., +) -> None: ... +@overload +def prnt(*values: object, **kwargs: Any) -> None: ... + +class UnicodeMixin: + def __str__(self) -> str: ... + +U_EMPTY_STRING: str +U_SPACE: str +U_DASH: str +U_TILDE: str +U_PLUS: str +U_STAR: str +U_ZERO: str +U_SLASH: str +U_SEMICOLON: str +U_X_LOWER: str +U_X_UPPER: str +U_PERCENT: str + +def rpr(s: str | None) -> str: ... +@overload +def force_unicode(s: None) -> None: ... +@overload +def force_unicode(s: str) -> str: ... + +class ImmutableMixin: + _mutable: bool + def __setattr__(self, name: str, value: Any) -> None: ... + def __delattr__(self, name: str) -> None: ... + +def mutating_method(func: Callable[..., Any]) -> Callable[..., Any]: ... diff --git a/python/run_stubtest.py b/python/run_stubtest.py new file mode 100644 index 000000000..de316ae12 --- /dev/null +++ b/python/run_stubtest.py @@ -0,0 +1,13 @@ +import os +import sys +import tempfile + +from mypy import stubtest + +if __name__ == "__main__": + excluded = "\n".join(["phonenumbers.pb2.*"]) # exclude pb2/ (whitelist errors) + with tempfile.NamedTemporaryFile("w+", delete=False) as file: + file.write(excluded) + ret = stubtest.test_stubs(stubtest.parse_options(["phonenumbers", "--whitelist", file.name])) + os.remove(file.name) + sys.exit(ret) diff --git a/python/setup.py b/python/setup.py index 7abc83623..457f50840 100755 --- a/python/setup.py +++ b/python/setup.py @@ -51,11 +51,13 @@ # Various parameters depend on whether we are the lite package or not if lite: pkgname = 'phonenumberslite' - pkgs = ['phonenumbers', 'phonenumbers.data', 'phonenumbers.shortdata'] + pkgs = ['phonenumbers', 'phonenumbers.data', 'phonenumbers.shortdata', + 'tests', 'tests.testdata'] else: pkgname = 'phonenumbers' pkgs = ['phonenumbers', 'phonenumbers.data', 'phonenumbers.geodata', 'phonenumbers.shortdata', - 'phonenumbers.carrierdata', 'phonenumbers.tzdata'] + 'phonenumbers.carrierdata', 'phonenumbers.tzdata', + 'tests', 'tests.testdata', 'tests.testcarrierdata', 'tests.testgeodata', 'tests.testtzdata'] distutils.core.setup(name=pkgname, version=__version__, @@ -69,6 +71,7 @@ packages=pkgs, test_suite="tests.examplenumberstest", platforms='Posix; MacOS X; Windows', + package_data={"": ["*.pyi", "py.typed"]}, classifiers=['Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', 'License :: OSI Approved :: Apache Software License', @@ -84,6 +87,8 @@ 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy', ], diff --git a/python/tests/testcarrierdata/__init__.pyi b/python/tests/testcarrierdata/__init__.pyi new file mode 100644 index 000000000..e47cb065e --- /dev/null +++ b/python/tests/testcarrierdata/__init__.pyi @@ -0,0 +1,2 @@ +CARRIER_DATA: dict[str, dict[str, str]] +CARRIER_LONGEST_PREFIX: int diff --git a/python/tests/testdata/__init__.pyi b/python/tests/testdata/__init__.pyi new file mode 100644 index 000000000..15e9a5dfa --- /dev/null +++ b/python/tests/testdata/__init__.pyi @@ -0,0 +1,5 @@ +_AVAILABLE_REGION_CODES: list[str] +_AVAILABLE_NONGEO_COUNTRY_CODES: list[int] + +def _load_region(code: str | int) -> None: ... +_COUNTRY_CODE_TO_REGION_CODE: dict[int, tuple[str, ...]] diff --git a/python/tests/testgeodata/__init__.pyi b/python/tests/testgeodata/__init__.pyi new file mode 100644 index 000000000..0ddf83d19 --- /dev/null +++ b/python/tests/testgeodata/__init__.pyi @@ -0,0 +1,2 @@ +GEOCODE_DATA: dict[str, dict[str, str]] +GEOCODE_LONGEST_PREFIX: int diff --git a/python/tests/testtzdata/__init__.pyi b/python/tests/testtzdata/__init__.pyi new file mode 100644 index 000000000..45d621a7a --- /dev/null +++ b/python/tests/testtzdata/__init__.pyi @@ -0,0 +1,2 @@ +TIMEZONE_DATA: dict[str, tuple[str, ...]] +TIMEZONE_LONGEST_PREFIX: int diff --git a/tools/python/buildmetadatafromxml.py b/tools/python/buildmetadatafromxml.py index 7398523a4..3412ee9f4 100755 --- a/tools/python/buildmetadatafromxml.py +++ b/tools/python/buildmetadatafromxml.py @@ -6,7 +6,7 @@ Processes the given XML metadata file and emit generated Python code. The output directory will be created if it does not exist, and -__init__.py and per-region data files will be created in the directory. +__init__.py[i] and per-region data files will be created in the directory. """ # Based on original Java code and XML file: @@ -72,6 +72,11 @@ def _load_region(code): for region_code in _AVAILABLE_REGION_CODES: PhoneMetadata.register_%(prefix)sregion_loader(region_code, _load_region) ''' +METADATA_FILE_TYPE_IMPORT = "from %(module)s.phonemetadata import NumberFormat\n" +METADATA_FILE_TYPE_INFO = ''' +def _load_region(code: str) -> None: ...''' +METADATA_FILE_TYPE_INFO_WITH_NONGEO = ''' +def _load_region(code: str | int) -> None: ...''' METADATA_NONGEO_FILE_LOOP = ''' for country_code in _AVAILABLE_NONGEO_COUNTRY_CODES: PhoneMetadata.register_nongeo_region_loader(country_code, _load_region) @@ -668,6 +673,21 @@ def emit_metadata_py(self, datadir, module_prefix): prnt(' %d: ("%s",),' % (country_code, '", "'.join(country_ids)), file=outfile) prnt("}", file=outfile) + # Emit corresponding typing info + with open(modulefilename + "i", "w") as pyifile: + if self.alt_territory is not None: + prnt(METADATA_FILE_TYPE_IMPORT % {'module': module_prefix}, file=pyifile) + prnt("_AVAILABLE_REGION_CODES: list[str]", file=pyifile) + if len(nongeo_codes) > 0: + prnt("_AVAILABLE_NONGEO_COUNTRY_CODES: list[int]", file=pyifile) + prnt(METADATA_FILE_TYPE_INFO_WITH_NONGEO, file=pyifile) + else: + prnt(METADATA_FILE_TYPE_INFO, file=pyifile) + if self.alt_territory is not None: + prnt("\n_ALT_NUMBER_FORMATS: dict[int, list[NumberFormat]]", file=pyifile) + if len(country_code_to_region_code.keys()) > 0: + prnt("_COUNTRY_CODE_TO_REGION_CODE: dict[int, tuple[str, ...]]", file=pyifile) + def _standalone(argv): """Parse the given XML file and emit generated code.""" diff --git a/tools/python/buildprefixdata.py b/tools/python/buildprefixdata.py index 86526a022..5bc780ca9 100755 --- a/tools/python/buildprefixdata.py +++ b/tools/python/buildprefixdata.py @@ -199,6 +199,14 @@ def output_prefixdata_code(prefixdata, outfilename, module_prefix, varprefix, pe prnt("del data", file=outfile) prnt("%s_LONGEST_PREFIX = %d" % (varprefix, longest_prefix), file=outfile) + # Emit corresponding typing info. + with open(outfilename + "i", "w") as pyifile: + if per_locale: + prnt("%s_DATA: dict[str, dict[str, str]]" % varprefix, file=pyifile) + else: + prnt("%s_DATA: dict[str, tuple[str, ...]]" % varprefix, file=pyifile) + prnt("%s_LONGEST_PREFIX: int" % varprefix, file=pyifile) + def output_prefixdata_chunk(prefixdata, outfilename, module_prefix, per_locale): with open(outfilename, "w") as outfile: diff --git a/tools/python/makefile b/tools/python/makefile index 2f900b681..b0380eb6d 100644 --- a/tools/python/makefile +++ b/tools/python/makefile @@ -10,7 +10,9 @@ DumpLocale.class: DumpLocale.java javac $< $(PYDIR)/phonenumbers/geodata/locale.py: DumpLocale.class | $(PYDIR)/phonenumbers/geodata java DumpLocale > $@ -locale: $(PYDIR)/phonenumbers/geodata/locale.py +$(PYDIR)/phonenumbers/geodata/locale.pyi: + echo "LOCALE_DATA: dict[str, dict[str, str]]" > $@ +locale: $(PYDIR)/phonenumbers/geodata/locale.py $(PYDIR)/phonenumbers/geodata/locale.pyi # Generate Python files from geocoding data $(PYDIR)/phonenumbers/geodata: