diff --git a/.yapfignore b/.yapfignore new file mode 100644 index 0000000..98a337c --- /dev/null +++ b/.yapfignore @@ -0,0 +1,6 @@ +# Virtual environments +venv +**/venv + +# Automatically generated nox files +.nox diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 939e534..31e0cd0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -22,6 +22,13 @@ use GitHub pull requests for this purpose. Consult [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more information on using pull requests. +Please run the nox tests before sending your code for review. You can do that +with: +```sh +pip install .[dev] +nox +``` + ## Community Guidelines This project follows [Google's Open Source Community diff --git a/build_golden_images.py b/build_golden_images.py index a4d8cdf..d403e67 100644 --- a/build_golden_images.py +++ b/build_golden_images.py @@ -22,6 +22,7 @@ import pkg_resources import pybadges + def generate_images(source_json_path, target_directory): os.makedirs(target_directory, exist_ok=True) with open(source_json_path) as f: @@ -34,15 +35,20 @@ def generate_images(source_json_path, target_directory): def main(): - parser = argparse.ArgumentParser(description='generate a github-style badge given some text and colors') + parser = argparse.ArgumentParser( + description='generate a github-style badge given some text and colors') - parser.add_argument('--source-path', - default=pkg_resources.resource_filename(__name__, 'tests/test-badges.json'), - help='the text to show on the left-hand-side of the badge') + parser.add_argument( + '--source-path', + default=pkg_resources.resource_filename(__name__, + 'tests/test-badges.json'), + help='the text to show on the left-hand-side of the badge') - parser.add_argument('--destination-dir', - default=pkg_resources.resource_filename(__name__, 'tests/golden-images'), - help='the text to show on the left-hand-side of the badge') + parser.add_argument( + '--destination-dir', + default=pkg_resources.resource_filename(__name__, + 'tests/golden-images'), + help='the text to show on the left-hand-side of the badge') args = parser.parse_args() generate_images(args.source_path, args.destination_dir) diff --git a/noxfile.py b/noxfile.py index 12a4738..cc10e47 100644 --- a/noxfile.py +++ b/noxfile.py @@ -11,20 +11,14 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """Nox config for running lint and unit tests.""" import nox def _run_tests(session): - session.run( - 'py.test', - '--quiet', - 'tests', - 'server-example', - *session.posargs - ) + session.run('py.test', '--quiet', 'tests', 'server-example', + *session.posargs) @nox.session @@ -33,10 +27,8 @@ def lint(session): Returns a failure if flake8 finds linting errors or sufficiently serious code quality issues. """ - session.install('flake8') - session.run('flake8', 'pybadges') - session.run('flake8', 'tests') - session.run('flake8', 'server-example') + session.install('yapf') + session.run('python3', '-m', 'yapf', '--diff', '-r', '.') @nox.session @@ -48,8 +40,9 @@ def unit(session): @nox.session(python=['3.4', '3.5', '3.6', '3.7', '3.8']) -@nox.parametrize('install', - ['Jinja2==2.9.0', 'Pillow==5.0.0', 'requests==2.9.0', 'xmldiff==2.4']) +@nox.parametrize( + 'install', + ['Jinja2==2.9.0', 'Pillow==5.0.0', 'requests==2.9.0', 'xmldiff==2.4']) def compatibility(session, install): """Run the unit test suite with each support library and Python version.""" @@ -64,8 +57,5 @@ def type_check(session): """Run type checking using pytype.""" session.install('-e', '.[dev]') session.install('pytype') - session.run( - 'pytype', - '--python-version=3.6', - '--disable=pyi-error', - 'pybadges') + session.run('pytype', '--python-version=3.6', '--disable=pyi-error', + 'pybadges') diff --git a/pybadges/__init__.py b/pybadges/__init__.py index ad889c2..e3c6d61 100644 --- a/pybadges/__init__.py +++ b/pybadges/__init__.py @@ -11,7 +11,6 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """Creates a github-style badge as a SVG image. This package seeks to generate semantically-identical output to the JavaScript @@ -44,7 +43,6 @@ from pybadges import text_measurer from pybadges import precalculated_text_measurer from pybadges.version import __version__ - _JINJA2_ENVIRONMENT = jinja2.Environment( trim_blocks=True, lstrip_blocks=True, @@ -77,6 +75,7 @@ def _remove_blanks(node): elif x.nodeType == minidom.Node.ELEMENT_NODE: _remove_blanks(x) + def _embed_image(url: str) -> str: parsed_url = urllib.parse.urlparse(url) @@ -90,8 +89,8 @@ def _embed_image(url: str) -> str: raise ValueError('no "Content-Type" header') content_type, image_type = content_type.split('/') if content_type != 'image': - raise ValueError('expected an image, got "{0}"'.format( - content_type)) + raise ValueError( + 'expected an image, got "{0}"'.format(content_type)) image_data = r.content elif parsed_url.scheme: raise ValueError('unsupported scheme "{0}"'.format(parsed_url.scheme)) @@ -113,16 +112,21 @@ def _embed_image(url: str) -> str: return 'data:image/{};base64,{}'.format(image_type, encoded_image) -def badge(left_text: str, right_text: str, left_link: Optional[str] = None, - right_link: Optional[str] = None, - whole_link: Optional[str] = None, logo: Optional[str] = None, - left_color: str = '#555', right_color: str = '#007ec6', - measurer: Optional[text_measurer.TextMeasurer] = None, - embed_logo: bool = False, - whole_title: Optional[str] = None, - left_title: Optional[str] = None, - right_title: Optional[str] = None, - ) -> str: +def badge( + left_text: str, + right_text: str, + left_link: Optional[str] = None, + right_link: Optional[str] = None, + whole_link: Optional[str] = None, + logo: Optional[str] = None, + left_color: str = '#555', + right_color: str = '#007ec6', + measurer: Optional[text_measurer.TextMeasurer] = None, + embed_logo: bool = False, + whole_title: Optional[str] = None, + left_title: Optional[str] = None, + right_title: Optional[str] = None, +) -> str: """Creates a github-style badge as an SVG image. >>> badge(left_text='coverage', right_text='23%', right_color='red') @@ -172,8 +176,7 @@ def badge(left_text: str, right_text: str, left_link: Optional[str] = None, """ if measurer is None: measurer = ( - precalculated_text_measurer.PrecalculatedTextMeasurer - .default()) + precalculated_text_measurer.PrecalculatedTextMeasurer.default()) if (left_link or right_link) and whole_link: raise ValueError( diff --git a/pybadges/__main__.py b/pybadges/__main__.py index d0fa722..faf6fb0 100644 --- a/pybadges/__main__.py +++ b/pybadges/__main__.py @@ -11,7 +11,6 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """Output a github-style badge as an SVG image given some text and colors. For more information, run: @@ -40,20 +39,19 @@ def main(): '--right-text', default='APACHE', help='the text to show on the right-hand-side of the badge') - parser.add_argument( - '--whole-link', - default=None, - help='the url to redirect to when the badge is clicked') + parser.add_argument('--whole-link', + default=None, + help='the url to redirect to when the badge is clicked') parser.add_argument( '--left-link', default=None, help='the url to redirect to when the left-hand of the badge is ' + - 'clicked') + 'clicked') parser.add_argument( '--right-link', default=None, help='the url to redirect to when the right-hand of the badge is ' + - 'clicked') + 'clicked') parser.add_argument( '--left-color', default='#555', @@ -73,68 +71,68 @@ def main(): const='yes', default='no', help='if the logo is specified then include the image data directly in ' - 'the badge (this will prevent a URL fetch and may work around the ' - 'fact that some browsers do not fetch external image references); ' - 'only works if --logo is a HTTP/HTTPS URI or a file path') - parser.add_argument( - '--browser', - action='store_true', - default=False, - help='display the badge in a browser tab') + 'the badge (this will prevent a URL fetch and may work around the ' + 'fact that some browsers do not fetch external image references); ' + 'only works if --logo is a HTTP/HTTPS URI or a file path') + parser.add_argument('--browser', + action='store_true', + default=False, + help='display the badge in a browser tab') parser.add_argument( '--use-pil-text-measurer', action='store_true', default=False, help='use the PilMeasurer to measure the length of text (kerning may ' - 'be more precise for non-Western languages. ' + - '--deja-vu-sans-path must also be set.') + 'be more precise for non-Western languages. ' + + '--deja-vu-sans-path must also be set.') parser.add_argument( '--deja-vu-sans-path', default=None, help='the path to the ttf font file containing DejaVu Sans. If not ' + - 'present on your system, you can download it from ' + - 'https://www.fontsquirrel.com/fonts/dejavu-sans') + 'present on your system, you can download it from ' + + 'https://www.fontsquirrel.com/fonts/dejavu-sans') parser.add_argument( '--whole-title', default=None, help='the title to associate with the entire badge. See ' - 'https://developer.mozilla.org/en-US/docs/Web/SVG/Element/title') + 'https://developer.mozilla.org/en-US/docs/Web/SVG/Element/title') parser.add_argument( '--left-title', default=None, help='the title to associate with the left part of the badge. See ' - 'https://developer.mozilla.org/en-US/docs/Web/SVG/Element/title') + 'https://developer.mozilla.org/en-US/docs/Web/SVG/Element/title') parser.add_argument( '--right-title', default=None, help='the title to associate with the right part of the badge. See ' - 'https://developer.mozilla.org/en-US/docs/Web/SVG/Element/title') + 'https://developer.mozilla.org/en-US/docs/Web/SVG/Element/title') parser.add_argument( - '-v', '--version', + '-v', + '--version', action='version', version='%(prog)s {version}'.format(version=__version__)) args = parser.parse_args() if (args.left_link or args.right_link) and args.whole_link: - print( - 'argument --whole-link: cannot be set with ' + - '--left-link or --right-link', - file=sys.stderr) + print('argument --whole-link: cannot be set with ' + + '--left-link or --right-link', + file=sys.stderr) sys.exit(1) measurer = None if args.use_pil_text_measurer: if args.deja_vu_sans_path is None: - print( - 'argument --use-pil-text-measurer: must also set ' + - '--deja-vu-sans-path', - file=sys.stderr) + print('argument --use-pil-text-measurer: must also set ' + + '--deja-vu-sans-path', + file=sys.stderr) sys.exit(1) from pybadges import pil_text_measurer measurer = pil_text_measurer.PilMeasurer(args.deja_vu_sans_path) - badge = pybadges.badge(left_text=args.left_text, right_text=args.right_text, - left_link=args.left_link, right_link=args.right_link, + badge = pybadges.badge(left_text=args.left_text, + right_text=args.right_text, + left_link=args.left_link, + right_link=args.right_link, whole_link=args.whole_link, left_color=args.left_color, right_color=args.right_color, diff --git a/pybadges/pil_text_measurer.py b/pybadges/pil_text_measurer.py index 3105a3c..6e04226 100644 --- a/pybadges/pil_text_measurer.py +++ b/pybadges/pil_text_measurer.py @@ -11,7 +11,6 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """Measure the width, in pixels, of a string rendered using DejaVu Sans 110pt. Uses a PIL/Pillow to determine the string length. diff --git a/pybadges/precalculate_text.py b/pybadges/precalculate_text.py index e8f537f..cac7c0a 100644 --- a/pybadges/precalculate_text.py +++ b/pybadges/precalculate_text.py @@ -11,7 +11,6 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """Creates a JSON file that can be used by precalculated_text_measurer.py. Creates a JSON file that can be used by @@ -106,8 +105,8 @@ def calculate_character_to_length_mapping( def calculate_pair_to_kern_mapping( - measurer: text_measurer.TextMeasurer, - char_to_length: Mapping[str, float], + measurer: text_measurer.TextMeasurer, char_to_length: Mapping[str, + float], characters: Iterable[str]) -> Mapping[str, float]: """Returns a mapping between each *pair* of characters and their kerning. @@ -147,16 +146,20 @@ def write_json(f: TextIO, deja_vu_sans_path: str, generate_supported_characters(deja_vu_sans_path)) kerning_characters = ''.join( generate_encodeable_characters(supported_characters, encodings)) - char_to_length = calculate_character_to_length_mapping(measurer, - supported_characters) + char_to_length = calculate_character_to_length_mapping( + measurer, supported_characters) pair_to_kerning = calculate_pair_to_kern_mapping(measurer, char_to_length, kerning_characters) json.dump( - {'mean-character-length': statistics.mean(char_to_length.values()), - 'character-lengths': char_to_length, - 'kerning-characters': kerning_characters, - 'kerning-pairs': pair_to_kerning}, - f, sort_keys=True, indent=1) + { + 'mean-character-length': statistics.mean(char_to_length.values()), + 'character-lengths': char_to_length, + 'kerning-characters': kerning_characters, + 'kerning-pairs': pair_to_kerning + }, + f, + sort_keys=True, + indent=1) def main(): @@ -167,8 +170,8 @@ def main(): '--deja-vu-sans-path', required=True, help='the path to the ttf font file containing DejaVu Sans. If not ' + - 'present on your system, you can download it from ' + - 'https://www.fontsquirrel.com/fonts/dejavu-sans') + 'present on your system, you can download it from ' + + 'https://www.fontsquirrel.com/fonts/dejavu-sans') parser.add_argument( '--kerning-pair-encodings', @@ -178,11 +181,10 @@ def main(): parser.add_argument( '--output-json-file', - default=os.path.join(os.path.dirname(__file__), - 'default-widths.json'), + default=os.path.join(os.path.dirname(__file__), 'default-widths.json'), help='the path where the generated JSON will be placed. If the ' + - 'provided filename extension ends with .xz then the output' + - 'will be compressed using lzma.') + 'provided filename extension ends with .xz then the output' + + 'will be compressed using lzma.') args = parser.parse_args() @@ -196,8 +198,8 @@ def main(): return open(args.output_json_file, 'wt') with create_file() as f: - write_json( - f, args.deja_vu_sans_path, measurer, args.kerning_pair_encodings) + write_json(f, args.deja_vu_sans_path, measurer, + args.kerning_pair_encodings) if __name__ == '__main__': diff --git a/pybadges/precalculated_text_measurer.py b/pybadges/precalculated_text_measurer.py index 4ccd9fd..6e769af 100644 --- a/pybadges/precalculated_text_measurer.py +++ b/pybadges/precalculated_text_measurer.py @@ -11,7 +11,6 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """Measure the width, in pixels, of a string rendered using DejaVu Sans 110pt. Uses a precalculated set of metrics to calculate the string length. diff --git a/pybadges/text_measurer.py b/pybadges/text_measurer.py index e821589..cd622a9 100644 --- a/pybadges/text_measurer.py +++ b/pybadges/text_measurer.py @@ -11,7 +11,6 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """Measure the width, in pixels, of a string rendered using DejaVu Sans 110pt. Contains only an abstract base class. diff --git a/pybadges/version.py b/pybadges/version.py index ae031a1..ab8773b 100644 --- a/pybadges/version.py +++ b/pybadges/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__='2.2.1' # Also change in setup.py. +__version__ = '2.3.0' # Also change in setup.py. diff --git a/server-example/app.py b/server-example/app.py index 70acd42..a6501c4 100644 --- a/server-example/app.py +++ b/server-example/app.py @@ -11,7 +11,6 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """ Example CI server that serves badges.""" from flask import Flask @@ -23,10 +22,9 @@ app = Flask(__name__) @app.route('/') def serveBadges(): - badge = pybadges.badge( - left_text='build', - right_text='passing', - right_color='#008000') + badge = pybadges.badge(left_text='build', + right_text='passing', + right_color='#008000') response = flask.make_response(badge) response.content_type = 'image/svg+xml' diff --git a/server-example/test_app.py b/server-example/test_app.py index 36587b4..3e7fd21 100644 --- a/server-example/test_app.py +++ b/server-example/test_app.py @@ -14,16 +14,17 @@ "Tests for app" - import pytest import app + @pytest.fixture def client(): with app.app.test_client() as client: yield client + def test_image(client): rv = client.get("/") assert b'build' in rv.data diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..494f106 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,2 @@ +[yapf] +based_on_style = google diff --git a/setup.py b/setup.py index 0ea31d3..0d8160b 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,6 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """A setup module for pybadges.""" import base64 @@ -19,6 +18,7 @@ import re from setuptools import setup + def get_long_description(): """Transform README.md into a usable long description. @@ -34,13 +34,12 @@ def get_long_description(): '%s?sanitize=true)' % svg_path) return re.sub(r'\(tests/golden-images/.*?\.svg\)', - replace_relative_with_absolute, - read_me) + replace_relative_with_absolute, read_me) setup( name='pybadges', - version= '2.2.1', # Also change in version.py. + version='2.3.0', # Also change in version.py. author='Brian Quinlan', author_email='brian@sweetapp.com', classifiers=[ @@ -57,13 +56,11 @@ setup( 'Operating System :: OS Independent', ], description='A library and command-line tool for generating Github-style ' + - 'badges', + 'badges', keywords="github gh-badges badge shield status", package_data={ 'pybadges': [ - 'badge-template-full.svg', - 'default-widths.json', - 'py.typed' + 'badge-template-full.svg', 'default-widths.json', 'py.typed' ] }, long_description=get_long_description(), @@ -72,8 +69,9 @@ setup( install_requires=['Jinja2>=2.9.0,<3', 'requests>=2.9.0,<3'], extras_require={ 'pil-measurement': ['Pillow>=5,<6'], - 'dev': ['fonttools>=3.26', 'nox', 'Pillow>=5', - 'pytest>=3.6', 'xmldiff>=2.4'], + 'dev': [ + 'fonttools>=3.26', 'nox', 'Pillow>=5', 'pytest>=3.6', 'xmldiff>=2.4' + ], }, license='Apache-2.0', packages=["pybadges"], diff --git a/tests/test_precalculated_text_measurer.py b/tests/test_precalculated_text_measurer.py index d6d6ec1..8beb96d 100644 --- a/tests/test_precalculated_text_measurer.py +++ b/tests/test_precalculated_text_measurer.py @@ -11,7 +11,6 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """Tests for PrecalculatedTextMeasurer.""" import unittest @@ -24,7 +23,10 @@ class TestPrecalculatedTextMeasurer(unittest.TestCase): def test_some_known_widths(self): measurer = precalculated_text_measurer.PrecalculatedTextMeasurer( default_character_width=5.1, - char_to_width={'H': 1.2, 'l': 1.3}, + char_to_width={ + 'H': 1.2, + 'l': 1.3 + }, pair_to_kern={}) text_width = measurer.text_width('Hello') @@ -32,32 +34,44 @@ class TestPrecalculatedTextMeasurer(unittest.TestCase): def test_kern_in_middle(self): measurer = precalculated_text_measurer.PrecalculatedTextMeasurer( - default_character_width=5, char_to_width={}, - pair_to_kern={'el': 3.3, 'll': 4.4, 'no': 5.5}) + default_character_width=5, + char_to_width={}, + pair_to_kern={ + 'el': 3.3, + 'll': 4.4, + 'no': 5.5 + }) text_width = measurer.text_width('Hello') self.assertAlmostEqual(text_width, 5 * 5 - 3.3 - 4.4) def test_kern_at_start(self): measurer = precalculated_text_measurer.PrecalculatedTextMeasurer( - default_character_width=5, char_to_width={}, - pair_to_kern={'He': 3.3, 'no': 4.4}) + default_character_width=5, + char_to_width={}, + pair_to_kern={ + 'He': 3.3, + 'no': 4.4 + }) text_width = measurer.text_width('Hello') self.assertAlmostEqual(text_width, 5 * 5 - 3.3) def test_kern_at_end(self): measurer = precalculated_text_measurer.PrecalculatedTextMeasurer( - default_character_width=5, char_to_width={}, - pair_to_kern={'lo': 3.3, 'no': 4.4}) + default_character_width=5, + char_to_width={}, + pair_to_kern={ + 'lo': 3.3, + 'no': 4.4 + }) text_width = measurer.text_width('Hello') self.assertAlmostEqual(text_width, 5 * 5 - 3.3) def test_default_usable(self): measurer = ( - precalculated_text_measurer.PrecalculatedTextMeasurer - .default()) + precalculated_text_measurer.PrecalculatedTextMeasurer.default()) measurer.text_width('This is a long string of text') diff --git a/tests/test_pybadges.py b/tests/test_pybadges.py index 3da2438..36691cf 100644 --- a/tests/test_pybadges.py +++ b/tests/test_pybadges.py @@ -11,7 +11,6 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """Tests for pybadges.""" import base64 @@ -42,7 +41,8 @@ class TestPybadgesBadge(unittest.TestCase): def test_whole_link_and_left_link(self): with self.assertRaises(ValueError): - pybadges.badge(left_text='foo', right_text='bar', + pybadges.badge(left_text='foo', + right_text='bar', left_link='http://example.com/', whole_link='http://example.com/') @@ -62,6 +62,7 @@ class TestPybadgesBadge(unittest.TestCase): diff = xmldiff.main.diff_texts(golden_image, pybadge_image) self.assertFalse(diff) + class TestEmbedImage(unittest.TestCase): """Tests for pybadges._embed_image."""