"""Source code."""
from __future__ import annotations
import hashlib
import logging
from pathlib import Path
from typing import TYPE_CHECKING, Iterator, List, Optional, Sequence, Union
import igittigitt
from runway.compat import cached_property
from runway.utils import FileHash
if TYPE_CHECKING:
from _typeshed import StrPath
LOGGER = logging.getLogger(__name__)
[docs]class SourceCode:
"""Source code iterable."""
gitignore_filter: igittigitt.IgnoreParser
"""Filter to use when zipping dependencies.
If file/folder matches the filter, it should be ignored.
"""
project_root: Path
"""Top-level directory containing the project metadata files and source code root directory.
The value can be the same as ``root_directory``.
If it is not, it must be a parent of ``root_directory``.
"""
root_directory: Path
"""The root directory containing the source code."""
[docs] def __init__(
self,
root_directory: StrPath,
*,
gitignore_filter: Optional[igittigitt.IgnoreParser] = None,
include_files_in_hash: Optional[Sequence[Path]] = None,
project_root: Optional[StrPath] = None,
) -> None:
"""Instantiate class.
Args:
root_directory: The root directory containing the source code.
gitignore_filter: Object that has been pre-populated with
rules/patterns to determine if a file should be ignored.
include_files_in_hash: Files that should be included in hash
calculation even if they are filtered by gitignore (e.g.
``poetry.lock``).
project_root: Optional project root if the source code is located
within a larger project. This should only be used if the
contents of value of ``include_files_in_hash`` contains paths
that exist outside of the root directory. If this is provided,
it must be a parent of the root directory.
"""
self._include_files_in_hash = include_files_in_hash or []
self.gitignore_filter = gitignore_filter or igittigitt.IgnoreParser()
self.root_directory = (
root_directory if isinstance(root_directory, Path) else Path(root_directory)
)
self.project_root = ( # defaults to root_directory if project_root not provided
project_root
if isinstance(project_root, Path)
else (Path(project_root) if project_root else self.root_directory)
)
if not gitignore_filter:
self.gitignore_filter.parse_rule_files(self.root_directory)
self.gitignore_filter.add_rule(".git/", self.root_directory)
self.gitignore_filter.add_rule(".gitignore", self.root_directory)
@cached_property
def md5_hash(self) -> str:
"""Calculate the md5 hash of the directory contents.
This can be resource intensive depending on the size of the project.
"""
sorted_files = list(self.sorted())
for include_file in self._include_files_in_hash:
if include_file not in sorted_files:
sorted_files.append(include_file)
file_hash = FileHash(hashlib.md5())
file_hash.add_files(sorted(sorted_files), relative_to=self.project_root)
return file_hash.hexdigest
[docs] def add_filter_rule(self, pattern: str) -> None:
"""Add rule to ignore filter.
Args:
pattern: The gitignore pattern to add to the filter.
"""
self.gitignore_filter.add_rule(pattern=pattern, base_path=self.root_directory)
[docs] def sorted(self, *, reverse: bool = False) -> List[Path]:
"""Sorted list of source code files.
Args:
reverse: Sort the list in reverse.
Returns:
Sorted list of source code files excluding those that match the
ignore filter.
"""
return sorted(self, reverse=reverse)
[docs] def __eq__(self, other: object) -> bool:
"""Compare if self is equal to another object."""
if isinstance(other, SourceCode):
return self.root_directory == other.root_directory
return False
[docs] def __fspath__(self) -> Union[str, bytes]:
"""Return the file system path representation of the object."""
return str(self.root_directory)
[docs] def __iter__(self) -> Iterator[Path]:
"""Iterate over the source code files.
Yields:
Files that do not match the ignore filter. Order in arbitrary.
"""
for child in self.root_directory.rglob("*"):
if child.is_dir():
continue # ignore directories
if self.gitignore_filter.match(child):
continue # ignore files that match the filter
yield child
[docs] def __str__(self) -> str:
"""Return the string representation of the object."""
return str(self.root_directory)
[docs] def __truediv__(self, other: StrPath) -> Path:
"""Create a new path object from source code's root directory."""
return self.root_directory / other