diff --git a/package.json b/package.json
index 074ae69bca5e..0f93441c6113 100644
--- a/package.json
+++ b/package.json
@@ -536,14 +536,16 @@
"pythonSurveyNotification",
"pythonPromptNewToolsExt",
"pythonTerminalEnvVarActivation",
- "pythonTestAdapter"
+ "pythonTestAdapter",
+ "pythonREPLSmartSend"
],
"enumDescriptions": [
"%python.experiments.All.description%",
"%python.experiments.pythonSurveyNotification.description%",
"%python.experiments.pythonPromptNewToolsExt.description%",
"%python.experiments.pythonTerminalEnvVarActivation.description%",
- "%python.experiments.pythonTestAdapter.description%"
+ "%python.experiments.pythonTestAdapter.description%",
+ "%python.experiments.pythonREPLSmartSend.description%"
]
},
"scope": "machine",
@@ -559,14 +561,16 @@
"pythonSurveyNotification",
"pythonPromptNewToolsExt",
"pythonTerminalEnvVarActivation",
- "pythonTestAdapter"
+ "pythonTestAdapter",
+ "pythonREPLSmartSend"
],
"enumDescriptions": [
"%python.experiments.All.description%",
"%python.experiments.pythonSurveyNotification.description%",
"%python.experiments.pythonPromptNewToolsExt.description%",
"%python.experiments.pythonTerminalEnvVarActivation.description%",
- "%python.experiments.pythonTestAdapter.description%"
+ "%python.experiments.pythonTestAdapter.description%",
+ "%python.experiments.pythonREPLSmartSend.description%"
]
},
"scope": "machine",
diff --git a/package.nls.json b/package.nls.json
index 7a6f789fdf2d..5687e51ab9df 100644
--- a/package.nls.json
+++ b/package.nls.json
@@ -41,6 +41,7 @@
"python.experiments.pythonPromptNewToolsExt.description": "Denotes the Python Prompt New Tools Extension experiment.",
"python.experiments.pythonTerminalEnvVarActivation.description": "Enables use of environment variables to activate terminals instead of sending activation commands.",
"python.experiments.pythonTestAdapter.description": "Denotes the Python Test Adapter experiment.",
+ "python.experiments.pythonREPLSmartSend.description": "Denotes the Python REPL Smart Send experiment.",
"python.formatting.autopep8Args.description": "Arguments passed in. Each argument is a separate item in the array.",
"python.formatting.autopep8Args.markdownDeprecationMessage": "This setting will soon be deprecated. Please use the [Autopep8 extension](https://marketplace.visualstudio.com/items?itemName=ms-python.autopep8).
Learn more [here](https://aka.ms/AAlgvkb).",
"python.formatting.autopep8Args.deprecationMessage": "This setting will soon be deprecated. Please use the Autopep8 extension. Learn more here: https://aka.ms/AAlgvkb.",
diff --git a/pythonFiles/normalizeSelection.py b/pythonFiles/normalizeSelection.py
index 0363702717ab..0ac47ab5dc3b 100644
--- a/pythonFiles/normalizeSelection.py
+++ b/pythonFiles/normalizeSelection.py
@@ -6,6 +6,7 @@
import re
import sys
import textwrap
+from typing import Iterable
def split_lines(source):
@@ -118,6 +119,8 @@ def normalize_lines(selection):
# Insert a newline between each top-level statement, and append a newline to the selection.
source = "\n".join(statements) + "\n"
+ if selection[-2] == "}":
+ source = source[:-1]
except Exception:
# If there's a problem when parsing statements,
# append a blank line to end the block and send it as-is.
@@ -126,17 +129,159 @@ def normalize_lines(selection):
return source
+top_level_nodes = []
+min_key = None
+
+
+def check_exact_exist(top_level_nodes, start_line, end_line):
+ exact_nodes = []
+ for node in top_level_nodes:
+ if node.lineno == start_line and node.end_lineno == end_line:
+ exact_nodes.append(node)
+
+ return exact_nodes
+
+
+def traverse_file(wholeFileContent, start_line, end_line, was_highlighted):
+ """
+ Intended to traverse through a user's given file content and find, collect all appropriate lines
+ that should be sent to the REPL in case of smart selection.
+ This could be exact statement such as just a single line print statement,
+ or a multiline dictionary, or differently styled multi-line list comprehension, etc.
+ Then call the normalize_lines function to normalize our smartly selected code block.
+ """
+
+ parsed_file_content = ast.parse(wholeFileContent)
+ smart_code = ""
+ should_run_top_blocks = []
+
+ # Purpose of this loop is to fetch and collect all the
+ # AST top level nodes, and its node.body as child nodes.
+ # Individual nodes will contain information like
+ # the start line, end line and get source segment information
+ # that will be used to smartly select, and send normalized code.
+ for node in ast.iter_child_nodes(parsed_file_content):
+ top_level_nodes.append(node)
+
+ ast_types_with_nodebody = (
+ ast.Module,
+ ast.Interactive,
+ ast.Expression,
+ ast.FunctionDef,
+ ast.AsyncFunctionDef,
+ ast.ClassDef,
+ ast.For,
+ ast.AsyncFor,
+ ast.While,
+ ast.If,
+ ast.With,
+ ast.AsyncWith,
+ ast.Try,
+ ast.Lambda,
+ ast.IfExp,
+ ast.ExceptHandler,
+ )
+ if isinstance(node, ast_types_with_nodebody) and isinstance(
+ node.body, Iterable
+ ):
+ for child_nodes in node.body:
+ top_level_nodes.append(child_nodes)
+
+ exact_nodes = check_exact_exist(top_level_nodes, start_line, end_line)
+
+ # Just return the exact top level line, if present.
+ if len(exact_nodes) > 0:
+ which_line_next = 0
+ for same_line_node in exact_nodes:
+ should_run_top_blocks.append(same_line_node)
+ smart_code += (
+ f"{ast.get_source_segment(wholeFileContent, same_line_node)}\n"
+ )
+ which_line_next = get_next_block_lineno(should_run_top_blocks)
+ return {
+ "normalized_smart_result": smart_code,
+ "which_line_next": which_line_next,
+ }
+
+ # For each of the nodes in the parsed file content,
+ # add the appropriate source code line(s) to be sent to the REPL, dependent on
+ # user is trying to send and execute single line/statement or multiple with smart selection.
+ for top_node in ast.iter_child_nodes(parsed_file_content):
+ if start_line == top_node.lineno and end_line == top_node.end_lineno:
+ should_run_top_blocks.append(top_node)
+
+ smart_code += f"{ast.get_source_segment(wholeFileContent, top_node)}\n"
+ break # If we found exact match, don't waste computation in parsing extra nodes.
+ elif start_line >= top_node.lineno and end_line <= top_node.end_lineno:
+ # Case to apply smart selection for multiple line.
+ # This is the case for when we have to add multiple lines that should be included in the smart send.
+ # For example:
+ # 'my_dictionary': {
+ # 'Audi': 'Germany',
+ # 'BMW': 'Germany',
+ # 'Genesis': 'Korea',
+ # }
+ # with the mouse cursor at 'BMW': 'Germany', should send all of the lines that pertains to my_dictionary.
+
+ should_run_top_blocks.append(top_node)
+
+ smart_code += str(ast.get_source_segment(wholeFileContent, top_node))
+ smart_code += "\n"
+
+ normalized_smart_result = normalize_lines(smart_code)
+ which_line_next = get_next_block_lineno(should_run_top_blocks)
+ return {
+ "normalized_smart_result": normalized_smart_result,
+ "which_line_next": which_line_next,
+ }
+
+
+# Look at the last top block added, find lineno for the next upcoming block,
+# This will be used in calculating lineOffset to move cursor in VS Code.
+def get_next_block_lineno(which_line_next):
+ last_ran_lineno = int(which_line_next[-1].end_lineno)
+ next_lineno = int(which_line_next[-1].end_lineno)
+
+ for reverse_node in top_level_nodes:
+ if reverse_node.lineno > last_ran_lineno:
+ next_lineno = reverse_node.lineno
+ break
+ return next_lineno
+
+
if __name__ == "__main__":
# Content is being sent from the extension as a JSON object.
# Decode the data from the raw bytes.
stdin = sys.stdin if sys.version_info < (3,) else sys.stdin.buffer
raw = stdin.read()
contents = json.loads(raw.decode("utf-8"))
+ # Empty highlight means user has not explicitly selected specific text.
+ empty_Highlight = contents.get("emptyHighlight", False)
- normalized = normalize_lines(contents["code"])
+ # We also get the activeEditor selection start line and end line from the typescript VS Code side.
+ # Remember to add 1 to each of the received since vscode starts line counting from 0 .
+ vscode_start_line = contents["startLine"] + 1
+ vscode_end_line = contents["endLine"] + 1
# Send the normalized code back to the extension in a JSON object.
- data = json.dumps({"normalized": normalized})
+ data = None
+ which_line_next = 0
+
+ if empty_Highlight and contents.get("smartSendExperimentEnabled"):
+ result = traverse_file(
+ contents["wholeFileContent"],
+ vscode_start_line,
+ vscode_end_line,
+ not empty_Highlight,
+ )
+ normalized = result["normalized_smart_result"]
+ which_line_next = result["which_line_next"]
+ data = json.dumps(
+ {"normalized": normalized, "nextBlockLineno": result["which_line_next"]}
+ )
+ else:
+ normalized = normalize_lines(contents["code"])
+ data = json.dumps({"normalized": normalized})
stdout = sys.stdout if sys.version_info < (3,) else sys.stdout.buffer
stdout.write(data.encode("utf-8"))
diff --git a/pythonFiles/tests/test_dynamic_cursor.py b/pythonFiles/tests/test_dynamic_cursor.py
new file mode 100644
index 000000000000..7aea59427aa6
--- /dev/null
+++ b/pythonFiles/tests/test_dynamic_cursor.py
@@ -0,0 +1,203 @@
+import importlib
+import textwrap
+
+import normalizeSelection
+
+
+def test_dictionary_mouse_mover():
+ """
+ Having the mouse cursor on second line,
+ 'my_dict = {'
+ and pressing shift+enter should bring the
+ mouse cursor to line 6, on and to be able to run
+ 'print('only send the dictionary')'
+ """
+ importlib.reload(normalizeSelection)
+ src = textwrap.dedent(
+ """\
+ not_dictionary = 'hi'
+ my_dict = {
+ "key1": "value1",
+ "key2": "value2"
+ }
+ print('only send the dictionary')
+ """
+ )
+
+ result = normalizeSelection.traverse_file(src, 2, 2, False)
+
+ assert result["which_line_next"] == 6
+
+
+def test_beginning_func():
+ """
+ Pressing shift+enter on the very first line,
+ of function definition, such as 'my_func():'
+ It should properly skip the comment and assert the
+ next executable line to be executed is line 5 at
+ 'my_dict = {'
+ """
+ importlib.reload(normalizeSelection)
+ src = textwrap.dedent(
+ """\
+ def my_func():
+ print("line 2")
+ print("line 3")
+ # Skip line 4 because it is a comment
+ my_dict = {
+ "key1": "value1",
+ "key2": "value2"
+ }
+ """
+ )
+
+ result = normalizeSelection.traverse_file(src, 1, 1, False)
+
+ assert result["which_line_next"] == 5
+
+
+def test_cursor_forloop():
+ importlib.reload(normalizeSelection)
+ src = textwrap.dedent(
+ """\
+ lucid_dream = ["Corgi", "Husky", "Pomsky"]
+ for dogs in lucid_dream: # initial starting position
+ print(dogs)
+ print("I wish I had a dog!")
+
+ print("This should be the next block that should be ran")
+ """
+ )
+
+ result = normalizeSelection.traverse_file(src, 2, 2, False)
+
+ assert result["which_line_next"] == 6
+
+
+def test_inside_forloop():
+ importlib.reload(normalizeSelection)
+ src = textwrap.dedent(
+ """\
+ for food in lucid_dream:
+ print("We are starting") # initial starting position
+ print("Next cursor should be here!")
+
+ """
+ )
+
+ result = normalizeSelection.traverse_file(src, 2, 2, False)
+
+ assert result["which_line_next"] == 3
+
+
+def test_skip_sameline_statements():
+ importlib.reload(normalizeSelection)
+ src = textwrap.dedent(
+ """\
+ print("Audi");print("BMW");print("Mercedes")
+ print("Next line to be run is here!")
+ """
+ )
+ result = normalizeSelection.traverse_file(src, 1, 1, False)
+
+ assert result["which_line_next"] == 2
+
+
+def test_skip_multi_comp_lambda():
+ importlib.reload(normalizeSelection)
+ src = textwrap.dedent(
+ """\
+ (
+ my_first_var
+ for my_first_var in range(1, 10)
+ if my_first_var % 2 == 0
+ )
+
+ my_lambda = lambda x: (
+ x + 1
+ )
+ """
+ )
+
+ result = normalizeSelection.traverse_file(src, 1, 1, False)
+ # Shift enter from the very first ( should make
+ # next executable statement as the lambda expression
+ assert result["which_line_next"] == 7
+
+
+def test_move_whole_class():
+ """
+ Shift+enter on a class definition
+ should move the cursor after running whole class.
+ """
+ importlib.reload(normalizeSelection)
+ src = textwrap.dedent(
+ """\
+ class Stub(object):
+ def __init__(self):
+ self.calls = []
+
+ def add_call(self, name, args=None, kwargs=None):
+ self.calls.append((name, args, kwargs))
+ print("We should be here after running whole class")
+ """
+ )
+ result = normalizeSelection.traverse_file(src, 1, 1, False)
+
+ assert result["which_line_next"] == 7
+
+
+def test_def_to_def():
+ importlib.reload(normalizeSelection)
+ src = textwrap.dedent(
+ """\
+ def my_dogs():
+ print("Corgi")
+ print("Husky")
+ print("Corgi2")
+ print("Husky2")
+ print("no dogs")
+
+ # Skip here
+ def next_func():
+ print("Not here but above")
+ """
+ )
+ result = normalizeSelection.traverse_file(src, 1, 1, False)
+
+ assert result["which_line_next"] == 9
+
+
+def test_try_catch_move():
+ importlib.reload(normalizeSelection)
+ src = textwrap.dedent(
+ """\
+ try:
+ 1+1
+ except:
+ print("error")
+
+ print("Should be here afterwards")
+ """
+ )
+
+ result = normalizeSelection.traverse_file(src, 1, 1, False)
+ assert result["which_line_next"] == 6
+
+
+def test_skip_nested():
+ importlib.reload(normalizeSelection)
+ src = textwrap.dedent(
+ """\
+ for i in range(1, 6):
+ for j in range(1, 6):
+ for x in range(1, 5):
+ for y in range(1, 5):
+ for z in range(1,10):
+ print(i, j, x, y, z)
+
+ print("Cursor should be here after running line 1")
+ """
+ )
+ result = normalizeSelection.traverse_file(src, 1, 1, False)
+ assert result["which_line_next"] == 8
diff --git a/pythonFiles/tests/test_normalize_selection.py b/pythonFiles/tests/test_normalize_selection.py
index 138c5ad2f522..5f4d6d7d4a1f 100644
--- a/pythonFiles/tests/test_normalize_selection.py
+++ b/pythonFiles/tests/test_normalize_selection.py
@@ -1,8 +1,12 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.
+
+import importlib
import textwrap
+# __file__ = "/Users/anthonykim/Desktop/vscode-python/pythonFiles/normalizeSelection.py"
+# sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__))))
import normalizeSelection
@@ -215,3 +219,52 @@ def show_something():
)
result = normalizeSelection.normalize_lines(src)
assert result == expected
+
+ def test_fstring(self):
+ importlib.reload(normalizeSelection)
+ src = textwrap.dedent(
+ """\
+ name = "Ahri"
+ age = 10
+
+ print(f'My name is {name}')
+ """
+ )
+
+ expected = textwrap.dedent(
+ """\
+ name = "Ahri"
+ age = 10
+ print(f'My name is {name}')
+ """
+ )
+ result = normalizeSelection.normalize_lines(src)
+
+ assert result == expected
+
+ def test_list_comp(self):
+ importlib.reload(normalizeSelection)
+ src = textwrap.dedent(
+ """\
+ names = ['Ahri', 'Bobby', 'Charlie']
+ breed = ['Pomeranian', 'Welsh Corgi', 'Siberian Husky']
+ dogs = [(name, breed) for name, breed in zip(names, breed)]
+
+ print(dogs)
+ my_family_dog = 'Corgi'
+ """
+ )
+
+ expected = textwrap.dedent(
+ """\
+ names = ['Ahri', 'Bobby', 'Charlie']
+ breed = ['Pomeranian', 'Welsh Corgi', 'Siberian Husky']
+ dogs = [(name, breed) for name, breed in zip(names, breed)]
+ print(dogs)
+ my_family_dog = 'Corgi'
+ """
+ )
+
+ result = normalizeSelection.normalize_lines(src)
+
+ assert result == expected
diff --git a/pythonFiles/tests/test_smart_selection.py b/pythonFiles/tests/test_smart_selection.py
new file mode 100644
index 000000000000..b86e6f9dc82e
--- /dev/null
+++ b/pythonFiles/tests/test_smart_selection.py
@@ -0,0 +1,388 @@
+import importlib
+import textwrap
+
+import normalizeSelection
+
+
+def test_part_dictionary():
+ importlib.reload(normalizeSelection)
+ src = textwrap.dedent(
+ """\
+ not_dictionary = 'hi'
+ my_dict = {
+ "key1": "value1",
+ "key2": "value2"
+ }
+ print('only send the dictionary')
+ """
+ )
+
+ expected = textwrap.dedent(
+ """\
+ my_dict = {
+ "key1": "value1",
+ "key2": "value2"
+ }
+ """
+ )
+
+ result = normalizeSelection.traverse_file(src, 3, 3, False)
+ assert result["normalized_smart_result"] == expected
+
+
+def test_nested_loop():
+ importlib.reload(normalizeSelection)
+ src = textwrap.dedent(
+ """\
+ for i in range(1, 6):
+ for j in range(1, 6):
+ for x in range(1, 5):
+ for y in range(1, 5):
+ for z in range(1,10):
+ print(i, j, x, y, z)
+ """
+ )
+ expected = textwrap.dedent(
+ """\
+ for i in range(1, 6):
+ for j in range(1, 6):
+ for x in range(1, 5):
+ for y in range(1, 5):
+ for z in range(1,10):
+ print(i, j, x, y, z)
+
+ """
+ )
+ result = normalizeSelection.traverse_file(src, 1, 1, False)
+ assert result["normalized_smart_result"] == expected
+
+
+def test_smart_shift_enter_multiple_statements():
+ importlib.reload(normalizeSelection)
+ src = textwrap.dedent(
+ """\
+ import textwrap
+ import ast
+
+ print("Porsche")
+ print("Genesis")
+
+
+ print("Audi");print("BMW");print("Mercedes")
+
+ print("dont print me")
+
+ """
+ )
+ # Expected to printing statement line by line,
+ # for when multiple print statements are ran
+ # from the same line.
+ expected = textwrap.dedent(
+ """\
+ print("Audi")
+ print("BMW")
+ print("Mercedes")
+ """
+ )
+ result = normalizeSelection.traverse_file(src, 8, 8, False)
+ assert result["normalized_smart_result"] == expected
+
+
+def test_two_layer_dictionary():
+ importlib.reload(normalizeSelection)
+ src = textwrap.dedent(
+ """\
+ print("dont print me")
+
+ two_layered_dictionary = {
+ 'inner_dict_one': {
+ 'Audi': 'Germany',
+ 'BMW': 'Germnay',
+ 'Genesis': 'Korea',
+ },
+ 'inner_dict_two': {
+ 'Mercedes': 'Germany',
+ 'Porsche': 'Germany',
+ 'Lamborghini': 'Italy',
+ 'Ferrari': 'Italy',
+ 'Maserati': 'Italy'
+ }
+ }
+ """
+ )
+ expected = textwrap.dedent(
+ """\
+ two_layered_dictionary = {
+ 'inner_dict_one': {
+ 'Audi': 'Germany',
+ 'BMW': 'Germnay',
+ 'Genesis': 'Korea',
+ },
+ 'inner_dict_two': {
+ 'Mercedes': 'Germany',
+ 'Porsche': 'Germany',
+ 'Lamborghini': 'Italy',
+ 'Ferrari': 'Italy',
+ 'Maserati': 'Italy'
+ }
+ }
+ """
+ )
+ result = normalizeSelection.traverse_file(src, 6, 7, False)
+
+ assert result["normalized_smart_result"] == expected
+
+
+def test_run_whole_func():
+ importlib.reload(normalizeSelection)
+ src = textwrap.dedent(
+ """\
+ print("Decide which dog you will choose")
+ def my_dogs():
+ print("Corgi")
+ print("Husky")
+ print("Corgi2")
+ print("Husky2")
+ print("no dogs")
+ """
+ )
+
+ expected = textwrap.dedent(
+ """\
+ def my_dogs():
+ print("Corgi")
+ print("Husky")
+ print("Corgi2")
+ print("Husky2")
+ print("no dogs")
+
+ """
+ )
+ result = normalizeSelection.traverse_file(src, 2, 2, False)
+
+ assert result["normalized_smart_result"] == expected
+
+
+def test_small_forloop():
+ importlib.reload(normalizeSelection)
+ src = textwrap.dedent(
+ """\
+ for i in range(1, 6):
+ print(i)
+ print("Please also send this print statement")
+ """
+ )
+ expected = textwrap.dedent(
+ """\
+ for i in range(1, 6):
+ print(i)
+ print("Please also send this print statement")
+
+ """
+ )
+
+ # Cover the whole for loop block with multiple inner statements
+ # Make sure to contain all of the print statements included.
+ result = normalizeSelection.traverse_file(src, 1, 1, False)
+
+ assert result["normalized_smart_result"] == expected
+
+
+def inner_for_loop_component():
+ """
+ Pressing shift+enter inside a for loop,
+ specifically on a viable expression
+ by itself, such as print(i)
+ should only return that exact expression
+ """
+ importlib.reload(normalizeSelection)
+ src = textwrap.dedent(
+ """\
+ for i in range(1, 6):
+ print(i)
+ print("Please also send this print statement")
+ """
+ )
+ result = normalizeSelection.traverse_file(src, 2, 2, False)
+ expected = textwrap.dedent(
+ """\
+ print(i)
+ """
+ )
+
+ assert result["normalized_smart_result"] == expected
+
+
+def test_dict_comprehension():
+ """
+ Having the mouse cursor on the first line,
+ and pressing shift+enter should return the
+ whole dictionary comp, respecting user's code style.
+ """
+
+ importlib.reload
+ src = textwrap.dedent(
+ """\
+ my_dict_comp = {temp_mover:
+ temp_mover for temp_mover in range(1, 7)}
+ """
+ )
+
+ expected = textwrap.dedent(
+ """\
+ my_dict_comp = {temp_mover:
+ temp_mover for temp_mover in range(1, 7)}
+ """
+ )
+
+ result = normalizeSelection.traverse_file(src, 1, 1, False)
+
+ assert result["normalized_smart_result"] == expected
+
+
+def test_send_whole_generator():
+ """
+ Pressing shift+enter on the first line, which is the '('
+ should be returning the whole generator expression instead of just the '('
+ """
+
+ importlib.reload(normalizeSelection)
+ src = textwrap.dedent(
+ """\
+ (
+ my_first_var
+ for my_first_var in range(1, 10)
+ if my_first_var % 2 == 0
+ )
+ """
+ )
+
+ expected = textwrap.dedent(
+ """\
+ (
+ my_first_var
+ for my_first_var in range(1, 10)
+ if my_first_var % 2 == 0
+ )
+
+ """
+ )
+
+ result = normalizeSelection.traverse_file(src, 1, 1, False)
+
+ assert result["normalized_smart_result"] == expected
+
+
+def test_multiline_lambda():
+ """
+ Shift+enter on part of the lambda expression
+ should return the whole lambda expression,
+ regardless of whether all the component of
+ lambda expression is on the same or not.
+ """
+
+ importlib.reload(normalizeSelection)
+ src = textwrap.dedent(
+ """\
+ my_lambda = lambda x: (
+ x + 1
+ )
+ """
+ )
+ expected = textwrap.dedent(
+ """\
+ my_lambda = lambda x: (
+ x + 1
+ )
+
+ """
+ )
+
+ result = normalizeSelection.traverse_file(src, 1, 1, False)
+ assert result["normalized_smart_result"] == expected
+
+
+def test_send_whole_class():
+ """
+ Shift+enter on a class definition
+ should send the whole class definition
+ """
+ importlib.reload(normalizeSelection)
+ src = textwrap.dedent(
+ """\
+ class Stub(object):
+ def __init__(self):
+ self.calls = []
+
+ def add_call(self, name, args=None, kwargs=None):
+ self.calls.append((name, args, kwargs))
+ print("We should be here after running whole class")
+ """
+ )
+ result = normalizeSelection.traverse_file(src, 1, 1, False)
+ expected = textwrap.dedent(
+ """\
+ class Stub(object):
+ def __init__(self):
+ self.calls = []
+ def add_call(self, name, args=None, kwargs=None):
+ self.calls.append((name, args, kwargs))
+
+ """
+ )
+ assert result["normalized_smart_result"] == expected
+
+
+def test_send_whole_if_statement():
+ """
+ Shift+enter on an if statement
+ should send the whole if statement
+ including statements inside and else.
+ """
+ importlib.reload(normalizeSelection)
+ src = textwrap.dedent(
+ """\
+ if True:
+ print('send this')
+ else:
+ print('also send this')
+
+ print('cursor here afterwards')
+ """
+ )
+ expected = textwrap.dedent(
+ """\
+ if True:
+ print('send this')
+ else:
+ print('also send this')
+
+ """
+ )
+ result = normalizeSelection.traverse_file(src, 1, 1, False)
+ assert result["normalized_smart_result"] == expected
+
+
+def test_send_try():
+ importlib.reload(normalizeSelection)
+ src = textwrap.dedent(
+ """\
+ try:
+ 1+1
+ except:
+ print("error")
+
+ print("Not running this")
+ """
+ )
+ expected = textwrap.dedent(
+ """\
+ try:
+ 1+1
+ except:
+ print("error")
+
+ """
+ )
+ result = normalizeSelection.traverse_file(src, 1, 1, False)
+ assert result["normalized_smart_result"] == expected
diff --git a/src/client/common/application/commands.ts b/src/client/common/application/commands.ts
index 763fa4dde79d..ba29f0dcd956 100644
--- a/src/client/common/application/commands.ts
+++ b/src/client/common/application/commands.ts
@@ -99,4 +99,12 @@ export interface ICommandNameArgumentTypeMapping extends ICommandNameWithoutArgu
[Commands.Tests_Configure]: [undefined, undefined | CommandSource, undefined | Uri];
[Commands.LaunchTensorBoard]: [TensorBoardEntrypoint, TensorBoardEntrypointTrigger];
['workbench.view.testing.focus']: [];
+ ['cursorMove']: [
+ {
+ to: string;
+ by: string;
+ value: number;
+ },
+ ];
+ ['cursorEnd']: [];
}
diff --git a/src/client/common/experiments/groups.ts b/src/client/common/experiments/groups.ts
index 1ee06469095c..b7a598e0a08a 100644
--- a/src/client/common/experiments/groups.ts
+++ b/src/client/common/experiments/groups.ts
@@ -18,3 +18,7 @@ export enum ShowFormatterExtensionPrompt {
export enum EnableTestAdapterRewrite {
experiment = 'pythonTestAdapter',
}
+// Experiment to enable smart shift+enter, advance cursor.
+export enum EnableREPLSmartSend {
+ experiment = 'pythonREPLSmartSend',
+}
diff --git a/src/client/telemetry/index.ts b/src/client/telemetry/index.ts
index 7883e0cd7555..95496c828018 100644
--- a/src/client/telemetry/index.ts
+++ b/src/client/telemetry/index.ts
@@ -821,11 +821,11 @@ export interface IEventNamePropertyMapping {
*/
[EventName.EXECUTION_CODE]: {
/**
- * Whether the user executed a file in the terminal or just the selected text.
+ * Whether the user executed a file in the terminal or just the selected text or line by shift+enter.
*
* @type {('file' | 'selection')}
*/
- scope: 'file' | 'selection';
+ scope: 'file' | 'selection' | 'line';
/**
* How was the code executed (through the command or by clicking the `Run File` icon).
*
diff --git a/src/client/terminals/codeExecution/codeExecutionManager.ts b/src/client/terminals/codeExecution/codeExecutionManager.ts
index 2dd619a1816a..ed31e194b2d2 100644
--- a/src/client/terminals/codeExecution/codeExecutionManager.ts
+++ b/src/client/terminals/codeExecution/codeExecutionManager.ts
@@ -159,7 +159,11 @@ export class CodeExecutionManager implements ICodeExecutionManager {
}
const codeExecutionHelper = this.serviceContainer.get(ICodeExecutionHelper);
const codeToExecute = await codeExecutionHelper.getSelectedTextToExecute(activeEditor!);
- const normalizedCode = await codeExecutionHelper.normalizeLines(codeToExecute!);
+ let wholeFileContent = '';
+ if (activeEditor && activeEditor.document) {
+ wholeFileContent = activeEditor.document.getText();
+ }
+ const normalizedCode = await codeExecutionHelper.normalizeLines(codeToExecute!, wholeFileContent);
if (!normalizedCode || normalizedCode.trim().length === 0) {
return;
}
diff --git a/src/client/terminals/codeExecution/helper.ts b/src/client/terminals/codeExecution/helper.ts
index 0d5694b4a28d..c560de9c17b7 100644
--- a/src/client/terminals/codeExecution/helper.ts
+++ b/src/client/terminals/codeExecution/helper.ts
@@ -5,7 +5,12 @@ import '../../common/extensions';
import { inject, injectable } from 'inversify';
import { l10n, Position, Range, TextEditor, Uri } from 'vscode';
-import { IApplicationShell, IDocumentManager, IWorkspaceService } from '../../common/application/types';
+import {
+ IApplicationShell,
+ ICommandManager,
+ IDocumentManager,
+ IWorkspaceService,
+} from '../../common/application/types';
import { PYTHON_LANGUAGE } from '../../common/constants';
import * as internalScripts from '../../common/process/internal/scripts';
import { IProcessServiceFactory } from '../../common/process/types';
@@ -14,7 +19,10 @@ import { IInterpreterService } from '../../interpreter/contracts';
import { IServiceContainer } from '../../ioc/types';
import { ICodeExecutionHelper } from '../types';
import { traceError } from '../../logging';
-import { Resource } from '../../common/types';
+import { IConfigurationService, IExperimentService, Resource } from '../../common/types';
+import { EnableREPLSmartSend } from '../../common/experiments/groups';
+import { sendTelemetryEvent } from '../../telemetry';
+import { EventName } from '../../telemetry/constants';
@injectable()
export class CodeExecutionHelper implements ICodeExecutionHelper {
@@ -26,14 +34,22 @@ export class CodeExecutionHelper implements ICodeExecutionHelper {
private readonly interpreterService: IInterpreterService;
+ private readonly commandManager: ICommandManager;
+
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-expect-error TS6133: 'configSettings' is declared but its value is never read.
+ private readonly configSettings: IConfigurationService;
+
constructor(@inject(IServiceContainer) private readonly serviceContainer: IServiceContainer) {
this.documentManager = serviceContainer.get(IDocumentManager);
this.applicationShell = serviceContainer.get(IApplicationShell);
this.processServiceFactory = serviceContainer.get(IProcessServiceFactory);
this.interpreterService = serviceContainer.get(IInterpreterService);
+ this.configSettings = serviceContainer.get(IConfigurationService);
+ this.commandManager = serviceContainer.get(ICommandManager);
}
- public async normalizeLines(code: string, resource?: Uri): Promise {
+ public async normalizeLines(code: string, wholeFileContent?: string, resource?: Uri): Promise {
try {
if (code.trim().length === 0) {
return '';
@@ -42,6 +58,7 @@ export class CodeExecutionHelper implements ICodeExecutionHelper {
// So just remove cr from the input.
code = code.replace(new RegExp('\\r', 'g'), '');
+ const activeEditor = this.documentManager.activeTextEditor;
const interpreter = await this.interpreterService.getActiveInterpreter(resource);
const processService = await this.processServiceFactory.create(resource);
@@ -63,10 +80,24 @@ export class CodeExecutionHelper implements ICodeExecutionHelper {
normalizeOutput.resolve(normalized);
},
});
-
+ // If there is no explicit selection, we are exeucting 'line' or 'block'.
+ if (activeEditor?.selection?.isEmpty) {
+ sendTelemetryEvent(EventName.EXECUTION_CODE, undefined, { scope: 'line' });
+ }
// The normalization script expects a serialized JSON object, with the selection under the "code" key.
// We're using a JSON object so that we don't have to worry about encoding, or escaping non-ASCII characters.
- const input = JSON.stringify({ code });
+ const startLineVal = activeEditor?.selection?.start.line ?? 0;
+ const endLineVal = activeEditor?.selection?.end.line ?? 0;
+ const emptyHighlightVal = activeEditor?.selection?.isEmpty ?? true;
+ const smartSendExperimentEnabledVal = pythonSmartSendEnabled(this.serviceContainer);
+ const input = JSON.stringify({
+ code,
+ wholeFileContent,
+ startLine: startLineVal,
+ endLine: endLineVal,
+ emptyHighlight: emptyHighlightVal,
+ smartSendExperimentEnabled: smartSendExperimentEnabledVal,
+ });
observable.proc?.stdin?.write(input);
observable.proc?.stdin?.end();
@@ -74,6 +105,11 @@ export class CodeExecutionHelper implements ICodeExecutionHelper {
const result = await normalizeOutput.promise;
const object = JSON.parse(result);
+ if (activeEditor?.selection) {
+ const lineOffset = object.nextBlockLineno - activeEditor!.selection.start.line - 1;
+ await this.moveToNextBlock(lineOffset, activeEditor);
+ }
+
return parse(object.normalized);
} catch (ex) {
traceError(ex, 'Python: Failed to normalize code for execution in terminal');
@@ -81,6 +117,30 @@ export class CodeExecutionHelper implements ICodeExecutionHelper {
}
}
+ /**
+ * Depending on whether or not user is in experiment for smart send,
+ * dynamically move the cursor to the next block of code.
+ * The cursor movement is not moved by one everytime,
+ * since with the smart selection, the next executable code block
+ * can be multiple lines away.
+ * Intended to provide smooth shift+enter user experience
+ * bringing user's cursor to the next executable block of code when used with smart selection.
+ */
+ // eslint-disable-next-line class-methods-use-this
+ private async moveToNextBlock(lineOffset: number, activeEditor?: TextEditor): Promise {
+ if (pythonSmartSendEnabled(this.serviceContainer)) {
+ if (activeEditor?.selection?.isEmpty) {
+ await this.commandManager.executeCommand('cursorMove', {
+ to: 'down',
+ by: 'line',
+ value: Number(lineOffset),
+ });
+ await this.commandManager.executeCommand('cursorEnd');
+ }
+ }
+ return Promise.resolve();
+ }
+
public async getFileToExecute(): Promise {
const activeEditor = this.documentManager.activeTextEditor;
if (!activeEditor) {
@@ -110,6 +170,7 @@ export class CodeExecutionHelper implements ICodeExecutionHelper {
const { selection } = textEditor;
let code: string;
+
if (selection.isEmpty) {
code = textEditor.document.lineAt(selection.start.line).text;
} else if (selection.isSingleLine) {
@@ -117,6 +178,7 @@ export class CodeExecutionHelper implements ICodeExecutionHelper {
} else {
code = getMultiLineSelectionText(textEditor);
}
+
return code;
}
@@ -235,3 +297,9 @@ function getMultiLineSelectionText(textEditor: TextEditor): string {
// ↑<---------------- To here
return selectionText;
}
+
+function pythonSmartSendEnabled(serviceContainer: IServiceContainer): boolean {
+ const experiment = serviceContainer.get(IExperimentService);
+
+ return experiment ? experiment.inExperimentSync(EnableREPLSmartSend.experiment) : false;
+}
diff --git a/src/client/terminals/types.ts b/src/client/terminals/types.ts
index 47ac16d9e08b..48e39d4e1c81 100644
--- a/src/client/terminals/types.ts
+++ b/src/client/terminals/types.ts
@@ -15,7 +15,7 @@ export interface ICodeExecutionService {
export const ICodeExecutionHelper = Symbol('ICodeExecutionHelper');
export interface ICodeExecutionHelper {
- normalizeLines(code: string): Promise;
+ normalizeLines(code: string, wholeFileContent?: string): Promise;
getFileToExecute(): Promise;
saveFileIfDirty(file: Uri): Promise;
getSelectedTextToExecute(textEditor: TextEditor): Promise;
diff --git a/src/test/pythonFiles/terminalExec/sample_smart_selection.py b/src/test/pythonFiles/terminalExec/sample_smart_selection.py
new file mode 100644
index 000000000000..3933f06b5d65
--- /dev/null
+++ b/src/test/pythonFiles/terminalExec/sample_smart_selection.py
@@ -0,0 +1,21 @@
+my_dict = {
+ "key1": "value1",
+ "key2": "value2"
+}
+#Sample
+
+print("Audi");print("BMW");print("Mercedes")
+
+# print("dont print me")
+
+def my_dogs():
+ print("Corgi")
+ print("Husky")
+ print("Corgi2")
+ print("Husky2")
+ print("no dogs")
+
+# Skip me to prove that you did a good job
+def next_func():
+ print("You")
+
diff --git a/src/test/smoke/smartSend.smoke.test.ts b/src/test/smoke/smartSend.smoke.test.ts
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/src/test/terminals/codeExecution/smartSend.test.ts b/src/test/terminals/codeExecution/smartSend.test.ts
new file mode 100644
index 000000000000..8d70ab6e01e0
--- /dev/null
+++ b/src/test/terminals/codeExecution/smartSend.test.ts
@@ -0,0 +1,229 @@
+import * as TypeMoq from 'typemoq';
+import * as path from 'path';
+import { TextEditor, Selection, Position, TextDocument } from 'vscode';
+import * as fs from 'fs-extra';
+import { SemVer } from 'semver';
+import { assert, expect } from 'chai';
+import { IApplicationShell, ICommandManager, IDocumentManager } from '../../../client/common/application/types';
+import { IProcessService, IProcessServiceFactory } from '../../../client/common/process/types';
+import { IInterpreterService } from '../../../client/interpreter/contracts';
+import { IConfigurationService, IExperimentService } from '../../../client/common/types';
+import { CodeExecutionHelper } from '../../../client/terminals/codeExecution/helper';
+import { IServiceContainer } from '../../../client/ioc/types';
+import { ICodeExecutionHelper } from '../../../client/terminals/types';
+import { EnableREPLSmartSend } from '../../../client/common/experiments/groups';
+import { EXTENSION_ROOT_DIR } from '../../../client/common/constants';
+import { EnvironmentType, PythonEnvironment } from '../../../client/pythonEnvironments/info';
+import { PYTHON_PATH } from '../../common';
+import { Architecture } from '../../../client/common/utils/platform';
+import { ProcessService } from '../../../client/common/process/proc';
+
+const TEST_FILES_PATH = path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'pythonFiles', 'terminalExec');
+
+suite('REPL - Smart Send', () => {
+ let documentManager: TypeMoq.IMock;
+ let applicationShell: TypeMoq.IMock;
+
+ let interpreterService: TypeMoq.IMock;
+ let commandManager: TypeMoq.IMock;
+
+ let processServiceFactory: TypeMoq.IMock;
+ let configurationService: TypeMoq.IMock;
+
+ let serviceContainer: TypeMoq.IMock;
+ let codeExecutionHelper: ICodeExecutionHelper;
+ let experimentService: TypeMoq.IMock;
+
+ let processService: TypeMoq.IMock;
+
+ let document: TypeMoq.IMock;
+ const workingPython: PythonEnvironment = {
+ path: PYTHON_PATH,
+ version: new SemVer('3.6.6-final'),
+ sysVersion: '1.0.0.0',
+ sysPrefix: 'Python',
+ displayName: 'Python',
+ envType: EnvironmentType.Unknown,
+ architecture: Architecture.x64,
+ };
+
+ // suite set up only run once for each suite. Very start
+ // set up --- before each test
+ // tests -- actual tests
+ // tear down -- run after each test
+ // suite tear down only run once at the very end.
+
+ // all object that is common to every test. What each test needs
+ setup(() => {
+ documentManager = TypeMoq.Mock.ofType();
+ applicationShell = TypeMoq.Mock.ofType();
+ processServiceFactory = TypeMoq.Mock.ofType();
+ interpreterService = TypeMoq.Mock.ofType();
+ commandManager = TypeMoq.Mock.ofType();
+ configurationService = TypeMoq.Mock.ofType();
+ serviceContainer = TypeMoq.Mock.ofType();
+ experimentService = TypeMoq.Mock.ofType();
+ processService = TypeMoq.Mock.ofType();
+
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ processService.setup((x: any) => x.then).returns(() => undefined);
+ serviceContainer
+ .setup((c) => c.get(TypeMoq.It.isValue(IDocumentManager)))
+ .returns(() => documentManager.object);
+ serviceContainer
+ .setup((c) => c.get(TypeMoq.It.isValue(IApplicationShell)))
+ .returns(() => applicationShell.object);
+ processServiceFactory
+ .setup((p) => p.create(TypeMoq.It.isAny()))
+ .returns(() => Promise.resolve(processService.object));
+ serviceContainer
+ .setup((c) => c.get(TypeMoq.It.isValue(IProcessServiceFactory)))
+ .returns(() => processServiceFactory.object);
+ serviceContainer
+ .setup((c) => c.get(TypeMoq.It.isValue(IInterpreterService)))
+ .returns(() => interpreterService.object);
+ serviceContainer.setup((c) => c.get(TypeMoq.It.isValue(ICommandManager))).returns(() => commandManager.object);
+ serviceContainer
+ .setup((c) => c.get(TypeMoq.It.isValue(IConfigurationService)))
+ .returns(() => configurationService.object);
+ serviceContainer
+ .setup((s) => s.get(TypeMoq.It.isValue(IExperimentService)))
+ .returns(() => experimentService.object);
+ interpreterService
+ .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny()))
+ .returns(() => Promise.resolve(workingPython));
+
+ codeExecutionHelper = new CodeExecutionHelper(serviceContainer.object);
+ document = TypeMoq.Mock.ofType();
+ });
+
+ test('Cursor is not moved when explicit selection is present', async () => {
+ experimentService
+ .setup((exp) => exp.inExperimentSync(TypeMoq.It.isValue(EnableREPLSmartSend.experiment)))
+ .returns(() => true);
+
+ const activeEditor = TypeMoq.Mock.ofType();
+ const firstIndexPosition = new Position(0, 0);
+ const selection = TypeMoq.Mock.ofType();
+ const wholeFileContent = await fs.readFile(path.join(TEST_FILES_PATH, `sample_smart_selection.py`), 'utf8');
+
+ selection.setup((s) => s.anchor).returns(() => firstIndexPosition);
+ selection.setup((s) => s.active).returns(() => firstIndexPosition);
+ selection.setup((s) => s.isEmpty).returns(() => false);
+ activeEditor.setup((e) => e.selection).returns(() => selection.object);
+
+ documentManager.setup((d) => d.activeTextEditor).returns(() => activeEditor.object);
+ document.setup((d) => d.getText(TypeMoq.It.isAny())).returns(() => wholeFileContent);
+ const actualProcessService = new ProcessService();
+
+ const { execObservable } = actualProcessService;
+
+ processService
+ .setup((p) => p.execObservable(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()))
+ .returns((file, args, options) => execObservable.apply(actualProcessService, [file, args, options]));
+
+ await codeExecutionHelper.normalizeLines('my_dict = {', wholeFileContent);
+
+ commandManager
+ .setup((c) => c.executeCommand('cursorMove', TypeMoq.It.isAny()))
+ .callback((_, arg2) => {
+ assert.deepEqual(arg2, {
+ to: 'down',
+ by: 'line',
+ value: 3,
+ });
+ return Promise.resolve();
+ })
+ .verifiable(TypeMoq.Times.never());
+
+ commandManager
+ .setup((c) => c.executeCommand('cursorEnd'))
+ .returns(() => Promise.resolve())
+ .verifiable(TypeMoq.Times.never());
+
+ commandManager.verifyAll();
+ });
+
+ test('Smart send should perform smart selection and move cursor', async () => {
+ experimentService
+ .setup((exp) => exp.inExperimentSync(TypeMoq.It.isValue(EnableREPLSmartSend.experiment)))
+ .returns(() => true);
+
+ const activeEditor = TypeMoq.Mock.ofType();
+ const firstIndexPosition = new Position(0, 0);
+ const selection = TypeMoq.Mock.ofType();
+ const wholeFileContent = await fs.readFile(path.join(TEST_FILES_PATH, `sample_smart_selection.py`), 'utf8');
+
+ selection.setup((s) => s.anchor).returns(() => firstIndexPosition);
+ selection.setup((s) => s.active).returns(() => firstIndexPosition);
+ selection.setup((s) => s.isEmpty).returns(() => true);
+ activeEditor.setup((e) => e.selection).returns(() => selection.object);
+
+ documentManager.setup((d) => d.activeTextEditor).returns(() => activeEditor.object);
+ document.setup((d) => d.getText(TypeMoq.It.isAny())).returns(() => wholeFileContent);
+ const actualProcessService = new ProcessService();
+
+ const { execObservable } = actualProcessService;
+
+ processService
+ .setup((p) => p.execObservable(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()))
+ .returns((file, args, options) => execObservable.apply(actualProcessService, [file, args, options]));
+
+ const actualSmartOutput = await codeExecutionHelper.normalizeLines('my_dict = {', wholeFileContent);
+
+ // my_dict = { <----- smart shift+enter here
+ // "key1": "value1",
+ // "key2": "value2"
+ // } <---- cursor should be here afterwards, hence offset 3
+ commandManager
+ .setup((c) => c.executeCommand('cursorMove', TypeMoq.It.isAny()))
+ .callback((_, arg2) => {
+ assert.deepEqual(arg2, {
+ to: 'down',
+ by: 'line',
+ value: 3,
+ });
+ return Promise.resolve();
+ })
+ .verifiable(TypeMoq.Times.once());
+
+ commandManager
+ .setup((c) => c.executeCommand('cursorEnd'))
+ .returns(() => Promise.resolve())
+ .verifiable(TypeMoq.Times.once());
+
+ const expectedSmartOutput = 'my_dict = {\n "key1": "value1",\n "key2": "value2"\n}\n';
+ expect(actualSmartOutput).to.be.equal(expectedSmartOutput);
+ commandManager.verifyAll();
+ });
+
+ // Do not perform smart selection when there is explicit selection
+ test('Smart send should not perform smart selection when there is explicit selection', async () => {
+ experimentService
+ .setup((exp) => exp.inExperimentSync(TypeMoq.It.isValue(EnableREPLSmartSend.experiment)))
+ .returns(() => true);
+ const activeEditor = TypeMoq.Mock.ofType();
+ const firstIndexPosition = new Position(0, 0);
+ const selection = TypeMoq.Mock.ofType();
+ const wholeFileContent = await fs.readFile(path.join(TEST_FILES_PATH, `sample_smart_selection.py`), 'utf8');
+
+ selection.setup((s) => s.anchor).returns(() => firstIndexPosition);
+ selection.setup((s) => s.active).returns(() => firstIndexPosition);
+ selection.setup((s) => s.isEmpty).returns(() => false);
+ activeEditor.setup((e) => e.selection).returns(() => selection.object);
+
+ documentManager.setup((d) => d.activeTextEditor).returns(() => activeEditor.object);
+ document.setup((d) => d.getText(TypeMoq.It.isAny())).returns(() => wholeFileContent);
+ const actualProcessService = new ProcessService();
+
+ const { execObservable } = actualProcessService;
+
+ processService
+ .setup((p) => p.execObservable(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()))
+ .returns((file, args, options) => execObservable.apply(actualProcessService, [file, args, options]));
+
+ const actualNonSmartResult = await codeExecutionHelper.normalizeLines('my_dict = {', wholeFileContent);
+ const expectedNonSmartResult = 'my_dict = {\n\n'; // Standard for previous normalization logic
+ expect(actualNonSmartResult).to.be.equal(expectedNonSmartResult);
+ });
+});
diff --git a/src/testMultiRootWkspc/smokeTests/create_delete_file.py b/src/testMultiRootWkspc/smokeTests/create_delete_file.py
new file mode 100644
index 000000000000..399bc4863c15
--- /dev/null
+++ b/src/testMultiRootWkspc/smokeTests/create_delete_file.py
@@ -0,0 +1,5 @@
+with open('smart_send_smoke.txt', 'w') as f:
+ f.write('This is for smart send smoke test')
+import os
+
+os.remove('smart_send_smoke.txt')