Repository: pybuilder/pybuilder
Issue: Mix packages and modules in command line parameters for Pylint

Pylint is meant to be called from the command line. The usage is
```
pylint [options] modules_or_packages
```
You shoud passed modules OR packages. If you pass mix of modules and packages you should check that its not intercect between each other, because Pylint searches modules in packages and we got double messages in report (can live with it) or code duplication message in bad case (its a problem).

Test project (https://drive.google.com/file/d/13n1RZFdgwitfgUDOv76ZMYOPIrBYVsAT/view?usp=sharing):
```
a/__init__.py
a/module.py
a/b/__init__.py
a/b/module.py
b/__init__.py
b/module.py
```

Run pyb analyze -X:
```
[INFO]  Executing pylint on project sources
[DEBUG] Executing command: 'c:\\\\work\\\\projects\\\\misc\\\\pybuilder-pylint-issue\\\\.pybuilder\\\\plugins\\\\cpython-3.9.7.final.0\\\\scripts\\\\pylint.EXE' '--max-line-length=100' '--no-docstring-rgx=.*' 'a.module' 'a' 'a.b.module' 'a.b' 'b.module' 'b'
```

Report:
```
************* Module a.module
c:\\work\\projects\\misc\\pybuilder-pylint-issue\\src\\main\\python\\a\\module.py:1:0: C0114: Missing module docstring (missing-module-docstring)
c:\\work\\projects\\misc\\pybuilder-pylint-issue\\src\\main\\python\\a\\module.py:1:0: W0611: Unused import sys (unused-import)
c:\\work\\projects\\misc\\pybuilder-pylint-issue\\src\\main\\python\\a\\module.py:1:0: C0114: Missing module docstring (missing-module-docstring)
c:\\work\\projects\\misc\\pybuilder-pylint-issue\\src\\main\\python\\a\\module.py:1:0: W0611: Unused import sys (unused-import)
************* Module a.b.module
c:\\work\\projects\\misc\\pybuilder-pylint-issue\\src\\main\\python\\a\\b\\module.py:1:0: C0114: Missing module docstring (missing-module-docstring)
c:\\work\\projects\\misc\\pybuilder-pylint-issue\\src\\main\\python\\a\\b\\module.py:1:0: W0611: Unused import sys (unused-import)
c:\\work\\projects\\misc\\pybuilder-pylint-issue\\src\\main\\python\\a\\b\\module.py:1:0: C0114: Missing module docstring (missing-module-docstring)
c:\\work\\projects\\misc\\pybuilder-pylint-issue\\src\\main\\python\\a\\b\\module.py:1:0: W0611: Unused import sys (unused-import)
c:\\work\\projects\\misc\\pybuilder-pylint-issue\\src\\main\\python\\a\\b\\module.py:1:0: C0114: Missing module docstring (missing-module-docstring)
c:\\work\\projects\\misc\\pybuilder-pylint-issue\\src\\main\\python\\a\\b\\module.py:1:0: W0611: Unused import sys (unused-import)
************* Module b.module
c:\\work\\projects\\misc\\pybuilder-pylint-issue\\src\\main\\python\\b\\module.py:1:0: C0114: Missing module docstring (missing-module-docstring)
c:\\work\\projects\\misc\\pybuilder-pylint-issue\\src\\main\\python\\b\\module.py:5:0: C0103: Function name "b" doesn't conform to snake_case naming style (invalid-name)
c:\\work\\projects\\misc\\pybuilder-pylint-issue\\src\\main\\python\\b\\module.py:1:0: W0611: Unused import sys (unused-import)
c:\\work\\projects\\misc\\pybuilder-pylint-issue\\src\\main\\python\\b\\module.py:2:0: W0611: Unused module imported from a as a (unused-import)
c:\\work\\projects\\misc\\pybuilder-pylint-issue\\src\\main\\python\\b\\module.py:1:0: C0114: Missing module docstring (missing-module-docstring)
c:\\work\\projects\\misc\\pybuilder-pylint-issue\\src\\main\\python\\b\\module.py:5:0: C0103: Function name "b" doesn't conform to snake_case naming style (invalid-name)
c:\\work\\projects\\misc\\pybuilder-pylint-issue\\src\\main\\python\\b\\module.py:1:0: W0611: Unused import sys (unused-import)
c:\\work\\projects\\misc\\pybuilder-pylint-issue\\src\\main\\python\\b\\module.py:2:0: W0611: Unused module imported from a as a (unused-import)
c:\\work\\projects\\misc\\pybuilder-pylint-issue\\src\\main\\python\\b\\module.py:1:0: R0801: Similar lines in 2 files
==b.module:[0:6]
==b.module:[0:6]
import sys
from a import module as a
from a.b import module as ab

def b():
    print('b') (duplicate-code)

------------------------------------------------------------------

Your code has been rated at 0.50/10 (previous run: 0.50/10, +0.00)
```

If I execute Pylint and pass only modules or only packages - all works great.

`pylint 'a' 'b'`:

```
************* Module a.module
a\\module.py:1:0: C0114: Missing module docstring (missing-module-docstring)
a\\module.py:1:0: W0611: Unused import sys (unused-import)
************* Module a.b.module
a\\b\\module.py:1:0: C0114: Missing module docstring (missing-module-docstring)
a\\b\\module.py:1:0: W0611: Unused import sys (unused-import)
************* Module b.module
b\\module.py:1:0: C0114: Missing module docstring (missing-module-docstring)
b\\module.py:5:0: C0103: Function name "b" doesn't conform to snake_case naming style (invalid-name)
b\\module.py:5:0: C0116: Missing function or method docstring (missing-function-docstring)
b\\module.py:1:0: W0611: Unused import sys (unused-import)
b\\module.py:2:0: W0611: Unused module imported from a as a (unused-import)

------------------------------------------------------------------
Your code has been rated at 0.00/10 (previous run: 0.00/10, +0.00)
```

`pylint 'a.module' 'a.b.module' 'b.module'`:

```
************* Module a.module
a\\module.py:1:0: C0114: Missing module docstring (missing-module-docstring)
a\\module.py:1:0: W0611: Unused import sys (unused-import)
************* Module a.b.module
a\\b\\module.py:1:0: C0114: Missing module docstring (missing-module-docstring)
a\\b\\module.py:1:0: W0611: Unused import sys (unused-import)
************* Module b.module
b\\module.py:1:0: C0114: Missing module docstring (missing-module-docstring)
b\\module.py:5:0: C0103: Function name "b" doesn't conform to snake_case naming style (invalid-name)
b\\module.py:5:0: C0116: Missing function or method docstring (missing-function-docstring)
b\\module.py:1:0: W0611: Unused import sys (unused-import)
b\\module.py:2:0: W0611: Unused module imported from a as a (unused-import)

------------------------------------------------------------------
Your code has been rated at 0.00/10 (previous run: 0.00/10, +0.00)
```




Changed files:
src/unittest/python/plugins/python/python_plugin_helper_tests.py, src/unittest/python/plugins/python/pylint_plugin_tests.py, src/main/python/pybuilder/plugins/python/pylint_plugin.py, src/main/python/pybuilder/plugins/python/python_plugin_helper.py, src/integrationtest/python/issue_818_tests.py

diff --git a/src/integrationtest/python/issue_818_tests.py b/src/integrationtest/python/issue_818_tests.py
index 3baa0da..f0ad52c 100644
--- a/src/integrationtest/python/issue_818_tests.py
+++ b/src/integrationtest/python/issue_818_tests.py
@@ -38,6 +38,7 @@ def init (project):
         """)
 
         self.create_directory("src/main/python/a")
+        self.create_directory("src/main/python/name/space")
         self.write_file("src/main/python/a/__init__.py", textwrap.dedent(
             """
             print("Hello from the package")
@@ -46,12 +47,20 @@ def init (project):
             """
             print("Hello from the module")
             """))
+        self.write_file("src/main/python/name/space/__init__.py", textwrap.dedent(
+            """
+            print("Hello from the namespace package")
+            """))
+        self.write_file("src/main/python/name/space/module.py", textwrap.dedent(
+            """
+            print("Hello from the namespace module")
+            """))
         reactor = self.prepare_reactor()
 
         with self.assertRaises(BuildFailedException) as raised_e:
             reactor.build("analyze")
 
-        self.assertEqual(raised_e.exception.message, "Pylint found 2 warning(s).")
+        self.assertEqual(raised_e.exception.message, "PyLint found 4 warning(s).")
 
 
 if __name__ == "__main__":
diff --git a/src/main/python/pybuilder/plugins/python/pylint_plugin.py b/src/main/python/pybuilder/plugins/python/pylint_plugin.py
index 29e8363..1c31252 100644
--- a/src/main/python/pybuilder/plugins/python/pylint_plugin.py
+++ b/src/main/python/pybuilder/plugins/python/pylint_plugin.py
@@ -18,52 +18,63 @@
 
 from pybuilder.core import use_plugin, after, init, task
 from pybuilder.errors import BuildFailedException
-from pybuilder.plugins.python.python_plugin_helper import execute_tool_on_modules
-from pybuilder.utils import read_file
+from pybuilder.pluginhelper.external_command import ExternalCommandBuilder
 
 use_plugin("python.core")
 use_plugin("analysis")
 
 DEFAULT_PYLINT_OPTIONS = ["--max-line-length=100", "--no-docstring-rgx=.*"]
-DEFAULT_PYLINT_BREAK_BUILD = False
 
 
 @init
 def init_pylint(project):
     project.plugin_depends_on("pylint")
     project.set_property_if_unset("pylint_options", DEFAULT_PYLINT_OPTIONS)
-    project.set_property_if_unset("pylint_break_build", DEFAULT_PYLINT_BREAK_BUILD)
+    project.set_property_if_unset("pylint_break_build", False)
+    project.set_property_if_unset("pylint_include_test_sources", False)
+    project.set_property_if_unset("pylint_include_scripts", False)
 
 
 @after("prepare")
 def check_pylint_availability(project, logger, reactor):
-    logger.debug("Checking availability of pylint")
+    logger.debug("Checking availability of PyLint")
     reactor.pybuilder_venv.verify_can_execute(["pylint"], "pylint", "plugin python.pylint")
-    logger.debug("pylint has been found")
 
 
 @task("analyze")
 def execute_pylint(project, logger, reactor):
     logger.info("Executing pylint on project sources")
 
-    command_and_arguments = ["pylint"] + project.get_property("pylint_options")
-    result_tuple = execute_tool_on_modules(project, "pylint", reactor.pybuilder_venv, command_and_arguments,
-                                           extend_pythonpath=True,
-                                           include_package_modules=False)
+    verbose = project.get_property("verbose")
+    project.set_property_if_unset("pylint_verbose_output", verbose)
 
-    report_file = result_tuple[1]  # access by position to avoid changing mocking behaviour
+    command = ExternalCommandBuilder("pylint", project, reactor)
+
+    for opt in project.get_property("pylint_options"):
+        command.use_argument(opt)
+
+    include_test_sources = project.get_property("pylint_include_test_sources")
+    include_scripts = project.get_property("pylint_include_scripts")
+
+    result = command.run_on_production_source_files(logger,
+                                                    include_test_sources=include_test_sources,
+                                                    include_scripts=include_scripts)
+
+    break_build = project.get_property("pylint_break_build")
+    if result.exit_code == 32 and break_build:
+        raise BuildFailedException("PyLint failed with exit code %s", result.exit_code)
 
     warnings = [line.rstrip()
-                for line in read_file(report_file)
+                for line in result.report_lines
                 if line.find(".py:") >= 0]
     warning_count = len(warnings)
 
     if warning_count:
         for warning in warnings:
-            logger.warn("PyLint: %s", warning)
+            logger.warn("pylint: %s", warning)
 
-        message = "Pylint found {} warning(s).".format(warning_count)
-        if project.get_property("pylint_break_build"):
+        message = "PyLint found {} warning(s).".format(warning_count)
+        if break_build:
             logger.error(message)
             raise BuildFailedException(message)
         else:
diff --git a/src/main/python/pybuilder/plugins/python/python_plugin_helper.py b/src/main/python/pybuilder/plugins/python/python_plugin_helper.py
index 0395a3f..5011812 100644
--- a/src/main/python/pybuilder/plugins/python/python_plugin_helper.py
+++ b/src/main/python/pybuilder/plugins/python/python_plugin_helper.py
@@ -37,32 +37,36 @@ def discover_python_files(directory, exclude_glob=None):
 
 
 def discover_affected_files(include_test_sources, include_scripts, project):
-    source_dir = project.get_property("dir_source_main_python")
+    source_dir = project.expand_path("$dir_source_main_python")
     files = discover_python_files(source_dir)
 
     if include_test_sources:
         if project.get_property("dir_source_unittest_python"):
-            unittest_dir = project.get_property("dir_source_unittest_python")
+            unittest_dir = project.expand_path("$dir_source_unittest_python")
             files = itertools.chain(files, discover_python_files(unittest_dir))
         if project.get_property("dir_source_integrationtest_python"):
-            integrationtest_dir = project.get_property("dir_source_integrationtest_python")
+            integrationtest_dir = project.expand_path("$dir_source_integrationtest_python")
             files = itertools.chain(files, discover_python_files(integrationtest_dir))
+
     if include_scripts and project.get_property("dir_source_main_scripts"):
-        scripts_dir = project.get_property("dir_source_main_scripts")
+        scripts_dir = project.expand_path("$dir_source_main_scripts")
         files = itertools.chain(files,
                                 discover_files_matching(scripts_dir, "*"))  # we have no idea how scripts might look
+
     return files
 
 
 def discover_affected_dirs(include_test_sources, include_scripts, project):
-    files = [project.get_property("dir_source_main_python")]
+    files = [project.expand_path("$dir_source_main_python")]
     if include_test_sources:
         if _if_property_set_and_dir_exists(project.get_property("dir_source_unittest_python")):
-            files.append(project.get_property("dir_source_unittest_python"))
+            files.append(project.expand_path("$dir_source_unittest_python"))
         if _if_property_set_and_dir_exists(project.get_property("dir_source_integrationtest_python")):
-            files.append(project.get_property("dir_source_integrationtest_python"))
+            files.append(project.expand_path("$dir_source_integrationtest_python"))
+
     if include_scripts and _if_property_set_and_dir_exists(project.get_property("dir_source_main_scripts")):
-        files.append(project.get_property("dir_source_main_scripts"))
+        files.append(project.expand_path("$dir_source_main_scripts"))
+
     return files
 
 
@@ -86,7 +90,7 @@ def execute_tool_on_source_files(project, name, python_env, command_and_argument
     report_file = execution_result[1]
     report_lines = read_file(report_file)
 
-    if project.get_property(name + "_verbose_output") and logger:
+    if logger and project.get_property(name + "_verbose_output"):
         log_report(logger, name, report_lines)
 
     return execution_result
diff --git a/src/unittest/python/plugins/python/pylint_plugin_tests.py b/src/unittest/python/plugins/python/pylint_plugin_tests.py
index 9b40a41..eec3983 100644
--- a/src/unittest/python/plugins/python/pylint_plugin_tests.py
+++ b/src/unittest/python/plugins/python/pylint_plugin_tests.py
@@ -17,6 +17,7 @@
 #   limitations under the License.
 
 from unittest import TestCase
+from unittest.mock import call
 
 from pybuilder.core import Project, Logger
 from pybuilder.errors import BuildFailedException
@@ -24,7 +25,7 @@ from pybuilder.plugins.python.pylint_plugin import (check_pylint_availability,
                                                     init_pylint,
                                                     execute_pylint,
                                                     DEFAULT_PYLINT_OPTIONS)
-from test_utils import Mock, patch, ANY
+from test_utils import Mock, patch
 
 PYLINT_ERROR_OUTPUT = [
     '************* Module mode.file',
@@ -59,54 +60,85 @@ class PylintPluginTests(TestCase):
 
         self.reactor.pybuilder_venv.verify_can_execute.assert_called_with(['pylint'], 'pylint', 'plugin python.pylint')
 
-    @patch('pybuilder.plugins.python.pylint_plugin.read_file', return_value=PYLINT_NORMAL_OUTPUT)
-    def test_should_run_pylint_with_default_options(self, *_):
+    @patch('pybuilder.plugins.python.pylint_plugin.ExternalCommandBuilder')
+    def test_should_run_pylint_with_default_options(self, ecb):
         init_pylint(self.project)
 
+        result = Mock()
+        result.exit_code = 16
+        result.report_lines = PYLINT_NORMAL_OUTPUT
+        ecb().run_on_production_source_files.return_value = result
+
         execute_pylint(self.project, Mock(Logger), self.reactor)
 
-        self.reactor.pybuilder_venv.execute_command.assert_called_with(["pylint"] + DEFAULT_PYLINT_OPTIONS, ANY,
-                                                                       env=ANY)
+        self.assertEqual(ecb().use_argument.call_args_list,
+                         [call(arg) for arg in DEFAULT_PYLINT_OPTIONS])
 
-    @patch('pybuilder.plugins.python.pylint_plugin.read_file', return_value=PYLINT_NORMAL_OUTPUT)
-    def test_should_run_pylint_with_custom_options(self, *_):
+    @patch('pybuilder.plugins.python.pylint_plugin.ExternalCommandBuilder')
+    def test_should_run_pylint_with_custom_options(self, ecb):
         init_pylint(self.project)
+
+        result = Mock()
+        result.exit_code = 16
+        result.report_lines = PYLINT_NORMAL_OUTPUT
+        ecb().run_on_production_source_files.return_value = result
+
         self.project.set_property("pylint_options", ["--test", "-f", "--x=y"])
 
         execute_pylint(self.project, Mock(Logger), self.reactor)
 
-        self.reactor.pybuilder_venv.execute_command.assert_called_with(["pylint", "--test", "-f", "--x=y"], ANY,
-                                                                       env=ANY)
+        self.assertEqual(ecb().use_argument.call_args_list,
+                         [call(arg) for arg in ["--test", "-f", "--x=y"]])
 
-    @patch('pybuilder.plugins.python.pylint_plugin.read_file', return_value=PYLINT_ERROR_OUTPUT)
-    @patch('pybuilder.plugins.python.pylint_plugin.execute_tool_on_modules')
-    def test_should_break_build_when_warnings_and_set(self, *_):
+    @patch('pybuilder.plugins.python.pylint_plugin.ExternalCommandBuilder')
+    def test_should_break_build_when_warnings_and_set(self, ecb):
         init_pylint(self.project)
+
+        result = Mock()
+        result.exit_code = 16
+        result.report_lines = PYLINT_ERROR_OUTPUT
+        ecb().run_on_production_source_files.return_value = result
+
         self.project.set_property("pylint_break_build", True)
 
         with self.assertRaises(BuildFailedException):
             execute_pylint(self.project, Mock(Logger), self.reactor)
 
-    @patch('pybuilder.plugins.python.pylint_plugin.read_file', return_value=PYLINT_ERROR_OUTPUT)
-    @patch('pybuilder.plugins.python.pylint_plugin.execute_tool_on_modules')
-    def test_should_not_break_build_when_warnings_and_not_set(self, *_):
+    @patch('pybuilder.plugins.python.pylint_plugin.ExternalCommandBuilder')
+    def test_should_not_break_build_when_warnings_and_not_set(self, ecb):
         init_pylint(self.project)
+
+        result = Mock()
+        result.exit_code = 16
+        result.report_lines = PYLINT_ERROR_OUTPUT
+        ecb().run_on_production_source_files.return_value = result
+
         self.project.set_property("pylint_break_build", False)
 
         execute_pylint(self.project, Mock(Logger), self.reactor)
 
-    @patch('pybuilder.plugins.python.pylint_plugin.read_file', return_value=PYLINT_NORMAL_OUTPUT)
-    @patch('pybuilder.plugins.python.pylint_plugin.execute_tool_on_modules')
-    def test_should_not_break_build_when_no_warnings_and_set(self, *_):
+    @patch('pybuilder.plugins.python.pylint_plugin.ExternalCommandBuilder')
+    def test_should_not_break_build_when_no_warnings_and_set(self, ecb):
         init_pylint(self.project)
+
+        result = Mock()
+        result.exit_code = 16
+        result.report_lines = PYLINT_NORMAL_OUTPUT
+        ecb().run_on_production_source_files.return_value = result
+
         self.project.set_property("pylint_break_build", True)
 
         execute_pylint(self.project, Mock(Logger), self.reactor)
 
-    @patch('pybuilder.plugins.python.pylint_plugin.read_file', return_value=PYLINT_NORMAL_OUTPUT)
-    @patch('pybuilder.plugins.python.pylint_plugin.execute_tool_on_modules')
-    def test_should_not_break_build_when_no_warnings_and_not_set(self, *_):
+    @patch('pybuilder.plugins.python.pylint_plugin.ExternalCommandBuilder')
+    def test_should_not_break_build_when_no_warnings_and_not_set(self, ecb):
         init_pylint(self.project)
+
+        result = Mock()
+        result.exit_code = 16
+        result.report_lines = PYLINT_NORMAL_OUTPUT
+        ecb().run_on_production_source_files.return_value = result
+
         self.project.set_property("pylint_break_build", False)
 
         execute_pylint(self.project, Mock(Logger), self.reactor)
diff --git a/src/unittest/python/plugins/python/python_plugin_helper_tests.py b/src/unittest/python/plugins/python/python_plugin_helper_tests.py
index 334daf2..4a4a406 100644
--- a/src/unittest/python/plugins/python/python_plugin_helper_tests.py
+++ b/src/unittest/python/plugins/python/python_plugin_helper_tests.py
@@ -46,10 +46,11 @@ class DiscoverAffectedFilesTest(unittest.TestCase):
     def test_should_discover_source_files_when_test_sources_not_included(self, discover_python_files):
         project = Mock()
         project.get_property.return_value = 'source_directory'
+        project.expand_path.return_value = '$source_directory'
         discover_python_files.return_value = ['foo.py', 'bar.py']
 
         files = discover_affected_files(False, False, project)
-        discover_python_files.assert_called_with('source_directory')
+        discover_python_files.assert_called_with('$source_directory')
         self.assertEqual(files, ['foo.py', 'bar.py'])
 
     @patch('pybuilder.plugins.python.python_plugin_helper.discover_python_files')
@@ -57,13 +58,14 @@ class DiscoverAffectedFilesTest(unittest.TestCase):
         project = Mock()
 
         project.get_property.side_effect = lambda _property: _property
+        project.expand_path.side_effect = lambda _property: _property
 
         discover_affected_files(True, False, project)
 
         self.assertEqual(discover_python_files.call_args_list,
-                         [call('dir_source_main_python'),
-                          call('dir_source_unittest_python'),
-                          call('dir_source_integrationtest_python')])
+                         [call('$dir_source_main_python'),
+                          call('$dir_source_unittest_python'),
+                          call('$dir_source_integrationtest_python')])
 
     @patch('pybuilder.plugins.python.python_plugin_helper.discover_python_files')
     @patch('pybuilder.plugins.python.python_plugin_helper.discover_files_matching')
@@ -72,88 +74,89 @@ class DiscoverAffectedFilesTest(unittest.TestCase):
 
         project.get_property.return_value = True
         project.get_property.side_effect = lambda _property: _property
+        project.expand_path.side_effect = lambda _property: _property
 
         discover_affected_files(False, True, project)
 
-        discover_files_matching.assert_called_with('dir_source_main_scripts', '*')
+        discover_files_matching.assert_called_with('$dir_source_main_scripts', '*')
 
     @patch('pybuilder.plugins.python.python_plugin_helper.discover_python_files')
     def test_should_discover_source_files_when_test_sources_are_included_and_only_unittests(self,
                                                                                             discover_python_files):
         project = Mock()
 
-        def get_property(property):
-            if property == 'dir_source_integrationtest_python':
-                return None
-            return property
+        project.get_property.side_effect = lambda _property: (
+            _property if _property != 'dir_source_integrationtest_python' else None)
 
-        project.get_property.side_effect = get_property
+        project.expand_path.side_effect = lambda _property: (
+            _property if _property != '$dir_source_integrationtest_python' else None)
 
         discover_affected_files(True, False, project)
 
         self.assertEqual(discover_python_files.call_args_list,
-                         [call('dir_source_main_python'),
-                          call('dir_source_unittest_python')])
+                         [call('$dir_source_main_python'),
+                          call('$dir_source_unittest_python')])
 
     @patch('pybuilder.plugins.python.python_plugin_helper.discover_python_files')
     def test_should_discover_source_files_when_test_sources_are_included_and_only_integrationtests(self,
                                                                                                    discover_python_files):
         project = Mock()
 
-        def get_property(property):
-            if property == 'dir_source_unittest_python':
-                return None
-            return property
+        project.get_property.side_effect = lambda _property: (
+            _property if _property != 'dir_source_unittest_python' else None)
 
-        project.get_property.side_effect = get_property
+        project.expand_path.side_effect = lambda _property: (
+            _property if _property != '$dir_source_unittest_python' else None)
 
         discover_affected_files(True, False, project)
 
         self.assertEqual(discover_python_files.call_args_list,
-                         [call('dir_source_main_python'),
-                          call('dir_source_integrationtest_python')])
+                         [call('$dir_source_main_python'),
+                          call('$dir_source_integrationtest_python')])
 
     @patch('pybuilder.plugins.python.python_plugin_helper.discover_python_files')
     def test_should_discover_source_files_when_test_sources_are_included_and_no_tests(self, discover_python_files):
         project = Mock()
 
-        def get_property(property):
-            if property == 'dir_source_main_python':
-                return property
-            return None
+        project.get_property.side_effect = lambda _property: (
+            _property if _property == 'dir_source_main_python' else None)
 
-        project.get_property.side_effect = get_property
+        project.expand_path.side_effect = lambda _property: (
+            _property if _property == '$dir_source_main_python' else None)
 
         discover_affected_files(True, False, project)
 
         self.assertEqual(discover_python_files.call_args_list,
-                         [call('dir_source_main_python')])
+                         [call('$dir_source_main_python')])
 
 
 class DiscoverAffectedDirsTest(unittest.TestCase):
     def test_should_discover_source_dirs_when_test_sources_not_included(self):
         project = Mock()
         project.get_property.return_value = 'source_directory'
+        project.expand_path.return_value = '$source_directory'
 
         files = discover_affected_dirs(False, False, project)
-        self.assertEqual(files, ['source_directory'])
+        self.assertEqual(files, ['$source_directory'])
 
     @patch('pybuilder.plugins.python.python_plugin_helper.os.path.isdir', return_value=True)
     def test_should_discover_source_dirs_when_test_sources_are_included(self, _):
         project = Mock()
 
         project.get_property.side_effect = lambda _property: _property
+        project.expand_path.side_effect = lambda _property: _property
 
         files = discover_affected_dirs(True, False, project)
 
-        self.assertEqual(project.get_property.call_args_list,
-                         [call('dir_source_main_python'),
-                          call('dir_source_unittest_python'),
-                          call('dir_source_unittest_python'),
-                          call('dir_source_integrationtest_python'),
-                          call('dir_source_integrationtest_python')])
+        self.assertEqual(project.expand_path.call_args_list,
+                         [call('$dir_source_main_python'),
+                          call('$dir_source_unittest_python'),
+                          call('$dir_source_integrationtest_python'),
+                          ])
         self.assertEqual(files,
-                         ['dir_source_main_python', 'dir_source_unittest_python', 'dir_source_integrationtest_python'])
+                         ['$dir_source_main_python',
+                          '$dir_source_unittest_python',
+                          '$dir_source_integrationtest_python'])
 
     @patch('pybuilder.plugins.python.python_plugin_helper.os.path.isdir', return_value=True)
     def test_should_discover_source_dirs_when_test_sources_are_included_no_unittests(self, _):
@@ -162,15 +165,17 @@ class DiscoverAffectedDirsTest(unittest.TestCase):
         project.get_property.side_effect = lambda _property: (
             _property if _property != 'dir_source_unittest_python' else None)
 
+        project.expand_path.side_effect = lambda _property: (
+            _property if _property != '$dir_source_unittest_python' else None)
+
         files = discover_affected_dirs(True, False, project)
 
-        self.assertEqual(project.get_property.call_args_list,
-                         [call('dir_source_main_python'),
-                          call('dir_source_unittest_python'),
-                          call('dir_source_integrationtest_python'),
-                          call('dir_source_integrationtest_python')])
+        self.assertEqual(project.expand_path.call_args_list,
+                         [call('$dir_source_main_python'),
+                          call('$dir_source_integrationtest_python'),
+                          ])
         self.assertEqual(files,
-                         ['dir_source_main_python', 'dir_source_integrationtest_python'])
+                         ['$dir_source_main_python', '$dir_source_integrationtest_python'])
 
     @patch('pybuilder.plugins.python.python_plugin_helper.os.path.isdir', return_value=True)
     def test_should_discover_source_dirs_when_test_sources_are_included_no_integrationtests(self, _):
@@ -179,15 +184,16 @@ class DiscoverAffectedDirsTest(unittest.TestCase):
         project.get_property.side_effect = lambda _property: (
             _property if _property != 'dir_source_integrationtest_python' else None)
 
+        project.expand_path.side_effect = lambda _property: (
+            _property if _property != '$dir_source_integrationtest_python' else None)
+
         files = discover_affected_dirs(True, False, project)
 
-        self.assertEqual(project.get_property.call_args_list,
-                         [call('dir_source_main_python'),
-                          call('dir_source_unittest_python'),
-                          call('dir_source_unittest_python'),
-                          call('dir_source_integrationtest_python')])
+        self.assertEqual(project.expand_path.call_args_list,
+                         [call('$dir_source_main_python'),
+                          call('$dir_source_unittest_python')])
         self.assertEqual(files,
-                         ['dir_source_main_python', 'dir_source_unittest_python'])
+                         ['$dir_source_main_python', '$dir_source_unittest_python'])
 
     @patch('pybuilder.plugins.python.python_plugin_helper.os.path.isdir', return_value=True)
     def test_should_discover_source_dirs_when_script_sources_are_included(self, _):
@@ -196,14 +202,16 @@ class DiscoverAffectedDirsTest(unittest.TestCase):
         project.get_property.side_effect = lambda _property: (
             _property if _property != 'dir_source_integrationtest_python' else None)
 
+        project.expand_path.side_effect = lambda _property: (
+            _property if _property != '$dir_source_integrationtest_python' else None)
+
         files = discover_affected_dirs(False, True, project)
 
-        self.assertEqual(project.get_property.call_args_list,
-                         [call('dir_source_main_python'),
-                          call('dir_source_main_scripts'),
-                          call('dir_source_main_scripts')])
+        self.assertEqual(project.expand_path.call_args_list,
+                         [call('$dir_source_main_python'),
+                          call('$dir_source_main_scripts')])
         self.assertEqual(files,
-                         ['dir_source_main_python', 'dir_source_main_scripts'])
+                         ['$dir_source_main_python', '$dir_source_main_scripts'])
 
     @patch('pybuilder.plugins.python.python_plugin_helper.os.path.isdir', return_value=True)
     def test_if_property_set_and_dir_exists(self, exists):