Skip to content

Commit 594cd27

Browse files
PYSCAN-44 Read extra analysis properties from pyproject.toml files (#34)
1 parent 4650db7 commit 594cd27

File tree

6 files changed

+110
-24
lines changed

6 files changed

+110
-24
lines changed

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ There are multiple ways of providing these properties, described below in descen
2020

2121
* Through CLI arguments to the `sonar-scanner-python` command
2222
* Under the `[tool.sonar]` key of the `pyproject.toml` file
23+
* Through common properties extracted from the `pyproject.toml`
2324
* In a dedicated `sonar-project.properties` file
2425
* Through environment variables
2526

@@ -77,6 +78,16 @@ Or:
7778
sonar-scanner-python -Dsonar.projectHome="path/to/projectHome"
7879
```
7980

81+
82+
### Through project properties extracted from the `pyproject.toml`
83+
84+
When a `pyproject.toml` file is available, it is possible to set the `-read-project-config` flag
85+
to allow the scanner to deduce analysis properties from the project configuration.
86+
87+
This is currently supported only for projects using `poetry`.
88+
89+
The Sonar scanner will then use the project name and version defined through Poetry, they won't have to be duplicated under a dedicated `tool.sonar` section.
90+
8091
### With a sonar-project.properties file
8192

8293
Exactly like [__SonarScanner__](https://docs.sonarsource.com/sonarqube/9.9/analyzing-source-code/scanners/sonarscanner/),

src/py_sonar_scanner/configuration.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ def __init__(self):
4444
self.sonar_scanner_version = "4.6.2.2472"
4545
self.sonar_scanner_executable_path = ""
4646
self.scan_arguments = []
47-
self.wrapper_arguments = argparse.Namespace(debug=False)
47+
self.wrapper_arguments = argparse.Namespace(debug=False, read_project_config=False)
4848

4949
def setup(self) -> None:
5050
"""This is executed when run from the command line"""
@@ -60,6 +60,14 @@ def _read_wrapper_arguments(self):
6060
argument_parser.add_argument("-toml.path", "-Dtoml.path", "--toml.path", dest="toml_path")
6161
argument_parser.add_argument("-project.home", "-Dproject.home", "--project.home", dest="project_home")
6262
argument_parser.add_argument("-X", action="store_true", dest="debug")
63+
argument_parser.add_argument(
64+
"-read.project.config",
65+
"-Dread.project.config",
66+
"--read-project-config",
67+
"-read-project-config",
68+
action="store_true",
69+
dest="read_project_config",
70+
)
6371
self.wrapper_arguments, _ = argument_parser.parse_known_args(args=sys.argv[1:])
6472

6573
def _read_toml_args(self) -> list[str]:
@@ -69,6 +77,10 @@ def _read_toml_args(self) -> list[str]:
6977
toml_data = self._read_toml_file()
7078
except OSError as e:
7179
self.log.exception("Error while opening .toml file: %s", str(e))
80+
if self.wrapper_arguments.read_project_config:
81+
common_toml_properties = self._extract_common_properties(toml_data)
82+
for key, value in common_toml_properties.items():
83+
self._add_parameter_to_scanner_args(scan_arguments, key, value)
7284
sonar_properties = self._extract_sonar_properties(toml_data)
7385
for key, value in sonar_properties.items():
7486
self._add_parameter_to_scanner_args(scan_arguments, key, value)
@@ -83,6 +95,23 @@ def _extract_sonar_properties(self, toml_properties):
8395
sonar_properties = tool_data["sonar"]
8496
return sonar_properties if isinstance(sonar_properties, dict) else {}
8597

98+
def _extract_common_properties(self, toml_properties):
99+
properties = {}
100+
if "tool" in toml_properties.keys() and "poetry" in toml_properties["tool"]:
101+
poetry_properties = toml_properties["tool"]["poetry"]
102+
properties = self._extract_from_poetry_properties(poetry_properties)
103+
return properties
104+
105+
def _extract_from_poetry_properties(self, poetry_properties):
106+
result = {}
107+
if "name" in poetry_properties:
108+
result["project.name"] = poetry_properties["name"]
109+
if "version" in poetry_properties:
110+
result["project.version"] = poetry_properties["version"]
111+
# Note: Python version can be extracted from dependencies.python, however it
112+
# may be specified with constraints, e.g ">3.8", which is not currently supported by sonar-python
113+
return result
114+
86115
def _add_parameter_to_scanner_args(self, scan_arguments: list[str], key: str, value: Union[str, dict]):
87116
if isinstance(value, str):
88117
scan_arguments.append(f"-Dsonar.{key}={value}")

tests/resources/test_toml_file.toml

Lines changed: 0 additions & 10 deletions
This file was deleted.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
[tool]
2+
ignored_property="something"
3+
4+
[tool.poetry]
5+
name = "my_name"
6+
version = "0.0.1"
7+
8+
[tool.sonar]
9+
project.name = "overridden_name"
10+
python.version = "3.10"
11+
12+
[tool.sonar.property_class]
13+
property1="value1"
14+
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
[tool.poetry]
2+
random="42"
3+
4+
[tool.sonar]
5+
project.name = "my_project_name"
6+
python.version = "3.10"

tests/test_configuration.py

Lines changed: 49 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@
2424
from py_sonar_scanner.logger import ApplicationLogger
2525

2626
CURRENT_DIR = os.path.dirname(os.path.abspath(__file__))
27-
TEST_TOML_FILE = "test_toml_file.toml"
27+
TEST_TOML_FILE_POETRY = "test_toml_file_poetry.toml"
28+
TOML_NO_COMMON_PROPERTIES = "toml_no_common_properties.toml"
2829
SAMPLE_SCANNER_PATH = "path/to/scanner/py-sonar-scanner"
2930

3031

@@ -129,40 +130,75 @@ def test_dict_with_valid_values(self, mock_sys):
129130
@patch("py_sonar_scanner.configuration.sys")
130131
def test_toml_with_valid_values(self, mock_sys):
131132
configuration = Configuration()
132-
toml_file_path = os.path.join(CURRENT_DIR, "resources", TEST_TOML_FILE)
133+
toml_file_path = os.path.join(CURRENT_DIR, "resources", TEST_TOML_FILE_POETRY)
133134
mock_sys.argv = [SAMPLE_SCANNER_PATH, f"-Dtoml.path={toml_file_path}"]
134135
configuration.setup()
135136
self.assertListEqual(
136137
configuration.scan_arguments,
137138
[
138-
"-Dsonar.property1=value1",
139-
"-Dsonar.property2=value2",
139+
"-Dsonar.project.name=overridden_name",
140+
"-Dsonar.python.version=3.10",
140141
"-Dsonar.property_class.property1=value1",
141-
f"-Dtoml.path={CURRENT_DIR}/resources/test_toml_file.toml",
142+
f"-Dtoml.path={CURRENT_DIR}/resources/{TEST_TOML_FILE_POETRY}",
143+
],
144+
)
145+
146+
@patch("py_sonar_scanner.configuration.sys")
147+
def test_toml_overridden_common_properties(self, mock_sys):
148+
configuration = Configuration()
149+
toml_file_path = os.path.join(CURRENT_DIR, "resources", TEST_TOML_FILE_POETRY)
150+
mock_sys.argv = [SAMPLE_SCANNER_PATH, f"-Dtoml.path={toml_file_path}", "-read.project.config"]
151+
configuration.setup()
152+
self.assertListEqual(
153+
configuration.scan_arguments,
154+
[
155+
"-Dsonar.project.name=my_name",
156+
"-Dsonar.project.version=0.0.1",
157+
"-Dsonar.project.name=overridden_name",
158+
"-Dsonar.python.version=3.10",
159+
"-Dsonar.property_class.property1=value1",
160+
f"-Dtoml.path={CURRENT_DIR}/resources/{TEST_TOML_FILE_POETRY}",
161+
"-read.project.config",
162+
],
163+
)
164+
165+
@patch("py_sonar_scanner.configuration.sys")
166+
def test_toml_no_common_properties(self, mock_sys):
167+
configuration = Configuration()
168+
toml_file_path = os.path.join(CURRENT_DIR, "resources", TOML_NO_COMMON_PROPERTIES)
169+
mock_sys.argv = [SAMPLE_SCANNER_PATH, f"-Dtoml.path={toml_file_path}", "-read.project.config"]
170+
configuration.setup()
171+
self.assertListEqual(
172+
configuration.scan_arguments,
173+
[
174+
"-Dsonar.project.name=my_project_name",
175+
"-Dsonar.python.version=3.10",
176+
f"-Dtoml.path={CURRENT_DIR}/resources/{TOML_NO_COMMON_PROPERTIES}",
177+
"-read.project.config",
142178
],
143179
)
144180

145181
@patch("py_sonar_scanner.configuration.sys")
146182
def test_duplicate_values_toml_cli(self, mock_sys):
147183
configuration = Configuration()
148-
toml_file_path = os.path.join(CURRENT_DIR, "resources", TEST_TOML_FILE)
149-
mock_sys.argv = [SAMPLE_SCANNER_PATH, f"-Dtoml.path={toml_file_path}", "-Dsonar.property1=value1"]
184+
toml_file_path = os.path.join(CURRENT_DIR, "resources", TEST_TOML_FILE_POETRY)
185+
mock_sys.argv = [SAMPLE_SCANNER_PATH, f"-Dtoml.path={toml_file_path}", "-Dsonar.project.name=second_override"]
150186
configuration.setup()
151187
self.assertListEqual(
152188
configuration.scan_arguments,
153189
[
154-
"-Dsonar.property1=value1",
155-
"-Dsonar.property2=value2",
190+
"-Dsonar.project.name=overridden_name",
191+
"-Dsonar.python.version=3.10",
156192
"-Dsonar.property_class.property1=value1",
157-
f"-Dtoml.path={CURRENT_DIR}/resources/test_toml_file.toml",
158-
"-Dsonar.property1=value1",
193+
f"-Dtoml.path={CURRENT_DIR}/resources/{TEST_TOML_FILE_POETRY}",
194+
"-Dsonar.project.name=second_override",
159195
],
160196
)
161197

162198
@patch("builtins.open")
163199
@patch("py_sonar_scanner.configuration.sys")
164200
def test_error_while_reading_toml_file(self, mock_sys, mock_open):
165-
toml_file_path = os.path.join(CURRENT_DIR, "resources", TEST_TOML_FILE)
201+
toml_file_path = os.path.join(CURRENT_DIR, "resources", TEST_TOML_FILE_POETRY)
166202
mock_sys.argv = ["path/to/scanner/py-sonar-scanner", f"-Dtoml.path={toml_file_path}"]
167203

168204
mock_open.side_effect = OSError("Test error while opening file.")
@@ -173,7 +209,7 @@ def test_error_while_reading_toml_file(self, mock_sys, mock_open):
173209
self.assertListEqual(
174210
configuration.scan_arguments,
175211
[
176-
f"-Dtoml.path={CURRENT_DIR}/resources/test_toml_file.toml",
212+
f"-Dtoml.path={CURRENT_DIR}/resources/{TEST_TOML_FILE_POETRY}",
177213
],
178214
)
179215

0 commit comments

Comments
 (0)