# brintOS on-target probe for python3-platformdirs 4.2.0 (BRINTOS-1841). # # ONE interpreter start for the whole of 99-python3-platformdirs. A python3 start # on this arm costs tens of seconds before any work begins, so every behavioural # statement the suite makes is measured here and emitted as one key=value line; # the suite greps the keys. Nothing is printed that the suite does not read, and # no key is emitted twice. # # WHAT MAKES AN x86-64 CONTROL TRANSFERABLE. platformdirs is pure Python and # every answer it gives is a string composed by os.path.join out of an # environment variable, a home directory and a uid. Not one assertion depends on # pointer width, byte order or the platform tag. The two values that DO vary # between the control and the target -- the home directory and the uid -- are # emitted as their own keys (`home`, `uid`) and the suite composes the expected # paths from them, so what is asserted is the COMPOSITION RULE rather than a # path that happens to be this machine's. # # THE ENVIRONMENT IS SET BY THIS PROBE, NOT INHERITED. Every XDG_* variable is # removed from os.environ before the defaults are measured and set explicitly # before the overrides are, so the answers do not depend on whatever the guest's # init happened to export. HOME is left exactly as inherited: it is the one input # whose real value the port is a statement about. # # argv[1] is a writable scratch directory; the XDG override targets and the # user-dirs.dirs fixture are created under it. import os import sys import traceback OUT = [] def emit(k, v): OUT.append("%s=%s" % (k, v)) def yn(b): return "yes" if b else "no" def flush(): sys.stdout.write("\n".join(OUT) + "\n") sys.stdout.flush() def die(where, exc): # A failure anywhere below leaves the remaining keys absent, and every suite # case reading them reds with got:''. This key is what names the cause. emit("fatal_at", where) emit("fatal", type(exc).__name__) flush() traceback.print_exc(file=sys.stderr) sys.exit(0) W = sys.argv[1] if len(sys.argv) > 1 else "/tmp" XDG_ALL = ( "XDG_DATA_HOME", "XDG_CONFIG_HOME", "XDG_CACHE_HOME", "XDG_STATE_HOME", "XDG_RUNTIME_DIR", "XDG_DATA_DIRS", "XDG_CONFIG_DIRS", "XDG_DOCUMENTS_DIR", "XDG_DOWNLOAD_DIR", "XDG_PICTURES_DIR", "XDG_VIDEOS_DIR", "XDG_MUSIC_DIR", "XDG_DESKTOP_DIR", ) def clear_xdg(): for k in XDG_ALL: os.environ.pop(k, None) # The two machine-dependent inputs, emitted so the suite composes its # expectations from them instead of from this machine's own answers. emit("home", os.path.expanduser("~")) emit("uid", str(os.getuid())) # --------------------------------------------------------------- the module --- try: import platformdirs from platformdirs import PlatformDirs, PlatformDirsABC except Exception as e: # noqa: BLE001 emit("import_ok", "no") die("import", e) emit("import_ok", "yes") emit("version", getattr(platformdirs, "__version__", "")) emit("version_info", ".".join(str(x) for x in getattr(platformdirs, "__version_info__", ()))) emit("module_file", getattr(platformdirs, "__file__", "")) # The three modules that actually ship and are reachable here. api.py holds the # ABC every answer goes through; unix.py is the ONLY platform backend this image # can select, and naming its file is what says the selection landed there rather # than on the macOS or Windows one. for name in ("api", "unix"): try: __import__("platformdirs." + name) emit(name + "_file", getattr(sys.modules["platformdirs." + name], "__file__", "")) except Exception: # noqa: BLE001 emit(name + "_file", "") emit("all_names", ",".join(getattr(platformdirs, "__all__", ()))) # WHICH BACKEND WAS SELECTED. _set_platform_dir_class() branches on sys.platform # and on the two ANDROID_* variables; on this image sys.platform is "linux" and # neither is set, so Unix is the only correct answer. A backend that had resolved # to Android or macOS would still answer every accessor -- with paths that do not # exist here -- so this is asserted directly rather than inferred from a path. emit("sys_platform", sys.platform) emit("platform_class", PlatformDirs.__name__) emit("platform_class_module", PlatformDirs.__module__) emit("platform_is_abc_subclass", yn(issubclass(PlatformDirs, PlatformDirsABC))) # appdirs' name for the same class. platformdirs is appdirs' successor and this # alias is the whole of its backwards compatibility; a consumer that still says # AppDirs must get the same class object, not merely a similar one. emit("appdirs_is_platformdirs", yn(getattr(platformdirs, "AppDirs", None) is PlatformDirs)) # ---------------------------------------------------- the defaults, XDG unset -- # THE UNIX DEFAULTS FROM THE BASEDIR SPEC. With no XDG_* in the environment every # user accessor composes out of HOME and every site accessor is a fixed absolute # path. These are what an application on this image gets when nobody has # configured anything, which is the ordinary case. try: clear_xdg() d = PlatformDirs() emit("unset_user_data", d.user_data_dir) emit("unset_user_config", d.user_config_dir) emit("unset_user_cache", d.user_cache_dir) emit("unset_user_state", d.user_state_dir) emit("unset_user_log", d.user_log_dir) emit("unset_user_runtime", d.user_runtime_dir) emit("unset_site_data", d.site_data_dir) emit("unset_site_config", d.site_config_dir) emit("unset_site_cache", d.site_cache_dir) emit("unset_site_runtime", d.site_runtime_dir) emit("unset_user_documents", d.user_documents_dir) emit("unset_user_downloads", d.user_downloads_dir) emit("unset_user_pictures", d.user_pictures_dir) emit("unset_user_videos", d.user_videos_dir) emit("unset_user_music", d.user_music_dir) emit("unset_user_desktop", d.user_desktop_dir) emit("defaults_ok", "yes") except Exception as e: # noqa: BLE001 emit("defaults_ok", "no") die("defaults", e) # user_log_dir is user_state_dir with "log" appended, and ONLY because opinion # defaults True. Both directions are measured: an implementation that had stopped # being opinionated would return the two equal, and one that appended # unconditionally would ignore opinion=False. try: emit("unopinionated_log", PlatformDirs(opinion=False).user_log_dir) emit("opinion_ok", "yes") except Exception as e: # noqa: BLE001 emit("opinion_ok", "no") # --------------------------------------------- the overrides, XDG_* set ------ # THE SPEC'S WHOLE POINT: each accessor follows its OWN variable and no other. A # distinct directory per variable is what makes a crossed wire visible -- an # implementation that read XDG_DATA_HOME for the cache would pass a test that set # them all to one path. XD = { "XDG_DATA_HOME": os.path.join(W, "xdg-data"), "XDG_CONFIG_HOME": os.path.join(W, "xdg-config"), "XDG_CACHE_HOME": os.path.join(W, "xdg-cache"), "XDG_STATE_HOME": os.path.join(W, "xdg-state"), "XDG_RUNTIME_DIR": os.path.join(W, "xdg-runtime"), } try: clear_xdg() for k, v in XD.items(): os.makedirs(v, exist_ok=True) os.environ[k] = v d = PlatformDirs() emit("set_user_data", d.user_data_dir) emit("set_user_config", d.user_config_dir) emit("set_user_cache", d.user_cache_dir) emit("set_user_state", d.user_state_dir) emit("set_user_log", d.user_log_dir) emit("set_user_runtime", d.user_runtime_dir) emit("set_site_runtime", d.site_runtime_dir) # A variable set to whitespace is treated as UNSET -- `if not path.strip()`. # It is upstream's documented behaviour and a shell that exports an empty # variable is common enough that the fallback is what actually runs. os.environ["XDG_DATA_HOME"] = " " emit("blank_user_data", PlatformDirs().user_data_dir) os.environ["XDG_DATA_HOME"] = XD["XDG_DATA_HOME"] emit("overrides_ok", "yes") except Exception as e: # noqa: BLE001 emit("overrides_ok", "no") die("overrides", e) # ----------------------------------------------- appname, version, appauthor -- # The path elements platformdirs appends, which is what separates one # application's data from another's on a shared image. try: d = PlatformDirs("bstapp") emit("app_data", d.user_data_dir) d = PlatformDirs("bstapp", version="1.2") emit("app_ver_data", d.user_data_dir) emit("app_ver_config", d.user_config_dir) emit("app_ver_cache", d.user_cache_dir) emit("app_ver_state", d.user_state_dir) emit("app_ver_log", d.user_log_dir) # appauthor is a WINDOWS concept. On Unix it must change nothing at all, and # a backend that had started honouring it would put every application's data # one directory deeper than the spec says. emit("appauthor_ignored", yn(PlatformDirs("bstapp", "bstorg").user_data_dir == PlatformDirs("bstapp").user_data_dir)) emit("appname_ok", "yes") except Exception as e: # noqa: BLE001 emit("appname_ok", "no") # --------------------------------------------------------------- multipath ---- # site_data_dir/site_config_dir return the FIRST entry by default and the whole # os.pathsep-joined list when multipath is on. Both are asserted because a # consumer that iterates the default answer would silently read one directory. try: os.environ["XDG_DATA_DIRS"] = "/bst/a:/bst/b" os.environ["XDG_CONFIG_DIRS"] = "/bst/c:/bst/d" emit("site_data_first", PlatformDirs("bstapp").site_data_dir) emit("site_data_multi", PlatformDirs("bstapp", multipath=True).site_data_dir) emit("site_config_first", PlatformDirs("bstapp").site_config_dir) emit("site_config_multi", PlatformDirs("bstapp", multipath=True).site_config_dir) # ...and site_data_path collapses a multipath answer back to ONE Path, which # is the documented asymmetry between the _dir and _path halves of the API. emit("site_data_path_multi", str(PlatformDirs("bstapp", multipath=True).site_data_path)) emit("iter_data_dirs", ",".join(PlatformDirs("bstapp").iter_data_dirs())) emit("iter_config_dirs", ",".join(PlatformDirs("bstapp").iter_config_dirs())) os.environ.pop("XDG_DATA_DIRS", None) os.environ.pop("XDG_CONFIG_DIRS", None) emit("multipath_ok", "yes") except Exception as e: # noqa: BLE001 emit("multipath_ok", "no") # ------------------------------------------------------- the module functions -- # platformdirs' public surface is 32 module-level functions as well as the class, # and they are what most callers actually use. Each must agree with the property # it wraps; a divergence would be invisible to anyone testing only the class. # # THE SURFACE IS NOT UNIFORM, and that is upstream's shape rather than a defect. # The ten appname-aware functions take (appname, appauthor, version, ...); the six # MEDIA functions -- documents, downloads, pictures, videos, music, desktop -- # take NO arguments at all, because ~/Documents belongs to the user and not to an # application. They are therefore driven separately, and the refusal is measured # by type rather than assumed: a release that quietly gave them a version # parameter would change what every caller's paths look like. try: d = PlatformDirs("bstapp", version="1.2") same = True checked = 0 for prop in ("user_data_dir", "user_config_dir", "user_cache_dir", "user_state_dir", "user_log_dir", "user_runtime_dir", "site_data_dir", "site_config_dir", "site_cache_dir", "site_runtime_dir"): fn = getattr(platformdirs, prop) checked += 1 if fn("bstapp", version="1.2") != getattr(d, prop): same = False emit("func_mismatch", prop) break emit("func_checked", str(checked)) emit("func_matches_property", yn(same)) plain = PlatformDirs() media_same = True media_checked = 0 media_rejects = 0 for prop in ("user_documents_dir", "user_downloads_dir", "user_pictures_dir", "user_videos_dir", "user_music_dir", "user_desktop_dir"): fn = getattr(platformdirs, prop) media_checked += 1 if fn() != getattr(plain, prop): media_same = False emit("media_mismatch", prop) break try: fn("bstapp", version="1.2") except TypeError: media_rejects += 1 emit("media_checked", str(media_checked)) emit("media_matches_property", yn(media_same)) emit("media_reject_appname", str(media_rejects)) # The *_path half returns pathlib.Path over the same string. Asserted as both # a type and a value: a Path built from a different string would still be a # Path, and a str returned where a Path is documented breaks `/` on the # caller's side. p = platformdirs.user_data_path("bstapp", version="1.2") emit("path_is_pathlib", yn(type(p).__name__ in ("PosixPath", "Path"))) emit("path_matches_dir", yn(str(p) == platformdirs.user_data_dir("bstapp", version="1.2"))) emit("functions_ok", "yes") except Exception as e: # noqa: BLE001 emit("functions_ok", "no") # ------------------------------------------------------------ ensure_exists --- # The one accessor with a SIDE EFFECT, and the only place platformdirs touches # the filesystem at all. It is worth a case of its own here because it is the one # behaviour a read-only or otherwise unusual rootfs could break while every # string answer stayed right. try: os.environ["XDG_DATA_HOME"] = os.path.join(W, "xdg-data") want = os.path.join(W, "xdg-data", "bstmk", "9.9") emit("ensure_absent_before", yn(not os.path.isdir(want))) got = PlatformDirs("bstmk", version="9.9", ensure_exists=True).user_data_dir emit("ensure_returns", got) emit("ensure_created", yn(os.path.isdir(want))) # ...and the default must NOT create anything, or every path query on this # image would litter the rootfs. want2 = os.path.join(W, "xdg-data", "bstnomk", "9.9") PlatformDirs("bstnomk", version="9.9").user_data_dir emit("nocreate_default", yn(not os.path.isdir(want2))) emit("ensure_ok", "yes") except Exception as e: # noqa: BLE001 emit("ensure_ok", "no") # ------------------------------------------------------- user-dirs.dirs ------- # THE ONE PLACE platformdirs READS A FILE. The media accessors consult # $XDG_CONFIG_HOME/user-dirs.dirs through ConfigParser before falling back to the # environment and then to ~/Xxx, and they expand a literal $HOME inside it. That # path pulls in configparser, a stdlib module in a different split package from # the rest, so it is a statement about this image's python3 as well as about # platformdirs -- and it is the only accessor here that can fail on an image # where every string-composing one works. try: cfg = os.path.join(W, "xdg-config") os.makedirs(cfg, exist_ok=True) os.environ["XDG_CONFIG_HOME"] = cfg with open(os.path.join(cfg, "user-dirs.dirs"), "w") as fh: fh.write('XDG_DOCUMENTS_DIR="$HOME/BstDocs"\n') fh.write('XDG_MUSIC_DIR="/bst/abs/music"\n') d = PlatformDirs() emit("userdirs_documents", d.user_documents_dir) emit("userdirs_music", d.user_music_dir) # A key ABSENT from the file falls through to the environment, and only then # to the tilde default. Both fallthrough steps are measured. os.environ["XDG_VIDEOS_DIR"] = os.path.join(W, "envvid") emit("userdirs_videos_env", d.user_videos_dir) os.environ.pop("XDG_VIDEOS_DIR", None) emit("userdirs_pictures_fallback", d.user_pictures_dir) os.remove(os.path.join(cfg, "user-dirs.dirs")) emit("userdirs_ok", "yes") except Exception as e: # noqa: BLE001 emit("userdirs_ok", "no") # ------------------------------------------------------------- __main__ ------- # The package ships an executable module and no console script, so `python3 -m # platformdirs` is its only command-line surface. It is driven here rather than # from the shell because a second interpreter start costs the suite more than # every other case in it put together. main() prints four blocks of fifteen # properties; what is asserted is that it ran to the end and named its version. try: clear_xdg() import io from platformdirs.__main__ import main as pd_main buf = io.StringIO() saved, sys.stdout = sys.stdout, buf try: pd_main() finally: sys.stdout = saved text = buf.getvalue() lines = [ln for ln in text.splitlines() if ln.strip()] emit("main_ran", "yes") emit("main_first_line", lines[0] if lines else "") emit("main_line_count", str(len(lines))) emit("main_has_user_data", yn(any(ln.startswith("user_data_dir: ") for ln in lines))) emit("main_blocks", str(text.count("-- app dirs"))) except Exception as e: # noqa: BLE001 emit("main_ran", "no") # ------------------------------------------------------- no compiled extension -- # Asserted from INSIDE the interpreter as well as from the filesystem in the # suite: this reports what the imported package's own directory holds, which is # the copy that answered every measurement above. try: pkgdir = os.path.dirname(platformdirs.__file__) names = sorted(os.listdir(pkgdir)) emit("pkg_dir", pkgdir) emit("pkg_py_names", ",".join(n for n in names if n.endswith(".py"))) emit("pkg_so_count", str(len([n for n in names if ".so" in n]))) except Exception: # noqa: BLE001 emit("pkg_so_count", "") emit("done", "yes") flush()