Skip to content

Commit 4060c9f

Browse files
authored
Chore/refactor inspect module
* chore:SP-3607 refactor inspect module * chore: Adds local linter to Makefile
1 parent 4d5482e commit 4060c9f

File tree

19 files changed

+449
-284
lines changed

19 files changed

+449
-284
lines changed

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99
### Added
1010
- Upcoming changes...
1111

12+
## [1.40.1] - 2025-10-29
13+
### Changed
14+
- Refactored inspect module structure for better organization
15+
- Reorganized inspection modules into `policy_check` and `summary` subdirectories
16+
- Moved copyleft and undeclared component checks to `policy_check/scanoss/`
17+
- Moved component, license, and match summaries to `summary/`
18+
- Moved Dependency Track policy checks to `policy_check/dependency_track/`
19+
- Extracted common scan result processing logic into `ScanResultProcessor` utility class
20+
- Improved type safety with `PolicyOutput` named tuple for policy check results
21+
- Made `PolicyCheck` class explicitly abstract with ABC
22+
### Added
23+
- Added Makefile targets for running ruff linter (`linter`, `linter-fix`, `linter-docker`, `linter-docker-fix`)
24+
1225
## [1.40.0] - 2025-10-29
1326
### Added
1427
- Add support for `--rest` to `folder-scan` command
@@ -716,3 +729,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
716729
[1.38.0]: https://github.com/scanoss/scanoss.py/compare/v1.37.1...v1.38.0
717730
[1.39.0]: https://github.com/scanoss/scanoss.py/compare/v1.38.0...v1.39.0
718731
[1.40.0]: https://github.com/scanoss/scanoss.py/compare/v1.39.0...v1.40.0
732+
[1.40.1]: https://github.com/scanoss/scanoss.py/compare/v1.40.0...v1.40.1

Makefile

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ dev_setup: date_time_clean ## Setup Python dev env for the current user
3535
@echo "Setting up dev env for the current user..."
3636
pip3 install -e .
3737

38+
dev_install: ## Install dev dependencies
39+
pip3 install -r requirements-dev.txt
40+
3841
dev_uninstall: ## Uninstall Python dev setup for the current user
3942
@echo "Uninstalling dev env..."
4043
pip3 uninstall -y scanoss
@@ -50,6 +53,18 @@ publish_test: ## Publish the Python package to TestPyPI
5053
@echo "Publishing package to TestPyPI..."
5154
twine upload --repository testpypi dist/*
5255

56+
lint-docker: ## Run ruff linter with docker
57+
@./tools/linter.sh --docker
58+
59+
lint-docker-fix: ## Run ruff linter with docker and auto-fix
60+
@./tools/linter.sh --docker --fix
61+
62+
lint: ## Run ruff linter locally
63+
@./tools/linter.sh
64+
65+
lint-fix: ## Run ruff linter locally with auto-fix
66+
@./tools/linter.sh --fix
67+
5368
publish: ## Publish Python package to PyPI
5469
@echo "Publishing package to PyPI..."
5570
twine upload dist/*

src/scanoss/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,4 @@
2222
THE SOFTWARE.
2323
"""
2424

25-
__version__ = '1.40.0'
25+
__version__ = '1.40.1'

src/scanoss/cli.py

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,6 @@
3535
from scanoss.cryptography import Cryptography, create_cryptography_config_from_args
3636
from scanoss.delta import Delta
3737
from scanoss.export.dependency_track import DependencyTrackExporter
38-
from scanoss.inspection.dependency_track.project_violation import (
39-
DependencyTrackProjectViolationPolicyCheck,
40-
)
41-
from scanoss.inspection.raw.component_summary import ComponentSummary
42-
from scanoss.inspection.raw.license_summary import LicenseSummary
43-
from scanoss.inspection.raw.match_summary import MatchSummary
4438
from scanoss.scanners.container_scanner import (
4539
DEFAULT_SYFT_COMMAND,
4640
DEFAULT_SYFT_TIMEOUT,
@@ -75,8 +69,14 @@
7569
from .cyclonedx import CycloneDx
7670
from .filecount import FileCount
7771
from .gitlabqualityreport import GitLabQualityReport
78-
from .inspection.raw.copyleft import Copyleft
79-
from .inspection.raw.undeclared_component import UndeclaredComponent
72+
from .inspection.policy_check.dependency_track.project_violation import (
73+
DependencyTrackProjectViolationPolicyCheck,
74+
)
75+
from .inspection.policy_check.scanoss.copyleft import Copyleft
76+
from .inspection.policy_check.scanoss.undeclared_component import UndeclaredComponent
77+
from .inspection.summary.component_summary import ComponentSummary
78+
from .inspection.summary.license_summary import LicenseSummary
79+
from .inspection.summary.match_summary import MatchSummary
8080
from .results import Results
8181
from .scancodedeps import ScancodeDeps
8282
from .scanner import FAST_WINNOWING, Scanner
@@ -1753,7 +1753,6 @@ def inspect_copyleft(parser, args):
17531753
exclude=args.exclude, # Licenses to ignore
17541754
explicit=args.explicit, # Explicit license list
17551755
)
1756-
17571756
# Execute inspection and exit with appropriate status code
17581757
status, _ = i_copyleft.run()
17591758
sys.exit(status)

src/scanoss/gitlabqualityreport.py

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,16 +74,21 @@ def __init__(self, debug: bool = False, trace: bool = False, quiet: bool = False
7474
Initialise the GitLabCodeQuality class
7575
"""
7676
super().__init__(debug, trace, quiet)
77+
self.print_trace(f"GitLabQualityReport initialized with debug={debug}, trace={trace}, quiet={quiet}")
7778

7879

7980
def _get_code_quality(self, file_name: str, result: dict) -> CodeQuality or None:
81+
self.print_trace(f"_get_code_quality called for file: {file_name}")
82+
self.print_trace(f"Processing result: {result}")
83+
8084
if not result.get('file_hash'):
8185
self.print_debug(f"Warning: no hash found for result: {result}")
8286
return None
8387

8488
if result.get('id') == 'file':
89+
self.print_debug(f"Processing file match for: {file_name}")
8590
description = f"File match found in: {file_name}"
86-
return CodeQuality(
91+
code_quality = CodeQuality(
8792
description=description,
8893
check_name=file_name,
8994
fingerprint=result.get('file_hash'),
@@ -95,17 +100,21 @@ def _get_code_quality(self, file_name: str, result: dict) -> CodeQuality or None
95100
)
96101
)
97102
)
103+
self.print_trace(f"Created file CodeQuality object: {code_quality}")
104+
return code_quality
98105

99106
if not result.get('lines'):
100107
self.print_debug(f"Warning: No lines found for result: {result}")
101108
return None
102109
lines = scanoss_scan_results_utils.get_lines(result.get('lines'))
110+
self.print_trace(f"Extracted lines: {lines}")
103111
if len(lines) == 0:
104112
self.print_debug(f"Warning: empty lines for result: {result}")
105113
return None
106114
end_line = lines[len(lines) - 1] if len(lines) > 1 else lines[0]
107115
description = f"Snippet found in: {file_name} - lines {lines[0]}-{end_line}"
108-
return CodeQuality(
116+
self.print_debug(f"Processing snippet match for: {file_name}, lines: {lines[0]}-{end_line}")
117+
code_quality = CodeQuality(
109118
description=description,
110119
check_name=file_name,
111120
fingerprint=result.get('file_hash'),
@@ -117,35 +126,47 @@ def _get_code_quality(self, file_name: str, result: dict) -> CodeQuality or None
117126
)
118127
)
119128
)
129+
self.print_trace(f"Created snippet CodeQuality object: {code_quality}")
130+
return code_quality
120131

121132
def _write_output(self, data: list[CodeQuality], output_file: str = None) -> bool:
122133
"""Write the Gitlab Code Quality Report to output."""
134+
self.print_trace(f"_write_output called with {len(data)} items, output_file: {output_file}")
123135
try:
124136
json_data = [item.to_dict() for item in data]
137+
self.print_trace(f"JSON data: {json_data}")
125138
file = open(output_file, 'w') if output_file else sys.stdout
126139
print(json.dumps(json_data, indent=2), file=file)
127140
if output_file:
128141
file.close()
142+
self.print_debug(f"Wrote output to file: {output_file}")
143+
else:
144+
self.print_debug("Wrote output to 'stdout'")
129145
return True
130146
except Exception as e:
131147
self.print_stderr(f'Error writing output: {str(e)}')
132148
return False
133149

134150
def _produce_from_json(self, data: dict, output_file: str = None) -> bool:
151+
self.print_trace(f"_produce_from_json called with output_file: {output_file}")
152+
self.print_debug(f"Processing {len(data)} files from JSON data")
135153
code_quality = []
136154
for file_name, results in data.items():
155+
self.print_trace(f"Processing file: {file_name} with {len(results)} results")
137156
for result in results:
138157
if not result.get('id'):
139158
self.print_debug(f"Warning: No ID found for result: {result}")
140159
continue
141160
if result.get('id') != 'snippet' and result.get('id') != 'file':
142-
self.print_debug(f"Skipping non-snippet/file match: {result}")
161+
self.print_debug(f"Skipping non-snippet/file match: {file_name}, id: '{result['id']}'")
143162
continue
144163
code_quality_item = self._get_code_quality(file_name, result)
145164
if code_quality_item:
146165
code_quality.append(code_quality_item)
166+
self.print_trace(f"Added code quality item for {file_name}")
147167
else:
148168
self.print_debug(f"Warning: No Code Quality found for result: {result}")
169+
self.print_debug(f"Generated {len(code_quality)} code quality items")
149170
self._write_output(data=code_quality,output_file=output_file)
150171
return True
151172

@@ -156,11 +177,15 @@ def _produce_from_str(self, json_str: str, output_file: str = None) -> bool:
156177
:param output_file: Output file (optional)
157178
:return: True if successful, False otherwise
158179
"""
180+
self.print_trace(f"_produce_from_str called with output_file: {output_file}")
159181
if not json_str:
160182
self.print_stderr('ERROR: No JSON string provided to parse.')
161183
return False
184+
self.print_debug(f"Parsing JSON string of length: {len(json_str)}")
162185
try:
163186
data = json.loads(json_str)
187+
self.print_debug("Successfully parsed JSON data")
188+
self.print_trace(f"Parsed data structure: {type(data)}")
164189
except Exception as e:
165190
self.print_stderr(f'ERROR: Problem parsing input JSON: {e}')
166191
return False
@@ -174,12 +199,16 @@ def produce_from_file(self, json_file: str, output_file: str = None) -> bool:
174199
:param output_file:
175200
:return: True if successful, False otherwise
176201
"""
202+
self.print_trace(f"produce_from_file called with json_file: {json_file}, output_file: {output_file}")
203+
self.print_debug(f"Input JSON file: {json_file}, output_file: {output_file}")
177204
if not json_file:
178205
self.print_stderr('ERROR: No JSON file provided to parse.')
179206
return False
180207
if not os.path.isfile(json_file):
181208
self.print_stderr(f'ERROR: JSON file does not exist or is not a file: {json_file}')
182209
return False
210+
self.print_debug(f"Reading JSON file: {json_file}")
183211
with open(json_file, 'r') as f:
184-
success = self._produce_from_str(f.read(), output_file)
212+
json_content = f.read()
213+
success = self._produce_from_str(json_content, output_file)
185214
return success

src/scanoss/inspection/policy_check/dependency_track/__init__.py

Whitespace-only changes.

src/scanoss/inspection/dependency_track/project_violation.py renamed to src/scanoss/inspection/policy_check/dependency_track/project_violation.py

Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,9 @@
2626
from datetime import datetime
2727
from typing import Any, Dict, List, Optional, TypedDict
2828

29-
from ...services.dependency_track_service import DependencyTrackService
30-
from ..policy_check import PolicyCheck, PolicyStatus
31-
from ..utils.markdown_utils import generate_jira_table, generate_table
29+
from ....services.dependency_track_service import DependencyTrackService
30+
from ...utils.markdown_utils import generate_jira_table, generate_table
31+
from ..policy_check import PolicyCheck, PolicyOutput, PolicyStatus
3232

3333
# Constants
3434
PROCESSING_RETRY_DELAY = 5 # seconds
@@ -171,7 +171,7 @@ def __init__( # noqa: PLR0913
171171
self.url = url.strip().rstrip('/') if url else None
172172
self.dep_track_service = DependencyTrackService(self.api_key, self.url, debug=debug, trace=trace, quiet=quiet)
173173

174-
def _json(self, project_violations: list[PolicyViolationDict]) -> Dict[str, Any]:
174+
def _json(self, project_violations: list[PolicyViolationDict]) -> PolicyOutput:
175175
"""
176176
Format project violations as JSON.
177177
@@ -181,12 +181,12 @@ def _json(self, project_violations: list[PolicyViolationDict]) -> Dict[str, Any]
181181
Returns:
182182
Dictionary containing JSON formatted results and summary
183183
"""
184-
return {
185-
"details": json.dumps(project_violations, indent=2),
186-
"summary": f'{len(project_violations)} policy violations were found.\n',
187-
}
184+
return PolicyOutput(
185+
details= json.dumps(project_violations, indent=2),
186+
summary= f'{len(project_violations)} policy violations were found.\n',
187+
)
188188

189-
def _markdown(self, project_violations: list[PolicyViolationDict]) -> Dict[str, Any]:
189+
def _markdown(self, project_violations: list[PolicyViolationDict]) -> PolicyOutput:
190190
"""
191191
Format Dependency Track violations to Markdown format.
192192
@@ -198,7 +198,7 @@ def _markdown(self, project_violations: list[PolicyViolationDict]) -> Dict[str,
198198
"""
199199
return self._md_summary_generator(project_violations, generate_table)
200200

201-
def _jira_markdown(self, data: list[PolicyViolationDict]) -> Dict[str, Any]:
201+
def _jira_markdown(self, data: list[PolicyViolationDict]) -> PolicyOutput:
202202
"""
203203
Format project violations for Jira Markdown.
204204
@@ -357,8 +357,7 @@ def _set_project_id(self) -> None:
357357
self.print_stderr(f'Error: Failed to get project uuid from: {dt_project}')
358358
raise ValueError(f'Error: Project {self.project_name}@{self.project_version} does not have a valid UUID')
359359

360-
@staticmethod
361-
def _sort_project_violations(violations: List[PolicyViolationDict]) -> List[PolicyViolationDict]:
360+
def _sort_project_violations(self,violations: List[PolicyViolationDict]) -> List[PolicyViolationDict]:
362361
"""
363362
Sort project violations by priority.
364363
@@ -377,7 +376,7 @@ def _sort_project_violations(violations: List[PolicyViolationDict]) -> List[Poli
377376
key=lambda x: -type_priority.get(x.get('type', 'OTHER'), 1)
378377
)
379378

380-
def _md_summary_generator(self, project_violations: list[PolicyViolationDict], table_generator):
379+
def _md_summary_generator(self, project_violations: list[PolicyViolationDict], table_generator) -> PolicyOutput:
381380
"""
382381
Generates a Markdown summary of project policy violations.
383382
@@ -396,10 +395,10 @@ def _md_summary_generator(self, project_violations: list[PolicyViolationDict], t
396395
"""
397396
if project_violations is None:
398397
self.print_stderr('Warning: No project violations found. Returning empty results.')
399-
return {
400-
"details": "h3. Dependency Track Project Violations\n\nNo policy violations found.\n",
401-
"summary": "0 policy violations were found.\n",
402-
}
398+
return PolicyOutput(
399+
details= "h3. Dependency Track Project Violations\n\nNo policy violations found.\n",
400+
summary= "0 policy violations were found.\n",
401+
)
403402
headers = ['State', 'Risk Type', 'Policy Name', 'Component', 'Date']
404403
c_cols = [0, 1]
405404
rows: List[List[str]] = []
@@ -424,11 +423,11 @@ def _md_summary_generator(self, project_violations: list[PolicyViolationDict], t
424423
]
425424
rows.append(row)
426425
# End for loop
427-
return {
428-
"details": f'### Dependency Track Project Violations\n{table_generator(headers, rows, c_cols)}\n\n'
426+
return PolicyOutput(
427+
details= f'### Dependency Track Project Violations\n{table_generator(headers, rows, c_cols)}\n\n'
429428
f'View project in Dependency Track [here]({self.url}/projects/{self.project_id}).\n',
430-
"summary": f'{len(project_violations)} policy violations were found.\n'
431-
}
429+
summary= f'{len(project_violations)} policy violations were found.\n'
430+
)
432431

433432
def run(self) -> int:
434433
"""
@@ -470,10 +469,11 @@ def run(self) -> int:
470469
self.print_stderr('Error: Invalid format specified.')
471470
return PolicyStatus.ERROR.value
472471
# Format and output data - handle empty results gracefully
473-
data = formatter(self._sort_project_violations(dt_project_violations))
474-
self.print_to_file_or_stdout(data['details'], self.output)
475-
self.print_to_file_or_stderr(data['summary'], self.status)
472+
policy_output = formatter(self._sort_project_violations(dt_project_violations))
473+
self.print_to_file_or_stdout(policy_output.details, self.output)
474+
self.print_to_file_or_stderr(policy_output.summary, self.status)
476475
# Return appropriate status based on violation count
477476
if len(dt_project_violations) > 0:
478477
return PolicyStatus.POLICY_FAIL.value
479478
return PolicyStatus.POLICY_SUCCESS.value
479+

0 commit comments

Comments
 (0)