Repository: ansible/ansible-runner

Issue:
Directory isolation copying fails in sandboxed environments
The call to `shutil.copytree` when copying the project directory to the `directory_isolation_path` fails since `shutil.copytree`'s default functionality when the target directory already exists (which is created by the call to `tempfile.mkdtemp`) is to raise `FileExistsError` (ref: https://docs.python.org/3/library/shutil.html).

https://github.com/ansible/ansible-runner/blob/0594cea6e415af0d186b611e95e04195317cf816/src/ansible_runner/config/runner.py#L138C60-L138C60

We're running Python 3.11.5 in our environment.

Discussion:
Shrews: We need to dig into the history of this. It seems there is a `directory_isolation_cleanup` configuration setting that doesn't appear to be exposed that is supposed to control cleaning up of the isolation directory. I'm not certain we should _always_ just blindly overwrite it based on the existence of this setting.
christophert: It looks like that setting would control cleaning it up once the run was completed. This change allows `copytree` to copy into the newly made directory by `tempfile.mkdtemp` in line 135, not any existing project isolation directory created by previous runs.

Patch:
diff --git a/docs/requirements.txt b/docs/requirements.txt
index 35f8e29..0704274 100644
--- a/docs/requirements.txt
+++ b/docs/requirements.txt
@@ -30,4 +30,4 @@ sphinxcontrib-jquery==4.1
 sphinxcontrib-jsmath==1.0.1
 sphinxcontrib-qthelp==1.0.3
 sphinxcontrib-serializinghtml==1.1.5
-urllib3==1.26.15
+urllib3==1.26.17
diff --git a/src/ansible_runner/config/runner.py b/src/ansible_runner/config/runner.py
index 70379ef..adf24e8 100644
--- a/src/ansible_runner/config/runner.py
+++ b/src/ansible_runner/config/runner.py
@@ -135,7 +135,7 @@ class RunnerConfig(BaseConfig):
             self.directory_isolation_path = tempfile.mkdtemp(prefix='runner_di_', dir=self.directory_isolation_path)
             if os.path.exists(self.project_dir):
                 output.debug(f"Copying directory tree from {self.project_dir} to {self.directory_isolation_path} for working directory isolation")
-                shutil.copytree(self.project_dir, self.directory_isolation_path, symlinks=True)
+                shutil.copytree(self.project_dir, self.directory_isolation_path, dirs_exist_ok=True, symlinks=True)
 
         self.prepare_inventory()
         self.prepare_command()


Test patch:
diff --git a/test/integration/__init__.py b/test/integration/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/test/integration/conftest.py b/test/integration/conftest.py
index de098cf..559031a 100644
--- a/test/integration/conftest.py
+++ b/test/integration/conftest.py
@@ -1,13 +1,15 @@
 import json
 import os
 import subprocess
-import yaml
 import pathlib
-import pytest
-import pexpect
 import random
+
 from string import ascii_lowercase
 
+import pexpect
+import pytest
+import yaml
+
 from ansible_runner.config.runner import RunnerConfig
 
 here = pathlib.Path(__file__).parent
@@ -15,22 +17,22 @@ here = pathlib.Path(__file__).parent
 
 @pytest.fixture(scope='function')
 def rc(tmp_path):
-    rc = RunnerConfig(str(tmp_path))
-    rc.suppress_ansible_output = True
-    rc.expect_passwords = {
+    conf = RunnerConfig(str(tmp_path))
+    conf.suppress_ansible_output = True
+    conf.expect_passwords = {
         pexpect.TIMEOUT: None,
         pexpect.EOF: None
     }
-    rc.cwd = str(tmp_path)
-    rc.env = {}
-    rc.job_timeout = 10
-    rc.idle_timeout = 0
-    rc.pexpect_timeout = 2.
-    rc.pexpect_use_poll = True
-    return rc
+    conf.cwd = str(tmp_path)
+    conf.env = {}
+    conf.job_timeout = 10
+    conf.idle_timeout = 0
+    conf.pexpect_timeout = 2.
+    conf.pexpect_use_poll = True
+    return conf
 
 
-class CompletedProcessProxy(object):
+class CompletedProcessProxy:
 
     def __init__(self, result):
         self.result = result
@@ -54,7 +56,7 @@ class CompletedProcessProxy(object):
 
 
 @pytest.fixture(scope='function')
-def cli(request):
+def cli():
     def run(args, *a, **kw):
         if not kw.pop('bare', None):
             args = ['ansible-runner'] + args
@@ -73,7 +75,7 @@ def cli(request):
         })
 
         try:
-            ret = CompletedProcessProxy(subprocess.run(' '.join(args), shell=True, *a, **kw))
+            ret = CompletedProcessProxy(subprocess.run(' '.join(args), check=kw.pop('check'), shell=True, *a, **kw))
         except subprocess.CalledProcessError as err:
             pytest.fail(
                 f"Running {err.cmd} resulted in a non-zero return code: {err.returncode} - stdout: {err.stdout}, stderr: {err.stderr}"
@@ -84,7 +86,7 @@ def cli(request):
 
 
 @pytest.fixture
-def container_image(request, cli, tmp_path):
+def container_image(request, cli, tmp_path):  # pylint: disable=W0621
     try:
         containerized = request.getfixturevalue('containerized')
         if not containerized:
@@ -104,7 +106,7 @@ def container_image(request, cli, tmp_path):
         bare=True,
     )
 
-    wheel = next(tmp_path.glob('*.whl'))
+    wheel = next(tmp_path.glob('*.whl'))  # pylint: disable=R1708
 
     runtime = request.getfixturevalue('runtime')
     dockerfile_path = tmp_path / 'Dockerfile'
diff --git a/test/integration/containerized/__init__.py b/test/integration/containerized/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/test/integration/containerized/test_cli_containerized.py b/test/integration/containerized/test_cli_containerized.py
index 3f770f0..ef709e1 100644
--- a/test/integration/containerized/test_cli_containerized.py
+++ b/test/integration/containerized/test_cli_containerized.py
@@ -3,12 +3,11 @@ import os
 import signal
 import sys
 
+from test.utils.common import iterate_timeout
 from uuid import uuid4
 
 import pytest
 
-from test.utils.common import iterate_timeout
-
 
 @pytest.mark.test_all_runtimes
 def test_module_run(cli, project_fixtures, runtime, container_image):
diff --git a/test/integration/containerized/test_container_management.py b/test/integration/containerized/test_container_management.py
index 2f8212d..c11b4ff 100644
--- a/test/integration/containerized/test_container_management.py
+++ b/test/integration/containerized/test_container_management.py
@@ -14,7 +14,7 @@ from ansible_runner.interface import run
 def is_running(cli, runtime, container_name):
     cmd = [runtime, 'ps', '-aq', '--filter', f'name={container_name}']
     r = cli(cmd, bare=True)
-    output = '{}{}'.format(r.stdout, r.stderr)
+    output = f'{r.stdout}{r.stderr}'
     print(' '.join(cmd))
     print(output)
     return output.strip()
@@ -24,7 +24,7 @@ class CancelStandIn:
     def __init__(self, runtime, cli, container_name, delay=0.2):
         self.runtime = runtime
         self.cli = cli
-        self.delay = 0.2
+        self.delay = delay
         self.container_name = container_name
         self.checked_running = False
         self.start_time = None
@@ -37,7 +37,7 @@ class CancelStandIn:
             return False
         # guard against false passes by checking for running container
         if not self.checked_running:
-            for i in range(5):
+            for _ in range(5):
                 if is_running(self.cli, self.runtime, self.container_name):
                     break
                 time.sleep(0.2)
diff --git a/test/integration/test___main__.py b/test/integration/test___main__.py
index 4df07a7..912d5c5 100644
--- a/test/integration/test___main__.py
+++ b/test/integration/test___main__.py
@@ -16,7 +16,7 @@ def random_string():
 
 
 def random_json(keys=None):
-    data = dict()
+    data = {}
     if keys:
         for key in keys:
             data[key] = random_string()
diff --git a/test/integration/test_config.py b/test/integration/test_config.py
index 7096052..137daa3 100644
--- a/test/integration/test_config.py
+++ b/test/integration/test_config.py
@@ -1,8 +1,8 @@
+import os
+
 from ansible_runner.config._base import BaseConfig
 from ansible_runner.interface import run
 
-import os
-
 
 def test_combine_python_and_file_settings(project_fixtures):
     rc = BaseConfig(private_data_dir=str(project_fixtures / 'job_env'), settings={'job_timeout': 40}, container_image='bar')
@@ -32,7 +32,7 @@ def test_custom_stdout_callback_via_host_environ(project_fixtures, mocker):
     assert '"msg": "Hello world!"' in stdout, stdout
 
 
-def test_custom_stdout_callback_via_envvars(project_fixtures, mocker):
+def test_custom_stdout_callback_via_envvars(project_fixtures):
     res = run(private_data_dir=str(project_fixtures / 'debug'), playbook='debug.yml', envvars={'ANSIBLE_STDOUT_CALLBACK': 'minimal'})
     with res.stdout as f:
         stdout = f.read()
diff --git a/test/integration/test_display_callback.py b/test/integration/test_display_callback.py
index e649a1d..153b8bd 100644
--- a/test/integration/test_display_callback.py
+++ b/test/integration/test_display_callback.py
@@ -4,9 +4,10 @@ import json
 import os
 import yaml
 
+import pytest
+
 from ansible_runner.interface import init_runner
 
-import pytest
 
 HERE = os.path.abspath(os.path.dirname(__file__))
 
@@ -71,9 +72,9 @@ def executor(tmp_path, request):
     {'ANSIBLE_CALLBACK_PLUGINS': ''}],
     ids=['local-callback-plugin', 'no-callback-plugin']
 )
-def test_callback_plugin_receives_events(executor, event, playbook, envvars):
+def test_callback_plugin_receives_events(executor, event, playbook, envvars):  # pylint: disable=W0613,W0621
     executor.run()
-    assert len(list(executor.events))
+    assert list(executor.events)
     assert event in [task['event'] for task in executor.events]
 
 
@@ -156,9 +157,9 @@ def test_callback_plugin_receives_events(executor, event, playbook, envvars):
         ignore_errors: yes
 '''},  # noqa
 ])
-def test_callback_plugin_no_log_filters(executor, playbook):
+def test_callback_plugin_no_log_filters(executor, playbook):  # pylint: disable=W0613,W0621
     executor.run()
-    assert len(list(executor.events))
+    assert list(executor.events)
     assert 'SENSITIVE' not in json.dumps(list(executor.events))
 
 
@@ -175,7 +176,7 @@ def test_callback_plugin_no_log_filters(executor, playbook):
     - uri: url=https://example.org url_username="PUBLIC" url_password="PRIVATE"
 '''},  # noqa
 ])
-def test_callback_plugin_task_args_leak(executor, playbook):
+def test_callback_plugin_task_args_leak(executor, playbook):  # pylint: disable=W0613,W0621
     executor.run()
     events = list(executor.events)
     assert events[0]['event'] == 'playbook_on_start'
@@ -212,7 +213,7 @@ def test_callback_plugin_task_args_leak(executor, playbook):
         },  # noqa
     ],
 )
-def test_resolved_actions(executor, playbook, skipif_pre_ansible212):
+def test_resolved_actions(executor, playbook, skipif_pre_ansible212):  # pylint: disable=W0613,W0621
     executor.run()
     events = list(executor.events)
 
@@ -237,7 +238,7 @@ def test_resolved_actions(executor, playbook, skipif_pre_ansible212):
     - debug: msg="{{ command_register.results|map(attribute='stdout')|list }}"
 '''},  # noqa
 ])
-def test_callback_plugin_censoring_does_not_overwrite(executor, playbook):
+def test_callback_plugin_censoring_does_not_overwrite(executor, playbook):  # pylint: disable=W0613,W0621
     executor.run()
     events = list(executor.events)
     assert events[0]['event'] == 'playbook_on_start'
@@ -246,7 +247,7 @@ def test_callback_plugin_censoring_does_not_overwrite(executor, playbook):
     # task 1
     assert events[2]['event'] == 'playbook_on_task_start'
     # Ordering of task and item events may differ randomly
-    assert set(['runner_on_start', 'runner_item_on_ok', 'runner_on_ok']) == set([data['event'] for data in events[3:6]])
+    assert set(['runner_on_start', 'runner_item_on_ok', 'runner_on_ok']) == {data['event'] for data in events[3:6]}
 
     # task 2 no_log=True
     assert events[6]['event'] == 'playbook_on_task_start'
@@ -264,9 +265,9 @@ def test_callback_plugin_censoring_does_not_overwrite(executor, playbook):
     - shell: echo "Hello, World!"
 '''},  # noqa
 ])
-def test_callback_plugin_strips_task_environ_variables(executor, playbook):
+def test_callback_plugin_strips_task_environ_variables(executor, playbook):  # pylint: disable=W0613,W0621
     executor.run()
-    assert len(list(executor.events))
+    assert list(executor.events)
     for event in list(executor.events):
         assert os.environ['PATH'] not in json.dumps(event)
 
@@ -282,7 +283,7 @@ def test_callback_plugin_strips_task_environ_variables(executor, playbook):
           foo: "bar"
 '''},  # noqa
 ])
-def test_callback_plugin_saves_custom_stats(executor, playbook):
+def test_callback_plugin_saves_custom_stats(executor, playbook):  # pylint: disable=W0613,W0621
     executor.run()
     for event in executor.events:
         event_data = event.get('event_data', {})
@@ -308,9 +309,9 @@ def test_callback_plugin_saves_custom_stats(executor, playbook):
         - my_handler
 '''},  # noqa
 ])
-def test_callback_plugin_records_notify_events(executor, playbook):
+def test_callback_plugin_records_notify_events(executor, playbook):  # pylint: disable=W0613,W0621
     executor.run()
-    assert len(list(executor.events))
+    assert list(executor.events)
     notify_events = [x for x in executor.events if x['event'] == 'playbook_on_notify']
     assert len(notify_events) == 1
     assert notify_events[0]['event_data']['handler'] == 'my_handler'
@@ -332,12 +333,12 @@ def test_callback_plugin_records_notify_events(executor, playbook):
         url_password: "{{ pw }}"
 '''},  # noqa
 ])
-def test_module_level_no_log(executor, playbook):
+def test_module_level_no_log(executor, playbook):  # pylint: disable=W0613,W0621
     # It's possible for `no_log=True` to be defined at the _module_ level,
     # e.g., for the URI module password parameter
     # This test ensures that we properly redact those
     executor.run()
-    assert len(list(executor.events))
+    assert list(executor.events)
     assert 'john-jacob-jingleheimer-schmidt' in json.dumps(list(executor.events))
     assert 'SENSITIVE' not in json.dumps(list(executor.events))
 
@@ -354,15 +355,15 @@ def test_output_when_given_invalid_playbook(tmp_path):
     #
     # But no test validated it.  This does that.
     private_data_dir = str(tmp_path)
-    executor = init_runner(
+    ex = init_runner(
         private_data_dir=private_data_dir,
         inventory='localhost ansible_connection=local ansible_python_interpreter="{{ ansible_playbook_python }}"',
         envvars={"ANSIBLE_DEPRECATION_WARNINGS": "False"},
         playbook=os.path.join(private_data_dir, 'fake_playbook.yml')
     )
 
-    executor.run()
-    with executor.stdout as f:
+    ex.run()
+    with ex.stdout as f:
         stdout = f.read()
     assert "ERROR! the playbook:" in stdout
     assert "could not be found" in stdout
@@ -391,20 +392,20 @@ def test_output_when_given_non_playbook_script(tmp_path):
     with open(os.path.join(private_data_dir, "env", "settings"), 'w') as settings_file:
         settings_file.write("pexpect_timeout: 0.2")
 
-    executor = init_runner(
+    ex = init_runner(
         private_data_dir=private_data_dir,
         inventory='localhost ansible_connection=local ansible_python_interpreter="{{ ansible_playbook_python }}"',
         envvars={"ANSIBLE_DEPRECATION_WARNINGS": "False"}
     )
 
-    executor.run()
+    ex.run()
 
-    with executor.stdout as f:
+    with ex.stdout as f:
         stdout = f.readlines()
     assert stdout[0].strip() == "hi world"
     assert stdout[1].strip() == "goodbye world"
 
-    events = list(executor.events)
+    events = list(ex.events)
 
     assert len(events) == 2
     assert events[0]['event'] == 'verbose'
@@ -425,7 +426,7 @@ def test_output_when_given_non_playbook_script(tmp_path):
         msg: "{{ ('F' * 150) | list }}"
 '''},  # noqa
 ])
-def test_large_stdout_parsing_when_using_json_output(executor, playbook):
+def test_large_stdout_parsing_when_using_json_output(executor, playbook):  # pylint: disable=W0613,W0621
     # When the json flag is used, it is possible to output more data than
     # pexpect's maxread default of 2000 characters.  As a result, if not
     # handled properly, the stdout can end up being corrupted with partial
diff --git a/test/integration/test_events.py b/test/integration/test_events.py
index bf7905e..add859f 100644
--- a/test/integration/test_events.py
+++ b/test/integration/test_events.py
@@ -28,8 +28,7 @@ def test_basic_events(containerized, runtime, tmp_path, container_image, is_run_
         thread.join()  # ensure async run finishes
 
     event_types = [x['event'] for x in r.events if x['event'] != 'verbose']
-    okay_events = [x for x in filter(lambda x: 'event' in x and x['event'] == 'runner_on_ok',
-                                     r.events)]
+    okay_events = list(filter(lambda x: 'event' in x and x['event'] == 'runner_on_ok', r.events))
 
     assert event_types[0] == 'playbook_on_start'
     assert "playbook_on_play_start" in event_types
@@ -61,7 +60,7 @@ def test_basic_serializeable(tmp_path):
     r = run(private_data_dir=str(tmp_path),
             inventory=inv,
             playbook=[{'hosts': 'all', 'gather_facts': False, 'tasks': [{'debug': {'msg': "test"}}]}])
-    events = [x for x in r.events]
+    events = list(r.events)
     json.dumps(events)
 
 
@@ -79,7 +78,7 @@ def test_event_omission(tmp_path):
             continue
         events.append(x)
 
-    assert not any([x['event_data'] for x in events])
+    assert not any(x['event_data'] for x in events)
 
 
 def test_event_omission_except_failed(tmp_path):
@@ -101,12 +100,11 @@ def test_event_omission_except_failed(tmp_path):
     assert len(all_event_datas) == 1
 
 
-def test_runner_on_start(rc, tmp_path):
+def test_runner_on_start(tmp_path):
     r = run(private_data_dir=str(tmp_path),
             inventory='localhost ansible_connection=local ansible_python_interpreter="{{ ansible_playbook_python }}"',
             playbook=[{'hosts': 'all', 'gather_facts': False, 'tasks': [{'debug': {'msg': "test"}}]}])
-    start_events = [x for x in filter(lambda x: 'event' in x and x['event'] == 'runner_on_start',
-                                      r.events)]
+    start_events = list(filter(lambda x: 'event' in x and x['event'] == 'runner_on_start', r.events))
     assert len(start_events) == 1
 
 
diff --git a/test/integration/test_interface.py b/test/integration/test_interface.py
index 0b60916..4e17503 100644
--- a/test/integration/test_interface.py
+++ b/test/integration/test_interface.py
@@ -77,11 +77,11 @@ def get_env_data(res):
         )
         if found and 'res' in event['event_data']:
             return event['event_data']['res']
-    else:
-        print('output:')
-        with res.stdout as f:
-            print(f.read())
-        raise RuntimeError('Count not find look_at_environment task from playbook')
+
+    print('output:')
+    with res.stdout as f:
+        print(f.read())
+    raise RuntimeError('Count not find look_at_environment task from playbook')
 
 
 def test_env_accuracy(request, project_fixtures):
@@ -117,7 +117,7 @@ def test_env_accuracy(request, project_fixtures):
     assert os.path.exists(printenv_example / "env/envvars") == 1
 
 
-def test_no_env_files(request, project_fixtures):
+def test_no_env_files(project_fixtures):
     printenv_example = project_fixtures / 'printenv'
     os.environ['SET_BEFORE_TEST'] = 'MADE_UP_VALUE'
 
@@ -174,7 +174,7 @@ def test_env_accuracy_inside_container(request, project_fixtures, runtime, conta
     # all environment variables, particularly those set by the entrypoint script
     for key, value in expected_env.items():
         assert key in actual_env
-        assert actual_env[key] == value, 'Reported value wrong for {0} env var'.format(key)
+        assert actual_env[key] == value, f'Reported value wrong for {key} env var'
 
     assert env_data['cwd'] == res.config.cwd
 
@@ -230,7 +230,7 @@ def test_run_command(project_fixtures):
 
 
 def test_run_command_injection_error():
-    out, err, rc = run_command(
+    _, err, rc = run_command(
         executable_cmd='whoami',
         cmdline_args=[';hostname'],
         runner_mode='subprocess',
@@ -241,7 +241,7 @@ def test_run_command_injection_error():
 
 @pytest.mark.test_all_runtimes
 def test_run_command_injection_error_within_container(runtime, container_image):
-    out, err, rc = run_command(
+    _, err, rc = run_command(
         executable_cmd='whoami',
         cmdline_args=[';hostname'],
         runner_mode='subprocess',
@@ -278,7 +278,7 @@ def test_run_ansible_command_within_container(project_fixtures, runtime, contain
 def test_run_script_within_container(project_fixtures, runtime, container_image):
     private_data_dir = project_fixtures / 'debug'
     script_path = project_fixtures / 'files'
-    container_volume_mounts = ["{}:{}:Z".format(script_path, script_path)]
+    container_volume_mounts = [f"{script_path}:{script_path}:Z"]
     container_kwargs = {
         'process_isolation_executable': runtime,
         'process_isolation': True,
@@ -437,6 +437,7 @@ def test_run_role(project_fixtures):
     assert 'Hello World!' in stdout
 
 
+# pylint: disable=W0613
 def test_get_role_list(project_fixtures, skipif_pre_ansible211):
     """
     Test get_role_list() running locally, specifying a playbook directory
@@ -450,7 +451,7 @@ def test_get_role_list(project_fixtures, skipif_pre_ansible211):
         }
     }
 
-    resp, err = get_role_list(playbook_dir=pdir)
+    resp, _ = get_role_list(playbook_dir=pdir)
     assert isinstance(resp, dict)
 
     # So that tests can work locally, where multiple roles might be returned,
@@ -478,7 +479,7 @@ def test_get_role_list_within_container(project_fixtures, runtime, skipif_pre_an
         'process_isolation': True,
         'container_image': container_image,
     }
-    resp, err = get_role_list(private_data_dir=pdir, playbook_dir="/runner/project", **container_kwargs)
+    resp, _ = get_role_list(private_data_dir=pdir, playbook_dir="/runner/project", **container_kwargs)
     assert isinstance(resp, dict)
     assert resp == expected
 
@@ -512,7 +513,7 @@ def test_get_role_argspec(project_fixtures, skipif_pre_ansible211):
         }
     }
 
-    resp, err = get_role_argspec('Into_The_Mystic', playbook_dir=use_role_example)
+    resp, _ = get_role_argspec('Into_The_Mystic', playbook_dir=use_role_example)
     assert isinstance(resp, dict)
     assert 'Into_The_Mystic' in resp
     assert resp['Into_The_Mystic']['entry_points'] == expected_epoint
@@ -554,7 +555,7 @@ def test_get_role_argspec_within_container(project_fixtures, runtime, skipif_pre
         'process_isolation': True,
         'container_image': container_image,
     }
-    resp, err = get_role_argspec('Into_The_Mystic', private_data_dir=pdir, playbook_dir="/runner/project", **container_kwargs)
+    resp, _ = get_role_argspec('Into_The_Mystic', private_data_dir=pdir, playbook_dir="/runner/project", **container_kwargs)
     assert isinstance(resp, dict)
     assert 'Into_The_Mystic' in resp
     assert resp['Into_The_Mystic']['entry_points'] == expected_epoint
diff --git a/test/integration/test_runner.py b/test/integration/test_runner.py
index e9d08dc..553d947 100644
--- a/test/integration/test_runner.py
+++ b/test/integration/test_runner.py
@@ -3,19 +3,19 @@
 import json
 import os
 import re
-import pytest
 import sys
 
-from ansible_runner import Runner
+from test.utils.common import iterate_timeout
 
-from ansible_runner.exceptions import AnsibleRunnerException
+import pytest
 
-from test.utils.common import iterate_timeout
+from ansible_runner import Runner
+from ansible_runner.exceptions import AnsibleRunnerException
 
 
 @pytest.mark.xfail(reason='Test is unstable')
 def test_password_prompt(rc):
-    rc.command = [sys.executable, '-c' 'import time; print(input("Password: "))']
+    rc.command = [sys.executable, '-c', 'import time; print(input("Password: "))']
     rc.expect_passwords[re.compile(r'Password:\s*?$', re.M)] = '1234'
     status, exitcode = Runner(config=rc).run()
     assert status == 'successful'
@@ -52,7 +52,7 @@ def test_run_command_with_unicode(rc):
         assert data.get('command') == ['echo', expected]
         assert 'cwd' in data
         assert isinstance(data.get('env'), dict)
-        assert u"䉪ቒ칸" in data.get('env')
+        assert "䉪ቒ칸" in data.get('env')
 
 
 def test_run_command_finished_callback(rc, mocker):
@@ -84,7 +84,7 @@ def test_run_command_explosive_cancel_callback(rc):
 
 
 def test_run_command_cancel_callback(rc):
-    def cancel(*args):
+    def cancel(*args):  # pylint: disable=W0613
         return True
     rc.command = ['sleep', '1']
     runner = Runner(config=rc, cancel_callback=cancel)
@@ -152,7 +152,7 @@ def test_run_command_events_missing(rc):
     status, exitcode = runner.run()
     assert status == 'successful'
     assert exitcode == 0
-    assert list(runner.events) == []
+    assert not list(runner.events)
 
 
 def test_run_command_stdout_missing(rc):
@@ -183,9 +183,9 @@ def test_run_command_ansible(rc):
     status, exitcode = runner.run()
     assert status == 'successful'
     assert exitcode == 0
-    assert list(runner.events) != []
+    assert list(runner.events)
     assert runner.stats != {}
-    assert list(runner.host_events('localhost')) != [], repr(list(runner.events))
+    assert list(runner.host_events('localhost')), repr(list(runner.events))
     with runner.stdout as f:
         stdout = f.read()
     assert stdout != ""
@@ -255,7 +255,7 @@ def test_set_fact_cache(rc):
     rc.host_pattern = "localhost"
     rc.prepare()
     runner = Runner(config=rc)
-    runner.set_fact_cache('localhost', dict(message='hello there'))
+    runner.set_fact_cache('localhost', {'message': 'hello there'})
     status, exitcode = runner.run()
     assert status == 'successful'
     assert exitcode == 0
@@ -270,10 +270,10 @@ def test_set_extra_vars(rc):
     rc.module = "debug"
     rc.module_args = "var=test_extra_vars"
     rc.host_pattern = "localhost"
-    rc.extra_vars = dict(test_extra_vars='hello there')
+    rc.extra_vars = {'test_extra_vars': 'hello there'}
     rc.prepare()
     runner = Runner(config=rc)
-    status, exitcode = runner.run()
+    runner.run()
     # stdout file can be subject to a race condition
     for _ in iterate_timeout(30.0, 'stdout file to be written with "hello there" in it', interval=0.2):
         with open(os.path.join(rc.artifact_dir, 'stdout')) as f:
diff --git a/test/integration/test_transmit_worker_process.py b/test/integration/test_transmit_worker_process.py
index fc89331..540e5dc 100644
--- a/test/integration/test_transmit_worker_process.py
+++ b/test/integration/test_transmit_worker_process.py
@@ -1,5 +1,6 @@
 import base64
 import io
+import json
 import os
 import socket
 import concurrent.futures
@@ -7,7 +8,6 @@ import time
 import threading
 
 import pytest
-import json
 
 from ansible_runner import run
 from ansible_runner.streaming import Transmitter, Worker, Processor
@@ -16,22 +16,23 @@ import ansible_runner.interface  # AWX import pattern
 
 
 class TestStreamingUsage:
+    # pylint: disable=W0201
 
     @pytest.fixture(autouse=True)
     def reset_self_props(self):
         self.status_data = None
 
-    def status_handler(self, status_data, runner_config=None):
+    def status_handler(self, status_data, runner_config=None):  # pylint: disable=W0613
         self.status_data = status_data
 
     def get_job_kwargs(self, job_type):
         """For this test scenaro, the ansible-runner interface kwargs"""
         if job_type == 'run':
-            job_kwargs = dict(playbook='debug.yml')
+            job_kwargs = {'playbook': 'debug.yml'}
         else:
-            job_kwargs = dict(module='setup', host_pattern='localhost')
+            job_kwargs = {'module': 'setup', 'host_pattern': 'localhost'}
         # also test use of user env vars
-        job_kwargs['envvars'] = dict(MY_ENV_VAR='bogus')
+        job_kwargs['envvars'] = {'MY_ENV_VAR': 'bogus'}
         return job_kwargs
 
     @staticmethod
@@ -115,6 +116,7 @@ class TestStreamingUsage:
         None,  # default disable, test sets envvar for keepalives
     ])
     def test_keepalive_setting(self, tmp_path, project_fixtures, keepalive_setting):
+        # pylint: disable=W0212
         verbosity = None
         output_corruption_test_mode = 0 < (keepalive_setting or 0) < 1
 
@@ -131,8 +133,8 @@ class TestStreamingUsage:
 
         worker_dir = tmp_path / 'for_worker'
         process_dir = tmp_path / 'for_process'
-        for dir in (worker_dir, process_dir):
-            dir.mkdir()
+        for directory in (worker_dir, process_dir):
+            directory.mkdir()
 
         outgoing_buffer = io.BytesIO()
         incoming_buffer = io.BytesIO()
@@ -141,7 +143,7 @@ class TestStreamingUsage:
 
         status, rc = Transmitter(
             _output=outgoing_buffer, private_data_dir=project_fixtures / 'sleep',
-            playbook='sleep.yml', extravars=dict(sleep_interval=2), verbosity=verbosity
+            playbook='sleep.yml', extravars={'sleep_interval': 2}, verbosity=verbosity
         ).run()
         assert rc in (None, 0)
         assert status == 'unstarted'
@@ -348,7 +350,7 @@ def transmit_stream(project_fixtures, tmp_path):
 
 
 @pytest.fixture
-def worker_stream(transmit_stream, tmp_path):
+def worker_stream(transmit_stream, tmp_path):  # pylint: disable=W0621
     ingoing_buffer = tmp_path / 'buffer2'  # basically how some demos work
     ingoing_buffer.touch()
 
@@ -364,7 +366,7 @@ def worker_stream(transmit_stream, tmp_path):
             return ingoing_buffer
 
 
-def test_worker_without_delete_no_dir(tmp_path, cli, transmit_stream):
+def test_worker_without_delete_no_dir(tmp_path, cli, transmit_stream):  # pylint: disable=W0621
     worker_dir = tmp_path / 'for_worker'
 
     with open(transmit_stream, 'rb') as stream:
@@ -375,7 +377,7 @@ def test_worker_without_delete_no_dir(tmp_path, cli, transmit_stream):
     assert worker_dir.joinpath('project', 'debug.yml').exists()
 
 
-def test_worker_without_delete_dir_exists(tmp_path, cli, transmit_stream):
+def test_worker_without_delete_dir_exists(tmp_path, cli, transmit_stream):  # pylint: disable=W0621
     worker_dir = tmp_path / 'for_worker'
     worker_dir.mkdir()
 
@@ -391,7 +393,7 @@ def test_worker_without_delete_dir_exists(tmp_path, cli, transmit_stream):
     assert test_file_path.exists()
 
 
-def test_worker_delete_no_dir(tmp_path, cli, transmit_stream):
+def test_worker_delete_no_dir(tmp_path, cli, transmit_stream):  # pylint: disable=W0621
     """
     Case where non-existing --delete is provided to worker command
     it should always delete everything both before and after the run
@@ -407,7 +409,7 @@ def test_worker_delete_no_dir(tmp_path, cli, transmit_stream):
     assert not worker_dir.joinpath('project', 'debug.yml').exists()
 
 
-def test_worker_delete_dir_exists(tmp_path, cli, transmit_stream):
+def test_worker_delete_dir_exists(tmp_path, cli, transmit_stream):  # pylint: disable=W0621
     """
     Case where non-existing --delete is provided to worker command
     it should always delete everything both before and after the run
@@ -427,7 +429,7 @@ def test_worker_delete_dir_exists(tmp_path, cli, transmit_stream):
     assert not worker_dir.joinpath('project', 'debug.yml').exists()
 
 
-def test_process_with_custom_ident(tmp_path, cli, worker_stream):
+def test_process_with_custom_ident(tmp_path, cli, worker_stream):  # pylint: disable=W0621
     process_dir = tmp_path / 'for_process'
     process_dir.mkdir()
 
@@ -441,7 +443,7 @@ def test_process_with_custom_ident(tmp_path, cli, worker_stream):
     assert (process_dir / 'artifacts' / 'custom_ident' / 'job_events').exists()
 
 
-def test_missing_private_dir_transmit(tmpdir):
+def test_missing_private_dir_transmit():
     outgoing_buffer = io.BytesIO()
 
     # Transmit
@@ -503,7 +505,7 @@ def test_unparsable_really_big_line_processor(tmp_path):
     process_dir.mkdir()
     incoming_buffer = io.BytesIO(bytes(f'not-json-data with extra garbage:{"f"*10000}', encoding='utf-8'))
 
-    def status_receiver(status_data, runner_config):
+    def status_receiver(status_data, runner_config):  # pylint: disable=W0613
         assert status_data['status'] == 'error'
         assert 'Failed to JSON parse a line from worker stream.' in status_data['job_explanation']
         assert 'not-json-data with extra garbage:ffffffffff' in status_data['job_explanation']
diff --git a/test/unit/config/test_runner.py b/test/unit/config/test_runner.py
index 397fd9a..b8848ac 100644
--- a/test/unit/config/test_runner.py
+++ b/test/unit/config/test_runner.py
@@ -239,7 +239,7 @@ def test_prepare_env_directory_isolation_from_settings(mocker, project_fixtures)
     mkdtemp.assert_called_once_with(prefix='runner_di_', dir='/tmp/runner')
 
     # The project files should be copied to the isolation path.
-    copy_tree.assert_called_once_with(rc.project_dir, rc.directory_isolation_path, symlinks=True)
+    copy_tree.assert_called_once_with(rc.project_dir, rc.directory_isolation_path, dirs_exist_ok=True, symlinks=True)
 
 
 def test_prepare_inventory(mocker):