├── lib ├── __init__.py └── fontv │ ├── __init__.py │ ├── exceptions.py │ ├── utilities.py │ ├── settings.py │ ├── app.py │ ├── libfv.py │ └── commandlines.py ├── tests ├── __init__.py ├── testfiles │ ├── test.txt │ ├── deepdir │ │ ├── test.txt │ │ └── deepdir2 │ │ │ └── deepdir3 │ │ │ └── deepdir4 │ │ │ └── test.txt │ ├── Hack-Regular.ttf │ ├── Test-VersionDEV.otf │ ├── Test-VersionDEV.ttf │ ├── Test-VersionREL.otf │ ├── Test-VersionREL.ttf │ ├── Test-VersionSha.otf │ ├── Test-VersionSha.ttf │ ├── Test-VersionMeta.otf │ ├── Test-VersionMeta.ttf │ ├── Test-VersionOnly.otf │ ├── Test-VersionOnly.ttf │ ├── Test-VersionShaDEV.otf │ ├── Test-VersionShaDEV.ttf │ ├── Test-VersionShaREL.otf │ ├── Test-VersionShaREL.ttf │ ├── Test-VersionDEVMeta.otf │ ├── Test-VersionDEVMeta.ttf │ ├── Test-VersionMoreMeta.otf │ ├── Test-VersionMoreMeta.ttf │ ├── Test-VersionRELMeta.otf │ ├── Test-VersionRELMeta.ttf │ ├── Test-VersionShaMeta.otf │ ├── Test-VersionShaMeta.ttf │ ├── Test-VersionShaDEVMeta.otf │ ├── Test-VersionShaDEVMeta.ttf │ ├── Test-VersionShaRELMeta.otf │ ├── Test-VersionShaRELMeta.ttf │ ├── Test-MismatchVersionNumbers.otf │ └── HACK_LICENSE.md ├── test_main.py ├── lint.sh ├── test_settings.py ├── test_fonttools.py ├── test_utilities.py └── test_libfv.py ├── docs ├── requirements.txt ├── index.rst ├── README.rst ├── Makefile ├── LICENSE └── conf.py ├── codecov.yml ├── MANIFEST.in ├── setup.cfg ├── .github ├── dependabot.yml └── workflows │ ├── publish-release.yml │ ├── py-ci.yml │ └── codeql-analysis.yml ├── coverage.sh ├── requirements.txt ├── tox.ini ├── .readthedocs.yml ├── .gitignore ├── setup.py ├── CONTRIBUTING.md ├── CHANGELOG.md └── README.md /lib/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/testfiles/test.txt: -------------------------------------------------------------------------------- 1 | testing -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx==5.3.0 -------------------------------------------------------------------------------- /tests/testfiles/deepdir/test.txt: -------------------------------------------------------------------------------- 1 | test file -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | codecov: 2 | max_report_age: off 3 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include docs/LICENSE 2 | include docs/README.rst 3 | -------------------------------------------------------------------------------- /tests/testfiles/deepdir/deepdir2/deepdir3/deepdir4/test.txt: -------------------------------------------------------------------------------- 1 | test file -------------------------------------------------------------------------------- /lib/fontv/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | -------------------------------------------------------------------------------- /tests/test_main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import sys 5 | import pytest 6 | -------------------------------------------------------------------------------- /tests/testfiles/Hack-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/source-foundry/font-v/HEAD/tests/testfiles/Hack-Regular.ttf -------------------------------------------------------------------------------- /tests/testfiles/Test-VersionDEV.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/source-foundry/font-v/HEAD/tests/testfiles/Test-VersionDEV.otf -------------------------------------------------------------------------------- /tests/testfiles/Test-VersionDEV.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/source-foundry/font-v/HEAD/tests/testfiles/Test-VersionDEV.ttf -------------------------------------------------------------------------------- /tests/testfiles/Test-VersionREL.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/source-foundry/font-v/HEAD/tests/testfiles/Test-VersionREL.otf -------------------------------------------------------------------------------- /tests/testfiles/Test-VersionREL.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/source-foundry/font-v/HEAD/tests/testfiles/Test-VersionREL.ttf -------------------------------------------------------------------------------- /tests/testfiles/Test-VersionSha.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/source-foundry/font-v/HEAD/tests/testfiles/Test-VersionSha.otf -------------------------------------------------------------------------------- /tests/testfiles/Test-VersionSha.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/source-foundry/font-v/HEAD/tests/testfiles/Test-VersionSha.ttf -------------------------------------------------------------------------------- /tests/testfiles/Test-VersionMeta.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/source-foundry/font-v/HEAD/tests/testfiles/Test-VersionMeta.otf -------------------------------------------------------------------------------- /tests/testfiles/Test-VersionMeta.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/source-foundry/font-v/HEAD/tests/testfiles/Test-VersionMeta.ttf -------------------------------------------------------------------------------- /tests/testfiles/Test-VersionOnly.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/source-foundry/font-v/HEAD/tests/testfiles/Test-VersionOnly.otf -------------------------------------------------------------------------------- /tests/testfiles/Test-VersionOnly.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/source-foundry/font-v/HEAD/tests/testfiles/Test-VersionOnly.ttf -------------------------------------------------------------------------------- /tests/testfiles/Test-VersionShaDEV.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/source-foundry/font-v/HEAD/tests/testfiles/Test-VersionShaDEV.otf -------------------------------------------------------------------------------- /tests/testfiles/Test-VersionShaDEV.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/source-foundry/font-v/HEAD/tests/testfiles/Test-VersionShaDEV.ttf -------------------------------------------------------------------------------- /tests/testfiles/Test-VersionShaREL.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/source-foundry/font-v/HEAD/tests/testfiles/Test-VersionShaREL.otf -------------------------------------------------------------------------------- /tests/testfiles/Test-VersionShaREL.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/source-foundry/font-v/HEAD/tests/testfiles/Test-VersionShaREL.ttf -------------------------------------------------------------------------------- /tests/testfiles/Test-VersionDEVMeta.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/source-foundry/font-v/HEAD/tests/testfiles/Test-VersionDEVMeta.otf -------------------------------------------------------------------------------- /tests/testfiles/Test-VersionDEVMeta.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/source-foundry/font-v/HEAD/tests/testfiles/Test-VersionDEVMeta.ttf -------------------------------------------------------------------------------- /tests/testfiles/Test-VersionMoreMeta.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/source-foundry/font-v/HEAD/tests/testfiles/Test-VersionMoreMeta.otf -------------------------------------------------------------------------------- /tests/testfiles/Test-VersionMoreMeta.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/source-foundry/font-v/HEAD/tests/testfiles/Test-VersionMoreMeta.ttf -------------------------------------------------------------------------------- /tests/testfiles/Test-VersionRELMeta.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/source-foundry/font-v/HEAD/tests/testfiles/Test-VersionRELMeta.otf -------------------------------------------------------------------------------- /tests/testfiles/Test-VersionRELMeta.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/source-foundry/font-v/HEAD/tests/testfiles/Test-VersionRELMeta.ttf -------------------------------------------------------------------------------- /tests/testfiles/Test-VersionShaMeta.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/source-foundry/font-v/HEAD/tests/testfiles/Test-VersionShaMeta.otf -------------------------------------------------------------------------------- /tests/testfiles/Test-VersionShaMeta.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/source-foundry/font-v/HEAD/tests/testfiles/Test-VersionShaMeta.ttf -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal = 0 3 | 4 | [metadata] 5 | license_file = docs/LICENSE 6 | 7 | [flake8] 8 | max-line-length = 90 9 | -------------------------------------------------------------------------------- /tests/testfiles/Test-VersionShaDEVMeta.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/source-foundry/font-v/HEAD/tests/testfiles/Test-VersionShaDEVMeta.otf -------------------------------------------------------------------------------- /tests/testfiles/Test-VersionShaDEVMeta.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/source-foundry/font-v/HEAD/tests/testfiles/Test-VersionShaDEVMeta.ttf -------------------------------------------------------------------------------- /tests/testfiles/Test-VersionShaRELMeta.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/source-foundry/font-v/HEAD/tests/testfiles/Test-VersionShaRELMeta.otf -------------------------------------------------------------------------------- /tests/testfiles/Test-VersionShaRELMeta.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/source-foundry/font-v/HEAD/tests/testfiles/Test-VersionShaRELMeta.ttf -------------------------------------------------------------------------------- /tests/testfiles/Test-MismatchVersionNumbers.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/source-foundry/font-v/HEAD/tests/testfiles/Test-MismatchVersionNumbers.otf -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: pip 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | -------------------------------------------------------------------------------- /tests/lint.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | PREFIX=../lib/fontv 4 | 5 | pylint --disable=line-too-long,fixme "$PREFIX"/app.py "$PREFIX"/libfv.py "$PREFIX"/utilities.py 6 | 7 | -------------------------------------------------------------------------------- /coverage.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | coverage run --source fontv -m py.test 4 | coverage report -m 5 | # coverage html 6 | 7 | #coverage xml 8 | #codecov --token=$CODECOV_{{font-v}} 9 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | 2 | libfv Documentation 3 | =================== 4 | 5 | .. toctree:: 6 | :maxdepth: 2 7 | :caption: Contents: 8 | 9 | 10 | .. autoclass:: fontv.libfv.FontVersion 11 | :members: 12 | :private-members: 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile 3 | # To update, run: 4 | # 5 | # pip-compile 6 | # 7 | fonttools==4.38.0 # via font-v (setup.py) 8 | gitdb==4.0.10 # via gitpython 9 | gitpython==3.1.31 # via font-v (setup.py) 10 | smmap==5.0.0 # via gitdb 11 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py310 3 | 4 | [testenv] 5 | passenv = TOXENV CI TRAVIS TRAVIS_* 6 | commands = 7 | py.test tests 8 | 9 | deps = 10 | -rrequirements.txt 11 | pytest 12 | pytest-mock 13 | mock 14 | coverage 15 | codecov>=1.4.0 16 | 17 | 18 | ;[testenv:cov-report] 19 | ;commands = py.test --cov=ufolint --cov-report=term --cov-report=html -------------------------------------------------------------------------------- /docs/README.rst: -------------------------------------------------------------------------------- 1 | font-v 2 | ======== 3 | 4 | font-v is an open source font version string library (libfv) and executable (font-v) for reading, reporting, modifying, and writing OpenType name table ID 5 records and head table fontRevision records in *.otf and *.ttf fonts. 5 | 6 | font-v is built with Python and can be used on Linux, macOS, and Windows platforms with current versions of the Python 2 and Python 3 interpreters. 7 | 8 | Source and documentation: https://github.com/source-foundry/font-v 9 | 10 | libfv API documentation: https://font-v.readthedocs.io/en/latest/ 11 | 12 | Issue reporting: https://github.com/source-foundry/font-v/issues 13 | 14 | License: MIT 15 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SPHINXPROJ = font-v 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /tests/test_settings.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from fontv.settings import lib_name, major_version, minor_version, patch_version, HELP, VERSION, USAGE 5 | 6 | import pytest 7 | 8 | 9 | def test_settings_lib_name(): 10 | assert lib_name == "font-v" 11 | 12 | 13 | def test_settings_major_version(): 14 | assert len(major_version) > 0 15 | 16 | 17 | def test_settings_minor_version(): 18 | assert len(minor_version) > 0 19 | 20 | 21 | def test_settings_patch_version(): 22 | assert len(patch_version) > 0 23 | 24 | 25 | def test_settings_help_string(): 26 | assert len(HELP) > 0 27 | 28 | 29 | def test_settings_version_string(): 30 | assert len(VERSION) > 0 31 | 32 | 33 | def test_settings_usage_string(): 34 | assert len(USAGE) > 0 35 | 36 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yaml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Set the version of Python and other tools you might need 9 | build: 10 | os: ubuntu-20.04 11 | tools: 12 | python: "3.9" 13 | 14 | # Build documentation in the docs/ directory with Sphinx 15 | sphinx: 16 | configuration: docs/conf.py 17 | builder: html 18 | # If using Sphinx, optionally build your docs in additional formats such as PDF 19 | # formats: 20 | # - pdf 21 | 22 | # Optionally declare the Python requirements required to build your docs 23 | python: 24 | install: 25 | - requirements: docs/requirements.txt 26 | - requirements: requirements.txt 27 | - method: pip 28 | path: . 29 | -------------------------------------------------------------------------------- /tests/test_fonttools.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | 5 | from __future__ import unicode_literals 6 | 7 | import sys 8 | import pytest 9 | 10 | from fontTools.misc.py23 import unicode, tounicode, tobytes, tostr 11 | 12 | 13 | def test_fontv_fonttools_lib_unicode(): 14 | test_string = tobytes("hello") 15 | test_string_str = tostr("hello") 16 | test_string_unicode = tounicode(test_string, 'utf-8') 17 | test_string_str_unicode = tounicode(test_string_str, 'utf-8') 18 | 19 | assert (isinstance(test_string, unicode)) is False 20 | if sys.version_info[0] == 2: 21 | assert (isinstance(test_string_str, unicode)) is False # str != unicode in Python 2 22 | elif sys.version_info[0] == 3: 23 | assert (isinstance(test_string_str, unicode)) is True # str = unicode in Python 3 24 | assert (isinstance(test_string_unicode, unicode)) is True # after cast with fonttools function, Py2+3 = unicode 25 | assert (isinstance(test_string_str_unicode, unicode)) is True # ditto 26 | assert test_string_unicode == "hello" 27 | -------------------------------------------------------------------------------- /docs/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Source Foundry Authors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # PyInstaller 27 | # Usually these files are written by a python script from a template 28 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 29 | *.manifest 30 | *.spec 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .coverage 40 | .coverage.* 41 | .cache 42 | nosetests.xml 43 | coverage.xml 44 | *,cover 45 | .hypothesis/ 46 | 47 | # Translations 48 | *.mo 49 | *.pot 50 | 51 | # Django stuff: 52 | *.log 53 | 54 | # Sphinx documentation 55 | docs/_build/ 56 | 57 | # PyBuilder 58 | target/ 59 | 60 | #Ipython Notebook 61 | .ipynb_checkpoints 62 | 63 | # PyCharm files 64 | .idea/ 65 | 66 | # Project files 67 | tests/runner.py 68 | tests/profiler.py 69 | 70 | # OS X 71 | .DS_Store 72 | 73 | # Test directories/files 74 | .pytest_cache 75 | scratchpad.py 76 | 77 | .venv 78 | .vscode 79 | -------------------------------------------------------------------------------- /lib/fontv/exceptions.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | 5 | class CommandlinesError(Exception): 6 | """Base exception class for all exceptions raised by the commandlines library""" 7 | 8 | def __init__(self, message): 9 | Exception.__init__(self, message) 10 | 11 | 12 | class MissingArgumentError(CommandlinesError): 13 | """Missing argument exception""" 14 | 15 | def __init__(self, argument): 16 | self.error_message = ( 17 | "Missing argument exception: the argument '" + argument + "' was not found." 18 | ) 19 | CommandlinesError.__init__(self, self.error_message) 20 | 21 | 22 | class MissingDictionaryKeyError(CommandlinesError): 23 | """Missing dictionary key exception""" 24 | 25 | def __init__(self, dict_key): 26 | self.error_message = ( 27 | "Missing dictionary key exception: the dictionary key '" 28 | + dict_key 29 | + "' was not found." 30 | ) 31 | CommandlinesError.__init__(self, self.error_message) 32 | 33 | 34 | class IndexOutOfRangeError(CommandlinesError, IndexError): 35 | """Index out of range exception""" 36 | 37 | def __init__(self): 38 | self.error_message = "Index out of range exception. The requested index fell outside of the index range." 39 | IndexError.__init__(self) 40 | CommandlinesError.__init__(self, self.error_message) 41 | -------------------------------------------------------------------------------- /.github/workflows/publish-release.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | # Sequence of patterns matched against refs/tags 4 | tags: 5 | - "v*" # Push events to matching v*, i.e. v1.0, v20.15.10 6 | 7 | name: Create and Publish Release 8 | 9 | jobs: 10 | build: 11 | name: Create and Publish Release 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: 15 | python-version: ["3.10"] 16 | steps: 17 | - uses: actions/checkout@v2 18 | - name: Set up Python ${{ matrix.python-version }} 19 | uses: actions/setup-python@v2 20 | with: 21 | python-version: ${{ matrix.python-version }} 22 | - name: Install dependencies 23 | run: | 24 | python -m pip install --upgrade pip 25 | pip install --upgrade setuptools wheel twine 26 | - name: Create GitHub release 27 | id: create_release 28 | uses: actions/create-release@v1 29 | env: 30 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 31 | with: 32 | tag_name: ${{ github.ref }} 33 | release_name: ${{ github.ref }} 34 | body: | 35 | Please see the root of the repository for the CHANGELOG.md 36 | draft: false 37 | prerelease: false 38 | - name: Build and publish to PyPI 39 | env: 40 | TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} 41 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 42 | run: | 43 | python setup.py sdist bdist_wheel 44 | twine upload dist/* 45 | -------------------------------------------------------------------------------- /.github/workflows/py-ci.yml: -------------------------------------------------------------------------------- 1 | name: Python CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | matrix: 10 | os: [ubuntu-latest, macos-latest, windows-latest] 11 | python-version: ["3.7", "3.8", "3.9", "3.10", "pypy-3.8"] 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: Set up Python ${{ matrix.python-version }} 16 | uses: actions/setup-python@v2 17 | with: 18 | python-version: ${{ matrix.python-version }} 19 | - name: Display Python version & architecture 20 | run: | 21 | python -c "import sys; print(sys.version)" 22 | python -c "import struct; print(struct.calcsize('P') * 8)" 23 | - name: Get pip cache dir 24 | id: pip-cache 25 | run: | 26 | pip install --upgrade pip 27 | echo "::set-output name=dir::$(pip cache dir)" 28 | - name: pip cache 29 | uses: actions/cache@v2 30 | with: 31 | path: ${{ steps.pip-cache.outputs.dir }} 32 | key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} 33 | restore-keys: | 34 | ${{ runner.os }}-pip- 35 | - name: Install Python testing dependencies 36 | run: pip install --upgrade pytest 37 | - name: Install Python project dependencies 38 | uses: py-actions/py-dependency-install@v2 39 | with: 40 | update-pip: "true" 41 | update-setuptools: "true" 42 | update-wheel: "true" 43 | - name: Install project 44 | run: pip install -r requirements.txt . 45 | - name: Python unit tests 46 | run: pytest 47 | -------------------------------------------------------------------------------- /lib/fontv/utilities.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # ==================================================== 5 | # Copyright 2018 Christopher Simpkins 6 | # MIT License 7 | # ==================================================== 8 | 9 | from __future__ import unicode_literals 10 | 11 | import os 12 | 13 | 14 | def dir_exists(dirpath): 15 | """Tests for existence of a directory on the string filepath""" 16 | if os.path.exists(dirpath) and os.path.isdir( 17 | dirpath 18 | ): # test that exists and is a directory 19 | return True 20 | else: 21 | return False 22 | 23 | 24 | def file_exists(filepath): 25 | """Tests for existence of a file on the string filepath""" 26 | if os.path.exists(filepath) and os.path.isfile( 27 | filepath 28 | ): # test that exists and is a file 29 | return True 30 | else: 31 | return False 32 | 33 | 34 | def get_git_root_path(filepath): 35 | """ 36 | Recursively searches for git root path over 5 directory levels above working directory 37 | :param filepath: (string) - path to the font file that is under git version control 38 | :return: validated git root path as string 39 | :raises: IOError if unable to detect the root of the git repository through this path traversal 40 | """ 41 | 42 | # begin by defining directory that contains font as the git root needle 43 | gitroot_path = os.path.dirname(os.path.abspath(filepath)) 44 | 45 | # search up to five directories above for the git repo root 46 | for _ in range(6): 47 | if dir_exists(os.path.join(gitroot_path, ".git")): 48 | return gitroot_path 49 | gitroot_path = os.path.dirname(gitroot_path) 50 | 51 | raise IOError( 52 | "Unable to determine git repository root for font file " + filepath 53 | ) 54 | 55 | 56 | def is_font(filepath): 57 | """ 58 | Tests filepath argument to determine if it has a .ttf or .otf file extension (definition of "font" for this 59 | application) 60 | 61 | :param filepath: (string) file path to a font file for testing 62 | :return: (boolean) True = appears to be a font file path; False = does not appear to be a font file path 63 | """ 64 | if len(filepath) > 4: 65 | if filepath[-4:].lower() == ".ttf" or filepath[-4:].lower() == ".otf": 66 | return True 67 | else: 68 | return False 69 | else: 70 | return False 71 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | # The branches below must be a subset of the branches above 8 | branches: [master] 9 | schedule: 10 | - cron: '0 1 * * 2' 11 | 12 | jobs: 13 | analyze: 14 | name: Analyze 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | fail-fast: false 19 | matrix: 20 | # Override automatic language detection by changing the below list 21 | # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python'] 22 | language: ['python'] 23 | # Learn more... 24 | # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection 25 | 26 | steps: 27 | - name: Checkout repository 28 | uses: actions/checkout@v2 29 | with: 30 | # We must fetch at least the immediate parents so that if this is 31 | # a pull request then we can checkout the head. 32 | fetch-depth: 2 33 | 34 | # If this run was triggered by a pull request event, then checkout 35 | # the head of the pull request instead of the merge commit. 36 | - run: git checkout HEAD^2 37 | if: ${{ github.event_name == 'pull_request' }} 38 | 39 | # Initializes the CodeQL tools for scanning. 40 | - name: Initialize CodeQL 41 | uses: github/codeql-action/init@v1 42 | with: 43 | languages: ${{ matrix.language }} 44 | # If you wish to specify custom queries, you can do so here or in a config file. 45 | # By default, queries listed here will override any specified in a config file. 46 | # Prefix the list here with "+" to use these queries and those in the config file. 47 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 48 | 49 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 50 | # If this step fails, then you should remove it and run the build manually (see below) 51 | - name: Autobuild 52 | uses: github/codeql-action/autobuild@v1 53 | 54 | # ℹ️ Command-line programs to run using the OS shell. 55 | # 📚 https://git.io/JvXDl 56 | 57 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 58 | # and modify them (or add more) to build your code if your project 59 | # uses a compiled language 60 | 61 | #- run: | 62 | # make bootstrap 63 | # make release 64 | 65 | - name: Perform CodeQL Analysis 66 | uses: github/codeql-action/analyze@v1 67 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import io 2 | import os 3 | import re 4 | import sys 5 | from setuptools import setup, find_packages 6 | 7 | 8 | REQUIRES_PYTHON = ">=3.7.0" 9 | 10 | # Use repository Markdown README.md for PyPI long description 11 | try: 12 | with io.open("README.md", encoding="utf-8") as f: 13 | readme = f.read() 14 | except IOError as readme_e: 15 | sys.stderr.write( 16 | "[ERROR] setup.py: Failed to read the README.md file for the long description definition: {}".format( 17 | str(readme_e) 18 | ) 19 | ) 20 | raise readme_e 21 | 22 | 23 | def version_read(): 24 | settings_file = open( 25 | os.path.join(os.path.dirname(__file__), "lib", "fontv", "settings.py") 26 | ).read() 27 | major_regex = r"""major_version\s*?=\s*?["']{1}(\d+)["']{1}""" 28 | minor_regex = r"""minor_version\s*?=\s*?["']{1}(\d+)["']{1}""" 29 | patch_regex = r"""patch_version\s*?=\s*?["']{1}(\d+)["']{1}""" 30 | major_match = re.search(major_regex, settings_file) 31 | minor_match = re.search(minor_regex, settings_file) 32 | patch_match = re.search(patch_regex, settings_file) 33 | major_version = major_match.group(1) 34 | minor_version = minor_match.group(1) 35 | patch_version = patch_match.group(1) 36 | if len(major_version) == 0: 37 | major_version = 0 38 | if len(minor_version) == 0: 39 | minor_version = 0 40 | if len(patch_version) == 0: 41 | patch_version = 0 42 | return major_version + "." + minor_version + "." + patch_version 43 | 44 | 45 | setup( 46 | name="font-v", 47 | version=version_read(), 48 | description="Font version reporting and modification tool", 49 | long_description=readme, 50 | long_description_content_type="text/markdown", 51 | url="https://github.com/source-foundry/font-v", 52 | license="MIT license", 53 | author="Christopher Simpkins", 54 | author_email="chris@sourcefoundry.org", 55 | platforms=["any"], 56 | packages=find_packages("lib"), 57 | package_dir={"": "lib"}, 58 | python_requires=REQUIRES_PYTHON, 59 | install_requires=["gitpython", "fonttools"], 60 | entry_points={ 61 | "console_scripts": ["font-v = fontv.app:main"], 62 | }, 63 | keywords="", 64 | include_package_data=True, 65 | classifiers=[ 66 | "Development Status :: 5 - Production/Stable", 67 | "Natural Language :: English", 68 | "License :: OSI Approved :: MIT License", 69 | "Operating System :: OS Independent", 70 | "Programming Language :: Python", 71 | "Programming Language :: Python :: 3", 72 | "Programming Language :: Python :: 3.7", 73 | "Programming Language :: Python :: 3.8", 74 | "Programming Language :: Python :: 3.9", 75 | "Programming Language :: Python :: 3.10", 76 | ], 77 | ) 78 | -------------------------------------------------------------------------------- /tests/test_utilities.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import os 5 | 6 | import pytest 7 | 8 | from fontv.utilities import file_exists, dir_exists, get_git_root_path, is_font 9 | 10 | 11 | def test_utilities_file_exists_function_passes(): 12 | testfile = "tests/testfiles/Hack-Regular.ttf" 13 | assert file_exists(testfile) is True 14 | 15 | 16 | def test_utilities_file_exists_function_fails(): 17 | testfile = "tests/testfiles/bogus.in" 18 | assert file_exists(testfile) is False 19 | 20 | 21 | def test_utilities_dir_exists_function_passes(): 22 | testdir = "tests/testfiles" 23 | assert dir_exists(testdir) is True 24 | 25 | 26 | def test_utilities_dir_exists_function_fails(): 27 | testdir = "tests/bogus" 28 | assert dir_exists(testdir) is False 29 | 30 | 31 | def test_utilities_get_gitrootpath_function_returns_proper_path_cwd(): 32 | filepath = "CHANGELOG.md" 33 | gitdir_path = get_git_root_path(filepath) 34 | assert os.path.basename(gitdir_path) == "font-v" 35 | assert os.path.isdir(gitdir_path) is True 36 | 37 | 38 | def test_utilities_get_gitrootpath_function_returns_proper_path_one_level_up(): 39 | filepath = "tests/test_utilities.py" 40 | gitdir_path = get_git_root_path(filepath) 41 | assert os.path.basename(gitdir_path) == "font-v" 42 | assert os.path.isdir(gitdir_path) is True 43 | 44 | 45 | def test_utilities_get_gitrootpath_function_returns_proper_path_two_levels_up(): 46 | filepath = "tests/testfiles/Hack-Regular.ttf" 47 | gitdir_path = get_git_root_path(filepath) 48 | assert os.path.basename(gitdir_path) == "font-v" 49 | assert os.path.isdir(gitdir_path) is True 50 | 51 | 52 | def test_utilities_get_gitrootpath_function_returns_proper_path_three_levels_up(): 53 | filepath = "tests/testfiles/deepdir/test.txt" 54 | gitdir_path = get_git_root_path(filepath) 55 | assert os.path.basename(gitdir_path) == "font-v" 56 | assert os.path.isdir(gitdir_path) is True 57 | 58 | 59 | def test_utilities_get_gitrootpath_function_raises_ioerror_six_levels_up(): 60 | with pytest.raises(IOError): 61 | filepath = "tests/testfiles/deepdir/deepdir2/deepdir3/deepdir4/test.txt" 62 | get_git_root_path(filepath) 63 | 64 | 65 | def test_utilities_is_font_ttf(): 66 | assert is_font("Test-Regular.ttf") is True 67 | 68 | 69 | def test_utilities_is_font_otf(): 70 | assert is_font("Test-Regular.otf") is True 71 | 72 | 73 | def test_utilities_is_font_long_ttf(): 74 | assert is_font(os.path.join("tests", "deeper", "Test-Regular.ttf")) is True 75 | 76 | 77 | def test_utilities_is_font_long_otf(): 78 | assert is_font(os.path.join("tests", "deeper", "Test-Regular.otf")) is True 79 | 80 | 81 | def test_utilities_is_font_badpath_no_extension(): 82 | assert is_font("Test-Regular") is False 83 | 84 | 85 | def test_utilities_is_font_badpath_bad_extension(): 86 | assert is_font("Test-Regular.gif") is False 87 | 88 | 89 | def test_utilities_is_font_badpath_too_short(): 90 | assert is_font(".ttf") is False 91 | -------------------------------------------------------------------------------- /lib/fontv/settings.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # ------------------------------------------------------------------------------ 5 | # Library Name 6 | # ------------------------------------------------------------------------------ 7 | lib_name = "font-v" 8 | 9 | # ------------------------------------------------------------------------------ 10 | # Version Number 11 | # ------------------------------------------------------------------------------ 12 | major_version = "2" 13 | minor_version = "1" 14 | patch_version = "0" 15 | 16 | # ------------------------------------------------------------------------------ 17 | # Help String 18 | # ------------------------------------------------------------------------------ 19 | 20 | HELP = """==================================================== 21 | font-v 22 | Copyright 2018 Christopher Simpkins 23 | MIT License 24 | Source: https://github.com/source-foundry/font-v 25 | ==================================================== 26 | 27 | font-v is a font version string reporting and modification tool for ttf and otf fonts. 28 | 29 | USAGE: 30 | 31 | Include a subcommand and desired options in your command line request: 32 | 33 | font-v [subcommand] (options) [font file path 1] ([font file path ...]) 34 | 35 | Subcommands and options: 36 | 37 | report - report OpenType name table ID 5 and head table fontRevision records 38 | --dev - include all name table ID 5 x platformID records in report 39 | 40 | write - write version number to head table fontRevision records and 41 | version string to name table ID 5 records. The following options 42 | can be used to modify the version string write: 43 | head fontRevision + name ID 5 option: 44 | --ver=[version #] - change version number to `version #` definition 45 | name ID 5 options: 46 | --dev - add development status metadata (mutually exclusive with --rel) 47 | --rel - add release status metadata (mutually exclusive with --dev) 48 | --sha1 - add git commit sha1 short hash state metadata 49 | 50 | NOTES: 51 | 52 | The write subcommand --dev and --rel flags are mutually exclusive. Include up to one of these options. 53 | 54 | For platforms that treat the period as a special shell character, an underscore or dash glyph can be used in place of a period to define the version number on the command line with the `--ver=[version #]` option. This means that 2.001 can be defined with any of the following: 55 | 56 | $ font-v write --ver=2.001 57 | $ font-v write --ver=2_001 58 | $ font-v write --ver=2-001 59 | 60 | You can include version number, status, and state options in the same request to make all of these modifications simultaneously. 61 | 62 | The write subcommand modifies all nameID 5 records identified in the OpenType name table of the font (i.e. across all platformID). 63 | 64 | """ 65 | 66 | # ------------------------------------------------------------------------------ 67 | # Version String 68 | # ------------------------------------------------------------------------------ 69 | 70 | VERSION = "font-v v" + major_version + "." + minor_version + "." + patch_version 71 | 72 | 73 | # ------------------------------------------------------------------------------ 74 | # Usage String 75 | # ------------------------------------------------------------------------------ 76 | 77 | USAGE = """ 78 | font-v [subcommand] (options) [font file path 1] ([font file path ...]) 79 | """ 80 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Issue Reports 4 | 5 | Please review the existing issue reports (including open and closed) for a history of the issue that you would like to report in order to confirm that we have not already addressed it. If your issue is new, please [file a new issue report](https://github.com/source-foundry/font-v/issues/new) here on the Github repository. 6 | 7 | ## Source Code Contributions 8 | 9 | Contributions to the source code are highly encouraged and welcomed! We recommend that you open a new issue report to discuss a major source code refactor, implementation of a major new feature, or any other major modification that requires an extended amount of time/effort before you invest the time in the work (assuming that your intent is for this to be merged upstream). 10 | 11 | ### License 12 | 13 | To contribute source code to this project you must be willing to contribute your source changes under the existing [MIT license](https://github.com/source-foundry/font-v/blob/master/docs/LICENSE). If this is not acceptable, please do not submit your changes for review. 14 | 15 | ### Development Installs 16 | 17 | git clone the font-v repository and base your work on the `dev` branch. Install a local development version of the project with the following command (executed in the root of the repository): 18 | 19 | ``` 20 | $ python setup.py develop 21 | ``` 22 | 23 | This will allow you to immediately test source code changes that you make in the Python modules (i.e. without a new install with every change). 24 | 25 | ### Source Code Testing 26 | 27 | The font-v project is tested against current versions of the Python 3.7+ interpreters across Linux, macOS, and Windows platforms. We intend to maintain this breadth of cross platform and Python interpreter release history support as new Python releases become available. Please submit an issue report on the repository to discuss any proposed changes that will narrow the level of Python interpreter or platform support in the project. 28 | 29 | We use [tox](https://tox.readthedocs.io/en/latest/) and [pytest](https://docs.pytest.org/en/latest/) for Python source code testing. You can install these testing packages on your development system with: 30 | 31 | ``` 32 | $ pip install tox 33 | $ pip install pytest 34 | ``` 35 | 36 | To run the `font-v` project tests locally across different Python interpreter versions, install all Python interpreter versions that you intend to use for testing and then use a command like the following from the root of the repository, specifying the target Python interpreter versions: 37 | 38 | ``` 39 | $ tox -e py310 40 | ``` 41 | 42 | See the tox documentation for additional details and further information about available Python interpreter versions. 43 | 44 | Please include new pytest tests (or update existing tests if appropriate) with all source changes! This will greatly accelerate the review process for your changes. 45 | 46 | Cross platform continuous integration testing is performed on all pull requests that are submitted to the project. You may view the results of the tests on your source code changes in the pull request thread. 47 | 48 | ### Propose your changes 49 | 50 | When you are ready to propose your source code changes for upstream review, submit a pull request to the `font-v` repository using the Github UI. Please include sufficient information in the initial post of the pull request to orient the project maintainer to your changes as well as links to any pertinent open issue report threads. 51 | 52 | Please refer to Github documentation for details on the pull request workflow or feel free to contact us to ask for additional information if you have not previously attempted a pull request on Github. We would be glad to help so that this is not a barrier to your contribution! -------------------------------------------------------------------------------- /tests/testfiles/HACK_LICENSE.md: -------------------------------------------------------------------------------- 1 | The work in the Hack project is Copyright 2017 Source Foundry Authors and licensed under the MIT License 2 | 3 | The work in the DejaVu project was committed to the public domain. 4 | 5 | Bitstream Vera Sans Mono Copyright 2003 Bitstream Inc. and licensed under the Bitstream Vera License with Reserved Font Names "Bitstream" and "Vera" 6 | 7 | ### MIT License 8 | 9 | Copyright (c) 2017 Source Foundry Authors 10 | 11 | Permission is hereby granted, free of charge, to any person obtaining a copy 12 | of this software and associated documentation files (the "Software"), to deal 13 | in the Software without restriction, including without limitation the rights 14 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 15 | copies of the Software, and to permit persons to whom the Software is 16 | furnished to do so, subject to the following conditions: 17 | 18 | The above copyright notice and this permission notice shall be included in all 19 | copies or substantial portions of the Software. 20 | 21 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 22 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 23 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 24 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 25 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 26 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 27 | SOFTWARE. 28 | 29 | ### BITSTREAM VERA LICENSE 30 | 31 | Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved. Bitstream Vera is a trademark of Bitstream, Inc. 32 | 33 | Permission is hereby granted, free of charge, to any person obtaining a copy of the fonts accompanying this license ("Fonts") and associated documentation files (the "Font Software"), to reproduce and distribute the Font Software, including without limitation the rights to use, copy, merge, publish, distribute, and/or sell copies of the Font Software, and to permit persons to whom the Font Software is furnished to do so, subject to the following conditions: 34 | 35 | The above copyright and trademark notices and this permission notice shall be included in all copies of one or more of the Font Software typefaces. 36 | 37 | The Font Software may be modified, altered, or added to, and in particular the designs of glyphs or characters in the Fonts may be modified and additional glyphs or characters may be added to the Fonts, only if the fonts are renamed to names not containing either the words "Bitstream" or the word "Vera". 38 | 39 | This License becomes null and void to the extent applicable to Fonts or Font Software that has been modified and is distributed under the "Bitstream Vera" names. 40 | 41 | The Font Software may be sold as part of a larger software package but no copy of one or more of the Font Software typefaces may be sold by itself. 42 | 43 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL BITSTREAM OR THE GNOME FOUNDATION BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE. 44 | 45 | Except as contained in this notice, the names of Gnome, the Gnome Foundation, and Bitstream Inc., shall not be used in advertising or otherwise to promote the sale, use or other dealings in this Font Software without prior written authorization from the Gnome Foundation or Bitstream Inc., respectively. For further information, contact: fonts at gnome dot org. 46 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # font-v documentation build configuration file, created by 4 | # sphinx-quickstart on Tue Nov 28 21:54:45 2017. 5 | # 6 | # This file is execfile()d with the current directory set to its 7 | # containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | # If extensions (or modules to document with autodoc) are in another directory, 16 | # add these directories to sys.path here. If the directory is relative to the 17 | # documentation root, use os.path.abspath to make it absolute, like shown here. 18 | # 19 | # import os 20 | # import sys 21 | # sys.path.insert(0, os.path.abspath('.')) 22 | 23 | 24 | # -- General configuration ------------------------------------------------ 25 | 26 | # If your documentation needs a minimal Sphinx version, state it here. 27 | # 28 | needs_sphinx = "4.2.0" 29 | 30 | # Add any Sphinx extension module names here, as strings. They can be 31 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 32 | # ones. 33 | extensions = [ 34 | "sphinx.ext.autodoc", 35 | "sphinx.ext.intersphinx", 36 | "sphinx.ext.coverage", 37 | "sphinx.ext.imgmath", 38 | "sphinx.ext.viewcode", 39 | ] 40 | 41 | # Add any paths that contain templates here, relative to this directory. 42 | templates_path = ["_templates"] 43 | 44 | # The suffix(es) of source filenames. 45 | # You can specify multiple suffix as a list of string: 46 | # 47 | # source_suffix = ['.rst', '.md'] 48 | source_suffix = ".rst" 49 | 50 | # The master toctree document. 51 | master_doc = "index" 52 | 53 | # General information about the project. 54 | project = u"libfv" 55 | copyright = u"2018, Christopher Simpkins. CC BY 4.0" 56 | author = u"Christopher Simpkins" 57 | 58 | # The version info for the project you're documenting, acts as replacement for 59 | # |version| and |release|, also used in various other places throughout the 60 | # built documents. 61 | # 62 | # The short X.Y version. 63 | version = u"v2.0.0" 64 | # The full version, including alpha/beta/rc tags. 65 | release = u"v2.0.0" 66 | 67 | # The language for content autogenerated by Sphinx. Refer to documentation 68 | # for a list of supported languages. 69 | # 70 | # This is also used if you do content translation via gettext catalogs. 71 | # Usually you set "language" from the command line for these cases. 72 | language = None 73 | 74 | # List of patterns, relative to source directory, that match files and 75 | # directories to ignore when looking for source files. 76 | # This patterns also effect to html_static_path and html_extra_path 77 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] 78 | 79 | # The name of the Pygments (syntax highlighting) style to use. 80 | pygments_style = "sphinx" 81 | 82 | # If true, `todo` and `todoList` produce output, else they produce nothing. 83 | todo_include_todos = False 84 | 85 | 86 | autodoc_member_order = "bysource" 87 | 88 | # -- Options for HTML output ---------------------------------------------- 89 | 90 | # The theme to use for HTML and HTML Help pages. See the documentation for 91 | # a list of builtin themes. 92 | # 93 | html_theme = "sphinx_rtd_theme" 94 | 95 | # Theme options are theme-specific and customize the look and feel of a theme 96 | # further. For a list of options available for each theme, see the 97 | # documentation. 98 | # 99 | # html_theme_options = {} 100 | 101 | # Add any paths that contain custom static files (such as style sheets) here, 102 | # relative to this directory. They are copied after the builtin static files, 103 | # so a file named "default.css" will overwrite the builtin "default.css". 104 | html_static_path = ["_static"] 105 | 106 | # Custom sidebar templates, must be a dictionary that maps document names 107 | # to template names. 108 | # 109 | # This is required for the alabaster theme 110 | # refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars 111 | html_sidebars = { 112 | "**": [ 113 | "relations.html", # needs 'show_related': True theme option to display 114 | "searchbox.html", 115 | ] 116 | } 117 | 118 | 119 | # -- Options for HTMLHelp output ------------------------------------------ 120 | 121 | # Output file base name for HTML help builder. 122 | htmlhelp_basename = "libfvdoc" 123 | 124 | 125 | # -- Options for LaTeX output --------------------------------------------- 126 | 127 | latex_elements = { 128 | # The paper size ('letterpaper' or 'a4paper'). 129 | # 130 | # 'papersize': 'letterpaper', 131 | # The font size ('10pt', '11pt' or '12pt'). 132 | # 133 | # 'pointsize': '10pt', 134 | # Additional stuff for the LaTeX preamble. 135 | # 136 | # 'preamble': '', 137 | # Latex figure (float) alignment 138 | # 139 | # 'figure_align': 'htbp', 140 | } 141 | 142 | # Grouping the document tree into LaTeX files. List of tuples 143 | # (source start file, target name, title, 144 | # author, documentclass [howto, manual, or own class]). 145 | latex_documents = [ 146 | (master_doc, "libfv.tex", u"libfv Documentation", u"Christopher Simpkins", "manual"), 147 | ] 148 | 149 | 150 | # -- Options for manual page output --------------------------------------- 151 | 152 | # One entry per manual page. List of tuples 153 | # (source start file, name, description, authors, manual section). 154 | man_pages = [(master_doc, "libfv", u"libfv Documentation", [author], 1)] 155 | 156 | 157 | # -- Options for Texinfo output ------------------------------------------- 158 | 159 | # Grouping the document tree into Texinfo files. List of tuples 160 | # (source start file, target name, title, author, 161 | # dir menu entry, description, category) 162 | texinfo_documents = [ 163 | ( 164 | master_doc, 165 | "libfv", 166 | u"libfv Documentation", 167 | author, 168 | "font-v", 169 | "One line description of project.", 170 | "Miscellaneous", 171 | ), 172 | ] 173 | 174 | 175 | # -- Options for Epub output ---------------------------------------------- 176 | 177 | # Bibliographic Dublin Core info. 178 | epub_title = project 179 | epub_author = author 180 | epub_publisher = author 181 | epub_copyright = copyright 182 | 183 | # The unique identifier of the text. This can be a ISBN number 184 | # or the project homepage. 185 | # 186 | # epub_identifier = '' 187 | 188 | # A unique identification for the text. 189 | # 190 | # epub_uid = '' 191 | 192 | # A list of files that should not be packed into the epub file. 193 | epub_exclude_files = ["search.html"] 194 | 195 | 196 | # Example configuration for intersphinx: refer to the Python standard library. 197 | intersphinx_mapping = {"https://docs.python.org/": None} 198 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Changelog 2 | 3 | ## v2.1.0 4 | 5 | - `get_git_root_path` now searches up to five directory levels for the root .git directory path before failing (broadens suppport for more deeply nested font paths) 6 | - update fonttools dependency to v4.28.2 7 | 8 | ## v2.0.0 9 | 10 | - drop support for Python interpreters < 3.7 (dropped by our fonttools dependency) 11 | - drop Py 3.6 CI testing 12 | - add Py 3.10 CI testing 13 | - transition GitHub Actions workflows to Python 3.10 interpreter default 14 | - use Py 3.10 in tox.ini config 15 | - remove OpenFV spec references from source and repository documentation 16 | - udpate fonttools dependency to v4.28.1 17 | - update gitdb dependency to v4.0.9 18 | - update gitpython dependency to v3.1.24 19 | - update smmap dependency to v5.0.0 20 | 21 | ### v1.0.5 22 | 23 | - add Python 3.9 classifier to `setup.py` 24 | - minor `setup.py` source formatting updates 25 | - update fonttools dependency to v4.17.0 26 | - update gitpython dependency to v3.1.11 27 | 28 | ### v1.0.4 29 | 30 | - add cPython 3.9 interpreter testing 31 | - add CodeQL testing 32 | - update fonttools dependency to v4.16.1 33 | - update gitpython dependency to v3.1.10 34 | 35 | ### v1.0.3 36 | 37 | - update fonttools dependency to v4.14.0 38 | - update gitdb dependency to v4.0.5 39 | - update gitpython dependency to v3.1.8 40 | - update smmap dependency to v3.0.4 41 | - transition CI testing to the GitHub Actions service 42 | 43 | ### v1.0.2 44 | 45 | - add Production/Stable classifier to `setup.py` 46 | - remove Python < 3.6 classifiers from `setup.py` 47 | - add Python 3.7, Python 3.8 classifiers to `setup.py` 48 | 49 | ### v1.0.1 50 | 51 | - remove Py2 wheel builds 52 | - add requirements.txt defined build dependency installs in CI testing 53 | 54 | ### v1.0.0 55 | 56 | - remove Py2.7 support 57 | - remove Py3.5 and below support 58 | - update project Python dependencies 59 | - fix: CI testing configuration and unit tests, including those that were Py2.7 dependent tests 60 | 61 | ### v0.7.1 62 | 63 | - added license to Python wheel distributions 64 | - updated fontTools dependency to v3.28.0 65 | 66 | ### v0.7.0 67 | 68 | - removed timestamp recalculations on version string modification file writes 69 | - removed libfv method `FontVersion.get_version_string` (deprecated with warning since v0.6.0) 70 | - updated fontTools dependency to v3.27.0 71 | - updated gitpython dependency to v2.1.10 72 | 73 | ### v0.6.5 74 | 75 | - updated fontTools dependency to v3.25.0 76 | - updated gitpython dependency to v2.1.9 77 | 78 | ### v0.6.4 79 | 80 | - updated fontTools dependency to v3.24.1 81 | 82 | ### v0.6.3 83 | 84 | - updated fontTools dependency to v3.23.0 - includes library bugfix 85 | 86 | ### v0.6.2 87 | 88 | - updated fontTools dependency to v3.22.0 89 | 90 | ### v0.6.1 91 | 92 | - added pin for fontTools dependency at version 3.21.2 93 | - added pin for gitpython dependency at version 2.1.8 94 | - updated PyPI documentation 95 | 96 | ### v0.6.0 97 | 98 | font-v executable changes: 99 | 100 | - added head table fontRevision record reporting to report subcommand output (default) 101 | - added head table fontRevision record write support to write subcommand (default) 102 | - refactored from deprecated libfv.FontVersion.get_version_string to new libfv.FontVersion.get_name_id5_version_string method 103 | - updated in-application help documentation 104 | 105 | libfv changes: 106 | 107 | - added support for head.fontRevision read/writes 108 | - added new public FontVersion class attribute head_fontRevision 109 | - added new public FontVersion method get_head_fontrevision_version_number 110 | - added new public FontVersion method get_version_number_string 111 | - add new public FontVersion method get_name_id5_version_string (to replace get_version_string) 112 | - deprecated FontVersion method get_version_string (warnings added as of this release) 113 | - updated public FontVersion method set_version_number with head.fontRevision record write support 114 | - updated public FontVersion method set_version_string with head.fontRevision record write support 115 | - updated public FontVersion method write_version_string with head.fontRevision record write support 116 | - refactor nameID 5 class attribute dictionary name 117 | 118 | ### v0.5.0 119 | 120 | font-v executable changes: 121 | 122 | - added full support for OpenFV font versioning specification (including version number substring, state metadata substring, status metadata substring, other metadata substring(s)) 123 | - refactored entire `write` subcommand implementation to the libfv library 124 | - changed invalid ttf/otf file error to std error stream from std output stream 125 | - fixed incorrect option argument string displayed in the error message for `write` with undefined `--ver=` argument 126 | 127 | libfv changes: 128 | 129 | - modified the formatting of git commit SHA1 hash string state writes to `[sha1]` from `sha1` to support OpenFV specification 130 | - added FontVersion object attribute parsing after git commit sha1 hash writes to in memory version strings 131 | - refactored development/release status substring truth testing method approach to eliminate matches against strings that fall outside of spec 132 | - refactored FontVersion.get_status_substring method to FontVersion.get_state_status_substring with new implementation 133 | - refactored FontVersion.\_set_status_substring to FontVersion.\_set_state_status_substring with new implementation 134 | - eliminated FontVersion.status object attributed (unncessary) 135 | - revised version strings in test fonts to support OpenFV specification 136 | - modified all supporting tests for above changes 137 | 138 | ### v0.4.1 139 | 140 | - Added `__str__` method to libfv.FontVersion class for informative human readable data on prints 141 | - Added `is_font` function to utilities module 142 | - Refactored `font-v report` subcommand on the new libfv library 143 | - Removed encoding from the `font-v report --dev` report 144 | 145 | ### v0.4.0 146 | 147 | - new: `libfv` library that exposes public FontVersion class for work with font version strings 148 | - bugfix: `font-v` git commit SHA1 parsing error on Windows platform 149 | - changed: refactored commandlines library to this project (from external dependency) 150 | 151 | ### v0.3.3 152 | 153 | - added modified version string notification to standard output stream on new version writes (#13) 154 | 155 | ### v0.3.2 156 | 157 | - bug fix for DEV/RELEASE version substring duplication when there are two version substrings (#7) 158 | 159 | ### v0.3.1 160 | 161 | - bug fix for incorrect git sha1 string encoding in the version string (issue #12) 162 | 163 | ### v0.3.0 164 | 165 | - added stdout reporting of name record encoding with new --dev flag for report command 166 | - added new git sha1 string length approach to address collisions (issue #2) 167 | - fixed duplicated dev/release strings 168 | - added new command line subcommand error handling 169 | 170 | ### v0.2.0 171 | 172 | - initial release 173 | -------------------------------------------------------------------------------- /lib/fontv/app.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # font-v────────────────────────────────────────────────────────┐ 5 | # │ │ 6 | # │ A font version string reporting and modification tool │ 7 | # │ │ 8 | # │ Copyright 2018 Christopher Simpkins │ 9 | # │ MIT License │ 10 | # │ │ 11 | # │ Source: https://github.com/source-foundry/font-v │ 12 | # │ │ 13 | # └─────────────────────────────────────────────────────────────┘ 14 | 15 | from __future__ import unicode_literals 16 | 17 | import os 18 | import sys 19 | 20 | from fontv import settings 21 | from fontv.commandlines import Command 22 | from fontv.libfv import FontVersion 23 | from fontv.utilities import file_exists, is_font 24 | 25 | 26 | def main(): 27 | c = Command() 28 | 29 | if c.does_not_validate_missing_args(): 30 | sys.stderr.write( 31 | "[font-v] ERROR: Please include a subcommand and appropriate optional arguments in " 32 | "your command." + os.linesep 33 | ) 34 | sys.exit(1) 35 | 36 | if c.is_help_request(): 37 | print(settings.HELP) 38 | sys.exit(0) 39 | elif c.is_version_request(): 40 | print(settings.VERSION) 41 | sys.exit(0) 42 | elif c.is_usage_request(): 43 | print(settings.USAGE) 44 | sys.exit(0) 45 | 46 | if c.subcmd == "report": 47 | # argument test 48 | if c.argc < 2: 49 | sys.stderr.write( 50 | "[font-v] ERROR: Command is missing necessary arguments. Check " 51 | "`font-v --help`." + os.linesep 52 | ) 53 | sys.exit(1) 54 | 55 | for arg in c.argv[1:]: 56 | if is_font(arg): 57 | font_path = arg 58 | if file_exists(font_path): 59 | fv = FontVersion(font_path) 60 | print(os.linesep + fv.fontpath + ":") 61 | print("----- name.ID = 5:") 62 | # --dev switch report prints every version string in name records 63 | if "--dev" in c.argv: 64 | for record, v_string in fv.name_ID5_dict.items(): 65 | devstring = str(record) + ":" + os.linesep + str(v_string) 66 | print(devstring) 67 | else: # default report handling 68 | print(fv.get_name_id5_version_string()) 69 | print("----- head.fontRevision:") 70 | head_fontrevision = fv.get_head_fontrevision_version_number() 71 | print("{:.3f}".format(head_fontrevision)) 72 | else: 73 | sys.stderr.write( 74 | "[font-v] ERROR: " 75 | + font_path 76 | + " does not appear to be a valid ttf " 77 | "or otf font file path." + os.linesep 78 | ) 79 | sys.exit(1) 80 | elif c.subcmd == "write": 81 | # argument test 82 | if c.argc < 2: 83 | sys.stderr.write( 84 | "[font-v] ERROR: Command is missing necessary arguments. " 85 | "Check `font-v --help`." + os.linesep 86 | ) 87 | sys.exit(1) 88 | 89 | # argument parsing flags 90 | add_sha1 = False 91 | add_release_string = False 92 | add_dev_string = False 93 | add_new_version = False 94 | fontpath_list = [] # list of font paths that user submits on command line 95 | 96 | # test for mutually exclusive arguments 97 | # do not refactor this below the level of the argument tests that follow 98 | if "--rel" in c.argv and "--dev" in c.argv: 99 | sys.stderr.write( 100 | "[font-v] ERROR: Please use either --dev or --rel argument, not both." 101 | + os.linesep 102 | ) 103 | sys.exit(1) 104 | 105 | # Parse command line arguments to determine user request(s) 106 | for arg in c.argv[1:]: 107 | if arg == "--sha1": 108 | add_sha1 = True 109 | elif arg == "--rel": 110 | add_release_string = True 111 | elif arg == "--dev": 112 | add_dev_string = True 113 | elif arg[0:6] == "--ver=": 114 | add_new_version = True 115 | # split on the = symbol and use second part as definition 116 | version_list = arg.split("=") 117 | if len(version_list) < 2: 118 | sys.stderr.write( 119 | "[font-v] ERROR: --ver=version argument does not have a valid definition" 120 | " in your command." + os.linesep 121 | ) 122 | sys.exit(1) 123 | version_pre = version_list[1] 124 | version_pre = version_pre.replace( 125 | "-", "." 126 | ) # specified on command line as 1-000 127 | version_final = version_pre.replace("_", ".") # or as 1_000 128 | elif len(arg) > 4 and ( 129 | arg[-4:].lower() == ".ttf" or arg[-4:].lower() == ".otf" 130 | ): 131 | if file_exists(arg): 132 | fontpath_list.append(arg) 133 | else: 134 | sys.stderr.write( 135 | "[font-v] ERROR: " + arg + " does not appear to be a valid " 136 | "font file path." + os.linesep 137 | ) 138 | sys.exit(1) 139 | 140 | if ( 141 | add_sha1 is False 142 | and add_release_string is False 143 | and add_dev_string is False 144 | and add_new_version is False 145 | ): 146 | print("[font-v] No changes specified. Nothing to do.") 147 | sys.exit(0) 148 | 149 | for fontpath in fontpath_list: 150 | fv = FontVersion(fontpath) 151 | 152 | # define a new version number substring 153 | if add_new_version is True: 154 | fv.set_version_number(version_final) 155 | 156 | # define new state +/- status metadata substring 157 | if add_sha1 is True: 158 | if add_dev_string is True: 159 | fv.set_state_git_commit_sha1(development=True) 160 | elif add_release_string is True: 161 | fv.set_state_git_commit_sha1(release=True) 162 | else: 163 | fv.set_state_git_commit_sha1() 164 | else: 165 | # define new status metadata substring only 166 | if add_dev_string is True: 167 | fv.set_development_status() 168 | elif add_release_string is True: 169 | fv.set_release_status() 170 | 171 | fv.write_version_string() 172 | 173 | print( 174 | "[✓] " + fontpath + " version string was successfully changed " 175 | "to:" + os.linesep + fv.get_name_id5_version_string() + os.linesep 176 | ) 177 | else: # user did not enter an acceptable subcommand 178 | sys.stderr.write( 179 | "[font-v] ERROR: Please enter a font-v subcommand with your request." 180 | + os.linesep 181 | ) 182 | sys.exit(1) 183 | 184 | 185 | if __name__ == "__main__": 186 | main() 187 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](https://github.com/source-foundry/font-v/raw/images/images/font-v-crunch.png) 2 | 3 | [![PyPI](https://img.shields.io/pypi/v/font-v.svg)](https://pypi.org/project/font-v) 4 | ![Python CI](https://github.com/source-foundry/font-v/workflows/Python%20CI/badge.svg) 5 | [![Codacy Badge](https://api.codacy.com/project/badge/Grade/09e28ad7bc31400a806704ac1d2da70c)](https://app.codacy.com/app/SourceFoundry/font-v) 6 | 7 | ## About 8 | 9 | font-v is an open source font version string library (`libfv`) and executable (`font-v`) for reading, reporting, modifying, and writing OpenType name table ID 5 records and head table fontRevision records in `*.otf` and `*.ttf` fonts. 10 | 11 | font-v is built with Python and can be used on Linux, macOS, and Windows platforms with current versions of the Python 2 and Python 3 interpreters. 12 | 13 | ## Contents 14 | 15 | - [Installation](#installation) 16 | - [font-v Executable Usage](#font-v-executable-usage) 17 | - [libfv Library Usage](https://github.com/source-foundry/font-v/tree/dev#libfv-usage) 18 | - [libfv Library API Documentation](http://font-v.readthedocs.io) 19 | - [Contributing to font-v](#contributing-to-font-v) 20 | - [License](#license) 21 | 22 | ## Installation 23 | 24 | The `libfv` library and the `font-v` executable are installed simultaneously with the following installation instructions. 25 | 26 | Installation with the [pip package manager](https://pip.pypa.io/en/stable/) is the recommended approach. 27 | 28 | ### Install with pip 29 | 30 | Install with pip using the following command: 31 | 32 | ``` 33 | $ pip install font-v 34 | ``` 35 | 36 | ### Upgrade with pip 37 | 38 | Upgrade to a new version of font-v with the following command: 39 | 40 | ``` 41 | $ pip install --upgrade font-v 42 | ``` 43 | 44 | ## font-v Executable Usage 45 | 46 | font-v is executed with a set of subcommands and options that define your command line request. 47 | 48 | ``` 49 | $ font-v [subcommand] (options) [font path 1] ([font path ...]) 50 | ``` 51 | 52 | ### Available subcommands and options 53 | 54 | #### Subcommands 55 | 56 | #### `report` 57 | 58 | Report OpenType name table ID 5 and head table fontRevision records 59 | 60 | **_Option_**: 61 | 62 | - `--dev` - include all name table ID 5 x platformID records in report 63 | 64 | #### `write` 65 | 66 | Write version number to head table fontRevision records and version string to name table ID 5 records. 67 | 68 | **_Options_**: 69 | 70 | The following option is used with `write` to modify the version number in both the head fontRevision record and the name ID 5 record(s): 71 | 72 | - `--ver=[version #]` - modify current version number with a new version number using `1.000`, `1_000` or `1-000` syntax on the command line (the latter two formats are provided to support definitions in shells where the period is a special shell character) 73 | 74 | The following options can be used with `write` to modify the version string in name ID 5: 75 | 76 | - `--dev` - add development status metadata to the version string (mutually exclusive with `--rel`) 77 | - `--rel` - add release status metadata to the version string (mutually exclusive with `--dev`) 78 | - `--sha1` - add git commit sha1 short hash state metadata to the version string (requires source under git version control) 79 | 80 | ### Examples 81 | 82 | ### Version string reporting with `report` 83 | 84 | Enter the following to display the head fontRevision version number and name ID 5 font version string for the font Example-Regular.ttf: 85 | 86 | ``` 87 | $ font-v report Example-Regular.ttf 88 | ``` 89 | 90 | Include the `--dev` flag to include the version string (nameID 5) contained in all platformID records: 91 | 92 | ``` 93 | $ font-v report --dev Example-Regular.ttf 94 | ``` 95 | 96 | ### Version number modification with `write` 97 | 98 | The name ID 5 record(s) and head fontRevision record are modified when `--ver=` is used in your command. 99 | 100 | Enter the desired version number in `MAJOR.MINOR` format after the `--ver=` flag. Support is provided for the intended period glyph to be replaced in the command with an underscore `_` or dash `-` for users on platforms where the period is a special shell character. 101 | 102 | All of the following result in modification of the version number to `2.020`: 103 | 104 | ``` 105 | $ font-v write --ver=2.020 Example-Regular.ttf 106 | ``` 107 | 108 | ``` 109 | $ font-v write --ver=2_020 Example-Regular.ttf 110 | ``` 111 | 112 | ``` 113 | $ font-v write --ver=2-020 Example-Regular.ttf 114 | ``` 115 | 116 | This request can be combined with other options to include state and status metadata simultaneously. 117 | 118 | ### git SHA1 commit short hash state metadata with `write` 119 | 120 | If your typeface source is under git version control, you can stamp the name ID 5 version string with a short SHA1 hash digest (generally n=7-8 characters, a number that is determined in order to confirm that it represents a unique value for the repository commit) that represents the git commit at the HEAD of the active git branch. The git commit SHA1 hash digest is defined by the `git rev-list` command at the HEAD of your active repository branch and will match the initial n characters of the git commit SHA1 hash digest that is displayed when you review your `git log` (or review the commit hashes in the UI of git repository hosting platforms like Github). This is intended to maintain metadata in the font binary about source code state at build time. 121 | 122 | Use the `--sha1` option with the `write` subcommand like this: 123 | 124 | ``` 125 | $ font-v write --sha1 Example-Regular.ttf 126 | ``` 127 | 128 | The short SHA1 hash digest is added with the following version string formatting: 129 | 130 | ``` 131 | Version 1.000;[cf8dc25] 132 | ``` 133 | 134 | This can be combined with other options (e.g. to modify the version number +/- add development or release status metadata) in the same command. Other metadata are maintained and appended to the revised version string in a semicolon delimited format with this modification. 135 | 136 | This option does not modify the head fontRevision record. 137 | 138 | ### Add development / release status metadata with `write` 139 | 140 | You can modify the name ID 5 version string to indicate that a build is intended as a development build or release build with the `--dev` or `--rel` flag. These are mutually exclusive options. Include only one in your command. 141 | 142 | To add development status metadata, use a command like this: 143 | 144 | ``` 145 | $ font-v write --dev Example-Regular.ttf 146 | ``` 147 | 148 | and the version string is modified to the following format: 149 | 150 | ``` 151 | Version 1.000;DEV 152 | ``` 153 | 154 | To add release status metadata, use a command like this: 155 | 156 | ``` 157 | $ font-v write --rel Example-Regular.ttf 158 | ``` 159 | 160 | and the version string is modified with the following format: 161 | 162 | ``` 163 | Version 1.000;RELEASE 164 | ``` 165 | 166 | Include the `--sha1` flag with either the `--dev` or `--rel` flag in the command to include both status and state metadata to the version string: 167 | 168 | ``` 169 | $ font-v write --sha1 --dev Example-Regular.ttf 170 | $ font-v report Example-Regular.ttf 171 | 172 | Example-Regular.ttf: 173 | ----- name.ID = 5: 174 | Version 1.000;[cf8dc25]-dev 175 | ----- head.fontRevision: 176 | 1.000 177 | ``` 178 | 179 | or 180 | 181 | ``` 182 | $ git write --sha1 --rel Example-Regular.ttf 183 | $ git report Example-Regular.ttf 184 | 185 | Example-Regular.ttf: 186 | ----- name.ID = 5: 187 | Version 1.000;[cf8dc25]-release 188 | ----- head.fontRevision: 189 | 1.000 190 | ``` 191 | 192 | Any data that followed the original version number substring are maintained and appended after the status metadata in a semicolon delimited format. 193 | 194 | These options do not modify the head fontRevision record. 195 | 196 | ## libfv Usage 197 | 198 | The libfv Python library exposes the `FontVersion` object along with an associated set of attributes and public methods for reads, modifications, and writes of the OpenType head fontRevision record version number and the name ID 5 record(s) version string. The `font-v` executable is built on the public methods available in this library. 199 | 200 | Full documentation of the libfv API is available at http://font-v.readthedocs.io/ 201 | 202 | ### Import `libfv` Library into Your Project 203 | 204 | To use the libfv library, install the font-v project with the instructions above and import the `FontVersion` class into your Python script with the following: 205 | 206 | ```python 207 | from fontv.libfv import FontVersion 208 | ``` 209 | 210 | ### Create an Instance of the `FontVersion` Class 211 | 212 | Next, create an instance of the `FontVersion` class with one of the following approaches: 213 | 214 | ```python 215 | # Instantiate with a file path to the .ttf or .otf font 216 | fv = FontVersion("path/to/font") 217 | ``` 218 | 219 | or 220 | 221 | ```python 222 | # Instantiate with a fontTools TTFont object 223 | # See the fonttools documentation for details (https://github.com/fonttools/fonttools) 224 | fv = FontVersion(fontToolsTTFont) 225 | ``` 226 | 227 | The libfv library will automate parsing of the version string to a set of public `FontVersion` class attributes and expose public methods that you can use to examine and modify the version string. Modified version strings can then be written back out to the font file or to a new font at a different file path. 228 | 229 | Note that all modifications to the version string are made in memory. File writes with these modified data occur when the calling code explicitly calls the write method `FontVersion.write_version_string()` (details are available below). 230 | 231 | ### What You Can Do with the `FontVersion` Object 232 | 233 | #### Read/write version string 234 | 235 | You can examine the full name ID 5 version string and the head fontRevision version number in memory (including after modifications that you make with calling code) with the following: 236 | 237 | ##### Get name ID 5 version string (including associated metadata) 238 | 239 | ```python 240 | fv = FontVersion("path/to/font") 241 | vs = fv.get_name_id5_version_string() 242 | ``` 243 | 244 | ##### Get head fontRevision version number 245 | 246 | ```python 247 | fv = FontVersion("path/to/font") 248 | vs = fv.get_head_fontrevision_version_number() 249 | ``` 250 | 251 | All version modifications with the public methods are made in memory. When you are ready to write them out to a font file, call the following method: 252 | 253 | ##### Write version string modifications to font file 254 | 255 | ```python 256 | fv = FontVersion("path/to/font") 257 | # do things to version string 258 | fv.write_version_string() # writes to file used to instantiate FontVersion object 259 | fv.write_version_string(fontpath="path/to/differentfont") # writes to a different file path 260 | ``` 261 | 262 | `FontVersion.write_version_string()` provides an optional parameter `fontpath=` that can be used to define a different file path than that which was used to instantiate the `FontVersion` object. 263 | 264 | #### Compare Version Strings 265 | 266 | ##### Test version equality / inequality 267 | 268 | Compare name table ID 5 record equality between two fonts: 269 | 270 | ```python 271 | fv1 = FontVersion("path/to/font1") 272 | fv2 = FontVersion("path/to/font2") 273 | 274 | print(fv1 == fv2) 275 | print(fv1 != fv2) 276 | ``` 277 | 278 | #### Modify Version String 279 | 280 | Some common font version string modification tasks that are supported by the `libfv` library include the following: 281 | 282 | ##### Set version number 283 | 284 | Set the version number in the name ID 5 and head fontRevision records: 285 | 286 | ```python 287 | fv = FontVersion("path/to/font") 288 | fv.set_version_number("1.001") 289 | ``` 290 | 291 | ##### Set entire version string with associated metadata 292 | 293 | Set the full version string in the name ID 5 record. The version number is parsed and used to define the head fontRevision record. 294 | 295 | ```python 296 | fv = FontVersion("path/to/font") 297 | fv.set_version_string("Version 2.015; my metadata; more metadata") 298 | ``` 299 | 300 | ##### Work with major/minor version number integers 301 | 302 | ```python 303 | fv = FontVersion("path/to/font") 304 | # version number = "Version 1.234" 305 | vno = fv.get_version_number_tuple() 306 | print(vno) 307 | >>> (1, 2, 3, 4) 308 | fv2 = FontVersion("path/to/font2") 309 | # version number = "Version 10.234" 310 | vno2 = fv2.get_version_number_tuple() 311 | print(vno2) 312 | >>> (10, 2, 3, 4) 313 | ``` 314 | 315 | ##### Eliminate all metadata from a version string 316 | 317 | Remove all metadata from the version string: 318 | 319 | ```python 320 | fv = FontVersion("path/to/font") 321 | # pre modification version string = "Version 1.000; some metadata; other metadata" 322 | fv.clear_metadata() 323 | # post modification version string = "Version 1.000" 324 | ``` 325 | 326 | ##### Set development/release status metadata of the font build 327 | 328 | Add a development/release status substring to the name ID 5 record: 329 | 330 | ```python 331 | fv = FontVersion("path/to/font") 332 | # Label as development build 333 | fv.set_development_status() 334 | # --> adds `DEV` status metadata to version string 335 | 336 | # Label as release build 337 | fv.set_release_status() 338 | # --> adds `RELEASE` status metadata to version string 339 | ``` 340 | 341 | ##### Set git commit SHA1 hash state metadata to maintain documentation of build time source state 342 | 343 | Add source code state metadata to the name ID 5 record: 344 | 345 | ```python 346 | fv = FontVersion("path/to/font") 347 | 348 | # Set git commit SHA1 only 349 | fv.set_state_git_commit_sha1() 350 | # --> adds "[sha1 hash]" state metadata to build 351 | 352 | # Set git commit SHA1 with development status indicator 353 | fv.set_state_git_commit_sha1(development=True) 354 | # --> adds "[sha1 hash]-dev" state metadata to build 355 | 356 | # Set git commit SHA1 with release status indicator 357 | fv.set_state_git_commit_sha1(release=True) 358 | # --> adds "[sha1 hash]-release" state metadata to build 359 | ``` 360 | 361 | ### libfv API 362 | 363 | Full documentation of the `libfv` API is available at http://font-v.readthedocs.io/ 364 | 365 | ## Contributing to font-v 366 | 367 | Source contributions to the libfv library and font-v executable are encouraged and welcomed! Please see the [CONTRIBUTING.md](CONTRIBUTING.md) documentation for details. 368 | 369 | ## Acknowledgments 370 | 371 | Built with the fantastic [fonttools](https://github.com/fonttools/fonttools) and [GitPython](https://github.com/gitpython-developers/GitPython) Python libraries. 372 | 373 | ## License 374 | 375 | [MIT License](https://github.com/source-foundry/font-v/blob/master/docs/LICENSE) 376 | -------------------------------------------------------------------------------- /lib/fontv/libfv.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # libfv.py────────────────────────────────────────────────────────────────┐ 5 | # │ │ 6 | # │ A Python library module that supports read/modification/write of .otf │ 7 | # │ and .ttf font version strings │ 8 | # │ │ 9 | # │ Copyright 2018 Christopher Simpkins │ 10 | # │ MIT License │ 11 | # │ │ 12 | # │ Source: https://github.com/source-foundry/font-v │ 13 | # │ │ 14 | # └───────────────────────────────────────────────────────────────────────┘ 15 | 16 | from __future__ import unicode_literals 17 | 18 | import os 19 | import re 20 | 21 | from fontTools import ttLib 22 | from git import Repo 23 | 24 | from fontv.utilities import get_git_root_path 25 | 26 | 27 | class FontVersion(object): 28 | """ 29 | FontVersion is a ttf and otf font version string class that provides support for font version string reads, 30 | reporting, modification, & writes. Support is provided for instantiation from ttf and otf fonts, as well 31 | as from fontTools.ttLib.ttFont objects (https://github.com/fonttools/fonttools). 32 | 33 | The class works on Python "strings". String types indicated below refer to the Python2 unicode type and Python3 34 | string type. 35 | 36 | PUBLIC ATTRIBUTES: 37 | 38 | contains_metadata: (boolean) boolean for presence of metadata in version string 39 | 40 | contains_state: (boolean) boolean for presence of state substring metadata in the version string 41 | 42 | contains_status: (boolean) boolean for presence of development/release status substring in the version string 43 | 44 | develop_string: (string) The string to use for development builds in the absence of git commit SHA1 string 45 | 46 | fontpath: (string) The path to the font file 47 | 48 | is_development: (boolean) boolean for presence of development status substring at version_string_parts[1] 49 | 50 | is_release: (boolean) boolean for presence of release status status substring at version_string_parts[1] 51 | 52 | metadata: (list) A list of metadata substrings in the version string. Either version_string_parts[1:] or empty list 53 | 54 | release_string: (string) The string to use for release builds in the absence of git commit SHA1 string 55 | 56 | sha1_develop: (string) The string to append to the git SHA1 hash string for development builds 57 | 58 | sha1_release: (string) The string to append to the git SHA1 hash string for release builds 59 | 60 | state: (string) The state metadata substring 61 | 62 | ttf: (fontTools.ttLib.TTFont) for font file 63 | 64 | version_string_parts: (list) List that maintains in memory semicolon parsed substrings of font version string 65 | 66 | version: (string) The version number substring formatted as "Version X.XXX" 67 | 68 | 69 | PRIVATE ATTRIBUTES 70 | 71 | _nameID_5_dict: (dictionary) {(platformID, platEncID,langID) : fontTools.ttLib.TTFont name record ID 5 object } map 72 | 73 | :parameter font: (string) file path to the .otf or .ttf font file OR (ttLib.TTFont) object for appropriate font file 74 | 75 | :parameter develop: (string) the string to use for development builds in the absence of git commit SHA1 string 76 | 77 | :parameter release: (string) the string to use for release builds in the absence of a git commit SHA1 string 78 | 79 | :parameter sha1_develop: (string) the string to append to the git SHA1 hash string for development builds 80 | 81 | :parameter sha1_release: (string) the string to append to the git SHA1 hash string for release builds 82 | 83 | :raises: fontTools.ttLib.TTLibError if fontpath is not a ttf or otf font 84 | 85 | :raises: IndexError if there are no nameID 5 records in the font name table 86 | 87 | :raises: IOError if fontpath does not exist 88 | """ 89 | 90 | def __init__( 91 | self, 92 | font, 93 | develop="DEV", 94 | release="RELEASE", 95 | sha1_develop="-dev", 96 | sha1_release="-release", 97 | ): 98 | try: 99 | # assume that it is a ttLib.TTFont object and attempt to call object attributes 100 | self.fontpath = font.reader.file.name 101 | # if it does not raise AttributeError, we guessed correctly, can set the ttf attr here 102 | self.ttf = font 103 | except AttributeError: 104 | # if above attempt to call TTFont attribute raises AttributeError (as it would with string file path) 105 | # then instantiate a ttLib.TTFont object and define the fontpath attribute with the file path string 106 | self.ttf = ttLib.TTFont(file=font, recalcTimestamp=False) 107 | self.fontpath = font 108 | 109 | self.develop_string = develop 110 | self.release_string = release 111 | self.sha1_develop = sha1_develop 112 | self.sha1_release = sha1_release 113 | 114 | # name.ID = 5 version string substring data 115 | self.name_ID5_dict = {} 116 | 117 | self.version_string_parts = ( 118 | [] 119 | ) # list of substring items in version string (; delimited parse to list) 120 | self.version = "" 121 | self.state = "" 122 | self.metadata = [] 123 | 124 | # truth test values for version string contents, updated with self._parse() method calls following updates to 125 | # in memory version string data with methods in this library 126 | self.contains_metadata = False 127 | self.contains_state = False 128 | self.contains_status = False 129 | self.is_development = False 130 | self.is_release = False 131 | 132 | # head.fontRevision data. float type 133 | self.head_fontRevision = 0.0 134 | 135 | # object instantiation method call (truth test values updated in the following method) 136 | self._read_version_string() 137 | 138 | def __eq__(self, otherfont): 139 | """ 140 | Equality comparison between FontVersion objects 141 | 142 | :param otherfont: fontv.libfv.FontVersion object for comparison 143 | 144 | :return: (boolean) True = versions are the same; False = versions are not the same 145 | """ 146 | if type(otherfont) is type(self): 147 | return self.version_string_parts == otherfont.version_string_parts 148 | return False 149 | 150 | def __ne__(self, otherfont): 151 | """ 152 | Inequality comparison between FontVersion objects 153 | 154 | :param otherfont: fontv.libfv.FontVersion object for comparison 155 | 156 | :return: (boolean) True = versions differ; False = versions are the same 157 | """ 158 | return not self.__eq__(otherfont) 159 | 160 | def __str__(self): 161 | """ 162 | Human readable string formatting 163 | 164 | :return: (string) 165 | """ 166 | return ( 167 | " " 168 | + os.linesep 169 | + self.get_name_id5_version_string() 170 | + os.linesep 171 | + "file path:" 172 | " " + self.fontpath 173 | ) 174 | 175 | # TODO: confirm comparisons of version numbers like "Version 1.001", "Version 1.01", "Version 1.1" as not the same 176 | # TODO: before this is released. Will need to be documented as such because this is not obvious behavior 177 | # def __gt__(self, otherfont): 178 | # """ 179 | # 180 | # :param otherfont: 181 | # 182 | # :return: 183 | # """ 184 | # return self.get_version_number_tuple() > otherfont.get_version_number_tuple() 185 | # 186 | # def __lt__(self, otherfont): 187 | # """ 188 | # 189 | # :param otherfont: 190 | # 191 | # :return: 192 | # """ 193 | # return self.get_version_number_tuple() < otherfont.get_version_number_tuple() 194 | 195 | def _parse(self): 196 | """ 197 | Private method that parses version string data to set FontVersion object attributes. Called on FontVersion 198 | object instantiation and at the completion of setter methods in the library in order to update object 199 | attributes with new data. 200 | 201 | :return: None 202 | """ 203 | # metadata parsing 204 | self._parse_metadata() # parse the metadata 205 | self._parse_state() # parse the state substring data 206 | self._parse_status() # parse the version substring dev/rel status indicator data 207 | 208 | def _read_version_string(self): 209 | """ 210 | Private method that reads OpenType name ID 5 and head.fontRevision record data from a fontTools.ttLib.ttFont 211 | object and sets FontVersion object properties. The method is called on instantiation of a FontVersion object 212 | 213 | :return: None 214 | """ 215 | 216 | # Read the name.ID=5 record 217 | namerecord_list = self.ttf["name"].names 218 | # read in name records 219 | for record in namerecord_list: 220 | if record.nameID == 5: 221 | # map dictionary as {(platformID, platEncID, langID) : version string} 222 | recordkey = (record.platformID, record.platEncID, record.langID) 223 | self.name_ID5_dict[recordkey] = record.toUnicode() 224 | 225 | # assert that at least one nameID 5 record was obtained from the font in order to instantiate 226 | # a FontVersion object 227 | if len(self.name_ID5_dict) == 0: 228 | raise IndexError( 229 | "Unable to read nameID 5 version records from the font " + self.fontpath 230 | ) 231 | 232 | # define the version string from the dictionary 233 | for vs in self.name_ID5_dict.values(): 234 | version_string = vs 235 | break # take the first value that dictionary serves up 236 | 237 | # parse version string into substrings 238 | self._parse_version_substrings(version_string) 239 | 240 | # define version as first substring 241 | self.version = self.version_string_parts[0] 242 | 243 | # Read the head.fontRevision record (stored as a float) 244 | self.head_fontRevision = self.ttf["head"].fontRevision 245 | 246 | self._parse() # update FontVersion object attributes based upon the data read in 247 | 248 | def _get_repo_commit(self): 249 | """ 250 | Private method that makes a system git call via the GitPython library and returns a short git commit 251 | SHA1 hash string for the commit at HEAD using `git rev-list`. 252 | 253 | :return: (string) short git commit SHA1 hash string 254 | """ 255 | repo = Repo(get_git_root_path(self.fontpath)) 256 | gitpy = repo.git 257 | # git rev-list --abbrev-commit --max-count=1 --format="%h" HEAD - abbreviated unique sha1 for the repository 258 | # number of sha1 hex characters determined by git (addresses https://github.com/source-foundry/font-v/issues/2) 259 | full_git_sha_string = gitpy.rev_list( 260 | "--abbrev-commit", "--max-count=1", '--format="%h"', "HEAD" 261 | ) 262 | unicode_full_sha_string = full_git_sha_string 263 | sha_string_list = unicode_full_sha_string.split("\n") 264 | final_sha_string = sha_string_list[1].replace('"', "") 265 | return final_sha_string 266 | 267 | def _parse_metadata(self): 268 | """ 269 | Private method that parses a font version string for semicolon delimited font version 270 | string metadata. Metadata are defined as anything beyond the first substring item of a version string. 271 | 272 | :return: None 273 | """ 274 | if len(self.version_string_parts) > 1: 275 | # set to True if there are > 1 sub strings as others are defined as metadata 276 | self.contains_metadata = True 277 | self.metadata = ( 278 | [] 279 | ) # reset to empty and allow following code to define the list items 280 | for metadata_item in self.version_string_parts[1:]: 281 | self.metadata.append(metadata_item) 282 | else: 283 | self.metadata = [] 284 | self.contains_metadata = False 285 | 286 | def _parse_state(self): 287 | """ 288 | Private method that parses a font version string for [ ... ] delimited data that represents the State 289 | substring. The result of this test is used to define State data 290 | in the FontVersion object. 291 | 292 | :return: None 293 | """ 294 | if len(self.version_string_parts) > 1: 295 | # Test for regular expression pattern match for state substring at version string list position 1 296 | # This method call returns tuple of (truth test for match, matched state string (or empty string)) 297 | response = self._is_state_substring_return_state_match( 298 | self.version_string_parts[1] 299 | ) 300 | is_state_substring = response[0] 301 | state_substring_match = response[1] 302 | if is_state_substring is True: 303 | self.contains_state = True 304 | self.state = state_substring_match 305 | else: 306 | self.contains_state = False 307 | self.state = "" 308 | else: 309 | self.contains_state = False 310 | self.state = "" 311 | 312 | def _parse_status(self): 313 | """ 314 | Private method that parses a font version string to determine if it contains development/release Status 315 | substring metadata. The result of this test is used to define Status 316 | data in the FontVersion object. 317 | 318 | :return: None 319 | """ 320 | if len(self.version_string_parts) > 1: 321 | # define as list item 1 322 | status_needle = self.version_string_parts[1] 323 | # reset each time there is a parse attempt and let logic below define 324 | self.contains_status = False 325 | 326 | if self._is_development_substring(status_needle): 327 | self.contains_status = True 328 | self.is_development = True 329 | else: 330 | self.is_development = False 331 | 332 | if self._is_release_substring(status_needle): 333 | self.contains_status = True 334 | self.is_release = True 335 | else: 336 | self.is_release = False 337 | else: 338 | self.contains_status = False 339 | self.is_development = False 340 | self.is_release = False 341 | 342 | def _parse_version_substrings(self, version_string): 343 | """ 344 | Private method that splits a full semicolon delimited version string on semicolon characters to a Python list. 345 | 346 | :param version_string: (string) the semicolon delimited version string to split 347 | 348 | :return: None 349 | """ 350 | # split semicolon delimited list of version substrings 351 | if ";" in version_string: 352 | self.version_string_parts = version_string.split(";") 353 | else: 354 | self.version_string_parts = [version_string] 355 | 356 | self.version = self.version_string_parts[0] 357 | 358 | def _set_state_status_substring(self, state_status_string): 359 | """ 360 | Private method that sets the State/Status substring in the FontVersion.version_string_parts[1] list position. 361 | The method preserves Other metadata when present in the version string. 362 | 363 | :param state_status_string: (string) the string value to insert at the status substring position of the 364 | self.version_string_parts list 365 | 366 | :return: None 367 | """ 368 | if len(self.version_string_parts) > 1: 369 | prestring = self.version_string_parts[1] 370 | state_response = self._is_state_substring_return_state_match(prestring) 371 | is_state_substring = state_response[0] 372 | if ( 373 | self._is_release_substring(prestring) 374 | or self._is_development_substring(prestring) 375 | or is_state_substring 376 | ): 377 | # directly replace when existing status substring 378 | self.version_string_parts[1] = state_status_string 379 | else: 380 | # if the second item of the substring list is not a status string, save it and all subsequent list items 381 | # then create a new list with inserted status string value 382 | self.version_string_parts = [ 383 | self.version_string_parts[0] 384 | ] # redefine list as list with version number 385 | self.version_string_parts.append( 386 | state_status_string 387 | ) # define the status substring as next item 388 | for ( 389 | item 390 | ) in ( 391 | self.metadata 392 | ): # iterate through all previous metadata substrings and append to list 393 | self.version_string_parts.append(item) 394 | else: 395 | # if the version string is defined as only a version number substring (i.e. list size = 1), 396 | # write the new status substring to the list. Nothing else required 397 | self.version_string_parts.append(state_status_string) 398 | 399 | # update FontVersion truth testing properties based upon the new data 400 | self._parse() 401 | 402 | def _is_development_substring(self, needle): 403 | """ 404 | Private method that returns a boolean that indicates whether the needle string meets the 405 | definition of a Development Status metadata substring. 406 | 407 | :param needle: (string) test string 408 | 409 | :return: boolean True = is development substring and False = is not a development substring 410 | """ 411 | if ( 412 | self.develop_string == needle.strip() 413 | or self.sha1_develop in needle[-len(self.sha1_develop) :] 414 | ): 415 | return True 416 | else: 417 | return False 418 | 419 | def _is_release_substring(self, needle): 420 | """ 421 | Private method that returns a boolean that indicates whether the needle string meets the 422 | definition of a Release Status metadata substring. 423 | 424 | :param needle: (string) test string 425 | 426 | :return: boolean True = is release substring and False = is not a release substring 427 | """ 428 | if ( 429 | self.release_string == needle.strip() 430 | or self.sha1_release in needle[-len(self.sha1_release) :] 431 | ): 432 | return True 433 | else: 434 | return False 435 | 436 | def _is_state_substring_return_state_match(self, needle): 437 | """ 438 | Private method that returns a tuple of boolean, string. The boolean value reflects the truth test needle is a 439 | State substring. The match value is defined as the contents inside [ and ] delimiters as defined by the 440 | regex pattern. If there is no match, the string item in the tuple is an empty string. 441 | 442 | :param needle: (string) test string to attempt match for state substring 443 | :return: (boolean, string) see full docstring for details re: interpretation of returned values 444 | """ 445 | regex_pattern = r"\s?\[([a-zA-Z0-9_\-\.]{1,50})\]" 446 | p = re.compile(regex_pattern) 447 | m = p.match(needle) 448 | if m: 449 | return True, m.group(1) 450 | else: 451 | return False, "" 452 | 453 | def clear_metadata(self): 454 | """ 455 | Public method that clears all version string metadata in memory. This results in a version string that ONLY 456 | includes the version number substring. The intent is to support removal of unnecessary version string data 457 | that are included in a font binary. 458 | 459 | :return: None 460 | """ 461 | self.version_string_parts = [self.version_string_parts[0]] 462 | self._parse() 463 | 464 | def get_version_number_string(self): 465 | """ 466 | Public method that returns a string of the version number in XXX.XXX format. A version number match is defined 467 | with up to three digits on either side of the period. 468 | 469 | :return: string (Python 3) or unicode (Python 2). Empty string if unable to parse version number format 470 | """ 471 | match = re.search(r"\d{1,3}\.\d{1,3}", self.version) 472 | if match: 473 | return match.group(0) 474 | else: 475 | return "" 476 | 477 | def get_version_number_tuple(self): 478 | """ 479 | Public method that returns a tuple of integer values with the following definition: 480 | 481 | ( major version, minor version position 1, minor version position 2, minor version position 3 ) 482 | 483 | where position is the decimal position of the integer in the minor version string. 484 | 485 | :return: tuple of integers or None if the version number substring is inappropriately formatted 486 | """ 487 | match = re.search(r"\d{1,3}\.\d{1,3}", self.version) 488 | if match: 489 | version_number_int_list = [] 490 | 491 | version_number_string = match.group(0) 492 | version_number_list = version_number_string.split(".") 493 | version_number_major_int = int(version_number_list[0]) 494 | version_number_int_list.append( 495 | version_number_major_int 496 | ) # add major version integer 497 | 498 | for minor_int in version_number_list[1]: 499 | version_number_int_list.append(int(minor_int)) 500 | 501 | return tuple(version_number_int_list) 502 | else: 503 | return None 504 | 505 | def get_head_fontrevision_version_number(self): 506 | """ 507 | Public method that returns the version number that is parsed from head.fontRevision record as a float value. 508 | 509 | :return: float 510 | """ 511 | return self.head_fontRevision 512 | 513 | # TODO: remove this deprecated method (commented out in v0.7.0, deprecation warnings in v0.6.0) 514 | # def get_version_string(self): 515 | # """ 516 | # DEPRECATED: Please convert to use of FontVersion.get_name_id5_version_string() method 517 | # """ 518 | # warnings.simplefilter('always') 519 | # warnstring = "[WARNING] FontVersion.get_version_string is a deprecated method. Please convert to " \ 520 | # "FontVersion.get_name_id5_version_string." 521 | # warnings.warn(warnstring, DeprecationWarning, stacklevel=2) 522 | # return ";".join(self.version_string_parts) 523 | 524 | def get_name_id5_version_string(self): 525 | """ 526 | Public method that returns the full version string as the semicolon delimiter joined contents of the 527 | FontVersion.version_string_parts Python list. 528 | 529 | :return: string (Python 3) or unicode (Python 2) 530 | """ 531 | return ";".join(self.version_string_parts) 532 | 533 | def get_metadata_list(self): 534 | """ 535 | Public method that returns a Python list containing metadata substring items generated by splitting the 536 | string on a semicolon delimiter. The version number string (i.e. "Version X.XXX") is not present in 537 | this list. 538 | 539 | :return: list of string (Python 3) or list of unicode (Python 2) 540 | """ 541 | return self.metadata 542 | 543 | def get_state_status_substring(self): 544 | """ 545 | Public method that returns the State and/or Status substring at position 2 of the semicolon delimited version 546 | string. This substring may include any of the following metadata: 547 | 548 | - "DEV" 549 | - "RELEASE" 550 | - "[state]-dev" 551 | - "[state]-release" 552 | 553 | :return: string (Python 3) or unicode (Python 2), empty string if this substring is not set in the font 554 | """ 555 | if len(self.version_string_parts) > 1: 556 | if self.is_development or self.is_release or self.contains_state: 557 | return self.version_string_parts[1] 558 | else: 559 | return "" 560 | else: 561 | return "" 562 | 563 | def set_state_git_commit_sha1(self, development=False, release=False): 564 | """ 565 | Public method that adds a git commit sha1 hash label to the font version string at the State metadata position. 566 | This can be combined with a Development/Release Status metadata substring if the calling code defines either the 567 | development or release parameter to a value of True. Note that development and release are mutually exclusive. 568 | ValueError is raised if both are set to True. The font source must be under git version control in order to use 569 | this method. If the font source is not under git version control, an IOError is raised during the attempt to 570 | locate the .git directory in the project. 571 | 572 | :param development: (boolean) False (default) = do not add development status indicator; True = add indicator 573 | 574 | :param release: (boolean) False (default) = do not add release status indicator; True = add indicator 575 | 576 | :raises: IOError when the git repository root cannot be identified using the directory traversal in the 577 | fontv.utilities.get_git_root_path() function 578 | 579 | :raises: ValueError when calling code sets both development and release parameters to True as these are 580 | mutually exclusive requests 581 | 582 | :return: None 583 | """ 584 | git_sha1_hash = self._get_repo_commit() 585 | git_sha1_hash_formatted = "[" + git_sha1_hash + "]" 586 | 587 | if development and release: 588 | raise ValueError( 589 | "Cannot set both development parameter and release parameter to a value of True in " 590 | "fontv.libfv.FontVersion.set_state_git_commit_sha1() method. These are mutually " 591 | "exclusive." 592 | ) 593 | 594 | if ( 595 | development 596 | ): # if request for development status label, append FontVersion.sha1_develop to hash digest 597 | hash_substring = git_sha1_hash_formatted + self.sha1_develop 598 | elif ( 599 | release 600 | ): # if request for release status label, append FontVersion.sha1_release to hash digest 601 | hash_substring = git_sha1_hash_formatted + self.sha1_release 602 | else: # else just use the hash digest 603 | hash_substring = git_sha1_hash_formatted 604 | 605 | self._set_state_status_substring(hash_substring) 606 | 607 | def set_development_status(self): 608 | """ 609 | Public method that sets the in memory Development Status metadata substring for the font version string. 610 | 611 | :return: None 612 | """ 613 | self._set_state_status_substring(self.develop_string) 614 | 615 | def set_release_status(self): 616 | """ 617 | Public method that sets the in memory Release Status metadata substring for the font version string. 618 | 619 | :return: None 620 | """ 621 | self._set_state_status_substring(self.release_string) 622 | 623 | def set_version_number(self, version_number): 624 | """ 625 | Public method that sets the version number substring with the version_number parameter. 626 | 627 | The method will raise ValueError if the version_string cannot be cast to a float type. This is mandatory 628 | for the definition of the head table fontRevision record definition in the font binary. Attempts to add 629 | metadata strings to the version_number violate this library's specification and are intentionally not permitted. 630 | 631 | :param version_number: (string) version number in X.XXX format where X are integers 632 | 633 | :return: None 634 | """ 635 | version_number_substring = "Version " + version_number 636 | self.version_string_parts[0] = version_number_substring 637 | self.version = self.version_string_parts[0] # "Version X.XXX" 638 | self.head_fontRevision = float(version_number) # X.XXX 639 | self._parse() 640 | 641 | def set_version_string(self, version_string): 642 | """ 643 | Public method that sets the entire version string (including metadata if desired) with a version_string 644 | parameter. 645 | 646 | The method will raise a ValueError if the version number used in the version_string cannot be cast to a 647 | float type. This is mandatory for the definition of the head table fontRevision record definition in the 648 | font binary. Attempts to add metadata strings to the version_number violate this library's specification and 649 | are intentionally not permitted. 650 | 651 | :param version_string: (string) The version string with semicolon delimited metadata (if metadata are included) 652 | 653 | :return: None 654 | """ 655 | self._parse_version_substrings(version_string) 656 | self._parse() 657 | self.head_fontRevision = float(self.get_version_number_string()) 658 | 659 | def write_version_string(self, fontpath=None): 660 | """ 661 | Public method that writes the in memory version data to: 662 | 663 | (1) each OpenType name table ID 5 record in original font file 664 | (2) OpenType head table fontRevision record 665 | 666 | The name table ID 5 record(s) write is with a semicolon joined list of the items in 667 | FontVersion.version_string_parts 668 | 669 | The head table fontRevision record write is with the version number float value in FontVersion.head_fontRevision 670 | 671 | The write is to a .otf file if the FontVersion object was instantiated from a .otf binary and a .ttf 672 | file if the FontVersion object was instantiated from a .ttf binary. By default the write is to the same 673 | file path that was used for instantiation of the FontVersion object. This write path default can be modified by 674 | passing a new file path in the fontpath parameter. 675 | 676 | :param fontpath: (string) optional file path to write out the font version string to a font binary 677 | 678 | :return: None 679 | """ 680 | # Write to name table ID 5 record 681 | version_string = self.get_name_id5_version_string() 682 | namerecord_list = self.ttf["name"].names 683 | for record in namerecord_list: 684 | if record.nameID == 5: 685 | # write to fonttools ttLib object name ID 5 table record for each nameID 5 record found in the font 686 | record.string = version_string 687 | 688 | # Write version number to head table fontRevision record 689 | self.ttf["head"].fontRevision = self.head_fontRevision 690 | 691 | # Write changes out to the font binary path 692 | if fontpath is None: 693 | self.ttf.save(self.fontpath) 694 | else: 695 | self.ttf.save(fontpath) 696 | -------------------------------------------------------------------------------- /lib/fontv/commandlines.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # 5 | # commandlines.py 6 | # Command line application argument parsing library for Python 7 | # 8 | # Copyright 2018 Christopher Simpkins 9 | # MIT License 10 | # 11 | 12 | """The commandlines module contains the Command, Arguments, Definitions, Mops, MultiDefinitions, and Switches 13 | classes. These objects are used to parse command line argument strings to command line syntax specific Python objects. 14 | 15 | Exceptions raised by this module are in the `exceptions.py` module. 16 | """ 17 | 18 | import sys 19 | from fontv.exceptions import ( 20 | IndexOutOfRangeError, 21 | MissingArgumentError, 22 | MissingDictionaryKeyError, 23 | ) 24 | 25 | 26 | class Command(object): 27 | """An object that maintains syntax specific components of a command line string and provides methods to support 28 | the development of Python command line applications. 29 | 30 | The class is instantiated from the list of command line arguments that are passed to a Python script in `sys.argv`. 31 | 32 | Attributes: 33 | arg0 : (string) 34 | Argument at index position 0 35 | arg1 : (string) 36 | Argument at index position 1 37 | arg2 : (string) 38 | Argument at index position 2 39 | arg3 : (string) 40 | Argument at index position 3 41 | arg4 : (string) 42 | Argument at index position 4 43 | argc : (int) 44 | Length of the arguments list 45 | arglp : (string) 46 | Argument at last index position in the arguments list 47 | arguments: (Arguments, list) 48 | List of all ordered positional arguments in the command string 49 | defaults: (dict) 50 | Dictionary of default key : value mapped as option : argument value 51 | defs: (Definitions, dict) 52 | Dictionary of key=option : value=argument definition pairs 53 | mdefs: (MultiDefinitions, Definitions, dict) 54 | Dictionary of key=option : value=argument definition pairs for options included more than once in command 55 | mops: (set) 56 | Set of multi-option short syntax (i.e. single dash) switches 57 | subcmd: (string) 58 | The first positional argument (=arg0) 59 | subsubcmd: (string) 60 | The second positional argument (=arg1) 61 | switches: (set) 62 | Set of long and short switch syntax arguments 63 | """ 64 | 65 | def __init__(self): 66 | self.argv = sys.argv[1:] 67 | self.arguments = Arguments(self.argv) 68 | self.defaults = {} 69 | self.switches = Switches(self.argv) 70 | self.mops = Mops(self.argv) 71 | self.defs = Definitions(self.argv) 72 | self.mdefs = MultiDefinitions(self.argv) 73 | self.argc = len(self.argv) 74 | self.arg0 = self.arguments.get_argument_for_commandobj(0) 75 | self.arg1 = self.arguments.get_argument_for_commandobj(1) 76 | self.arg2 = self.arguments.get_argument_for_commandobj(2) 77 | self.arg3 = self.arguments.get_argument_for_commandobj(3) 78 | self.arg4 = self.arguments.get_argument_for_commandobj(4) 79 | self.arglp = self.arguments.get_argument_for_commandobj(self.argc - 1) 80 | self.subcmd = self.arg0 81 | self.subsubcmd = self.arg1 82 | self.has_args = len(self.arguments) > 0 83 | self.has_switches = len(self.switches) > 0 84 | self.has_mops = len(self.mops) > 0 85 | self.has_defs = len(self.defs) > 0 86 | self.has_mdefs = len(self.mdefs) > 0 87 | 88 | # TODO: implement support for short / long option alternatives 89 | 90 | def __repr__(self): 91 | return "< Command object > instantiated from arguments: " + self.argv.__str__() 92 | 93 | def __str__(self): 94 | return "< Command object > instantiated from arguments: " + self.argv.__str__() 95 | 96 | # ////////////////////////////////////////////////////////////// 97 | # 98 | # Validation methods 99 | # 100 | # ////////////////////////////////////////////////////////////// 101 | 102 | def does_not_validate_missing_args(self): 103 | """Command string validation for missing arguments to the executable. 104 | 105 | :returns: boolean. True = does not validate. False = validates""" 106 | 107 | return self.argc == 0 108 | 109 | def does_not_validate_missing_defs(self): 110 | """Command string validation for missing definitions to the executable 111 | 112 | :returns: boolean. True = does not validate. False = validates""" 113 | 114 | return len(self.defs) == 0 and len(self.mdefs) == 0 115 | 116 | def does_not_validate_missing_mops(self): 117 | """Command string validation for missing multi-option short syntax arguments to the executable 118 | 119 | :returns: boolean. True = does not validate. False = validates""" 120 | 121 | return len(self.mops) == 0 122 | 123 | def does_not_validate_missing_switches(self): 124 | """Command string validation for missing switches to the executable 125 | 126 | :returns: boolean. True = does not validate. False = validates""" 127 | 128 | return len(self.switches) == 0 129 | 130 | def does_not_validate_n_args(self, number): 131 | """Command string validation for inclusion of exactly n arguments to executable. 132 | 133 | :param number: (integer) Defines the number of expected arguments for this test 134 | :returns: boolean. True = does not validate. False = validates""" 135 | 136 | if self.argc == number: 137 | return False 138 | else: 139 | return True 140 | 141 | def validates_includes_args(self): 142 | """Command string validation for inclusion of at least one argument to the executable 143 | 144 | :returns: boolean. True = validates. False = does not validate.""" 145 | 146 | return self.argc > 0 147 | 148 | def validates_includes_definitions(self): 149 | """Command string validation for inclusion of at least one definition (option-argument) to the executable 150 | 151 | :returns: boolean. True = validates. False = does not validate.""" 152 | 153 | return len(self.defs) > 0 154 | 155 | def validates_includes_mops(self): 156 | """Command string validation for inclusion of at least one multi-option short syntax argument to the 157 | executable. 158 | 159 | :returns: boolean. True = validates. False = does not validate.""" 160 | 161 | return len(self.mops) > 0 162 | 163 | def validates_includes_switches(self): 164 | """Command string validation for inclusion of at least one switch argument to the executable. 165 | 166 | :returns: boolean. True = validates. False = does not validate.""" 167 | 168 | return len(self.switches) > 0 169 | 170 | def validates_includes_n_args(self, number): 171 | """Command string validation for inclusion of exactly `number` arguments to the executable. 172 | 173 | :param number: (integer) Defines the number of expected arguments for this test 174 | :returns: boolean. True = validates. False = does not validate.""" 175 | 176 | return self.argc == number 177 | 178 | # ////////////////////////////////////////////////////////////// 179 | # 180 | # Default option:argument mapping methods 181 | # 182 | # ////////////////////////////////////////////////////////////// 183 | 184 | def set_defaults(self, default_dictionary): 185 | """Sets default option : argument definitions with a dictionary parameter. The option keys should not include 186 | dashes at the beginning of the option string. One or more key:value pairs can be included in the 187 | default_dictionary parameter. 188 | 189 | :param default_dictionary: (dict) Defines the default key=option : value=argument mapping 190 | :returns: None""" 191 | 192 | self.defaults.update(default_dictionary) 193 | 194 | def contains_defaults(self, *default_needles): 195 | """Tests for the presence of one or more default option : argument definitions in the Command.defaults parameter 196 | 197 | :param default_needles: (tuple) One or more test default option strings 198 | :returns: boolean. True = the default options are defined. False = the default options are not defined""" 199 | 200 | for needle in default_needles: 201 | if needle in self.defaults.keys(): 202 | pass 203 | else: 204 | return False # if any needle is absent, returns False 205 | return True # if all tests pass, returns True 206 | 207 | def get_default(self, default_needle): 208 | """Gets the value for an existing default option : argument definition in the Command.defaults 209 | parameter. The default_needle option string should not include dashes at the beginning of the string. 210 | 211 | :param default_needle: (string) The existing default option for which a value is requested 212 | :returns: User-specified type. A value of any type that is permissible as a value in Python dictionaries 213 | :raises: MissingDictionaryKeyError if the key is not found in the Command.defaults dictionary""" 214 | 215 | if default_needle in self.defaults.keys(): 216 | return self.defaults[default_needle] 217 | else: 218 | raise MissingDictionaryKeyError(default_needle) 219 | 220 | # ////////////////////////////////////////////////////////////// 221 | # 222 | # Application logic methods 223 | # 224 | # ////////////////////////////////////////////////////////////// 225 | 226 | def contains_switches(self, *switch_needles): 227 | """Test for the presence of one or more switches in the command string. Returns boolean that indicates presence 228 | (True) or absence (False) of switches. Dashes should not be used at the beginning of the strings in the 229 | `switch_needles` parameter. 230 | 231 | :param switch_needles: (tuple) One or more expected switch strings. 232 | :returns: boolean""" 233 | 234 | return self.switches.contains(switch_needles) 235 | 236 | def contains_mops(self, *mops_needles): 237 | """Returns boolean that indicates presence (True) or absence (False) of one or more multi-option 238 | short syntax switch characters. 239 | 240 | :type mops_needles: tuple of one or more expected single character switches 241 | :returns: boolean""" 242 | 243 | return self.mops.contains(mops_needles) 244 | 245 | def contains_definitions(self, *def_needles): 246 | """Test for the presence of one or more option-argument definitions in the command string. Returns boolean 247 | that indicates presence (True) or absence (False) of definition options. Dashes should not be used at the 248 | beginning of the strings in the `def_needles` parameter. 249 | 250 | :param def_needles: (tuple) One or more expected definition option key(s). 251 | :returns: boolean""" 252 | 253 | return self.defs.contains(def_needles) 254 | 255 | def contains_multi_definitions(self, *def_needles): 256 | """Test for the presence of multiple option-argument definitions that use the same option string. An example is 257 | 258 | `$ executable -o file1 -o file2` 259 | 260 | The dashes in the argument strings should not be included in the `def_needles` parameter. Returns boolean that 261 | indicates presence (True) or absence (False) of one or more multi-definition options. 262 | 263 | :param def_needles: (tuple) One or more expected definition option key(s). 264 | :returns: boolean""" 265 | 266 | return self.mdefs.contains(def_needles) 267 | 268 | def has_command_sequence(self, *cmd_list): 269 | """Test for a sequence of command line tokens in the command string. The test begins at index position 0 270 | of the argument list and is case-sensitive. 271 | 272 | :param cmd_list: (tuple) Expected commands in expected order starting at Command.argv index position 0 273 | :returns: boolean""" 274 | 275 | if len(cmd_list) > len( 276 | self.argv 277 | ): # request does not inlude more args than the Command.argv property includes 278 | return False 279 | else: 280 | index = 0 281 | for test_arg in cmd_list: 282 | if ( 283 | self.argv[index] == test_arg 284 | ): # test that argument at index position matches in parameter order 285 | index += 1 286 | else: 287 | return False 288 | return True 289 | 290 | def has_args_after(self, argument_needle, number=1): 291 | """Test for the presence of at least one (default) positional arguments following an existing argument 292 | (argument_needle). The number of expected arguments is modified by defining the `number` method parameter. 293 | 294 | :param number: (integer) The number of expected arguments after the test argument 295 | :param argument_needle: (string) The test argument that is known to be present in the command 296 | :raises: MissingArgumentError when argument_needle is not found in the parsed argument list""" 297 | 298 | if argument_needle in self.arguments: 299 | position = self.arguments.get_arg_position(argument_needle) 300 | if len(self.argv) > (position + number): 301 | return True 302 | else: 303 | return False 304 | else: 305 | raise MissingArgumentError(argument_needle) 306 | 307 | def next_arg_is_in(self, start_argument, supported_at_next_position): 308 | """Test for the presence of a supported argument in the n+1 index position for a known argument at the 309 | n position. start_argument is called as the full argument string including any expected dashes. 310 | 311 | :param start_argument: (string) The argument string including any beginning dashes as used on the command line. 312 | :param supported_at_next_position: (list) list of strings that define supported arguments in the n+1 index 313 | :raises: MissingArgumentError when start_argument is not found in the parsed argument list""" 314 | 315 | if start_argument in self.arguments: 316 | position = self.arguments.get_arg_position(start_argument) 317 | test_argument = self.arguments.get_arg_next(position) 318 | if test_argument in supported_at_next_position: 319 | return True 320 | else: 321 | return False 322 | else: 323 | raise MissingArgumentError(start_argument) 324 | 325 | # ////////////////////////////////////////////////////////////// 326 | # 327 | # Special command line idiom testing methods 328 | # 329 | # ////////////////////////////////////////////////////////////// 330 | 331 | def has_double_dash(self): 332 | """Test for the presence of the double dash `--` command line idiom. 333 | 334 | :returns: boolean. True = has double dash token. False = does not contain double dash token.""" 335 | 336 | if "--" in self.arguments: 337 | return True 338 | else: 339 | return False 340 | 341 | # ////////////////////////////////////////////////////////////// 342 | # 343 | # Getter methods for command line argument strings 344 | # 345 | # ////////////////////////////////////////////////////////////// 346 | 347 | def get_definition(self, def_needle): 348 | """Returns the argument to an option that is part of an option-argument definition pair. 349 | 350 | :param def_needle: (string) The option string of the option-argument pair 351 | :returns: string 352 | :raises: MissingDictionaryKeyError when the option string is not found""" 353 | 354 | return self.defs.get_def_argument(def_needle) 355 | 356 | def get_multiple_definitions(self, def_needle): 357 | """Returns a list of argument strings to an option that is included multiple times using option-argument 358 | syntax on the command line (e.g. `$ executable -o file1 -o file2`) 359 | 360 | :param def_needle: (string) The option string of the option-argument pair 361 | :returns: string 362 | :raises: MissingDictionaryKeyError when the option string is not found""" 363 | 364 | return self.mdefs.get_def_argument(def_needle) 365 | 366 | def get_arg_after(self, target_arg): 367 | """Returns the next positional argument at index position n + 1 to a command line argument at index position n. 368 | 369 | :param target_arg: (string) Argument string for the test. 370 | :returns: string 371 | :raises: MissingArgumentError when target_arg is not found in the parsed argument list 372 | :raises: IndexOutOfRangeError when target_arg is the last positional argument""" 373 | 374 | if target_arg in self.argv: 375 | recipient_position = self.arguments.get_arg_position(target_arg) 376 | return self.arguments.get_arg_next(recipient_position) 377 | else: 378 | raise MissingArgumentError(target_arg) 379 | 380 | def get_double_dash_args(self): 381 | """Returns the arguments after the double dash `--` command line idiom as a list. 382 | 383 | :returns: list of strings""" 384 | 385 | if "--" in self.arguments: 386 | dd_position = self.arguments.get_arg_position("--") 387 | start_position = dd_position + 1 388 | return self.arguments[start_position:] 389 | else: 390 | raise MissingArgumentError("--") 391 | 392 | # ///////////////////////////////////////////////////////////// 393 | # 394 | # Default parsing methods for commonly used options/switches 395 | # - Includes support for POSIX / Gnu standard options 396 | # 397 | # ///////////////////////////////////////////////////////////// 398 | 399 | def is_help_request(self): 400 | """Tests for `-h` and `--help` options in command string 401 | 402 | :returns: boolean. True = included help option. False = did not include help option.""" 403 | 404 | if "help" in self.switches or "h" in self.switches: 405 | return True 406 | else: 407 | return False 408 | 409 | def is_quiet_request(self): 410 | """Tests for `--quiet` option in command string 411 | 412 | :returns: boolean. True = included quiet option. False = did not include quiet option.""" 413 | 414 | if "quiet" in self.switches: 415 | return True 416 | else: 417 | return False 418 | 419 | def is_usage_request(self): 420 | """Tests for `--usage` option in command string 421 | 422 | :returns: boolean. True = included usage option. False = did not include usage option.""" 423 | 424 | if "usage" in self.switches: 425 | return True 426 | else: 427 | return False 428 | 429 | def is_verbose_request(self): 430 | """Tests for `--verbose` option in command string 431 | 432 | :returns: boolean. True = included verbose option. False = did not include verbose option.""" 433 | 434 | if "verbose" in self.switches: 435 | return True 436 | else: 437 | return False 438 | 439 | def is_version_request(self): 440 | """Tests for `-v` and `--version` options in command string. 441 | 442 | :returns: boolean. True = included version option. False = did not include version option.""" 443 | 444 | if "version" in self.switches or "v" in self.switches: 445 | return True 446 | else: 447 | return False 448 | 449 | # ///////////////////////////////////////////////////////////// 450 | # 451 | # Development + Testing methods 452 | # 453 | # ///////////////////////////////////////////////////////////// 454 | 455 | def obj_string(self): 456 | """Returns a string of the instance attributes of the Command object intended for standard output use. 457 | Print the returned string to view the parsed arguments in the standard output stream. 458 | 459 | :returns: string""" 460 | 461 | the_string = "obj.argc = " + str(self.argc) 462 | the_string = the_string + "\n" + "obj.arguments = " + str(self.arguments) 463 | the_string = the_string + "\n" + "obj.defaults = " + str(self.defaults) 464 | the_string = the_string + "\n" + "obj.switches = " + str(self.switches) 465 | the_string = the_string + "\n" + "obj.defs = " + str(self.defs) 466 | the_string = the_string + "\n" + "obj.mdefs = " + str(self.mdefs) 467 | the_string = the_string + "\n" + "obj.mops = " + str(self.mops) 468 | the_string = ( 469 | the_string 470 | + "\n" 471 | + "obj.arg0 = " 472 | + self._get_obj_string_format_arg(self.arg0) 473 | ) 474 | the_string = ( 475 | the_string 476 | + "\n" 477 | + "obj.arg1 = " 478 | + self._get_obj_string_format_arg(self.arg1) 479 | ) 480 | the_string = ( 481 | the_string 482 | + "\n" 483 | + "obj.arg2 = " 484 | + self._get_obj_string_format_arg(self.arg2) 485 | ) 486 | the_string = ( 487 | the_string 488 | + "\n" 489 | + "obj.arg3 = " 490 | + self._get_obj_string_format_arg(self.arg3) 491 | ) 492 | the_string = ( 493 | the_string 494 | + "\n" 495 | + "obj.arg4 = " 496 | + self._get_obj_string_format_arg(self.arg4) 497 | ) 498 | the_string = ( 499 | the_string 500 | + "\n" 501 | + "obj.arglp = " 502 | + self._get_obj_string_format_arg(self.arglp) 503 | ) 504 | the_string = ( 505 | the_string 506 | + "\n" 507 | + "obj.subcmd = " 508 | + self._get_obj_string_format_arg(self.subcmd) 509 | ) 510 | the_string = ( 511 | the_string 512 | + "\n" 513 | + "obj.subsubcmd = " 514 | + self._get_obj_string_format_arg(self.subsubcmd) 515 | ) 516 | 517 | return the_string 518 | 519 | def _get_obj_string_format_arg(self, the_string): 520 | """Formats argument strings for standard output display 521 | 522 | :returns: string""" 523 | 524 | if the_string == "": 525 | return "''" 526 | else: 527 | return "'" + the_string + "'" 528 | 529 | 530 | class Arguments(list): 531 | """A class that includes all command line arguments with positional argument order maintained. Instantiated with 532 | a list of command line string tokens. 533 | 534 | The class is derived from the Python list type. 535 | 536 | :param argv: A list of command line arguments that maintain the argument order that was entered on command line""" 537 | 538 | def __init__(self, argv): 539 | list.__init__(self, argv) 540 | 541 | def __repr__(self): 542 | argument_string = "" 543 | if len(self) > 0: 544 | for argument in self: 545 | argument_string = argument_string + "'" + argument + "', " 546 | argument_string = argument_string.rstrip() 547 | argument_string = argument_string.rstrip(",") 548 | 549 | return "[" + argument_string + "]" 550 | 551 | def __str__(self): 552 | argument_string = "" 553 | if len(self) > 0: 554 | for argument in self: 555 | argument_string = argument_string + "'" + argument + "', " 556 | argument_string = argument_string.rstrip() 557 | argument_string = argument_string.rstrip(",") 558 | 559 | return "[" + argument_string + "]" 560 | 561 | def get_argument_for_commandobj(self, position): 562 | """An argument parsing method for the instantation of the Command object. This is not intended for public use. 563 | Public calls should use the get_argument() method instead. 564 | 565 | :param position: The command line index position 566 | :returns: string or empty string if the index position is out of the index range""" 567 | 568 | if (len(self) > position) and (position >= 0): 569 | return self[position] 570 | else: 571 | return "" # intentionally set as empty string rather than raise exception for Command obj instantation 572 | 573 | def get_argument(self, position): 574 | """Returns an argument string by the argument list index position. 575 | 576 | :param position: (integer) The command line index position 577 | :returns: string 578 | :raises: IndexOutOfRangeError if the requested index falls outside of the list index range""" 579 | 580 | if (len(self) > position) and (position >= 0): 581 | return self[position] 582 | else: 583 | raise IndexOutOfRangeError() 584 | 585 | def get_arg_position(self, test_arg): 586 | """Returns the index position of the `test_arg` parameter candidate argument string. The argument string 587 | should include the dashes at the beginning of the argument string that would be expected with use on the 588 | command line. 589 | 590 | :param test_arg: (string) The argument string for which the index position is requested 591 | :returns: string 592 | :raises: MissingArgumentError if the requested argument is not in the Argument list""" 593 | 594 | if test_arg in self: 595 | return self.index(test_arg) 596 | else: 597 | raise MissingArgumentError(test_arg) 598 | 599 | def get_arg_next(self, position): 600 | """Returns the next argument at index `position` + 1 in the command sequence. 601 | 602 | :param position: (integer) The argument index position in the Argument list 603 | :returns: string 604 | :raises: IndexOutOfRangeError if the `position` + 1 index falls outside of the existing index range""" 605 | 606 | if len(self) > (position + 1): 607 | return self[position + 1] 608 | else: 609 | raise IndexOutOfRangeError() 610 | 611 | def contains(self, needle): 612 | """Returns boolean that indicates the presence (True) or absence (False) of a tuple of one or more test 613 | arguments. 614 | 615 | :param needle: (iterable) An iterable that contains one or more test argument strings. 616 | :returns: boolean""" 617 | 618 | for expected_argument in needle: 619 | if expected_argument in self: 620 | pass 621 | else: 622 | return False 623 | 624 | return True # if all tests above pass 625 | 626 | 627 | class Switches(set): 628 | """A class that is instantiated with all command line switches that have the syntax `-s`, `--longswitch`, 629 | or `-onedashlong`. 630 | 631 | The class is derived from the Python set type and arguments with this syntax are saved as set items. 632 | 633 | :param argv: (list) A list of command line arguments that maintain the argument order that was entered on command line 634 | """ 635 | 636 | def __init__(self, argv): 637 | set.__init__(self, self._make_switch_set(argv)) 638 | 639 | def __repr__(self): 640 | switch_string = "" 641 | if len(self) > 0: 642 | for switch in self: 643 | switch_string = switch_string + "'" + switch + "', " 644 | switch_string = switch_string.rstrip() 645 | switch_string = switch_string.rstrip(",") 646 | 647 | return "{" + switch_string + "}" 648 | 649 | def __str__(self): 650 | switch_string = "" 651 | if len(self) > 0: 652 | for switch in self: 653 | switch_string = switch_string + "'" + switch + "', " 654 | switch_string = switch_string.rstrip() 655 | switch_string = switch_string.rstrip(",") 656 | 657 | return "{" + switch_string + "}" 658 | 659 | def _make_switch_set(self, argv): 660 | """Returns a set that includes all switches that are parsed from the command string. Used to instantiate Switch 661 | objects. 662 | 663 | :param argv: (list) A list of command line arguments that maintain the argument order that was entered on command line 664 | :returns: set""" 665 | 666 | switchset = set() 667 | for switch_candidate in argv: 668 | if "-" in switch_candidate[0] and "=" not in switch_candidate: 669 | # ignore everything after the double dash idiom, no longer considered switch context 670 | if switch_candidate == "--": 671 | break 672 | else: 673 | switch_candidate = switch_candidate.lstrip("-") 674 | switchset.add(switch_candidate) 675 | 676 | return switchset 677 | 678 | def contains(self, needle): 679 | """Returns boolean that indicates the presence (True) or absence (False) of a tuple of test switches. 680 | Switch parameters in needle tuple should be passed without initial dash character(s) in the test switch 681 | argument name. 682 | 683 | :param needle: (iterable) An iterable that contains one or more test argument strings. 684 | :returns: boolean""" 685 | 686 | for expected_argument in needle: 687 | if expected_argument in self: 688 | pass 689 | else: 690 | return False 691 | 692 | return True # if all tests above pass 693 | 694 | 695 | class Mops(set): 696 | """A class that is instantiated with unique switches from multi-option command line options that use short, 697 | single dash syntax. 698 | 699 | Examples: -rnj -tlx 700 | 701 | Each alphabetic character in the option token is parsed to a separate option token. 702 | 703 | The class is derived from the Python set type and the single character option switches are stored as set items. 704 | 705 | :param argv: (list) A list of command line arguments that maintain the argument order that was entered on command line 706 | """ 707 | 708 | def __init__(self, argv): 709 | set.__init__(self, self._make_mops_set(argv)) 710 | 711 | def __repr__(self): 712 | mops_string = "" 713 | if len(self) > 0: 714 | for switch in self: 715 | mops_string = mops_string + "'" + switch + "', " 716 | mops_string = mops_string.rstrip() 717 | mops_string = mops_string.rstrip(",") 718 | 719 | return "{" + mops_string + "}" 720 | 721 | def __str__(self): 722 | mops_string = "" 723 | if len(self) > 0: 724 | for switch in self: 725 | mops_string = mops_string + "'" + switch + "', " 726 | mops_string = mops_string.rstrip() 727 | mops_string = mops_string.rstrip(",") 728 | 729 | return "{" + mops_string + "}" 730 | 731 | def _make_mops_set(self, argv): 732 | """Returns a set of multi-option short syntax option characters that are parsed from a list of ordered 733 | command string arguments in the parameter `argv`. 734 | 735 | :param argv: (list) A list of command line arguments that maintain the argument order that was entered on command line 736 | :returns: set""" 737 | 738 | mopsset = set() 739 | for mops_candidate in argv: 740 | if "-" in mops_candidate[0] and "=" not in mops_candidate: 741 | if ( 742 | len(mops_candidate) > 2 743 | ): # the argument includes '-' and more than one character following dash 744 | if ( 745 | mops_candidate[1] != "-" 746 | ): # it is not long option syntax (e.g. --long) 747 | mops_candidate = mops_candidate.replace("-", "") 748 | for switch in mops_candidate: 749 | mopsset.add(switch) 750 | return mopsset 751 | 752 | def contains(self, needle): 753 | """Returns boolean that indicates the presence (True) or absence (False) of a tuple of test Mops syntax option 754 | switches. The test strings should each be a single character without the dash that is used at the beginning of 755 | the entire token. 756 | 757 | :param needle: (iterable) An iterable that contains one or more test argument characters as strings. 758 | :returns: boolean""" 759 | 760 | for expected_argument in needle: 761 | if expected_argument in self: 762 | pass 763 | else: 764 | return False 765 | 766 | return True 767 | 768 | 769 | class Definitions(dict): 770 | """A class that is instantiated with all command line definition options as defined by the syntax 771 | `-s `, `--longoption `, 772 | `--longoption=`, or `-longoption `. 773 | 774 | To parse as a definition option, the argument to the option must not contain any dashes at the beginning of 775 | the argument string. For example, `-o --long` is not considered a definition option-arg pair, whereas 776 | `-o long` is. 777 | 778 | This class is derived from the Python dictionary type. The mapping is: 779 | 780 | key = option string with all dash '-' character(s) at the beginning of the string removed. Internal dashes are 781 | maintained. 782 | 783 | value = definition argument string. 784 | 785 | :param argv: (list) A list of command line arguments that maintain the argument order that was entered on command line 786 | """ 787 | 788 | def __init__(self, argv): 789 | dict.__init__(self, self._make_definitions_obj(argv)) 790 | 791 | def _make_definitions_obj(self, argv): 792 | """Parses definition options from a list of ordered command line arguments to define the dictionary that 793 | is used to instantiate the Definitions class. Option string keys are stripped of dash characters before the 794 | first alphabetic character in the option name. 795 | 796 | :param argv: (list) A list of command line arguments that maintain the argument order that was entered on command line 797 | :returns: dictionary with {key = option string : value = definition argument string} mapping""" 798 | 799 | defmap = {} 800 | arglist_length = len(argv) 801 | counter = 0 802 | for def_candidate in argv: 803 | # performance improvement to eliminate multiple string testing calls within this loop 804 | # dash_truth_test = def_candidate.startswith("-") 805 | dash_truth_test = "-" in def_candidate[0] 806 | if dash_truth_test is True: 807 | # ignore all definition syntax strings after the double dash `--` command line idiom 808 | if def_candidate == "--": 809 | break 810 | else: 811 | # defines -option=definition syntax 812 | if "=" in def_candidate: 813 | split_def = def_candidate.split("=") 814 | cleaned_key = split_def[0].lstrip( 815 | "-" 816 | ) # remove dash characters from the option 817 | defmap[cleaned_key] = split_def[1] 818 | # defines -d or --define syntax 819 | elif counter < (arglist_length - 1): 820 | if not argv[counter + 1].startswith("-"): 821 | def_candidate = def_candidate.lstrip("-") 822 | defmap[def_candidate] = argv[counter + 1] 823 | 824 | counter += 1 825 | 826 | return defmap 827 | 828 | def contains(self, needle): 829 | """Returns boolean that indicates the presence (True) or absence (False) of a tuple of option-argument 830 | definitions by option match attempt. 831 | 832 | The definition option string should be used without any initial dash characters in the definition argument name 833 | in contrast to how they were used on the command line. 834 | 835 | :param needle: (tuple) A tuple of one or more option strings from expected definition option-argument string pairs 836 | :returns: boolean""" 837 | 838 | for expected_definition in needle: 839 | if expected_definition in self.keys(): 840 | pass 841 | else: 842 | return False 843 | 844 | return True # if all tests above pass returns True 845 | 846 | def get_def_argument(self, needle): 847 | """Returns the definition argument string for a definition option test. The needle parameter should not include 848 | dash characters at the beginning of the option string (i.e. use 'test' rather than '--test' and 't' rather than 849 | '-t'). 850 | 851 | :param needle: (string) The requested option string from the definition option-argument pair. 852 | :returns: string 853 | :raises: MissingDictionaryKeyError if the option needle is not a key defined in the Definitions object""" 854 | 855 | if needle in self.keys(): 856 | return self[needle] 857 | else: 858 | raise MissingDictionaryKeyError(needle) 859 | 860 | 861 | class MultiDefinitions(Definitions): 862 | """A class that is used to parse option-argument definitions from a command line argument list where command line 863 | use includes multiple same option strings with different argument definitions. An example is: 864 | 865 | `$ executable -o file1 -o file2` 866 | 867 | The class is derived from the commandlines.Definitions class (which is derived from Python dict). The 868 | commandlines.Definitions.contains and commandlines.Definitions.get_def_arguments methods are inherited from the 869 | Definitions class. 870 | 871 | The dictionary mapping is: 872 | 873 | key = option string with all dash '-' character(s) at the beginning of the string removed. Internal dashes are 874 | maintained. 875 | 876 | value = list of all argument strings associated with the option string on the command line 877 | 878 | :param argv: (list) A list of command line arguments that maintain the argument order that was entered on command line 879 | """ 880 | 881 | def __init__(self, argv): 882 | Definitions.__init__(self, argv) 883 | 884 | def _make_definitions_obj(self, argv): 885 | defmap = {} 886 | arglist_length = len(argv) 887 | counter = 0 888 | for def_candidate in argv: 889 | # performance improvement to eliminate multiple string testing calls within this loop 890 | # dash_truth_test = def_candidate.startswith("-") 891 | dash_truth_test = "-" in def_candidate[0] 892 | if dash_truth_test is True: 893 | # ignore all definition syntax strings after the double dash `--` command line idiom 894 | if def_candidate == "--": 895 | break 896 | else: 897 | # defines -option=definition syntax 898 | if "=" in def_candidate: 899 | split_def = def_candidate.split("=") 900 | cleaned_key = split_def[0].lstrip( 901 | "-" 902 | ) # remove dash characters from the option 903 | if cleaned_key in defmap.keys(): 904 | defmap[cleaned_key].append(split_def[1]) 905 | else: 906 | defmap[cleaned_key] = [split_def[1]] 907 | # defines -d or --define syntax 908 | elif counter < (arglist_length - 1): 909 | if not argv[counter + 1].startswith("-"): 910 | def_candidate = def_candidate.lstrip("-") 911 | if def_candidate in defmap.keys(): 912 | defmap[def_candidate].append(argv[counter + 1]) 913 | else: 914 | defmap[def_candidate] = [argv[counter + 1]] 915 | 916 | counter += 1 917 | 918 | # keep only the dictionary key:value pairs that include multiple values from key:value items parsed above 919 | multi_map = {} 920 | for key in defmap.keys(): 921 | if len(defmap[key]) > 1: 922 | multi_map[key] = defmap[key] 923 | 924 | return multi_map 925 | -------------------------------------------------------------------------------- /tests/test_libfv.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from __future__ import unicode_literals 5 | 6 | import math 7 | import os 8 | import os.path 9 | import re 10 | 11 | import pytest 12 | 13 | from fontTools.ttLib import TTFont, TTLibError 14 | 15 | from fontv.libfv import FontVersion 16 | 17 | # TEST FONT FILE CREATION 18 | # fv = FontVersion("testfiles/Hack-Regular.ttf") 19 | # 20 | # fv.version_string_parts = ['Version 1.010'] 21 | # fv.write_version_string(fontpath="testfiles/Test-VersionOnly.ttf") 22 | # 23 | # fv.version_string_parts = ["Version 1.010", "DEV"] 24 | # fv.write_version_string(fontpath="testfiles/Test-VersionDEV.ttf") 25 | # 26 | # fv.version_string_parts = ["Version 1.010", "RELEASE"] 27 | # fv.write_version_string(fontpath="testfiles/Test-VersionREL.ttf") 28 | # 29 | # fv.version_string_parts = ["Version 1.010", "[abcd123]"] 30 | # fv.write_version_string(fontpath="testfiles/Test-VersionSha.ttf") 31 | # 32 | # fv.version_string_parts = ["Version 1.010", "[abcd123]", "metadata string"] 33 | # fv.write_version_string(fontpath="testfiles/Test-VersionShaMeta.ttf") 34 | # 35 | # fv.version_string_parts = ["Version 1.010", "[abcd123]-dev"] 36 | # fv.write_version_string(fontpath="testfiles/Test-VersionShaDEV.ttf") 37 | # 38 | # fv.version_string_parts = ["Version 1.010", "[abcd123]-release"] 39 | # fv.write_version_string(fontpath="testfiles/Test-VersionShaREL.ttf") 40 | # 41 | # fv.version_string_parts = ["Version 1.010", "metadata string"] 42 | # fv.write_version_string(fontpath="testfiles/Test-VersionMeta.ttf") 43 | # 44 | # fv.version_string_parts = ["Version 1.010", "metadata string", "another metadata string"] 45 | # fv.write_version_string(fontpath="testfiles/Test-VersionMoreMeta.ttf") 46 | # 47 | # fv.version_string_parts = ["Version 1.010", "DEV", "metadata string"] 48 | # fv.write_version_string(fontpath="testfiles/Test-VersionDEVMeta.ttf") 49 | # 50 | # fv.version_string_parts = ["Version 1.010", "RELEASE", "metadata string"] 51 | # fv.write_version_string(fontpath="testfiles/Test-VersionRELMeta.ttf") 52 | # 53 | # fv.version_string_parts = ["Version 1.010", "[abcd123]-dev", "metadata string"] 54 | # fv.write_version_string(fontpath="testfiles/Test-VersionShaDEVMeta.ttf") 55 | # 56 | # fv.version_string_parts = ["Version 1.010", "[abcd123]-release", "metadata string"] 57 | # fv.write_version_string(fontpath="testfiles/Test-VersionShaRELMeta.ttf") 58 | 59 | 60 | # Test file version strings (ttf shown, otf with same paths include the same version strings) 61 | 62 | # Test-VersionDEV.ttf: 63 | # Version 1.010;DEV 64 | 65 | # Test-VersionDEVMeta.ttf: 66 | # Version 1.010;DEV;metadata string 67 | 68 | # Test-VersionMeta.ttf: 69 | # Version 1.010;metadata string 70 | 71 | # Test-VersionMoreMeta.ttf: 72 | # Version 1.010;metadata string;another metadata string 73 | 74 | # Test-VersionOnly.ttf: 75 | # Version 1.010 76 | 77 | # Test-VersionREL.ttf: 78 | # Version 1.010;RELEASE 79 | 80 | # Test-VersionRELMeta.ttf: 81 | # Version 1.010;RELEASE;metadata string 82 | 83 | # Test-VersionSha.ttf: 84 | # Version 1.010;[abcd123] 85 | 86 | # Test-VersionShaMeta.ttf: 87 | # Version 1.010;[abcd123];metadata string 88 | 89 | # Test-VersionShaDEV.ttf: 90 | # Version 1.010;[abcd123]-dev 91 | 92 | # Test-VersionShaDEVMeta.ttf: 93 | # Version 1.010;[abcd123]-dev;metadata string 94 | 95 | # Test-VersionShaREL.ttf: 96 | # Version 1.010;[abcd123]-release 97 | 98 | # Test-VersionShaRELMeta.ttf: 99 | # Version 1.010;[abcd123]-release;metadata string 100 | 101 | all_testfiles_list = [ 102 | "tests/testfiles/Test-VersionDEV.ttf", 103 | "tests/testfiles/Test-VersionDEVMeta.ttf", 104 | "tests/testfiles/Test-VersionMeta.ttf", 105 | "tests/testfiles/Test-VersionMoreMeta.ttf", 106 | "tests/testfiles/Test-VersionOnly.ttf", 107 | "tests/testfiles/Test-VersionREL.ttf", 108 | "tests/testfiles/Test-VersionRELMeta.ttf", 109 | "tests/testfiles/Test-VersionSha.ttf", 110 | "tests/testfiles/Test-VersionShaMeta.ttf", 111 | "tests/testfiles/Test-VersionShaDEV.ttf", 112 | "tests/testfiles/Test-VersionShaDEVMeta.ttf", 113 | "tests/testfiles/Test-VersionShaREL.ttf", 114 | "tests/testfiles/Test-VersionShaRELMeta.ttf", 115 | "tests/testfiles/Test-VersionDEV.otf", 116 | "tests/testfiles/Test-VersionDEVMeta.otf", 117 | "tests/testfiles/Test-VersionMeta.otf", 118 | "tests/testfiles/Test-VersionMoreMeta.otf", 119 | "tests/testfiles/Test-VersionOnly.otf", 120 | "tests/testfiles/Test-VersionREL.otf", 121 | "tests/testfiles/Test-VersionRELMeta.otf", 122 | "tests/testfiles/Test-VersionSha.otf", 123 | "tests/testfiles/Test-VersionShaMeta.otf", 124 | "tests/testfiles/Test-VersionShaDEV.otf", 125 | "tests/testfiles/Test-VersionShaDEVMeta.otf", 126 | "tests/testfiles/Test-VersionShaREL.otf", 127 | "tests/testfiles/Test-VersionShaRELMeta.otf", 128 | ] 129 | 130 | meta_testfiles_list = [ 131 | "tests/testfiles/Test-VersionMeta.ttf", 132 | "tests/testfiles/Test-VersionMoreMeta.ttf", 133 | "tests/testfiles/Test-VersionMeta.otf", 134 | "tests/testfiles/Test-VersionMoreMeta.otf", 135 | ] 136 | 137 | dev_testfiles_list = [ 138 | "tests/testfiles/Test-VersionDEV.ttf", 139 | "tests/testfiles/Test-VersionDEVMeta.ttf", 140 | "tests/testfiles/Test-VersionShaDEV.ttf", 141 | "tests/testfiles/Test-VersionShaDEVMeta.ttf", 142 | "tests/testfiles/Test-VersionDEV.otf", 143 | "tests/testfiles/Test-VersionDEVMeta.otf", 144 | "tests/testfiles/Test-VersionShaDEV.otf", 145 | "tests/testfiles/Test-VersionShaDEVMeta.otf", 146 | ] 147 | 148 | rel_testfiles_list = [ 149 | "tests/testfiles/Test-VersionREL.ttf", 150 | "tests/testfiles/Test-VersionRELMeta.ttf", 151 | "tests/testfiles/Test-VersionShaREL.ttf", 152 | "tests/testfiles/Test-VersionShaRELMeta.ttf", 153 | "tests/testfiles/Test-VersionREL.otf", 154 | "tests/testfiles/Test-VersionRELMeta.otf", 155 | "tests/testfiles/Test-VersionShaREL.otf", 156 | "tests/testfiles/Test-VersionShaRELMeta.otf", 157 | ] 158 | 159 | state_testfiles_list = [ 160 | "tests/testfiles/Test-VersionSha.ttf", 161 | "tests/testfiles/Test-VersionShaMeta.ttf", 162 | "tests/testfiles/Test-VersionShaDEV.ttf", 163 | "tests/testfiles/Test-VersionShaREL.ttf", 164 | "tests/testfiles/Test-VersionShaDEVMeta.ttf", 165 | "tests/testfiles/Test-VersionShaRELMeta.ttf", 166 | "tests/testfiles/Test-VersionSha.otf", 167 | "tests/testfiles/Test-VersionShaMeta.otf", 168 | "tests/testfiles/Test-VersionShaDEV.otf", 169 | "tests/testfiles/Test-VersionShaREL.otf", 170 | "tests/testfiles/Test-VersionShaDEVMeta.otf", 171 | "tests/testfiles/Test-VersionShaRELMeta.otf", 172 | ] 173 | 174 | # pytest fixtures for parametrized testing of various groupings of test files 175 | 176 | 177 | @pytest.fixture(params=all_testfiles_list) 178 | def allfonts(request): 179 | return request.param 180 | 181 | 182 | @pytest.fixture(params=meta_testfiles_list) 183 | def metafonts(request): 184 | return request.param 185 | 186 | 187 | @pytest.fixture(params=dev_testfiles_list) 188 | def devfonts(request): 189 | return request.param 190 | 191 | 192 | @pytest.fixture(params=rel_testfiles_list) 193 | def relfonts(request): 194 | return request.param 195 | 196 | 197 | @pytest.fixture(params=state_testfiles_list) 198 | def statefonts(request): 199 | return request.param 200 | 201 | 202 | # utilities for testing 203 | def _test_hexadecimal_sha1_formatted_string_matches(needle): 204 | p = re.compile(r"""\[[(a-f|0-9)]{7,15}\]""") 205 | m = p.match(needle) 206 | if m is None: 207 | return False 208 | else: 209 | return True 210 | 211 | 212 | def _test_hexadecimal_sha1_string_matches(needle): 213 | p = re.compile("""[(a-f|0-9)]{7,15}""") 214 | m = p.match(needle) 215 | if m is None: 216 | return False 217 | else: 218 | return True 219 | 220 | 221 | def _get_mock_missing_nameid5_ttfont(filepath): 222 | ttf = TTFont(filepath) 223 | record_list = [] 224 | for record in ttf["name"].names: 225 | if record.nameID == 5: 226 | pass 227 | else: 228 | record_list.append(record) 229 | ttf["name"].names = record_list 230 | 231 | return ttf 232 | 233 | 234 | # TESTS 235 | 236 | # 237 | # 238 | # BEGIN FontVersion INSTANTIATION TESTS 239 | # 240 | # 241 | 242 | 243 | def test_libfv_missing_file_read_attempt(): 244 | with pytest.raises(IOError): 245 | fv = FontVersion("tests/testfiles/bogus.ttf") 246 | 247 | 248 | def test_libfv_nonfont_file_read_attempt(): 249 | with pytest.raises(TTLibError): 250 | fv = FontVersion("tests/testfiles/test.txt") 251 | 252 | 253 | def test_libfv_mocked_missing_name_tables_attempt(): 254 | with pytest.raises(IndexError): 255 | ttf = _get_mock_missing_nameid5_ttfont("tests/testfiles/Test-VersionOnly.ttf") 256 | fv = FontVersion(ttf) 257 | 258 | 259 | def test_libfv_fontversion_obj_instantiation_with_filepath_string(allfonts): 260 | fv = FontVersion(allfonts) 261 | 262 | 263 | def test_libfv_fontversion_obj_instantiation_with_ttfont_object(allfonts): 264 | ttf = TTFont(allfonts) 265 | fv1 = FontVersion(ttf) 266 | fv2 = FontVersion(allfonts) 267 | assert fv1.fontpath == fv2.fontpath 268 | assert fv1.version_string_parts == fv2.version_string_parts 269 | assert fv1.develop_string == fv2.develop_string 270 | assert fv1.release_string == fv2.release_string 271 | assert fv1.sha1_develop == fv2.sha1_develop 272 | assert fv1.sha1_release == fv2.sha1_release 273 | assert fv1.version == fv2.version 274 | assert fv1.metadata == fv2.metadata 275 | assert fv1.contains_status == fv2.contains_status 276 | assert fv1.contains_metadata == fv2.contains_metadata 277 | assert fv1.is_release == fv2.is_release 278 | assert fv1.is_development == fv2.is_development 279 | 280 | 281 | def test_libfv_version_string_property_set_on_instantiation(allfonts): 282 | fv = FontVersion(allfonts) 283 | assert fv.version == "Version 1.010" 284 | 285 | 286 | def test_libfv_version_string_property_set_on_instantiation_ttfont_object(allfonts): 287 | ttf = TTFont(allfonts) 288 | fv = FontVersion(ttf) 289 | assert fv.version == "Version 1.010" 290 | 291 | 292 | def test_libfv_head_fontrevision_property_set_on_instantiation(allfonts): 293 | fv = FontVersion(allfonts) 294 | assert math.isclose(fv.head_fontRevision, 1.010, abs_tol=0.00001) 295 | 296 | 297 | def test_libfv_head_fontrevision_property_set_on_instantiation_ttfont_object(allfonts): 298 | ttf = TTFont(allfonts) 299 | fv = FontVersion(ttf) 300 | assert math.isclose(fv.head_fontRevision, 1.010, abs_tol=0.00001) 301 | 302 | 303 | def test_libfv_fontversion_object_parameter_properties_defaults(allfonts): 304 | fv = FontVersion(allfonts) 305 | assert fv.develop_string == "DEV" 306 | assert fv.release_string == "RELEASE" 307 | assert fv.sha1_develop == "-dev" 308 | assert fv.sha1_release == "-release" 309 | 310 | 311 | def test_libfv_fontversion_object_parameter_properties_defaults_ttfont_object(allfonts): 312 | ttf = TTFont(allfonts) 313 | fv = FontVersion(ttf) 314 | assert fv.develop_string == "DEV" 315 | assert fv.release_string == "RELEASE" 316 | assert fv.sha1_develop == "-dev" 317 | assert fv.sha1_release == "-release" 318 | 319 | 320 | def test_libfv_fontversion_object_properties_truth_defaults(): 321 | fv1 = FontVersion("tests/testfiles/Test-VersionOnly.ttf") 322 | fv2 = FontVersion("tests/testfiles/Test-VersionOnly.otf") 323 | assert fv1.contains_metadata is False 324 | assert fv1.contains_status is False 325 | assert fv1.is_development is False 326 | assert fv1.is_release is False 327 | 328 | assert fv2.contains_metadata is False 329 | assert fv2.contains_status is False 330 | assert fv2.is_development is False 331 | assert fv2.is_release is False 332 | 333 | 334 | def test_libfv_fontversion_object_properties_truth_defaults_ttfont_object(): 335 | ttf1 = TTFont("tests/testfiles/Test-VersionOnly.ttf") 336 | fv1 = FontVersion(ttf1) 337 | ttf2 = TTFont("tests/testfiles/Test-VersionOnly.otf") 338 | fv2 = FontVersion(ttf2) 339 | 340 | assert fv1.contains_metadata is False 341 | assert fv1.contains_status is False 342 | assert fv1.is_development is False 343 | assert fv1.is_release is False 344 | 345 | assert fv2.contains_metadata is False 346 | assert fv2.contains_status is False 347 | assert fv2.is_development is False 348 | assert fv2.is_release is False 349 | 350 | 351 | def test_libfv_fontversion_object_properties_truth_defaults_with_metaonly(metafonts): 352 | fv = FontVersion(metafonts) 353 | assert fv.contains_metadata is True 354 | assert fv.contains_status is False 355 | assert fv.is_development is False 356 | assert fv.is_release is False 357 | 358 | 359 | def test_libfv_fontversion_object_properties_truth_defaults_with_metaonly_ttfont_object( 360 | metafonts, 361 | ): 362 | ttf = TTFont(metafonts) 363 | fv = FontVersion(ttf) 364 | assert fv.contains_metadata is True 365 | assert fv.contains_status is False 366 | assert fv.is_development is False 367 | assert fv.is_release is False 368 | 369 | 370 | def test_libfv_fontversion_object_properties_truth_development(devfonts): 371 | fv = FontVersion(devfonts) 372 | assert fv.contains_metadata is True 373 | assert fv.contains_status is True 374 | assert fv.is_development is True 375 | assert fv.is_release is False 376 | 377 | 378 | def test_libfv_fontversion_object_properties_truth_development_ttfont_object(devfonts): 379 | ttf = TTFont(devfonts) 380 | fv = FontVersion(ttf) 381 | assert fv.contains_metadata is True 382 | assert fv.contains_status is True 383 | assert fv.is_development is True 384 | assert fv.is_release is False 385 | 386 | 387 | def test_libfv_fontversion_object_properties_truth_release(relfonts): 388 | fv = FontVersion(relfonts) 389 | assert fv.contains_metadata is True 390 | assert fv.contains_status is True 391 | assert fv.is_development is False 392 | assert fv.is_release is True 393 | 394 | 395 | def test_libfv_fontversion_object_properties_truth_release_ttfont_object(relfonts): 396 | ttf = TTFont(relfonts) 397 | fv = FontVersion(ttf) 398 | assert fv.contains_metadata is True 399 | assert fv.contains_status is True 400 | assert fv.is_development is False 401 | assert fv.is_release is True 402 | 403 | 404 | def test_libfv_fontversion_object_properties_truth_sha(statefonts): 405 | fv = FontVersion(statefonts) 406 | assert fv.contains_state is True 407 | assert len(fv.state) > 0 408 | 409 | 410 | def test_libfv_fontversion_object_properties_truth_sha_ttfont_object(statefonts): 411 | ttf = TTFont(statefonts) 412 | fv = FontVersion(ttf) 413 | assert fv.contains_state is True 414 | assert len(fv.state) > 0 415 | 416 | 417 | def test_libfv_fontversion_object_properties_truth_state_versionstring_only(): 418 | fv1 = FontVersion("tests/testfiles/Test-VersionOnly.ttf") 419 | fv2 = FontVersion("tests/testfiles/Test-VersionOnly.otf") 420 | 421 | assert fv1.contains_state is False 422 | assert fv2.contains_state is False 423 | 424 | assert len(fv1.state) == 0 425 | assert len(fv2.state) == 0 426 | 427 | 428 | def test_libfv_fontversion_object_properties_truth_state_meta_without_state(): 429 | fv1 = FontVersion("tests/testfiles/Test-VersionMeta.ttf") 430 | fv2 = FontVersion("tests/testfiles/Test-VersionMeta.otf") 431 | fv3 = FontVersion("tests/testfiles/Test-VersionMoreMeta.ttf") 432 | fv4 = FontVersion("tests/testfiles/Test-VersionMoreMeta.otf") 433 | 434 | assert fv1.contains_state is False 435 | assert fv2.contains_state is False 436 | assert fv3.contains_state is False 437 | assert fv4.contains_state is False 438 | 439 | assert len(fv1.state) == 0 440 | assert len(fv2.state) == 0 441 | assert len(fv3.state) == 0 442 | assert len(fv4.state) == 0 443 | 444 | 445 | def test_libfv_fontversion_object_versionparts_meta_lists_versionstring_only(): 446 | fv1 = FontVersion("tests/testfiles/Test-VersionOnly.ttf") 447 | fv2 = FontVersion("tests/testfiles/Test-VersionOnly.otf") 448 | 449 | assert len(fv1.version_string_parts) == 1 450 | assert len(fv1.metadata) == 0 451 | 452 | assert len(fv2.version_string_parts) == 1 453 | assert len(fv2.metadata) == 0 454 | 455 | 456 | def test_libfv_fontversion_object_versionparts_meta_lists_versionstring_only_ttfont_object(): 457 | ttf1 = TTFont("tests/testfiles/Test-VersionOnly.ttf") 458 | fv1 = FontVersion(ttf1) 459 | ttf2 = TTFont("tests/testfiles/Test-VersionOnly.otf") 460 | fv2 = FontVersion(ttf2) 461 | 462 | assert len(fv1.version_string_parts) == 1 463 | assert len(fv1.metadata) == 0 464 | 465 | assert len(fv2.version_string_parts) == 1 466 | assert len(fv2.metadata) == 0 467 | 468 | 469 | def test_libfv_fontversion_object_versionparts_meta_lists_version_with_onemeta(): 470 | fv1 = FontVersion("tests/testfiles/Test-VersionMeta.ttf") 471 | assert len(fv1.version_string_parts) == 2 472 | assert fv1.version_string_parts[0] == "Version 1.010" 473 | assert fv1.version_string_parts[1] == "metadata string" 474 | assert len(fv1.metadata) == 1 475 | assert fv1.metadata[0] == "metadata string" 476 | 477 | fv2 = FontVersion("tests/testfiles/Test-VersionMeta.otf") 478 | assert len(fv2.version_string_parts) == 2 479 | assert fv2.version_string_parts[0] == "Version 1.010" 480 | assert fv2.version_string_parts[1] == "metadata string" 481 | assert len(fv2.metadata) == 1 482 | assert fv2.metadata[0] == "metadata string" 483 | 484 | 485 | def test_libfv_fontversion_object_versionparts_meta_lists_version_with_onemeta_ttfont_object(): 486 | ttf1 = TTFont("tests/testfiles/Test-VersionMeta.ttf") 487 | 488 | fv1 = FontVersion(ttf1) 489 | assert len(fv1.version_string_parts) == 2 490 | assert fv1.version_string_parts[0] == "Version 1.010" 491 | assert fv1.version_string_parts[1] == "metadata string" 492 | assert len(fv1.metadata) == 1 493 | assert fv1.metadata[0] == "metadata string" 494 | 495 | ttf2 = TTFont("tests/testfiles/Test-VersionMeta.otf") 496 | 497 | fv2 = FontVersion(ttf2) 498 | assert len(fv2.version_string_parts) == 2 499 | assert fv2.version_string_parts[0] == "Version 1.010" 500 | assert fv2.version_string_parts[1] == "metadata string" 501 | assert len(fv2.metadata) == 1 502 | assert fv2.metadata[0] == "metadata string" 503 | 504 | 505 | def test_libfv_fontversion_object_versionparts_meta_lists_version_with_twometa(): 506 | fv = FontVersion("tests/testfiles/Test-VersionMoreMeta.ttf") 507 | assert len(fv.version_string_parts) == 3 508 | assert fv.version_string_parts[0] == "Version 1.010" 509 | assert fv.version_string_parts[1] == "metadata string" 510 | assert fv.version_string_parts[2] == "another metadata string" 511 | assert len(fv.metadata) == 2 512 | assert fv.metadata[0] == "metadata string" 513 | assert fv.metadata[1] == "another metadata string" 514 | 515 | fv2 = FontVersion("tests/testfiles/Test-VersionMoreMeta.otf") 516 | assert len(fv2.version_string_parts) == 3 517 | assert fv2.version_string_parts[0] == "Version 1.010" 518 | assert fv2.version_string_parts[1] == "metadata string" 519 | assert fv2.version_string_parts[2] == "another metadata string" 520 | assert len(fv2.metadata) == 2 521 | assert fv2.metadata[0] == "metadata string" 522 | assert fv2.metadata[1] == "another metadata string" 523 | 524 | 525 | def test_libfv_fontversion_object_versionparts_meta_lists_version_with_twometa_ttfont_object(): 526 | ttf1 = TTFont("tests/testfiles/Test-VersionMoreMeta.ttf") 527 | 528 | fv1 = FontVersion(ttf1) 529 | assert len(fv1.version_string_parts) == 3 530 | assert fv1.version_string_parts[0] == "Version 1.010" 531 | assert fv1.version_string_parts[1] == "metadata string" 532 | assert fv1.version_string_parts[2] == "another metadata string" 533 | assert len(fv1.metadata) == 2 534 | assert fv1.metadata[0] == "metadata string" 535 | assert fv1.metadata[1] == "another metadata string" 536 | 537 | ttf2 = TTFont("tests/testfiles/Test-VersionMoreMeta.otf") 538 | 539 | fv2 = FontVersion(ttf2) 540 | assert len(fv2.version_string_parts) == 3 541 | assert fv2.version_string_parts[0] == "Version 1.010" 542 | assert fv2.version_string_parts[1] == "metadata string" 543 | assert fv2.version_string_parts[2] == "another metadata string" 544 | assert len(fv2.metadata) == 2 545 | assert fv2.metadata[0] == "metadata string" 546 | assert fv2.metadata[1] == "another metadata string" 547 | 548 | 549 | # 550 | # 551 | # END FontVersion INSTANTIATION TESTS 552 | # 553 | # 554 | 555 | # 556 | # 557 | # BEGIN FontVersion METHOD TESTS 558 | # 559 | # 560 | 561 | 562 | def test_libfv_fontversion_object_str_method(allfonts): 563 | fv = FontVersion(allfonts) 564 | test_string = fv.__str__() 565 | assert test_string.startswith(" ") is True 566 | assert fv.get_name_id5_version_string() in test_string 567 | assert fv.fontpath in test_string 568 | 569 | 570 | def test_libfv_fontversion_object_equality(allfonts): 571 | fv1 = FontVersion(allfonts) 572 | fv2 = FontVersion(allfonts) 573 | fv3 = FontVersion(allfonts) 574 | fv3.version_string_parts[0] = "Version 12.000" 575 | assert fv1 == fv2 576 | assert (fv1 == fv3) is False 577 | assert (fv1 == "test string") is False 578 | assert (fv1 == fv1.version_string_parts) is False 579 | 580 | 581 | def test_libfv_fontversion_object_inequality(allfonts): 582 | fv1 = FontVersion(allfonts) 583 | fv2 = FontVersion(allfonts) 584 | fv3 = FontVersion(allfonts) 585 | fv3.version_string_parts[0] = "Version 12.000" 586 | assert (fv1 != fv2) is False 587 | assert fv1 != fv3 588 | assert fv1 != "test string" 589 | assert fv1 != fv1.version_string_parts 590 | 591 | 592 | def test_libfv_clear_metadata_method(allfonts): 593 | fv = FontVersion(allfonts) 594 | fv.clear_metadata() 595 | assert len(fv.version_string_parts) == 1 596 | assert fv.version_string_parts[0] == "Version 1.010" 597 | 598 | 599 | def test_libfv_get_head_fontrevision_method(allfonts): 600 | fv = FontVersion(allfonts) 601 | assert math.isclose( 602 | fv.get_head_fontrevision_version_number(), 1.010, abs_tol=0.00001 603 | ) 604 | 605 | 606 | def test_libfv_get_metadata_method(): 607 | fv1 = FontVersion("tests/testfiles/Test-VersionOnly.ttf") 608 | fv2 = FontVersion("tests/testfiles/Test-VersionMeta.ttf") 609 | fv3 = FontVersion("tests/testfiles/Test-VersionMoreMeta.ttf") 610 | assert fv1.get_metadata_list() == [] 611 | assert fv2.get_metadata_list() == ["metadata string"] 612 | assert fv3.get_metadata_list() == ["metadata string", "another metadata string"] 613 | 614 | fv4 = FontVersion("tests/testfiles/Test-VersionOnly.otf") 615 | fv5 = FontVersion("tests/testfiles/Test-VersionMeta.otf") 616 | fv6 = FontVersion("tests/testfiles/Test-VersionMoreMeta.otf") 617 | assert fv4.get_metadata_list() == [] 618 | assert fv5.get_metadata_list() == ["metadata string"] 619 | assert fv6.get_metadata_list() == ["metadata string", "another metadata string"] 620 | 621 | 622 | def test_libfv_get_status_method_onlyversion(): 623 | fv = FontVersion("tests/testfiles/Test-VersionOnly.ttf") 624 | status_string = fv.get_state_status_substring() 625 | assert status_string == "" 626 | 627 | 628 | def test_libfv_get_status_method_development(devfonts): 629 | fv = FontVersion(devfonts) 630 | status_string = fv.get_state_status_substring() 631 | assert status_string == fv.version_string_parts[1] 632 | 633 | 634 | def test_libfv_get_status_method_release(relfonts): 635 | fv = FontVersion(relfonts) 636 | status_string = fv.get_state_status_substring() 637 | assert status_string == fv.version_string_parts[1] 638 | 639 | 640 | def test_libfv_get_status_method_nostatus(metafonts): 641 | fv = FontVersion(metafonts) 642 | status_string = fv.get_state_status_substring() 643 | assert status_string == "" 644 | 645 | 646 | def test_libfv_is_state_substring_return_match_valid(): 647 | fv = FontVersion("tests/testfiles/Test-VersionOnly.ttf") 648 | 649 | is_state_substring, state_substring = fv._is_state_substring_return_state_match( 650 | "[abcd123]" 651 | ) 652 | assert is_state_substring is True 653 | assert state_substring == "abcd123" 654 | 655 | is_state_substring, state_substring = fv._is_state_substring_return_state_match( 656 | "[abcd123]-dev" 657 | ) 658 | assert is_state_substring is True 659 | assert state_substring == "abcd123" 660 | 661 | is_state_substring, state_substring = fv._is_state_substring_return_state_match( 662 | "[abcd123]-release" 663 | ) 664 | assert is_state_substring is True 665 | assert state_substring == "abcd123" 666 | 667 | 668 | def test_libfv_is_state_substring_return_match_invalid(): 669 | fv = FontVersion("tests/testfiles/Test-VersionOnly.ttf") 670 | 671 | is_state_substring, state_substring = fv._is_state_substring_return_state_match( 672 | "abcd123" 673 | ) 674 | assert is_state_substring is False 675 | assert state_substring == "" 676 | 677 | is_state_substring, state_substring = fv._is_state_substring_return_state_match( 678 | "{abcd123}" 679 | ) 680 | assert is_state_substring is False 681 | assert state_substring == "" 682 | 683 | is_state_substring, state_substring = fv._is_state_substring_return_state_match( 684 | "[&%$#@!]" 685 | ) 686 | assert is_state_substring is False 687 | assert state_substring == "" 688 | 689 | 690 | def test_libfv_get_version_number_string(allfonts): 691 | fv = FontVersion(allfonts) 692 | assert fv.get_version_number_string() == "1.010" 693 | 694 | 695 | def test_libfv_get_version_number_string_bad_version_number(): 696 | fv = FontVersion("tests/testfiles/Test-VersionOnly.ttf") 697 | 698 | with pytest.raises(ValueError): 699 | # mock a bad version number substring 700 | fv.set_version_number("x.xxx") 701 | 702 | assert fv.get_version_number_string() == "" 703 | 704 | 705 | def test_libfv_get_version_number_tuple(): 706 | fv = FontVersion("tests/testfiles/Test-VersionOnly.ttf") 707 | assert fv.get_version_number_tuple() == (1, 0, 1, 0) 708 | 709 | # mock new version numbers in memory and confirm that they are correct in tuples 710 | fv.version = "Version 1.1" 711 | assert fv.get_version_number_tuple() == (1, 1) 712 | fv.version = "Version 1.01" 713 | assert fv.get_version_number_tuple() == (1, 0, 1) 714 | fv.version = "Version 1.001" 715 | assert fv.get_version_number_tuple() == (1, 0, 0, 1) 716 | fv.version = "Version 10.1" 717 | assert fv.get_version_number_tuple() == (10, 1) 718 | fv.version = "Version 10.01" 719 | assert fv.get_version_number_tuple() == (10, 0, 1) 720 | fv.version = "Version 10.001" 721 | assert fv.get_version_number_tuple() == (10, 0, 0, 1) 722 | fv.version = "Version 100.001" 723 | assert fv.get_version_number_tuple() == (100, 0, 0, 1) 724 | 725 | 726 | def test_libfv_get_version_number_tuple_bad_version_number(): 727 | fv = FontVersion("tests/testfiles/Test-VersionOnly.ttf") 728 | assert fv.get_version_number_tuple() == (1, 0, 1, 0) 729 | 730 | with pytest.raises(ValueError): 731 | # mock a bad version number substring 732 | fv.set_version_number("x.xxx") 733 | 734 | assert fv.get_version_number_tuple() is None 735 | 736 | 737 | def test_libfv_get_name_id5_version_string_method(): 738 | fv1 = FontVersion("tests/testfiles/Test-VersionOnly.ttf") 739 | fv2 = FontVersion("tests/testfiles/Test-VersionMeta.ttf") 740 | fv3 = FontVersion("tests/testfiles/Test-VersionMoreMeta.ttf") 741 | assert fv1.get_name_id5_version_string() == "Version 1.010" 742 | assert fv2.get_name_id5_version_string() == "Version 1.010;metadata string" 743 | assert ( 744 | fv3.get_name_id5_version_string() 745 | == "Version 1.010;metadata string;another metadata string" 746 | ) 747 | 748 | fv4 = FontVersion("tests/testfiles/Test-VersionOnly.otf") 749 | fv5 = FontVersion("tests/testfiles/Test-VersionMeta.otf") 750 | fv6 = FontVersion("tests/testfiles/Test-VersionMoreMeta.otf") 751 | assert fv4.get_name_id5_version_string() == "Version 1.010" 752 | assert fv5.get_name_id5_version_string() == "Version 1.010;metadata string" 753 | assert ( 754 | fv6.get_name_id5_version_string() 755 | == "Version 1.010;metadata string;another metadata string" 756 | ) 757 | 758 | 759 | def test_libfv_set_development_method_on_versiononly(): 760 | fv = FontVersion("tests/testfiles/Test-VersionOnly.ttf") 761 | assert len(fv.version_string_parts) == 1 762 | fv.set_development_status() 763 | assert len(fv.version_string_parts) == 2 764 | assert fv.version_string_parts[0] == "Version 1.010" 765 | assert fv.version_string_parts[1] == "DEV" 766 | assert fv.is_development is True 767 | assert fv.is_release is False 768 | assert fv.contains_status is True 769 | assert fv.contains_metadata is True 770 | 771 | fv2 = FontVersion("tests/testfiles/Test-VersionOnly.otf") 772 | assert len(fv2.version_string_parts) == 1 773 | fv2.set_development_status() 774 | assert len(fv2.version_string_parts) == 2 775 | assert fv2.version_string_parts[0] == "Version 1.010" 776 | assert fv2.version_string_parts[1] == "DEV" 777 | assert fv2.is_development is True 778 | assert fv2.is_release is False 779 | assert fv2.contains_status is True 780 | assert fv2.contains_metadata is True 781 | 782 | 783 | def test_libfv_set_development_method_on_release(relfonts): 784 | fv = FontVersion(relfonts) 785 | prelength = len(fv.version_string_parts) 786 | fv.set_development_status() 787 | postlength = len(fv.version_string_parts) 788 | assert prelength == postlength 789 | assert fv.version_string_parts[0] == "Version 1.010" 790 | assert fv.version_string_parts[1] == "DEV" 791 | assert fv.is_development is True 792 | assert fv.is_release is False 793 | assert fv.contains_status is True 794 | assert fv.contains_metadata is True 795 | 796 | 797 | def test_libfv_set_development_method_on_development(devfonts): 798 | fv = FontVersion(devfonts) 799 | prelength = len(fv.version_string_parts) 800 | fv.set_development_status() 801 | postlength = len(fv.version_string_parts) 802 | assert prelength == postlength 803 | assert fv.version_string_parts[0] == "Version 1.010" 804 | assert fv.version_string_parts[1] == "DEV" 805 | assert fv.is_development is True 806 | assert fv.is_release is False 807 | assert fv.contains_status is True 808 | assert fv.contains_metadata is True 809 | 810 | 811 | def test_libfv_set_development_method_on_nostatus(metafonts): 812 | fv = FontVersion(metafonts) 813 | prelength = len(fv.version_string_parts) 814 | fv.set_development_status() 815 | postlength = len(fv.version_string_parts) 816 | assert prelength == ( 817 | postlength - 1 818 | ) # should add an additional substring to the version string here 819 | assert fv.version_string_parts[0] == "Version 1.010" 820 | assert fv.version_string_parts[1] == "DEV" 821 | assert fv.is_development is True 822 | assert fv.is_release is False 823 | assert fv.contains_status is True 824 | assert fv.contains_metadata is True 825 | 826 | 827 | def test_libfv_set_release_method_on_versiononly(): 828 | fv = FontVersion("tests/testfiles/Test-VersionOnly.ttf") 829 | assert len(fv.version_string_parts) == 1 830 | fv.set_release_status() 831 | assert len(fv.version_string_parts) == 2 832 | assert fv.version_string_parts[0] == "Version 1.010" 833 | assert fv.version_string_parts[1] == "RELEASE" 834 | assert fv.is_development is False 835 | assert fv.is_release is True 836 | assert fv.contains_status is True 837 | assert fv.contains_metadata is True 838 | 839 | fv2 = FontVersion("tests/testfiles/Test-VersionOnly.otf") 840 | assert len(fv2.version_string_parts) == 1 841 | fv2.set_release_status() 842 | assert len(fv2.version_string_parts) == 2 843 | assert fv2.version_string_parts[0] == "Version 1.010" 844 | assert fv2.version_string_parts[1] == "RELEASE" 845 | assert fv2.is_development is False 846 | assert fv2.is_release is True 847 | assert fv2.contains_status is True 848 | assert fv2.contains_metadata is True 849 | 850 | 851 | def test_libfv_set_release_method_on_release(relfonts): 852 | fv = FontVersion(relfonts) 853 | prelength = len(fv.version_string_parts) 854 | fv.set_release_status() 855 | postlength = len(fv.version_string_parts) 856 | assert prelength == postlength 857 | assert fv.version_string_parts[0] == "Version 1.010" 858 | assert fv.version_string_parts[1] == "RELEASE" 859 | assert fv.is_development is False 860 | assert fv.is_release is True 861 | assert fv.contains_status is True 862 | assert fv.contains_metadata is True 863 | 864 | 865 | def test_libfv_set_release_method_on_development(devfonts): 866 | fv = FontVersion(devfonts) 867 | prelength = len(fv.version_string_parts) 868 | fv.set_release_status() 869 | postlength = len(fv.version_string_parts) 870 | assert prelength == postlength 871 | assert fv.version_string_parts[0] == "Version 1.010" 872 | assert fv.version_string_parts[1] == "RELEASE" 873 | assert fv.is_development is False 874 | assert fv.is_release is True 875 | assert fv.contains_status is True 876 | assert fv.contains_metadata is True 877 | 878 | 879 | def test_libfv_set_release_method_on_nostatus(metafonts): 880 | fv = FontVersion(metafonts) 881 | prelength = len(fv.version_string_parts) 882 | fv.set_release_status() 883 | postlength = len(fv.version_string_parts) 884 | assert prelength == ( 885 | postlength - 1 886 | ) # should add an additional substring to the version string here 887 | assert fv.version_string_parts[0] == "Version 1.010" 888 | assert fv.version_string_parts[1] == "RELEASE" 889 | assert fv.is_development is False 890 | assert fv.is_release is True 891 | assert fv.contains_status is True 892 | assert fv.contains_metadata is True 893 | 894 | 895 | def test_libfv_set_gitsha1_bad_parameters_raises_valueerror(allfonts): 896 | with pytest.raises(ValueError): 897 | fv = FontVersion(allfonts) 898 | fv.set_state_git_commit_sha1(development=True, release=True) 899 | 900 | 901 | def test_libfv_set_default_gitsha1_method(allfonts): 902 | fv = FontVersion(allfonts) 903 | fv.set_state_git_commit_sha1() 904 | sha_needle = fv.version_string_parts[1] 905 | assert ( 906 | _test_hexadecimal_sha1_formatted_string_matches(sha_needle) is True 907 | ) # confirm that set with state label 908 | assert ( 909 | _test_hexadecimal_sha1_string_matches(fv.state) is True 910 | ) # confirm that state property is properly set 911 | assert ("-dev" in sha_needle) is False 912 | assert ("-release" in sha_needle) is False 913 | assert ("DEV" in sha_needle) is False 914 | assert ("RELEASE" in sha_needle) is False 915 | 916 | 917 | def test_libfv_set_development_gitsha1_method(allfonts): 918 | fv = FontVersion(allfonts) 919 | fv.set_state_git_commit_sha1(development=True) 920 | sha_needle = fv.version_string_parts[1] 921 | assert ( 922 | _test_hexadecimal_sha1_formatted_string_matches(sha_needle) is True 923 | ) # confirm that set with state label 924 | assert ( 925 | _test_hexadecimal_sha1_string_matches(fv.state) is True 926 | ) # confirm that state property is properly set 927 | assert ("-dev" in sha_needle) is True 928 | assert ("-release" in sha_needle) is False 929 | assert ("DEV" in sha_needle) is False 930 | assert ("RELEASE" in sha_needle) is False 931 | 932 | 933 | def test_libfv_set_release_gitsha1_method(allfonts): 934 | fv = FontVersion(allfonts) 935 | fv.set_state_git_commit_sha1(release=True) 936 | sha_needle = fv.version_string_parts[1] 937 | assert ( 938 | _test_hexadecimal_sha1_formatted_string_matches(sha_needle) is True 939 | ) # confirm that set with state label 940 | assert ( 941 | _test_hexadecimal_sha1_string_matches(fv.state) is True 942 | ) # confirm that state property is properly set 943 | assert ("-dev" in sha_needle) is False 944 | assert ("-release" in sha_needle) is True 945 | assert ("DEV" in sha_needle) is False 946 | assert ("RELEASE" in sha_needle) is False 947 | 948 | 949 | def test_libfv_set_gitsha1_both_dev_release_error(capsys): 950 | fv = FontVersion("tests/testfiles/Test-VersionMeta.ttf") 951 | with pytest.raises(ValueError) as pytest_wrapped_e: 952 | fv.set_state_git_commit_sha1(release=True, development=True) 953 | 954 | out, err = capsys.readouterr() 955 | assert pytest_wrapped_e.type == ValueError 956 | 957 | 958 | def test_libfv_set_version_number(allfonts): 959 | fv = FontVersion(allfonts) 960 | prelength = len(fv.version_string_parts) 961 | fv.set_version_number("2.000") 962 | postlength = len(fv.version_string_parts) 963 | assert prelength == postlength 964 | assert fv.version_string_parts[0] == "Version 2.000" 965 | assert fv.version == "Version 2.000" 966 | assert fv.head_fontRevision == 2.000 967 | 968 | 969 | def test_libfv_set_version_number_invalid_number(allfonts): 970 | fv = FontVersion(allfonts) 971 | 972 | with pytest.raises(ValueError): 973 | # mock a bad version number substring 974 | fv.set_version_number("x.xxx") 975 | response = fv.get_version_number_string() 976 | assert len(response) == 0 977 | 978 | 979 | def test_libfv_set_version_string_one_substring(): 980 | fv = FontVersion("tests/testfiles/Test-VersionOnly.ttf") 981 | fv.set_version_string("Version 2.000") 982 | assert len(fv.version_string_parts) == 1 983 | assert fv.version_string_parts[0] == "Version 2.000" 984 | assert fv.version == "Version 2.000" 985 | assert fv.head_fontRevision == 2.000 986 | 987 | fv2 = FontVersion("tests/testfiles/Test-VersionOnly.otf") 988 | fv2.set_version_string("Version 2.000") 989 | assert len(fv2.version_string_parts) == 1 990 | assert fv2.version_string_parts[0] == "Version 2.000" 991 | assert fv2.version == "Version 2.000" 992 | assert fv2.head_fontRevision == 2.000 993 | 994 | 995 | def test_libfv_set_version_string_two_substrings(): 996 | fv = FontVersion("tests/testfiles/Test-VersionOnly.ttf") 997 | fv.set_version_string("Version 2.000;DEV") 998 | assert len(fv.version_string_parts) == 2 999 | assert fv.version_string_parts[0] == "Version 2.000" 1000 | assert fv.version_string_parts[1] == "DEV" 1001 | assert fv.version == "Version 2.000" 1002 | assert fv.head_fontRevision == 2.000 1003 | 1004 | fv2 = FontVersion("tests/testfiles/Test-VersionOnly.otf") 1005 | fv2.set_version_string("Version 2.000;DEV") 1006 | assert len(fv2.version_string_parts) == 2 1007 | assert fv2.version_string_parts[0] == "Version 2.000" 1008 | assert fv2.version_string_parts[1] == "DEV" 1009 | assert fv.version == "Version 2.000" 1010 | assert fv.head_fontRevision == 2.000 1011 | 1012 | 1013 | def test_libfv_set_version_string_three_substrings(): 1014 | fv = FontVersion("tests/testfiles/Test-VersionOnly.ttf") 1015 | fv.set_version_string("Version 2.000;DEV;other stuff") 1016 | assert len(fv.version_string_parts) == 3 1017 | assert fv.version_string_parts[0] == "Version 2.000" 1018 | assert fv.version_string_parts[1] == "DEV" 1019 | assert fv.version_string_parts[2] == "other stuff" 1020 | assert fv.version == "Version 2.000" 1021 | assert fv.head_fontRevision == 2.000 1022 | 1023 | fv2 = FontVersion("tests/testfiles/Test-VersionOnly.otf") 1024 | fv2.set_version_string("Version 2.000;DEV;other stuff") 1025 | assert len(fv2.version_string_parts) == 3 1026 | assert fv2.version_string_parts[0] == "Version 2.000" 1027 | assert fv2.version_string_parts[1] == "DEV" 1028 | assert fv2.version_string_parts[2] == "other stuff" 1029 | assert fv2.version == "Version 2.000" 1030 | assert fv2.head_fontRevision == 2.000 1031 | 1032 | 1033 | def test_libfv_write_version_string_method(allfonts): 1034 | temp_out_file_path = os.path.join( 1035 | "tests", "testfiles", "Test-Temp.ttf" 1036 | ) # temp file write path 1037 | fv = FontVersion(allfonts) 1038 | fv.set_version_number("2.000") 1039 | fv.write_version_string(fontpath=temp_out_file_path) 1040 | fv2 = FontVersion(temp_out_file_path) 1041 | assert fv2.version_string_parts[0] == "Version 2.000" 1042 | assert fv2.version == "Version 2.000" 1043 | assert fv2.head_fontRevision == 2.000 1044 | # modify again to test write to same temp file path without use of the fontpath parameter in 1045 | # order to test the block of code where that is handled 1046 | fv2.set_version_number("3.000") 1047 | fv2.write_version_string() 1048 | fv3 = FontVersion(temp_out_file_path) 1049 | assert fv3.version_string_parts[0] == "Version 3.000" 1050 | assert fv3.version == "Version 3.000" 1051 | assert fv3.head_fontRevision == 3.000 1052 | 1053 | os.remove(temp_out_file_path) 1054 | 1055 | 1056 | def test_libfv_write_version_string_method_ttfont_object(allfonts): 1057 | temp_out_file_path = os.path.join( 1058 | "tests", "testfiles", "Test-Temp.ttf" 1059 | ) # temp file write path 1060 | ttf = TTFont(allfonts) 1061 | fv = FontVersion(ttf) 1062 | fv.set_version_number("2.000") 1063 | fv.write_version_string(fontpath=temp_out_file_path) 1064 | fv2 = FontVersion(temp_out_file_path) 1065 | assert fv2.version_string_parts[0] == "Version 2.000" 1066 | assert fv2.version == "Version 2.000" 1067 | assert fv2.head_fontRevision == 2.000 1068 | # modify again to test write to same temp file path without use of the fontpath parameter in 1069 | # order to test the block of code where that is handled 1070 | fv2.set_version_number("3.000") 1071 | fv2.write_version_string() 1072 | fv3 = FontVersion(temp_out_file_path) 1073 | assert fv3.version_string_parts[0] == "Version 3.000" 1074 | assert fv3.version == "Version 3.000" 1075 | assert fv3.head_fontRevision == 3.000 1076 | 1077 | os.remove(temp_out_file_path) 1078 | --------------------------------------------------------------------------------