Skip to content

interpreter: canonicalize when initializing search paths dynamically - #8299

Merged
youknowone merged 1 commit into
RustPython:mainfrom
lsahn-gh:lsahn
Jul 19, 2026
Merged

youknowone merged 1 commit into
RustPython:mainfrom
lsahn-gh:lsahn

Conversation

@lsahn-gh

@lsahn-gh lsahn-gh commented Jul 17, 2026 •

Copy link
Copy Markdown
Contributor

RustPython Test Discovery Path Mismatch: Root Cause and Solution Comparison

This document provides a detailed analysis of the AssertionError: Path must be within the project failure during unittest discovery in RustPython, comparing two approaches to fix it.

1. Root Cause Analysis

The Error Scenario

When executing the test suite with:

cargo run -- Lib/test/test_unittest/test_program.py

unittest.loader encounters an assertion failure during test discovery:

AssertionError: Path must be within the project

The Mechanism

  1. Unittest Loader Constraint:
    CPython's unittest library asserts that any discovered test package (start_dir) must reside inside the project's top-level directory (top_level_dir).
    It checks this by computing the relative path and verifying it doesn't navigate upwards:

    _relpath = os.path.relpath(path, self._top_level_dir)
    assert not _relpath.startswith('..'), "Path must be within the project"
  2. Dual-Root Mismatch in RustPython:
    Unlike CPython, RustPython manages the standard library in two separate locations in the workspace:

    • Workspace Lib/: Contains the test suite (Lib/test/...) and standard library files.
    • crates/pylib/Lib/: A helper directory used for packaging. crates/pylib/Lib is a symlink pointing to the real workspace ../../Lib/.
  3. Runtime Directory Discrepancy:

    • sys.path contains crates/pylib/Lib (the path to the symlink).
    • Thus, when test.support is imported, its __file__ is resolved relative to the symlink: /home/lsahn/projects/oss/RustPython/crates/pylib/Lib/test/support/__init__.py.
    • Consequently, STDLIB_DIR resolves to the symlink root: /home/lsahn/projects/oss/RustPython/crates/pylib/Lib.
    • However, since test_program.py is executed directly from the workspace, its pkg_dir is: /home/lsahn/projects/oss/RustPython/Lib/test/test_unittest/testmock.
    • When os.path.relpath(pkg_dir, top_dir) is called:
      os.path.relpath(
          "/home/lsahn/projects/oss/RustPython/Lib/test/test_unittest/testmock",
          "/home/lsahn/projects/oss/RustPython/crates/pylib/Lib"
      )
      # Result: "../../../Lib/test/test_unittest/testmock"
      Because the relative path starts with .., the assertion fails.

2. Comparison of Solutions

Solution 1: Python-Level Fix (Modifying Lib/test/support/__init__.py)

This approach intercepts the path discrepancy within the test framework itself.

  • Implementation:
    Inside load_package_tests in Lib/test/support/__init__.py, check if pkg_dir falls outside STDLIB_DIR (using .startswith('..')). If so, trace up from pkg_dir to dynamically find the standard library root containing the test package.
  • Pros:
    • Localized strictly to the test framework execution path.
  • Cons:
    • Violates Upstream Merging: RustPython periodically syncs test files from upstream CPython in bulk. Modifying files under Lib/test/ creates merge conflicts and complicates maintenance.
    • Band-Aid Fix: Solves the symptoms for unittest discovery but does not resolve the underlying path duplicate representation in sys.path.

Solution 2: Rust-Level Fix (Modifying src/interpreter.rs - Recommended)

This approach resolves the symlink at the interpreter level when building standard library search paths.

  • Implementation:
    When initializing search paths dynamically in setup_dynamic_stdlib using collect_stdlib_paths(), run std::fs::canonicalize on the paths. This resolves the crates/pylib/Lib symlink to its target /home/lsahn/projects/oss/RustPython/Lib before adding it to sys.path.
  • Pros:
    • Zero Changes to Standard Library: Keeps Lib/test files pristine and 100% compatible with upstream CPython.
    • Fixes the Root Cause: Standardizes search paths so that __file__ always refers to canonical paths, preventing path mismatches across the entire runtime environment.
    • Robust: Prevents other modules or user code from experiencing similar symlink-based path mismatches.
    • No side effects on CPython standard library sync.
  • Cons:
    • Requires rebuilding the interpreter (though this is standard when changing Rust sources).

Why Did Only This Specific Test Case Fail?

This document explains why the path resolution mismatch and subsequent AssertionError only affected a tiny subset of tests (specifically unittest discovery within test_unittest), while other tests involving symlinks ran successfully.

1. The Normal Case: Why Most Tests Succeed

Most standard library tests (e.g., test_dict.py, test_bytes.py) do not fail because they do not trigger path verification:

  • No Test Discovery: They are single files imported and executed directly by the test runner. They never invoke unittest.loader.discover().
  • Single Directory Root: Even when standard unittest discover commands are run from the CLI (e.g., rustpython -m unittest discover -s Lib/test), both the start directory (start_dir) and top-level directory (top_level_dir) reside under the same folder (Lib/). Since there is no cross-referencing between different root directories, os.path.relpath() never returns a path starting with ...

2. The Failure Case: The Mismatch Scenario

The failure in testSelectedTestNamesFunctionalTest occurred because it met a highly specific set of conditions:

  1. Sub-package Hook (testmock):
    The testmock sub-package defines a custom load_tests hook that delegates to a shared helper in test.support:

    def load_tests(*args):
        return load_package_tests(os.path.dirname(__file__), *args)
  2. Hardcoded STDLIB_DIR in load_package_tests:
    The helper in test.support invokes discovery, hardcoding the project root to STDLIB_DIR:

    # Inside test.support.load_package_tests
    top_dir = STDLIB_DIR
    package_tests = loader.discover(start_dir=pkg_dir, top_level_dir=top_dir, ...)
  3. Dual-Root Mismatch:
    Because test.support is imported from the packaged stdlib directory, while the test file itself runs from the workspace directory:

    • pkg_dir (start directory) resolves to: .../Lib/test/test_unittest/testmock (workspace realpath).
    • top_dir (top-level directory) resolves to: .../crates/pylib/Lib (symlink path).

Because loader.discover was passed two paths belonging to different directories (one starting with Lib/ and the other with crates/pylib/Lib/), os.path.relpath computed a relative path that navigated upwards (beginning with ..). This directly triggered the assertion: assert not _relpath.startswith('..').

Summary

The error only surfaces when a test package runs from the workspace Lib directory but delegates its sub-test discovery to test.support (imported from crates/pylib/Lib), forcing the unittest discoverer to compare paths across two different filesystem roots.

Summary by CodeRabbit

  • Bug Fixes
    • Improved standard library path resolution to make module discovery more reliable.
    • Enhanced Windows compatibility by removing extended (\\?\) path prefixes when present.
    • Kept existing fallback behavior when a standard library path can’t be canonicalized.

@coderabbitai

coderabbitai Bot commented Jul 17, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: d71ab4e0-3cc3-4626-a8bb-b1e8889a0445

📥 Commits

Reviewing files that changed from the base of the PR and between 2244383 and 4090ca4.

⛔ Files ignored due to path filters (1)
  • Lib/test/test_unittest/test_program.py is excluded by !Lib/**
📒 Files selected for processing (1)
  • src/interpreter.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/interpreter.rs

📝 Walkthrough

Walkthrough

Dynamic standard-library paths are canonicalized before configuring the standard-library directory and module search paths, with fallback handling and Windows path-prefix normalization.

Changes

Dynamic stdlib path normalization

Layer / File(s) Summary
Normalize dynamic stdlib paths
src/interpreter.rs
setup_dynamic_stdlib canonicalizes collected paths, falls back to the original path when needed, strips the Windows \\?\ prefix, and uses the resulting paths for stdlib and module search configuration.

Estimated code review effort: 2 (Simple) | ~10 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: canonicalizing dynamic interpreter search paths during initialization.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/interpreter.rs`:
- Around line 65-72: Update the path-string closure around the local variable s
to avoid a let-and-return pattern on non-Windows builds: keep the Windows
strip_prefix handling, but defer to_string_lossy().into_owned() until the final
return expression so the conversion is performed once without duplicating
platform-specific logic.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: a8265471-32b9-4925-b0a9-ca7a37610390

📥 Commits

Reviewing files that changed from the base of the PR and between 1205fd2 and 2244383.

⛔ Files ignored due to path filters (1)
  • Lib/test/test_unittest/test_program.py is excluded by !Lib/**
📒 Files selected for processing (1)
  • src/interpreter.rs

Comment thread src/interpreter.rs Outdated
@github-actions

Copy link
Copy Markdown
Contributor

📦 Library Dependencies

The following Lib/ modules were modified. Here are their dependencies:

[ ] lib: cpython/Lib/unittest
[x] test: cpython/Lib/test/test_unittest (TODO: 11)

dependencies:

  • unittest (native: _io, _log, async_case, builtins, case, loader, main, os.path, result, runner, signals, suite, sys, time, unittest.util, util)
    • warnings (native: _contextvars, _thread, _warnings, builtins, sys)
    • _colorize, annotationlib, argparse, asyncio, collections, contextlib, contextvars, dataclasses, difflib, fnmatch, functools, inspect, io, logging, os, pkgutil, pprint, re, signal, threading, traceback, types, weakref

dependent tests: (408 tests)

  • unittest: regrtestdata test___all__ test__colorize test__locale test__opcode test__osx_support test_abc test_abstract_numbers test_android test_annotationlib test_apple test_argparse test_array test_asdl_parser test_ast test_asyncgen test_asyncio test_atexit test_audit test_augassign test_base64 test_baseexception test_bdb test_bigaddrspace test_bigmem test_binascii test_binop test_bisect test_bool test_buffer test_bufio test_builtin test_bytes test_bz2 test_c_locale_coercion test_calendar test_call test_charmapcodec test_class test_clinic test_cmath test_cmd test_cmd_line test_cmd_line_script test_code test_code_module test_codeccallbacks test_codecencodings_cn test_codecencodings_hk test_codecencodings_iso2022 test_codecencodings_jp test_codecencodings_kr test_codecencodings_tw test_codecmaps_cn test_codecmaps_hk test_codecmaps_jp test_codecmaps_kr test_codecmaps_tw test_codecs test_codeop test_collections test_colorsys test_compare test_compile test_compileall test_complex test_concurrent_futures test_configparser test_contains test_context test_contextlib test_contextlib_async test_copy test_copyreg test_coroutines test_cprofile test_csv test_ctypes test_datetime test_dbm test_dbm_dumb test_dbm_sqlite3 test_decimal test_decorators test_defaultdict test_deque test_descr test_descrtut test_devpoll test_dict test_dictcomps test_dictviews test_difflib test_dis test_doctest test_doctest2 test_docxmlrpc test_dtrace test_dummy_thread test_dummy_threading test_dynamic test_dynamicclassattribute test_eintr test_email test_embed test_ensurepip test_enum test_enumerate test_eof test_epoll test_errno test_except_star test_exception_group test_exception_hierarchy test_exception_variations test_exceptions test_extcall test_external_inspection test_faulthandler test_fcntl test_file test_file_eintr test_filecmp test_fileinput test_fileio test_fileutils test_finalization test_float test_flufl test_fnmatch test_fork1 test_format test_fractions test_frame test_frozen test_fstring test_ftplib test_funcattrs test_functools test_future_stmt test_gc test_generated_cases test_generator_stop test_generators test_genericalias test_genericclass test_genericpath test_genexps test_getopt test_getpass test_gettext test_glob test_global test_grammar test_graphlib test_grp test_gzip test_hash test_hashlib test_heapq test_hmac test_html test_htmlparser test_http_cookiejar test_http_cookies test_httplib test_httpservers test_idle test_imaplib test_importlib test_index test_inspect test_int test_int_literal test_io test_ioctl test_ipaddress test_isinstance test_iter test_iterlen test_itertools test_json test_keyword test_keywordonlyarg test_kqueue test_largefile test_launcher test_linecache test_list test_listcomps test_locale test_logging test_long test_longexp test_lzma test_mailbox test_marshal test_math test_math_property test_memoryio test_memoryview test_metaclass test_mimetypes test_minidom test_mmap test_modulefinder test_monitoring test_msvcrt test_multibytecodec test_multiprocessing_fork test_multiprocessing_forkserver test_multiprocessing_main_handling test_multiprocessing_spawn test_named_expressions test_netrc test_ntpath test_nturl2path test_numeric_tower test_opcache test_opcodes test_openpty test_operator test_optimizer test_optparse test_ordered_dict test_os test_osx_env test_pathlib test_patma test_pdb test_peepholer test_peg_generator test_pep646_syntax test_perfmaps test_pickle test_picklebuffer test_pickletools test_pkg test_pkgutil test_platform test_plistlib test_poll test_popen test_poplib test_positional_only_arg test_posix test_posixpath test_pow test_pprint test_print test_profile test_property test_pstats test_pty test_pulldom test_pwd test_py_compile test_pyclbr test_pydoc test_pyexpat test_pyrepl test_queue test_quopri test_raise test_random test_range test_re test_regrtest test_repl test_reprlib test_resource test_richcmp test_rlcompleter test_robotparser test_runpy test_sax test_sched test_scope test_script_helper test_secrets test_select test_selectors test_set test_setcomps test_shelve test_shlex test_shutil test_signal test_site test_slice test_smtplib test_smtpnet test_socket test_socketserver test_sort test_sqlite3 test_ssl test_stable_abi_ctypes test_stat test_statistics test_str test_strftime test_string test_string_literals test_stringprep test_strptime test_strtod test_struct test_structseq test_subclassinit test_subprocess test_sundry test_super test_support test_symtable test_syntax test_sys test_sys_setprofile test_sys_settrace test_sysconfig test_syslog test_tabnanny test_tarfile test_tempfile test_termios test_textwrap test_thread test_thread_local_bytecode test_threadedtempfile test_threading test_threading_local test_threadsignals test_time test_timeit test_timeout test_tokenize test_tomllib test_tools test_trace test_traceback test_tracemalloc test_tstring test_tty test_tuple test_turtle test_type_aliases test_type_annotations test_type_cache test_type_comments test_type_params test_typechecks test_types test_typing test_ucn test_unary test_unicode_file test_unicode_file_functions test_unicode_identifiers test_unicodedata test_unittest test_univnewlines test_unpack test_unpack_ex test_unparse test_urllib test_urllib2 test_urllib2_localnet test_urllib2net test_urllib_response test_urllibnet test_urlparse test_userdict test_userlist test_userstring test_utf8_mode test_utf8source test_uuid test_venv test_wait3 test_wait4 test_wave test_weakref test_weakset test_webbrowser test_winapi test_winconsoleio test_winreg test_winsound test_with test_wmi test_wsgiref test_xml_dom_minicompat test_xml_dom_xmlbuilder test_xml_etree test_xml_etree_c test_xmlrpc test_xpickle test_xxlimited test_xxtestfuzz test_yield_from test_zipapp test_zipfile test_zipfile64 test_zipimport test_zipimport_support test_zlib test_zoneinfo test_zstd

Legend:

  • [+] path exists in CPython
  • [x] up-to-date, [ ] outdated

@moreal moreal added the z-ca-2026 Tag to track Contribution Academy 2026 label Jul 17, 2026
Dual-Root Mismatch in RustPython:
Unlike CPython, RustPython manages the standard library
in two separate locations in the workspace:
 - Workspace 'Lib/': Contains the test suite ('Lib/test/...') and
   standard library files.
 - crates/pylib/Lib/: A helper directory used for packaging.
   'crates/pylib/Lib' is a symlink pointing to the real workspace '../../Lib/'.

Runtime Directory Discrepancy:
 - 'sys.path' contains 'crates/pylib/Lib' (the path to the symlink).
 - Thus, when 'test.support' is imported, its '__file__' is resolved relative
   to the symlink: '/opt/RustPython/crates/pylib/Lib/test/support/__init__.py'.
 - Consequently, 'STDLIB_DIR' resolves to the symlink root:
   '/opt/RustPython/crates/pylib/Lib'.
 - However, since 'test_program.py' is executed directly from the workspace,
   its 'pkg_dir' is: '/opt/RustPython/Lib/test/test_unittest/testmock'.
 - When 'os.path.relpath(pkg_dir, top_dir)' is called:
   ---
   os.path.relpath(
       "/opt/RustPython/Lib/test/test_unittest/testmock",
       "/opt/RustPython/crates/pylib/Lib"
   )
   # Result: "../../../Lib/test/test_unittest/testmock"
   ---
   Because the relative path starts with '..', the assertion fails.

Assisted-by: Gemini

@youknowone youknowone left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

catching good point, thanks!

@youknowone
youknowone merged commit e68d9c2 into RustPython:main Jul 19, 2026
27 checks passed
@lsahn-gh
lsahn-gh deleted the lsahn branch July 19, 2026 05:32
youknowone pushed a commit that referenced this pull request Sep 16, 2026
…8299)

Dual-Root Mismatch in RustPython:
Unlike CPython, RustPython manages the standard library
in two separate locations in the workspace:
 - Workspace 'Lib/': Contains the test suite ('Lib/test/...') and
   standard library files.
 - crates/pylib/Lib/: A helper directory used for packaging.
   'crates/pylib/Lib' is a symlink pointing to the real workspace '../../Lib/'.

Runtime Directory Discrepancy:
 - 'sys.path' contains 'crates/pylib/Lib' (the path to the symlink).
 - Thus, when 'test.support' is imported, its '__file__' is resolved relative
   to the symlink: '/opt/RustPython/crates/pylib/Lib/test/support/__init__.py'.
 - Consequently, 'STDLIB_DIR' resolves to the symlink root:
   '/opt/RustPython/crates/pylib/Lib'.
 - However, since 'test_program.py' is executed directly from the workspace,
   its 'pkg_dir' is: '/opt/RustPython/Lib/test/test_unittest/testmock'.
 - When 'os.path.relpath(pkg_dir, top_dir)' is called:
   ---
   os.path.relpath(
       "/opt/RustPython/Lib/test/test_unittest/testmock",
       "/opt/RustPython/crates/pylib/Lib"
   )
   # Result: "../../../Lib/test/test_unittest/testmock"
   ---
   Because the relative path starts with '..', the assertion fails.

Assisted-by: Gemini
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

z-ca-2026 Tag to track Contribution Academy 2026

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants