From 894b2c291f19c464b2ff5454e1f7d4fd9424d0e0 Mon Sep 17 00:00:00 2001 From: Gregory Oschwald Date: Tue, 28 Jan 2025 12:02:06 -0800 Subject: [PATCH 01/10] Run ruff's fixer --- docs/conf.py | 3 +- examples/benchmark.py | 5 +- geoip2/_internal.py | 3 +- geoip2/database.py | 52 ++++++++++------- geoip2/errors.py | 1 - geoip2/models.py | 18 +++--- geoip2/records.py | 19 +++--- geoip2/webservice.py | 42 ++++++++++---- tests/database_test.py | 42 ++++++++------ tests/models_test.py | 121 ++++++++++++++++++++++++++++----------- tests/webservice_test.py | 98 ++++++++++++++++++++----------- 11 files changed, 270 insertions(+), 134 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index e8526e92..dc9d5ee4 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- # # geoip2 documentation build configuration file, created by # sphinx-quickstart on Tue Apr 9 13:34:57 2013. @@ -12,8 +11,8 @@ # All configuration values have a default; values that are commented out # serve to show the default. -import sys import os +import sys # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the diff --git a/examples/benchmark.py b/examples/benchmark.py index 4a60afcc..ef940f80 100644 --- a/examples/benchmark.py +++ b/examples/benchmark.py @@ -1,15 +1,14 @@ #!/usr/bin/python -# -*- coding: utf-8 -*- -from __future__ import print_function import argparse -import geoip2.database import random import socket import struct import timeit +import geoip2.database + parser = argparse.ArgumentParser(description="Benchmark maxminddb.") parser.add_argument("--count", default=250000, type=int, help="number of lookups") parser.add_argument("--mode", default=0, type=int, help="reader mode to use") diff --git a/geoip2/_internal.py b/geoip2/_internal.py index 6f37ced5..bf2b86b6 100644 --- a/geoip2/_internal.py +++ b/geoip2/_internal.py @@ -2,13 +2,12 @@ # pylint: disable=too-few-public-methods from abc import ABCMeta -from typing import Any class Model(metaclass=ABCMeta): """Shared methods for MaxMind model classes""" - def __eq__(self, other: Any) -> bool: + def __eq__(self, other: object) -> bool: return isinstance(other, self.__class__) and self.to_dict() == other.to_dict() def __ne__(self, other): diff --git a/geoip2/database.py b/geoip2/database.py index 4652d7d0..13e7d88f 100644 --- a/geoip2/database.py +++ b/geoip2/database.py @@ -7,41 +7,41 @@ import inspect import os -from typing import Any, AnyStr, cast, IO, Optional, Sequence, Type, Union +from collections.abc import Sequence +from typing import IO, Any, AnyStr, Optional, Type, Union, cast import maxminddb - from maxminddb import ( MODE_AUTO, - MODE_MMAP, - MODE_MMAP_EXT, + MODE_FD, MODE_FILE, MODE_MEMORY, - MODE_FD, + MODE_MMAP, + MODE_MMAP_EXT, ) import geoip2 -import geoip2.models import geoip2.errors -from geoip2.types import IPAddress +import geoip2.models from geoip2.models import ( ASN, + ISP, AnonymousIP, City, ConnectionType, Country, Domain, Enterprise, - ISP, ) +from geoip2.types import IPAddress __all__ = [ "MODE_AUTO", - "MODE_MMAP", - "MODE_MMAP_EXT", + "MODE_FD", "MODE_FILE", "MODE_MEMORY", - "MODE_FD", + "MODE_MMAP", + "MODE_MMAP_EXT", "Reader", ] @@ -135,9 +135,9 @@ def country(self, ip_address: IPAddress) -> Country: :returns: :py:class:`geoip2.models.Country` object """ - return cast( - Country, self._model_for(geoip2.models.Country, "Country", ip_address) + Country, + self._model_for(geoip2.models.Country, "Country", ip_address), ) def city(self, ip_address: IPAddress) -> City: @@ -161,7 +161,9 @@ def anonymous_ip(self, ip_address: IPAddress) -> AnonymousIP: return cast( AnonymousIP, self._flat_model_for( - geoip2.models.AnonymousIP, "GeoIP2-Anonymous-IP", ip_address + geoip2.models.AnonymousIP, + "GeoIP2-Anonymous-IP", + ip_address, ), ) @@ -174,7 +176,8 @@ def asn(self, ip_address: IPAddress) -> ASN: """ return cast( - ASN, self._flat_model_for(geoip2.models.ASN, "GeoLite2-ASN", ip_address) + ASN, + self._flat_model_for(geoip2.models.ASN, "GeoLite2-ASN", ip_address), ) def connection_type(self, ip_address: IPAddress) -> ConnectionType: @@ -188,7 +191,9 @@ def connection_type(self, ip_address: IPAddress) -> ConnectionType: return cast( ConnectionType, self._flat_model_for( - geoip2.models.ConnectionType, "GeoIP2-Connection-Type", ip_address + geoip2.models.ConnectionType, + "GeoIP2-Connection-Type", + ip_address, ), ) @@ -227,7 +232,8 @@ def isp(self, ip_address: IPAddress) -> ISP: """ return cast( - ISP, self._flat_model_for(geoip2.models.ISP, "GeoIP2-ISP", ip_address) + ISP, + self._flat_model_for(geoip2.models.ISP, "GeoIP2-ISP", ip_address), ) def _get(self, database_type: str, ip_address: IPAddress) -> Any: @@ -253,13 +259,20 @@ def _model_for( ) -> Union[Country, Enterprise, City]: (record, prefix_len) = self._get(types, ip_address) return model_class( - self._locales, ip_address=ip_address, prefix_len=prefix_len, **record + self._locales, + ip_address=ip_address, + prefix_len=prefix_len, + **record, ) def _flat_model_for( self, model_class: Union[ - Type[Domain], Type[ISP], Type[ConnectionType], Type[ASN], Type[AnonymousIP] + Type[Domain], + Type[ISP], + Type[ConnectionType], + Type[ASN], + Type[AnonymousIP], ], types: str, ip_address: IPAddress, @@ -278,5 +291,4 @@ def metadata( def close(self) -> None: """Closes the GeoIP2 database.""" - self._db_reader.close() diff --git a/geoip2/errors.py b/geoip2/errors.py index b3e15d30..e56d56c7 100644 --- a/geoip2/errors.py +++ b/geoip2/errors.py @@ -54,7 +54,6 @@ def __init__( @property def network(self) -> Optional[Union[ipaddress.IPv4Network, ipaddress.IPv6Network]]: """The network for the error""" - if self.ip_address is None or self._prefix_len is None: return None return ipaddress.ip_network(f"{self.ip_address}/{self._prefix_len}", False) diff --git a/geoip2/models.py b/geoip2/models.py index ac0199c7..a3d6310b 100644 --- a/geoip2/models.py +++ b/geoip2/models.py @@ -14,7 +14,8 @@ # pylint: disable=too-many-instance-attributes,too-few-public-methods,too-many-arguments import ipaddress from abc import ABCMeta -from typing import Dict, List, Optional, Sequence, Union +from collections.abc import Sequence +from typing import Dict, List, Optional, Union import geoip2.records from geoip2._internal import Model @@ -94,10 +95,12 @@ def __init__( self.continent = geoip2.records.Continent(locales, **(continent or {})) self.country = geoip2.records.Country(locales, **(country or {})) self.registered_country = geoip2.records.Country( - locales, **(registered_country or {}) + locales, + **(registered_country or {}), ) self.represented_country = geoip2.records.RepresentedCountry( - locales, **(represented_country or {}) + locales, + **(represented_country or {}), ) self.maxmind = geoip2.records.MaxMind(**(maxmind or {})) @@ -112,8 +115,8 @@ def __init__( def __repr__(self) -> str: return ( - f"{self.__module__}.{self.__class__.__name__}({repr(self._locales)}, " - f"{', '.join(f'{k}={repr(v)}' for k, v in self.to_dict().items())})" + f"{self.__module__}.{self.__class__.__name__}({self._locales!r}, " + f"{', '.join(f'{k}={v!r}' for k, v in self.to_dict().items())})" ) @@ -387,7 +390,7 @@ def __repr__(self) -> str: f"{self.__module__}.{self.__class__.__name__}(" + repr(str(self._ip_address)) + ", " - + ", ".join(f"{k}={repr(v)}" for k, v in d.items()) + + ", ".join(f"{k}={v!r}" for k, v in d.items()) + ")" ) @@ -395,7 +398,8 @@ def __repr__(self) -> str: def ip_address(self): """The IP address for the record""" if not isinstance( - self._ip_address, (ipaddress.IPv4Address, ipaddress.IPv6Address) + self._ip_address, + (ipaddress.IPv4Address, ipaddress.IPv6Address), ): self._ip_address = ipaddress.ip_address(self._ip_address) return self._ip_address diff --git a/geoip2/records.py b/geoip2/records.py index 17c1f37b..985f3b7b 100644 --- a/geoip2/records.py +++ b/geoip2/records.py @@ -11,7 +11,8 @@ # pylint:disable=R0903 from abc import ABCMeta -from typing import Dict, Optional, Type, Sequence, Union +from collections.abc import Sequence +from typing import Dict, Optional, Type, Union from geoip2._internal import Model @@ -113,7 +114,6 @@ class Continent(PlaceRecord): Attributes: - .. attribute:: code A two character continent code like "NA" (North America) @@ -167,7 +167,6 @@ class Country(PlaceRecord): Attributes: - .. attribute:: confidence A value from 0-100 indicating MaxMind's confidence that @@ -244,7 +243,6 @@ class RepresentedCountry(Country): Attributes: - .. attribute:: confidence A value from 0-100 indicating MaxMind's confidence that @@ -470,7 +468,11 @@ class Postal(Record): confidence: Optional[int] def __init__( - self, *, code: Optional[str] = None, confidence: Optional[int] = None, **_ + self, + *, + code: Optional[str] = None, + confidence: Optional[int] = None, + **_, ) -> None: self.code = code self.confidence = confidence @@ -556,7 +558,9 @@ class Subdivisions(tuple): """ def __new__( - cls: Type["Subdivisions"], locales: Optional[Sequence[str]], *subdivisions + cls: Type["Subdivisions"], + locales: Optional[Sequence[str]], + *subdivisions, ) -> "Subdivisions": subobjs = tuple(Subdivision(locales, **x) for x in subdivisions) obj = super().__new__(cls, subobjs) # type: ignore @@ -923,7 +927,8 @@ def __init__( def ip_address(self): """The IP address for the record""" if not isinstance( - self._ip_address, (ipaddress.IPv4Address, ipaddress.IPv6Address) + self._ip_address, + (ipaddress.IPv4Address, ipaddress.IPv6Address), ): self._ip_address = ipaddress.ip_address(self._ip_address) return self._ip_address diff --git a/geoip2/webservice.py b/geoip2/webservice.py index 3158d735..1abbd33c 100644 --- a/geoip2/webservice.py +++ b/geoip2/webservice.py @@ -27,7 +27,8 @@ import ipaddress import json -from typing import Any, Dict, cast, Optional, Sequence, Type, Union +from collections.abc import Sequence +from typing import Any, Dict, Optional, Type, Union, cast import aiohttp import aiohttp.http @@ -106,7 +107,11 @@ def _handle_success(body: str, uri: str) -> Any: ) from ex def _exception_for_error( - self, status: int, content_type: str, body: str, uri: str + self, + status: int, + content_type: str, + body: str, + uri: str, ) -> GeoIP2Error: if 400 <= status < 500: return self._exception_for_4xx_status(status, content_type, body, uri) @@ -115,7 +120,11 @@ def _exception_for_error( return self._exception_for_non_200_status(status, uri, body) def _exception_for_4xx_status( - self, status: int, content_type: str, body: str, uri: str + self, + status: int, + content_type: str, + body: str, + uri: str, ) -> GeoIP2Error: if not body: return HTTPError( @@ -145,7 +154,10 @@ def _exception_for_4xx_status( if "code" in decoded_body and "error" in decoded_body: return self._exception_for_web_service_error( - decoded_body.get("error"), decoded_body.get("code"), status, uri + decoded_body.get("error"), + decoded_body.get("code"), + status, + uri, ) return HTTPError( "Response contains JSON but it does not specify code or error keys", @@ -156,7 +168,10 @@ def _exception_for_4xx_status( @staticmethod def _exception_for_web_service_error( - message: str, code: str, status: int, uri: str + message: str, + code: str, + status: int, + uri: str, ) -> Union[ AuthenticationError, AddressNotFoundError, @@ -184,7 +199,9 @@ def _exception_for_web_service_error( @staticmethod def _exception_for_5xx_status( - status: int, uri: str, body: Optional[str] + status: int, + uri: str, + body: Optional[str], ) -> HTTPError: return HTTPError( f"Received a server error ({status}) for {uri}", @@ -195,7 +212,9 @@ def _exception_for_5xx_status( @staticmethod def _exception_for_non_200_status( - status: int, uri: str, body: Optional[str] + status: int, + uri: str, + body: Optional[str], ) -> HTTPError: return HTTPError( f"Received a very surprising HTTP status ({status}) for {uri}", @@ -289,7 +308,8 @@ async def city(self, ip_address: IPAddress = "me") -> City: """ return cast( - City, await self._response_for("city", geoip2.models.City, ip_address) + City, + await self._response_for("city", geoip2.models.City, ip_address), ) async def country(self, ip_address: IPAddress = "me") -> Country: @@ -465,7 +485,8 @@ def country(self, ip_address: IPAddress = "me") -> Country: """ return cast( - Country, self._response_for("country", geoip2.models.Country, ip_address) + Country, + self._response_for("country", geoip2.models.Country, ip_address), ) def insights(self, ip_address: IPAddress = "me") -> Insights: @@ -482,7 +503,8 @@ def insights(self, ip_address: IPAddress = "me") -> Insights: """ return cast( - Insights, self._response_for("insights", geoip2.models.Insights, ip_address) + Insights, + self._response_for("insights", geoip2.models.Insights, ip_address), ) def _response_for( diff --git a/tests/database_test.py b/tests/database_test.py index 3008f574..672aafd4 100644 --- a/tests/database_test.py +++ b/tests/database_test.py @@ -1,18 +1,17 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- -from __future__ import unicode_literals import ipaddress import sys import unittest -from unittest.mock import patch, MagicMock +from unittest.mock import MagicMock, patch sys.path.append("..") +import maxminddb + import geoip2.database import geoip2.errors -import maxminddb try: import maxminddb.extension @@ -48,7 +47,7 @@ def test_unknown_address_network(self) -> None: except geoip2.errors.AddressNotFoundError as e: self.assertEqual(e.network, ipaddress.ip_network("10.0.0.0/8")) except Exception as e: - self.fail(f"Expected AddressNotFoundError, got {type(e)}: {str(e)}") + self.fail(f"Expected AddressNotFoundError, got {type(e)}: {e!s}") finally: reader.close() @@ -64,14 +63,15 @@ def test_wrong_database(self) -> None: def test_invalid_address(self) -> None: reader = geoip2.database.Reader("tests/data/test-data/GeoIP2-City-Test.mmdb") with self.assertRaisesRegex( - ValueError, "u?'invalid' does not appear to be an IPv4 or IPv6 address" + ValueError, + "u?'invalid' does not appear to be an IPv4 or IPv6 address", ): reader.city("invalid") reader.close() def test_anonymous_ip(self) -> None: reader = geoip2.database.Reader( - "tests/data/test-data/GeoIP2-Anonymous-IP-Test.mmdb" + "tests/data/test-data/GeoIP2-Anonymous-IP-Test.mmdb", ) ip_address = "1.2.0.1" @@ -88,7 +88,7 @@ def test_anonymous_ip(self) -> None: def test_anonymous_ip_all_set(self) -> None: reader = geoip2.database.Reader( - "tests/data/test-data/GeoIP2-Anonymous-IP-Test.mmdb" + "tests/data/test-data/GeoIP2-Anonymous-IP-Test.mmdb", ) ip_address = "81.2.69.1" @@ -129,11 +129,15 @@ def test_city(self) -> None: record = reader.city("81.2.69.160") self.assertEqual( - record.country.name, "United Kingdom", "The default locale is en" + record.country.name, + "United Kingdom", + "The default locale is en", ) self.assertEqual(record.country.is_in_european_union, False) self.assertEqual( - record.location.accuracy_radius, 100, "The accuracy_radius is populated" + record.location.accuracy_radius, + 100, + "The accuracy_radius is populated", ) self.assertEqual(record.registered_country.is_in_european_union, False) self.assertFalse(record.traits.is_anycast) @@ -145,14 +149,16 @@ def test_city(self) -> None: def test_connection_type(self) -> None: reader = geoip2.database.Reader( - "tests/data/test-data/GeoIP2-Connection-Type-Test.mmdb" + "tests/data/test-data/GeoIP2-Connection-Type-Test.mmdb", ) ip_address = "1.0.1.0" record = reader.connection_type(ip_address) self.assertEqual( - record, eval(repr(record)), "ConnectionType repr can be eval'd" + record, + eval(repr(record)), + "ConnectionType repr can be eval'd", ) self.assertEqual(record.connection_type, "Cellular") @@ -207,7 +213,7 @@ def test_domain(self) -> None: def test_enterprise(self) -> None: with geoip2.database.Reader( - "tests/data/test-data/GeoIP2-Enterprise-Test.mmdb" + "tests/data/test-data/GeoIP2-Enterprise-Test.mmdb", ) as reader: ip_address = "74.209.24.0" record = reader.enterprise(ip_address) @@ -221,7 +227,8 @@ def test_enterprise(self) -> None: self.assertTrue(record.traits.is_legitimate_proxy) self.assertEqual(record.traits.ip_address, ipaddress.ip_address(ip_address)) self.assertEqual( - record.traits.network, ipaddress.ip_network("74.209.16.0/20") + record.traits.network, + ipaddress.ip_network("74.209.16.0/20"), ) self.assertFalse(record.traits.is_anycast) @@ -234,7 +241,7 @@ def test_enterprise(self) -> None: def test_isp(self) -> None: with geoip2.database.Reader( - "tests/data/test-data/GeoIP2-ISP-Test.mmdb" + "tests/data/test-data/GeoIP2-ISP-Test.mmdb", ) as reader: ip_address = "1.128.0.0" record = reader.isp(ip_address) @@ -260,11 +267,12 @@ def test_isp(self) -> None: def test_context_manager(self) -> None: with geoip2.database.Reader( - "tests/data/test-data/GeoIP2-Country-Test.mmdb" + "tests/data/test-data/GeoIP2-Country-Test.mmdb", ) as reader: record = reader.country("81.2.69.160") self.assertEqual( - record.traits.ip_address, ipaddress.ip_address("81.2.69.160") + record.traits.ip_address, + ipaddress.ip_address("81.2.69.160"), ) @patch("maxminddb.open_database") diff --git a/tests/models_test.py b/tests/models_test.py index 3f72ec27..dadd6b02 100644 --- a/tests/models_test.py +++ b/tests/models_test.py @@ -1,12 +1,10 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- -from __future__ import unicode_literals -import sys import ipaddress -from typing import Dict +import sys import unittest +from typing import Dict sys.path.append("..") @@ -98,10 +96,14 @@ def test_insights_full(self) -> None: model = geoip2.models.Insights(["en"], **raw) # type: ignore self.assertEqual( - type(model), geoip2.models.Insights, "geoip2.models.Insights object" + type(model), + geoip2.models.Insights, + "geoip2.models.Insights object", ) self.assertEqual( - type(model.city), geoip2.records.City, "geoip2.records.City object" + type(model.city), + geoip2.records.City, + "geoip2.records.City object", ) self.assertEqual( type(model.continent), @@ -109,7 +111,9 @@ def test_insights_full(self) -> None: "geoip2.records.Continent object", ) self.assertEqual( - type(model.country), geoip2.records.Country, "geoip2.records.Country object" + type(model.country), + geoip2.records.Country, + "geoip2.records.Country object", ) self.assertEqual( type(model.registered_country), @@ -132,23 +136,35 @@ def test_insights_full(self) -> None: "geoip2.records.Subdivision object", ) self.assertEqual( - type(model.traits), geoip2.records.Traits, "geoip2.records.Traits object" + type(model.traits), + geoip2.records.Traits, + "geoip2.records.Traits object", ) self.assertEqual(model.to_dict(), raw, "to_dict() method matches raw input") self.assertEqual( - model.subdivisions[0].iso_code, "MN", "div 1 has correct iso_code" + model.subdivisions[0].iso_code, + "MN", + "div 1 has correct iso_code", ) self.assertEqual( - model.subdivisions[0].confidence, 88, "div 1 has correct confidence" + model.subdivisions[0].confidence, + 88, + "div 1 has correct confidence", ) self.assertEqual( - model.subdivisions[0].geoname_id, 574635, "div 1 has correct geoname_id" + model.subdivisions[0].geoname_id, + 574635, + "div 1 has correct geoname_id", ) self.assertEqual( - model.subdivisions[0].names, {"en": "Minnesota"}, "div 1 names are correct" + model.subdivisions[0].names, + {"en": "Minnesota"}, + "div 1 names are correct", ) self.assertEqual( - model.subdivisions[1].name, "Hennepin", "div 2 has correct name" + model.subdivisions[1].name, + "Hennepin", + "div 2 has correct name", ) self.assertEqual( model.subdivisions.most_specific.iso_code, @@ -170,7 +186,9 @@ def test_insights_full(self) -> None: self.assertEqual(model.location.longitude, 93.2636, "correct longitude") self.assertEqual(model.location.metro_code, 765, "correct metro_code") self.assertEqual( - model.location.population_density, 1341, "correct population_density" + model.location.population_density, + 1341, + "correct population_density", ) self.assertRegex( @@ -188,7 +206,9 @@ def test_insights_full(self) -> None: ) self.assertEqual( - model.location, eval(repr(model.location)), "Location repr can be eval'd" + model.location, + eval(repr(model.location)), + "Location repr can be eval'd", ) self.assertIs(model.country.is_in_european_union, False) @@ -210,10 +230,14 @@ def test_insights_full(self) -> None: def test_insights_min(self) -> None: model = geoip2.models.Insights(["en"], traits={"ip_address": "5.6.7.8"}) self.assertEqual( - type(model), geoip2.models.Insights, "geoip2.models.Insights object" + type(model), + geoip2.models.Insights, + "geoip2.models.Insights object", ) self.assertEqual( - type(model.city), geoip2.records.City, "geoip2.records.City object" + type(model.city), + geoip2.records.City, + "geoip2.records.City object", ) self.assertEqual( type(model.continent), @@ -221,7 +245,9 @@ def test_insights_min(self) -> None: "geoip2.records.Continent object", ) self.assertEqual( - type(model.country), geoip2.records.Country, "geoip2.records.Country object" + type(model.country), + geoip2.records.Country, + "geoip2.records.Country object", ) self.assertEqual( type(model.registered_country), @@ -234,7 +260,9 @@ def test_insights_min(self) -> None: "geoip2.records.Location object", ) self.assertEqual( - type(model.traits), geoip2.records.Traits, "geoip2.records.Traits object" + type(model.traits), + geoip2.records.Traits, + "geoip2.records.Traits object", ) self.assertEqual( type(model.subdivisions.most_specific), @@ -242,7 +270,9 @@ def test_insights_min(self) -> None: "geoip2.records.Subdivision object returned even when none are available.", ) self.assertEqual( - model.subdivisions.most_specific.names, {}, "Empty names hash returned" + model.subdivisions.most_specific.names, + {}, + "Empty names hash returned", ) def test_city_full(self) -> None: @@ -270,7 +300,9 @@ def test_city_full(self) -> None: model = geoip2.models.City(["en"], **raw) # type: ignore self.assertEqual(type(model), geoip2.models.City, "geoip2.models.City object") self.assertEqual( - type(model.city), geoip2.records.City, "geoip2.records.City object" + type(model.city), + geoip2.records.City, + "geoip2.records.City object", ) self.assertEqual( type(model.continent), @@ -278,7 +310,9 @@ def test_city_full(self) -> None: "geoip2.records.Continent object", ) self.assertEqual( - type(model.country), geoip2.records.Country, "geoip2.records.Country object" + type(model.country), + geoip2.records.Country, + "geoip2.records.Country object", ) self.assertEqual( type(model.registered_country), @@ -291,18 +325,26 @@ def test_city_full(self) -> None: "geoip2.records.Location object", ) self.assertEqual( - type(model.traits), geoip2.records.Traits, "geoip2.records.Traits object" + type(model.traits), + geoip2.records.Traits, + "geoip2.records.Traits object", ) self.assertEqual( - model.to_dict(), raw, "to_dict method output matches raw input" + model.to_dict(), + raw, + "to_dict method output matches raw input", ) self.assertEqual(model.continent.geoname_id, 42, "continent geoname_id is 42") self.assertEqual(model.continent.code, "NA", "continent code is NA") self.assertEqual( - model.continent.names, {"en": "North America"}, "continent names is correct" + model.continent.names, + {"en": "North America"}, + "continent names is correct", ) self.assertEqual( - model.continent.name, "North America", "continent name is correct" + model.continent.name, + "North America", + "continent name is correct", ) self.assertEqual(model.country.geoname_id, 1, "country geoname_id is 1") self.assertEqual(model.country.iso_code, "US", "country iso_code is US") @@ -312,11 +354,15 @@ def test_city_full(self) -> None: "country names is correct", ) self.assertEqual( - model.country.name, "United States of America", "country name is correct" + model.country.name, + "United States of America", + "country name is correct", ) self.assertEqual(model.country.confidence, None, "country confidence is None") self.assertEqual( - model.registered_country.iso_code, "CA", "registered_country iso_code is CA" + model.registered_country.iso_code, + "CA", + "registered_country iso_code is CA", ) self.assertEqual( model.registered_country.names, @@ -346,7 +392,8 @@ def test_city_full(self) -> None: self.assertEqual(model.to_dict(), raw, "to_dict method matches raw input") self.assertRegex( - str(model), r"^geoip2.models.City\(\[.*en.*\], .*geoname_id.*\)" + str(model), + r"^geoip2.models.City\(\[.*en.*\], .*geoname_id.*\)", ) self.assertFalse(model == True, "__eq__ does not blow up on weird input") @@ -393,7 +440,9 @@ def test_unknown_keys(self) -> None: with self.assertRaises(AttributeError): model.traits.invalid # type: ignore self.assertEqual( - model.traits.ip_address, ipaddress.ip_address("1.2.3.4"), "correct ip" + model.traits.ip_address, + ipaddress.ip_address("1.2.3.4"), + "correct ip", ) @@ -460,16 +509,22 @@ def test_two_locales(self) -> None: def test_unknown_locale(self) -> None: model = geoip2.models.Country(locales=["aa"], **self.raw) self.assertEqual( - model.continent.name, None, "continent name is undef (no Afar available)" + model.continent.name, + None, + "continent name is undef (no Afar available)", ) self.assertEqual( - model.country.name, None, "country name is in None (no Afar available)" + model.country.name, + None, + "country name is in None (no Afar available)", ) def test_german(self) -> None: model = geoip2.models.Country(locales=["de"], **self.raw) self.assertEqual( - model.continent.name, "Nordamerika", "Correct german name for continent" + model.continent.name, + "Nordamerika", + "Correct german name for continent", ) diff --git a/tests/webservice_test.py b/tests/webservice_test.py index 0d6cc496..d19a64a9 100644 --- a/tests/webservice_test.py +++ b/tests/webservice_test.py @@ -1,17 +1,16 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- import asyncio import copy import ipaddress import sys -from typing import cast, Dict import unittest -from pytest_httpserver import HeaderValueMatcher -import pytest_httpserver -import pytest from collections import defaultdict +from typing import Dict, cast +import pytest +import pytest_httpserver +from pytest_httpserver import HeaderValueMatcher sys.path.append("..") import geoip2 @@ -68,7 +67,8 @@ def setup_httpserver(self, httpserver: pytest_httpserver.HTTPServer): def test_country_ok(self): self.httpserver.expect_request( - "/geoip/v2.1/country/1.2.3.4", method="GET" + "/geoip/v2.1/country/1.2.3.4", + method="GET", ).respond_with_json( self.country, status=200, @@ -76,12 +76,16 @@ def test_country_ok(self): ) country = self.run_client(self.client.country("1.2.3.4")) self.assertEqual( - type(country), geoip2.models.Country, "return value of client.country" + type(country), + geoip2.models.Country, + "return value of client.country", ) self.assertEqual(country.continent.geoname_id, 42, "continent geoname_id is 42") self.assertEqual(country.continent.code, "NA", "continent code is NA") self.assertEqual( - country.continent.name, "North America", "continent name is North America" + country.continent.name, + "North America", + "continent name is North America", ) self.assertEqual(country.country.geoname_id, 1, "country geoname_id is 1") self.assertIs( @@ -91,7 +95,9 @@ def test_country_ok(self): ) self.assertEqual(country.country.iso_code, "US", "country iso_code is US") self.assertEqual( - country.country.names, {"en": "United States of America"}, "country names" + country.country.names, + {"en": "United States of America"}, + "country names", ) self.assertEqual( country.country.name, @@ -99,7 +105,9 @@ def test_country_ok(self): "country name is United States of America", ) self.assertEqual( - country.maxmind.queries_remaining, 11, "queries_remaining is 11" + country.maxmind.queries_remaining, + 11, + "queries_remaining is 11", ) self.assertIs( country.registered_country.is_in_european_union, @@ -107,14 +115,17 @@ def test_country_ok(self): "registered_country is_in_european_union is True", ) self.assertEqual( - country.traits.network, ipaddress.ip_network("1.2.3.0/24"), "network" + country.traits.network, + ipaddress.ip_network("1.2.3.0/24"), + "network", ) self.assertTrue(country.traits.is_anycast) self.assertEqual(country.to_dict(), self.country, "raw response is correct") def test_me(self): self.httpserver.expect_request( - "/geoip/v2.1/country/me", method="GET" + "/geoip/v2.1/country/me", + method="GET", ).respond_with_json( self.country, status=200, @@ -122,7 +133,9 @@ def test_me(self): ) implicit_me = self.run_client(self.client.country()) self.assertEqual( - type(implicit_me), geoip2.models.Country, "country() returns Country object" + type(implicit_me), + geoip2.models.Country, + "country() returns Country object", ) explicit_me = self.run_client(self.client.country()) self.assertEqual( @@ -133,7 +146,8 @@ def test_me(self): def test_200_error(self): self.httpserver.expect_request( - "/geoip/v2.1/country/1.1.1.1", method="GET" + "/geoip/v2.1/country/1.1.1.1", + method="GET", ).respond_with_data( "", status=200, @@ -141,32 +155,37 @@ def test_200_error(self): ) with self.assertRaisesRegex( - GeoIP2Error, "could not decode the response as JSON" + GeoIP2Error, + "could not decode the response as JSON", ): self.run_client(self.client.country("1.1.1.1")) def test_bad_ip_address(self): with self.assertRaisesRegex( - ValueError, "'1.2.3' does not appear to be an IPv4 or IPv6 address" + ValueError, + "'1.2.3' does not appear to be an IPv4 or IPv6 address", ): self.run_client(self.client.country("1.2.3")) def test_no_body_error(self): self.httpserver.expect_request( - "/geoip/v2.1/country/1.2.3.7", method="GET" + "/geoip/v2.1/country/1.2.3.7", + method="GET", ).respond_with_data( "", status=400, content_type=self._content_type("country"), ) with self.assertRaisesRegex( - HTTPError, "Received a 400 error for .* with no body" + HTTPError, + "Received a 400 error for .* with no body", ): self.run_client(self.client.country("1.2.3.7")) def test_weird_body_error(self): self.httpserver.expect_request( - "/geoip/v2.1/country/1.2.3.8", method="GET" + "/geoip/v2.1/country/1.2.3.8", + method="GET", ).respond_with_json( {"wierd": 42}, status=400, @@ -181,20 +200,23 @@ def test_weird_body_error(self): def test_bad_body_error(self): self.httpserver.expect_request( - "/geoip/v2.1/country/1.2.3.9", method="GET" + "/geoip/v2.1/country/1.2.3.9", + method="GET", ).respond_with_data( "bad body", status=400, content_type=self._content_type("country"), ) with self.assertRaisesRegex( - HTTPError, "it did not include the expected JSON body" + HTTPError, + "it did not include the expected JSON body", ): self.run_client(self.client.country("1.2.3.9")) def test_500_error(self): self.httpserver.expect_request( - "/geoip/v2.1/country/1.2.3.10", method="GET" + "/geoip/v2.1/country/1.2.3.10", + method="GET", ).respond_with_data( "", status=500, @@ -205,14 +227,16 @@ def test_500_error(self): def test_300_error(self): self.httpserver.expect_request( - "/geoip/v2.1/country/1.2.3.11", method="GET" + "/geoip/v2.1/country/1.2.3.11", + method="GET", ).respond_with_data( "", status=300, content_type=self._content_type("country"), ) with self.assertRaisesRegex( - HTTPError, r"Received a very surprising HTTP status \(300\) for" + HTTPError, + r"Received a very surprising HTTP status \(300\) for", ): self.run_client(self.client.country("1.2.3.11")) @@ -253,7 +277,8 @@ def _test_error(self, status, error_code, error_class): msg = "Some error message" body = {"error": msg, "code": error_code} self.httpserver.expect_request( - "/geoip/v2.1/country/1.2.3.18", method="GET" + "/geoip/v2.1/country/1.2.3.18", + method="GET", ).respond_with_json( body, status=status, @@ -267,7 +292,8 @@ def test_unknown_error(self): ip = "1.2.3.19" body = {"error": msg, "code": "UNKNOWN_TYPE"} self.httpserver.expect_request( - "/geoip/v2.1/country/" + ip, method="GET" + "/geoip/v2.1/country/" + ip, + method="GET", ).respond_with_json( body, status=400, @@ -294,7 +320,7 @@ def user_agent_compare(actual: str, expected: str) -> bool: defaultdict( lambda: HeaderValueMatcher.default_header_value_matcher, {"User-Agent": user_agent_compare}, - ) + ), ), ).respond_with_json( self.country, @@ -305,7 +331,8 @@ def user_agent_compare(actual: str, expected: str) -> bool: def test_city_ok(self): self.httpserver.expect_request( - "/geoip/v2.1/city/1.2.3.4", method="GET" + "/geoip/v2.1/city/1.2.3.4", + method="GET", ).respond_with_json( self.country, status=200, @@ -314,13 +341,16 @@ def test_city_ok(self): city = self.run_client(self.client.city("1.2.3.4")) self.assertEqual(type(city), geoip2.models.City, "return value of client.city") self.assertEqual( - city.traits.network, ipaddress.ip_network("1.2.3.0/24"), "network" + city.traits.network, + ipaddress.ip_network("1.2.3.0/24"), + "network", ) self.assertTrue(city.traits.is_anycast) def test_insights_ok(self): self.httpserver.expect_request( - "/geoip/v2.1/insights/1.2.3.4", method="GET" + "/geoip/v2.1/insights/1.2.3.4", + method="GET", ).respond_with_json( self.insights, status=200, @@ -328,10 +358,14 @@ def test_insights_ok(self): ) insights = self.run_client(self.client.insights("1.2.3.4")) self.assertEqual( - type(insights), geoip2.models.Insights, "return value of client.insights" + type(insights), + geoip2.models.Insights, + "return value of client.insights", ) self.assertEqual( - insights.traits.network, ipaddress.ip_network("1.2.3.0/24"), "network" + insights.traits.network, + ipaddress.ip_network("1.2.3.0/24"), + "network", ) self.assertTrue(insights.traits.is_anycast) self.assertEqual(insights.traits.static_ip_score, 1.3, "static_ip_score is 1.3") From 04260bf7954f7cef60aa0580413f9b67bb510dae Mon Sep 17 00:00:00 2001 From: Gregory Oschwald Date: Tue, 28 Jan 2025 12:13:25 -0800 Subject: [PATCH 02/10] Apply ruff unsafe fixes --- examples/benchmark.py | 9 +++--- geoip2/_internal.py | 6 ++-- geoip2/database.py | 14 ++++---- geoip2/errors.py | 2 +- geoip2/models.py | 40 +++++++++++------------ geoip2/records.py | 25 +++++++------- geoip2/types.py | 2 +- geoip2/webservice.py | 16 ++++----- tests/database_test.py | 2 +- tests/models_test.py | 5 ++- tests/webservice_test.py | 70 ++++++++++++++++++++-------------------- 11 files changed, 94 insertions(+), 97 deletions(-) diff --git a/examples/benchmark.py b/examples/benchmark.py index ef940f80..b9fbc7c2 100644 --- a/examples/benchmark.py +++ b/examples/benchmark.py @@ -2,6 +2,7 @@ import argparse +import contextlib import random import socket import struct @@ -19,12 +20,10 @@ reader = geoip2.database.Reader(args.file, mode=args.mode) -def lookup_ip_address(): +def lookup_ip_address() -> None: ip = socket.inet_ntoa(struct.pack("!L", random.getrandbits(32))) - try: - record = reader.city(str(ip)) - except geoip2.errors.AddressNotFoundError: - pass + with contextlib.suppress(geoip2.errors.AddressNotFoundError): + reader.city(str(ip)) elapsed = timeit.timeit( diff --git a/geoip2/_internal.py b/geoip2/_internal.py index bf2b86b6..27ce459a 100644 --- a/geoip2/_internal.py +++ b/geoip2/_internal.py @@ -1,11 +1,11 @@ -"""This package contains internal utilities""" +"""This package contains internal utilities.""" # pylint: disable=too-few-public-methods from abc import ABCMeta class Model(metaclass=ABCMeta): - """Shared methods for MaxMind model classes""" + """Shared methods for MaxMind model classes.""" def __eq__(self, other: object) -> bool: return isinstance(other, self.__class__) and self.to_dict() == other.to_dict() @@ -15,7 +15,7 @@ def __ne__(self, other): # pylint: disable=too-many-branches def to_dict(self): - """Returns a dict of the object suitable for serialization""" + """Returns a dict of the object suitable for serialization.""" result = {} for key, value in self.__dict__.items(): if key.startswith("_"): diff --git a/geoip2/database.py b/geoip2/database.py index 13e7d88f..6c975a3d 100644 --- a/geoip2/database.py +++ b/geoip2/database.py @@ -8,7 +8,7 @@ import inspect import os from collections.abc import Sequence -from typing import IO, Any, AnyStr, Optional, Type, Union, cast +from typing import IO, Any, AnyStr, Optional, Union, cast import maxminddb from maxminddb import ( @@ -253,7 +253,7 @@ def _get(self, database_type: str, ip_address: IPAddress) -> Any: def _model_for( self, - model_class: Union[Type[Country], Type[Enterprise], Type[City]], + model_class: Union[type[Country], type[Enterprise], type[City]], types: str, ip_address: IPAddress, ) -> Union[Country, Enterprise, City]: @@ -268,11 +268,11 @@ def _model_for( def _flat_model_for( self, model_class: Union[ - Type[Domain], - Type[ISP], - Type[ConnectionType], - Type[ASN], - Type[AnonymousIP], + type[Domain], + type[ISP], + type[ConnectionType], + type[ASN], + type[AnonymousIP], ], types: str, ip_address: IPAddress, diff --git a/geoip2/errors.py b/geoip2/errors.py index e56d56c7..71bb57bc 100644 --- a/geoip2/errors.py +++ b/geoip2/errors.py @@ -53,7 +53,7 @@ def __init__( @property def network(self) -> Optional[Union[ipaddress.IPv4Network, ipaddress.IPv6Network]]: - """The network for the error""" + """The network for the error.""" if self.ip_address is None or self._prefix_len is None: return None return ipaddress.ip_network(f"{self.ip_address}/{self._prefix_len}", False) diff --git a/geoip2/models.py b/geoip2/models.py index a3d6310b..c66c53a9 100644 --- a/geoip2/models.py +++ b/geoip2/models.py @@ -15,7 +15,7 @@ import ipaddress from abc import ABCMeta from collections.abc import Sequence -from typing import Dict, List, Optional, Union +from typing import Optional, Union import geoip2.records from geoip2._internal import Model @@ -81,14 +81,14 @@ def __init__( self, locales: Optional[Sequence[str]], *, - continent: Optional[Dict] = None, - country: Optional[Dict] = None, + continent: Optional[dict] = None, + country: Optional[dict] = None, ip_address: Optional[IPAddress] = None, - maxmind: Optional[Dict] = None, + maxmind: Optional[dict] = None, prefix_len: Optional[int] = None, - registered_country: Optional[Dict] = None, - represented_country: Optional[Dict] = None, - traits: Optional[Dict] = None, + registered_country: Optional[dict] = None, + represented_country: Optional[dict] = None, + traits: Optional[dict] = None, **_, ) -> None: self._locales = locales @@ -200,18 +200,18 @@ def __init__( self, locales: Optional[Sequence[str]], *, - city: Optional[Dict] = None, - continent: Optional[Dict] = None, - country: Optional[Dict] = None, - location: Optional[Dict] = None, + city: Optional[dict] = None, + continent: Optional[dict] = None, + country: Optional[dict] = None, + location: Optional[dict] = None, ip_address: Optional[IPAddress] = None, - maxmind: Optional[Dict] = None, - postal: Optional[Dict] = None, + maxmind: Optional[dict] = None, + postal: Optional[dict] = None, prefix_len: Optional[int] = None, - registered_country: Optional[Dict] = None, - represented_country: Optional[Dict] = None, - subdivisions: Optional[List[Dict]] = None, - traits: Optional[Dict] = None, + registered_country: Optional[dict] = None, + represented_country: Optional[dict] = None, + subdivisions: Optional[list[dict]] = None, + traits: Optional[dict] = None, **_, ) -> None: super().__init__( @@ -360,7 +360,7 @@ class Enterprise(City): class SimpleModel(Model, metaclass=ABCMeta): - """Provides basic methods for non-location models""" + """Provides basic methods for non-location models.""" _ip_address: IPAddress _network: Optional[Union[ipaddress.IPv4Network, ipaddress.IPv6Network]] @@ -396,7 +396,7 @@ def __repr__(self) -> str: @property def ip_address(self): - """The IP address for the record""" + """The IP address for the record.""" if not isinstance( self._ip_address, (ipaddress.IPv4Address, ipaddress.IPv6Address), @@ -406,7 +406,7 @@ def ip_address(self): @property def network(self) -> Optional[Union[ipaddress.IPv4Network, ipaddress.IPv6Network]]: - """The network for the record""" + """The network for the record.""" # This code is duplicated for performance reasons network = self._network if network is not None: diff --git a/geoip2/records.py b/geoip2/records.py index 985f3b7b..5ed0b52f 100644 --- a/geoip2/records.py +++ b/geoip2/records.py @@ -12,7 +12,7 @@ # pylint:disable=R0903 from abc import ABCMeta from collections.abc import Sequence -from typing import Dict, Optional, Type, Union +from typing import Optional, Union from geoip2._internal import Model @@ -28,13 +28,13 @@ def __repr__(self) -> str: class PlaceRecord(Record, metaclass=ABCMeta): """All records with :py:attr:`names` subclass :py:class:`PlaceRecord`.""" - names: Dict[str, str] + names: dict[str, str] _locales: Sequence[str] def __init__( self, locales: Optional[Sequence[str]], - names: Optional[Dict[str, str]], + names: Optional[dict[str, str]], ) -> None: if locales is None: locales = ["en"] @@ -98,7 +98,7 @@ def __init__( *, confidence: Optional[int] = None, geoname_id: Optional[int] = None, - names: Optional[Dict[str, str]] = None, + names: Optional[dict[str, str]] = None, **_, ) -> None: self.confidence = confidence @@ -152,7 +152,7 @@ def __init__( *, code: Optional[str] = None, geoname_id: Optional[int] = None, - names: Optional[Dict[str, str]] = None, + names: Optional[dict[str, str]] = None, **_, ) -> None: self.code = code @@ -224,7 +224,7 @@ def __init__( geoname_id: Optional[int] = None, is_in_european_union: bool = False, iso_code: Optional[str] = None, - names: Optional[Dict[str, str]] = None, + names: Optional[dict[str, str]] = None, **_, ) -> None: self.confidence = confidence @@ -305,7 +305,7 @@ def __init__( geoname_id: Optional[int] = None, is_in_european_union: bool = False, iso_code: Optional[str] = None, - names: Optional[Dict[str, str]] = None, + names: Optional[dict[str, str]] = None, # pylint:disable=redefined-builtin type: Optional[str] = None, **_, @@ -536,7 +536,7 @@ def __init__( confidence: Optional[int] = None, geoname_id: Optional[int] = None, iso_code: Optional[str] = None, - names: Optional[Dict[str, str]] = None, + names: Optional[dict[str, str]] = None, **_, ) -> None: self.confidence = confidence @@ -558,13 +558,12 @@ class Subdivisions(tuple): """ def __new__( - cls: Type["Subdivisions"], + cls: type["Subdivisions"], locales: Optional[Sequence[str]], *subdivisions, ) -> "Subdivisions": subobjs = tuple(Subdivision(locales, **x) for x in subdivisions) - obj = super().__new__(cls, subobjs) # type: ignore - return obj + return super().__new__(cls, subobjs) # type: ignore def __init__( self, @@ -925,7 +924,7 @@ def __init__( @property def ip_address(self): - """The IP address for the record""" + """The IP address for the record.""" if not isinstance( self._ip_address, (ipaddress.IPv4Address, ipaddress.IPv6Address), @@ -935,7 +934,7 @@ def ip_address(self): @property def network(self) -> Optional[Union[ipaddress.IPv4Network, ipaddress.IPv6Network]]: - """The network for the record""" + """The network for the record.""" # This code is duplicated for performance reasons network = self._network if network is not None: diff --git a/geoip2/types.py b/geoip2/types.py index ba6d2b52..d86f1c0e 100644 --- a/geoip2/types.py +++ b/geoip2/types.py @@ -1,4 +1,4 @@ -"""Provides types used internally""" +"""Provides types used internally.""" from ipaddress import IPv4Address, IPv6Address from typing import Union diff --git a/geoip2/webservice.py b/geoip2/webservice.py index 1abbd33c..7b1a9e1b 100644 --- a/geoip2/webservice.py +++ b/geoip2/webservice.py @@ -28,7 +28,7 @@ import ipaddress import json from collections.abc import Sequence -from typing import Any, Dict, Optional, Type, Union, cast +from typing import Any, Optional, Union, cast import aiohttp import aiohttp.http @@ -358,7 +358,7 @@ async def _session(self) -> aiohttp.ClientSession: async def _response_for( self, path: str, - model_class: Union[Type[Insights], Type[City], Type[Country]], + model_class: Union[type[Insights], type[City], type[Country]], ip_address: IPAddress, ) -> Union[Country, City, Insights]: uri = self._uri(path, ip_address) @@ -372,8 +372,8 @@ async def _response_for( decoded_body = self._handle_success(body, uri) return model_class(self._locales, **decoded_body) - async def close(self): - """Close underlying session + async def close(self) -> None: + """Close underlying session. This will close the session and any associated connections. """ @@ -441,7 +441,7 @@ class Client(BaseClient): """ _session: requests.Session - _proxies: Optional[Dict[str, str]] + _proxies: Optional[dict[str, str]] def __init__( # pylint: disable=too-many-arguments,too-many-positional-arguments self, @@ -510,7 +510,7 @@ def insights(self, ip_address: IPAddress = "me") -> Insights: def _response_for( self, path: str, - model_class: Union[Type[Insights], Type[City], Type[Country]], + model_class: Union[type[Insights], type[City], type[Country]], ip_address: IPAddress, ) -> Union[Country, City, Insights]: uri = self._uri(path, ip_address) @@ -523,8 +523,8 @@ def _response_for( decoded_body = self._handle_success(body, uri) return model_class(self._locales, **decoded_body) - def close(self): - """Close underlying session + def close(self) -> None: + """Close underlying session. This will close the session and any associated connections. """ diff --git a/tests/database_test.py b/tests/database_test.py index 672aafd4..9f6daed4 100644 --- a/tests/database_test.py +++ b/tests/database_test.py @@ -283,5 +283,5 @@ def test_modes(self, mock_open) -> None: with geoip2.database.Reader( path, mode=geoip2.database.MODE_MMAP_EXT, - ) as reader: + ): mock_open.assert_called_once_with(path, geoip2.database.MODE_MMAP_EXT) diff --git a/tests/models_test.py b/tests/models_test.py index dadd6b02..58bff6e8 100644 --- a/tests/models_test.py +++ b/tests/models_test.py @@ -4,7 +4,6 @@ import ipaddress import sys import unittest -from typing import Dict sys.path.append("..") @@ -12,7 +11,7 @@ class TestModels(unittest.TestCase): - def setUp(self): + def setUp(self) -> None: self.maxDiff = 20_000 def test_insights_full(self) -> None: @@ -447,7 +446,7 @@ def test_unknown_keys(self) -> None: class TestNames(unittest.TestCase): - raw: Dict = { + raw: dict = { "continent": { "code": "NA", "geoname_id": 42, diff --git a/tests/webservice_test.py b/tests/webservice_test.py index d19a64a9..5230403b 100644 --- a/tests/webservice_test.py +++ b/tests/webservice_test.py @@ -6,7 +6,7 @@ import sys import unittest from collections import defaultdict -from typing import Dict, cast +from typing import cast import pytest import pytest_httpserver @@ -50,7 +50,7 @@ class TestBaseClient(unittest.TestCase): # this is not a comprehensive representation of the # JSON from the server - insights = cast(Dict, copy.deepcopy(country)) + insights = cast(dict, copy.deepcopy(country)) insights["traits"]["user_count"] = 2 insights["traits"]["static_ip_score"] = 1.3 @@ -62,10 +62,10 @@ def _content_type(self, endpoint): ) @pytest.fixture(autouse=True) - def setup_httpserver(self, httpserver: pytest_httpserver.HTTPServer): + def setup_httpserver(self, httpserver: pytest_httpserver.HTTPServer) -> None: self.httpserver = httpserver - def test_country_ok(self): + def test_country_ok(self) -> None: self.httpserver.expect_request( "/geoip/v2.1/country/1.2.3.4", method="GET", @@ -122,7 +122,7 @@ def test_country_ok(self): self.assertTrue(country.traits.is_anycast) self.assertEqual(country.to_dict(), self.country, "raw response is correct") - def test_me(self): + def test_me(self) -> None: self.httpserver.expect_request( "/geoip/v2.1/country/me", method="GET", @@ -144,7 +144,7 @@ def test_me(self): "country('me') returns Country object", ) - def test_200_error(self): + def test_200_error(self) -> None: self.httpserver.expect_request( "/geoip/v2.1/country/1.1.1.1", method="GET", @@ -160,14 +160,14 @@ def test_200_error(self): ): self.run_client(self.client.country("1.1.1.1")) - def test_bad_ip_address(self): + def test_bad_ip_address(self) -> None: with self.assertRaisesRegex( ValueError, "'1.2.3' does not appear to be an IPv4 or IPv6 address", ): self.run_client(self.client.country("1.2.3")) - def test_no_body_error(self): + def test_no_body_error(self) -> None: self.httpserver.expect_request( "/geoip/v2.1/country/1.2.3.7", method="GET", @@ -182,7 +182,7 @@ def test_no_body_error(self): ): self.run_client(self.client.country("1.2.3.7")) - def test_weird_body_error(self): + def test_weird_body_error(self) -> None: self.httpserver.expect_request( "/geoip/v2.1/country/1.2.3.8", method="GET", @@ -198,7 +198,7 @@ def test_weird_body_error(self): ): self.run_client(self.client.country("1.2.3.8")) - def test_bad_body_error(self): + def test_bad_body_error(self) -> None: self.httpserver.expect_request( "/geoip/v2.1/country/1.2.3.9", method="GET", @@ -213,7 +213,7 @@ def test_bad_body_error(self): ): self.run_client(self.client.country("1.2.3.9")) - def test_500_error(self): + def test_500_error(self) -> None: self.httpserver.expect_request( "/geoip/v2.1/country/1.2.3.10", method="GET", @@ -225,7 +225,7 @@ def test_500_error(self): with self.assertRaisesRegex(HTTPError, r"Received a server error \(500\) for"): self.run_client(self.client.country("1.2.3.10")) - def test_300_error(self): + def test_300_error(self) -> None: self.httpserver.expect_request( "/geoip/v2.1/country/1.2.3.11", method="GET", @@ -240,40 +240,40 @@ def test_300_error(self): ): self.run_client(self.client.country("1.2.3.11")) - def test_ip_address_required(self): + def test_ip_address_required(self) -> None: self._test_error(400, "IP_ADDRESS_REQUIRED", InvalidRequestError) - def test_ip_address_not_found(self): + def test_ip_address_not_found(self) -> None: self._test_error(404, "IP_ADDRESS_NOT_FOUND", AddressNotFoundError) - def test_ip_address_reserved(self): + def test_ip_address_reserved(self) -> None: self._test_error(400, "IP_ADDRESS_RESERVED", AddressNotFoundError) - def test_permission_required(self): + def test_permission_required(self) -> None: self._test_error(403, "PERMISSION_REQUIRED", PermissionRequiredError) - def test_auth_invalid(self): + def test_auth_invalid(self) -> None: self._test_error(400, "AUTHORIZATION_INVALID", AuthenticationError) - def test_license_key_required(self): + def test_license_key_required(self) -> None: self._test_error(401, "LICENSE_KEY_REQUIRED", AuthenticationError) - def test_account_id_required(self): + def test_account_id_required(self) -> None: self._test_error(401, "ACCOUNT_ID_REQUIRED", AuthenticationError) - def test_user_id_required(self): + def test_user_id_required(self) -> None: self._test_error(401, "USER_ID_REQUIRED", AuthenticationError) - def test_account_id_unkown(self): + def test_account_id_unkown(self) -> None: self._test_error(401, "ACCOUNT_ID_UNKNOWN", AuthenticationError) - def test_user_id_unkown(self): + def test_user_id_unkown(self) -> None: self._test_error(401, "USER_ID_UNKNOWN", AuthenticationError) - def test_out_of_queries_error(self): + def test_out_of_queries_error(self) -> None: self._test_error(402, "OUT_OF_QUERIES", OutOfQueriesError) - def _test_error(self, status, error_code, error_class): + def _test_error(self, status, error_code, error_class) -> None: msg = "Some error message" body = {"error": msg, "code": error_code} self.httpserver.expect_request( @@ -284,10 +284,10 @@ def _test_error(self, status, error_code, error_class): status=status, content_type=self._content_type("country"), ) - with self.assertRaisesRegex(error_class, msg): + with pytest.raises(error_class, match=msg): self.run_client(self.client.country("1.2.3.18")) - def test_unknown_error(self): + def test_unknown_error(self) -> None: msg = "Unknown error type" ip = "1.2.3.19" body = {"error": msg, "code": "UNKNOWN_TYPE"} @@ -299,10 +299,10 @@ def test_unknown_error(self): status=400, content_type=self._content_type("country"), ) - with self.assertRaisesRegex(InvalidRequestError, msg): + with pytest.raises(InvalidRequestError, match=msg): self.run_client(self.client.country(ip)) - def test_request(self): + def test_request(self) -> None: def user_agent_compare(actual: str, expected: str) -> bool: if actual is None: return False @@ -329,7 +329,7 @@ def user_agent_compare(actual: str, expected: str) -> bool: ) self.run_client(self.client.country("1.2.3.4")) - def test_city_ok(self): + def test_city_ok(self) -> None: self.httpserver.expect_request( "/geoip/v2.1/city/1.2.3.4", method="GET", @@ -347,7 +347,7 @@ def test_city_ok(self): ) self.assertTrue(city.traits.is_anycast) - def test_insights_ok(self): + def test_insights_ok(self) -> None: self.httpserver.expect_request( "/geoip/v2.1/insights/1.2.3.4", method="GET", @@ -371,14 +371,14 @@ def test_insights_ok(self): self.assertEqual(insights.traits.static_ip_score, 1.3, "static_ip_score is 1.3") self.assertEqual(insights.traits.user_count, 2, "user_count is 2") - def test_named_constructor_args(self): + def test_named_constructor_args(self) -> None: id = 47 key = "1234567890ab" client = self.client_class(account_id=id, license_key=key) self.assertEqual(client._account_id, str(id)) self.assertEqual(client._license_key, key) - def test_missing_constructor_args(self): + def test_missing_constructor_args(self) -> None: with self.assertRaises(TypeError): self.client_class(license_key="1234567890ab") @@ -387,7 +387,7 @@ def test_missing_constructor_args(self): class TestClient(TestBaseClient): - def setUp(self): + def setUp(self) -> None: self.client_class = Client self.client = Client(42, "abcdef123456") self.client._base_uri = self.httpserver.url_for("/geoip/v2.1") @@ -398,14 +398,14 @@ def run_client(self, v): class TestAsyncClient(TestBaseClient): - def setUp(self): + def setUp(self) -> None: self._loop = asyncio.new_event_loop() self.client_class = AsyncClient self.client = AsyncClient(42, "abcdef123456") self.client._base_uri = self.httpserver.url_for("/geoip/v2.1") self.maxDiff = 20_000 - def tearDown(self): + def tearDown(self) -> None: self._loop.run_until_complete(self.client.close()) self._loop.close() From a88e4afc6f70ca58b83c8c58d90ed4b0e4a7127f Mon Sep 17 00:00:00 2001 From: Gregory Oschwald Date: Tue, 28 Jan 2025 12:40:10 -0800 Subject: [PATCH 03/10] Start adding ruff config We still have a lot of failures, but given that we aren't adding a CI check that seems fine. --- pyproject.toml | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index d55b8924..bcce8b28 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,6 +39,33 @@ test = [ "pytest-httpserver>=1.0.10", ] +[tool.ruff.lint] +select = ["ALL"] +ignore = [ + # Skip type annotation on **_ + "ANN003", + + # documenting magic methods + "D105", + + # Line length. We let black handle this for now. + "E501", + + # Don't bother with future imports for type annotations + "FA100", + + # Magic numbers for HTTP status codes seem ok most of the time. + "PLR2004", + + # pytest rules + "PT009", + "PT027", +] + +[tool.ruff.lint.per-file-ignores] +"geoip2/{models,records}.py" = [ "D107", "PLR0913" ] +"tests/*" = ["ANN201", "D"] + [tool.setuptools.package-data] geoip2 = ["py.typed"] From fb38ff629cff5e11b5b9b6c20ae23aa586840e99 Mon Sep 17 00:00:00 2001 From: Gregory Oschwald Date: Tue, 28 Jan 2025 13:14:10 -0800 Subject: [PATCH 04/10] Fix some mypy issues in test code --- tests/webservice_test.py | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/tests/webservice_test.py b/tests/webservice_test.py index 5230403b..c17f86b8 100644 --- a/tests/webservice_test.py +++ b/tests/webservice_test.py @@ -5,8 +5,9 @@ import ipaddress import sys import unittest +from abc import ABC, abstractmethod from collections import defaultdict -from typing import cast +from typing import cast, Callable, Union import pytest import pytest_httpserver @@ -26,7 +27,10 @@ from geoip2.webservice import AsyncClient, Client -class TestBaseClient(unittest.TestCase): +class TestBaseClient(unittest.TestCase, ABC): + client: Union[AsyncClient, Client] + client_class: Callable[[int, str], Union[AsyncClient, Client]] + country = { "continent": {"code": "NA", "geoname_id": 42, "names": {"en": "North America"}}, "country": { @@ -54,6 +58,9 @@ class TestBaseClient(unittest.TestCase): insights["traits"]["user_count"] = 2 insights["traits"]["static_ip_score"] = 1.3 + @abstractmethod + def run_client(self, v): ... + def _content_type(self, endpoint): return ( "application/vnd.maxmind.com-" @@ -319,7 +326,7 @@ def user_agent_compare(actual: str, expected: str) -> bool: header_value_matcher=HeaderValueMatcher( defaultdict( lambda: HeaderValueMatcher.default_header_value_matcher, - {"User-Agent": user_agent_compare}, + {"User-Agent": user_agent_compare}, # type: ignore[dict-item] ), ), ).respond_with_json( @@ -374,19 +381,22 @@ def test_insights_ok(self) -> None: def test_named_constructor_args(self) -> None: id = 47 key = "1234567890ab" - client = self.client_class(account_id=id, license_key=key) + client = self.client_class(id, key) self.assertEqual(client._account_id, str(id)) self.assertEqual(client._license_key, key) def test_missing_constructor_args(self) -> None: with self.assertRaises(TypeError): - self.client_class(license_key="1234567890ab") + + self.client_class(license_key="1234567890ab") # type: ignore[call-arg] with self.assertRaises(TypeError): - self.client_class("47") + self.client_class("47") # type: ignore class TestClient(TestBaseClient): + client: Client + def setUp(self) -> None: self.client_class = Client self.client = Client(42, "abcdef123456") @@ -398,6 +408,8 @@ def run_client(self, v): class TestAsyncClient(TestBaseClient): + client: AsyncClient + def setUp(self) -> None: self._loop = asyncio.new_event_loop() self.client_class = AsyncClient From d0da1633911a3ca35e87e93360ed24976070cd72 Mon Sep 17 00:00:00 2001 From: Gregory Oschwald Date: Tue, 28 Jan 2025 13:18:29 -0800 Subject: [PATCH 05/10] Set release date --- HISTORY.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HISTORY.rst b/HISTORY.rst index 3dd2f5a6..2654c8bb 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -4,7 +4,7 @@ History ------- -5.0.0 +5.0.0 (2025-01-28) ++++++++++++++++++ * BREAKING: The ``raw`` attribute on the model classes has been replaced From eda1f07f8ef849add9d8e157c7cbf5483fd666db Mon Sep 17 00:00:00 2001 From: Gregory Oschwald Date: Tue, 28 Jan 2025 13:35:35 -0800 Subject: [PATCH 06/10] Improve type hints --- geoip2/_internal.py | 4 ++-- geoip2/models.py | 8 +++----- geoip2/records.py | 13 +++++++------ 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/geoip2/_internal.py b/geoip2/_internal.py index 27ce459a..e1970c7e 100644 --- a/geoip2/_internal.py +++ b/geoip2/_internal.py @@ -10,11 +10,11 @@ class Model(metaclass=ABCMeta): def __eq__(self, other: object) -> bool: return isinstance(other, self.__class__) and self.to_dict() == other.to_dict() - def __ne__(self, other): + def __ne__(self, other) -> bool: return not self.__eq__(other) # pylint: disable=too-many-branches - def to_dict(self): + def to_dict(self) -> dict: """Returns a dict of the object suitable for serialization.""" result = {} for key, value in self.__dict__.items(): diff --git a/geoip2/models.py b/geoip2/models.py index c66c53a9..3f12654d 100644 --- a/geoip2/models.py +++ b/geoip2/models.py @@ -16,6 +16,7 @@ from abc import ABCMeta from collections.abc import Sequence from typing import Optional, Union +from ipaddress import IPv4Address, IPv6Address import geoip2.records from geoip2._internal import Model @@ -395,12 +396,9 @@ def __repr__(self) -> str: ) @property - def ip_address(self): + def ip_address(self) -> Union[IPv4Address, IPv6Address]: """The IP address for the record.""" - if not isinstance( - self._ip_address, - (ipaddress.IPv4Address, ipaddress.IPv6Address), - ): + if not isinstance(self._ip_address, (IPv4Address, IPv6Address)): self._ip_address = ipaddress.ip_address(self._ip_address) return self._ip_address diff --git a/geoip2/records.py b/geoip2/records.py index 5ed0b52f..0c3e90ff 100644 --- a/geoip2/records.py +++ b/geoip2/records.py @@ -12,9 +12,11 @@ # pylint:disable=R0903 from abc import ABCMeta from collections.abc import Sequence +from ipaddress import IPv4Address, IPv6Address from typing import Optional, Union from geoip2._internal import Model +from geoip2.types import IPAddress class Record(Model, metaclass=ABCMeta): @@ -841,7 +843,7 @@ class Traits(Record): autonomous_system_organization: Optional[str] connection_type: Optional[str] domain: Optional[str] - _ip_address: Optional[str] + _ip_address: IPAddress is_anonymous: bool is_anonymous_proxy: bool is_anonymous_vpn: bool @@ -912,6 +914,8 @@ def __init__( self.static_ip_score = static_ip_score self.user_type = user_type self.user_count = user_count + if ip_address is None: + raise TypeError("ip_address must be defined") self._ip_address = ip_address if network is None: self._network = None @@ -923,12 +927,9 @@ def __init__( self._prefix_len = prefix_len @property - def ip_address(self): + def ip_address(self) -> Union[IPv4Address, IPv6Address]: """The IP address for the record.""" - if not isinstance( - self._ip_address, - (ipaddress.IPv4Address, ipaddress.IPv6Address), - ): + if not isinstance(self._ip_address, (IPv4Address, IPv6Address)): self._ip_address = ipaddress.ip_address(self._ip_address) return self._ip_address From 056787f70bf4d406c56a2ace57dd3598dbeec6a5 Mon Sep 17 00:00:00 2001 From: Gregory Oschwald Date: Tue, 28 Jan 2025 13:47:45 -0800 Subject: [PATCH 07/10] Update for v5.0.0 --- geoip2/__init__.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/geoip2/__init__.py b/geoip2/__init__.py index 43c322a7..7f9adff0 100644 --- a/geoip2/__init__.py +++ b/geoip2/__init__.py @@ -1,7 +1,7 @@ # pylint:disable=C0111 __title__ = "geoip2" -__version__ = "4.8.1" +__version__ = "5.0.0" __author__ = "Gregory Oschwald" __license__ = "Apache License, Version 2.0" __copyright__ = "Copyright (c) 2013-2025 MaxMind, Inc." diff --git a/pyproject.toml b/pyproject.toml index bcce8b28..ee82234d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "geoip2" -version = "4.8.1" +version = "5.0.0" description = "MaxMind GeoIP2 API" authors = [ {name = "Gregory Oschwald", email = "goschwald@maxmind.com"}, From 28c3b12f9f8b23f99ae0fe2179c468f7dcc39cba Mon Sep 17 00:00:00 2001 From: Gregory Oschwald Date: Tue, 28 Jan 2025 15:01:11 -0800 Subject: [PATCH 08/10] Allow Traits ip_address to be None again --- HISTORY.rst | 6 ++++++ geoip2/records.py | 17 ++++++++++------- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 2654c8bb..cac8b4f1 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -4,6 +4,12 @@ History ------- +5.0.1 (2025-01-28) +++++++++++++++++++ + +* Allow ``ip_address`` in the ``Traits`` record to be ``None`` again. The + primary use case for this is from the ``minfraud`` package. + 5.0.0 (2025-01-28) ++++++++++++++++++ diff --git a/geoip2/records.py b/geoip2/records.py index 0c3e90ff..f8ed3992 100644 --- a/geoip2/records.py +++ b/geoip2/records.py @@ -843,7 +843,7 @@ class Traits(Record): autonomous_system_organization: Optional[str] connection_type: Optional[str] domain: Optional[str] - _ip_address: IPAddress + _ip_address: Optional[IPAddress] is_anonymous: bool is_anonymous_proxy: bool is_anonymous_vpn: bool @@ -914,8 +914,6 @@ def __init__( self.static_ip_score = static_ip_score self.user_type = user_type self.user_count = user_count - if ip_address is None: - raise TypeError("ip_address must be defined") self._ip_address = ip_address if network is None: self._network = None @@ -927,11 +925,16 @@ def __init__( self._prefix_len = prefix_len @property - def ip_address(self) -> Union[IPv4Address, IPv6Address]: + def ip_address(self) -> Optional[Union[IPv4Address, IPv6Address]]: """The IP address for the record.""" - if not isinstance(self._ip_address, (IPv4Address, IPv6Address)): - self._ip_address = ipaddress.ip_address(self._ip_address) - return self._ip_address + ip_address = self._ip_address + if ip_address is None: + return None + + if not isinstance(ip_address, (IPv4Address, IPv6Address)): + ip_address = ipaddress.ip_address(ip_address) + self._ip_address = ip_address + return ip_address @property def network(self) -> Optional[Union[ipaddress.IPv4Network, ipaddress.IPv6Network]]: From 7615adbc939246aca7e6a007ee1bd0f568ce7d9a Mon Sep 17 00:00:00 2001 From: Gregory Oschwald Date: Tue, 28 Jan 2025 15:08:57 -0800 Subject: [PATCH 09/10] Update ip_address types in docs It was incorrect. Longer term, it may make sense to drop the type info in the docs given that we have type hints in t he code. --- geoip2/models.py | 10 +++++----- geoip2/records.py | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/geoip2/models.py b/geoip2/models.py index 3f12654d..dd738bbf 100644 --- a/geoip2/models.py +++ b/geoip2/models.py @@ -471,7 +471,7 @@ class AnonymousIP(SimpleModel): The IP address used in the lookup. - :type: str + :type: ipaddress.IPv4Address or ipaddress.IPv6Address .. attribute:: network @@ -534,7 +534,7 @@ class ASN(SimpleModel): The IP address used in the lookup. - :type: str + :type: ipaddress.IPv4Address or ipaddress.IPv6Address .. attribute:: network @@ -587,7 +587,7 @@ class ConnectionType(SimpleModel): The IP address used in the lookup. - :type: str + :type: ipaddress.IPv4Address or ipaddress.IPv6Address .. attribute:: network @@ -628,7 +628,7 @@ class Domain(SimpleModel): The IP address used in the lookup. - :type: str + :type: ipaddress.IPv4Address or ipaddress.IPv6Address .. attribute:: network @@ -705,7 +705,7 @@ class ISP(ASN): The IP address used in the lookup. - :type: str + :type: ipaddress.IPv4Address or ipaddress.IPv6Address .. attribute:: network diff --git a/geoip2/records.py b/geoip2/records.py index f8ed3992..155e20ea 100644 --- a/geoip2/records.py +++ b/geoip2/records.py @@ -651,7 +651,7 @@ class Traits(Record): running on. If the system is behind a NAT, this may differ from the IP address locally assigned to it. - :type: str + :type: ipaddress.IPv4Address or ipaddress.IPv6Address .. attribute:: is_anonymous From 5df245db67f1677c76460862315bca0f1eadf122 Mon Sep 17 00:00:00 2001 From: Gregory Oschwald Date: Tue, 28 Jan 2025 15:20:20 -0800 Subject: [PATCH 10/10] Update for v5.0.1 --- geoip2/__init__.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/geoip2/__init__.py b/geoip2/__init__.py index 7f9adff0..2b13eaf7 100644 --- a/geoip2/__init__.py +++ b/geoip2/__init__.py @@ -1,7 +1,7 @@ # pylint:disable=C0111 __title__ = "geoip2" -__version__ = "5.0.0" +__version__ = "5.0.1" __author__ = "Gregory Oschwald" __license__ = "Apache License, Version 2.0" __copyright__ = "Copyright (c) 2013-2025 MaxMind, Inc." diff --git a/pyproject.toml b/pyproject.toml index ee82234d..b77899cc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "geoip2" -version = "5.0.0" +version = "5.0.1" description = "MaxMind GeoIP2 API" authors = [ {name = "Gregory Oschwald", email = "goschwald@maxmind.com"},