Compare commits

...
Author SHA1 Message Date
Hanif Koh 263a745fab Run the Flow Rate Fine Pass on the Extruder the Coarse Pass Used
When the fine pass starts from the coarse page the filament comes from
the cached preset, but the extruder was resolved from whatever the
preset page currently has selected. After a restart or reconnect, or if
the selection was changed, that no longer matches and the lookup falls
back to the per-AMS map, which with a Filament Track Switch pins every
AMS to the main extruder. Take the extruder from the cache in that path;
it already records the one the coarse pass was sent with.
2026-10-19 16:07:07 +08:00
Hanif Koh e1b8d8cdea Show Track-Switch AMS Units Under Both Nozzles in the Calibration Wizard
With a Filament Track Switch installed the device layer pins every AMS to
the main extruder and records the real binding in GetBindedExtruderSet(),
but the calibration preset page still split the AMS units by
GetExtruderId(). Every AMS landed under the right nozzle and the left
nozzle only offered its external spool.

Split the panels by the bound extruder set, blank the external spools when
the switch is installed, and take a selected filament's extruder from the
panel it was picked in rather than from its AMS.
2026-10-19 16:07:07 +08:00
HanifKoh ac022b56ad Wait for the Close Frame in the Moonraker Status Stream Test (#16635)
The fake printer counts the agent's close frame in its read handler, but
Beast answers the close before that read completes, so stop() can return
and the test can read the counter before it is incremented. On a loaded
runner the check saw 0.

Wait for the counter like the other fake-printer counters do, then keep
the exact-count check.
2026-10-19 16:06:41 +08:00
HanifKoh d3699dd9f6 Keep Python Bytecode out of the Bundled Python Home (#16550)
* Keep Python Bytecode out of the Bundled Python Home

The bundled stdlib ships .pyc files that are stale against their sources:
they are compiled when the Python deps are installed, and the copy into the
app does not keep the sources' mtimes. CPython then recompiles every module
it imports and writes the result next to the source. A drag-installed macOS
app rewrites files inside its own bundle on first launch, so its signature
no longer verifies, and read-only installs (AppImage, Flatpak, Program Files)
recompile the same modules on every launch.

Point pycache_prefix at <data_dir>/python/pycache so the interpreter reads
and writes bytecode there. The bundle is never written, the cache survives
between launches, and the data directory is already a plugin read/write root.

* Drop Cached Bytecode Through the Interpreter in the Module Release Test
2026-10-19 13:48:53 +08:00
HanifKoh a315a96c5c Show "No Available Printers" Instead of an Empty Printer Dropdown (#16611)
* Show "No Available Printers" Instead of an Empty Printer Dropdown

A ComboBox with no items opened a one-row-high popup that DropDown never
paints, leaving a blank strip with stale pixels under the box. Don't open
the list when it is empty, and label the printer box in the print and
send-to-printer dialogs when no printer is available.

* Add "No Available Printers" to the Translation Template
2026-10-19 13:30:36 +08:00
HanifKoh 24cf1c6bcc Fix Crash Selecting a User Printer Whose Parent Preset Is Not Loaded (#16579)
A user preset can inherit a system preset from a vendor that is not
installed. get_preset_base() returns null for it, and
get_selected_preset_base() dereferenced that unconditionally, so picking
the printer crashed while building the shared profiles notification URL.
Fall back to the selected preset itself. The create printer and create
filament dialogs had the same null dereference on such presets.
2026-10-19 13:29:26 +08:00
13 changed files with 143 additions and 40 deletions

No files matched your search

+3
View File
@@ -10657,6 +10657,9 @@ msgstr ""
msgid "Send Failed"
msgstr ""
msgid "No available printers"
msgstr ""
msgid "The name length exceeds the limit."
msgstr ""
+8 -1
View File
@@ -756,7 +756,14 @@ public:
Preset& get_edited_preset() { return m_edited_preset; }
const Preset& get_edited_preset() const { return m_edited_preset; }
const Preset& get_selected_preset_base() const { return *get_preset_base(m_presets[m_idx_selected]); }
// The root of the selected preset's inherits chain, or the selected preset itself when
// the chain names a preset that is not loaded (e.g. a vendor that is not installed).
const Preset& get_selected_preset_base() const
{
const Preset &selected = m_presets[m_idx_selected];
const Preset *base = get_preset_base(selected);
return base ? *base : selected;
}
// Return the last saved preset.
// const Preset& get_saved_preset() const { return m_saved_preset; }
+15 -7
View File
@@ -435,7 +435,7 @@ void CalibrationWizard::cache_preset_info(MachineObject *obj, float nozzle_dia,
if (obj->is_multi_extruders()) {
int ams_id, slot_id, tray_id;
get_tray_ams_and_slot_id(curr_obj, result.tray_id, ams_id, slot_id, tray_id);
result.extruder_id = preset_page->get_extruder_id(ams_id);
result.extruder_id = preset_page->get_extruder_id_by_tray_id(result.tray_id);
result.nozzle_diameter = preset_page->get_nozzle_diameter(result.extruder_id);
}
else {
@@ -753,7 +753,7 @@ void PressureAdvanceWizard::on_cali_start()
X1CCalibInfos::X1CCalibInfo calib_info;
get_tray_ams_and_slot_id(curr_obj, item.first, calib_info.ams_id, calib_info.slot_id, calib_info.tray_id);
calib_info.extruder_id = preset_page->get_extruder_id(calib_info.ams_id);
calib_info.extruder_id = preset_page->get_extruder_id_by_tray_id(item.first);
calib_info.extruder_type = preset_page->get_extruder_type(calib_info.extruder_id);
calib_info.nozzle_volume_type = preset_page->get_nozzle_volume_type(calib_info.extruder_id);
calib_info.nozzle_diameter = preset_page->get_nozzle_diameter(calib_info.extruder_id);
@@ -792,7 +792,7 @@ void PressureAdvanceWizard::on_cali_start()
CalibInfo calib_info;
calib_info.dev_id = curr_obj->get_dev_id();
get_tray_ams_and_slot_id(curr_obj, selected_filaments.begin()->first, calib_info.ams_id, calib_info.slot_id, selected_tray_id);
calib_info.extruder_id = preset_page->get_extruder_id(calib_info.ams_id);
calib_info.extruder_id = preset_page->get_extruder_id_by_tray_id(selected_filaments.begin()->first);
calib_info.extruder_type = preset_page->get_extruder_type(calib_info.extruder_id);
calib_info.nozzle_volume_type = preset_page->get_nozzle_volume_type(calib_info.extruder_id);
calib_info.nozzle_diameter = preset_page->get_nozzle_diameter(calib_info.extruder_id);
@@ -877,7 +877,7 @@ void PressureAdvanceWizard::on_cali_start()
calib_info.dev_id = curr_obj->get_dev_id();
get_tray_ams_and_slot_id(curr_obj, item.first, calib_info.ams_id, calib_info.slot_id, selected_tray_id);
calib_info.index = preset_page->get_index_by_tray_id(item.first);
calib_info.extruder_id = preset_page->get_extruder_id(calib_info.ams_id);
calib_info.extruder_id = preset_page->get_extruder_id_by_tray_id(item.first);
calib_info.nozzle_diameter = preset_page->get_nozzle_diameter(calib_info.extruder_id);
calib_info.extruder_type = preset_page->get_extruder_type(calib_info.extruder_id);
calib_info.nozzle_volume_type = preset_page->get_nozzle_volume_type(calib_info.extruder_id);
@@ -1252,7 +1252,7 @@ void FlowRateWizard::on_cali_start(CaliPresetStage stage, float cali_value, Flow
X1CCalibInfos::X1CCalibInfo calib_info;
calib_info.tray_id = item.first;
get_tray_ams_and_slot_id(curr_obj, item.first, calib_info.ams_id, calib_info.slot_id, calib_info.tray_id);
calib_info.extruder_id = preset_page->get_extruder_id(calib_info.ams_id);
calib_info.extruder_id = preset_page->get_extruder_id_by_tray_id(item.first);
calib_info.extruder_type = preset_page->get_extruder_type(calib_info.extruder_id);
calib_info.nozzle_volume_type = preset_page->get_nozzle_volume_type(calib_info.extruder_id);
calib_info.nozzle_diameter = preset_page->get_nozzle_diameter(calib_info.extruder_id);
@@ -1308,7 +1308,15 @@ void FlowRateWizard::on_cali_start(CaliPresetStage stage, float cali_value, Flow
int selected_tray_id = 0;
get_tray_ams_and_slot_id(curr_obj, selected_filaments.begin()->first, calib_info.ams_id, calib_info.slot_id, selected_tray_id);
calib_info.select_ams = std::to_string(selected_tray_id);
calib_info.extruder_id = preset_page->get_extruder_id(calib_info.ams_id);
calib_info.extruder_id = preset_page->get_extruder_id_by_tray_id(selected_filaments.begin()->first);
if (from_page == FlowRatioCaliSource::FROM_COARSE_PAGE) {
// The fine pass must run on the extruder the coarse pass used. The cache records it;
// the preset page's selection may have changed or been reset since.
const auto cached = get_cached_selected_filament_for_multi_extruder(curr_obj);
const auto it = cached.find(selected_filaments.begin()->first);
if (it != cached.end())
calib_info.extruder_id = it->second.extruder_id;
}
calib_info.extruder_type = preset_page->get_extruder_type(calib_info.extruder_id);
calib_info.nozzle_volume_type = preset_page->get_nozzle_volume_type(calib_info.extruder_id);
calib_info.nozzle_diameter = preset_page->get_nozzle_diameter(calib_info.extruder_id);
@@ -1724,7 +1732,7 @@ void MaxVolumetricSpeedWizard::on_cali_start()
int selected_tray_id = 0;
get_tray_ams_and_slot_id(curr_obj, selected_filaments.begin()->first, calib_info.ams_id, calib_info.slot_id, selected_tray_id);
calib_info.select_ams = std::to_string(selected_tray_id);
calib_info.extruder_id = preset_page->get_extruder_id(calib_info.ams_id);
calib_info.extruder_id = preset_page->get_extruder_id_by_tray_id(selected_filaments.begin()->first);
calib_info.extruder_type = preset_page->get_extruder_type(calib_info.extruder_id);
calib_info.nozzle_volume_type = preset_page->get_nozzle_volume_type(calib_info.extruder_id);
calib_info.nozzle_diameter = preset_page->get_nozzle_diameter(calib_info.extruder_id);
+53 -22
View File
@@ -587,8 +587,23 @@ void CalibrationPresetPage::on_sys_color_changed()
m_ams_sync_button->msw_rescale();
}
int CalibrationPresetPage::get_extruder_id(int ams_id)
int CalibrationPresetPage::get_extruder_id_by_tray_id(int tray_id)
{
// An AMS behind a Filament Track Switch feeds both extruders, so the extruder is the one
// whose panel the filament was selected in.
if (curr_obj && curr_obj->is_multi_extruders()) {
for (FilamentComboBox *fcb : get_selected_filament_combobox()) {
if (fcb->get_tray_id() != tray_id)
continue;
const bool is_deputy = std::find(m_deputy_filament_comboBox_list.begin(), m_deputy_filament_comboBox_list.end(), fcb) !=
m_deputy_filament_comboBox_list.end();
return is_deputy ? DEPUTY_EXTRUDER_ID : MAIN_EXTRUDER_ID;
}
}
int ams_id = tray_id / 4, slot_id, unused_tray_id;
if (curr_obj)
get_tray_ams_and_slot_id(curr_obj, tray_id, ams_id, slot_id, unused_tray_id);
if (m_ams_id_to_extruder_id_map.find(ams_id) != m_ams_id_to_extruder_id_map.end()) {
return m_ams_id_to_extruder_id_map[ams_id];
}
@@ -866,6 +881,18 @@ static void fill_nozzle_volume_combo(ComboBox *combo)
select_nozzle_volume(combo, NozzleVolumeType::nvtStandard);
}
static DynamicPrintConfig empty_tray_config()
{
DynamicPrintConfig config;
config.set_key_value("filament_id", new ConfigOptionStrings{""});
config.set_key_value("tag_uid", new ConfigOptionStrings{""});
config.set_key_value("filament_type", new ConfigOptionStrings{""});
config.set_key_value("tray_name", new ConfigOptionStrings{""});
config.set_key_value("filament_colour", new ConfigOptionStrings{""});
config.set_key_value("filament_exist", new ConfigOptionBools{false});
return config;
}
void CalibrationPresetPage::init_selection_values()
{
// init nozzle diameter and nozzle volume
@@ -2272,27 +2299,36 @@ void CalibrationPresetPage::sync_ams_info(MachineObject* obj)
}
}
// init virtual tray info
if (full_filament_ams_list.find(VIRTUAL_TRAY_MAIN_ID) != full_filament_ams_list.end()) {
filament_ams_list[VIRTUAL_TRAY_MAIN_ID] = full_filament_ams_list[VIRTUAL_TRAY_MAIN_ID];
}
// init virtual tray info. With a Filament Track Switch installed the external spools feed
// through the switch, so they can't be calibrated as their own trays.
if (obj->GetFilaSwitch()->IsInstalled()) {
filament_ams_list[VIRTUAL_TRAY_MAIN_ID] = empty_tray_config();
filament_ams_list[VIRTUAL_TRAY_DEPUTY_ID] = empty_tray_config();
} else {
if (full_filament_ams_list.find(VIRTUAL_TRAY_MAIN_ID) != full_filament_ams_list.end()) {
filament_ams_list[VIRTUAL_TRAY_MAIN_ID] = full_filament_ams_list[VIRTUAL_TRAY_MAIN_ID];
}
if (full_filament_ams_list.find(VIRTUAL_TRAY_DEPUTY_ID) != full_filament_ams_list.end()) {
filament_ams_list[VIRTUAL_TRAY_DEPUTY_ID] = full_filament_ams_list[VIRTUAL_TRAY_DEPUTY_ID];
if (full_filament_ams_list.find(VIRTUAL_TRAY_DEPUTY_ID) != full_filament_ams_list.end()) {
filament_ams_list[VIRTUAL_TRAY_DEPUTY_ID] = full_filament_ams_list[VIRTUAL_TRAY_DEPUTY_ID];
}
}
// update filament from panel, display only obj has ams
// update multi ams panel, display only obj has multi ams
if (obj->HasAms()) {
if (obj->is_multi_extruders()) {
// An AMS behind a Filament Track Switch is bound to both extruders and appears in both panels.
bool main_done = false;
bool deputy_done = false;
for (auto &ams_item : obj->GetFilaSystem()->GetAmsList()) {
if (ams_item.second->GetExtruderId() == 0 && !main_done) {
update_multi_extruder_filament_combobox(ams_item.second->GetAmsId(), ams_item.second->GetExtruderId());
const auto &extruder_set = ams_item.second->GetBindedExtruderSet();
if (!main_done && extruder_set.count(MAIN_EXTRUDER_ID) != 0) {
update_multi_extruder_filament_combobox(ams_item.second->GetAmsId(), MAIN_EXTRUDER_ID);
main_done = true;
} else if (ams_item.second->GetExtruderId() == 1 && !deputy_done) {
update_multi_extruder_filament_combobox(ams_item.second->GetAmsId(), ams_item.second->GetExtruderId());
}
if (!deputy_done && extruder_set.count(DEPUTY_EXTRUDER_ID) != 0) {
update_multi_extruder_filament_combobox(ams_item.second->GetAmsId(), DEPUTY_EXTRUDER_ID);
deputy_done = true;
}
}
@@ -2333,10 +2369,11 @@ void CalibrationPresetPage::sync_ams_info(MachineObject* obj)
if (ams->second->IsExist()
&& info.parse_ams_info(obj, ams->second, obj->GetFilaSystem()->IsDetectRemainEnabled(), obj->is_support_ams_humidity)) {
ams_info.push_back(info);
if (info.nozzle_id == 0) {
main_ams_info.push_back(info);
} else {
deputy_ams_info.push_back(info);
for (int extruder_id : ams->second->GetBindedExtruderSet()) {
if (extruder_id == MAIN_EXTRUDER_ID)
main_ams_info.push_back(info);
else
deputy_ams_info.push_back(info);
}
}
m_ams_id_to_extruder_id_map[stoi(info.ams_id)] = info.nozzle_id;
@@ -2584,13 +2621,7 @@ void CalibrationPresetPage::update_multi_extruder_filament_combobox(const std::s
}
}
DynamicPrintConfig empty_config;
empty_config.set_key_value("filament_id", new ConfigOptionStrings{""});
empty_config.set_key_value("tag_uid", new ConfigOptionStrings{""});
empty_config.set_key_value("filament_type", new ConfigOptionStrings{""});
empty_config.set_key_value("tray_name", new ConfigOptionStrings{""});
empty_config.set_key_value("filament_colour", new ConfigOptionStrings{""});
empty_config.set_key_value("filament_exist", new ConfigOptionBools{false});
DynamicPrintConfig empty_config = empty_tray_config();
if (filament_ams_list.empty()) return;
@@ -251,7 +251,7 @@ public:
void msw_rescale() override;
void on_sys_color_changed() override;
int get_extruder_id(int ams_id);
int get_extruder_id_by_tray_id(int tray_id);
float get_nozzle_diameter(int extruder_id) const;
NozzleVolumeType get_nozzle_volume_type(int extruder_id) const;
ExtruderType get_extruder_type(int extruder_id) const;
+2 -2
View File
@@ -3151,7 +3151,7 @@ void CreatePrinterPresetDialog::set_current_visible_printer()
m_printer_name_to_preset.clear();
for (const Preset &printer_preset : printer_presets) {
if (!printer_preset.is_visible) continue;
if (preset_bundle->printers.get_preset_base(printer_preset)->name != printer_preset.name) continue;
if (preset_bundle->printers.get_preset_base(printer_preset) != &printer_preset) continue;
if (auto printer_model = dynamic_cast<ConfigOptionString*>(const_cast<Preset&>(printer_preset).config.option("printer_model", false))) {
if (m_printer_name_to_preset.find(printer_model->value) == m_printer_name_to_preset.end()) {
printer_choice.push_back(from_u8(printer_model->value));
@@ -4962,7 +4962,7 @@ void CreatePresetForPrinterDialog::get_visible_printer_and_compatible_filament_p
if (filament_preset.is_default || !filament_preset.is_compatible || filament_preset.is_project_embedded) continue;
ConfigOptionStrings *filament_types;
const Preset * filament_preset_base = m_preset_bundle->filaments.get_preset_base(filament_preset);
if (filament_preset_base == &filament_preset) {
if (filament_preset_base == &filament_preset || filament_preset_base == nullptr) {
filament_types = dynamic_cast<ConfigOptionStrings *>(const_cast<Preset *>(&filament_preset)->config.option("filament_type"));
} else {
filament_types = dynamic_cast<ConfigOptionStrings *>(const_cast<Preset *>(filament_preset_base)->config.option("filament_type"));
+1 -1
View File
@@ -4087,7 +4087,7 @@ void SelectMachineDialog::update_user_printer()
{
m_printer_last_select = "";
update_select_layout(nullptr);
m_printer_box->GetPrinterComboBox()->SetTextLabel("");
m_printer_box->GetPrinterComboBox()->SetValue(_L("No available printers"));
}
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "for send task, current printer id = " << m_printer_last_select << std::endl;
+1 -1
View File
@@ -1229,7 +1229,7 @@ void SendToPrinterDialog::update_user_printer()
}
else {
m_printer_last_select = "";
m_comboBox_printer->SetTextLabel("");
m_comboBox_printer->SetValue(_L("No available printers"));
}
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "for send task, current printer id = " << m_printer_last_select << std::endl;
+3 -3
View File
@@ -287,7 +287,7 @@ void ComboBox::DoDeleteOneItem(unsigned int pos)
void ComboBox::ForceDropdownOpen()
{
if (!IsEnabled())
if (!IsEnabled() || items.empty())
return;
if (!drop_down) {
@@ -398,7 +398,7 @@ void ComboBox::mouseDown(wxMouseEvent &event)
SetFocus();
if (drop_down) {
drop.Hide();
} else if (drop.HasDismissLongTime()) {
} else if (!items.empty() && drop.HasDismissLongTime()) { // DropDown paints nothing for an empty list
drop.autoPosition();
drop_down = true;
drop.Popup(&drop);
@@ -426,7 +426,7 @@ void ComboBox::keyDown(wxKeyEvent& event)
case WXK_SPACE:
if (drop_down) {
drop.DismissAndNotify();
} else if (drop.HasDismissLongTime()) {
} else if (!items.empty() && drop.HasDismissLongTime()) {
drop.autoPosition();
drop_down = true;
drop.Popup();
+13
View File
@@ -584,6 +584,19 @@ bool PythonInterpreter::initialize()
}
BOOST_LOG_TRIVIAL(debug) << "Program name set successfully";
// Keep bytecode out of the Python home. The shipped .pyc files are stale against their
// sources, so a writable install would rewrite them inside the app (breaking a signed
// macOS bundle's seal) and a read-only one would recompile them on every launch.
const std::string pycache_prefix = data_dir() + "/python/pycache";
#ifdef _WIN32
status = PyConfig_SetString(&config, &config.pycache_prefix, boost::nowide::widen(pycache_prefix).c_str());
#else
status = PyConfig_SetBytesString(&config, &config.pycache_prefix, pycache_prefix.c_str());
#endif
if (PyStatus_Exception(status))
BOOST_LOG_TRIVIAL(warning) << "Failed to set the Python bytecode cache to " << pycache_prefix << ": "
<< (status.err_msg ? status.err_msg : "unknown error");
// Use pybind11's scoped_interpreter which properly initializes
// pybind11 internals for multi-threaded use — raw Py_InitializeFromConfig
// does not set up thread state tracking that pybind11 requires.
@@ -1219,6 +1219,24 @@ TEST_CASE("find_preset2 auto-matches removed Generic vendor profiles to the libr
CHECK(bundle.filaments.find_preset2("BrandX Generic PETG", /*auto_match=*/true) == nullptr);
}
TEST_CASE("The selected preset is its own base when its parent is not loaded", "[Preset]")
{
PresetCollection printers(Preset::TYPE_PRINTER, Preset::printer_options(),
static_cast<const PrintRegionConfig &>(FullPrintConfig::defaults()));
add_inmemory_preset(printers, "Printer");
add_inmemory_preset(printers, "User Printer", "Printer");
add_inmemory_preset(printers, "Orphan Printer", "Uninstalled Vendor Printer");
printers.select_preset_by_name("User Printer", true);
REQUIRE(printers.get_selected_preset_name() == "User Printer");
CHECK(printers.get_selected_preset_base().name == "Printer");
printers.select_preset_by_name("Orphan Printer", true);
REQUIRE(printers.get_selected_preset_name() == "Orphan Printer");
REQUIRE(printers.get_preset_base(printers.get_selected_preset()) == nullptr);
CHECK(printers.get_selected_preset_base().name == "Orphan Printer");
}
TEST_CASE("Renamed parent is normalized into a loaded preset's inherits", "[Preset][Rename]")
{
ScopedTemporaryDir temp_dir;
@@ -150,6 +150,13 @@ public:
return m_close_frames;
}
// The printer answers a close frame before its own read completes, so the agent's stop can return first.
bool wait_for_close_frames(int count)
{
std::unique_lock<std::mutex> lock(m_mutex);
return m_changed.wait_for(lock, 10s, [&] { return m_close_frames >= count; });
}
// Hangs up every connection that is waiting.
void release()
{
@@ -277,6 +284,7 @@ private:
if (ec == websocket::error::closed) {
std::lock_guard<std::mutex> lock(m_mutex);
++m_close_frames;
m_changed.notify_all();
}
if (ec)
reading = false;
@@ -449,6 +457,7 @@ TEST_CASE("Stopping the status stream sends the printer a close frame", "[Moonra
REQUIRE(f.printer.wait_for_ready_connections(1));
CHECK(f.stops_within(3s));
CHECK(f.printer.wait_for_close_frames(1));
CHECK(f.printer.close_frames() == 1);
}
+16 -2
View File
@@ -615,8 +615,22 @@ TEST_CASE("Python module release removes package submodules and owned sys.path",
// Ensure the next import executes the new submodule rather than reusing a stale package child.
write_package("'new'");
boost::system::error_code ec;
fs::remove_all(package_root / "__pycache__", ec);
{
// The rewrite can keep the old mtime and size, so drop the bytecode wherever the
// interpreter caches it.
PythonGILState gil;
REQUIRE(gil);
PyObjectPtr util(PyImport_ImportModule("importlib.util"));
REQUIRE(util);
for (const char* file : {"__init__.py", "sub.py"}) {
PyObjectPtr source(PyUnicode_DecodeFSDefault((package_root / file).string().c_str()));
REQUIRE(source);
PyObjectPtr cached(PyObject_CallMethod(util.get(), "cache_from_source", "O", source.get()));
REQUIRE(cached);
boost::system::error_code ec;
fs::remove(fs::path(PyUnicode_AsUTF8(cached.get())), ec);
}
}
paths.clear();
modules.clear();