"""One interpreter start for the whole of 99-python3-arrow. Emits `key=value' lines, one per line, in file order; the suite reads them back with grep. A python3 start on this arm is tens of seconds and every measurement below is arithmetic and string work over fixed inputs, so the start is the entire cost and taking it once is the difference between a half-minute suite and an hour. THE CLOCK IS AN INPUT HERE, NEVER AN AMBIENT FACT. Every instant this file measures is either a literal in this file or derived from one. `utcnow()' and `now()' are exercised, but only for their TYPE and their tzinfo -- never for a value -- so nothing below depends on when the suite runs, on the guest's timezone, or on any time source off the machine. `humanize' is always given an explicit `other', which is the whole reason it is assertable at all: with the argument omitted it is scored against `utcnow()' and has no fixed answer. THREE RULES THE VALUES OBEY. The runner parses its input LINE BY LINE, so no value may contain a newline. Every value here is a scalar or a comma-joined list of scalars. No value may be ENCODING-DEPENDENT. The guest runs under the C locale, so whether CPython coerces stdout to UTF-8 is a property of the platform rather than of this package. `arrow.locales' carries 80-odd locales and most of them are non-ASCII; the two exercised below (`en-us' and `fr-fr') were chosen because every string they produce for these inputs is ASCII, and the presence of the others is asserted by NAME through the locale registry rather than by rendering them. And no value may be a REPR THAT CARRIES AN ADDRESS. An `Arrow' defines `__repr__' without one, but a `tzinfo' does not always, so every timezone below is reported through `utcoffset()' in seconds and through `tzname()', never as the object. WHY AN OFFSET IS REPORTED IN SECONDS. `str(timedelta)' suppresses a zero field and renders a negative offset as `-1 day, 17:00:00', so two offsets that differ would be hard to read and one that changed sign would be hard to see. The integer is unambiguous and orders correctly. WHY THIS FILE IS NOT NAMED arrow.py. It sits in ${libdir}/brintos-tests/data and the interpreter puts a script's own directory FIRST on sys.path, so a probe named after the module would shadow the module it is measuring. `module_file' below is the case that would catch it if that ever stopped being true. """ import sys _BASELINE = set(sys.modules) emitted = [] def emit(key, value): emitted.append(key) print('%s=%s' % (key, value)) def raises(fn, *a, **kw): """The name of the exception type `fn' raised, or `'.""" try: fn(*a, **kw) except Exception as exc: # noqa: BLE001 return type(exc).__name__ return '' def emit_call(key, fn, *a, **kw): """Emit what `fn' returned, or the TYPE of the exception it raised. A raise is a first-class expected value here -- the whole `err_*' group asserts ABOUT raising -- so it is reported as `' rather than killing the probe and leaving every case after it reading got:''. """ try: emit(key, fn(*a, **kw)) except Exception as exc: # noqa: BLE001 emit(key, '<%s>' % type(exc).__name__) import arrow # noqa: E402 _AFTER_IMPORT = set(sys.modules) emit('module_file', getattr(arrow, '__file__', '')) emit('is_package', 'yes' if hasattr(arrow, '__path__') else 'no') emit('version_attr', arrow.__version__) try: from importlib.metadata import version as _md_version emit('dist_version', _md_version('arrow')) except Exception as exc: # noqa: BLE001 emit('dist_version', '<%s>' % type(exc).__name__) # ---------------------------------------------------------------- the API --- # `arrow/__init__.py' names 17 exports in `__all__' and the module docstring # points a caller at three of them. Each is asserted reachable at the package # root before anything below asserts what it does. emit('all_count', len(arrow.__all__)) emit('api_missing', ','.join(n for n in arrow.__all__ if not hasattr(arrow, n)) or '') emit('type_arrow', type(arrow.Arrow).__name__) emit('get_is_callable', 'yes' if callable(arrow.get) else 'no') # ------------------------------------------------------- the ambient clock --- # TYPE AND TZINFO ONLY -- see the header. `utcnow()' must be UTC and `now()' # with an explicit zone must carry that zone; neither value is asserted. _un = arrow.utcnow() emit('utcnow_type', type(_un).__name__) emit('utcnow_offset', int(_un.utcoffset().total_seconds())) emit('utcnow_tzname', _un.tzname()) _nw = arrow.now('Asia/Kolkata') emit('now_tz_offset', int(_nw.utcoffset().total_seconds())) # ------------------------------------------------------- arrow.get parsing --- # ISO 8601 WITH AN EXPLICIT OFFSET. The instant and the offset are asserted # separately: `isoformat()' carries both, but a release that dropped the offset # while keeping the wall clock would still print a plausible string. _a = arrow.get('2013-05-11T21:23:58.970460+07:00') emit('iso_offset_isoformat', _a.isoformat()) emit('iso_offset_utcoffset', int(_a.utcoffset().total_seconds())) emit('iso_offset_timestamp', repr(_a.int_timestamp)) emit('iso_offset_microsecond', _a.microsecond) emit('iso_offset_utc_wall', _a.to('UTC').format('YYYY-MM-DD HH:mm:ss')) # RFC 3339's `Z'. Upper case, and the lower case the grammar also allows. emit_call('rfc3339_z', lambda: arrow.get('2013-05-05T12:30:45Z').isoformat()) emit_call('rfc3339_z_offset', lambda: int(arrow.get('2013-05-05T12:30:45Z').utcoffset().total_seconds())) emit_call('rfc3339_lower_z', lambda: arrow.get('2013-05-05t12:30:45z').isoformat()) # ...and the space separator RFC 3339 ยง5.6 permits in place of `T'. emit_call('rfc3339_space', lambda: arrow.get('2013-05-05 12:30:45').isoformat()) # A NAKED DATE IS MIDNIGHT **UTC**, not midnight local. A caller who reads it # as local is wrong by the offset, silently. emit_call('bare_date', lambda: arrow.get('2013-05-05').isoformat()) emit_call('bare_date_offset', lambda: int(arrow.get('2013-05-05').utcoffset().total_seconds())) # The three shortened calendar forms. emit_call('basic_no_dashes', lambda: arrow.get('20130505').isoformat()) emit_call('year_month', lambda: arrow.get('2013-05').isoformat()) emit_call('year_only', lambda: arrow.get('2013').isoformat()) # A FRACTIONAL SECOND IS TRUNCATED TO MICROSECONDS, not rounded and not refused. emit_call('nanoseconds_truncated', lambda: arrow.get('2013-05-05T12:30:45.123456789').microsecond) # An offset written without a colon, and one written as hours only. emit_call('offset_no_colon', lambda: int(arrow.get('2013-05-05T12:30:45+0700').utcoffset().total_seconds())) emit_call('offset_hours_only', lambda: int(arrow.get('2013-05-05T12:30:45+07').utcoffset().total_seconds())) # `-00:00' is not `+00:00' in RFC 3339's prose, but it is here. emit_call('offset_negative_zero', lambda: int(arrow.get('2013-05-05T12:30:45-00:00').utcoffset().total_seconds())) # AN EPOCH IS A NUMBER, AND ONLY A NUMBER. emit_call('epoch_int', lambda: arrow.get(1367900664).isoformat()) emit_call('epoch_float', lambda: arrow.get(1367900664.152325).isoformat()) emit_call('epoch_int_offset', lambda: int(arrow.get(1367900664).utcoffset().total_seconds())) # THE ONE THAT SURPRISES: A STRING OF DIGITS IS **NOT** A TIMESTAMP. arrow 0.x # parsed `"1367900664"' as an epoch; 1.0 removed that on purpose, because the # same string is a plausible malformed date. A caller who upgraded and kept # passing the string gets a ParserError, not an instant. emit('epoch_string_raises', raises(arrow.get, '1367900664')) # ...and the documented way to say it anyway. emit_call('epoch_string_via_token', lambda: arrow.get('1367900664', 'X').isoformat()) # A FORMAT STRING IS THE OTHER ENTRY POINT, and a token that does not match is # a ParserError rather than a best effort. emit_call('tokens_parse', lambda: arrow.get('2013-05-05 12:30:45', 'YYYY-MM-DD HH:mm:ss').isoformat()) # A FORMAT SHORTER THAN THE INPUT IS **NOT** AN ERROR. The tokenised parser # matches a PREFIX and drops the remainder, so a caller who mistypes the format # gets a plausible instant with the time silently thrown away. Both halves are # reported -- that it does not raise, and what it returns instead. emit('tokens_short_format_raises', raises(arrow.get, '2013-05-05 12:30:45', 'YYYY-MM-DD')) emit_call('tokens_short_format_value', lambda: arrow.get('2013-05-05 12:30:45', 'YYYY-MM-DD').isoformat()) # A LIST OF FORMATS IS TRIED IN ORDER and the first that matches wins. emit_call('tokens_list', lambda: arrow.get('05-05-2013', ['YYYY-MM-DD', 'MM-DD-YYYY']).isoformat()) # A `datetime' IN, AND THE ZONE ARGUMENT. import datetime as _dt # noqa: E402 emit_call('from_naive_datetime', lambda: arrow.get(_dt.datetime(2013, 5, 5, 12, 30, 45)).isoformat()) emit_call('from_naive_datetime_tz', lambda: arrow.get(_dt.datetime(2013, 5, 5, 12, 30, 45), 'America/Los_Angeles').isoformat()) emit_call('from_date', lambda: arrow.get(_dt.date(2013, 5, 5)).isoformat()) emit_call('from_parts', lambda: arrow.get(2013, 5, 5, 12, 30, 45).isoformat()) emit_call('from_arrow', lambda: arrow.get(arrow.get('2013-05-05T12:30:45Z')).isoformat()) # NO ARGUMENT AT ALL IS `utcnow()', so only its tzinfo is assertable. emit_call('get_noargs_offset', lambda: int(arrow.get().utcoffset().total_seconds())) # ----------------------------------------------------- the refusal path --- # ASSERTED BY EXCEPTION TYPE, NOT BY MESSAGE. The messages are English prose # and a release may reword any of them; the TYPE is what a caller's `except' # clause names. `ParserError' is re-exported from the package root, which is # what makes `except arrow.ParserError' the documented spelling. emit('parsererror_at_root', 'yes' if hasattr(arrow, 'ParserError') else 'no') emit('parsererror_is_valueerror', 'yes' if issubclass(arrow.ParserError, ValueError) else 'no') emit('err_garbage', raises(arrow.get, 'blah')) emit('err_empty', raises(arrow.get, '')) emit('err_month_13', raises(arrow.get, '2013-13-05')) emit('err_day_32', raises(arrow.get, '2013-05-32')) emit('err_feb_30', raises(arrow.get, '2013-02-30')) emit('err_not_leap', raises(arrow.get, '2013-02-29')) emit('err_hour_25', raises(arrow.get, '2013-05-05T25:30:45')) emit('err_trailing_garbage', raises(arrow.get, '2013-05-05T12:30:45XYZ')) # AN OUT-OF-RANGE OFFSET IS **NOT** REFUSED. `+9999' is 99 hours and 99 # minutes, which no zone on earth has, and the parser takes it: the four digits # are read as HHMM and handed to `timedelta' without a bound. Reported as the # resulting offset in seconds rather than as a refusal, because there is none. emit('err_bad_offset', raises(arrow.get, '2013-05-05T12:30:45+9999')) # ...and the object it hands back is POISONED rather than merely odd: it # constructs, it is an `Arrow', and `utcoffset()' -- which `isoformat()', # `format()' and every comparison call -- raises `ValueError' out of the # standard library, hours or a serialisation later, from a `get()' that # reported success. Both facts are emitted, because either alone reads as the # other's absence. emit('bad_offset_type', type(arrow.get('2013-05-05T12:30:45+9999')).__name__) emit_call('bad_offset_seconds', lambda: int(arrow.get('2013-05-05T12:30:45+9999') .utcoffset().total_seconds())) emit('err_unknown_zone', raises(arrow.get, '2013-05-05T12:30:45', tzinfo='Not/AZone')) emit('err_nonsense_type', raises(arrow.get, object())) # ------------------------------------------------------------------ shift --- # A CALENDAR SHIFT IS A `relativedelta', NOT A `timedelta', AND THAT IS THE # WHOLE OF WHY THE FOUR CASES BELOW ARE NOT ARITHMETIC. # # MONTH-END IS CLAMPED, SILENTLY. 31 January plus one month is 28 February, # not 3 March and not an error. _jan31 = arrow.get('2013-01-31T00:00:00Z') emit('shift_month_end', _jan31.shift(months=+1).format('YYYY-MM-DD')) emit('shift_month_end_leap', arrow.get('2012-01-31T00:00:00Z').shift(months=+1).format('YYYY-MM-DD')) # ...so THE SHIFT IS NOT INVERTIBLE. +1 month then -1 month is 28 January, not # 31 January. A caller who round-trips a date through a month shift loses days # and nothing raises. emit('shift_month_roundtrip', _jan31.shift(months=+1).shift(months=-1).format('YYYY-MM-DD')) emit('shift_month_roundtrip_is_identity', 'yes' if _jan31.shift(months=+1).shift(months=-1) == _jan31 else 'no') # THE LEAP DAY HAS THE SAME SHAPE. 29 February plus one year is 28 February. emit('shift_leap_year', arrow.get('2020-02-29T00:00:00Z').shift(years=+1).format('YYYY-MM-DD')) emit('shift_leap_four_years', arrow.get('2020-02-29T00:00:00Z').shift(years=+4).format('YYYY-MM-DD')) # ...and 1900 was NOT a leap year while 2000 was, which is the century rule. emit('shift_leap_1900', arrow.get('1896-02-29T00:00:00Z').shift(years=+4).format('YYYY-MM-DD')) emit('shift_leap_2000', arrow.get('1996-02-29T00:00:00Z').shift(years=+4).format('YYYY-MM-DD')) # DAYS ACROSS A DST TRANSITION PRESERVE THE WALL CLOCK, NOT THE ELAPSED TIME. # US Pacific springs forward at 02:00 local on 2023-03-12. Shifting one DAY # from noon the day before lands on noon again -- and 23 hours have passed. _pac = arrow.get('2023-03-11T12:00:00-08:00').to('America/Los_Angeles') _pac_next = _pac.shift(days=+1) emit('dst_day_before', _pac.format('YYYY-MM-DD HH:mm:ss ZZ')) emit('dst_day_after', _pac_next.format('YYYY-MM-DD HH:mm:ss ZZ')) emit('dst_day_offset_before', int(_pac.utcoffset().total_seconds())) emit('dst_day_offset_after', int(_pac_next.utcoffset().total_seconds())) emit('dst_day_elapsed_seconds', int((_pac_next.to('UTC') - _pac.to('UTC')).total_seconds())) # HOURS ARE THE OTHER HALF OF THE SAME STATEMENT: an HOUR shift moves the # instant by exactly an hour and the WALL CLOCK by two, because 02:00 does not # exist that morning. _gap = arrow.get('2023-03-12T01:30:00-08:00').to('America/Los_Angeles') _gap_next = _gap.shift(hours=+1) emit('dst_gap_before', _gap.format('YYYY-MM-DD HH:mm:ss ZZ')) emit('dst_gap_after', _gap_next.format('YYYY-MM-DD HH:mm:ss ZZ')) emit('dst_gap_elapsed_seconds', int((_gap_next.to('UTC') - _gap.to('UTC')).total_seconds())) # THE AUTUMN REPEAT: 01:30 happens twice on 2023-11-05 in US Pacific. The two # are DIFFERENT INSTANTS an hour apart with the same wall clock, and arrow # reports both offsets rather than collapsing them. _fall_a = arrow.get('2023-11-05T08:30:00Z').to('America/Los_Angeles') _fall_b = arrow.get('2023-11-05T09:30:00Z').to('America/Los_Angeles') emit('dst_fold_first', _fall_a.format('YYYY-MM-DD HH:mm:ss ZZ')) emit('dst_fold_second', _fall_b.format('YYYY-MM-DD HH:mm:ss ZZ')) emit('dst_fold_same_wall', 'yes' if _fall_a.format('HH:mm') == _fall_b.format('HH:mm') else 'no') emit('dst_fold_offsets', '%d,%d' % (int(_fall_a.utcoffset().total_seconds()), int(_fall_b.utcoffset().total_seconds()))) # THE PLAIN SHIFTS, so the group above reads as the exception it is. emit('shift_days', arrow.get('2013-05-05T12:30:45Z').shift(days=+3).isoformat()) emit('shift_weeks', arrow.get('2013-05-05T12:30:45Z').shift(weeks=+2).isoformat()) emit('shift_hours', arrow.get('2013-05-05T12:30:45Z').shift(hours=-3).isoformat()) emit('shift_microseconds', arrow.get('2013-05-05T12:30:45Z').shift(microseconds=+1).isoformat()) # `weekday=' IS NOT A SHIFT AT ALL -- it moves FORWARD to the next named # weekday, and stands still when it is already there. emit('shift_weekday_next', arrow.get('2013-05-05T12:30:45Z').shift(weekday=0).format('YYYY-MM-DD ddd')) emit('shift_weekday_same', arrow.get('2013-05-05T12:30:45Z').shift(weekday=6).format('YYYY-MM-DD ddd')) # `replace()' IS THE OTHER OPERATION AND IS ABSOLUTE, not relative -- and it # refuses a plural, which is the mistake a caller makes first. emit_call('replace_absolute', lambda: arrow.get('2013-05-05T12:30:45Z').replace(hour=6).isoformat()) emit('replace_plural_raises', raises(arrow.get('2013-05-05T12:30:45Z').replace, hours=6)) # ----------------------------------------------------------------- format --- # ONE REFERENCE INSTANT, ENUMERATED. Every named constant `arrow/__init__.py' # exports is rendered against it, so a release that changed one is named here. _r = arrow.get('2013-05-11T21:23:58.970460+07:00') emit('fmt_default', _r.format()) emit('fmt_tokens', _r.format('YYYY-MM-DD HH:mm:ss ZZ')) emit('fmt_offset_z', _r.format('Z')) emit('fmt_12_hour', _r.format('YYYY-MM-DD hh:mm:ss A')) emit('fmt_day_names', _r.format('dddd DDDD ddd MMMM MMM')) emit('fmt_escaped', _r.format('[today is] YYYY')) emit('fmt_atom', _r.format(arrow.FORMAT_ATOM)) # `FORMAT_COOKIE' AND `FORMAT_RFC850' END IN A SPACE HERE, and that is a # finding rather than a rendering artefact: both templates end in the `ZZZ' # token, which is the timezone NAME, and a fixed-offset tzinfo parsed out of # `+07:00' has none -- so the token renders empty and the separator before it # survives. Neither string is a valid Cookie or RFC 850 date as a result. The # two are emitted with a `|' terminator so the trailing space is visible in the # value and can be carried by a shell literal at all. emit('fmt_cookie', _r.format(arrow.FORMAT_COOKIE) + '|') emit('fmt_rfc822', _r.format(arrow.FORMAT_RFC822)) emit('fmt_rfc850', _r.format(arrow.FORMAT_RFC850) + '|') # ...and the same two rendered for an instant that DOES carry a zone name, so # the emptiness above is shown to be about the tzinfo and not about the token. emit('fmt_cookie_named_zone', _r.to('America/Los_Angeles').format(arrow.FORMAT_COOKIE) + '|') emit('fmt_rfc1036', _r.format(arrow.FORMAT_RFC1036)) emit('fmt_rfc1123', _r.format(arrow.FORMAT_RFC1123)) emit('fmt_rfc2822', _r.format(arrow.FORMAT_RFC2822)) emit('fmt_rfc3339', _r.format(arrow.FORMAT_RFC3339)) emit('fmt_rss', _r.format(arrow.FORMAT_RSS)) emit('fmt_w3c', _r.format(arrow.FORMAT_W3C)) emit('str_is_isoformat', 'yes' if str(_r) == _r.isoformat() else 'no') # A FORMAT ROUND-TRIPS THROUGH THE PARSER for the two RFC constants a wire # protocol actually carries. emit_call('roundtrip_rfc3339', lambda: arrow.get(_r.format(arrow.FORMAT_RFC3339), arrow.FORMAT_RFC3339).isoformat()) emit_call('roundtrip_rfc2822', lambda: arrow.get(_r.format(arrow.FORMAT_RFC2822), arrow.FORMAT_RFC2822).isoformat()) # --------------------------------------------------------------- humanize --- # ALWAYS AGAINST AN EXPLICIT `other'. See the header: with the argument # omitted the answer is scored against the ambient clock and is not assertable. _base = arrow.get('2013-05-05T12:30:45Z') emit('hum_hour_ago', _base.humanize(_base.shift(hours=+1))) emit('hum_hour_hence', _base.humanize(_base.shift(hours=-1))) emit('hum_seconds', _base.humanize(_base.shift(seconds=+5))) emit('hum_minutes', _base.humanize(_base.shift(minutes=+45))) emit('hum_hours', _base.humanize(_base.shift(hours=+5))) emit('hum_day', _base.humanize(_base.shift(days=+1))) emit('hum_days', _base.humanize(_base.shift(days=+3))) emit('hum_week', _base.humanize(_base.shift(weeks=+1))) emit('hum_month', _base.humanize(_base.shift(months=+1))) emit('hum_months', _base.humanize(_base.shift(months=+5))) emit('hum_year', _base.humanize(_base.shift(years=+1))) emit('hum_years', _base.humanize(_base.shift(years=+5))) emit('hum_now', _base.humanize(_base)) emit('hum_granularity_hour', _base.humanize(_base.shift(hours=+5), granularity='hour')) emit('hum_granularity_list', _base.humanize(_base.shift(days=+2, hours=+3), granularity=['day', 'hour'])) emit('hum_only_distance', _base.humanize(_base.shift(hours=+5), only_distance=True)) emit_call('hum_locale_fr', lambda: _base.humanize(_base.shift(hours=+5), locale='fr-fr')) emit('hum_locale_unknown_raises', raises(_base.humanize, _base.shift(hours=+5), locale='xx-yy')) # `dehumanize' IS THE INVERSE and takes the same locale. emit_call('dehumanize', lambda: _base.dehumanize('in 5 hours').isoformat()) emit('dehumanize_unparseable_raises', raises(_base.dehumanize, 'blah')) # THE LOCALE REGISTRY IS ASSERTED BY NAME, not by rendering -- see the header. from arrow import locales as _locales # noqa: E402 emit('locale_count', len(_locales._locale_map)) emit('locale_has_en_us', 'yes' if 'en-us' in _locales._locale_map else 'no') emit('locale_has_fr_fr', 'yes' if 'fr-fr' in _locales._locale_map else 'no') emit('locale_default', arrow.constants.DEFAULT_LOCALE) # --------------------------------------------------------------------- to --- # A CONVERSION CHANGES THE WALL CLOCK AND NOT THE INSTANT, and both halves are # asserted: the offsets on each side, and that the epoch second is unchanged. _utc = arrow.get('2013-05-11T14:23:58.970460+00:00') _pacific = _utc.to('America/Los_Angeles') _kolkata = _utc.to('Asia/Kolkata') _moscow = _utc.to('Europe/Moscow') emit('to_source_offset', int(_utc.utcoffset().total_seconds())) emit('to_pacific', _pacific.format('YYYY-MM-DD HH:mm:ss ZZ')) emit('to_pacific_offset', int(_pacific.utcoffset().total_seconds())) emit('to_kolkata', _kolkata.format('YYYY-MM-DD HH:mm:ss ZZ')) # HALF-HOUR OFFSETS EXIST. A caller who stores an offset in whole hours is # wrong for India, and this is the case that says so. emit('to_kolkata_offset', int(_kolkata.utcoffset().total_seconds())) emit('to_moscow', _moscow.format('YYYY-MM-DD HH:mm:ss ZZ')) emit('to_moscow_offset', int(_moscow.utcoffset().total_seconds())) emit('to_instant_preserved', 'yes' if (_utc.int_timestamp == _pacific.int_timestamp == _kolkata.int_timestamp == _moscow.int_timestamp) else 'no') emit('to_equality', 'yes' if _utc == _pacific else 'no') emit('to_utc_roundtrip', _pacific.to('UTC').isoformat()) emit('to_utc_alias_offset', int(_utc.to('utc').utcoffset().total_seconds())) emit('to_unknown_zone_raises', raises(_utc.to, 'Not/AZone')) # A ZONE'S NAME ON EACH SIDE OF ITS OWN DST BOUNDARY, which is what makes # `tzname()' worth reporting at all. emit('tzname_pacific_summer', arrow.get('2023-07-01T12:00:00Z').to('America/Los_Angeles').tzname()) emit('tzname_pacific_winter', arrow.get('2023-01-01T12:00:00Z').to('America/Los_Angeles').tzname()) emit('offset_pacific_summer', int(arrow.get('2023-07-01T12:00:00Z') .to('America/Los_Angeles').utcoffset().total_seconds())) emit('offset_pacific_winter', int(arrow.get('2023-01-01T12:00:00Z') .to('America/Los_Angeles').utcoffset().total_seconds())) # THE ZONES ABOVE COME FROM THE IMAGE'S OWN tzdata through `dateutil.tz'. A # rootfs without /usr/share/zoneinfo answers None for every one of them, so # this case separates "the package is broken" from "the database is missing". from dateutil import tz as _tz # noqa: E402 emit('tzdata_reachable', 'yes' if _tz.gettz('America/Los_Angeles') is not None else 'no') # EVERY ZONE NAMED IN THIS FILE IS A CANONICAL IANA NAME, never one of the # `backward' compatibility links. `US/Pacific' is a link to # `America/Los_Angeles' in the full database and is ABSENT from a slim one -- # the x86-64 control this file's literals were measured on carries 454 zoneinfo # files and has no `US/' directory at all, while the image's tzdata carries # ~1800. A case naming a link would therefore assert something about which # tzdata build shipped rather than about arrow. # ------------------------------------------------- span, floor, ceil, range --- emit('span_hour', ','.join(x.isoformat() for x in _r.span('hour'))) emit('span_day', ','.join(x.isoformat() for x in _r.span('day'))) emit('span_week_start', _r.span('week')[0].format('YYYY-MM-DD ddd')) emit('floor_day', _r.floor('day').isoformat()) emit('ceil_day', _r.ceil('day').isoformat()) # THE SPAN IS HALF-OPEN BY A MICROSECOND, not by a second: `ceil' is # 23:59:59.999999, which is what a caller comparing with `<=' has to know. emit('ceil_microsecond', _r.ceil('day').microsecond) emit('range_hours_count', len(list(arrow.Arrow.range('hour', arrow.get('2013-05-05T12:30:00Z'), arrow.get('2013-05-05T17:30:00Z'))))) # `Arrow.range' RETURNS A GENERATOR, not a list -- `[0]' on it is a TypeError, # which is the first thing a caller writes and the first thing that breaks. emit('range_type', type(arrow.Arrow.range('hour', arrow.get('2013-05-05T12:30:00Z'), arrow.get('2013-05-05T17:30:00Z'))).__name__) emit('range_subscript_raises', raises(lambda: arrow.Arrow.range('hour', arrow.get('2013-05-05T12:30:00Z'), arrow.get('2013-05-05T17:30:00Z'))[0])) _rng = list(arrow.Arrow.range('hour', arrow.get('2013-05-05T12:30:00Z'), arrow.get('2013-05-05T17:30:00Z'))) emit('range_first_last', '%s,%s' % (_rng[0].isoformat(), _rng[-1].isoformat())) emit('span_range_count', len(list(arrow.Arrow.span_range('hour', arrow.get('2013-05-05T12:30:00Z'), arrow.get('2013-05-05T17:30:00Z'))))) emit('interval_count', len(list(arrow.Arrow.interval('hour', arrow.get('2013-05-05T12:00:00Z'), arrow.get('2013-05-05T18:00:00Z'), 2)))) # ------------------------------------------------------------- properties --- emit('prop_naive', _r.naive.isoformat()) emit('prop_naive_tzinfo', 'None' if _r.naive.tzinfo is None else 'set') emit('prop_datetime_tz_offset', int(_r.datetime.utcoffset().total_seconds())) emit('prop_timestamp', repr(_r.timestamp())) emit('prop_int_timestamp', repr(_r.int_timestamp)) emit('prop_float_timestamp', repr(_r.float_timestamp)) emit('prop_fields', '%d,%d,%d,%d,%d,%d,%d' % ( _r.year, _r.month, _r.day, _r.hour, _r.minute, _r.second, _r.microsecond)) emit('prop_isocalendar', ','.join(str(x) for x in _r.isocalendar())) emit('prop_weekday', _r.weekday()) emit('prop_timetuple_yday', _r.timetuple().tm_yday) emit('prop_for_json', _r.for_json()) emit('prop_utcoffset_seconds', int(_r.utcoffset().total_seconds())) emit('prop_dst_seconds', int(_r.dst().total_seconds())) # ARITHMETIC WITH A `timedelta' IS ABSOLUTE, which is the contrast `shift' # above is written against. emit('arith_add_timedelta', (_base + _dt.timedelta(days=1)).isoformat()) emit('arith_sub_arrow_seconds', int((_base.shift(hours=+2) - _base).total_seconds())) emit('arith_compare', 'yes' if _base < _base.shift(seconds=+1) else 'no') # ----------------------------------------------------------- the factory --- # `ArrowFactory' is the documented way to get a SUBCLASS back out of `get', # and `arrow.factory' is how a caller reaches it. class _MyArrow(arrow.Arrow): pass _f = arrow.api.factory(_MyArrow) emit('factory_type', type(_f.get('2013-05-05T12:30:45Z')).__name__) emit('factory_is_arrow', 'yes' if isinstance(_f.get('2013-05-05T12:30:45Z'), arrow.Arrow) else 'no') # ------------------------------------------------------------- the closure --- # NOTHING OUTSIDE THE STANDARD LIBRARY BUT `dateutil' IS REACHED BY THE IMPORT. # This image's python3 is ONE STATIC BINARY with an empty lib-dynload and no # dynamic loader (BRINTOS-1476), so a release that grew a third-party # dependency would fail at `import' here rather than resolve it. # # Scored against `sys.stdlib_module_names' rather than against a literal list: # what the import delta CONTAINS depends on what the interpreter had already # loaded, so a literal here would red on target for a reason that has nothing # to do with arrow. _pulled = sorted(_AFTER_IMPORT - _BASELINE - {'arrow'}) _stdlib = getattr(sys, 'stdlib_module_names', None) if _stdlib is None: emit('pulled_nonstdlib', '') else: emit('pulled_nonstdlib', ','.join(sorted({n.split('.')[0] for n in _pulled if n.split('.')[0] not in _stdlib and not n.startswith('arrow')})) or '') # ...and the four names the bbappend's RDEPENDS line is about, asserted # POSITIVELY. `dateutil' is the third-party one; `calendar' and `datetime' are # python3-datetime's and `decimal' is python3-numbers'. emit('needs_dateutil', 'yes' if 'dateutil' in sys.modules else 'no') emit('needs_datetime', 'yes' if 'datetime' in sys.modules else 'no') emit('needs_calendar', 'yes' if 'calendar' in sys.modules else 'no') emit('needs_decimal', 'yes' if 'decimal' in sys.modules else 'no') # ...and the two the bbappend REMOVES, asserted negatively -- both against the # IMPORT SNAPSHOT, because the claim being made is about what `import arrow' # reaches. `dateutil.tz' imports `dateutil.zoneinfo' LAZILY, on the first # lookup the system database cannot answer, and this file performs exactly such # a lookup itself in `to_unknown_zone_raises' above -- so scored against live # `sys.modules' the case reports that lazy import rather than arrow's RDEPENDS, # and reds on any image carrying `python3-dateutil-zoneinfo' however arrow is # packaged. emit('no_json', 'yes' if 'json' not in _AFTER_IMPORT - _BASELINE else 'no') emit('no_dateutil_zoneinfo', 'yes' if 'dateutil.zoneinfo' not in _AFTER_IMPORT - _BASELINE else 'no') # `typing_extensions' IS UNREACHABLE ON 3.12 -- every one of its five imports # is under `if sys.version_info < (3, 8)'. emit('no_typing_extensions', 'yes' if 'typing_extensions' not in sys.modules else 'no') emit('python_major_minor', '%d.%d' % sys.version_info[:2]) emit('probe_done', 'yes') emit('emitted_count', len(emitted) + 1)