pycountry icon indicating copy to clipboard operation
pycountry copied to clipboard

Deepcopy recurses indefinitely

Open MartinDevi opened this issue 2 years ago • 2 comments

Hello! 👋

>>> import pycountry
>>> from copy import deepcopy
>>> deepcopy(pycountry.countries.get(alpha_2='US'))

Raises error:

RecursionError: maximum recursion depth exceeded

I'm a bit confused by the behavior of the library overall, because it always returns the same instance when querying a country, which is why I guess it doesn't bother to implement __eq__ on Country , yet the country instances are mutable. Shouldn't they be frozen? In that case I would resolve this by overriding the deep copy behavior so that it returns the same instance.

MartinDevi avatar Apr 08 '24 17:04 MartinDevi

I've encountered this as well when trying to store country object in some of my dataclasses (dataclasses.dataclass). This is probably caused by missing __deepcopy__ dunder in the pycountry.db.Country class.

I've solved it for my case by subclassing Country. @MartinDevi if you want to use my code as a workaround, here it is:

class CopyableCountry(pycountry.db.Country):
    """
    Overriding of `pycountry.db.Country` in order to be used by `copy.deepcopy()` for making deep copies.
    This is because we use country class in `dataclasses.dataclass` which calls deepcopy on non-builtin data types
     when serializing the object.

    Thus, we can have a country object as an attribute of our dataclasses which can be serialized into a dictionary.
    """

    def __init__(self, country: pycountry.db.Country):
        if not isinstance(country, pycountry.db.Country):
            raise TypeError("Country argument must be an instance of `pycountry.db.Country`!")
        elif not country:
            raise ValueError("Country argument is empty!")


        super().__init__(
            alpha_2=country.alpha_2,
            alpha_3=country.alpha_3,
            name=country.name,
            numeric=country.numeric,
            official_name=country.official_name,
            common_name=country.common_name,
            flag=country.flag,
        )

    def __deepcopy__(self, memo):
        """Create a new instance of `CopyableCountry()` with the same attributes."""
        new_country = pycountry.db.Country(
            alpha_2=self.alpha_2,
            alpha_3=self.alpha_3,
            name=self.name,
            numeric=self.numeric,
            official_name=self.official_name,
            common_name=self.common_name,
            flag=self.flag,
        )
        new_country = CopyableCountry(new_country)
        memo[id(self)] = new_country
        return new_country

Usage:

>>> import pycountry
>>> from copy import deepcopy
>>> _country = pycountry.countries.get(alpha_2='US')
>>> id(_country)
140548732301472
>>> country = CopyableCountry(_country)
>>> id(country)
140548732806144
>>> copied = deepcopy(country)
>>> id(copied)
140548733014176

If you want to use shallow copy, instead of deepcopy, you must implement __copy__ dunder.

Also, if you want to use the latest version 24.6.1, be sure to get rid of common_name lines in the code, because in that release, this attribute has been removed from the Country class.

Dano-drevo avatar Jul 11 '24 11:07 Dano-drevo