Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add --git argument with 7 inputs #48

Open
wants to merge 16 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ Looking for a high-level overview of OpenType table differences rather than low-
- Display the first n lines of the diff output with the `--head` option
- Display the last n lines of the diff output with the `--tail` option
- Execute the diff with an external diff tool using the `--external` option
- Use in Git as a diff-driver to be the default diff tool for fonts

Run `fdiff --help` to view all available options.

Expand Down Expand Up @@ -103,6 +104,34 @@ $ fdiff [OPTIONS] [PRE-FONT FILE URL] [POST-FONT FILE FILE PATH]

⭐ **Tip**: Remote git repository hosting services (like Github) support access to files on different git branches by URL. Use these repository branch URL to compare fonts across git branches in your repository.

#### As Git's diff driver for fonts

Git can be configured to automatically use a specific tool to diff specific file types. These are the steps to use `fdiff` as the default diff output for font files.

1. Tell Git that the tool is available and how to run it. As with most git configuration options this may be set for a repository (`--local`), for your user (`--global`), or system (`--system`). We recommend setting this at the repository or user level. Assuming `fdiff` is available in your path, this setting should do the trick:

# Repository level
git config --local diff.fdiff.command 'fdiff -c --git'

# User level
git config --global diff.fdiff.command 'fdiff -c --git'

This will write something like the following to either the current repository's `.git/config` or your user's `$HOME/.gitconfig` file looking like this:

```gitconfig
[diff "fdiff"]
command = fdiff -c --git
chrissimpkins marked this conversation as resolved.
Show resolved Hide resolved
alerque marked this conversation as resolved.
Show resolved Hide resolved
```

Of course you may also edit the appropriate file and place that configuration manually.

2. Tell Git to actually use that specific tool for supported file types. This may also be done at multiple places. Each repository may have it's own `.gitattributes` file, a user may have one setting global defaults in `$XDG_HOME/git/attributes`, or there may be a system wide default file. Wherever you choose to place this, the lines are the same:

```gitattributes
*.otf diff=fdiff
*.ttf diff=fdiff
```

### Options

#### Color diffs
Expand Down
77 changes: 52 additions & 25 deletions lib/fdiff/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from fdiff.color import color_unified_diff_line
from fdiff.diff import external_diff, u_diff
from fdiff.textiter import head, tail
from fdiff.utils import file_exists, get_tables_argument_list
from fdiff.utils import path_exists, get_tables_argument_list


def main(): # pragma: no cover
Expand Down Expand Up @@ -45,13 +45,14 @@ def run(argv):
parser.add_argument(
"-l", "--lines", type=int, default=3, help="Number of context lines (default 3)"
)
parser.add_argument(
filters = parser.add_mutually_exclusive_group()
chrissimpkins marked this conversation as resolved.
Show resolved Hide resolved
filters.add_argument(
"--include",
type=str,
default=None,
help="Comma separated list of tables to include",
)
parser.add_argument(
filters.add_argument(
"--exclude",
type=str,
default=None,
Expand All @@ -63,40 +64,66 @@ def run(argv):
"--nomp", action="store_true", help="Do not use multi process optimizations"
)
parser.add_argument("--external", type=str, help="Run external diff tool command")
parser.add_argument("PREFILE", help="Font file path/URL 1")
parser.add_argument("POSTFILE", help="Font file path/URL 2")

args = parser.parse_args(argv)
parser.add_argument(
"--git",
type=str,
nargs=7,
help="Act as a diff driver for git (takes 7 parameters)",
metavar=(
"PATH",
"OLD-FILE",
"OLD-HEX",
"OLD-MODE",
"NEW-FILE",
"NEW-HEX",
"NEW-MODE",
),
)
# parser.add_argument("PREFILE", help="Font file path/URL 1")
# parser.add_argument("POSTFILE", help="Font file path/URL 2")

args, positionals = parser.parse_known_args(argv)

inputs = argparse.Namespace()
include_dir_paths = False
if args.git:
inputs.PREFILE = args.git[1]
inputs.POSTFILE = args.git[4]
# If the --git flag is used, we need to accept arguments
# that do not meet the definition of a file path using
# os.path.exists instead of os.path.isfile
# See https://github.com/source-foundry/fdiff/pull/48#discussion_r410424497
# for additional details
include_dir_paths = True
else:
inputparser = argparse.ArgumentParser()
inputparser.add_argument("PREFILE", help="Font file path/URL 1")
inputparser.add_argument("POSTFILE", help="Font file path/URL 2")
inputparser.parse_args(positionals, namespace=inputs)

# /////////////////////////////////////////////////////////
#
# Validations
#
# /////////////////////////////////////////////////////////

# ----------------------------------
# Incompatible argument validations
# ----------------------------------
# --include and --exclude are mutually exclusive options
if args.include and args.exclude:
chrissimpkins marked this conversation as resolved.
Show resolved Hide resolved
sys.stderr.write(
f"[*] Error: --include and --exclude are mutually exclusive options. "
f"Please use ONLY one of these options in your command.{os.linesep}"
)
sys.exit(1)

# -------------------------------
# File path argument validations
# -------------------------------

if not args.PREFILE.startswith("http") and not file_exists(args.PREFILE):
if not inputs.PREFILE.startswith("http") and not path_exists(
inputs.PREFILE, include_dir_paths=include_dir_paths
):
sys.stderr.write(
f"[*] ERROR: The file path '{args.PREFILE}' can not be found.{os.linesep}"
f"[*] ERROR: The file path '{inputs.PREFILE}' can not be found.{os.linesep}"
)
sys.exit(1)
if not args.PREFILE.startswith("http") and not file_exists(args.POSTFILE):
if not inputs.POSTFILE.startswith("http") and not path_exists(
inputs.POSTFILE, include_dir_paths=include_dir_paths
):
sys.stderr.write(
f"[*] ERROR: The file path '{args.POSTFILE}' can not be found.{os.linesep}"
f"[*] ERROR: The file path '{inputs.POSTFILE}' can not be found.{os.linesep}"
)
sys.exit(1)

Expand Down Expand Up @@ -138,8 +165,8 @@ def run(argv):
try:
diff = external_diff(
args.external,
args.PREFILE,
args.POSTFILE,
inputs.PREFILE,
inputs.POSTFILE,
include_tables=include_list,
exclude_tables=exclude_list,
use_multiprocess=use_mp,
Expand All @@ -164,8 +191,8 @@ def run(argv):
# perform the unified diff analysis
try:
diff = u_diff(
args.PREFILE,
args.POSTFILE,
inputs.PREFILE,
inputs.POSTFILE,
context_lines=args.lines,
include_tables=include_list,
exclude_tables=exclude_list,
Expand Down
10 changes: 7 additions & 3 deletions lib/fdiff/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,13 @@
from datetime import datetime, timezone


def file_exists(path):
"""Validates file path as existing local file"""
return os.path.isfile(path)
def path_exists(path, include_dir_paths=False):
"""Validates existing paths. The include_dir_paths parameter
toggles acceptance of dir paths in addition to file paths."""
if include_dir_paths:
return os.path.exists(path)
else:
return os.path.isfile(path)


def get_file_modtime(path):
Expand Down
41 changes: 32 additions & 9 deletions tests/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,24 @@

ROBOTO_BEFORE_PATH = os.path.join("tests", "testfiles", "Roboto-Regular.subset1.ttf")
ROBOTO_AFTER_PATH = os.path.join("tests", "testfiles", "Roboto-Regular.subset2.ttf")
ROBOTO_UDIFF_EXPECTED_PATH = os.path.join("tests", "testfiles", "roboto_udiff_expected.txt")
ROBOTO_UDIFF_COLOR_EXPECTED_PATH = os.path.join("tests", "testfiles", "roboto_udiff_color_expected.txt")
ROBOTO_UDIFF_1CONTEXT_EXPECTED_PATH = os.path.join("tests", "testfiles", "roboto_udiff_1context_expected.txt")
ROBOTO_UDIFF_HEADONLY_EXPECTED_PATH = os.path.join("tests", "testfiles", "roboto_udiff_headonly_expected.txt")
ROBOTO_UDIFF_HEADPOSTONLY_EXPECTED_PATH = os.path.join("tests", "testfiles", "roboto_udiff_headpostonly_expected.txt")
ROBOTO_UDIFF_EXCLUDE_HEADPOST_EXPECTED_PATH = os.path.join("tests", "testfiles", "roboto_udiff_ex_headpost_expected.txt")
ROBOTO_UDIFF_EXPECTED_PATH = os.path.join(
"tests", "testfiles", "roboto_udiff_expected.txt"
)
ROBOTO_UDIFF_COLOR_EXPECTED_PATH = os.path.join(
"tests", "testfiles", "roboto_udiff_color_expected.txt"
)
ROBOTO_UDIFF_1CONTEXT_EXPECTED_PATH = os.path.join(
"tests", "testfiles", "roboto_udiff_1context_expected.txt"
)
ROBOTO_UDIFF_HEADONLY_EXPECTED_PATH = os.path.join(
"tests", "testfiles", "roboto_udiff_headonly_expected.txt"
)
ROBOTO_UDIFF_HEADPOSTONLY_EXPECTED_PATH = os.path.join(
"tests", "testfiles", "roboto_udiff_headpostonly_expected.txt"
)
ROBOTO_UDIFF_EXCLUDE_HEADPOST_EXPECTED_PATH = os.path.join(
"tests", "testfiles", "roboto_udiff_ex_headpost_expected.txt"
)

ROBOTO_BEFORE_URL = "https://github.com/source-foundry/fdiff/raw/master/tests/testfiles/Roboto-Regular.subset1.ttf"
ROBOTO_AFTER_URL = "https://github.com/source-foundry/fdiff/raw/master/tests/testfiles/Roboto-Regular.subset2.ttf"
Expand Down Expand Up @@ -80,21 +92,32 @@ def test_main_filepath_validations_false_secondfont(capsys):
# Mutually exclusive argument tests
#


def test_main_include_exclude_defined_simultaneously(capsys):
args = ["--include", "head", "--exclude", "head", ROBOTO_BEFORE_PATH, ROBOTO_AFTER_PATH]
args = [
"--include",
"head",
"--exclude",
"head",
ROBOTO_BEFORE_PATH,
ROBOTO_AFTER_PATH,
]

with pytest.raises(SystemExit) as exit_info:
run(args)

captured = capsys.readouterr()
assert captured.err.startswith("[*] Error: --include and --exclude are mutually exclusive options")
assert exit_info.value.code == 1
assert captured.err.endswith(
"error: argument --exclude: not allowed with argument --include\n"
)
assert exit_info.value.code == 2


#
# Unified diff integration tests
#


def test_main_run_unified_default_local_files_no_diff(capsys):
"""Test default behavior when there is no difference in font files under evaluation"""
args = [ROBOTO_BEFORE_PATH, ROBOTO_BEFORE_PATH]
Expand Down
33 changes: 28 additions & 5 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,40 @@
import os
import re

from fdiff.utils import get_file_modtime, get_tables_argument_list, file_exists
from fdiff.utils import get_file_modtime, get_tables_argument_list, path_exists

import pytest


def test_file_exists_true():
assert file_exists(os.path.join("tests", "testfiles", "test.txt")) is True
def test_path_exists_default_true():
assert (
path_exists(
os.path.join("tests", "testfiles", "test.txt"), include_dir_paths=False
)
is True
)


def test_file_exists_false():
assert file_exists(os.path.join("tests", "testfiles", "bogus.jpg")) is False
def test_path_exists_default_false():
assert (
path_exists(
os.path.join("tests", "testfiles", "bogus.jpg"), include_dir_paths=False
)
is False
)


def test_path_exists_default_dirpath_fails():
assert (
path_exists(os.path.join("tests", "testfiles"), include_dir_paths=False)
is False
)


def test_path_exists_default_dirpath_toggle_succeeds():
assert (
path_exists(os.path.join("tests", "testfiles"), include_dir_paths=True) is True
)


def test_get_file_modtime():
Expand Down
13 changes: 13 additions & 0 deletions tests/test_utils_unix_only.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import os
import sys

import pytest

from fdiff.utils import path_exists

if sys.platform.startswith("win"):
pytest.skip("skipping Unix only tests", allow_module_level=True)


def test_path_exists_default_dirpath_toggle_succeeds():
assert path_exists("/dev/null", include_dir_paths=True) is True