From 83c1e0d04fe2e78963c8b508e8b7d0ae03bfcb16 Mon Sep 17 00:00:00 2001 From: Scott Gasch Date: Fri, 10 Sep 2021 13:23:31 -0700 Subject: [PATCH] Adding more tests, working on the test harness. --- conversion_utils.py | 160 +++++++++++++++- dateparse/dateparse_utils.py | 10 +- datetime_utils.py | 350 ++++++++++++++++++++++++++++++++--- executors.py | 2 +- text_utils.py | 22 +++ type/centcount.py | 3 +- unittest_utils.py | 32 ++-- 7 files changed, 530 insertions(+), 49 deletions(-) diff --git a/conversion_utils.py b/conversion_utils.py index b83d62e..d2225fd 100644 --- a/conversion_utils.py +++ b/conversion_utils.py @@ -7,11 +7,27 @@ import constants class Converter(object): + """A converter has a canonical name and a category. The name defines + a unit of measurement in a category or class of measurements. + This framework will allow conversion between named units in the + same category. e.g. name may be "meter", "inch", "mile" and their + category may be "length". + + The way that conversions work is that we convert the magnitude + first to a canonical unit and then (if needed) from the canonical + unit to the desired destination unit. e.g. if "meter" is the + canonical unit of measurement for the category "length", in order + to convert miles into inches we first convert miles into meters + and, from there, meters into inches. This is potentially + dangerous because it requires two floating point operations which + each have the potential to overflow, underflow, or introduce + floating point errors. Caveat emptor. + """ def __init__(self, name: str, category: str, - to_canonical: Callable, - from_canonical: Callable, + to_canonical: Callable, # convert to canonical unit + from_canonical: Callable, # convert from canonical unit unit: str) -> None: self.name = name self.category = category @@ -29,6 +45,7 @@ class Converter(object): return self.unit +# A catalog of converters. conversion_catalog = { "Second": Converter("Second", "time", @@ -94,90 +111,225 @@ def _convert(magnitude: Number, def sec_to_min(s: float) -> float: + """ + Convert seconds into minutes. + + >>> sec_to_min(120) + 2.0 + """ return convert(s, "Second", "Minute") def sec_to_hour(s: float) -> float: + """ + Convert seconds into hours. + + >>> sec_to_hour(1800) + 0.5 + """ return convert(s, "Second", "Hour") def sec_to_day(s: float) -> float: + """ + Convert seconds into days. + + >>> sec_to_day(1800) + 0.020833333333333332 + """ return convert(s, "Second", "Day") def sec_to_week(s: float) -> float: + """ + Convert seconds into weeks. + + >>> sec_to_week(1800) + 0.002976190476190476 + """ return convert(s, "Second", "Week") def min_to_sec(m: float) -> float: + """ + Convert minutes into seconds. + + >>> min_to_sec(5) + 300.0 + """ return convert(m, "Minute", "Second") def min_to_hour(m: float) -> float: + """ + Convert minutes into hours. + + >>> min_to_hour(120) + 2.0 + """ return convert(m, "Minute", "Hour") def min_to_day(m: float) -> float: + """ + Convert minutes into days. + + >>> min_to_day(60 * 12) + 0.5 + """ return convert(m, "Minute", "Day") def min_to_week(m: float) -> float: + """ + Convert minutes into weeks. + + >>> min_to_week(60 * 24 * 3) + 0.42857142857142855 + """ return convert(m, "Minute", "Week") def hour_to_sec(h: float) -> float: + """ + Convert hours into seconds. + + >>> hour_to_sec(1) + 3600.0 + """ return convert(h, "Hour", "Second") def hour_to_min(h: float) -> float: + """ + Convert hours into minutes. + + >>> hour_to_min(2) + 120.0 + """ return convert(h, "Hour", "Minute") def hour_to_day(h: float) -> float: + """ + Convert hours into days. + + >>> hour_to_day(36) + 1.5 + """ return convert(h, "Hour", "Day") def hour_to_week(h: float) -> float: + """ + Convert hours into weeks. + + >>> hour_to_week(24) + 0.14285714285714285 + """ return convert(h, "Hour", "Week") def day_to_sec(d: float) -> float: + """ + Convert days into seconds. + + >>> day_to_sec(1) + 86400.0 + """ return convert(d, "Day", "Second") def day_to_min(d: float) -> float: + """ + Convert days into minutes. + + >>> day_to_min(0.1) + 144.0 + """ return convert(d, "Day", "Minute") def day_to_hour(d: float) -> float: + """ + Convert days into hours. + + >>> day_to_hour(1) + 24.0 + """ return convert(d, "Day", "Hour") def day_to_week(d: float) -> float: + """ + Convert days into weeks. + + >>> day_to_week(14) + 2.0 + """ return convert(d, "Day", "Week") def week_to_sec(w: float) -> float: + """ + Convert weeks into seconds. + + >>> week_to_sec(10) + 6048000.0 + """ return convert(w, "Week", "Second") def week_to_min(w: float) -> float: + """ + Convert weeks into minutes. + + >>> week_to_min(1) + 10080.0 + """ return convert(w, "Week", "Minute") def week_to_hour(w: float) -> float: + """ + Convert weeks into hours. + + >>> week_to_hour(1) + 168.0 + """ return convert(w, "Week", "Hour") def week_to_day(w: float) -> float: + """ + Convert weeks into days. + + >>> week_to_day(1) + 7.0 + """ return convert(w, "Week", "Day") def f_to_c(temp_f: float) -> float: - """Fahrenheit to Celsius.""" + """ + Convert Fahrenheit into Celsius. + + >>> f_to_c(32.0) + 0.0 + """ return convert(temp_f, "Fahrenheit", "Celsius") def c_to_f(temp_c: float) -> float: - """Celsius to Fahrenheit.""" + """ + Convert Celsius to Fahrenheit. + + >>> c_to_f(0.0) + 32.0 + """ return convert(temp_c, "Celsius", "Fahrenheit") + + +if __name__ == '__main__': + import doctest + doctest.testmod() diff --git a/dateparse/dateparse_utils.py b/dateparse/dateparse_utils.py index e5e7e76..f354ad0 100755 --- a/dateparse/dateparse_utils.py +++ b/dateparse/dateparse_utils.py @@ -271,9 +271,9 @@ class DateParser(dateparse_utilsListener): return TimeUnit.MONTHS txt = orig.lower()[:3] if txt in self.day_name_to_number: - return(self.day_name_to_number[txt]) + return(TimeUnit(self.day_name_to_number[txt])) elif txt in self.delta_unit_to_constant: - return(self.delta_unit_to_constant[txt]) + return(TimeUnit(self.delta_unit_to_constant[txt])) raise ParseException(f'Invalid date unit: {orig}') def _figure_out_time_unit(self, orig: str) -> int: @@ -509,7 +509,7 @@ class DateParser(dateparse_utilsListener): unit = self.context['delta_unit'] dt = n_timeunits_from_base( count, - unit, + TimeUnit(unit), date_to_datetime(self.date) ) self.date = datetime_to_date(dt) @@ -890,7 +890,7 @@ class DateParser(dateparse_utilsListener): unit = self._figure_out_date_unit(unit) d = n_timeunits_from_base( count, - unit, + TimeUnit(unit), d) self.context['year'] = d.year self.context['month'] = d.month @@ -920,7 +920,7 @@ class DateParser(dateparse_utilsListener): unit = self._figure_out_date_unit(unit) d = n_timeunits_from_base( count, - unit, + TimeUnit(unit), d) self.context['year'] = d.year self.context['month'] = d.month diff --git a/datetime_utils.py b/datetime_utils.py index 7787c6f..db5b2b5 100644 --- a/datetime_utils.py +++ b/datetime_utils.py @@ -18,18 +18,44 @@ logger = logging.getLogger(__name__) def replace_timezone(dt: datetime.datetime, tz: datetime.tzinfo) -> datetime.datetime: + """ + Replaces the timezone on a datetime object. + + >>> from pytz import UTC + >>> d = now_pacific() + >>> d.tzinfo.tzname(d)[0] # Note: could be PST or PDT + 'P' + >>> o = replace_timezone(d, UTC) + >>> o.tzinfo.tzname(o) + 'UTC' + + """ return dt.replace(tzinfo=None).astimezone(tz=tz) def now() -> datetime.datetime: + """ + What time is it? Result returned in UTC + """ return datetime.datetime.now() -def now_pst() -> datetime.datetime: +def now_pacific() -> datetime.datetime: + """ + What time is it? Result in US/Pacifit time (PST/PDT) + """ return replace_timezone(now(), pytz.timezone("US/Pacific")) def date_to_datetime(date: datetime.date) -> datetime.datetime: + """ + Given a date, return a datetime with hour/min/sec zero (midnight) + + >>> import datetime + >>> date_to_datetime(datetime.date(2021, 12, 25)) + datetime.datetime(2021, 12, 25, 0, 0) + + """ return datetime.datetime( date.year, date.month, @@ -40,6 +66,16 @@ def date_to_datetime(date: datetime.date) -> datetime.datetime: def date_and_time_to_datetime(date: datetime.date, time: datetime.time) -> datetime.datetime: + """ + Given a date and time, merge them and return a datetime. + + >>> import datetime + >>> d = datetime.date(2021, 12, 25) + >>> t = datetime.time(12, 30, 0, 0) + >>> date_and_time_to_datetime(d, t) + datetime.datetime(2021, 12, 25, 12, 30) + + """ return datetime.datetime( date.year, date.month, @@ -47,26 +83,53 @@ def date_and_time_to_datetime(date: datetime.date, time.hour, time.minute, time.second, - time.millisecond + time.microsecond, ) def datetime_to_date_and_time( dt: datetime.datetime ) -> Tuple[datetime.date, datetime.time]: + """Return the component date and time objects of a datetime. + + >>> import datetime + >>> dt = datetime.datetime(2021, 12, 25, 12, 30) + >>> (d, t) = datetime_to_date_and_time(dt) + >>> d + datetime.date(2021, 12, 25) + >>> t + datetime.time(12, 30) + + """ return (dt.date(), dt.timetz()) def datetime_to_date(dt: datetime.datetime) -> datetime.date: + """Return the date part of a datetime. + + >>> import datetime + >>> dt = datetime.datetime(2021, 12, 25, 12, 30) + >>> datetime_to_date(dt) + datetime.date(2021, 12, 25) + + """ return datetime_to_date_and_time(dt)[0] def datetime_to_time(dt: datetime.datetime) -> datetime.time: + """Return the time part of a datetime. + + >>> import datetime + >>> dt = datetime.datetime(2021, 12, 25, 12, 30) + >>> datetime_to_time(dt) + datetime.time(12, 30) + + """ return datetime_to_date_and_time(dt)[1] -# An enum to represent units with which we can compute deltas. class TimeUnit(enum.Enum): + """An enum to represent units with which we can compute deltas.""" MONDAYS = 0 TUESDAYS = 1 WEDNESDAYS = 2 @@ -101,6 +164,47 @@ def n_timeunits_from_base( unit: TimeUnit, base: datetime.datetime ) -> datetime.datetime: + """Return a datetime that is N units before/after a base datetime. + e.g. 3 Wednesdays from base datetime, 2 weeks from base date, 10 + years before base datetime, 13 minutes after base datetime, etc... + Note: to indicate before/after the base date, use a positive or + negative count. + + >>> base = string_to_datetime("2021/09/10 11:24:51AM-0700")[0] + + The next (1) Monday from the base datetime: + >>> n_timeunits_from_base(+1, TimeUnit.MONDAYS, base) + datetime.datetime(2021, 9, 13, 11, 24, 51, tzinfo=datetime.timezone(datetime.timedelta(days=-1, seconds=61200))) + + Ten (10) years after the base datetime: + >>> n_timeunits_from_base(10, TimeUnit.YEARS, base) + datetime.datetime(2031, 9, 10, 11, 24, 51, tzinfo=datetime.timezone(datetime.timedelta(days=-1, seconds=61200))) + + Fifty (50) working days (M..F, not counting holidays) after base datetime: + >>> n_timeunits_from_base(50, TimeUnit.WORKDAYS, base) + datetime.datetime(2021, 11, 23, 11, 24, 51, tzinfo=datetime.timezone(datetime.timedelta(days=-1, seconds=61200))) + + Fifty (50) days (including weekends and holidays) after base datetime: + >>> n_timeunits_from_base(50, TimeUnit.DAYS, base) + datetime.datetime(2021, 10, 30, 11, 24, 51, tzinfo=datetime.timezone(datetime.timedelta(days=-1, seconds=61200))) + + Fifty (50) months before (note negative count) base datetime: + >>> n_timeunits_from_base(-50, TimeUnit.MONTHS, base) + datetime.datetime(2017, 7, 10, 11, 24, 51, tzinfo=datetime.timezone(datetime.timedelta(days=-1, seconds=61200))) + + Fifty (50) hours after base datetime: + >>> n_timeunits_from_base(50, TimeUnit.HOURS, base) + datetime.datetime(2021, 9, 12, 13, 24, 51, tzinfo=datetime.timezone(datetime.timedelta(days=-1, seconds=61200))) + + Fifty (50) minutes before base datetime: + >>> n_timeunits_from_base(-50, TimeUnit.MINUTES, base) + datetime.datetime(2021, 9, 10, 10, 34, 51, tzinfo=datetime.timezone(datetime.timedelta(days=-1, seconds=61200))) + + Fifty (50) seconds from base datetime: + >>> n_timeunits_from_base(50, TimeUnit.SECONDS, base) + datetime.datetime(2021, 9, 10, 11, 25, 41, tzinfo=datetime.timezone(datetime.timedelta(days=-1, seconds=61200))) + + """ assert TimeUnit.is_valid(unit) if count == 0: return base @@ -110,6 +214,21 @@ def n_timeunits_from_base( timedelta = datetime.timedelta(days=count) return base + timedelta + # N hours from base + elif unit == TimeUnit.HOURS: + timedelta = datetime.timedelta(hours=count) + return base + timedelta + + # N minutes from base + elif unit == TimeUnit.MINUTES: + timedelta = datetime.timedelta(minutes=count) + return base + timedelta + + # N seconds from base + elif unit == TimeUnit.SECONDS: + timedelta = datetime.timedelta(seconds=count) + return base + timedelta + # N workdays from base elif unit == TimeUnit.WORKDAYS: if count < 0: @@ -155,6 +274,7 @@ def n_timeunits_from_base( base.minute, base.second, base.microsecond, + base.tzinfo, ) # N years from base @@ -168,8 +288,18 @@ def n_timeunits_from_base( base.minute, base.second, base.microsecond, + base.tzinfo, ) + if unit not in set([TimeUnit.MONDAYS, + TimeUnit.TUESDAYS, + TimeUnit.WEDNESDAYS, + TimeUnit.THURSDAYS, + TimeUnit.FRIDAYS, + TimeUnit.SATURDAYS, + TimeUnit.SUNDAYS]): + raise ValueError(unit) + # N weekdays from base (e.g. 4 wednesdays from today) direction = 1 if count > 0 else -1 count = abs(count) @@ -177,7 +307,7 @@ def n_timeunits_from_base( start = base while True: dow = base.weekday() - if dow == unit and start != base: + if dow == unit.value and start != base: count -= 1 if count == 0: return base @@ -194,14 +324,31 @@ def get_format_string( include_fractional=False, twelve_hour=True, ) -> str: + """ + Helper to return a format string without looking up the documentation + for strftime. + + >>> get_format_string() + '%Y/%m/%d %I:%M:%S%p%z' + + >>> get_format_string(date_time_separator='@') + '%Y/%m/%d@%I:%M:%S%p%z' + + >>> get_format_string(include_dayname=True) + '%a/%Y/%m/%d %I:%M:%S%p%z' + + >>> get_format_string(include_dayname=True, twelve_hour=False) + '%a/%Y/%m/%d %H:%M:%S%z' + + """ fstring = "" if include_dayname: fstring += "%a/" if use_month_abbrevs: - fstring = f"%Y/%b/%d{date_time_separator}" + fstring = f"{fstring}%Y/%b/%d{date_time_separator}" else: - fstring = f"%Y/%m/%d{date_time_separator}" + fstring = f"{fstring}%Y/%m/%d{date_time_separator}" if twelve_hour: fstring += "%I:%M" if include_seconds: @@ -229,7 +376,21 @@ def datetime_to_string( include_fractional=False, twelve_hour=True, ) -> str: - """A nice way to convert a datetime into a string.""" + """ + A nice way to convert a datetime into a string; arguably better than + just printing it and relying on it __repr__(). + + >>> d = string_to_datetime( + ... "2021/09/10 11:24:51AM-0700", + ... )[0] + >>> d + datetime.datetime(2021, 9, 10, 11, 24, 51, tzinfo=datetime.timezone(datetime.timedelta(days=-1, seconds=61200))) + >>> datetime_to_string(d) + '2021/09/10 11:24:51AM-0700' + >>> datetime_to_string(d, include_dayname=True, include_seconds=False) + 'Fri/2021/09/10 11:24AM-0700' + + """ fstring = get_format_string( date_time_separator=date_time_separator, include_timezone=include_timezone, @@ -251,8 +412,16 @@ def string_to_datetime( include_fractional=False, twelve_hour=True, ) -> Tuple[datetime.datetime, str]: - """A nice way to convert a string into a datetime. Also consider - dateparse.dateparse_utils for a full parser. + """A nice way to convert a string into a datetime. Returns both the + datetime and the format string used to parse it. Also consider + dateparse.dateparse_utils for a full parser alternative. + + >>> d = string_to_datetime( + ... "2021/09/10 11:24:51AM-0700", + ... ) + >>> d + (datetime.datetime(2021, 9, 10, 11, 24, 51, tzinfo=datetime.timezone(datetime.timedelta(days=-1, seconds=61200))), '%Y/%m/%d %I:%M:%S%p%z') + """ fstring = get_format_string( date_time_separator=date_time_separator, @@ -268,7 +437,7 @@ def string_to_datetime( def timestamp() -> str: - """Return a timestamp for now in Pacific timezone.""" + """Return a timestamp for right now in Pacific timezone.""" ts = datetime.datetime.now(tz=pytz.timezone("US/Pacific")) return datetime_to_string(ts, include_timezone=True) @@ -281,7 +450,25 @@ def time_to_string( include_timezone=False, twelve_hour=True, ) -> str: - """A nice way to convert a datetime into a time (only) string.""" + """A nice way to convert a datetime into a time (only) string. + This ignores the date part of the datetime. + + >>> d = string_to_datetime( + ... "2021/09/10 11:24:51AM-0700", + ... )[0] + >>> d + datetime.datetime(2021, 9, 10, 11, 24, 51, tzinfo=datetime.timezone(datetime.timedelta(days=-1, seconds=61200))) + + >>> time_to_string(d) + '11:24:51AM' + + >>> time_to_string(d, include_seconds=False) + '11:24AM' + + >>> time_to_string(d, include_seconds=False, include_timezone=True) + '11:24AM-0700' + + """ fstring = "" if twelve_hour: fstring += "%l:%M" @@ -308,18 +495,62 @@ MinuteOfDay = NewType("MinuteOfDay", int) def minute_number(hour: int, minute: int) -> MinuteOfDay: - """Convert hour:minute into minute number from start of day.""" + """ + Convert hour:minute into minute number from start of day. + + >>> minute_number(0, 0) + 0 + + >>> minute_number(9, 15) + 555 + + >>> minute_number(23, 59) + 1439 + + """ return MinuteOfDay(hour * 60 + minute) def datetime_to_minute_number(dt: datetime.datetime) -> MinuteOfDay: - """Convert a datetime into a minute number (of the day)""" + """ + Convert a datetime into a minute number (of the day). Note that + this ignores the date part of the datetime and only uses the time + part. + + >>> d = string_to_datetime( + ... "2021/09/10 11:24:51AM-0700", + ... )[0] + + >>> datetime_to_minute_number(d) + 684 + + """ return minute_number(dt.hour, dt.minute) +def time_to_minute_number(t: datetime.time) -> MinuteOfDay: + """ + Convert a datetime.time into a minute number. + + >>> t = datetime.time(5, 15) + >>> time_to_minute_number(t) + 315 + + """ + return minute_number(t.hour, t.minute) + + def minute_number_to_time_string(minute_num: MinuteOfDay) -> str: - """Convert minute number from start of day into hour:minute am/pm + """ + Convert minute number from start of day into hour:minute am/pm string. + + >>> minute_number_to_time_string(315) + ' 5:15a' + + >>> minute_number_to_time_string(684) + '11:24a' + """ hour = minute_num // 60 minute = minute_num % 60 @@ -335,7 +566,22 @@ def minute_number_to_time_string(minute_num: MinuteOfDay) -> str: def parse_duration(duration: str) -> int: - """Parse a duration in string form.""" + """ + Parse a duration in string form into a delta seconds. + + >>> parse_duration('15 days, 2 hours') + 1303200 + + >>> parse_duration('15d 2h') + 1303200 + + >>> parse_duration('100s') + 100 + + >>> parse_duration('3min 2sec') + 182 + + """ if duration.isdigit(): return int(duration) seconds = 0 @@ -354,9 +600,24 @@ def parse_duration(duration: str) -> int: return seconds -def describe_duration(age: int) -> str: - """Describe a duration.""" - days = divmod(age, constants.SECONDS_PER_DAY) +def describe_duration(seconds: int, *, include_seconds = False) -> str: + """ + Describe a duration represented as a count of seconds nicely. + + >>> describe_duration(182) + '3 minutes' + + >>> describe_duration(182, include_seconds=True) + '3 minutes, and 2 seconds' + + >>> describe_duration(100, include_seconds=True) + '1 minute, and 40 seconds' + + describe_duration(1303200) + '15 days, 2 hours' + + """ + days = divmod(seconds, constants.SECONDS_PER_DAY) hours = divmod(days[1], constants.SECONDS_PER_HOUR) minutes = divmod(hours[1], constants.SECONDS_PER_MINUTE) @@ -365,30 +626,65 @@ def describe_duration(age: int) -> str: descr = f"{int(days[0])} days, " elif days[0] == 1: descr = "1 day, " + if hours[0] > 1: descr = descr + f"{int(hours[0])} hours, " elif hours[0] == 1: descr = descr + "1 hour, " - if len(descr) > 0: + + if not include_seconds and len(descr) > 0: descr = descr + "and " + if minutes[0] == 1: descr = descr + "1 minute" else: descr = descr + f"{int(minutes[0])} minutes" + + if include_seconds: + descr = descr + ', ' + if len(descr) > 0: + descr = descr + 'and ' + s = minutes[1] + if s == 1: + descr = descr + '1 second' + else: + descr = descr + f'{s} seconds' return descr -def describe_duration_briefly(age: int) -> str: - """Describe a duration briefly.""" - days = divmod(age, constants.SECONDS_PER_DAY) +def describe_duration_briefly(seconds: int, *, include_seconds=False) -> str: + """ + Describe a duration briefly. + + >>> describe_duration_briefly(182) + '3m' + + >>> describe_duration_briefly(182, include_seconds=True) + '3m 2s' + + >>> describe_duration_briefly(100, include_seconds=True) + '1m 40s' + + describe_duration_briefly(1303200) + '15d 2h' + + """ + days = divmod(seconds, constants.SECONDS_PER_DAY) hours = divmod(days[1], constants.SECONDS_PER_HOUR) minutes = divmod(hours[1], constants.SECONDS_PER_MINUTE) - descr = "" + descr = '' if days[0] > 0: - descr = f"{int(days[0])}d " + descr = f'{int(days[0])}d ' if hours[0] > 0: - descr = descr + f"{int(hours[0])}h " - if minutes[0] > 0 or len(descr) == 0: - descr = descr + f"{int(minutes[0])}m" + descr = descr + f'{int(hours[0])}h ' + if minutes[0] > 0 or (len(descr) == 0 and not include_seconds): + descr = descr + f'{int(minutes[0])}m ' + if minutes[1] > 0 and include_seconds: + descr = descr + f'{int(minutes[1])}s' return descr.strip() + + +if __name__ == '__main__': + import doctest + doctest.testmod() diff --git a/executors.py b/executors.py index e074c30..b243edd 100644 --- a/executors.py +++ b/executors.py @@ -64,7 +64,7 @@ SSH = 'ssh -oForwardX11=no' def make_cloud_pickle(fun, *args, **kwargs): - logger.info(f"Making cloudpickled bundle at {fun.__name__}") + logger.debug(f"Making cloudpickled bundle at {fun.__name__}") return cloudpickle.dumps((fun, args, kwargs)) diff --git a/text_utils.py b/text_utils.py index 8ea6e19..49ff9b9 100644 --- a/text_utils.py +++ b/text_utils.py @@ -268,6 +268,28 @@ class Indenter: print(self.pad_prefix + self.padding * self.level + text, end='') +def header(title: str, *, width: int = 80, color: str = ''): + """ + Returns a nice header line with a title. + + >>> header('title', width=60, color='') + '----[ title ]-----------------------------------------------' + + """ + w = width + w -= (len(title) + 4) + if w >= 4: + left = 4 * '-' + right = (w - 4) * '-' + if color != '' and color is not None: + r = reset() + else: + r = '' + return f'{left}[ {color}{title}{r} ]{right}' + else: + return '' + + if __name__ == '__main__': import doctest doctest.testmod() diff --git a/type/centcount.py b/type/centcount.py index ce18975..4e5b8a6 100644 --- a/type/centcount.py +++ b/type/centcount.py @@ -11,7 +11,8 @@ T = TypeVar('T', bound='CentCount') class CentCount(object): """A class for representing monetary amounts potentially with - different currencies. + different currencies meant to avoid floating point rounding + issues by treating amount as a simple integral count of cents. """ def __init__ ( diff --git a/unittest_utils.py b/unittest_utils.py index 2dc8cfe..8a0556b 100644 --- a/unittest_utils.py +++ b/unittest_utils.py @@ -148,15 +148,19 @@ def check_all_methods_for_perf_regressions(prefix='test_'): def breakpoint(): + """Hard code a breakpoint somewhere; drop into pdb.""" import pdb pdb.set_trace() class RecordStdout(object): """ - with uu.RecordStdout() as record: - print("This is a test!") - print({record().readline()}) + Record what is emitted to stdout. + + >>> with RecordStdout() as record: + ... print("This is a test!") + >>> print({record().readline()}) + {'This is a test!\\n'} """ def __init__(self) -> None: @@ -176,9 +180,13 @@ class RecordStdout(object): class RecordStderr(object): """ - with uu.RecordStderr() as record: - print("This is a test!", file=sys.stderr) - print({record().readline()}) + Record what is emitted to stderr. + + >>> import sys + >>> with RecordStderr() as record: + ... print("This is a test!", file=sys.stderr) + >>> print({record().readline()}) + {'This is a test!\\n'} """ def __init__(self) -> None: @@ -198,12 +206,9 @@ class RecordStderr(object): class RecordMultipleStreams(object): """ - with uu.RecordStreams(sys.stderr, sys.stdout) as record: - print("This is a test!") - print("This is one too.", file=sys.stderr) - - print(record().readlines()) + Record the output to more than one stream. """ + def __init__(self, *files) -> None: self.files = [*files] self.destination = tempfile.SpooledTemporaryFile(mode='r+') @@ -219,3 +224,8 @@ class RecordMultipleStreams(object): for f in self.files: f.write = self.saved_writes.pop() self.destination.seek(0) + + +if __name__ == '__main__': + import doctest + doctest.testmod() -- 2.46.0