diff --git a/README.md b/README.md index e7ab390..a643e09 100644 --- a/README.md +++ b/README.md @@ -162,7 +162,7 @@ When an invalid language value is passed to `Lang`, an `InvalidLanguageValue` ex ... except InvalidLanguageValue as e: ... e.msg ... -"'foobar' is not a valid ISO 639 name or identifier." +"'foobar' is not a valid Lang argument." ``` When a deprecated language value is passed to `Lang`, a `DeprecatedLanguageValue` exception is raised. diff --git a/iso639/exceptions.py b/iso639/exceptions.py index 3024714..eeef6f6 100644 --- a/iso639/exceptions.py +++ b/iso639/exceptions.py @@ -1,6 +1,6 @@ class InvalidLanguageValue(Exception): - """Exception raised when the argument passed to the `Lang` constructor is - not a valid: + """Exception raised when the arguments passed to the `Lang` constructor are + not valid and compatible: - ISO 639-1 identifier - ISO 639-2 English name - ISO 639-2/B identifier @@ -12,13 +12,20 @@ class InvalidLanguageValue(Exception): - ISO 639-5 identifier """ - def __init__(self, name_or_identifier): + def __init__(self, **kwargs): - self.invalid_value = name_or_identifier - self.msg = ( - f"'{name_or_identifier}' is not a valid " - "ISO 639 name or identifier." - ) + self.invalid_value = { + k: v + for k, v in kwargs.items() + if k == "name_or_identifier" or v is not None + } + if len(self.invalid_value) == 1: + main_arg = self.invalid_value["name_or_identifier"] + self.msg = f"{repr(main_arg)} is not a valid Lang argument." + else: + self.msg = ( + f"**{self.invalid_value} are not valid Lang keyword arguments." + ) super().__init__(self.msg) diff --git a/iso639/iso639.py b/iso639/iso639.py index 48b0cf0..04a3a7c 100644 --- a/iso639/iso639.py +++ b/iso639/iso639.py @@ -63,14 +63,71 @@ class Lang(tuple): __slots__ = () # set immutability of Lang - def __new__(cls, name_or_identifier: Union[str, "Lang"]): - lang_tuple = cls._validate_arg(name_or_identifier) - if lang_tuple == tuple(): # not valid argument - cls._assert_not_deprecated(name_or_identifier) - raise InvalidLanguageValue(name_or_identifier=name_or_identifier) + def __new__( + cls, + name_or_identifier: Optional[Union[str, "Lang"]] = None, + name: Optional[str] = None, + pt1: Optional[str] = None, + pt2b: Optional[str] = None, + pt2t: Optional[str] = None, + pt3: Optional[str] = None, + pt5: Optional[str] = None, + ): + # parse main argument + if name_or_identifier is None: + arg_lang_tuple = None + else: + arg_lang_tuple = cls._validate_arg(name_or_identifier) + + # parse other arguments + if all(v is None for v in (name, pt1, pt2b, pt2t, pt3, pt5)): + kwargs_lang_tuple = None + else: + kwargs_lang_tuple = cls._validate_kwargs( + name=name, pt1=pt1, pt2b=pt2b, pt2t=pt2t, pt3=pt3, pt5=pt5 + ) + + # check compatiblity between main argument and other arguments + if arg_lang_tuple is None and kwargs_lang_tuple is None: + lang_tuple = None + elif arg_lang_tuple is not None and kwargs_lang_tuple is None: + lang_tuple = arg_lang_tuple + elif kwargs_lang_tuple is not None and arg_lang_tuple is None: + lang_tuple = kwargs_lang_tuple + elif ( + arg_lang_tuple is not None + and kwargs_lang_tuple is not None + and arg_lang_tuple == kwargs_lang_tuple + ): + lang_tuple = arg_lang_tuple + else: + lang_tuple = tuple() - # instantiate as a tuple of ISO 639 language values - return tuple.__new__(cls, lang_tuple) + # chack if arguments match a deprecated language value + if lang_tuple == tuple(): + cls._assert_not_deprecated( + name_or_identifier=name_or_identifier, + name=name, + pt1=pt1, + pt2b=pt2b, + pt2t=pt2t, + pt3=pt3, + pt5=pt5, + ) + + if not lang_tuple: + raise InvalidLanguageValue( + name_or_identifier=name_or_identifier, + name=name, + pt1=pt1, + pt2b=pt2b, + pt2t=pt2t, + pt3=pt3, + pt5=pt5, + ) + else: + # instantiate as a tuple of ISO 639 language values + return tuple.__new__(cls, lang_tuple) def __repr__(self): chunks = ["=".join((tg, repr(getattr(self, tg)))) for tg in self._tags] @@ -210,15 +267,42 @@ def _validate_arg(cls, arg_value): return tuple() @classmethod - def _assert_not_deprecated(cls, arg_value): - for key in ("id", "name"): - try: - d = cls._deprecated[key][arg_value] - except KeyError: - pass - else: - d[key] = arg_value - raise DeprecatedLanguageValue(**d) + def _validate_kwargs(cls, **kwargs): + lang_tuples = set() + for tg, v in kwargs.items(): + if v: + lang_tuples.add(cls._get_language_tuple(tg, v)) + if len(lang_tuples) == 1: + return lang_tuples.pop() + + return tuple() + + @classmethod + def _assert_not_deprecated(cls, **kwargs): + deprecated = [] + for kw, arg_value in kwargs.items(): + if arg_value is None: + continue + elif kw == "name_or_identifier": + keys = ("id", "name") + elif kw == "name": + keys = ("name",) + elif kw in ("pt1", "pt2b", "pt2t", "pt3", "pt5"): + keys = ("id",) + + for k in keys: + try: + d = cls._deprecated[k][arg_value] + except KeyError: + pass + else: + d[k] = arg_value + deprecated.append(d) + + if deprecated and deprecated.count(deprecated[0]) == 1: + raise DeprecatedLanguageValue(**deprecated[0]) + + return True @classmethod def _get_language_tuple(cls, tag, arg_value): diff --git a/tests/test_iso639.py b/tests/test_iso639.py index 6b3ffc8..fbeba2e 100644 --- a/tests/test_iso639.py +++ b/tests/test_iso639.py @@ -46,12 +46,27 @@ def test_not_equal_languages_None(self): assert lg1 != lg2 def test_multiple_args(self): - with pytest.raises(TypeError): + Lang("fra", "French", "fr", "fre", "fra", "fra") == Lang("French") + + def test_wrong_multiple_args(self): + with pytest.raises(InvalidLanguageValue): Lang("fra", "fr") + def test_one_kwarg(self): + assert Lang(pt1="fr") == Lang("fr") + + def test_one_wrong_kwarg(self): + with pytest.raises(InvalidLanguageValue): + Lang(name="fr") + def test_mutliple_kwargs(self): - with pytest.raises(TypeError): - Lang(name_or_identifier="fr", pt3="fra") + Lang( + name="French", pt1="fr", pt2b="fre", pt2t="fra", pt3="fra" + ) == Lang("French") + + def test_mutliple_wrong_kwargs(self): + with pytest.raises(InvalidLanguageValue): + Lang(name="French", pt1="en") def test_kwarg_wrong_key(self): with pytest.raises(TypeError): @@ -62,7 +77,7 @@ def test_kwarg_wrong_value(self): Lang(name_or_identifier="foobar") def test_no_arg_no_kwarg(self): - with pytest.raises(TypeError): + with pytest.raises(InvalidLanguageValue): Lang() def test_none_arg(self): diff --git a/tests/test_readme_examples.py b/tests/test_readme_examples.py index e2da275..ec87ca4 100644 --- a/tests/test_readme_examples.py +++ b/tests/test_readme_examples.py @@ -138,7 +138,7 @@ def test_example_other_names(): def test_example_invalid_value(): with pytest.raises(InvalidLanguageValue) as exc_info: Lang("foobar") - s = "'foobar' is not a valid ISO 639 name or identifier." + s = "'foobar' is not a valid Lang argument." assert exc_info.value.msg == s