Repository: streamlit/streamlit

Issue:
Warn users when they try to run Streamlit on an `ipynb` file.
# Problem

[Some users](https://discuss.streamlit.io/t/app-loading-problem/199/7?u=adrien_treuille) have tried to run Streamlit on a Jupyter notebook (`.ipnyb`) file and are confused when this doesn't work.

# Solution

It would be great if for _any_ non `.py` file, Streamlit behaved as follows:

```bash
$ streamlit run my_msft_word_file.docx

Error: Streamlit requires raw Python (.py) files, not .docx.
For more information, please see https://streamlit.io/docs

$ streamlit run my_notebook.ipnyb

Error: Streamlit requires raw Python (.py) files, not .ipnyb.
For more information, please see https://streamlit.io/docs
```




Discussion:
Are you proposing we just check the file extension? I think we need to research if any other possible extensions would be valid too. 

From a quick look at [this list](http://dcjtech.info/topic/python-file-extensions/), I think it's probably safe to only accept these extensions:
- `.py`
- `.py3`

Everything else would lead to an error.

Note that if/when we implenent #662 we'll probably have to tweak this behavior to support running modules too.

Resolving patch:
diff --git a/lib/streamlit/cli.py b/lib/streamlit/cli.py
--- a/lib/streamlit/cli.py
+++ b/lib/streamlit/cli.py
@@ -34,7 +34,9 @@
 import streamlit.bootstrap as bootstrap
 from streamlit.case_converters import to_snake_case
 
-LOG_LEVELS = ["error", "warning", "info", "debug"]
+ACCEPTED_FILE_EXTENSIONS = ("py", "py3")
+
+LOG_LEVELS = ("error", "warning", "info", "debug")
 
 NEW_VERSION_TEXT = """
   %(new_version)s
@@ -207,6 +209,10 @@ def main_run(target, args=None, **kwargs):
 
     _apply_config_options_from_cli(kwargs)
 
+    _, extension = os.path.splitext(target)
+    if extension[1:] not in ACCEPTED_FILE_EXTENSIONS:
+        raise click.BadArgumentUsage("Streamlit requires raw Python (.py) files, not %s.\nFor more information, please see https://streamlit.io/docs" % extension)
+
     if url(target):
         from streamlit.temporary_directory import TemporaryDirectory
 


Test patch:
diff --git a/lib/tests/streamlit/cli_test.py b/lib/tests/streamlit/cli_test.py
--- a/lib/tests/streamlit/cli_test.py
+++ b/lib/tests/streamlit/cli_test.py
@@ -43,33 +43,43 @@ def setUp(self):
         streamlit._is_running_with_streamlit = False
 
     def test_run_no_arguments(self):
-        """streamlit run should fail if run with no arguments"""
+        """streamlit run should fail if run with no arguments."""
         result = self.runner.invoke(cli, ["run"])
         self.assertNotEqual(0, result.exit_code)
 
     def test_run_existing_file_argument(self):
-        """streamlit run succeeds if an existing file is passed"""
+        """streamlit run succeeds if an existing file is passed."""
         with patch("validators.url", return_value=False), patch(
             "streamlit.cli._main_run"
         ), patch("os.path.exists", return_value=True):
 
-            result = self.runner.invoke(cli, ["run", "file_name"])
+            result = self.runner.invoke(cli, ["run", "file_name.py"])
         self.assertEqual(0, result.exit_code)
 
     def test_run_non_existing_file_argument(self):
-        """streamlit run should fail if a non existing file is passed"""
+        """streamlit run should fail if a non existing file is passed."""
 
         with patch("validators.url", return_value=False), patch(
             "streamlit.cli._main_run"
         ), patch("os.path.exists", return_value=False):
 
-            result = self.runner.invoke(cli, ["run", "file_name"])
+            result = self.runner.invoke(cli, ["run", "file_name.py"])
         self.assertNotEqual(0, result.exit_code)
         self.assertTrue("File does not exist" in result.output)
 
+    def test_run_not_allowed_file_extension(self):
+        """streamlit run should fail if a not allowed file extension is passed.
+        """
+
+        result = self.runner.invoke(cli, ["run", "file_name.doc"])
+
+        self.assertNotEqual(0, result.exit_code)
+        self.assertTrue("Streamlit requires raw Python (.py) files, not .doc."
+                        in result.output)
+
     @tempdir()
     def test_run_valid_url(self, temp_dir):
-        """streamlit run succeeds if an existing url is passed"""
+        """streamlit run succeeds if an existing url is passed."""
 
         with patch("validators.url", return_value=True), patch(
             "streamlit.cli._main_run"
@@ -88,14 +98,15 @@ def test_run_valid_url(self, temp_dir):
     @tempdir()
     def test_run_non_existing_url(self, temp_dir):
         """streamlit run should fail if a non existing but valid
-         url is passed
+         url is passed.
          """
 
         with patch("validators.url", return_value=True), patch(
             "streamlit.cli._main_run"
         ), requests_mock.mock() as m:
 
-            m.get("http://url/app.py", exc=requests.exceptions.RequestException)
+            m.get("http://url/app.py",
+                  exc=requests.exceptions.RequestException)
             with patch("streamlit.temporary_directory.TemporaryDirectory") as mock_tmp:
                 mock_tmp.return_value.__enter__.return_value = temp_dir.path
                 result = self.runner.invoke(cli, ["run", "http://url/app.py"])
@@ -104,7 +115,7 @@ def test_run_non_existing_url(self, temp_dir):
         self.assertTrue("Unable to fetch" in result.output)
 
     def test_run_arguments(self):
-        """The correct command line should be passed downstream"""
+        """The correct command line should be passed downstream."""
         with patch("validators.url", return_value=False), patch(
             "os.path.exists", return_value=True
         ):
@@ -119,7 +130,8 @@ def test_run_arguments(self):
                     ],
                 )
         mock_main_run.assert_called_with(
-            "some script.py", ("argument with space", "argument with another space")
+            "some script.py",
+            ("argument with space", "argument with another space")
         )
         self.assertEqual(0, result.exit_code)
 
@@ -130,7 +142,8 @@ def test_get_command_line(self):
         mock_context = MagicMock()
         mock_context.parent.command_path = "mock_command"
         with patch("click.get_current_context", return_value=mock_context):
-            with patch("click.get_os_args", return_value=["os_arg1", "os_arg2"]):
+            with patch("click.get_os_args",
+                       return_value=["os_arg1", "os_arg2"]):
                 result = cli._get_command_line_as_string()
                 self.assertEqual("mock_command os_arg1 os_arg2", result)
 
@@ -168,7 +181,7 @@ def test_convert_config_option_to_click_option(self):
     @patch("streamlit.cli._config._set_option")
     def test_apply_config_options_from_cli(self, patched__set_option):
         """Test that _apply_config_options_from_cli parses the key properly and
-        passes down the parameters
+        passes down the parameters.
         """
 
         kwargs = {
@@ -200,7 +213,8 @@ def test_apply_config_options_from_cli(self, patched__set_option):
         )
 
     def test_credentials_headless_no_config(self):
-        """If headless mode and no config is present, activation should be None."""
+        """If headless mode and no config is present,
+        activation should be None."""
         from streamlit import config
 
         config.set_option("server.headless", True)
@@ -219,7 +233,8 @@ def test_credentials_headless_no_config(self):
 
     @parameterized.expand([(True,), (False,)])
     def test_credentials_headless_with_config(self, headless_mode):
-        """If headless, but a cofig file is present, activation should be defined.
+        """If headless, but a config file is present, activation should be
+        defined.
         So we call `_check_activated`.
         """
         from streamlit import config