generated from Hazel/python-project
Merge pull request 'config_default_behaviour' (#1) from config_default_behaviour into main
Reviewed-on: #1
This commit is contained in:
commit
a109f7f1f9
30
README.md
30
README.md
@ -28,7 +28,7 @@ except ValueError:
|
||||
|
||||
### Creating country class
|
||||
|
||||
You can call create an instance of `pycountry_wrapper.Country` in multiple slightly different ways.
|
||||
You can call create an instance of `Country` in multiple slightly different ways.
|
||||
|
||||
The [**ISO 3166-1**](https://en.wikipedia.org/wiki/ISO_3166-1) standart can either use 2 or 3 letters (alpha_2 or alpha_3).
|
||||
|
||||
@ -44,15 +44,20 @@ Country.from_alpha_2("DE")
|
||||
Country.from_alpha_3("DEU")
|
||||
```
|
||||
|
||||
If you want to use fuzzy search, you will have to use `Country.from_fuzzy()`.
|
||||
If the country can't be found it will raise a `EmptyCountryException` or use the fallback defined in `config.fallback_country`.
|
||||
|
||||
Alternatively you can get an instance of `Country` by using `Country.search`. This will return `None` if no country was found.
|
||||
|
||||
I also implemented a null-object pattern of `Country`, meaning you can get an `EmptyCountry` object. If you create a country from this object you'll get an instance of `Country` if it was found, and an instance of `EmptyCountry` if it wasn't.
|
||||
|
||||
```python
|
||||
from pycountry_wrapper import Country
|
||||
empty = EmptyCountry("InvalidCountry")
|
||||
print(type(empty)) # <class 'pycountry_wrapper.country.EmptyCountry'>
|
||||
|
||||
Country.from_fuzzy("Deutschland")
|
||||
found = EmptyCountry("US")
|
||||
print(type(found)) # <class 'pycountry_wrapper.country.Country'>
|
||||
```
|
||||
|
||||
|
||||
### Accessing information
|
||||
|
||||
There are only a handful (readonly) attributes.
|
||||
@ -60,10 +65,23 @@ There are only a handful (readonly) attributes.
|
||||
```python
|
||||
from pycountry_wrapper import Country
|
||||
|
||||
country = Country.from_alpha_2("DE")
|
||||
country = Country("DE")
|
||||
|
||||
country.name
|
||||
country.alpha_2
|
||||
country.alpha_3
|
||||
country.official_name
|
||||
```
|
||||
|
||||
If you have an `EmptyCountry` these attributes will all be `None`.
|
||||
|
||||
### Configuring behavior
|
||||
|
||||
If you want to set a fallback country or disable fuzzy search you can do that with the config module.
|
||||
|
||||
```python
|
||||
from pycountry_wrapper import config
|
||||
|
||||
config.fallback_country = "US"
|
||||
config.allow_fuzzy_search = False
|
||||
```
|
||||
|
@ -1,2 +0,0 @@
|
||||
__version__ = "0.0.4"
|
||||
__name__ = "pycountry_wrapper"
|
@ -1,118 +1,10 @@
|
||||
from __future__ import annotations
|
||||
from typing import Optional
|
||||
from functools import wraps
|
||||
import pycountry
|
||||
from .country import Country, EmptyCountry, EmptyCountryException
|
||||
from . import config
|
||||
|
||||
|
||||
def none_if_empty(func):
|
||||
@wraps(func)
|
||||
def wrapper(self, *args, **kwargs):
|
||||
if self.is_empty:
|
||||
return None
|
||||
|
||||
return func(self, *args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
class Country:
|
||||
"""
|
||||
This gets countries based on the ISO 3166-1 standart.
|
||||
|
||||
Two examples are:
|
||||
- Country.from_alpha_2("DE")
|
||||
- Country.from_alpha_3("DEU")
|
||||
|
||||
If the country couldn't be found, it raises a ValueError, or creates an empty object.
|
||||
Empty objects return for every attribute None
|
||||
"""
|
||||
|
||||
def __init__(self, country: Optional[str] = None, pycountry_object = None, allow_empty: bool = True) -> None:
|
||||
if country is not None:
|
||||
# auto detect if alpha_2 or alpha_3
|
||||
if len(country) == 2:
|
||||
pycountry_object = pycountry.countries.get(alpha_2=country.upper())
|
||||
elif len(country) == 3:
|
||||
pycountry_object = pycountry.countries.get(alpha_3=country.upper())
|
||||
|
||||
if pycountry_object is None and not allow_empty:
|
||||
raise ValueError(f"Country {country} couldn't be found")
|
||||
|
||||
self.pycountry_object = pycountry_object
|
||||
|
||||
@classmethod
|
||||
def from_alpha_2(cls, alpha_2: str) -> Country:
|
||||
return cls(pycountry_object=pycountry.countries.get(alpha_2=alpha_2.upper()))
|
||||
|
||||
@classmethod
|
||||
def from_alpha_3(cls, alpha_3: str) -> Country:
|
||||
return cls(pycountry_object=pycountry.countries.get(alpha_3=alpha_3.upper()))
|
||||
|
||||
@classmethod
|
||||
def from_fuzzy(cls, fuzzy: str) -> Country:
|
||||
return cls(pycountry_object=pycountry.countries.search_fuzzy(fuzzy))
|
||||
|
||||
@property
|
||||
def is_empty(self) -> bool:
|
||||
return self.pycountry_object is None
|
||||
|
||||
@property
|
||||
@none_if_empty
|
||||
def name(self) -> Optional[str]:
|
||||
return self.pycountry_object.name
|
||||
|
||||
@property
|
||||
@none_if_empty
|
||||
def alpha_2(self) -> Optional[str]:
|
||||
return self.pycountry_object.alpha_2
|
||||
|
||||
@property
|
||||
@none_if_empty
|
||||
def alpha_3(self) -> Optional[str]:
|
||||
return self.pycountry_object.alpha_3
|
||||
|
||||
@property
|
||||
@none_if_empty
|
||||
def numeric(self) -> Optional[str]:
|
||||
return self.pycountry_object.numeric
|
||||
|
||||
@property
|
||||
@none_if_empty
|
||||
def official_name(self) -> Optional[str]:
|
||||
return self.pycountry_object.official_name
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.pycountry_object.__str__()
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return self.pycountry_object.__repr__()
|
||||
|
||||
|
||||
class StrictCountry(Country):
|
||||
"""
|
||||
This works just like Country,
|
||||
but the object cant be empty
|
||||
"""
|
||||
|
||||
def __init__(self, country: Optional[str] = None, pycountry_object = None) -> None:
|
||||
super().__init__(country=country, pycountry_object=pycountry_object, allow_empty=False)
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return self.pycountry_object.name
|
||||
|
||||
@property
|
||||
def alpha_2(self) -> str:
|
||||
return self.pycountry_object.alpha_2
|
||||
|
||||
@property
|
||||
def alpha_3(self) -> str:
|
||||
return self.pycountry_object.alpha_3
|
||||
|
||||
@property
|
||||
def numeric(self) -> str:
|
||||
return self.pycountry_object.numeric
|
||||
|
||||
@property
|
||||
def official_name(self) -> str:
|
||||
return self.pycountry_object.official_name
|
||||
__name__ = "pycountry_wrapper"
|
||||
__all__ = [
|
||||
"Country",
|
||||
"EmptyCountry",
|
||||
"config",
|
||||
"EmptyCountryException",
|
||||
]
|
||||
|
@ -1,8 +1,28 @@
|
||||
from .__about__ import __name__, __version__
|
||||
from . import Country
|
||||
from . import __name__
|
||||
from . import Country, EmptyCountry, config
|
||||
|
||||
|
||||
def cli():
|
||||
print(f"Running {__name__} version {__version__} from __main__.py")
|
||||
print(f"Running {__name__} from __main__.py")
|
||||
import pycountry
|
||||
config.fallback_country = "US"
|
||||
t = pycountry.countries.get(alpha_2="DE")
|
||||
|
||||
print(Country(country="DE").name)
|
||||
country = EmptyCountry(pycountry_object=t)
|
||||
print(type(country))
|
||||
print(country)
|
||||
|
||||
print()
|
||||
empty_country = EmptyCountry(country="zwx")
|
||||
print(type(empty_country))
|
||||
print(empty_country)
|
||||
|
||||
print()
|
||||
normal_country = Country("UK")
|
||||
print(type(normal_country))
|
||||
print(normal_country)
|
||||
|
||||
print()
|
||||
fallback_country = Country("zwx")
|
||||
print(type(fallback_country))
|
||||
print(fallback_country)
|
8
pycountry_wrapper/config.py
Normal file
8
pycountry_wrapper/config.py
Normal file
@ -0,0 +1,8 @@
|
||||
import typing as _t
|
||||
|
||||
# defines the fallback country if a country can't be found
|
||||
# alpha_2 or alpha_3 of ISO 3166-1
|
||||
fallback_country: _t.Optional[str] = None
|
||||
|
||||
# should use fuzzy search if it cant find the country with alpha_2 or alpha_3
|
||||
allow_fuzzy_search: bool = True
|
178
pycountry_wrapper/country.py
Normal file
178
pycountry_wrapper/country.py
Normal file
@ -0,0 +1,178 @@
|
||||
from __future__ import annotations
|
||||
from typing import Optional, Union
|
||||
from functools import wraps
|
||||
import pycountry
|
||||
import pycountry.db
|
||||
|
||||
from . import config
|
||||
|
||||
|
||||
class EmptyCountryException(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
class Country:
|
||||
"""
|
||||
A class representing a country based on the ISO 3166-1 standard, wrapping the pycountry library.
|
||||
|
||||
This class provides multiple ways to look up countries:
|
||||
- By 2-letter alpha-2 code (e.g., "DE" for Germany)
|
||||
- By 3-letter alpha-3 code (e.g., "DEU" for Germany)
|
||||
- By fuzzy search of country names
|
||||
- Directly from pycountry Country objects
|
||||
|
||||
The class supports optional fallback behavior when a country isn't found,
|
||||
configurable through the module's config settings.
|
||||
|
||||
Examples:
|
||||
>>> Country("Germany") # Automatic detection
|
||||
>>> Country.search("Germany")
|
||||
>>>
|
||||
>>> Country.from_alpha_2("DE") # Germany by alpha-2 code
|
||||
>>> Country.from_alpha_3("DEU") # Germany by alpha-3 code
|
||||
>>> Country.from_fuzzy("Germany") # Germany by name search
|
||||
|
||||
Raises:
|
||||
EmptyCountryException: If the country cannot be found and no fallback is configured.
|
||||
|
||||
If you don't want to raise an Exception if no country you can create a Country instance by the following methods:
|
||||
- Country.search: returns None if nothing is found
|
||||
- initialize EmptyCountry instead: gives you either a Country instance or an EmptyCountry instance
|
||||
"""
|
||||
def __init__(
|
||||
self,
|
||||
country: Optional[str] = None,
|
||||
pycountry_object: Optional[pycountry.db.Country] = None,
|
||||
disable_fallback: bool = False
|
||||
) -> None:
|
||||
if pycountry_object is None:
|
||||
# search for the country string instead if the pycountry_object isn't given
|
||||
# this also implements the optional fallback
|
||||
pycountry_object = self._search_pycountry_object(country=country, disable_fallback=disable_fallback)
|
||||
|
||||
if pycountry_object is None:
|
||||
raise EmptyCountryException(f"the country {country} was not found and config.fallback_country isn't set")
|
||||
|
||||
self.pycountry_object: pycountry.db.Country = pycountry_object
|
||||
|
||||
|
||||
@classmethod
|
||||
def _search_pycountry_object(cls, country: Optional[str], disable_fallback: bool = False) -> Optional[pycountry.db.Country]:
|
||||
pycountry_object = None
|
||||
|
||||
if country is not None:
|
||||
# the reason I don't immediately return the result is because then there would be a chance
|
||||
# I would return None even though a country could be found through fuzzy search
|
||||
country = country.strip()
|
||||
if len(country) == 2:
|
||||
pycountry_object = pycountry.countries.get(alpha_2=country.upper())
|
||||
elif len(country) == 3:
|
||||
pycountry_object = pycountry.countries.get(alpha_3=country.upper())
|
||||
if pycountry_object is not None:
|
||||
return pycountry_object
|
||||
|
||||
# fuzzy search if enabled
|
||||
if config.allow_fuzzy_search:
|
||||
# fuzzy search raises lookup error if nothing was found
|
||||
try:
|
||||
found_countries = pycountry.countries.search_fuzzy(country)
|
||||
if len(found_countries):
|
||||
return found_countries[0]
|
||||
except LookupError:
|
||||
pass
|
||||
|
||||
if pycountry_object is not None:
|
||||
return pycountry_object
|
||||
|
||||
if config.fallback_country is not None and not disable_fallback:
|
||||
return cls._search_pycountry_object(country=config.fallback_country, disable_fallback=True)
|
||||
|
||||
|
||||
@classmethod
|
||||
def search(cls, country: Optional[str]) -> Optional[Country]:
|
||||
"""
|
||||
Search for a country and return None instead of raising if not found.
|
||||
|
||||
Args:
|
||||
country: String to search for (name, alpha-2, or alpha-3 code)
|
||||
|
||||
Returns:
|
||||
Country object if found, None otherwise
|
||||
"""
|
||||
return cls(pycountry_object=cls._search_pycountry_object(country=country))
|
||||
|
||||
@classmethod
|
||||
def from_alpha_2(cls, alpha_2: str) -> Country:
|
||||
return cls(pycountry_object=pycountry.countries.get(alpha_2=alpha_2.upper()))
|
||||
|
||||
@classmethod
|
||||
def from_alpha_3(cls, alpha_3: str) -> Country:
|
||||
return cls(pycountry_object=pycountry.countries.get(alpha_3=alpha_3.upper()))
|
||||
|
||||
@classmethod
|
||||
def from_fuzzy(cls, fuzzy: str) -> Country:
|
||||
return cls(pycountry_object=pycountry.countries.search_fuzzy(fuzzy)) # type: ignore
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return self.pycountry_object.name
|
||||
|
||||
@property
|
||||
def alpha_2(self) -> str:
|
||||
return self.pycountry_object.alpha_2
|
||||
|
||||
@property
|
||||
def alpha_3(self) -> str:
|
||||
return self.pycountry_object.alpha_3
|
||||
|
||||
@property
|
||||
def numeric(self) -> str:
|
||||
return self.pycountry_object.numeric
|
||||
|
||||
@property
|
||||
def official_name(self) -> str:
|
||||
return self.pycountry_object.official_name
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.pycountry_object.__str__()
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return self.pycountry_object.__repr__()
|
||||
|
||||
|
||||
class EmptyCountry(Country):
|
||||
"""
|
||||
A null-object pattern implementation of Country that returns None for all attributes.
|
||||
|
||||
>>> empty = EmptyCountry("InvalidCountry")
|
||||
>>> print(empty.name) # None
|
||||
>>> print(empty) # EmptyCountry()
|
||||
|
||||
It doubles as a factory, so if you instantiate the class you'll either get a Country object or EmptyCountry depending if it found a country.
|
||||
|
||||
>>> empty = EmptyCountry("InvalidCountry")
|
||||
>>> print(type(empty)) # <class 'pycountry_wrapper.country.EmptyCountry'>
|
||||
>>>
|
||||
>>> found = EmptyCountry("US")
|
||||
>>> print(type(found)) # <class 'pycountry_wrapper.country.Country'>
|
||||
"""
|
||||
def __new__(cls, country: Optional[str] = None, pycountry_object: Optional[pycountry.db.Country] = None, **kwargs) -> Union[Country, EmptyCountry]:
|
||||
try:
|
||||
return Country(country=country, pycountry_object=pycountry_object, disable_fallback=True)
|
||||
except EmptyCountryException:
|
||||
return super().__new__(cls)
|
||||
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
pass
|
||||
|
||||
name = None # type: ignore
|
||||
alpha_2 = None # type: ignore
|
||||
alpha_3 = None # type: ignore
|
||||
numeric = None # type: ignore
|
||||
|
||||
def __str__(self) -> str:
|
||||
return "EmptyCountry()"
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "EmptyCountry()"
|
||||
|
@ -3,7 +3,8 @@ name = "pycountry-wrapper"
|
||||
dependencies = [
|
||||
"pycountry~=24.6.1",
|
||||
]
|
||||
dynamic = ["version"]
|
||||
version = "1.0.0"
|
||||
dynamic = []
|
||||
authors = []
|
||||
description = "This is a wrapper for pycountry, to make said library more usable."
|
||||
readme = "README.md"
|
||||
@ -32,6 +33,3 @@ packages = ["pycountry_wrapper"]
|
||||
|
||||
[tool.hatch.metadata]
|
||||
allow-direct-references = true
|
||||
|
||||
[tool.hatch.version]
|
||||
path = "pycountry_wrapper/__about__.py"
|
||||
|
11
release
11
release
@ -2,18 +2,9 @@
|
||||
|
||||
# install build tools
|
||||
pip install build
|
||||
pip install twine
|
||||
pip install hatch
|
||||
|
||||
|
||||
# increment version in pyproject.toml
|
||||
hatch version micro
|
||||
git add pycountry_wrapper/__about__.py
|
||||
git commit -m "bump version"
|
||||
git push
|
||||
|
||||
# build package
|
||||
python3 -m build --wheel
|
||||
python3 -m build
|
||||
|
||||
# upload to pypi
|
||||
python3 -m twine upload dist/*
|
||||
|
43
tests/test_fallback.py
Normal file
43
tests/test_fallback.py
Normal file
@ -0,0 +1,43 @@
|
||||
import unittest
|
||||
|
||||
from pycountry_wrapper import Country, EmptyCountry, config, EmptyCountryException
|
||||
|
||||
|
||||
class TestCountry(unittest.TestCase):
|
||||
def test_alpha_2(self):
|
||||
config.fallback_country = "US"
|
||||
c = Country("DE")
|
||||
self.assertEqual(c.alpha_2, "DE")
|
||||
|
||||
def test_alpha_3(self):
|
||||
config.fallback_country = "US"
|
||||
c = Country("DEU")
|
||||
self.assertEqual(c.alpha_2, "DE")
|
||||
|
||||
def test_fuzzy(self):
|
||||
config.fallback_country = "US"
|
||||
c = Country("Germany")
|
||||
self.assertEqual(c.alpha_2, "DE")
|
||||
|
||||
def test_not_found(self):
|
||||
config.fallback_country = "US"
|
||||
c = Country("does not exist")
|
||||
self.assertEqual(c.alpha_2, "US")
|
||||
|
||||
|
||||
class TestEmptyCountry(unittest.TestCase):
|
||||
def test_found(self):
|
||||
config.fallback_country = "US"
|
||||
c = EmptyCountry("DE")
|
||||
self.assertEqual(type(c), Country)
|
||||
self.assertEqual(c.alpha_2, "DE")
|
||||
|
||||
def test_not_found(self):
|
||||
config.fallback_country = "US"
|
||||
c = EmptyCountry("does not exist")
|
||||
self.assertEqual(type(c), EmptyCountry)
|
||||
self.assertEqual(c.alpha_2, None)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
48
tests/test_no_fallback.py
Normal file
48
tests/test_no_fallback.py
Normal file
@ -0,0 +1,48 @@
|
||||
import unittest
|
||||
|
||||
from pycountry_wrapper import Country, EmptyCountry, config, EmptyCountryException
|
||||
|
||||
|
||||
|
||||
|
||||
class TestCountry(unittest.TestCase):
|
||||
def test_alpha_2(self):
|
||||
config.fallback_country = None
|
||||
c = Country("DE")
|
||||
self.assertEqual(c.alpha_2, "DE")
|
||||
|
||||
def test_alpha_3(self):
|
||||
config.fallback_country = None
|
||||
c = Country("DEU")
|
||||
self.assertEqual(c.alpha_2, "DE")
|
||||
|
||||
def test_fuzzy(self):
|
||||
config.fallback_country = None
|
||||
c = Country("Germany")
|
||||
self.assertEqual(c.alpha_2, "DE")
|
||||
|
||||
def test_not_found(self):
|
||||
config.fallback_country = None
|
||||
with self.assertRaises(EmptyCountryException):
|
||||
Country("does not exist")
|
||||
|
||||
|
||||
class TestEmptyCountry(unittest.TestCase):
|
||||
def test_found(self):
|
||||
config.fallback_country = None
|
||||
c = EmptyCountry("DE")
|
||||
self.assertEqual(type(c), Country)
|
||||
|
||||
def test_data(self):
|
||||
config.fallback_country = None
|
||||
c = EmptyCountry("DE")
|
||||
self.assertEqual(c.alpha_2, "DE")
|
||||
|
||||
def test_not_found(self):
|
||||
config.fallback_country = None
|
||||
c = EmptyCountry("does not exist")
|
||||
self.assertEqual(type(c), EmptyCountry)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
Loading…
x
Reference in New Issue
Block a user