| PYTRACEABILITY-1 |
{'info': 'pytraceability searches a directory for traceability decorators'} |
pytraceability/collector.py |
PyTraceabilityCollector._get_file_paths |
38 to 44 |
No |
def _get_file_paths(self) -> Generator[Path, None, None]:
_log.info("Using exclude patterns %s", self.config.exclude_patterns)
for file_path in self.config.base_directory.rglob("*.py"):
if file_is_excluded(file_path, self.config.exclude_patterns):
_log.debug("Skipping %s", file_path)
continue
yield file_path
|
|
| PYTRACEABILITY-2 |
{'info': 'pytraceability extracts traceability info from the decorators statically'} |
pytraceability/ast_processing.py |
extract_traceability_from_file_using_ast |
143 to 156 |
No |
def extract_traceability_from_file_using_ast(
file_path: Path, decorator_name: str
) -> list[TraceabilityReport]:
_log.info("Extracting traceability from file: %s", file_path)
with open(file_path, "r") as f:
source_code = f.read()
try:
tree = ast.parse(source_code, filename=file_path)
except SyntaxError:
_log.warning(f"Ignoring file due to syntax error: {file_path}")
return []
return TraceabilityVisitor(
decorator_name, file_path=file_path, source_code=source_code
).visit(tree)
|
|
| PYTRACEABILITY-3 |
{'info': "If pytraceability can't extract data statically, it has the option to try to extract it dynamically by importing the module."} |
pytraceability/collector.py |
PyTraceabilityCollector.collect |
51 to 100 |
No |
def collect(self) -> list[TraceabilityReport]:
traceability_reports: dict[str, TraceabilityReport] = {}
for file_path in self._get_file_paths():
for report in extract_traceability_from_file_using_ast(
file_path, self.config.decorator_name
):
traceability_reports[report.key] = report
incomplete_reports = [
t for t in traceability_reports.values() if t.contains_raw_source_code
]
_log.info(
"%s traceability decorators contain raw source code.",
len(incomplete_reports),
)
if (
self.config.mode == PyTraceabilityMode.MODULE_IMPORT
and len(incomplete_reports) > 0
):
if self.config.python_root is None: # pragma: no cover
# Should never actually end up here, because the model_validator will
# default this to base_directory, but we can't set it as non-optional
# because it would break typing checking at model creation
raise ValueError(
f"Python root directory must be set in {PyTraceabilityMode.MODULE_IMPORT} mode"
)
for file_path, traceabilities in groupby(
incomplete_reports, attrgetter("file_path")
):
for (
extracted_traceability
) in extract_traceabilities_using_module_import(
file_path, self.config.python_root, traceabilities
):
traceability_reports[
extracted_traceability.key
].metadata = extracted_traceability.metadata
if self.config.git_history_mode == GitHistoryMode.FUNCTION_HISTORY:
_log.info("Collecting git history for traceability reports")
git_histories = get_line_based_history(
list(traceability_reports.values()), self.config
)
for traceability_key, git_history in git_histories.items():
traceability_reports[traceability_key].history = git_history
elif self.config.git_history_mode != GitHistoryMode.NONE:
raise ValueError(
f"Unsupported git history mode: {self.config.git_history_mode}"
)
return list(traceability_reports.values())
|
|
| PYTRACEABILITY-4 |
{'info': "If pytraceability can't extract the key either statically or dynamically, anInvalidTraceabilityError is raised. This might happen for a closure where the traceability key is stored in a variable."} |
pytraceability/import_processing.py |
extract_traceabilities_using_module_import |
69 to 77 |
No |
def extract_traceabilities_using_module_import(
file_path: Path,
python_root: Path,
traceability_reports: Iterator[TraceabilityReport],
) -> Generator[Traceability, None, None]:
_log.info("Extracting traceability from %s using module import", file_path)
module = _load_python_module(file_path, python_root)
for traceability_report in traceability_reports:
yield from _extract_traceability(module, traceability_report.function_name)
|
|
| PYTRACEABILITY-5 |
{'info': 'pytraceability can extract a history of the code decorated by a given key from git'} |
pytraceability/history.py |
get_line_based_history |
52 to 107 |
No |
def get_line_based_history(
traceability_reports: list[TraceabilityReport], config: PyTraceabilityConfig
) -> dict[str, list[TraceabilityGitHistory]]:
current_file_for_key = CurrentFileForKey.from_traceability_reports(
traceability_reports, config
)
history: dict[str, list[TraceabilityGitHistory]] = {}
repo_root = get_repo_root(config.base_directory)
for commit in Repository(
str(repo_root),
order="reverse",
only_in_branch=config.git_branch,
).traverse_commits():
current_file_set = set(current_file_for_key.values())
relevant_files_first = sorted(
commit.modified_files,
key=lambda f: f.new_path in current_file_set,
)
current_file_for_key.reset_keys_for_relevant_files(relevant_files_first)
for modified_file in relevant_files_first:
if (
modified_file.source_code is None
or modified_file.new_path is None
or not modified_file.new_path.endswith("py")
or file_is_excluded(
Path(modified_file.new_path), config.exclude_patterns
)
):
continue
_log.debug("Processing file %s", modified_file.new_path)
tree = ast.parse(modified_file.source_code, filename=modified_file.new_path)
traceability_reports = TraceabilityVisitor(
config.decorator_name,
file_path=Path(modified_file.new_path),
source_code=modified_file.source_code,
).visit(tree)
for traceability_report in traceability_reports:
if traceability_report.key not in history:
history[traceability_report.key] = []
history[traceability_report.key].append(
TraceabilityGitHistory(
commit=commit.hash,
author_name=commit.author.name,
author_date=commit.author_date,
message=commit.msg.strip(),
source_code=traceability_report.source_code,
)
)
current_file_for_key[traceability_report.key] = modified_file.new_path
if all(current_file_for_key.values()):
_log.info("All traceability decorators located for commit")
break
return history
|
|