diff --git a/CHANGELOG.md b/CHANGELOG.md index 4790ddb4ec..5e8b0cce0f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ A more detailed list of changes is available in the corresponding milestones for ### Release notes - This release adds support for marking checks as **experimental**, which will make them not affect the process exit code and so will allow such checks to result in a FAIL without breaking continuous integration builds. This will give time for users to report problems on the implementation of new checks. If after some time nobody complains, then those new checks will more safely be made effective by removing their `experimental` tag. + - **Bugfix:** Display informative message when users forget to install the `extra` needed for the vendor-specific profile they're trying to run (issue #4292) + ### New checks #### Addded to the Google Fonts Profile - **EXPERIMENTAL - [com.google.fonts/check/metadata/empty_designer]:** Any font published on Google Fonts must credit one or several authors, foundry and/or individuals. (issue #3961) diff --git a/Lib/fontbakery/cli.py b/Lib/fontbakery/cli.py index 0b9d3cb8b4..6370b4d4e2 100644 --- a/Lib/fontbakery/cli.py +++ b/Lib/fontbakery/cli.py @@ -27,6 +27,9 @@ def run_profile_check(profilename): + from fontbakery.utils import set_profile_name + + set_profile_name(profilename) module = import_module(f"fontbakery.profiles.{profilename}") sys.exit(check_profile_main(module.profile)) diff --git a/Lib/fontbakery/profiles/fontval.py b/Lib/fontbakery/profiles/fontval.py index 0be90a7536..7b2a85229c 100644 --- a/Lib/fontbakery/profiles/fontval.py +++ b/Lib/fontbakery/profiles/fontval.py @@ -13,7 +13,7 @@ try: import lxml.etree except ImportError: - exit_with_install_instructions("fontval") + exit_with_install_instructions() profile_imports = [".shared_conditions"] profile = profile_factory( diff --git a/Lib/fontbakery/profiles/googlefonts.py b/Lib/fontbakery/profiles/googlefonts.py index 35939561ac..41d0ce219b 100644 --- a/Lib/fontbakery/profiles/googlefonts.py +++ b/Lib/fontbakery/profiles/googlefonts.py @@ -254,13 +254,14 @@ ) def com_google_fonts_check_canonical_filename(ttFont): """Checking file is named canonically.""" + try: from axisregistry import build_filename - - current_filename = os.path.basename(ttFont.reader.file.name) - expected_filename = build_filename(ttFont) except ImportError: - exit_with_install_instructions("googlefonts") + exit_with_install_instructions() + + current_filename = os.path.basename(ttFont.reader.file.name) + expected_filename = build_filename(ttFont) if current_filename != expected_filename: yield FAIL, Message( @@ -375,11 +376,14 @@ def com_google_fonts_check_description_urls(description_html): @condition def description_html(description): + try: + from lxml import etree + except ImportError: + exit_with_install_instructions() + if not description: return - from lxml import etree - html = "" + description + "" try: return etree.fromstring(html) @@ -444,8 +448,12 @@ def com_google_fonts_check_description_git_url(description_html): ) def com_google_fonts_check_description_valid_html(descfile, description): """Is this a proper HTML snippet?""" - passed = True + try: + from lxml import html + except ImportError: + exit_with_install_instructions() + passed = True if "" in description or "" in description: yield FAIL, Message( "html-tag", @@ -455,8 +463,6 @@ def com_google_fonts_check_description_valid_html(descfile, description): f" font family specimen webpage.", ) - from lxml import html - try: html.fromstring("" + description + "") except Exception as e: @@ -535,8 +541,11 @@ def com_google_fonts_check_description_eof_linebreak(description): ) def com_google_fonts_check_metadata_parses(family_directory): """Check METADATA.pb parse correctly.""" - from google.protobuf import text_format - from fontbakery.utils import get_FamilyProto_Message + try: + from google.protobuf import text_format + from fontbakery.utils import get_FamilyProto_Message + except ImportError: + exit_with_install_instructions() try: pb_file = os.path.join(family_directory, "METADATA.pb") @@ -1072,8 +1081,11 @@ def com_google_fonts_check_glyph_coverage( ttFont, font_codepoints, family_metadata, config ): """Check Google Fonts glyph coverage.""" - from glyphsets import GFGlyphData as glyph_data - import unicodedata2 + try: + from glyphsets import GFGlyphData as glyph_data + import unicodedata2 + except ImportError: + exit_with_install_instructions() def missing_encoded_glyphs(glyphs): encoded_glyphs = [g["unicode"] for g in glyphs if g["unicode"]] @@ -1139,8 +1151,11 @@ def com_google_fonts_check_metadata_unsupported_subsets( family_metadata, ttFont, font_codepoints ): """Check for METADATA subsets with zero support.""" - from glyphsets import codepoints - from glyphsets.subsets import SUBSETS + try: + from glyphsets import codepoints + from glyphsets.subsets import SUBSETS + except ImportError: + exit_with_install_instructions() codepoints.set_encoding_path(codepoints.nam_dir) @@ -1191,13 +1206,17 @@ def com_google_fonts_check_metadata_unreachable_subsetting( family_directory, font, ttFont, font_codepoints, config ): """Check for codepoints not covered by METADATA subsets.""" - from glyphsets import codepoints + try: + import unicodedata2 + from glyphsets import codepoints + except ImportError: + exit_with_install_instructions() + from fontbakery.utils import pretty_print_list from fontbakery.profiles.googlefonts_conditions import ( metadata_file, family_metadata, ) - import unicodedata2 codepoints.set_encoding_path(codepoints.nam_dir) @@ -3625,7 +3644,10 @@ def glyphs_surface_area(ttFont): @condition def uharfbuzz_blob(font): - import uharfbuzz as hb + try: + import uharfbuzz as hb + except ImportError: + exit_with_install_instructions() return hb.Blob.from_file_path(font) @@ -3647,8 +3669,12 @@ def uharfbuzz_blob(font): ) def com_google_fonts_check_slant_direction(ttFont, uharfbuzz_blob): """Checking direction of slnt axis angles""" + try: + import uharfbuzz as hb + except ImportError: + exit_with_install_instructions() + from fontbakery.utils import axis, PointsPen - import uharfbuzz as hb if not axis(ttFont, "slnt"): yield PASS, "Font has no slnt axis" @@ -3968,7 +3994,10 @@ def com_google_fonts_check_fontdata_namecheck(ttFont, familyname): ) def com_google_fonts_check_fontv(ttFont): """Check for font-v versioning.""" - from fontv.libfv import FontVersion + try: + from fontv.libfv import FontVersion + except ImportError: + exit_with_install_instructions() fv = FontVersion(ttFont) if fv.version and (fv.is_development or fv.is_release): @@ -6853,7 +6882,10 @@ def can_shape(ttFont, text, parameters=None): Returns true if the font can render a text string without any .notdef characters. """ - from vharfbuzz import Vharfbuzz + try: + from vharfbuzz import Vharfbuzz + except ImportError: + exit_with_install_instructions() filename = ttFont.reader.file.name vharfbuzz = Vharfbuzz(filename) @@ -6981,7 +7013,10 @@ def com_google_fonts_check_repo_sample_image(readme_contents, readme_directory, ) def com_google_fonts_check_metadata_can_render_samples(ttFont, family_metadata): """Check samples can be rendered.""" - from gflanguages import LoadLanguages + try: + from gflanguages import LoadLanguages + except ImportError: + exit_with_install_instructions() passed = True languages = LoadLanguages() diff --git a/Lib/fontbakery/profiles/googlefonts_conditions.py b/Lib/fontbakery/profiles/googlefonts_conditions.py index 8b5d66efd2..1ecb076cbb 100644 --- a/Lib/fontbakery/profiles/googlefonts_conditions.py +++ b/Lib/fontbakery/profiles/googlefonts_conditions.py @@ -184,7 +184,7 @@ def family_metadata(metadata_file): try: from google.protobuf import text_format except ImportError: - exit_with_install_instructions("googlefonts") + exit_with_install_instructions() from fontbakery.utils import get_FamilyProto_Message @@ -197,9 +197,13 @@ def family_metadata(metadata_file): @condition def registered_vendor_ids(): """Get a list of vendor IDs from Microsoft's website.""" - from bs4 import BeautifulSoup, NavigableString from pkg_resources import resource_filename + try: + from bs4 import BeautifulSoup, NavigableString + except ImportError: + exit_with_install_instructions() + registered_vendor_ids = {} CACHED = resource_filename( "fontbakery", "data/fontbakery-microsoft-vendorlist.cache" diff --git a/Lib/fontbakery/profiles/iso15008.py b/Lib/fontbakery/profiles/iso15008.py index 3bc8170e38..a6bee5fef3 100644 --- a/Lib/fontbakery/profiles/iso15008.py +++ b/Lib/fontbakery/profiles/iso15008.py @@ -16,7 +16,7 @@ try: import uharfbuzz as hb except ImportError: - exit_with_install_instructions("iso15008") + exit_with_install_instructions() profile = profile_factory(default_section=Section("Suitability for In-Car Display")) diff --git a/Lib/fontbakery/profiles/shaping.py b/Lib/fontbakery/profiles/shaping.py index 4e5aa39718..c6977eef0f 100644 --- a/Lib/fontbakery/profiles/shaping.py +++ b/Lib/fontbakery/profiles/shaping.py @@ -158,7 +158,7 @@ def run_a_set_of_shaping_tests( filename = Path(ttFont.reader.file.name) vharfbuzz = Vharfbuzz(filename) except ImportError: - exit_with_install_instructions("shaping") + exit_with_install_instructions() shaping_file_found = False ran_a_test = False @@ -408,7 +408,10 @@ def com_google_fonts_check_shaping_collides(config, ttFont): def setup_glyph_collides(ttFont, configuration): - from collidoscope import Collidoscope + try: + from collidoscope import Collidoscope + except ImportError: + exit_with_install_instructions() filename = Path(ttFont.reader.file.name) collidoscope_configuration = configuration.get("collidoscope") @@ -430,7 +433,10 @@ def setup_glyph_collides(ttFont, configuration): def run_collides_glyph_test( filename, vharfbuzz, test, configuration, failed_shaping_tests, extra_data ): - from stringbrewer import StringBrewer + try: + from stringbrewer import StringBrewer + except ImportError: + exit_with_install_instructions() col = extra_data["collidoscope"] is_stringbrewer = ( @@ -481,7 +487,10 @@ def collides_glyph_test_results(vharfbuzz, shaping_file, failed_shaping_tests): def is_complex_shaper_font(ttFont): - from ufo2ft.constants import INDIC_SCRIPTS, USE_SCRIPTS + try: + from ufo2ft.constants import INDIC_SCRIPTS, USE_SCRIPTS + except ImportError: + exit_with_install_instructions() for table in ["GSUB", "GPOS"]: if table not in ttFont: @@ -603,10 +612,14 @@ def find_mark_base(lookup, attachments): def com_google_fonts_check_soft_dotted(ttFont): """Ensure soft_dotted characters lose their dot when combined with marks that replace the dot.""" + try: + from vharfbuzz import Vharfbuzz + except ImportError: + exit_with_install_instructions() + import itertools from beziers.path import BezierPath from fontTools import unicodedata - from vharfbuzz import Vharfbuzz cmap = ttFont["cmap"].getBestCmap() diff --git a/Lib/fontbakery/profiles/typenetwork.py b/Lib/fontbakery/profiles/typenetwork.py index 8bf7ba76eb..d351203804 100755 --- a/Lib/fontbakery/profiles/typenetwork.py +++ b/Lib/fontbakery/profiles/typenetwork.py @@ -14,7 +14,12 @@ from fontbakery.profiles.universal import UNIVERSAL_PROFILE_CHECKS from fontbakery.section import Section from fontbakery.status import PASS, FAIL, WARN, SKIP, INFO -from fontbakery.utils import add_check_overrides, bullet_list, pretty_print_list +from fontbakery.utils import ( + add_check_overrides, + bullet_list, + exit_with_install_instructions, + pretty_print_list, +) from fontbakery.constants import ( NameID, PlatformID, @@ -354,7 +359,10 @@ ) def com_typenetwork_glyph_coverage(ttFont, font_codepoints, config): """Check Type Network minimum glyph coverage.""" - import unicodedata2 + try: + import unicodedata2 + except ImportError: + exit_with_install_instructions() TN_latin_set = { 0x0020: (" ", "SPACE"), diff --git a/Lib/fontbakery/profiles/ufo_sources.py b/Lib/fontbakery/profiles/ufo_sources.py index 40a6fb96cc..c45768bfac 100644 --- a/Lib/fontbakery/profiles/ufo_sources.py +++ b/Lib/fontbakery/profiles/ufo_sources.py @@ -25,14 +25,14 @@ @condition def ufo_font(ufo): - from fontTools.ufoLib.errors import UFOLibError - try: + from fontTools.ufoLib.errors import UFOLibError import defcon + except ImportError: + exit_with_install_instructions() + try: return defcon.Font(ufo) - except ImportError: - exit_with_install_instructions("ufo-sources") except UFOLibError: return None @@ -45,10 +45,13 @@ def designSpace(designspace): 'an object to read, write and edit interpolation systems for typefaces'. """ - if designspace: + try: from fontTools.designspaceLib import DesignSpaceDocument import defcon + except ImportError: + exit_with_install_instructions() + if designspace: DS = DesignSpaceDocument.fromfile(designspace) DS.loadSourceFonts(defcon.Font) return DS @@ -60,9 +63,12 @@ def designspace_sources(designSpace): Given a DesignSpaceDocument object, return a set of UFO font sources. """ - if designSpace: + try: import defcon + except ImportError: + exit_with_install_instructions() + if designSpace: return designSpace.loadSourceFonts(defcon.Font) diff --git a/Lib/fontbakery/utils.py b/Lib/fontbakery/utils.py index 1307f1e4c7..7224c37c71 100644 --- a/Lib/fontbakery/utils.py +++ b/Lib/fontbakery/utils.py @@ -24,12 +24,19 @@ from fontbakery.constants import NO_COLORS_THEME, DARK_THEME, LIGHT_THEME +profile_name = None -def exit_with_install_instructions(extras_name): + +def set_profile_name(name): + global profile_name + profile_name = name + + +def exit_with_install_instructions(): sys.exit( - f"\nTo run the {extras_name} profile, one needs to install\n" - f"fontbakery with the '{extras_name}' extra, like this:\n\n" - f" python -m pip install -U 'fontbakery[{extras_name}]'\n\n" + f"\nTo run the {profile_name} profile, one needs to install\n" + f"fontbakery with the '{profile_name}' extra, like this:\n\n" + f" python -m pip install -U 'fontbakery[{profile_name}]'\n\n" ) diff --git a/tests/test_utils.py b/tests/test_utils.py index 0e9b8ca56e..5111bde959 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -29,13 +29,16 @@ def test_exit_with_install_instructions(): - extras_name = "test-profile" + from fontbakery.utils import set_profile_name + + profile_name = "test-profile" + set_profile_name(profile_name) with patch("sys.exit") as mock_exit: - exit_with_install_instructions(extras_name) + exit_with_install_instructions() mock_exit.assert_called_with( - f"\nTo run the {extras_name} profile, one needs to install\n" - f"fontbakery with the '{extras_name}' extra, like this:\n\n" - f" python -m pip install -U 'fontbakery[{extras_name}]'\n\n" + f"\nTo run the {profile_name} profile, one needs to install\n" + f"fontbakery with the '{profile_name}' extra, like this:\n\n" + f" python -m pip install -U 'fontbakery[{profile_name}]'\n\n" )