. "$BST_LIB" # BST-TIMEOUT: 1200 # THREE python3 starts, and they are the whole cost. # # Everything else reads the filesystem or dpkg with shell # # builtins, or greps one probe transcript. # # # # The three: a bare `import isodate', a second import whose # # STDERR is the assertion, and the probe -- which takes ONE # # start for 229 measurements, upstream's own 278-case # # unittest suite among them. This package ships NO # # executable, so unlike 99-python3-docopt there is no # # shipped example program to run as a real process and no # # exit status to assert: nothing here needs a fourth start. # # # # The work inside a start is a few hundred `re` matches # # over strings under 40 bytes, plus upstream's suite, which # # measures 0.018s on the x86-64 control. Microseconds beside # # an interpreter start on this arm, which is tens of seconds # # before the first line of the program runs. # # # # 1200 is 400s per start, against the ~60s a python3 start # # has measured at here. The start dominates and nothing in # # this file is quadratic in anything. # # # # NOT MEASURED ON TARGET YET: this port's first on-target # # run is the batch verify. The measured elapsed goes in the # # test plan rather than in this comment, which would cost an # # image build to keep the shipped suite byte-identical. # # python3-isodate 0.6.1 (poky/meta recipes-devtools/python/python3-isodate_0.6.1.bb, # `inherit pypi setuptools3', PyPI sdist) on the wasm32-hwjs glibc image. # # ============================================================================ # WHAT THE PACKAGE DOES, AND WHY EVERY CASE BELOW COMES IN PAIRS. # ============================================================================ # # isodate is an ISO 8601 implementation that goes BOTH WAYS. Dates, times, # datetimes, durations and zones each have a `parse_' that reads the standard's # several notations into a Python value, and a matching `..._isoformat' that # writes a value back out in whichever of those notations a format constant # names. A parser alone would be half a library. # # So the suite is built on the round trip, in both directions, because neither # closes on its own: # # FORWARD a fixture string parsed and rendered back through the constant # matching its own form returns the canonical form of the input. # BACKWARD a value CONSTRUCTED here, rendered and re-parsed, returns the # value. # # A parser and a renderer that were wrong in the same direction pass the # forward half. That is what the backward half is for, and why every `rt_*' # group below has both. # # ============================================================================ # THE TYPE IS THE ASSERTION, NOT JUST THE VALUE. # ============================================================================ # # `parse_duration' returns `datetime.timedelta' for a duration naming neither # years nor months, and isodate's OWN `Duration' when it names either -- because # a year is not a fixed number of days and a timedelta cannot represent one. A # caller that assumed one type gets the other from a string it did not choose. # # Every duration case therefore asserts the TYPE beside the value (`durtype_*'), # and every value in this file is compared through `repr', which names the # class. `str(timedelta(days=730))' and `str(Duration(years=2))' are different # text, but `repr' is what makes `datetime.timedelta(days=730)' and # `isodate.duration.Duration(730, 0, 0, years=0, months=0)' impossible to # confuse -- and the Duration arithmetic against a fixed date is asserted # separately, because that is where the two types actually diverge. # # ============================================================================ # THE PARSERS ARE LOOSE AT THE END, AND THAT IS THE MOST USEFUL THING HERE. # ============================================================================ # # Every parser except `parse_duration' matches with `pattern.match', which # anchors at the START only. A string whose PREFIX is a valid ISO form parses, # and the rest is silently discarded -- so `2024-5-17', which a caller writes by # hand all the time, does not raise: `2024' matches the bare-year pattern and # the answer is the 1st of January, four months and sixteen days from what was # meant. `parse_duration' alone anchors both ends and rejects a tail. # # The `loose-*' and `strict-*' cases state that asymmetry. Nothing in the # package's docstrings does. # # ============================================================================ # THE FAILURE PATH HAS THREE OUTCOMES, NOT TWO. # ============================================================================ # # ISO8601Error the string is not an ISO form the parser recognises. # ValueError it IS a recognised form, and names a value `datetime' will # not hold -- `2024-13-01', `99:99:99'. The parser succeeded # and the CONSTRUCTOR refused. # TypeError `parse_duration' given a non-string, which it type-checks # through `six.string_types'. # # ISO8601Error derives from ValueError, so an existing `except ValueError' # catches the first two and cannot tell them apart, while `except ISO8601Error' # catches only the first and lets `2024-13-01' through. Both directions are # asserted, because which one a caller wrote decides which faults reach them. # # And `parse_tzinfo' has NO failure path in practice: every group in its regex # is optional, so it matches the empty prefix of anything and answers `None' -- # the "local time" designator -- for `+' and for `banana' alike. Validating a # zone designator by catching an exception validates nothing. # # ============================================================================ # UPSTREAM'S OWN 278 TESTS RUN ON THIS IMAGE. # ============================================================================ # # setup.py names `isodate.tests' in `packages', so test_date, test_time, # test_datetime, test_duration, test_strf and test_pickle land in # site-packages beside the library -- and python3-unittest is on this image, so # the whole of the author's suite executes for the cost of the interpreter start # the probe has already paid. `upstream-*' is that result. # # That is evidence of a different kind from everything else in this file: the # ~120 hand-written cases are this port's statements about the API a caller # reaches, chosen and measured here; upstream's are the author's statements # about the implementation, including the pickle round trip and the # fraction-carrying edges no hand-written table would think to cover. # # THE RECIPE DOES NOT DECLARE python3-unittest. The tests it ships are runnable # here only because the image installs unittest for other reasons, so # `upstream-ran' is what would go quiet if that stopped -- and it would go quiet # rather than red, which is why it is asserted as a COUNT. # # ============================================================================ # WHY IT PORTS AT ALL: PURE PYTHON, AND TWO DEPENDENCIES THAT ARE ALREADY HERE. # ============================================================================ # # This image's python3 is ONE STATIC BINARY with an EMPTY lib-dynload and no # dynamic loader (BRINTOS-1476; 99-python3 asserts both directly), so a module # carrying a compiled extension could not be imported here -- not "would be # slow", could not be imported. isodate ships nine .py files and no C: the wheel # says `Root-Is-Purelib: true' and `pkg-no-so' below says the same thing about # what actually landed. # # Its two runtime dependencies are both already on this image. `python3-six' # (BRINTOS-1744) is imported by isoduration.py at IMPORT time, so its absence # would be an import failure rather than a parse failure. `python3-numbers' # supplies `decimal', which both parse_time and parse_duration do their # fractional arithmetic in -- a hard requirement of the PARSERS. Neither waits # on anything, which is why this port waits on nothing. # # ============================================================================ # WHERE THE EXPECTED VALUES CAME FROM. # ============================================================================ # # Every literal below was measured on x86-64 against the sdist verified against # the recipe's own SRC_URI sha256 (48c5881d...bde9) before it was read, and # against the built .deb for the payload cases. The control interpreter is # CPython 3.12 and the image ships 3.12, the same minor. # # The fixtures are one instant, deliberately: 2024-05-17T15:04:05, a Friday in # week 20 of a leap year, day 138. Six notations name that same day, so a # week-date parser off by one, or an ordinal parser that forgot the leap day, # lands somewhere its five neighbours do not and says so -- which six unrelated # fixtures could not. # # NOTHING HERE READS THE CLOCK OR THE ZONE. `LOCAL' is asserted by what it IS, # never by the offset it reports, which is the image's TZ: a case pinning that # would red on any image whose zone moved rather than on a broken isodate. # Checked against the upstream suite, which passes identically under TZ unset, # UTC, America/New_York and Europe/Moscow. # # The on-target result is not yet measured: the batch verify is this port's # first run on wasm32-hwjs. P=python3-isodate # THE CANARY, which asserts nothing about python3-isodate. # # A suite's FIRST BST result line is lost between the guest's printf and the # host's read -- per-suite, deterministic, and NOT a defect in the host tool # (BRINTOS-1149 established that and filed it as linux-in-the-browser#177). The # harness computes its pass count as total-minus-failures, so THE DROPPED LINE # IS COUNTED AS A PASS. A nothing-case therefore stands in that position and the # eaten line costs nothing. It sits ahead of the `require' guards deliberately, # because a `require' that skips would emit its skip as the first line and be # eaten too. # # Remove this case when linux-in-the-browser#177 is fixed. bst_ok $P/suite-canary require python3 $P/python3-present SITE=/usr/lib/python3.12/site-packages PKGDIR=$SITE/isodate DI=$SITE/isodate-0.6.1.dist-info PKGLIST=/var/lib/dpkg/info/python3-isodate.list PROBE=$BST_DATA/python3-isodate-probe.py tmp=${TMPDIR:-/tmp} W=$tmp/bst-isodate.$$ mkdir -p "$W" || { bst_notok $P/workdir "cannot create $W"; exit 0; } # ------------------------------------------------------------- the payload --- # Where the package has to land. `import isodate' further down is the functional # statement; these say WHICH copy answered, so a package importing out of some # other path on sys.path is distinguishable from one importing out of what this # image installed. check $P/site-packages-dir test -d "$SITE" # A PACKAGE, NOT A MODULE -- the opposite shape from python3-docopt's single # file, and a release that flattened it would change the import surface without # any behavioural case noticing. check $P/package-dir test -d "$PKGDIR" check $P/package-init test -s "$PKGDIR/__init__.py" check $P/no-flat-module test ! -f "$SITE/isodate.py" # THE NINE SUBMODULES, each named. `__init__.py' re-exports from all of them, so # a missing one is an ImportError at `import isodate' and not at first use -- # and these cases are what say which. for _m in duration isodates isodatetime isoduration isoerror isostrf isotime \ isotzinfo tzinfo; do check $P/module-$_m test -s "$PKGDIR/$_m.py" done # The precompiled bytecode, which `import-quiet' below turns on. check $P/pycache test -s "$PKGDIR/__pycache__/__init__.cpython-312.pyc" # UPSTREAM'S TESTS SHIP, and that is a packaging fact worth pinning rather than # a happy accident: setup.py names `isodate.tests' in `packages', so the .deb # carries the author's six test modules into site-packages. `upstream-ran' below # is what runs them; these are what say they are here to run. check $P/tests-dir test -d "$PKGDIR/tests" for _t in test_date test_datetime test_duration test_pickle test_strf test_time; do check $P/tests-$(echo "$_t" | tr '_' '-') test -s "$PKGDIR/tests/$_t.py" done # NO COMPILED EXTENSION ANYWHERE THE PACKAGE PUT ANYTHING. On this image a .so # could not be loaded at all, so this is the case that fails a version bump # which grew one -- at install rather than at `import'. # # BRINTOS-WORKAROUND: brintos/linux#73 remove -- read `$PKGLIST' directly when # the redirect stops failing. The list is copied to tmpfs first and never # redirected from the rootfs: a top-level stdin redirect on the virtiofs root # makes dup2(2) return 513 and ash EXITS, ending the suite silently. cat "$PKGLIST" > "$W/pkgf" 2>/dev/null || : > "$W/pkgf" check $P/pkg-list-present test -s "$W/pkgf" check_eq $P/pkg-no-so "" \ "$(grep -E '\.so($|\.)' "$W/pkgf" 2>/dev/null)" # ...AND NOTHING IN ${bindir}. isodate declares no `scripts' and no # `console_scripts': it is a library with no CLI, so there is no executable and # no shebang to assert. Stated as a fact about the package rather than left to # be inferred from a case nobody wrote. check_eq $P/pkg-no-bin "" \ "$(grep -E '^/usr/s?bin/' "$W/pkgf" 2>/dev/null)" # EVERY REGULAR FILE THE PACKAGE SHIPS IS PYTHON OR ITS METADATA. The two cases # above name the failures that are expected; this one catches the shape nobody # predicted -- a binary, a .pyd, a stray data file -- without having to # enumerate what it might be. # # A dpkg .list names DIRECTORIES too, and without a trailing slash, so each # entry is tested for being a regular file first. _bad="" while IFS= read -r _f; do [ -f "$_f" ] || continue case "$_f" in *.py|*.pyc) continue ;; */__pycache__/*) continue ;; */*.dist-info/*) continue ;; esac _bad="$_bad $_f" done < "$W/pkgf" check_eq $P/pkg-only-python "" "$_bad" # ----------------------------------------------------------- the dist-info --- # The installed metadata, and here it is the ONLY statement of the version: # isodate defines no `__version__', so unlike python3-docopt there is no second # reading to cross-check against -- which is itself asserted, below, as # `has-dunder-version'. check $P/dist-info-dir test -d "$DI" check $P/dist-info-metadata test -f "$DI/METADATA" check_out $P/dist-info-version "Version: 0.6.1" cat "$DI/METADATA" # top_level.txt names the ONE importable this package installs; a wheel that # renamed the package would change it here first. check_out $P/dist-info-top-level "isodate" cat "$DI/top_level.txt" # PURE PYTHON, AS THE WHEEL ITSELF DECLARES IT -- the packaging-side statement # of `pkg-no-so' above: no platform tag, no extension module, nothing that could # need the absent dynamic loader. check_out $P/dist-info-purelib "Root-Is-Purelib: true" cat "$DI/WHEEL" # NO LICENCE FILE IN THE DIST-INFO, and that is measured rather than assumed. # This recipe's LIC_FILES_CHKSUM points at line 8 of the sdist's PKG-INFO, which # the wheel does not carry -- so unlike python3-docopt there is nothing here to # check the licence text against, and a case asserting one would red forever. check $P/dist-info-no-license test ! -e "$DI/LICENSE" # ------------------------------------------------------------- the package --- # dpkg KNOWS the package. Every case above would look identical if some other # package had installed the same files; this is the one that says # python3-isodate is a real package this image installed rather than an # IMAGE_INSTALL line that resolved to nothing. sed -n '/^Package: python3-isodate$/,/^$/p' /var/lib/dpkg/status > "$W/st" 2>/dev/null check $P/pkg-known test -s "$W/st" check_out $P/pkg-installed "Status: install ok installed" cat "$W/st" # THE TWO RUNTIME DEPENDENCIES THE RECIPE DECLARES, as dpkg recorded them. See # the header: python3-six is needed at IMPORT time by isoduration.py, and # python3-numbers supplies the `decimal' both fractional parsers compute in. # Each is asserted by name, because "the Depends line is non-empty" would pass # with either of them missing. grep '^Depends:' "$W/st" > "$W/dep" 2>/dev/null || : > "$W/dep" check $P/pkg-depends-line test -s "$W/dep" check_out $P/pkg-depends-core "python3-core" cat "$W/dep" check_out $P/pkg-depends-six "python3-six" cat "$W/dep" check_out $P/pkg-depends-numbers "python3-numbers" cat "$W/dep" # ...AND python3-unittest IS NOT AMONG THEM, which is the RDEPENDS gap the # header names: this package ships its own tests and does not declare what runs # them. Asserted so the gap is a stated fact rather than something a later # reader has to notice. check_eq $P/pkg-no-unittest-depend "" \ "$(grep -o 'python3-unittest' "$W/dep" 2>/dev/null)" # --------------------------------------------------------- the interpreter --- # THE IMPORT, ON ITS OWN, BEFORE THE PROBE. If isodate cannot be imported then # every probe case below reds at once and each names a different symptom; this # case is what says the cause is the import. It is also the cheapest possible # statement of the deliverable -- and, because isoduration.py imports six at # module level, the cheapest statement that python3-six is really here. check $P/import python3 -c 'import isodate' # ...AND THE IMPORT SAYS NOTHING WHILE DOING IT. A statement about the shipped # bytecode rather than about style: a module compiled from SOURCE at import time # would emit whatever diagnostics 3.12 raises over this code, and the package # ships a precompiled .pyc precisely so the compiler never runs. A red here # means the shipped .pyc was REJECTED -- a stale hash, a python3 minor bump that # changed the magic, or a missing __pycache__ -- and every import on this image # is paying to recompile nine modules. check_eq $P/import-quiet "" \ "$(python3 -c 'import isodate' 2>&1)" # ---------------------------------------------------------------- the probe --- if [ ! -f "$PROBE" ]; then bst_notok $P/probe-present "$PROBE did not ship" rm -rf "$W" exit 0 fi bst_ok $P/probe-present # ONE interpreter start for everything that follows. bst_probe bounds it and # keeps BOTH streams: a probe that dies early otherwise leaves every case below # reporting got:'' and naming a symptom each, with the actual traceback nowhere. # The bound is generous because a python3 start on this arm is tens of seconds # before any work begins. bst_probe $P/probe-run "$W/probe" 600 python3 "$PROBE" OUT="$W/probe" # Read a single key. The probe emits each key at most once, so head -n 1 is # defensive rather than load-bearing; the `^' and `=' anchors keep `dur_year' # distinguishable from `dur_year_days'. pv() { grep "^$1=" "$OUT" 2>/dev/null | head -n 1 | cut -d= -f2-; } # THE EVIDENCE PLANE. If the probe emitted nothing, every case below is reading # an absent file: each would red with got:'' and the set would read exactly like # a broken library, when what actually happened is that no measurement was # taken. The word `unscorable' is PARSED -- brintos_local_verify re-measures a # suite that prints it on a fresh guest rather than trusting a shared boot's # numbers (BRINTOS-1644) -- so it goes in this structural check, not a comment. # # The cases below still print their reds: withdraw, do not hide. This case is # what says what those reds measure. if [ -s "$OUT" ]; then bst_ok $P/probe-output else bst_notok $P/probe-output "the probe emitted nothing -- python3 did not start, or died before its first line; every CASE below is unscorable" fi # ...and it ran to the END. Every key is emitted in file order, so a probe that # died two thirds of the way through would leave the earlier cases green and the # later ones reading got:'' -- which looks like a partial library rather than a # partial run. This is the case that tells those apart, and `probe-count' is # what catches a key skipped in the middle. check_eq $P/probe-complete "yes" "$(pv probe_done)" check_eq $P/emitted-count "228" "$(pv emitted_count)" # The module answered out of the file this image installed, not from a stray # copy on sys.path -- which is what makes the payload cases above and the # behavioural cases below statements about the SAME isodate. check_eq $P/module-answered "$PKGDIR/__init__.py" "$(pv module_file)" # ------------------------------------------------------------- the identity --- # The shape of what was imported. `submodule-count' is nine because # `__init__.py' re-exports from every one of them at import time, so this # doubles as a statement that none of the nine failed to load. check_eq $P/is-package "yes" "$(pv is_package)" check_eq $P/submodule-count "9" "$(pv submodule_count)" # THE VERSION HAS EXACTLY ONE SOURCE, and that is the fact worth stating rather # than discovering: no `__version__' attribute, so the dist-info read through # importlib.metadata is the whole of it. python3-docopt carries two and this # suite's sibling cross-checks them against each other; here there is nothing to # cross-check, and `has-dunder-version' is the case that says why. check_eq $P/has-dunder-version "no" "$(pv has_dunder_version)" check_eq $P/dist-version "0.6.1" "$(pv dist_version)" # THE PUBLISHED SURFACE IS MOSTLY CONSTANTS: 48 names in `__all__', of which ten # are functions and one a class -- the other thirty-odd are format strings. That # ratio is the package's design, and it is why every `fmt-*' case below asserts # through `isodate.DATE_*' rather than against a pattern written out here: the # constants ARE the API, so a suite spelling '%Y-%m-%d' itself would keep # passing after a release changed what DATE_EXT_COMPLETE means. check_eq $P/all-count "48" "$(pv all_count)" check_eq $P/all-sorted-head "DATE_BAS_COMPLETE,DATE_BAS_MONTH,DATE_BAS_ORD_COMPLETE,DATE_BAS_WEEK,DATE_BAS_WEEK_COMPLETE" "$(pv all_sorted_head)" # The sixteen names a caller actually reaches for, each pinned present. A red # here says the export list moved, which no behavioural case below could # distinguish from the function itself breaking. check_eq $P/has-parse-date "yes" "$(pv has_parse_date)" check_eq $P/has-parse-time "yes" "$(pv has_parse_time)" check_eq $P/has-parse-datetime "yes" "$(pv has_parse_datetime)" check_eq $P/has-parse-duration "yes" "$(pv has_parse_duration)" check_eq $P/has-parse-tzinfo "yes" "$(pv has_parse_tzinfo)" check_eq $P/has-date-isoformat "yes" "$(pv has_date_isoformat)" check_eq $P/has-time-isoformat "yes" "$(pv has_time_isoformat)" check_eq $P/has-datetime-isoformat "yes" "$(pv has_datetime_isoformat)" check_eq $P/has-duration-isoformat "yes" "$(pv has_duration_isoformat)" check_eq $P/has-tz-isoformat "yes" "$(pv has_tz_isoformat)" check_eq $P/has-strftime "yes" "$(pv has_strftime)" check_eq $P/has-Duration "yes" "$(pv has_Duration)" check_eq $P/has-ISO8601Error "yes" "$(pv has_ISO8601Error)" check_eq $P/has-UTC "yes" "$(pv has_UTC)" check_eq $P/has-FixedOffset "yes" "$(pv has_FixedOffset)" check_eq $P/has-LOCAL "yes" "$(pv has_LOCAL)" # THE TWO RUNTIME DEPENDENCIES, from inside the interpreter this time. `pkg- # depends-six' above says the packaging declares it; this says the import # actually happened, which is the half that would still be false if six were # declared and absent. check_eq $P/six-imported "yes" "$(pv six_imported)" check_eq $P/decimal-available "yes" "$(pv decimal_available)" check_eq $P/decimal-module-file "/usr/lib/python3.12/decimal.py" "$(pv decimal_module_file)" # ON THIS IMAGE THE decimal ACCELERATOR IS EXPECTED TO BE THE C ONE, COMPILED # IN. python3 here is one static binary -- configure picks MODULE_BUILDTYPE= # static from host_cpu=wasm32, so every extension module, `_decimal' among them, # is linked into the interpreter instead of landing in the empty lib-dynload. # # EXPECTED, NOT YET MEASURED: this is derived from the recipe, and the batch # verify is the first time it runs on target. A red here is a statement about # the interpreter's static extension set and NOT about isodate, which works # either way -- python3-numbers ships `_pydecimal' too and it is a complete # implementation. check_eq $P/decimal-accelerated "yes" "$(pv decimal_accelerated)" # ------------------------------------------------------------- parse_date --- # THE SIX NOTATIONS FOR ONE DAY. Extended and basic complete, ordinal in both, # and the week date in both -- every one of them 2024-05-17. See the header: it # is the agreement between them that makes this a statement about the parser. check_eq $P/date-ext "datetime.date(2024, 5, 17)" "$(pv date_ext)" check_eq $P/date-bas "datetime.date(2024, 5, 17)" "$(pv date_bas)" check_eq $P/date-ord-ext "datetime.date(2024, 5, 17)" "$(pv date_ord_ext)" check_eq $P/date-ord-bas "datetime.date(2024, 5, 17)" "$(pv date_ord_bas)" check_eq $P/date-week-ext "datetime.date(2024, 5, 17)" "$(pv date_week_ext)" check_eq $P/date-week-bas "datetime.date(2024, 5, 17)" "$(pv date_week_bas)" # THE REDUCED-ACCURACY FORMS, where the parser must INVENT what the string does # not carry. It chooses the first of everything: the Monday of the named week, # the 1st of the named month, January of a bare year -- and, for a century, the # 1st of January of year 1 OF that century, which is 2001 and not 2000. That # last is the surprise and the reason a century case exists at all. check_eq $P/date-week-only "datetime.date(2024, 5, 13)" "$(pv date_week_only)" check_eq $P/date-month-ext "datetime.date(2024, 5, 1)" "$(pv date_month_ext)" check_eq $P/date-year "datetime.date(2024, 1, 1)" "$(pv date_year)" check_eq $P/date-century "datetime.date(2001, 1, 1)" "$(pv date_century)" # ...and those invented components are OVERRIDABLE, which is the half a caller # reads the signature for. check_eq $P/date-year-defaults "datetime.date(2024, 6, 15)" "$(pv date_year_defaults)" # THE EXPANDED FORM -- more than four year digits, with a MANDATORY sign. It is # the one call shape where `yeardigits' is not 4, and passing it turns # `expanded' on by itself. A four-digit year takes a sign only when asked, and # the pair below is what says so: the same string is a parse or an error # depending on one argument. check_eq $P/date-expanded "datetime.date(2024, 5, 17)" "$(pv date_expanded)" check_eq $P/date-signed-ok "datetime.date(2024, 5, 17)" "$(pv date_signed_ok)" check_eq $P/date-signed-refused "yes" "$(pv date_signed_refused)" # --------------------------------------------------------- date_isoformat --- # EVERY DATE FORMAT CONSTANT THE MODULE PUBLISHES, over the same day. Note what # the reduced forms do here: rendering through DATE_BAS_WEEK or DATE_YEAR # DISCARDS the components the format does not name, so these are lossy on # purpose and the round-trip group below uses only the complete ones. check_eq $P/fmt-date-bas-complete "'20240517'" "$(pv fmt_date_bas_complete)" check_eq $P/fmt-date-ext-complete "'2024-05-17'" "$(pv fmt_date_ext_complete)" check_eq $P/fmt-date-bas-ord-complete "'2024138'" "$(pv fmt_date_bas_ord_complete)" check_eq $P/fmt-date-ext-ord-complete "'2024-138'" "$(pv fmt_date_ext_ord_complete)" check_eq $P/fmt-date-bas-week-complete "'2024W205'" "$(pv fmt_date_bas_week_complete)" check_eq $P/fmt-date-ext-week-complete "'2024-W20-5'" "$(pv fmt_date_ext_week_complete)" check_eq $P/fmt-date-bas-week "'2024W20'" "$(pv fmt_date_bas_week)" check_eq $P/fmt-date-ext-week "'2024-W20'" "$(pv fmt_date_ext_week)" check_eq $P/fmt-date-bas-month "'202405'" "$(pv fmt_date_bas_month)" check_eq $P/fmt-date-ext-month "'2024-05'" "$(pv fmt_date_ext_month)" check_eq $P/fmt-date-year "'2024'" "$(pv fmt_date_year)" check_eq $P/fmt-date-century "'20'" "$(pv fmt_date_century)" # The constants' own text, so a red above says whether the FORMAT moved or the # renderer did -- two keys for the two halves of one statement. check_eq $P/const-date-ext-complete "%Y-%m-%d" "$(pv const_date_ext_complete)" check_eq $P/const-date-ext-week-complete "%Y-W%W-%w" "$(pv const_date_ext_week_complete)" # A non-four `yeardigits' PREFIXES A SIGN on the way out too -- the rendering # side of the expanded form, and the only reason date_isoformat takes a third # argument. check_eq $P/fmt-date-expanded "'+002024-05-17'" "$(pv fmt_date_expanded)" # ------------------------------------------------------ the date round trip --- # Both directions, over the four complete notations. See the header: the # forward half alone would pass a parser and a renderer that were wrong # together, which is what the backward half -- from a date CONSTRUCTED in the # probe rather than parsed -- is there to catch. check_eq $P/rt-date-str-ext "'2024-05-17'" "$(pv rt_date_str_ext)" check_eq $P/rt-date-val-ext "datetime.date(2024, 5, 17)" "$(pv rt_date_val_ext)" check_eq $P/rt-date-str-bas "'20240517'" "$(pv rt_date_str_bas)" check_eq $P/rt-date-val-bas "datetime.date(2024, 5, 17)" "$(pv rt_date_val_bas)" check_eq $P/rt-date-str-ord "'2024-138'" "$(pv rt_date_str_ord)" check_eq $P/rt-date-val-ord "datetime.date(2024, 5, 17)" "$(pv rt_date_val_ord)" check_eq $P/rt-date-str-week "'2024-W20-5'" "$(pv rt_date_str_week)" check_eq $P/rt-date-val-week "datetime.date(2024, 5, 17)" "$(pv rt_date_val_week)" # ------------------------------------------------------------- parse_time --- # COMPLETE TIMES in both spellings, and the fractional second -- which is where # `decimal' earns its dependency: `.25' of a second is 250000 microseconds # EXACTLY, where a float path would land near it. A comma is a decimal # separator too; ISO 8601 says so and the module honours it, which a caller # reading only the docstring's examples would never try. check_eq $P/time-ext "datetime.time(15, 4, 5)" "$(pv time_ext)" check_eq $P/time-bas "datetime.time(15, 4, 5)" "$(pv time_bas)" check_eq $P/time-frac "datetime.time(15, 4, 5, 250000)" "$(pv time_frac)" check_eq $P/time-frac-comma "datetime.time(15, 4, 5, 250000)" "$(pv time_frac_comma)" # REDUCED ACCURACY, where the fraction belongs to the LAST component present -- # so `.5' of a minute is thirty seconds and `.5' of an hour is thirty minutes. # The parser carries the fraction down; these say so. check_eq $P/time-minute "datetime.time(15, 4)" "$(pv time_minute)" check_eq $P/time-minute-frac "datetime.time(15, 4, 30)" "$(pv time_minute_frac)" check_eq $P/time-hour-frac "datetime.time(15, 30)" "$(pv time_hour_frac)" # The `T' designator is accepted on a BARE time, which the ISO grammar allows # and which is what lets parse_datetime hand its right-hand half over unchanged. check_eq $P/time-t-prefix "datetime.time(15, 4, 5)" "$(pv time_t_prefix)" # THE ZONE, attached to a time: Z, and an offset in each of the three spellings # the standard gives it -- and absent, which yields a NAIVE time rather than the # host's zone. # # These four are emitted as `||' rather # than as a repr: `repr(time(..., tzinfo=))' carries the object's ADDRESS, and a case asserting that # would red at random. What the parse decided is the class and the offset, and # that is what is compared. check_eq $P/time-zulu "datetime.time(15, 4, 5)|Utc|datetime.timedelta(0)" "$(pv time_zulu)" check_eq $P/time-off-ext "datetime.time(15, 4, 5)|FixedOffset|datetime.timedelta(seconds=19800)" "$(pv time_off_ext)" check_eq $P/time-off-bas "datetime.time(15, 4, 5)|FixedOffset|datetime.timedelta(days=-1, seconds=57600)" "$(pv time_off_bas)" check_eq $P/time-off-hour "datetime.time(15, 4, 5)|FixedOffset|datetime.timedelta(seconds=7200)" "$(pv time_off_hour)" check_eq $P/time-naive-tz "None" "$(pv time_naive_tz)" # --------------------------------------------------------- time_isoformat --- # The time format constants, over a UTC time carrying microseconds -- note that # none of them renders the fraction, which no `%f' appears in. check_eq $P/fmt-time-bas-complete "'150405'" "$(pv fmt_time_bas_complete)" check_eq $P/fmt-time-ext-complete "'15:04:05'" "$(pv fmt_time_ext_complete)" check_eq $P/fmt-time-bas-minute "'1504'" "$(pv fmt_time_bas_minute)" check_eq $P/fmt-time-ext-minute "'15:04'" "$(pv fmt_time_ext_minute)" check_eq $P/fmt-time-hour "'15'" "$(pv fmt_time_hour)" # THE DEFAULT FORMAT CARRIES THE ZONE and the named constants do not: it is # TIME_EXT_COMPLETE + TZ_EXT, so a zoned time keeps its offset, a UTC one # renders `Z', and a naive one renders nothing extra rather than raising. All # three, because a caller who never passes a format gets this one. check_eq $P/fmt-time-default-utc "'15:04:05Z'" "$(pv fmt_time_default_utc)" check_eq $P/fmt-time-default-naive "'15:04:05'" "$(pv fmt_time_default_naive)" check_eq $P/fmt-time-default-off "'15:04:05+05:30'" "$(pv fmt_time_default_off)" # Both directions again. The backward half closes on a time CONSTRUCTED with # microseconds and a zone -- and comes back without the microseconds, because # the default format does not render them: the round trip closes on what the # FORMAT preserves, which is the honest statement. check_eq $P/rt-time-str-ext "'15:04:05'" "$(pv rt_time_str_ext)" check_eq $P/rt-time-str-frac "'15:04:05'" "$(pv rt_time_str_frac)" check_eq $P/rt-time-str-zulu "'15:04:05Z'" "$(pv rt_time_str_zulu)" check_eq $P/rt-time-str-off "'15:04:05+05:30'" "$(pv rt_time_str_off)" check_eq $P/rt-time-val "datetime.time(15, 4, 5)|Utc|datetime.timedelta(0)" "$(pv rt_time_val)" # --------------------------------------------------------- parse_datetime --- # THE `T' IS LOAD-BEARING: parse_datetime splits on it and hands the halves to # parse_date and parse_time. That is why it accepts any COMBINATION of their # forms -- an ordinal date against an extended time, a basic date against an # extended one -- combinations the ISO standard itself does not allow, and every # one of them a real parse here. `dt-mixed' is the case that states it. check_eq $P/dt-ext "datetime.datetime(2024, 5, 17, 15, 4, 5)" "$(pv dt_ext)" check_eq $P/dt-bas "datetime.datetime(2024, 5, 17, 15, 4, 5)" "$(pv dt_bas)" check_eq $P/dt-ord "datetime.datetime(2024, 5, 17, 15, 4, 5)" "$(pv dt_ord)" check_eq $P/dt-week "datetime.datetime(2024, 5, 17, 15, 4, 5)" "$(pv dt_week)" check_eq $P/dt-frac "datetime.datetime(2024, 5, 17, 15, 4, 5, 250000)" "$(pv dt_frac)" check_eq $P/dt-mixed "datetime.datetime(2024, 5, 17, 15, 4, 5)" "$(pv dt_mixed)" # ...and the zone SURVIVES the split-and-recombine, which is the one thing # `datetime.combine' could silently drop. Emitted in the same # `value|class|offset' form as the times above, and for the same reason. check_eq $P/dt-zulu "datetime.datetime(2024, 5, 17, 15, 4, 5)|Utc|datetime.timedelta(0)" "$(pv dt_zulu)" check_eq $P/dt-off "datetime.datetime(2024, 5, 17, 15, 4, 5)|FixedOffset|datetime.timedelta(seconds=19800)" "$(pv dt_off)" check_eq $P/dt-off-west "datetime.datetime(2024, 5, 17, 15, 4, 5)|FixedOffset|datetime.timedelta(days=-1, seconds=57600)" "$(pv dt_off_west)" check_eq $P/dt-tzname "'+05:30'" "$(pv dt_tzname)" check_eq $P/dt-utcoffset "datetime.timedelta(seconds=19800)" "$(pv dt_utcoffset)" # ----------------------------------------------------- datetime_isoformat --- # The six DT_* constants, each the concatenation of a date format, `T', a time # format and a zone format. `dt-ext-is-composed' asserts that composition # directly, so a red in one of the six can be read against which of its three # parts moved. check_eq $P/fmt-dt-bas-complete "'20240517T150405Z'" "$(pv fmt_dt_bas_complete)" check_eq $P/fmt-dt-ext-complete "'2024-05-17T15:04:05Z'" "$(pv fmt_dt_ext_complete)" check_eq $P/fmt-dt-bas-ord-complete "'2024138T150405Z'" "$(pv fmt_dt_bas_ord_complete)" check_eq $P/fmt-dt-ext-ord-complete "'2024-138T15:04:05Z'" "$(pv fmt_dt_ext_ord_complete)" check_eq $P/fmt-dt-bas-week-complete "'2024W205T150405Z'" "$(pv fmt_dt_bas_week_complete)" check_eq $P/fmt-dt-ext-week-complete "'2024-W20-5T15:04:05Z'" "$(pv fmt_dt_ext_week_complete)" check_eq $P/fmt-dt-default "'2024-05-17T15:04:05Z'" "$(pv fmt_dt_default)" check_eq $P/const-dt-ext-complete "%Y-%m-%dT%H:%M:%S%Z" "$(pv const_dt_ext_complete)" check_eq $P/dt-ext-is-composed "yes" "$(pv dt_ext_is_composed)" # Both directions, over the zoned forms. check_eq $P/rt-dt-str-ext "'2024-05-17T15:04:05Z'" "$(pv rt_dt_str_ext)" check_eq $P/rt-dt-str-off "'2024-05-17T15:04:05+05:30'" "$(pv rt_dt_str_off)" check_eq $P/rt-dt-val "datetime.datetime(2024, 5, 17, 15, 4, 5)|Utc|datetime.timedelta(0)" "$(pv rt_dt_val)" # --------------------------------------------------------- parse_duration --- # THE TYPE SWITCH, which is the whole of this section. See the header: a # duration naming neither years nor months comes back a `datetime.timedelta', # and one naming either comes back isodate's own `Duration' -- so the type # depends on the STRING, not on the call. Every case asserts both the value and # the type, because a release that collapsed the two types would return the # right numbers under the wrong class and no value-only case would notice. # # First the timedelta half, including the two fraction spellings and the sign. check_eq $P/dur-day "datetime.timedelta(days=1)" "$(pv dur_day)" check_eq $P/durtype-day "timedelta" "$(pv durtype_day)" check_eq $P/dur-week "datetime.timedelta(days=14)" "$(pv dur_week)" check_eq $P/durtype-week "timedelta" "$(pv durtype_week)" check_eq $P/dur-time "datetime.timedelta(seconds=5400)" "$(pv dur_time)" check_eq $P/durtype-time "timedelta" "$(pv durtype_time)" check_eq $P/dur-full-nomonth "datetime.timedelta(days=3, seconds=14706)" "$(pv dur_full_nomonth)" check_eq $P/durtype-full-nomonth "timedelta" "$(pv durtype_full_nomonth)" check_eq $P/dur-frac-sec "datetime.timedelta(microseconds=500000)" "$(pv dur_frac_sec)" check_eq $P/durtype-frac-sec "timedelta" "$(pv durtype_frac_sec)" check_eq $P/dur-frac-comma "datetime.timedelta(microseconds=500000)" "$(pv dur_frac_comma)" check_eq $P/durtype-frac-comma "timedelta" "$(pv durtype_frac_comma)" check_eq $P/dur-neg "datetime.timedelta(days=-1)" "$(pv dur_neg)" check_eq $P/durtype-neg "timedelta" "$(pv durtype_neg)" check_eq $P/dur-zero "datetime.timedelta(0)" "$(pv dur_zero)" check_eq $P/durtype-zero "timedelta" "$(pv durtype_zero)" # ...and the Duration half: a year, a month, both, both plus a timedelta part, # and a negative year. check_eq $P/dur-year "isodate.duration.Duration(0, 0, 0, years=1, months=0)" "$(pv dur_year)" check_eq $P/durtype-year "Duration" "$(pv durtype_year)" check_eq $P/dur-month "isodate.duration.Duration(0, 0, 0, years=0, months=3)" "$(pv dur_month)" check_eq $P/durtype-month "Duration" "$(pv durtype_month)" check_eq $P/dur-ym "isodate.duration.Duration(0, 0, 0, years=1, months=6)" "$(pv dur_ym)" check_eq $P/durtype-ym "Duration" "$(pv durtype_ym)" check_eq $P/dur-mixed "isodate.duration.Duration(3, 14706, 0, years=1, months=2)" "$(pv dur_mixed)" check_eq $P/durtype-mixed "Duration" "$(pv durtype_mixed)" check_eq $P/dur-neg-year "isodate.duration.Duration(0, 0, 0, years=-1, months=0)" "$(pv dur_neg_year)" check_eq $P/durtype-neg-year "Duration" "$(pv durtype_neg_year)" # THE ALTERNATIVE FORMAT -- a duration written as though it were a date-time. # It takes the SECOND branch of the parser, reached only when the period regex # fails, and it is the shape most callers never learn exists; `P0001-02-03T04: # 05:06' is the same duration as `P1Y2M3DT4H5M6S' and lands on an identical # Duration. check_eq $P/dur-alt "isodate.duration.Duration(3, 14706, 0, years=1, months=2)" "$(pv dur_alt)" check_eq $P/durtype-alt "Duration" "$(pv durtype_alt)" # A WEEK IS SEVEN DAYS AND NOT A COMPONENT: `P2W' comes back as fourteen days # with nothing left that remembers it was written in weeks. That is why `dfmt- # week-*' below is a rendering CHOICE and not a round trip. check_eq $P/dur-week-days "14" "$(pv dur_week_days)" # ------------------------------------------------------ Duration semantics --- # WHY THE SECOND TYPE EXISTS, asserted against a fixed date -- which is the only # thing that gives a year or a month a length. Adding `P1Y2M' to 2024-05-17 is a # calendar operation, not an arithmetic one, and it is what a timedelta cannot # do at all. check_eq $P/dur-add-year "datetime.date(2025, 5, 17)" "$(pv dur_add_year)" check_eq $P/dur-add-ym "datetime.date(2025, 7, 17)" "$(pv dur_add_ym)" check_eq $P/dur-add-mixed "datetime.datetime(2025, 7, 20, 4, 5, 6)" "$(pv dur_add_mixed)" check_eq $P/dur-sub-year "datetime.date(2023, 5, 17)" "$(pv dur_sub_year)" # THE MONTH-END CLAMP, which is the rule a caller cannot guess: one month after # the 31st of January is the LAST day of February, not the 2nd or 3rd of March # -- 2024-02-29 in a leap year. And one year after 2024-02-29 is 2025-02-28, by # the same clamp. This is w3.org's adding-durations-to-dateTimes algorithm, and # the single most surprising thing Duration does. check_eq $P/dur-clamp-month "datetime.date(2024, 2, 29)" "$(pv dur_clamp_month)" check_eq $P/dur-clamp-leap "datetime.date(2025, 2, 28)" "$(pv dur_clamp_leap)" # ONE YEAR IS NOT 365 DAYS UNTIL A DATE SAYS IT IS, which is what totimedelta # is for. From 2024-05-17 it is 365; from a date on the other side of a leap day # it would not be, which is exactly the point. check_eq $P/dur-totimedelta "datetime.timedelta(days=365)" "$(pv dur_totimedelta)" # EQUALITY COMPARES THE TWO HALVES SEPARATELY -- years-and-months against # years-and-months, timedelta against timedelta. So a Duration carrying no years # or months IS equal to the plain timedelta beside it, and one carrying a year # is NOT equal to 365 days however the calendar falls. Both halves, because that # asymmetry is the contract; and `P1Y == P12M', because the comparison is on # `years * 12 + months' rather than on the fields. check_eq $P/dur-eq-timedelta "True" "$(pv dur_eq_timedelta)" check_eq $P/dur-ne-year "False" "$(pv dur_ne_year)" check_eq $P/dur-eq-months "True" "$(pv dur_eq_months)" # The rest of the arithmetic: negation, integer multiplication, and Duration + # Duration. check_eq $P/dur-negate "isodate.duration.Duration(0, 0, 0, years=-1, months=-2)" "$(pv dur_negate)" check_eq $P/dur-mul "isodate.duration.Duration(0, 0, 0, years=3, months=0)" "$(pv dur_mul)" check_eq $P/dur-add-dur "isodate.duration.Duration(0, 0, 0, years=1, months=6)" "$(pv dur_add_dur)" # `Duration.__getattr__' DELEGATES to the timedelta it holds, so `.days' on a # Duration is the TIMEDELTA's days and says nothing about its years: a caller # reading `.days' off a `P1Y' gets 0, not 365. The pair is what makes that # unmissable. check_eq $P/dur-year-days "0" "$(pv dur_year_days)" check_eq $P/dur-year-years "1" "$(pv dur_year_years)" # ----------------------------------------------------- duration_isoformat --- # `P%P' is the default, and it emits only the components that are NON-ZERO -- # which is why a zero duration renders `P0D' rather than an empty `P': the # renderer has an explicit "at least one component has to be there" branch, and # `dfmt-default-zero' is what pins it. Note `dfmt-default-week': `P2W' renders # as `P14D', because the parse turned the weeks into days and nothing kept them. check_eq $P/dfmt-default-day "'P1D'" "$(pv dfmt_default_day)" check_eq $P/dfmt-default-week "'P14D'" "$(pv dfmt_default_week)" check_eq $P/dfmt-default-time "'PT1H30M'" "$(pv dfmt_default_time)" check_eq $P/dfmt-default-year "'P1Y'" "$(pv dfmt_default_year)" check_eq $P/dfmt-default-ym "'P1Y6M'" "$(pv dfmt_default_ym)" check_eq $P/dfmt-default-mixed "'P1Y2M3DT4H5M6S'" "$(pv dfmt_default_mixed)" check_eq $P/dfmt-default-frac "'PT0.5S'" "$(pv dfmt_default_frac)" check_eq $P/dfmt-default-zero "'P0D'" "$(pv dfmt_default_zero)" check_eq $P/dfmt-default-neg "'-P1D'" "$(pv dfmt_default_neg)" # D_WEEK renders whatever the days DIVIDE into weeks, and truncates: ten days # come back `P1W'. A rendering choice, and a LOSSY one -- stated here rather # than left for a caller to discover on the wrong side of a round trip. check_eq $P/dfmt-week-2w "'P2W'" "$(pv dfmt_week_2w)" check_eq $P/dfmt-week-10d "'P1W'" "$(pv dfmt_week_10d)" # The alternative formats, which render a duration in the shape of a date-time. # THE TWO ORDINAL ONES ARE BROKEN UPSTREAM, and this is measured rather than # predicted: `%j' is in isostrf's date map and NOT in its duration map, so the # duration renderer leaves the directive in the output as literal text -- # `P0001-%jT04:05:06'. The cases below assert what the package DOES; they are # not a bug being papered over, they are the bug being pinned so that a release # which fixes it reds here and says so. It is upstream's defect in a # third-party package, not a brintOS one, and this project files nothing # against third-party public repositories. check_eq $P/dfmt-d-alt-ext "'P0001-02-03T04:05:06'" "$(pv dfmt_d_alt_ext)" check_eq $P/dfmt-d-alt-bas "'P00010203T040506'" "$(pv dfmt_d_alt_bas)" check_eq $P/dfmt-d-alt-ext-ord "'P0001-%jT04:05:06'" "$(pv dfmt_d_alt_ext_ord)" check_eq $P/dfmt-d-alt-bas-ord "'P0001%jT040506'" "$(pv dfmt_d_alt_bas_ord)" check_eq $P/const-d-default "P%P" "$(pv const_d_default)" check_eq $P/const-d-week "P%p" "$(pv const_d_week)" # Both directions. The backward half is the one that matters here: a Duration # CONSTRUCTED with years and months, rendered and re-parsed, must come back a # Duration and not a timedelta -- the type surviving the round trip is a # stronger statement than the value doing so. check_eq $P/rt-dur-str-day "'P1D'" "$(pv rt_dur_str_day)" check_eq $P/rt-dur-str-time "'PT1H30M'" "$(pv rt_dur_str_time)" check_eq $P/rt-dur-str-year "'P1Y'" "$(pv rt_dur_str_year)" check_eq $P/rt-dur-str-mixed "'P1Y2M3DT4H5M6S'" "$(pv rt_dur_str_mixed)" check_eq $P/rt-dur-val-duration "isodate.duration.Duration(3, 0, 0, years=1, months=2)" "$(pv rt_dur_val_duration)" check_eq $P/rt-dur-val-timedelta "datetime.timedelta(days=3, seconds=14400)" "$(pv rt_dur_val_timedelta)" # ----------------------------------------------------------- parse_tzinfo --- # THE THREE ANSWERS: the UTC SINGLETON for `Z' -- identity, not equality, which # is what `tz-z-is-utc' asserts -- a FixedOffset for an offset, and None for the # empty designator, which means "local time", i.e. the ABSENCE of a zone rather # than the presence of the host's. check_eq $P/tz-z-is-utc "yes" "$(pv tz_z_is_utc)" check_eq $P/tz-z-offset "datetime.timedelta(0)" "$(pv tz_z_offset)" check_eq $P/tz-ext "datetime.timedelta(seconds=19800)" "$(pv tz_ext)" check_eq $P/tz-bas "datetime.timedelta(days=-1, seconds=57600)" "$(pv tz_bas)" check_eq $P/tz-hour "datetime.timedelta(seconds=7200)" "$(pv tz_hour)" check_eq $P/tz-empty-none "yes" "$(pv tz_empty_none)" check_eq $P/tz-ext-class "FixedOffset" "$(pv tz_ext_class)" check_eq $P/tz-ext-name "'+05:30'" "$(pv tz_ext_name)" # THE LOCAL ZONE, asserted by WHAT IT IS and never by what it reports. `LOCAL' # is the module's tzinfo for the host's own zone; its offset is the image's TZ, # so a case pinning that would red on any image whose zone moved rather than on # a broken isodate. The class is the assertion, and that its offset is a # timedelta at all is the second half. check_eq $P/tz-local-class "LocalTimezone" "$(pv tz_local_class)" check_eq $P/tz-local-is-tzinfo "yes" "$(pv tz_local_is_tzinfo)" # tz_isoformat's three formats over one offset -- `%h' hours only, `%z' basic, # `%Z' extended -- and the two ends: UTC renders `Z' rather than `+00:00', and a # NAIVE value renders the empty string rather than raising. check_eq $P/tzfmt-hour "'+05'" "$(pv tzfmt_hour)" check_eq $P/tzfmt-bas "'+0530'" "$(pv tzfmt_bas)" check_eq $P/tzfmt-ext "'+05:30'" "$(pv tzfmt_ext)" check_eq $P/tzfmt-utc "'Z'" "$(pv tzfmt_utc)" check_eq $P/tzfmt-naive "''" "$(pv tzfmt_naive)" # ---------------------------------------------------------------- strftime --- # THE RENDERER UNDERNEATH ALL FOUR isoformat WRAPPERS, called directly. It is # NOT datetime.strftime: it takes a subset of the stdlib's codes plus a few of # its own, and `%W' here is the ISO week from isocalendar() where the stdlib's # is a different week numbering entirely. That divergence is why this section # exists rather than trusting the wrappers. check_eq $P/strf-date "'2024-05-17'" "$(pv strf_date)" check_eq $P/strf-week "'20'" "$(pv strf_week)" check_eq $P/strf-weekday "'5'" "$(pv strf_weekday)" check_eq $P/strf-ordinal "'138'" "$(pv strf_ordinal)" check_eq $P/strf-century "'20'" "$(pv strf_century)" check_eq $P/strf-percent "'100%'" "$(pv strf_percent)" check_eq $P/strf-expanded "'+002024'" "$(pv strf_expanded)" # ...and the SAME code over a Duration means something else: `%W' on a duration # is whole weeks of its timedelta, not a week-of-year, and `%d' is total days # rather than day-of-month. One format string, two meanings, chosen by the type # of the first argument -- which is the last place a caller would look for a # type dependency. check_eq $P/strf-dur-week "'02'" "$(pv strf_dur_week)" check_eq $P/strf-dur-days "'14'" "$(pv strf_dur_days)" # ------------------------------------------------------- the failure path --- # See the header for the three outcomes. ISO8601Error derives from ValueError, # so an existing `except ValueError' around a parse already catches it -- that # is the fact worth pinning, because a release that gave it a different base # would silently stop being caught by code that never mentions isodate's own # class. check_eq $P/err-base "ValueError" "$(pv err_base)" check_eq $P/err-is-valueerror "yes" "$(pv err_is_valueerror)" # WHAT ACTUALLY RAISES ISO8601Error: a string with no ISO form in it at all, # and a datetime missing its `T' -- which is its own error rather than a date # error, because the split happens before either half is parsed. check_eq $P/err-date-garbage "yes" "$(pv err_date_garbage)" check_eq $P/err-time-garbage "yes" "$(pv err_time_garbage)" check_eq $P/err-datetime-no-t "yes" "$(pv err_datetime_no_t)" check_eq $P/err-duration-garbage "yes" "$(pv err_duration_garbage)" check_eq $P/err-duration-nop "yes" "$(pv err_duration_nop)" # ...AND WHAT RAISES A PLAIN ValueError INSTEAD: a string that IS a recognised # ISO form and names a value `datetime' will not hold. `2024-13-01' and # `99:99:99' both match their patterns; the parser succeeded and the # CONSTRUCTOR refused. A caller catching only ISO8601Error lets these through, # and the `*-not-iso' cases are what say so in as many words. check_eq $P/err-date-month13 "yes" "$(pv err_date_month13)" check_eq $P/err-month13-not-iso "no" "$(pv err_month13_not_iso)" check_eq $P/err-time-range "yes" "$(pv err_time_range)" check_eq $P/err-time-range-not-iso "no" "$(pv err_time_range_not_iso)" # A NON-STRING IS A THIRD FAULT AND A THIRD EXCEPTION: parse_duration # type-checks its argument through `six.string_types' and raises TypeError, # which neither `except ISO8601Error' nor `except ValueError' catches. The one # place in this API where the error a caller gets is not a ValueError at all -- # and, incidentally, the only place six is reached at runtime rather than at # import. check_eq $P/err-duration-type "yes" "$(pv err_duration_type)" # `parse_tzinfo' HAS NO FAILURE PATH IN PRACTICE, which its own docstring's # `@raise ISO8601Error' does not tell you. Every group in TZ_REGEX is optional, # so the regex matches the empty prefix of ANYTHING -- `+', a word, a whole # sentence -- and build_tzinfo turns that empty match into `None', the local-time # answer. The ISO8601Error branch below it is unreachable from a str argument. # So a caller validating a designator by catching an exception validates # nothing, and gets "local time" for input that was never a zone at all. check_eq $P/err-tz-plus-raises "no" "$(pv err_tz_plus_raises)" check_eq $P/err-tz-plus-value "None" "$(pv err_tz_plus_value)" check_eq $P/err-tz-word-value "None" "$(pv err_tz_word_value)" # ------------------------------------------------- match(), NOT fullmatch() --- # THE TRAP, and the most useful thing this suite measured. See the header: every # parser but parse_duration anchors at the START only, so a string whose PREFIX # is a valid ISO form parses and the tail is discarded in silence. # # `2024-5-17' is the one a caller writes by hand: a single-digit month is not # ISO, it does not raise, `2024' matches the bare-year pattern, and the answer # is 2024-01-01 -- four months and sixteen days from what was meant, with no # error anywhere. `2024-05-17garbage' and `15:04:05xyz' are the same shape # stated plainly. check_eq $P/loose-date-short-month "datetime.date(2024, 1, 1)" "$(pv loose_date_short_month)" check_eq $P/loose-date-trailing "datetime.date(2024, 5, 17)" "$(pv loose_date_trailing)" check_eq $P/loose-time-trailing "datetime.time(15, 4, 5)" "$(pv loose_time_trailing)" # parse_duration is the EXCEPTION, and deliberately so: its regex is the only # one in the package anchored at both ends, so it alone rejects a trailing tail. # The asymmetry is asserted both behaviourally and against the pattern itself. check_eq $P/strict-duration-trailing "yes" "$(pv strict_duration_trailing)" check_eq $P/duration-regex-anchored "yes" "$(pv duration_regex_anchored)" # ------------------------------------------ UPSTREAM'S OWN TEST SUITE, RUN --- # See the header. The author's 278 tests, executed inside the probe's own # interpreter start, over the six test modules this package ships into # site-packages -- test_date, test_time, test_datetime, test_duration, # test_strf and test_pickle. # # THE COUNT IS THE ASSERTION, not just the failure total. A suite that collected # NOTHING reports zero failures and zero errors and looks green, which is # exactly what `python3 -m unittest isodate.tests' does here -- upstream builds # its suites from `test_suite()' callables that unittest's discovery does not # find. `upstream-ran' is what tells "278 tests passed" apart from "no test # ran", and it is also what would go quiet if python3-unittest left the image, # since the recipe does not declare it. # # Any failure's name and traceback go to the probe's STDERR, which bst_probe # keeps at .err -- so a red here is diagnosable without a second boot. check_eq $P/upstream-ran "278" "$(pv upstream_ran)" check_eq $P/upstream-failures "0" "$(pv upstream_failures)" check_eq $P/upstream-errors "0" "$(pv upstream_errors)" rm -rf "$W"