. "$BST_LIB" # BST-TIMEOUT: 1200 # THREE python3 starts, and they are the whole cost. Every # # case in this suite is a parse of a pyproject.toml this # # suite wrote into $TMPDIR -- no device, no peer, no # # terminal, no process the library spawns -- so 400s per # # start is the same per-start budget the neighbouring # # python3 suites use, and nothing else here costs anything. # # # # The three are: a bare `import', a second one whose STDERR # # is the assertion (`import-quiet'), and the probe, which # # takes ONE start for 143 measurements. # # # # NOT MEASURED ON TARGET YET: this port's first on-target # # run is the batch verify, and the measured elapsed # # replaces this note in the test plan rather than in this # # comment, which would cost an image build to keep the # # shipped suite byte-identical. # # python3-pyproject-metadata (pyproject-metadata 0.7.1, poky # meta/recipes-devtools/python/python3-pyproject-metadata_0.7.1.bb, `inherit # pypi python_setuptools_build_meta', PyPI sdist) on the wasm32-hwjs glibc # image. # # ============================================================================ # WHAT THE PACKAGE IS, AND THEREFORE WHAT HAS TO BE TESTED. # ============================================================================ # # ONE MODULE, 515 LINES. `StandardMetadata.from_pyproject(data, project_dir)' # validates an ALREADY-PARSED `[project]' table and returns a dataclass; # `as_rfc822()' renders that dataclass as the RFC 822 core metadata a wheel or # an sdist carries. It is what `meson-python' and other PEP 517 backends read # project metadata through. # # IT DOES NOT PARSE TOML. The recipe's own DESCRIPTION says so, and it is the # fact that shapes this suite: the fixtures are `pyproject.toml' files the # probe WRITES under $TMPDIR and reads back with `tomllib' (stdlib, and # python3-tomllib is already on this image). Nothing on the rootfs is read, # except by the two fields that name a file -- `project.license.file' and # `project.readme' -- which resolve against `project_dir' and are why every # fixture gets its own directory. # # So there is no I/O the library initiates on its own, no subprocess, no # device and no terminal anywhere in this package. That is what lets this port # close on its suite with no browser pass. # # ============================================================================ # THE PORT ITSELF IS A ROSTER LINE. THE RECIPE IS ALREADY RIGHT. # ============================================================================ # # The module imports collections, dataclasses, os, os.path, pathlib, re and # typing -- all in python3-core per poky's python3-manifest.json -- plus # `packaging', which is a port of its own (BRINTOS-1746) and already on this # image. poky's recipe carries # # RDEPENDS:${PN} += "python3-logging python3-packaging python3-profile" # # and the built .deb's Depends: line is `python3-core, python3-logging, # python3-packaging, python3-profile' -- asserted VERBATIM by # `pkg-depends-exact', so a poky manifest reshuffle reds there. Note that # `logging' and `profile' are NOT imported by the module; they come from the # recipe, not from the source, and the suite records that rather than trimming # it. There is no bbappend, no patch and no site answer in this port: the # package is a purelib wheel of one module and nothing in it reaches the cross # environment. # # The payload carries ONE NON-PYTHON FILE -- `py.typed', the PEP 561 marker, # which is ZERO BYTES. `pkg-only-python-or-marker' allows exactly that file and # nothing else, and `py-typed-empty' asserts the emptiness rather than # `test -s', which would red on a correct package. # # ============================================================================ # FOUR THINGS THE TICKET'S OWN READING GETS WRONG. THESE ARE THE POINT. # ============================================================================ # # AN UNKNOWN KEY IN `[project]' DOES NOT RAISE -- IT IS SILENTLY DROPPED. # `from_pyproject' reads the fields it knows through # `DataFetcher' and never enumerates the table, so # `frobnicate = 1' parses clean. Only the `license' and # `readme' SUB-tables reject an unexpected field, and they do # it by name. `unknown-key-ignored' beside # `license-unexpected-field' and # `readme-table-unexpected-field' is that pair of answers, # and the ticket asked for the raise at the top level. # # A MALFORMED VERSION RAISES `InvalidVersion', NOT `ConfigurationError'. # `packaging.version.Version(version_string)' is called # unguarded on line 231, so packaging's own exception comes # straight out; `requires-python' is the same shape and # raises `InvalidSpecifier'. A caller that catches only # ConfigurationError -- which is what the package's README # suggests, and what the ticket says -- catches neither. # `malformed-version' and `malformed-requires-python'. # # A MISSING VERSION IS NOT A PARSE ERROR AT ALL. `__post_init__' answers it # by APPENDING `version' to `dynamic', so the object is built # and `dynamic == ["version"]' on a file that declared no # dynamic fields. The failure is deferred to `as_rfc822()', # which raises ConfigurationError('Missing version field'). # Assigning a version afterwards RETRACTS the entry, because # `__setattr__' watches the field. Four cases, together, # because the pair is the finding: `version-absent-parses', # `version-absent-dynamic', `version-absent-rfc822', # `version-set-clears-dynamic'. # # `as_rfc822()' IS NOT IDEMPOTENT ONCE THERE ARE EXTRAS. `_build_extra_req' # MUTATES the stored `Requirement''s marker in place rather # than copying it, so a second render of the SAME UNCHANGED # object emits `extra == "test" and extra == "test"'. # `rfc-extra-req-first' and `rfc-extra-req-second' are the # two renderings, and a backend that renders metadata twice # -- for a wheel and for its sdist -- ships the second. # # THREE MORE THAT A READER OF THE SPECIFICATION WOULD NOT PREDICT, and which # `as_rfc822()' is asserted field by field to pin: # # `Keywords' IS SPACE-SEPARATED. `' '.join(self.keywords)', where the core # metadata specification says comma-separated. # `rfc-keywords'. # # A PERSON GIVEN AS AN EMAIL ALONE IS NAMED `Unknown'. `get_people' fills # `entry.get("name", "Unknown")', and `Author' carries only # the people WITHOUT an email while `Author-Email' carries # the rest as `Name '. So the fixture's two authors # land in different headers and its one maintainer renders as # `Unknown '. `rfc-author', # `rfc-author-email', `rfc-maintainer-absent', # `rfc-maintainer-email'. # # A `Project-URL' LABEL IS `str.capitalize()'d, which lowercases everything # after the first letter: `bug tracker' renders `Bug # tracker'. And `homepage' is rendered TWICE -- once as the # legacy `Home-page' and once as a `Project-URL'. # `rfc-project-url'. # # ONE UPSTREAM COPY-PASTE, RECORDED RATHER THAN CORRECTED: a readme file that # does not exist raises the right message and the WRONG key -- # `ConfigurationError.key' is `project.license.file' (`__init__.py':430). # `readme-missing-file-key' asserts the wrong value deliberately, so that a # future upstream fix reds here and is noticed rather than passing in silence. # # `scripts', `gui-scripts' and `entry-points' are parsed into three separate # fields and NONE of them reaches the RFC 822 rendering -- they are not core # metadata. `rfc-no-scripts-header' is the negative. P=python3-pyproject-metadata bst_ok $P/suite-canary require python3 $P/python3-present SITE=/usr/lib/python3.12/site-packages PKGDIR=$SITE/pyproject_metadata DI=$SITE/pyproject_metadata-0.7.1.dist-info PKGLIST=/var/lib/dpkg/info/python3-pyproject-metadata.list PROBE=$BST_DATA/python3-pyproject-metadata-probe.py tmp=${TMPDIR:-/tmp} W=$tmp/bst-pyproject-metadata-suite.$$ mkdir -p "$W" || { bst_notok $P/workdir "cannot create $W"; exit 0; } # ------------------------------------------------------------- the payload --- # A PACKAGE DIRECTORY holding ONE module, not a flat `pyproject_metadata.py': # the whole library is `__init__.py'. check $P/site-packages-dir test -d "$SITE" check $P/is-a-package test -d "$PKGDIR" check $P/init-file test -s "$PKGDIR/__init__.py" check $P/not-a-flat-module test ! -e "$SITE/pyproject_metadata.py" check $P/no-submodules \ test ! -e "$PKGDIR/project_table.py" -a ! -e "$PKGDIR/constants.py" # `py.typed' is the PEP 561 marker and is ZERO BYTES by design, so `test -s' # would red on a correct package. check $P/py-typed-present test -f "$PKGDIR/py.typed" check $P/py-typed-empty test ! -s "$PKGDIR/py.typed" # The sdist ships README.md, LICENSE, setup.cfg and pyproject.toml; the recipe # packages the built WHEEL, so none of them lands beside the module. check $P/no-readme test ! -e "$PKGDIR/README.md" check $P/no-setup-cfg test ! -e "$SITE/setup.cfg" check $P/no-pyproject test ! -e "$SITE/pyproject.toml" check $P/no-egg-info test ! -d "$SITE/pyproject_metadata.egg-info" check $P/pycache-init test -s "$PKGDIR/__pycache__/__init__.cpython-312.pyc" # ------------------------------------------------------- the package's list --- # ( ) so the redirect happens in a forked child: a top-level `< /var/lib/...' # on the virtiofs root can end the suite outright (brintos/linux#73). cat "$PKGLIST" > "$W/pkgf" 2>/dev/null || : > "$W/pkgf" check $P/pkg-list-present test -s "$W/pkgf" # A purelib wheel ships no shared object and nothing in ${bindir}. Both are # asserted as the ABSENCE of a match rather than as a count, so the failure # message carries the offending path. check_eq $P/pkg-no-so "" \ "$(grep -E '\.so($|\.)' "$W/pkgf" 2>/dev/null)" check_eq $P/pkg-no-bin "" \ "$(grep -E '^/usr/(s?bin)/' "$W/pkgf" 2>/dev/null)" # Everything in the payload is Python, a __pycache__ entry, a dist-info file -- # or `py.typed', the one marker file this package legitimately carries. _bad="" while IFS= read -r _f; do [ -f "$_f" ] || continue case "$_f" in *.py|*.pyc) continue ;; */__pycache__/*) continue ;; */*.dist-info/*) continue ;; */py.typed) continue ;; esac _bad="$_bad $_f" done < "$W/pkgf" # BST-REDIRECT-OK: $W is under $TMPDIR, a tmpfs, not the rootfs check_eq $P/pkg-only-python-or-marker "" "$_bad" # ---------------------------------------------------------- the dist-info ---- check $P/dist-info-dir test -d "$DI" check $P/dist-info-metadata test -f "$DI/METADATA" check_out $P/dist-info-name "Name: pyproject-metadata" cat "$DI/METADATA" check_out $P/dist-info-version "Version: 0.7.1" cat "$DI/METADATA" check $P/dist-info-record test -s "$DI/RECORD" check $P/dist-info-license test -s "$DI/LICENSE" # The upstream metadata's own `Requires-Dist' is the package's ONE runtime # dependency, and it is the reason python3-packaging (BRINTOS-1746) has to be # on this image for the import to work at all. check_out $P/dist-info-requires-packaging "Requires-Dist: packaging >=19.0" \ cat "$DI/METADATA" # The wheel tags say what this package is: pure Python, py3, no ABI. A recipe # change that started compiling something would move all three. check_out $P/dist-info-generator "Generator: bdist_wheel" cat "$DI/WHEEL" check_out $P/dist-info-purelib "Root-Is-Purelib: true" cat "$DI/WHEEL" check_out $P/dist-info-tag-py3 "Tag: py3-none-any" cat "$DI/WHEEL" check_eq $P/dist-info-no-py2-tag "" \ "$(grep '^Tag: py2' "$DI/WHEEL" 2>/dev/null)" check_out $P/dist-info-top-level "pyproject_metadata" cat "$DI/top_level.txt" # RECORD names py.typed, so the marker is part of the distribution rather than # something the packaging happened to sweep up. Its recorded size is zero. # # check_out's needle is a LITERAL SUBSTRING, not a regex, so the whole RECORD # entry is spelled out. That costs nothing in precision: for a zero-byte file # both fields are constants -- PEP 376 fixes the digest as urlsafe-base64 # sha256 with the padding stripped, and sha256("") is e3b0c442..., which is # this string. A py.typed that ever stopped being empty moves both fields. check_out $P/record-lists-py-typed "pyproject_metadata/py.typed" cat "$DI/RECORD" check_out $P/record-py-typed-zero-length \ "pyproject_metadata/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0" \ grep -E 'py\.typed' "$DI/RECORD" # The wheel declares no entry points of its own -- this is a library, and # nothing of it is on $PATH. check $P/no-entry-points-txt test ! -e "$DI/entry_points.txt" # ------------------------------------------------------------ dpkg status ---- sed -n '/^Package: python3-pyproject-metadata$/,/^$/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" check_out $P/pkg-version "Version: 0.7.1-r0" cat "$W/st" # THE RECIPE'S ONE INTERESTING LINE, ASSERTED VERBATIM. `logging' and `profile' # are NOT imported by the module -- they come from poky's RDEPENDS, not from # the source -- while `packaging' IS, and is the port this one stands on. A # manifest reshuffle that moved any of the four reds here, beside the import # that would have started failing. grep '^Depends:' "$W/st" > "$W/dep" 2>/dev/null || : > "$W/dep" check $P/pkg-depends-line test -s "$W/dep" check_eq $P/pkg-depends-exact \ "Depends: python3-core, python3-logging, python3-packaging, python3-profile" \ "$(cat "$W/dep")" # The two packages this port stands on, asserted as INSTALLED rather than # merely depended on: python3-packaging because the module imports it, and # python3-tomllib because the SUITE's own fixtures are read with it. check_out $P/packaging-installed "Status: install ok installed" \ sed -n '/^Package: python3-packaging$/,/^$/p' /var/lib/dpkg/status check_out $P/tomllib-installed "Status: install ok installed" \ sed -n '/^Package: python3-tomllib$/,/^$/p' /var/lib/dpkg/status # ---------------------------------------------------------------- import ----- check $P/import python3 -c 'import pyproject_metadata' # A clean stderr is a separate assertion from a zero exit: a DeprecationWarning # out of packaging or typing would leave the status zero and the import noisy. check_eq $P/import-quiet "" \ "$(python3 -c 'import pyproject_metadata' 2>&1)" # ----------------------------------------------------------------- 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 bst_probe $P/probe-run "$W/probe" 900 python3 "$PROBE" OUT="$W/probe" pv() { grep "^$1=" "$OUT" 2>/dev/null | head -n 1 | cut -d= -f2-; } 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 # The probe's own completeness, asserted before anything it measured: a probe # that died half-way would otherwise show up as a dozen unexplained reds. check_eq $P/probe-complete "yes" "$(pv probe_done)" check_eq $P/probe-count "143" "$(pv emitted_count)" # Every fixture the probe wrote lives under $TMPDIR, which is a tmpfs here. The # suite removes the tree; the probe deliberately does not, so a red case leaves # its pyproject.toml behind to be looked at. _pw="$(pv probe_workdir)" check_eq $P/probe-workdir-under-tmpdir "$tmp" \ "$(printf '%s' "$_pw" | cut -c1-${#tmp})" # ---------------------------------------------------------------- identity --- check_eq $P/module-answered "$PKGDIR/__init__.py" "$(pv module_file)" check_eq $P/is-package "yes" "$(pv is_package)" check_eq $P/version-attr "0.7.1" "$(pv version_attr)" # `tomllib' is the STDLIB parser, out of python3-tomllib -- not a vendored copy # and not python3-tomli, which this image also carries (BRINTOS-1838). check_eq $P/tomllib-is-stdlib "/usr/lib/python3.12/tomllib/__init__.py" \ "$(pv tomllib_file)" check_eq $P/packaging-version "23.2" "$(pv packaging_version)" # NO `__all__', so a star-import exports every non-underscore module-level name # -- the four stdlib modules the module imported, `packaging' itself, and the # `annotations' future flag among them. Sixteen names where a caller expected # three. check_eq $P/no-dunder-all "no" "$(pv has_dunder_all)" check_eq $P/star-names \ "Any,ConfigurationError,DataFetcher,License,Mapping,RFC822Message,Readme,StandardMetadata,annotations,collections,dataclasses,os,packaging,pathlib,re,typing" \ "$(pv star_names)" check_eq $P/is-dataclass "yes" "$(pv metadata_is_dataclass)" # The field ORDER is part of the constructor's contract -- `from_pyproject' # passes them all positionally. check_eq $P/dataclass-fields \ "name,version,description,license,readme,requires_python,dependencies,optional_dependencies,entrypoints,authors,maintainers,urls,classifiers,keywords,scripts,gui_scripts,dynamic" \ "$(pv metadata_fields)" check_eq $P/license-namedtuple "text,file" "$(pv license_is_namedtuple)" check_eq $P/readme-namedtuple "text,file,content_type" "$(pv readme_is_namedtuple)" check_eq $P/configuration-error-base "Exception" "$(pv configuration_error_base)" # ------------------------------------------------- the complete [project] ---- # One fixture exercising every field PEP 621 defines, asserted attribute by # attribute against the literal written. check_eq $P/full-name "sample-project-name" "$(pv full_name)" # NAME NORMALISATION AGREES WITH `packaging.utils.canonicalize_name' -- the # same `re.sub(r"[-_.]+", "-", name).lower()', reimplemented rather than # imported. The agreement is asserted, not assumed. check_eq $P/full-name-matches-canonicalize "yes" "$(pv full_name_matches_canonicalize)" check_eq $P/full-version "1.2.3.dev4" "$(pv full_version)" check_eq $P/full-version-type "Version" "$(pv full_version_type)" check_eq $P/full-version-prerelease "True" "$(pv full_version_is_prerelease)" check_eq $P/full-version-release "(1, 2, 3)" "$(pv full_version_release)" check_eq $P/full-version-matches-packaging "yes" "$(pv full_version_matches_packaging)" check_eq $P/full-description "A sample project used by the brintOS self-test." \ "$(pv full_description)" # `requires-python' becomes a packaging SpecifierSet, whose `str' is its # specifiers SORTED as strings -- so `>=3.8,<4' comes back `<4,>=3.8'. check_eq $P/full-requires-python "<4,>=3.8" "$(pv full_requires_python)" check_eq $P/full-requires-python-type "SpecifierSet" "$(pv full_requires_python_type)" check_eq $P/full-requires-python-accepts-39 "yes" "$(pv full_requires_python_accepts_39)" check_eq $P/full-requires-python-rejects-37 "yes" "$(pv full_requires_python_rejects_37)" check_eq $P/full-keywords "alpha,beta,gamma" "$(pv full_keywords)" check_eq $P/full-classifiers-count "2" "$(pv full_classifiers_count)" check_eq $P/full-classifier-first "Programming Language :: Python :: 3" \ "$(pv full_classifier_first)" # AN AUTHOR WITH NO `name' IS NAMED `Unknown', and an author with no `email' # gets None rather than being dropped. Both are what the RFC 822 rendering # below routes on. check_eq $P/full-authors "Ann Author|ann@example.com;Nameless Only|None" \ "$(pv full_authors)" check_eq $P/full-maintainers "Unknown|solo@example.com" "$(pv full_maintainers)" check_eq $P/full-urls \ "homepage=https://example.com/home,bug tracker=https://example.com/bugs" \ "$(pv full_urls)" check_eq $P/full-dependencies 'packaging>=19.0 / tomli; python_version < "3.11"' \ "$(pv full_dependencies)" check_eq $P/full-dependency-type "Requirement" "$(pv full_dependency_type)" check_eq $P/full-dependency-name "packaging" "$(pv full_dependency_name)" check_eq $P/full-optional-extras "test" "$(pv full_optional_extras)" # The extras' markers are NOT applied here -- they are added at render time, # which is where the idempotency break below lives. check_eq $P/full-optional-test "pytest>=7 / coverage" "$(pv full_optional_test)" # `scripts' and `gui-scripts' are their OWN fields, not folded into # `entrypoints' the way `importlib.metadata' presents them. check_eq $P/full-scripts "sample-cli=sample.cli:main" "$(pv full_scripts)" check_eq $P/full-gui-scripts "sample-gui=sample.gui:main" "$(pv full_gui_scripts)" check_eq $P/full-entrypoint-sections "sample.plugins" "$(pv full_entrypoint_sections)" check_eq $P/full-entrypoint-alpha "sample.plugins.alpha:Plugin" \ "$(pv full_entrypoint_alpha)" check_eq $P/full-entrypoints-exclude-scripts "yes" \ "$(pv full_entrypoints_exclude_scripts)" # The readme and the license are the only two fields that touch the # filesystem, and both are resolved against `project_dir'. check_eq $P/full-readme-text "The readme body.|Second line.|" "$(pv full_readme_text)" check_eq $P/full-readme-content-type "text/markdown" "$(pv full_readme_content_type)" check_eq $P/full-readme-file "README.md" "$(pv full_readme_file_name)" check_eq $P/full-license-text "MIT, for the purposes of this test.|" \ "$(pv full_license_text)" check_eq $P/full-license-file "LICENSE.txt" "$(pv full_license_file_name)" check_eq $P/full-dynamic-empty "(empty)" "$(pv full_dynamic)" # ------------------------------------------------------ name normalisation --- check_eq $P/norm-dotted "sample-project-name" "$(pv norm_dotted)" check_eq $P/norm-double-dash "a-b" "$(pv norm_double_dash)" check_eq $P/norm-mixed "a-b-c" "$(pv norm_mixed)" check_eq $P/norm-upper "upper" "$(pv norm_upper)" check_eq $P/norm-single "x" "$(pv norm_single)" check_eq $P/norm-agrees-with-packaging "yes" "$(pv norm_agrees_with_packaging)" # ----------------------------------------------------------------- dynamic --- check_eq $P/dynamic-list "description,keywords" "$(pv dynamic_list)" # A field DECLARED dynamic and not given is simply absent from the object. check_eq $P/dynamic-description-absent "yes" "$(pv dynamic_description_absent)" check_eq $P/dynamic-keywords-empty "yes" "$(pv dynamic_keywords_empty)" # Declared dynamic AND given a value -- the message carries upstream's own # grammar (`declared as dynamic in but is defined'), asserted verbatim so a # reworded upstream is seen rather than silently accepted. _exp=$(cat <<'EOX' !ConfigurationError: Field `project.description` declared as dynamic in but is defined EOX ) check_eq $P/dynamic-and-defined "$_exp" "$(pv dynamic_and_defined)" _exp=$(cat <<'EOX' !ConfigurationError: Unsupported field `name` in `project.dynamic` EOX ) check_eq $P/dynamic-name-rejected "$_exp" "$(pv dynamic_name_rejected)" # A MISSING VERSION IS NOT A PARSE ERROR. The object is built and `dynamic' # grows a `version' entry that the file never declared. check_eq $P/version-absent-parses "yes" "$(pv version_absent_parses)" check_eq $P/version-absent-dynamic "version" "$(pv version_absent_dynamic)" check_eq $P/version-absent-is-none "yes" "$(pv version_absent_version_is_none)" check_eq $P/version-absent-rfc822 "!ConfigurationError: Missing version field" \ "$(pv version_absent_rfc822)" # ... and assigning one afterwards retracts it, through `__setattr__'. check_eq $P/version-set-clears-dynamic "(empty)" "$(pv version_set_clears_dynamic)" check_eq $P/version-set-rfc822-version "2.0" "$(pv version_set_rfc822_version)" check_eq $P/dynamic-version-declared "version" "$(pv dynamic_version_declared)" # ------------------------------------------------------------- the readme ---- check_eq $P/readme-md-content-type "text/markdown" "$(pv readme_md_content_type)" check_eq $P/readme-rst-content-type "text/x-rst" "$(pv readme_rst_content_type)" # A `.txt' readme raises even though the file is right there: the content type # is inferred from the SUFFIX and there is no third branch. _exp=$(cat <<'EOX' !ConfigurationError: Could not infer content type for readme file `README.txt` EOX ) check_eq $P/readme-txt-no-content-type "$_exp" "$(pv readme_txt_no_content_type)" _exp=$(cat <<'EOX' !ConfigurationError: Readme file not found (`NOPE.md`) EOX ) check_eq $P/readme-missing-file "$_exp" "$(pv readme_missing_file)" # THE UPSTREAM COPY-PASTE, ASSERTED AS IT IS: the message says readme, the # `key' says license. Deliberately pinned to the WRONG value so that a future # upstream fix reds here and is noticed (`__init__.py':430). check_eq $P/readme-missing-file-key "project.license.file" \ "$(pv readme_missing_file_key)" check_eq $P/readme-table-text "inline body" "$(pv readme_table_text)" check_eq $P/readme-table-content-type "text/x-rst" "$(pv readme_table_content_type)" check_eq $P/readme-table-file-is-none "yes" "$(pv readme_table_file_is_none)" # The TABLE form does not infer a content type -- it insists on one, even for # the same `README.md' whose STRING spelling infers `text/markdown' one case # above. _exp=$(cat <<'EOX' !ConfigurationError: Field `project.readme.content-type` missing EOX ) check_eq $P/readme-table-file-needs-content-type "$_exp" \ "$(pv readme_table_file_needs_content_type)" _exp=$(cat <<'EOX' !ConfigurationError: Invalid `project.readme` value, expecting either `file` or `text` (got `{'file': 'README.md', 'text': 'x', 'content-type': 'text/markdown'}`) EOX ) check_eq $P/readme-table-both "$_exp" "$(pv readme_table_both)" _exp=$(cat <<'EOX' !ConfigurationError: Invalid `project.readme` value, expecting either `file` or `text` (got `{'content-type': 'text/markdown'}`) EOX ) check_eq $P/readme-table-neither "$_exp" "$(pv readme_table_neither)" _exp=$(cat <<'EOX' !ConfigurationError: Unexpected field `project.readme.charset` EOX ) check_eq $P/readme-table-unexpected-field "$_exp" "$(pv readme_table_unexpected_field)" # The type error's message carries upstream's stray comma (`expecting either, # a string'), which is asserted rather than tidied. _exp=$(cat <<'EOX' !ConfigurationError: Field `project.readme` has an invalid type, expecting either, a string or dictionary of strings (got `42`) EOX ) check_eq $P/readme-wrong-type "$_exp" "$(pv readme_wrong_type)" check_eq $P/readme-absent-is-none "yes" "$(pv readme_absent_is_none)" # ------------------------------------------------------------ the license ---- check_eq $P/license-text-form "MIT" "$(pv license_text_form)" check_eq $P/license-text-file-is-none "yes" "$(pv license_text_file_is_none)" check_eq $P/license-file-form "the license body|" "$(pv license_file_form)" _exp=$(cat <<'EOX' !ConfigurationError: Invalid `project.license` value, expecting either `file` or `text` (got `{'file': 'LIC', 'text': 'MIT'}`) EOX ) check_eq $P/license-both "$_exp" "$(pv license_both)" _exp=$(cat <<'EOX' !ConfigurationError: Invalid `project.license` value, expecting either `file` or `text` (got `{}`) EOX ) check_eq $P/license-neither "$_exp" "$(pv license_neither)" # The license sub-table DOES reject an unexpected field -- unlike `[project]' # itself, three cases below. _exp=$(cat <<'EOX' !ConfigurationError: Unexpected field `project.license.spdx` EOX ) check_eq $P/license-unexpected-field "$_exp" "$(pv license_unexpected_field)" _exp=$(cat <<'EOX' !ConfigurationError: License file not found (`NOPE`) EOX ) check_eq $P/license-missing-file "$_exp" "$(pv license_missing_file)" check_eq $P/license-absent-is-none "yes" "$(pv license_absent_is_none)" # --------------------------------------------------------- the error paths --- _exp=$(cat <<'EOX' !ConfigurationError: Section `project` missing in pyproject.toml EOX ) check_eq $P/missing-project-section "$_exp" "$(pv missing_project_section)" _exp=$(cat <<'EOX' !ConfigurationError: Field `project.name` missing EOX ) check_eq $P/missing-name "$_exp" "$(pv missing_name)" # An EMPTY name is the same answer, because the test is `if not name'. check_eq $P/empty-name "$_exp" "$(pv empty_name)" # NOT ConfigurationError. packaging's own exception comes straight out of an # unguarded `Version(...)' / `SpecifierSet(...)', so a caller catching only # ConfigurationError does not catch either of these two. check_eq $P/malformed-version "!InvalidVersion: Invalid version: 'not.a.version'" \ "$(pv malformed_version)" check_eq $P/malformed-requires-python \ "!InvalidSpecifier: Invalid specifier: 'totally bogus'" \ "$(pv malformed_requires_python)" # AN UNKNOWN KEY IS SILENTLY IGNORED -- the headline negative, and the one the # ticket expected to raise. check_eq $P/unknown-key-ignored "yes" "$(pv unknown_key_ignored)" check_eq $P/unknown-key-not-an-attribute "yes" "$(pv unknown_key_not_an_attribute)" _exp=$(cat <<'EOX' !ConfigurationError: Field `project.name` has an invalid type, expecting a string (got `42`) EOX ) check_eq $P/name-wrong-type "$_exp" "$(pv name_wrong_type)" _exp=$(cat <<'EOX' !ConfigurationError: Field `project.keywords` has an invalid type, expecting a list of strings (got `alpha`) EOX ) check_eq $P/keywords-wrong-type "$_exp" "$(pv keywords_wrong_type)" _exp=$(cat <<'EOX' !ConfigurationError: Field `project.keywords` contains item with invalid type, expecting a string (got `2`) EOX ) check_eq $P/keywords-wrong-item-type "$_exp" "$(pv keywords_wrong_item_type)" _exp=$(cat <<'EOX' !ConfigurationError: Field `project.urls.homepage` has an invalid type, expecting a string (got `1`) EOX ) check_eq $P/urls-wrong-value-type "$_exp" "$(pv urls_wrong_value_type)" _exp=$(cat <<'EOX' !ConfigurationError: Field `project.authors` has an invalid type, expecting a list of dictionaries containing the `name` and/or `email` keys (got `['Ann']`) EOX ) check_eq $P/authors-wrong-type "$_exp" "$(pv authors_wrong_type)" # packaging's parse error is quoted INTO the ConfigurationError message, carets # and all -- three lines, folded here onto one with `|'. _exp=$(cat <<'EOX' !ConfigurationError: Field `project.dependencies` contains an invalid PEP 508 requirement string `not a requirement!!` (`Expected end or semicolon (after name and no valid version specifier)| not a requirement!!| ^`) EOX ) check_eq $P/bad-dependency "$_exp" "$(pv bad_dependency)" _exp=$(cat <<'EOX' !ConfigurationError: Field `project.optional-dependencies.test` contains an invalid PEP 508 requirement string `not a requirement!!` (`Expected end or semicolon (after name and no valid version specifier)| not a requirement!!| ^`) EOX ) check_eq $P/bad-optional-dependency "$_exp" "$(pv bad_optional_dependency)" _exp=$(cat <<'EOX' !ConfigurationError: Field `project.optional-dependencies.test` has an invalid type, expecting a dictionary PEP 508 requirement strings (got `pytest`) EOX ) check_eq $P/optional-dependency-wrong-type "$_exp" \ "$(pv optional_dependency_wrong_type)" _exp=$(cat <<'EOX' !ConfigurationError: Field `project.entry-points.alpha` has an invalid type, expecting a dictionary of entrypoints (got `not-a-table`) EOX ) check_eq $P/entrypoint-wrong-type "$_exp" "$(pv entrypoint_wrong_type)" # ------------------------------------------------------ as_rfc822(), by field - check_eq $P/rfc-type "RFC822Message" "$(pv rfc_type)" # The header ORDER is `write_to_rfc822''s statement order, and a reader of the # generated PKG-INFO depends on it as much as on the values. check_eq $P/rfc-headers-order \ "Metadata-Version,Name,Version,Summary,Keywords,Home-page,Author,Author-Email,Maintainer-Email,License,Classifier,Project-URL,Requires-Python,Requires-Dist,Provides-Extra,Description-Content-Type" \ "$(pv rfc_headers_order)" # 2.1 while `dynamic' is empty; 2.2 the moment it is not (`dyn-metadata-version'). check_eq $P/rfc-metadata-version "2.1" "$(pv rfc_metadata_version)" # The NORMALISED name, not the name as written in the file. check_eq $P/rfc-name "sample-project-name" "$(pv rfc_name)" check_eq $P/rfc-version "1.2.3.dev4" "$(pv rfc_version)" check_eq $P/rfc-summary "A sample project used by the brintOS self-test." \ "$(pv rfc_summary)" # SPACE-SEPARATED, where the core metadata specification says comma-separated. check_eq $P/rfc-keywords "alpha beta gamma" "$(pv rfc_keywords)" # `homepage' is rendered TWICE: once as the legacy `Home-page', once as a # `Project-URL' -- and the Project-URL label is `str.capitalize()'d, which # lowercases everything after the first letter. check_eq $P/rfc-home-page "https://example.com/home" "$(pv rfc_home_page)" check_eq $P/rfc-project-url \ "Homepage, https://example.com/home|Bug tracker, https://example.com/bugs" \ "$(pv rfc_project_url)" # `Author' carries only the people WITHOUT an email; everyone WITH one is in # `Author-Email' as `Name '. So the fixture's two authors land in # different headers, `Maintainer' never appears at all because the one # maintainer has an email, and that maintainer's missing name renders as the # literal `Unknown'. check_eq $P/rfc-author "Nameless Only" "$(pv rfc_author)" check_eq $P/rfc-author-email "Ann Author " "$(pv rfc_author_email)" check_eq $P/rfc-maintainer-absent "(absent)" "$(pv rfc_maintainer)" check_eq $P/rfc-maintainer-email "Unknown " \ "$(pv rfc_maintainer_email)" check_eq $P/rfc-license "MIT, for the purposes of this test.|" "$(pv rfc_license)" # One `Classifier' header PER classifier -- the repeat semantics the # specification requires, and the half a reader most easily gets wrong. check_eq $P/rfc-classifier \ "Programming Language :: Python :: 3|License :: OSI Approved :: MIT License" \ "$(pv rfc_classifier)" check_eq $P/rfc-requires-python "<4,>=3.8" "$(pv rfc_requires_python)" # One `Requires-Dist' per dependency, then one per extra requirement with the # extra's marker appended -- and `Provides-Extra' once per extra. check_eq $P/rfc-requires-dist \ 'packaging>=19.0|tomli; python_version < "3.11"|pytest>=7; extra == "test"|coverage; extra == "test"' \ "$(pv rfc_requires_dist)" check_eq $P/rfc-provides-extra "test" "$(pv rfc_provides_extra)" # The readme becomes the message BODY, with its content type as a header. check_eq $P/rfc-description-content-type "text/markdown" \ "$(pv rfc_description_content_type)" check_eq $P/rfc-body "The readme body.|Second line.|" "$(pv rfc_body)" # NONE of `scripts', `gui-scripts' or `entry-points' is core metadata, and no # `Dynamic' header appears while `dynamic' is empty. check_eq $P/rfc-no-scripts-header "yes" "$(pv rfc_no_scripts_header)" check_eq $P/rfc-no-dynamic-header "(absent)" "$(pv rfc_no_dynamic_header)" # THE IDEMPOTENCY BREAK. `_build_extra_req' mutates the STORED Requirement, so # rendering the same unchanged object a second time doubles the extra marker. # A backend that renders metadata for a wheel and again for its sdist ships the # second one. check_eq $P/rfc-extra-req-first \ 'pytest>=7; extra == "test" / coverage; extra == "test"' \ "$(pv rfc_extra_req_first)" check_eq $P/rfc-extra-req-second \ 'pytest>=7; extra == "test" and extra == "test" / coverage; extra == "test" and extra == "test"' \ "$(pv rfc_extra_req_second)" # ------------------------------------------- the minimal object, on its own -- # `__setitem__' DROPS a falsy value, so a project with no keywords and no # authors emits three headers and nothing else -- not empty ones. check_eq $P/mini-headers "Metadata-Version,Name,Version" "$(pv mini_headers)" check_eq $P/mini-metadata-version "2.1" "$(pv mini_metadata_version)" check_eq $P/mini-no-keywords "yes" "$(pv mini_no_keywords)" check_eq $P/mini-no-author "yes" "$(pv mini_no_author)" check_eq $P/mini-body-is-none "yes" "$(pv mini_body_is_none)" check_eq $P/mini-str "Metadata-Version: 2.1|Name: mini|Version: 0.1|" "$(pv mini_str)" check_eq $P/mini-bytes-match "yes" "$(pv mini_bytes_match)" # A non-empty `dynamic' lifts Metadata-Version to 2.2 and adds one `Dynamic' # header per entry -- the Core Metadata 2.2 half of the renderer. check_eq $P/dyn-metadata-version "2.2" "$(pv dyn_metadata_version)" check_eq $P/dyn-dynamic-headers "description|keywords" "$(pv dyn_dynamic_headers)" # ------------------------------------------- RFC822Message on its own -------- check_eq $P/msg-drops-empty "yes" "$(pv msg_drops_empty)" check_eq $P/msg-drops-none "yes" "$(pv msg_drops_none)" check_eq $P/msg-repeats "one|two" "$(pv msg_repeats)" # A multi-line value is folded with EIGHT spaces of continuation indent. check_eq $P/msg-fold "Rep: one|Rep: two|Fold: first| second|" "$(pv msg_fold)" check_eq $P/msg-body-absent "yes" "$(pv msg_body_absent)" rm -rf "$W" "$_pw"