Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package python-humanfriendly for openSUSE:Factory checked in at 2021-08-18 08:56:23 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-humanfriendly (Old) and /work/SRC/openSUSE:Factory/.python-humanfriendly.new.1899 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-humanfriendly" Wed Aug 18 08:56:23 2021 rev:15 rq:912717 version:9.2 Changes: -------- --- /work/SRC/openSUSE:Factory/python-humanfriendly/python-humanfriendly.changes 2020-04-23 18:37:20.900880987 +0200 +++ /work/SRC/openSUSE:Factory/.python-humanfriendly.new.1899/python-humanfriendly.changes 2021-08-18 08:57:14.074881593 +0200 @@ -1,0 +2,35 @@ +Tue Aug 17 10:58:03 UTC 2021 - John Paul Adrian Glaubitz <adrian.glaub...@suse.com> + +- Update to 9.2 + Maintenance release: + * Merged pull request `#46`_ which fixes several :pypi:`flake8` warnings. + * Merged pull request `#49`_ which marks Python 3.9 support final. + * Merged pull request `#51`_ which helps to stabilize the test suite. + * Merged pull request `#52`_ which updates the :mod:`humanfriendly.sphinx` + module to include Sphinx extension metadata that has become mandatory in a + recent Sphinx release. After merging the pull request I added additional + metadata including the version. +- from version 9.1 + * Added :func:`~humanfriendly.compat.on_macos()` function to detect Apple MacOS + (I need this in an upcoming :pypi:`coloredlogs` release and don't want to have + to think about how to detect MacOS again in the future ????). +- from version 9.0 + The major version number was bumped because the bug fix for + :func:`~humanfriendly.text.pluralize()` is backwards incompatible + and (even though this seems like very "cosmetic" functionality) + version numbers are cheap, so who cares ????. + **Bug fixes:** + * Changed :func:`~humanfriendly.format_number()` to properly support negative + numbers (as suggested in `issue #40`_). + * Changed :func:`~humanfriendly.text.pluralize()` to generate "1.5 seconds" + instead of "1.5 second" (as suggested in `issue #43`_). + **Enhancements:** + * Enhanced :func:`~humanfriendly.text.concatenate()` to support ``conjunction`` + and ``serial_comma`` keyword arguments (as suggested in `issue #30`_). + * Added :func:`~humanfriendly.text.pluralize_raw()` to select singular or + plural form without prefixing the count to the text that is returned. +- from version 8.2 + * Added a simple case insensitive dictionary implementation, for details refer to + the new :mod:`humanfriendly.case` module. + +------------------------------------------------------------------- Old: ---- humanfriendly-8.1.tar.gz New: ---- humanfriendly-9.2.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-humanfriendly.spec ++++++ --- /var/tmp/diff_new_pack.PfEhKm/_old 2021-08-18 08:57:14.542881042 +0200 +++ /var/tmp/diff_new_pack.PfEhKm/_new 2021-08-18 08:57:14.546881039 +0200 @@ -1,7 +1,7 @@ # -# spec file for package python +# spec file # -# Copyright (c) 2020 SUSE LLC +# Copyright (c) 2021 SUSE LLC # # All modifications and additions to the file contributed by third parties # remain the property of their copyright owners, unless otherwise agreed @@ -27,7 +27,7 @@ %bcond_without python2 %{?!python_module:%define python_module() python-%{**} python3-%{**}} Name: python-humanfriendly%{psuffix} -Version: 8.1 +Version: 9.2 Release: 0 Summary: Human friendly input/output for text interfaces using Python License: MIT @@ -38,7 +38,7 @@ BuildRequires: python-rpm-macros Requires: python Requires(post): update-alternatives -Requires(postun): update-alternatives +Requires(postun):update-alternatives BuildArch: noarch %if %{with test} BuildRequires: %{python_module capturer >= 2.1} ++++++ humanfriendly-8.1.tar.gz -> humanfriendly-9.2.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/humanfriendly-8.1/CHANGELOG.rst new/humanfriendly-9.2/CHANGELOG.rst --- old/humanfriendly-8.1/CHANGELOG.rst 2020-03-06 22:01:01.000000000 +0100 +++ new/humanfriendly-9.2/CHANGELOG.rst 2021-06-11 12:52:37.000000000 +0200 @@ -11,6 +11,74 @@ .. _Keep a Changelog: http://keepachangelog.com/ .. _semantic versioning: http://semver.org/ +`Release 9.2`_ (2021-06-11) +--------------------------- + +Maintenance release: + +- Merged pull request `#46`_ which fixes several :pypi:`flake8` warnings. + +- Merged pull request `#49`_ which marks Python 3.9 support final. + +- Merged pull request `#51`_ which helps to stabilize the test suite. + +- Merged pull request `#52`_ which updates the :mod:`humanfriendly.sphinx` + module to include Sphinx extension metadata that has become mandatory in a + recent Sphinx release. After merging the pull request I added additional + metadata including the version. + +.. _Release 9.2: https://github.com/xolox/python-humanfriendly/compare/9.1...9.2 +.. _#46: https://github.com/xolox/python-humanfriendly/pull/46 +.. _#49: https://github.com/xolox/python-humanfriendly/pull/49 +.. _#51: https://github.com/xolox/python-humanfriendly/pull/51 +.. _#52: https://github.com/xolox/python-humanfriendly/pull/52 + +`Release 9.1`_ (2020-12-10) +--------------------------- + +Added :func:`~humanfriendly.compat.on_macos()` function to detect Apple MacOS +(I need this in an upcoming :pypi:`coloredlogs` release and don't want to have +to think about how to detect MacOS again in the future ????). + +.. _Release 9.1: https://github.com/xolox/python-humanfriendly/compare/9.0...9.1 + +`Release 9.0`_ (2020-12-01) +--------------------------- + +The major version number was bumped because the bug fix for +:func:`~humanfriendly.text.pluralize()` is backwards incompatible +and (even though this seems like very "cosmetic" functionality) +version numbers are cheap, so who cares ????. + +**Bug fixes:** + +- Changed :func:`~humanfriendly.format_number()` to properly support negative + numbers (as suggested in `issue #40`_). + +- Changed :func:`~humanfriendly.text.pluralize()` to generate "1.5 seconds" + instead of "1.5 second" (as suggested in `issue #43`_). + +**Enhancements:** + +- Enhanced :func:`~humanfriendly.text.concatenate()` to support ``conjunction`` + and ``serial_comma`` keyword arguments (as suggested in `issue #30`_). + +- Added :func:`~humanfriendly.text.pluralize_raw()` to select singular or + plural form without prefixing the count to the text that is returned. + +.. _Release 9.0: https://github.com/xolox/python-humanfriendly/compare/8.2...9.0 +.. _issue #30: https://github.com/xolox/python-humanfriendly/issues/30 +.. _issue #40: https://github.com/xolox/python-humanfriendly/issues/40 +.. _issue #43: https://github.com/xolox/python-humanfriendly/issues/43 + +`Release 8.2`_ (2020-04-19) +--------------------------- + +Added a simple case insensitive dictionary implementation, for details refer to +the new :mod:`humanfriendly.case` module. + +.. _Release 8.2: https://github.com/xolox/python-humanfriendly/compare/8.1...8.2 + `Release 8.1`_ (2020-03-06) --------------------------- diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/humanfriendly-8.1/LICENSE.txt new/humanfriendly-9.2/LICENSE.txt --- old/humanfriendly-8.1/LICENSE.txt 2020-02-06 01:08:58.000000000 +0100 +++ new/humanfriendly-9.2/LICENSE.txt 2021-06-11 12:43:08.000000000 +0200 @@ -1,4 +1,4 @@ -Copyright (c) 2020 Peter Odding +Copyright (c) 2021 Peter Odding Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/humanfriendly-8.1/PKG-INFO new/humanfriendly-9.2/PKG-INFO --- old/humanfriendly-8.1/PKG-INFO 2020-03-06 22:01:19.000000000 +0100 +++ new/humanfriendly-9.2/PKG-INFO 2021-06-11 12:53:09.000000000 +0200 @@ -1,6 +1,6 @@ Metadata-Version: 1.2 Name: humanfriendly -Version: 8.1 +Version: 9.2 Summary: Human friendly output for text interfaces using Python Home-page: https://humanfriendly.readthedocs.io Author: Peter Odding @@ -152,7 +152,7 @@ This software is licensed under the `MIT license`_. - ?? 2020 Peter Odding. + ?? 2021 Peter Odding. .. External references: .. _#4: https://github.com/xolox/python-humanfriendly/issues/4 @@ -184,6 +184,7 @@ Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: Topic :: Communications diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/humanfriendly-8.1/README.rst new/humanfriendly-9.2/README.rst --- old/humanfriendly-8.1/README.rst 2020-02-16 20:22:55.000000000 +0100 +++ new/humanfriendly-9.2/README.rst 2021-06-11 12:43:08.000000000 +0200 @@ -144,7 +144,7 @@ This software is licensed under the `MIT license`_. -?? 2020 Peter Odding. +?? 2021 Peter Odding. .. External references: .. _#4: https://github.com/xolox/python-humanfriendly/issues/4 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/humanfriendly-8.1/docs/api.rst new/humanfriendly-9.2/docs/api.rst --- old/humanfriendly-8.1/docs/api.rst 2020-03-02 01:06:35.000000000 +0100 +++ new/humanfriendly-9.2/docs/api.rst 2020-04-19 02:22:19.000000000 +0200 @@ -34,6 +34,12 @@ .. automodule:: humanfriendly :members: +:mod:`humanfriendly.case` +------------------------ + +.. automodule:: humanfriendly.case + :members: + :mod:`humanfriendly.cli` ------------------------ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/humanfriendly-8.1/docs/conf.py new/humanfriendly-9.2/docs/conf.py --- old/humanfriendly-8.1/docs/conf.py 2020-03-01 15:16:58.000000000 +0100 +++ new/humanfriendly-9.2/docs/conf.py 2021-06-11 12:43:08.000000000 +0200 @@ -32,7 +32,7 @@ # General information about the project. project = 'humanfriendly' -copyright = '2020, Peter Odding' +copyright = '2021, Peter Odding' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/humanfriendly-8.1/humanfriendly/__init__.py new/humanfriendly-9.2/humanfriendly/__init__.py --- old/humanfriendly-8.1/humanfriendly/__init__.py 2020-03-06 22:01:01.000000000 +0100 +++ new/humanfriendly-9.2/humanfriendly/__init__.py 2021-06-11 12:52:37.000000000 +0200 @@ -1,7 +1,7 @@ # Human friendly input/output in Python. # # Author: Peter Odding <pe...@peterodding.com> -# Last Change: March 6, 2020 +# Last Change: June 11, 2021 # URL: https://humanfriendly.readthedocs.io """The main module of the `humanfriendly` package.""" @@ -51,7 +51,7 @@ ) # Semi-standard module versioning. -__version__ = '8.1' +__version__ = '9.2' # Named tuples to define units of size. SizeUnit = collections.namedtuple('SizeUnit', 'divider, symbol, name') @@ -353,7 +353,8 @@ 6,000,000,000 """ integer_part, _, decimal_part = str(float(number)).partition('.') - reversed_digits = ''.join(reversed(integer_part)) + negative_sign = integer_part.startswith('-') + reversed_digits = ''.join(reversed(integer_part.lstrip('-'))) parts = [] while reversed_digits: parts.append(reversed_digits[:3]) @@ -362,6 +363,8 @@ decimals_to_add = decimal_part[:num_decimals].rstrip('0') if decimals_to_add: formatted_number += '.' + decimals_to_add + if negative_sign: + formatted_number = '-' + formatted_number return formatted_number diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/humanfriendly-8.1/humanfriendly/case.py new/humanfriendly-9.2/humanfriendly/case.py --- old/humanfriendly-8.1/humanfriendly/case.py 1970-01-01 01:00:00.000000000 +0100 +++ new/humanfriendly-9.2/humanfriendly/case.py 2020-04-19 02:22:19.000000000 +0200 @@ -0,0 +1,157 @@ +# Human friendly input/output in Python. +# +# Author: Peter Odding <pe...@peterodding.com> +# Last Change: April 19, 2020 +# URL: https://humanfriendly.readthedocs.io + +""" +Simple case insensitive dictionaries. + +The :class:`CaseInsensitiveDict` class is a dictionary whose string keys +are case insensitive. It works by automatically coercing string keys to +:class:`CaseInsensitiveKey` objects. Keys that are not strings are +supported as well, just without case insensitivity. + +At its core this module works by normalizing strings to lowercase before +comparing or hashing them. It doesn't support proper case folding nor +does it support Unicode normalization, hence the word "simple". +""" + +# Standard library modules. +import collections + +try: + # Python >= 3.3. + from collections.abc import Iterable, Mapping +except ImportError: + # Python 2.7. + from collections import Iterable, Mapping + +# Modules included in our package. +from humanfriendly.compat import basestring, unicode + +# Public identifiers that require documentation. +__all__ = ("CaseInsensitiveDict", "CaseInsensitiveKey") + + +class CaseInsensitiveDict(collections.OrderedDict): + + """ + Simple case insensitive dictionary implementation (that remembers insertion order). + + This class works by overriding methods that deal with dictionary keys to + coerce string keys to :class:`CaseInsensitiveKey` objects before calling + down to the regular dictionary handling methods. While intended to be + complete this class has not been extensively tested yet. + """ + + def __init__(self, other=None, **kw): + """Initialize a :class:`CaseInsensitiveDict` object.""" + # Initialize our superclass. + super(CaseInsensitiveDict, self).__init__() + # Handle the initializer arguments. + self.update(other, **kw) + + def coerce_key(self, key): + """ + Coerce string keys to :class:`CaseInsensitiveKey` objects. + + :param key: The value to coerce (any type). + :returns: If `key` is a string then a :class:`CaseInsensitiveKey` + object is returned, otherwise the value of `key` is + returned unmodified. + """ + if isinstance(key, basestring): + key = CaseInsensitiveKey(key) + return key + + @classmethod + def fromkeys(cls, iterable, value=None): + """Create a case insensitive dictionary with keys from `iterable` and values set to `value`.""" + return cls((k, value) for k in iterable) + + def get(self, key, default=None): + """Get the value of an existing item.""" + return super(CaseInsensitiveDict, self).get(self.coerce_key(key), default) + + def pop(self, key, default=None): + """Remove an item from a case insensitive dictionary.""" + return super(CaseInsensitiveDict, self).pop(self.coerce_key(key), default) + + def setdefault(self, key, default=None): + """Get the value of an existing item or add a new item.""" + return super(CaseInsensitiveDict, self).setdefault(self.coerce_key(key), default) + + def update(self, other=None, **kw): + """Update a case insensitive dictionary with new items.""" + if isinstance(other, Mapping): + # Copy the items from the given mapping. + for key, value in other.items(): + self[key] = value + elif isinstance(other, Iterable): + # Copy the items from the given iterable. + for key, value in other: + self[key] = value + elif other is not None: + # Complain about unsupported values. + msg = "'%s' object is not iterable" + type_name = type(value).__name__ + raise TypeError(msg % type_name) + # Copy the keyword arguments (if any). + for key, value in kw.items(): + self[key] = value + + def __contains__(self, key): + """Check if a case insensitive dictionary contains the given key.""" + return super(CaseInsensitiveDict, self).__contains__(self.coerce_key(key)) + + def __delitem__(self, key): + """Delete an item in a case insensitive dictionary.""" + return super(CaseInsensitiveDict, self).__delitem__(self.coerce_key(key)) + + def __getitem__(self, key): + """Get the value of an item in a case insensitive dictionary.""" + return super(CaseInsensitiveDict, self).__getitem__(self.coerce_key(key)) + + def __setitem__(self, key, value): + """Set the value of an item in a case insensitive dictionary.""" + return super(CaseInsensitiveDict, self).__setitem__(self.coerce_key(key), value) + + +class CaseInsensitiveKey(unicode): + + """ + Simple case insensitive dictionary key implementation. + + The :class:`CaseInsensitiveKey` class provides an intentionally simple + implementation of case insensitive strings to be used as dictionary keys. + + If you need features like Unicode normalization or proper case folding + please consider using a more advanced implementation like the :pypi:`istr` + package instead. + """ + + def __new__(cls, value): + """Create a :class:`CaseInsensitiveKey` object.""" + # Delegate string object creation to our superclass. + obj = unicode.__new__(cls, value) + # Store the lowercased string and its hash value. + normalized = obj.lower() + obj._normalized = normalized + obj._hash_value = hash(normalized) + return obj + + def __hash__(self): + """Get the hash value of the lowercased string.""" + return self._hash_value + + def __eq__(self, other): + """Compare two strings as lowercase.""" + if isinstance(other, CaseInsensitiveKey): + # Fast path (and the most common case): Comparison with same type. + return self._normalized == other._normalized + elif isinstance(other, unicode): + # Slow path: Comparison with strings that need lowercasing. + return self._normalized == other.lower() + else: + return NotImplemented diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/humanfriendly-8.1/humanfriendly/compat.py new/humanfriendly-9.2/humanfriendly/compat.py --- old/humanfriendly-8.1/humanfriendly/compat.py 2020-03-01 19:23:29.000000000 +0100 +++ new/humanfriendly-9.2/humanfriendly/compat.py 2020-12-10 01:12:05.000000000 +0100 @@ -1,7 +1,7 @@ # Human friendly input/output in Python. # # Author: Peter Odding <pe...@peterodding.com> -# Last Change: March 1, 2020 +# Last Change: December 10, 2020 # URL: https://humanfriendly.readthedocs.io """ @@ -52,6 +52,7 @@ 'is_unicode', 'monotonic', 'name2codepoint', + 'on_macos', 'on_windows', 'unichr', 'unicode', @@ -132,6 +133,15 @@ return isinstance(value, unicode) +def on_macos(): + """ + Check if we're running on Apple MacOS. + + :returns: :data:`True` if running MacOS, :data:`False` otherwise. + """ + return sys.platform.startswith('darwin') + + def on_windows(): """ Check if we're running on the Microsoft Windows OS. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/humanfriendly-8.1/humanfriendly/sphinx.py new/humanfriendly-9.2/humanfriendly/sphinx.py --- old/humanfriendly-8.1/humanfriendly/sphinx.py 2020-03-01 20:01:16.000000000 +0100 +++ new/humanfriendly-9.2/humanfriendly/sphinx.py 2021-06-11 12:43:08.000000000 +0200 @@ -1,7 +1,7 @@ # Human friendly input/output in Python. # # Author: Peter Odding <pe...@peterodding.com> -# Last Change: March 1, 2020 +# Last Change: June 11, 2021 # URL: https://humanfriendly.readthedocs.io """ @@ -245,12 +245,16 @@ like that idea you may be better of calling the individual functions from your own ``setup()`` function. """ + from humanfriendly import __version__ + enable_deprecation_notes(app) enable_man_role(app) enable_pypi_role(app) enable_special_methods(app) enable_usage_formatting(app) + return dict(parallel_read_safe=True, parallel_write_safe=True, version=__version__) + def special_methods_callback(app, what, name, obj, skip, options): """ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/humanfriendly-8.1/humanfriendly/tests.py new/humanfriendly-9.2/humanfriendly/tests.py --- old/humanfriendly-8.1/humanfriendly/tests.py 2020-03-06 22:01:01.000000000 +0100 +++ new/humanfriendly-9.2/humanfriendly/tests.py 2021-06-11 12:43:08.000000000 +0200 @@ -4,7 +4,7 @@ # Tests for the `humanfriendly' package. # # Author: Peter Odding <peter.odd...@paylogic.eu> -# Last Change: March 6, 2020 +# Last Change: June 11, 2021 # URL: https://humanfriendly.readthedocs.io """Test suite for the `humanfriendly` package.""" @@ -44,6 +44,7 @@ prompts, round_number, ) +from humanfriendly.case import CaseInsensitiveDict, CaseInsensitiveKey from humanfriendly.cli import main from humanfriendly.compat import StringIO from humanfriendly.decorators import cached @@ -129,6 +130,60 @@ """Container for the `humanfriendly` test suite.""" + def test_case_insensitive_dict(self): + """Test the CaseInsensitiveDict class.""" + # Test support for the dict(iterable) signature. + assert len(CaseInsensitiveDict([('key', True), ('KEY', False)])) == 1 + # Test support for the dict(iterable, **kw) signature. + assert len(CaseInsensitiveDict([('one', True), ('ONE', False)], one=False, two=True)) == 2 + # Test support for the dict(mapping) signature. + assert len(CaseInsensitiveDict(dict(key=True, KEY=False))) == 1 + # Test support for the dict(mapping, **kw) signature. + assert len(CaseInsensitiveDict(dict(one=True, ONE=False), one=False, two=True)) == 2 + # Test support for the dict(**kw) signature. + assert len(CaseInsensitiveDict(one=True, ONE=False, two=True)) == 2 + # Test support for dict.fromkeys(). + obj = CaseInsensitiveDict.fromkeys(["One", "one", "ONE", "Two", "two", "TWO"]) + assert len(obj) == 2 + # Test support for dict.get(). + obj = CaseInsensitiveDict(existing_key=42) + assert obj.get('Existing_Key') == 42 + # Test support for dict.pop(). + obj = CaseInsensitiveDict(existing_key=42) + assert obj.pop('Existing_Key') == 42 + assert len(obj) == 0 + # Test support for dict.setdefault(). + obj = CaseInsensitiveDict(existing_key=42) + assert obj.setdefault('Existing_Key') == 42 + obj.setdefault('other_key', 11) + assert obj['Other_Key'] == 11 + # Test support for dict.__contains__(). + obj = CaseInsensitiveDict(existing_key=42) + assert 'Existing_Key' in obj + # Test support for dict.__delitem__(). + obj = CaseInsensitiveDict(existing_key=42) + del obj['Existing_Key'] + assert len(obj) == 0 + # Test support for dict.__getitem__(). + obj = CaseInsensitiveDict(existing_key=42) + assert obj['Existing_Key'] == 42 + # Test support for dict.__setitem__(). + obj = CaseInsensitiveDict(existing_key=42) + obj['Existing_Key'] = 11 + assert obj['existing_key'] == 11 + + def test_case_insensitive_key(self): + """Test the CaseInsensitiveKey class.""" + # Test the __eq__() special method. + polite = CaseInsensitiveKey("Please don't shout") + rude = CaseInsensitiveKey("PLEASE DON'T SHOUT") + assert polite == rude + # Test the __hash__() special method. + mapping = {} + mapping[polite] = 1 + mapping[rude] = 2 + assert len(mapping) == 1 + def test_capture_output(self): """Test the CaptureOutput class.""" with CaptureOutput() as capturer: @@ -386,7 +441,7 @@ # Make sure milliseconds are never shown separately when detailed=False. # https://github.com/xolox/python-humanfriendly/issues/10 assert '1 minute, 1 second and 100 milliseconds' == format_timespan(61.10, detailed=True) - assert '1 minute and 1.1 second' == format_timespan(61.10, detailed=False) + assert '1 minute and 1.1 seconds' == format_timespan(61.10, detailed=False) # Test for loss of precision as reported in issue 11: # https://github.com/xolox/python-humanfriendly/issues/11 assert '1 minute and 0.3 seconds' == format_timespan(60.300) @@ -520,6 +575,8 @@ self.assertEqual('1,000', format_number(1000.12, 0)) self.assertEqual('1,000,000', format_number(1000000)) self.assertEqual('1,000,000.42', format_number(1000000.42)) + # Regression test for https://github.com/xolox/python-humanfriendly/issues/40. + self.assertEqual('-285.67', format_number(-285.67)) def test_round_number(self): """Test :func:`humanfriendly.round_number()`.""" @@ -673,6 +730,10 @@ assert concatenate(['one']) == 'one' assert concatenate(['one', 'two']) == 'one and two' assert concatenate(['one', 'two', 'three']) == 'one, two and three' + # Test the 'conjunction' option. + assert concatenate(['one', 'two', 'three'], conjunction='or') == 'one, two or three' + # Test the 'serial_comma' option. + assert concatenate(['one', 'two', 'three'], serial_comma=True) == 'one, two, and three' def test_split(self): """Test :func:`humanfriendly.text.split()`.""" @@ -732,8 +793,8 @@ .replace(ANSI_HIDE_CURSOR, '')) lines = [line for line in output.split(ANSI_ERASE_LINE) if line] self.assertTrue(len(lines) > 0) - self.assertTrue(all('test spinner' in l for l in lines)) - self.assertTrue(all('%' in l for l in lines)) + self.assertTrue(all('test spinner' in line for line in lines)) + self.assertTrue(all('%' in line for line in lines)) self.assertEqual(sorted(set(lines)), sorted(lines)) def test_automatic_spinner(self): @@ -897,7 +958,7 @@ # https://github.com/xolox/python-humanfriendly/issues/28 returncode, output = run_cli(main, '--demo') assert returncode == 0 - lines = [ansi_strip(l) for l in output.splitlines()] + lines = [ansi_strip(line) for line in output.splitlines()] assert "Text styles:" in lines assert "Foreground colors:" in lines assert "Background colors:" in lines diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/humanfriendly-8.1/humanfriendly/text.py new/humanfriendly-9.2/humanfriendly/text.py --- old/humanfriendly-8.1/humanfriendly/text.py 2020-03-01 19:23:29.000000000 +0100 +++ new/humanfriendly-9.2/humanfriendly/text.py 2020-12-01 01:05:44.000000000 +0100 @@ -1,7 +1,7 @@ # Human friendly input/output in Python. # # Author: Peter Odding <pe...@peterodding.com> -# Last Change: March 1, 2020 +# Last Change: December 1, 2020 # URL: https://humanfriendly.readthedocs.io """ @@ -20,7 +20,6 @@ """ # Standard library modules. -import math import numbers import random import re @@ -38,6 +37,7 @@ 'is_empty_line', 'join_lines', 'pluralize', + 'pluralize_raw', 'random_string', 'split', 'split_paragraphs', @@ -94,20 +94,40 @@ return ''.join(lines) -def concatenate(items): +def concatenate(items, conjunction='and', serial_comma=False): """ Concatenate a list of items in a human friendly way. - :param items: A sequence of strings. - :returns: A single string. + :param items: + + A sequence of strings. + + :param conjunction: + + The word to use before the last item (a string, defaults to "and"). + + :param serial_comma: + + :data:`True` to use a `serial comma`_, :data:`False` otherwise + (defaults to :data:`False`). + + :returns: + + A single string. >>> from humanfriendly.text import concatenate >>> concatenate(["eggs", "milk", "bread"]) 'eggs, milk and bread' + + .. _serial comma: https://en.wikipedia.org/wiki/Serial_comma """ items = list(items) if len(items) > 1: - return ', '.join(items[:-1]) + ' and ' + items[-1] + final_item = items.pop() + formatted = ', '.join(items) + if serial_comma: + formatted += ',' + return ' '.join([formatted, conjunction, final_item]) elif items: return items[0] else: @@ -276,19 +296,36 @@ """ Combine a count with the singular or plural form of a word. - If the plural form of the word is not provided it is obtained by - concatenating the singular form of the word with the letter "s". Of course - this will not always be correct, which is why you have the option to - specify both forms. + :param count: The count (a number). + :param singular: The singular form of the word (a string). + :param plural: The plural form of the word (a string or :data:`None`). + :returns: The count and singular or plural word concatenated (a string). + + See :func:`pluralize_raw()` for the logic underneath :func:`pluralize()`. + """ + return '%s %s' % (count, pluralize_raw(count, singular, plural)) + + +def pluralize_raw(count, singular, plural=None): + """ + Select the singular or plural form of a word based on a count. :param count: The count (a number). :param singular: The singular form of the word (a string). :param plural: The plural form of the word (a string or :data:`None`). - :returns: The count and singular/plural word concatenated (a string). + :returns: The singular or plural form of the word (a string). + + When the given count is exactly 1.0 the singular form of the word is + selected, in all other cases the plural form of the word is selected. + + If the plural form of the word is not provided it is obtained by + concatenating the singular form of the word with the letter "s". Of course + this will not always be correct, which is why you have the option to + specify both forms. """ if not plural: plural = singular + 's' - return '%s %s' % (count, singular if math.floor(float(count)) == 1 else plural) + return singular if float(count) == 1.0 else plural def random_string(length=(25, 100), characters=string.ascii_letters): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/humanfriendly-8.1/humanfriendly/usage.py new/humanfriendly-9.2/humanfriendly/usage.py --- old/humanfriendly-8.1/humanfriendly/usage.py 2020-02-16 01:03:47.000000000 +0100 +++ new/humanfriendly-9.2/humanfriendly/usage.py 2021-06-11 12:43:08.000000000 +0200 @@ -1,7 +1,7 @@ # Human friendly input/output in Python. # # Author: Peter Odding <pe...@peterodding.com> -# Last Change: June 24, 2017 +# Last Change: June 11, 2021 # URL: https://humanfriendly.readthedocs.io """ @@ -258,7 +258,7 @@ ('\n\n'.join(render_paragraph(p, meta_variables) for p in split_paragraphs(description))).rstrip(), ]) csv_lines = csv_buffer.getvalue().splitlines() - output.append('\n'.join(' %s' % l for l in csv_lines)) + output.append('\n'.join(' %s' % line for line in csv_lines)) logger.debug("Rendered output: %s", output) return '\n\n'.join(trim_empty_lines(o) for o in output) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/humanfriendly-8.1/humanfriendly.egg-info/PKG-INFO new/humanfriendly-9.2/humanfriendly.egg-info/PKG-INFO --- old/humanfriendly-8.1/humanfriendly.egg-info/PKG-INFO 2020-03-06 22:01:19.000000000 +0100 +++ new/humanfriendly-9.2/humanfriendly.egg-info/PKG-INFO 2021-06-11 12:53:09.000000000 +0200 @@ -1,6 +1,6 @@ Metadata-Version: 1.2 Name: humanfriendly -Version: 8.1 +Version: 9.2 Summary: Human friendly output for text interfaces using Python Home-page: https://humanfriendly.readthedocs.io Author: Peter Odding @@ -152,7 +152,7 @@ This software is licensed under the `MIT license`_. - ?? 2020 Peter Odding. + ?? 2021 Peter Odding. .. External references: .. _#4: https://github.com/xolox/python-humanfriendly/issues/4 @@ -184,6 +184,7 @@ Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: Topic :: Communications diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/humanfriendly-8.1/humanfriendly.egg-info/SOURCES.txt new/humanfriendly-9.2/humanfriendly.egg-info/SOURCES.txt --- old/humanfriendly-8.1/humanfriendly.egg-info/SOURCES.txt 2020-03-06 22:01:19.000000000 +0100 +++ new/humanfriendly-9.2/humanfriendly.egg-info/SOURCES.txt 2021-06-11 12:53:09.000000000 +0200 @@ -19,6 +19,7 @@ docs/images/spinner-with-progress.gif docs/images/spinner-with-timer.gif humanfriendly/__init__.py +humanfriendly/case.py humanfriendly/cli.py humanfriendly/compat.py humanfriendly/decorators.py diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/humanfriendly-8.1/requirements-tests.txt new/humanfriendly-9.2/requirements-tests.txt --- old/humanfriendly-8.1/requirements-tests.txt 2020-03-01 19:23:29.000000000 +0100 +++ new/humanfriendly-9.2/requirements-tests.txt 2021-06-11 12:26:04.000000000 +0200 @@ -1,6 +1,6 @@ # Test suite requirements. capturer >= 2.1 -coloredlogs >= 2.0 +coloredlogs >= 15.0.1 docutils >= 0.15 mock >= 3.0.5 pytest >= 3.0.7 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/humanfriendly-8.1/requirements-travis.txt new/humanfriendly-9.2/requirements-travis.txt --- old/humanfriendly-8.1/requirements-travis.txt 2018-10-20 07:21:01.000000000 +0200 +++ new/humanfriendly-9.2/requirements-travis.txt 2021-06-11 12:26:04.000000000 +0200 @@ -1,3 +1,5 @@ --requirement=requirements-checks.txt --requirement=requirements-tests.txt coveralls +# Dependency of coveralls: +cryptography < 3; python_version == '2.7' and platform_python_implementation == "PyPy" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/humanfriendly-8.1/setup.py new/humanfriendly-9.2/setup.py --- old/humanfriendly-8.1/setup.py 2020-02-16 02:15:19.000000000 +0100 +++ new/humanfriendly-9.2/setup.py 2021-06-11 12:43:08.000000000 +0200 @@ -125,6 +125,7 @@ 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy', 'Topic :: Communications',