Repository: scribe-org/Scribe-Data

Issue:
Missing error message for command line get with only --language
### Terms

- [x] I have searched all [open bug reports](https://github.com/scribe-org/Scribe-Data/issues?q=is%3Aopen+is%3Aissue+label%3Abug)
- [x] I agree to follow Scribe-Data's [Code of Conduct](https://github.com/scribe-org/Scribe-Data/blob/main/.github/CODE_OF_CONDUCT.md)

### Behavior

With `scribe-data get --language=danish` I get an error:  
```
Traceback (most recent call last):
  File ".../scribe-data", line 8, in <module>
    sys.exit(main())
  File "...Scribe-Data/src/scribe_data/scribe_data/cli/main.py", line 363, in main
    get_data(
  File "...Scribe-Data/src/scribe_data/cli/get.py", line 210, in get_data
    f"Updating data for language(s): {language.title()}; data type(s): {data_type.capitalize()}"
AttributeError: 'NoneType' object has no attribute 'capitalize'
```
If I do `scribe-data get --language=danish --data-type verbs -od ./scribe-data` it works ok, so I guess it is missing command line objects that are an issue?

Hints from issue comments:
@andrewtavis: Thanks for the issue and testing it out, @fnielsen! Really appreciate it :)

CC @axif0: Let's try to get to this one soon 😊@axif0: If we want to get forms for all data-types in a language, we need to put `-a` at the end. Like

```
scribe-data get --language=danish -a
```



@axif0: So, As the error is showing because of the wrong condition in [get.py](https://github.com/scribe-org/Scribe-Data/blob/main/src/scribe_data/cli/get.py#L206).

Instead of `elif language or data_type`, we can modify it by `elif language and data_type`. So that it can show us currect error.

![Image](https://github.com/user-attachments/assets/4b78d8a4-5262-43cd-b669-63bd9705ae54)@andrewtavis: Sounds good, @axif0! Feel free to open a PR for this :)@andrewtavis: Given updates in #574 the error message here will be:

```
You must provide at least one --language (-l) and one --data-type (-dt). You can also use --all (-a) for all combinations or all data types using --all (-a) in place of --data-type (-dt).
```@andrewtavis: Closed by #574 :) Please let us know if you find anything else or if you have suggestions, @fnielsen! Really appreciate you taking the time to open this 😊 And thanks for the great work in closing this, @axif0! 🚀

Solution patch:
diff --git a/src/scribe_data/cli/get.py b/src/scribe_data/cli/get.py
index bfc18361..ff9d8bf5 100644
--- a/src/scribe_data/cli/get.py
+++ b/src/scribe_data/cli/get.py
@@ -3,15 +3,15 @@
 Functions for getting languages-data types packs for the Scribe-Data CLI.
 """
 
+import json
 import os
+import urllib.error
 from pathlib import Path
 from typing import List, Union
-import json
-import urllib.error
-from SPARQLWrapper.SPARQLExceptions import EndPointInternalError
 
 import questionary
 from rich import print as rprint
+from SPARQLWrapper.SPARQLExceptions import EndPointInternalError
 
 from scribe_data.cli.convert import convert_wrapper
 from scribe_data.unicode.generate_emoji_keywords import generate_emoji
@@ -203,7 +203,7 @@ def get_data(
 
     # MARK: Query Data
 
-    elif language or data_type:
+    elif language and data_type:
         language_or_sub_language = language.split(" ")[0]
         data_type = data_type[0] if isinstance(data_type, list) else data_type
         print(
@@ -271,7 +271,7 @@ def get_data(
 
     else:
         raise ValueError(
-            "You must provide at least one of the --language (-l) or --data-type (-dt) options, or use --all (-a)."
+            "You must provide at least one --language (-l) and one --data-type (-dt). You can also use --all (-a) for all combinations or all data types using --all (-a) in place of --data-type (-dt)."
         )
 
     # Output Conversion
diff --git a/src/scribe_data/cli/total.py b/src/scribe_data/cli/total.py
index c1699832..ce5952eb 100644
--- a/src/scribe_data/cli/total.py
+++ b/src/scribe_data/cli/total.py
@@ -292,8 +292,13 @@ def get_total_lexemes(language, data_type, do_print=True):
 
             else:
                 print("Query failed after retries.")
+                return None
 
     # Check if the query returned any results.
+    if results is None:
+        print("Total number of lexemes: Not found")
+        return None
+
     if (
         "results" in results
         and "bindings" in results["results"]


Test patch:
diff --git a/tests/cli/test_get.py b/tests/cli/test_get.py
index 2cb52061..ee5c23b0 100644
--- a/tests/cli/test_get.py
+++ b/tests/cli/test_get.py
@@ -3,11 +3,14 @@
 Tests for the CLI get functionality.
 """
 
+import json
 import unittest
+import urllib.error
 from pathlib import Path
 from unittest.mock import patch
 
 from scribe_data.cli.get import get_data
+from SPARQLWrapper.SPARQLExceptions import EndPointInternalError
 
 
 class TestGetData(unittest.TestCase):
@@ -360,3 +363,143 @@ class TestGetData(unittest.TestCase):
             wikidata_dump_path="scribe",
             overwrite_all=False,
         )
+
+    # MARK: All Languages for Data Type
+    @patch("scribe_data.cli.get.parse_wd_lexeme_dump")
+    @patch("scribe_data.cli.get.questionary.confirm")
+    def test_get_all_languages_for_data_type_user_says_no(
+        self, mock_questionary_confirm, mock_parse
+    ):
+        """
+        Test retrieving all languages for a specific data type when user chooses to use lexeme dump.
+
+        This tests the behavior when using --all (-a) with --data-type (-dt) and the user
+        chooses not to query Wikidata directly.
+        """
+        # Mock user choosing to use lexeme dump instead of querying Wikidata.
+        mock_questionary_confirm.return_value.ask.return_value = False
+
+        get_data(all_bool=True, data_type="verbs", output_dir="test")
+
+        mock_parse.assert_called_once_with(
+            language="all",
+            wikidata_dump_type=["form"],
+            data_types=["verbs"],
+            type_output_dir="test",
+            wikidata_dump_path=None,
+            overwrite_all=False,
+        )
+
+    @patch("scribe_data.cli.get.query_data")
+    @patch("scribe_data.cli.get.questionary.confirm")
+    def test_get_all_languages_for_data_type_user_says_yes(
+        self, mock_questionary_confirm, mock_query_data
+    ):
+        """
+        Test retrieving all languages for a specific data type when user chooses to query Wikidata.
+
+        This tests the behavior when using --all (-a) with --data-type (-dt) and the user
+        chooses to query Wikidata directly.
+        """
+        # Mock user choosing to query Wikidata directly.
+        mock_questionary_confirm.return_value.ask.return_value = True
+
+        get_data(all_bool=True, data_type="verbs", output_dir="test")
+
+        mock_query_data.assert_called_once_with(
+            languages=None,
+            data_type=["verbs"],
+            output_dir="test",
+            overwrite=False,
+        )
+
+    # MARK: Error Handling
+
+    @patch("scribe_data.cli.get.query_data")
+    def test_json_decode_error_handling(self, mock_query_data):
+        """
+        Test handling of JSONDecodeError when querying data.
+        """
+        mock_query_data.side_effect = json.decoder.JSONDecodeError("Msg", "Doc", 0)
+
+        get_data(language="German", data_type="verbs")
+
+    @patch("scribe_data.cli.get.query_data")
+    def test_http_error_handling(self, mock_query_data):
+        """
+        Test handling of HTTPError when querying data.
+        """
+        mock_query_data.side_effect = urllib.error.HTTPError(
+            url="test", code=500, msg="error", hdrs={}, fp=None
+        )
+
+        get_data(language="German", data_type="verbs")
+
+    @patch("scribe_data.cli.get.query_data")
+    def test_endpoint_error_handling(self, mock_query_data):
+        """
+        Test handling of EndPointInternalError when querying data.
+        """
+        mock_query_data.side_effect = EndPointInternalError
+
+        get_data(language="German", data_type="verbs")
+
+    # MARK: Output Type Handling
+
+    @patch("scribe_data.cli.get.query_data")
+    @patch("scribe_data.cli.get.convert_wrapper")
+    @patch("scribe_data.cli.get.Path.exists")
+    @patch("scribe_data.cli.get.os.remove")
+    def test_output_type_conversion(
+        self, mock_remove, mock_exists, mock_convert, mock_query_data
+    ):
+        """
+        Test conversion of output to different file types.
+        """
+        mock_exists.return_value = True
+
+        get_data(
+            language="German",
+            data_type="verbs",
+            output_type="csv",
+            output_dir="test_dir",
+            identifier_case="snake",
+        )
+
+        # Use Path to create platform-appropriate path.
+        expected_input_file = str(Path("test_dir/German/verbs.json"))
+
+        mock_convert.assert_called_once_with(
+            language="German",
+            data_type="verbs",
+            output_type="csv",
+            input_file=expected_input_file,
+            output_dir="test_dir",
+            overwrite=False,
+            identifier_case="snake",
+        )
+        mock_remove.assert_called_once()
+
+    # MARK: Default Output Directory
+
+    def test_default_output_directory_selection(self):
+        """
+        Test that correct default output directory is selected based on output type.
+        """
+        test_cases = [
+            ("csv", "scribe_data_csv_export"),
+            ("json", "scribe_data_json_export"),
+            ("sqlite", "scribe_data_sqlite_export"),
+            ("tsv", "scribe_data_tsv_export"),
+        ]
+
+        for output_type, expected_dir in test_cases:
+            with patch("scribe_data.cli.get.query_data") as mock_query:
+                get_data(language="German", data_type="verbs", output_type=output_type)
+                mock_query.assert_called_with(
+                    languages=["German"],
+                    data_type=["verbs"],
+                    output_dir=expected_dir,
+                    overwrite=False,
+                    interactive=False,
+                )
diff --git a/tests/cli/test_total.py b/tests/cli/test_total.py
index 2d6f56e9..ca4905db 100644
--- a/tests/cli/test_total.py
+++ b/tests/cli/test_total.py
@@ -5,7 +5,9 @@ Tests for the CLI total functionality.
 
 import json
 import unittest
+from http.client import IncompleteRead
 from unittest.mock import MagicMock, call, patch
+from urllib.error import HTTPError
 
 from scribe_data.cli.total import (
     get_datatype_list,
@@ -75,7 +77,7 @@ class TestTotalLexemes(unittest.TestCase):
         mock_get_qid.return_value = None
         mock_query.return_value = MagicMock()
 
-        # Call the function with empty and None inputs
+        # Call the function with empty and None inputs.
         with patch("builtins.print") as mock_print:
             get_total_lexemes(language="", data_type="nouns")
             get_total_lexemes(None, "verbs")
@@ -112,7 +114,7 @@ class TestTotalLexemes(unittest.TestCase):
 
         mock_query.return_value = mock_results
 
-        # Call the function with different data types
+        # Call the function with different data types.
         with patch("builtins.print") as mock_print:
             get_total_lexemes(language="English", data_type="verbs")
             get_total_lexemes(language="English", data_type="nouns")
@@ -349,3 +351,185 @@ class TestTotalWrapper(unittest.TestCase):
         """
         total_wrapper(language="Q9217", data_type="Q1084")  # Q1084 is noun QID
         mock_get_total.assert_called_once_with(language="Q9217", data_type="Q1084")
+
+    # MARK: Multiple Languages and Data Types
+
+    @patch("scribe_data.cli.total.get_total_lexemes")
+    def test_total_wrapper_multiple_languages(self, mock_get_total):
+        """
+        Test retrieving totals for multiple languages.
+        """
+        # Mock return value to avoid formatting error.
+        mock_get_total.return_value = 100
+
+        total_wrapper(language=["English", "German"], data_type="nouns")
+
+        expected_calls = [
+            call(language="English", data_type="nouns", do_print=False),
+            call(language="German", data_type="nouns", do_print=False),
+        ]
+        mock_get_total.assert_has_calls(expected_calls)
+
+    @patch("scribe_data.cli.total.get_total_lexemes")
+    def test_total_wrapper_multiple_data_types(self, mock_get_total):
+        """
+        Test retrieving totals for multiple data types.
+        """
+        # Mock return value to avoid formatting error.
+        mock_get_total.return_value = 100
+
+        total_wrapper(language="English", data_type=["nouns", "verbs"])
+
+        expected_calls = [
+            call(language="English", data_type="nouns", do_print=False),
+            call(language="English", data_type="verbs", do_print=False),
+        ]
+        mock_get_total.assert_has_calls(expected_calls)
+
+    @patch("scribe_data.cli.total.get_total_lexemes")
+    def test_total_wrapper_multiple_languages_and_types(self, mock_get_total):
+        """
+        Test retrieving totals for multiple languages and data types.
+        """
+        # Mock return value to avoid formatting error.
+        mock_get_total.return_value = 100
+
+        total_wrapper(language=["English", "German"], data_type=["nouns", "verbs"])
+
+        expected_calls = [
+            call(language="English", data_type="nouns", do_print=False),
+            call(language="English", data_type="verbs", do_print=False),
+            call(language="German", data_type="nouns", do_print=False),
+            call(language="German", data_type="verbs", do_print=False),
+        ]
+        mock_get_total.assert_has_calls(expected_calls)
+
+    # MARK: Error Handling
+
+    @patch("scribe_data.cli.total.sparql.query")
+    def test_get_total_lexemes_http_error(self, mock_query):
+        """
+        Test handling of HTTPError when querying totals.
+        """
+        # Set up mock to return None for results after max retries.
+        mock_query.side_effect = [
+            HTTPError(url="test", code=500, msg="error", hdrs={}, fp=None),
+            HTTPError(url="test", code=500, msg="error", hdrs={}, fp=None),
+            HTTPError(url="test", code=500, msg="error", hdrs={}, fp=None),
+        ]
+
+        with patch("builtins.print") as mock_print:
+            result = get_total_lexemes(language="English", data_type="nouns")
+
+        self.assertIsNone(result)
+        mock_print.assert_any_call("Query failed after retries.")
+
+    @patch("scribe_data.cli.total.sparql.query")
+    def test_get_total_lexemes_incomplete_read(self, mock_query):
+        """
+        Test handling of IncompleteRead error when querying totals.
+        """
+        # Set up mock to return None for results after max retries.
+        mock_query.side_effect = [
+            IncompleteRead(partial=b""),
+            IncompleteRead(partial=b""),
+            IncompleteRead(partial=b""),
+        ]
+
+        with patch("builtins.print") as mock_print:
+            result = get_total_lexemes(language="English", data_type="nouns")
+
+        self.assertIsNone(result)
+        mock_print.assert_any_call("Query failed after retries.")
+
+    # MARK: Sub-language Handling
+
+    @patch("scribe_data.cli.total.get_datatype_list")
+    @patch("scribe_data.cli.total.get_total_lexemes")
+    def test_print_total_lexemes_with_sublanguages(
+        self, mock_get_total, mock_get_datatypes
+    ):
+        """
+        Test printing totals for a language with sub-languages.
+        """
+        mock_get_datatypes.return_value = ["nouns", "verbs"]
+        mock_get_total.return_value = 100
+
+        with patch("builtins.print") as mock_print:
+            total_wrapper(language="Norwegian")
+
+        # Verify header was printed.
+        mock_print.assert_any_call(
+            f"{'Language':<20} {'Data Type':<25} {'Total Wikidata Lexemes':<25}"
+        )
+        mock_print.assert_any_call("=" * 70)
+
+        # Verify data was printed for each data type.
+        mock_get_total.assert_any_call(
+            language="Norwegian", data_type="nouns", do_print=False
+        )
+        mock_get_total.assert_any_call(
+            language="Norwegian", data_type="verbs", do_print=False
+        )
+
+    # MARK: Data Type List Handling
+
+    @patch("scribe_data.cli.total.language_metadata")
+    @patch("scribe_data.cli.total.list_all_languages")
+    @patch("scribe_data.cli.total.LANGUAGE_DATA_EXTRACTION_DIR")
+    def test_get_datatype_list_with_sublanguages(
+        self, mock_dir, mock_list_languages, mock_metadata
+    ):
+        """
+        Test getting data type list for a language with sub-languages.
+        """
+        # Mock language metadata and list_all_languages.
+        mock_metadata_dict = {
+            "norwegian": {
+                "sub_languages": {"bokmal": {"iso": "nb"}, "nynorsk": {"iso": "nn"}}
+            }
+        }
+
+        # Mock dictionary-like behavior for language_metadata.
+        mock_metadata.__iter__.return_value = mock_metadata_dict.items()
+        mock_metadata.items.return_value = mock_metadata_dict.items()
+        mock_metadata.get.return_value = mock_metadata_dict["norwegian"]
+        mock_metadata.__getitem__.return_value = mock_metadata_dict["norwegian"]
+
+        mock_list_languages.return_value = ["norwegian"]
+
+        # Create mock directory entries with proper string names.
+        mock_nouns = MagicMock()
+        mock_nouns.name = "nouns"
+        mock_nouns.is_dir.return_value = True
+
+        mock_verbs = MagicMock()
+        mock_verbs.name = "verbs"
+        mock_verbs.is_dir.return_value = True
+
+        # Mock directory structure for both sub-languages.
+        def mock_path_handler(path):
+            mock_path = MagicMock()
+            mock_path.exists.return_value = True
+            mock_path.iterdir.return_value = [mock_nouns, mock_verbs]
+            return mock_path
+
+        mock_dir.__truediv__.side_effect = mock_path_handler
+
+        result = get_datatype_list("norwegian")  # note: lowercase
+        self.assertEqual(sorted(result), ["nouns", "verbs"])
+
+    @patch("scribe_data.cli.total.language_metadata")
+    @patch("scribe_data.cli.total.LANGUAGE_DATA_EXTRACTION_DIR")
+    def test_get_datatype_list_empty_directory(self, mock_dir, mock_metadata):
+        """
+        Test getting data type list from an empty directory.
+        """
+        # Mock language metadata.
+        mock_metadata.get.return_value = {}
+
+        mock_dir.__truediv__.return_value.exists.return_value = True
+        mock_dir.__truediv__.return_value.iterdir.return_value = []
+
+        with self.assertRaises(ValueError):
+            get_datatype_list("English")