Approval Validator
A CLI to validate that sufficient approvals have been received for a changeset in the context of a project.
Table of Contents
Installation
The quickest way to use the tool in anger is to install it using pip
:
pip install approvals-validator
.
Alternatively, run the executable from the project root. Install dependencies
with bin/setup
.
Usage
% validate_approvals --help
Usage: validate_approvals REQUIRED_FLAGS
Validate that the correct approvals have been received to approve changes
to the given files.
Note: Multiple approvers and/or changed files can be passed as CSV strings.
Example:
validate_approvals --approvers alovelace,eclarke --changed-files src/com/twitter/follow/Follow.java
Options:
-a, --approvers USERNAMES Username(s) of approvals. [required]
-c, --changed-files FILE_PATHS File paths. [required]
-h, --help Show this message and exit.
Requirements
- Python >= 3.8.0 (for
functools.cached_property
)
A .tool-versions
file is included for asdf users.
Dependencies
The test-runner script (./test
) will attempt to install dependencies in a
virtualenv at project root named ./env
.
For reference, bin/setup
usage instructions:
Usage:
./bin/setup [OPTIONS] ENV
Install dependencies for `validate_approvals` in a virtualenv at project root.
Available environments:
dev Install all dependencies
prod Install minimal dependencies for running `validate_approvals`
test Install minimal and test dependencies
Available options:
--silent Run without verbose output
Tests
A test runner script is included to run the entire test suite and display code
coverage metrics. Pass the --docker
flag to (re-)build a Docker image and
run tests with Docker.
Acceptances tests are written in Bash script, unit and integration tests in Python with pytest.
% ./test
Running acceptance tests...
./validate_approvals -c data/minimal/y/file -a B
./validate_approvals -c data/minimal/y/file -a A,C
./validate_approvals -c data/minimal/y/file -a D
./validate_approvals --approvers alovelace,ghopper --changed-files data/repo/src/com/twitter/follow/Follow.java,data/repo/src/com/twitter/user/User.java
./validate_approvals --approvers alovelace --changed-files data/repo/src/com/twitter/follow/Follow.java
./validate_approvals --approvers eclarke --changed-files data/repo/src/com/twitter/follow/Follow.java
./validate_approvals --approvers alovelace,eclarke --changed-files data/repo/src/com/twitter/follow/Follow.java
./validate_approvals --approvers mfox --changed-files data/repo/src/com/twitter/tweet/Tweet.java
Running pytest tests...
Running mypy on 11 files... done with status 0
Success: no issues found in 11 source files
............................... [100%]
---------- coverage: platform darwin, python 3.8.0-final-0 -----------
Name Stmts Miss Cover
------------------------------------------------------------------------
approval_validator/__init__.py 3 0 100%
approval_validator/change_set.py 14 14 0%
approval_validator/changed_directory.py 30 0 100%
approval_validator/cli_utils.py 14 14 0%
approval_validator/exceptions.py 9 2 78%
approval_validator/file_utils.py 74 0 100%
approval_validator/tests/__init__.py 0 0 100%
approval_validator/tests/changed_directory_test.py 37 0 100%
approval_validator/tests/file_utils_test.py 59 4 93%
------------------------------------------------------------------------
TOTAL 240 34 86%
Design Notes
The script entrypoint is the CLI function in the executable
validate_approvals
.
The approval_validator.cli_utils
module defines how arguments are parsed.
ChangeSet
, ChangedDirectory
The main classes are ChangeSet
and ChangedDirectory
.
The former models an entire changeset (i.e., all the files passed via the
--changed-files
flag), the latter each individual entry in the list of files
passed to --changed_files
.
# approval_validator/changed_directory.py L24-37
@cached_property
def affected_directories(self) -> Tuple[Path, ...]:
return util.find_dependent_dirs(self.directory)
@cached_property
def approved(self) -> bool:
"""
Return true if sufficient approval has been received for this
ChangedDirectory.
"""
for impacted_dir in self.impacted_directories:
if not self.__change_approved(impacted_dir):
return False
return True
file_utils
File-parsing and directory-traversal logic is housed in the file_utils
module.
exceptions
Defines ApprovalValidatorError
, the base class for library-specific
exceptions, and ProjectRootNotFoundError
, which is raised when a project root
can't be found.
# approval_validator/exceptions.py L8-20
class ProjectRootNotFoundError(ApprovalValidatorError):
"""Raised when a project root can't be found."""
def __init__(self, start_dir):
self.start_dir = start_dir
def __str__(self):
message = f"""
Project root search failed. Started from: {self.start_dir}
Note: We detect the presence of a project root using the entries of
PROJECT_ROOT_FILES. (see: approval_validator/file_utils.py)
"""
return f"\n\n{cleandoc(message)}"
Performance
Caching improved running time by ~20%. The following facilities are used: