Repository: dbt-labs/dbt-core

Issue:
[CT-1591] [Bug] New python files .py cause parsing and compilation errors 
### Is this a new bug in dbt-core?

- [X] I believe this is a new bug in dbt-core
- [X] I have searched the existing issues, and I could not find an existing issue for this bug

### Current Behavior

When you open a new python file .py it causes a parsing and compilation error until the model code is inputted. 
```
def model(dbt, session):
 ...
    return df
```
This is not great for mid development experience and blocks the ability to run any of your other models simply because you started working on a `.py `file. 
https://www.loom.com/share/cea560725928459cb896a398289369e2

### Expected Behavior

No error compilation for new and empty python files. 
Allow you to run other models in your pipeline during python development. 

### Steps To Reproduce

1. In dbt cloud on v1.3
2. Create a new .py file without any code 



### Relevant log output

_No response_

### Environment

```markdown
- OS:
- Python:
- dbt:
```


### Which database adapter are you using with dbt?

_No response_

### Additional Context

_No response_


Discussion:
Thanks for discovering and reporting this @hope-wat !

Per your initial convo with @jtcohen6 in internal Slack the fix would just be:
> avoid parsing the file at all / raising that error if the file is totally empty

---

### Side not
In your screenshot, it contains a phrase which isn't the most clear:
> dbt only allow one model defined per python file

Thankfully an update to that piece was merged just yesterday in https://github.com/dbt-labs/dbt-core/pull/6251. The new message will read:
> dbt allows exactly one model defined per python file, found {x}
This should be a really quick fix, and a nontrivial quality-of-life improvement! Let's try to make it happen.
Talked about this live with @aranke 
Acceptance criteria:
[This](https://github.com/dbt-labs/dbt-core/blob/main/core/dbt/parser/models.py#L63) is where the error being raised. We should make sure we add another unittest similar to [this one](https://github.com/dbt-labs/dbt-core/blob/main/test/unit/test_parser.py#L668) but with empty file.

Resolving patch:
diff --git a/core/dbt/parser/models.py b/core/dbt/parser/models.py
--- a/core/dbt/parser/models.py
+++ b/core/dbt/parser/models.py
@@ -37,7 +37,6 @@
     UndefinedMacroException,
 )
 
-
 dbt_function_key_words = set(["ref", "source", "config", "get"])
 dbt_function_full_names = set(["dbt.ref", "dbt.source", "dbt.config", "dbt.config.get"])
 
@@ -197,41 +196,46 @@ def get_compiled_path(cls, block: FileBlock):
         return block.path.relative_path
 
     def parse_python_model(self, node, config, context):
+        config_keys_used = []
+        config_keys_defaults = []
+
         try:
             tree = ast.parse(node.raw_code, filename=node.original_file_path)
         except SyntaxError as exc:
             raise PythonParsingException(exc, node=node) from exc
 
-        # We are doing a validator and a parser because visit_FunctionDef in parser
-        # would actually make the parser not doing the visit_Calls any more
-        dbtValidator = PythonValidationVisitor()
-        dbtValidator.visit(tree)
-        dbtValidator.check_error(node)
+        # Only parse if AST tree has instructions in body
+        if tree.body:
+            # We are doing a validator and a parser because visit_FunctionDef in parser
+            # would actually make the parser not doing the visit_Calls any more
+            dbt_validator = PythonValidationVisitor()
+            dbt_validator.visit(tree)
+            dbt_validator.check_error(node)
+
+            dbt_parser = PythonParseVisitor(node)
+            dbt_parser.visit(tree)
+
+            for (func, args, kwargs) in dbt_parser.dbt_function_calls:
+                if func == "get":
+                    num_args = len(args)
+                    if num_args == 0:
+                        raise ParsingException(
+                            "dbt.config.get() requires at least one argument",
+                            node=node,
+                        )
+                    if num_args > 2:
+                        raise ParsingException(
+                            f"dbt.config.get() takes at most 2 arguments ({num_args} given)",
+                            node=node,
+                        )
+                    key = args[0]
+                    default_value = args[1] if num_args == 2 else None
+                    config_keys_used.append(key)
+                    config_keys_defaults.append(default_value)
+                    continue
+
+                context[func](*args, **kwargs)
 
-        dbtParser = PythonParseVisitor(node)
-        dbtParser.visit(tree)
-        config_keys_used = []
-        config_keys_defaults = []
-        for (func, args, kwargs) in dbtParser.dbt_function_calls:
-            if func == "get":
-                num_args = len(args)
-                if num_args == 0:
-                    raise ParsingException(
-                        "dbt.config.get() requires at least one argument",
-                        node=node,
-                    )
-                if num_args > 2:
-                    raise ParsingException(
-                        f"dbt.config.get() takes at most 2 arguments ({num_args} given)",
-                        node=node,
-                    )
-                key = args[0]
-                default_value = args[1] if num_args == 2 else None
-                config_keys_used.append(key)
-                config_keys_defaults.append(default_value)
-                continue
-
-            context[func](*args, **kwargs)
         if config_keys_used:
             # this is being used in macro build_config_dict
             context["config"](


Test patch:
diff --git a/test/unit/test_parser.py b/test/unit/test_parser.py
--- a/test/unit/test_parser.py
+++ b/test/unit/test_parser.py
@@ -571,6 +571,8 @@ def model1(dbt, session):
     return dbt.ref("some_model")
 """
 
+python_model_empty_file = """    """
+
 python_model_multiple_returns = """
 def model(dbt, session):
     dbt.config(materialized='table')
@@ -749,6 +751,11 @@ def test_python_model_incorrect_function_name(self):
         with self.assertRaises(ParsingException):
             self.parser.parse_file(block)
 
+    def test_python_model_empty_file(self):
+        block = self.file_block_for(python_model_empty_file, "nested/py_model.py")
+        self.parser.manifest.files[block.file.file_id] = block.file
+        self.assertIsNone(self.parser.parse_file(block))
+
     def test_python_model_multiple_returns(self):
         block = self.file_block_for(python_model_multiple_returns, 'nested/py_model.py')
         self.parser.manifest.files[block.file.file_id] = block.file
@@ -786,6 +793,13 @@ def test_python_model_custom_materialization(self):
         node = list(self.parser.manifest.nodes.values())[0]
         self.assertEqual(node.get_materialization(), "view")
 
+    def test_python_model_custom_materialization(self):
+        block = self.file_block_for(python_model_custom_materialization, 'nested/py_model.py')
+        self.parser.manifest.files[block.file.file_id] = block.file
+        self.parser.parse_file(block)
+        node = list(self.parser.manifest.nodes.values())[0]
+        self.assertEqual(node.get_materialization(), "view")
+
 class StaticModelParserTest(BaseParserTest):
     def setUp(self):
         super().setUp()