From 4ae789c82ac4d12bdd2d083af390d32646c82530 Mon Sep 17 00:00:00 2001 From: SinghCod3r Date: Sun, 4 Oct 2026 12:43:26 +0530 Subject: [PATCH 1/4] Improve duplicate printer workflow --- newprinter.py | 213 ++++++++++++++++++++++++++++++++++----- system-config-printer.py | 92 +++++++++++++---- test_newprinter.py | 179 +++++++++++++++++++++++++++++++- 3 files changed, 436 insertions(+), 48 deletions(-) diff --git a/newprinter.py b/newprinter.py index 291c893f7..9ba00789d 100644 --- a/newprinter.py +++ b/newprinter.py @@ -725,18 +725,25 @@ def destroy (self): self.emit ('destroy') def init(self, dialog_mode, device_uri=None, name=None, ppd=None, - devid="", host=None, encryption=None, parent=None, xid=0): + devid="", host=None, encryption=None, parent=None, xid=0, + description="", location="", is_shared=False, + discovered_device=None): self.parent = parent if not self.parent: self.NewPrinterWindow.set_focus_on_map (False) self.dialog_mode = dialog_mode + self.duplicate_driver_selection = False self._device_uri = device_uri self.orig_ppd = ppd self.devid = devid + self.discovered_device = discovered_device self._host = host self._encryption = encryption self._name = name + self._description = description + self._location = location + self._is_shared = is_shared if not host: self._host = cups.getServer () if not encryption: @@ -827,6 +834,8 @@ def init(self, dialog_mode, device_uri=None, name=None, ppd=None, elif self.dialog_mode == "download_driver": self._initialiseDownloadDriverMode () return True + elif self.dialog_mode == "duplicate": + self._initialiseDuplicateMode () if xid == 0 and self.parent: self.NewPrinterWindow.set_transient_for (parent) @@ -836,7 +845,7 @@ def init(self, dialog_mode, device_uri=None, name=None, ppd=None, return True def _validInitParameters (self): - if self.dialog_mode in ['printer_with_uri', 'device', 'ppd']: + if self.dialog_mode in ['printer_with_uri', 'device', 'ppd', 'duplicate']: return self._device_uri is not None elif self.dialog_mode == 'download_driver': return self.devid != "" @@ -866,6 +875,25 @@ def _initialiseDeviceMode (self): self.ntbkNewPrinter.set_current_page (self.PAGE_SELECT_DEVICE) self.fillDeviceTab (self._device_uri) + def _initialiseDuplicateMode (self): + self._initialiseWidgetsForMode ("duplicate") + if getattr(self, 'discovered_device', None) is not None: + self.device = self.discovered_device + if "driverless" in self.device.info: + self.device.driverless = True + else: + self._initialiseDeviceFromURI () + if self.orig_ppd: + self.ppd = self.orig_ppd + self.entNPDriver.set_text ( + _get_driver_name_from_ppd (self.ppd, self.ppds)) + self.entNPDriver.get_style_context().add_class ("readonly") + + self.NewPrinterWindow.set_title (_("Duplicate Printer")) + self.rbtnChangePPDKeepSettings.set_active (True) + self._initialiseAutoVariables () + self.ntbkNewPrinter.set_current_page (self.PAGE_DESCRIBE_PRINTER) + def _initialisePrinterWithURIMode (self): self._initialiseWidgetsForMode ("printer") self._initialiseDeviceFromURI () @@ -914,17 +942,28 @@ def _initialisePPDMode (self): self._initialiseAutoVariables () def _initialiseWidgetsForMode (self, mode_name): - self.entNPName.set_text (self.makeNameUnique (mode_name)) + if mode_name == "duplicate": + self.entNPName.set_text (self.makeNameUnique (self._name + "_copy")) + self.isSharedCbx.set_active (self._is_shared) + self.entNPLocation.set_text (self._location) + self.entNPDescription.set_text (self._description) + self.entNPDriver.set_text ('') + self.entSMBURI.set_text ('') + self.entSMBUsername.set_text ('') + self.entSMBPassword.set_text ('') + else: + self.entNPName.set_text (self.makeNameUnique (mode_name)) + self.isSharedCbx.set_active (self.isShared) + for widget in [self.entNPLocation, + self.entNPDescription, self.entNPDriver, + self.entSMBURI, self.entSMBUsername, + self.entSMBPassword]: + widget.set_text ('') self.entNPName.grab_focus () - self.isSharedCbx.set_active(self.isShared) - for widget in [self.entNPLocation, - self.entNPDescription, self.entNPDriver, - self.entSMBURI, self.entSMBUsername, - self.entSMBPassword]: - widget.set_text ('') - - def _initialiseDeviceFromURI (self): - device_dict = { } + + def _initialiseDeviceFromURI (self, device_dict=None): + if device_dict is None: + device_dict = { } self.device = cupshelpers.Device (self._device_uri, **device_dict) def _initialiseAutoVariables (self): @@ -962,11 +1001,12 @@ def on_ppdsloader_finished_next (self, ppdsloader): This method is called when the PPDs loader has finished loading PPDs in preparation for the next screen the user will see, having clicked 'Forward'. We are creating a new queue, - and dialog_mode is either "printer" or "printer_with_uri". + and dialog_mode is one of the printer creation modes. """ self._getPPDs_reply (ppdsloader) if not self.ppds: + self._set_driver_ppd_loading(False) self.setNPButtons() return @@ -981,6 +1021,7 @@ def on_ppdsloader_finished_next (self, ppdsloader): self.nextNPTab (step = 0) else: self.nextNPTab () + self._set_driver_ppd_loading(False) # get PPDs @@ -1061,6 +1102,12 @@ def on_tvNCNotMembers_cursor_changed(self, widget): # Navigation buttons def on_NPCancel(self, widget, event=None): + if getattr(self, '_driver_ppd_loading', False): + if self.ppdsloader: + self.ppdsloader.destroy () + self.ppdsloader = None + self._set_driver_ppd_loading(False) + if self.fetchDevices_conn: self.fetchDevices_conn.destroy () self.fetchDevices_conn = None @@ -1092,19 +1139,86 @@ def on_NPCancel(self, widget, event=None): return True def on_btnNPBack_clicked(self, widget): + if getattr(self, '_driver_ppd_loading', False): + if self.ppdsloader: + self.ppdsloader.destroy () + self.ppdsloader = None + self._set_driver_ppd_loading(False) self.nextNPTab(-1) def on_btnNPForward_clicked(self, widget): self.nextNPTab() def on_btnNPChooseDriver_clicked(self, widget): - self.device.driverless = False + self.duplicate_driver_selection = self.dialog_mode == "duplicate" + if not (self.duplicate_driver_selection and + getattr(self.device, 'driverless', False)): + self.device.driverless = False self.exactdrivermatch = False self.searchedfordriverpackages = True + self._set_driver_ppd_loading(True) self.rbtnNPFoomatic.set_active(True) self.on_rbtnNPFoomatic_toggled(self.rbtnNPFoomatic) self.ntbkNewPrinter.set_current_page(self.PAGE_SELECT_INSTALL_METHOD) self.nextNPTab(step=0) + if self.ppds is not None and self.ppdsloader is None: + self._set_driver_ppd_loading(False) + + def _set_driver_ppd_loading(self, loading): + if loading: + if getattr(self, '_driver_ppd_loading', False): + return + if not self._ensure_driver_selection_spinner(): + return + self._driver_ppd_loading = True + self._driver_selection_stack.set_visible_child_name("loading") + self._driver_selection_spinner.start () + self.ntbkPPDSource.set_sensitive(False) + elif getattr(self, '_driver_ppd_loading', False): + self._driver_ppd_loading = False + self._driver_selection_spinner.stop () + self._driver_selection_stack.set_visible_child_name("content") + self.ntbkPPDSource.set_sensitive(True) + + def _ensure_driver_selection_spinner(self): + if getattr(self, '_driver_selection_stack', None) is not None: + return True + + makes_view = getattr(self, 'tvNPMakes', None) + if makes_view is None: + return False + makes_scroll = makes_view.get_parent() + makes_box = makes_scroll.get_parent() if makes_scroll else None + if not isinstance(makes_scroll, Gtk.Widget) or \ + not isinstance(makes_box, Gtk.Widget): + return False + + stack = Gtk.Stack() + stack.set_transition_type(Gtk.StackTransitionType.CROSSFADE) + stack.set_homogeneous(True) + makes_box.remove(makes_scroll) + stack.add_named(makes_scroll, "content") + + loading_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12) + loading_box.set_halign(Gtk.Align.CENTER) + loading_box.set_valign(Gtk.Align.CENTER) + loading_box.set_vexpand(True) + loading_box.set_hexpand(True) + + spinner = VectorSpinner(size=48) + spinner.set_halign(Gtk.Align.CENTER) + label = Gtk.Label(label=_("Loading printer drivers...")) + label.set_halign(Gtk.Align.CENTER) + loading_box.pack_start(spinner, False, False, 0) + loading_box.pack_start(label, False, False, 0) + stack.add_named(loading_box, "loading") + + makes_box.pack_start(stack, True, True, 0) + stack.show_all() + stack.set_visible_child_name("content") + self._driver_selection_stack = stack + self._driver_selection_spinner = spinner + return True def installdriverpackage (self, driver): install_info = self._getDriverInstallationInfo (driver) @@ -1331,8 +1445,16 @@ def nextNPTab(self, step=1): debugprint ("Next clicked on page %d" % page_nr) keep_going = True - if self.dialog_mode == "printer" or self.dialog_mode == "printer_with_uri" or \ - self.dialog_mode == "ppd" or self.dialog_mode == "download_driver": + duplicate_uses_original_ppd = ( + self.dialog_mode == "duplicate" and + not getattr(self, 'duplicate_driver_selection', False) and + getattr(self, 'orig_ppd', None) is not None) + if (self.dialog_mode == "printer" or + self.dialog_mode == "printer_with_uri" or + self.dialog_mode == "ppd" or + self.dialog_mode == "download_driver" or + (self.dialog_mode == "duplicate" and + not duplicate_uses_original_ppd)): install_result = self._handlePrinterInstallationMode (step) if install_result == self.INSTALL_RESULT_OPS_PENDING: # Do not continue if the installation process says so @@ -1358,7 +1480,9 @@ def nextNPTab(self, step=1): debugprint ("Will fetch ppd? %d" % fetch_ppd) if fetch_ppd: - self.ppd = self.getNPPPD() + if not (self.dialog_mode == "duplicate" and + not self.duplicate_driver_selection): + self.ppd = self.getNPPPD() self.installable_options = False if self.ppd is None: if getattr(self.device, 'driverless', False): @@ -1387,7 +1511,11 @@ def nextNPTab(self, step=1): if not self.installable_options: if next_page_nr == self.PAGE_INSTALLABLE_OPTIONS: # step over if empty - next_page_nr = order[order.index(next_page_nr)+1] + if (self.dialog_mode == "duplicate" and + not getattr(self, 'duplicate_driver_selection', False)): + next_page_nr = self.PAGE_DESCRIBE_PRINTER + else: + next_page_nr = order[order.index(next_page_nr)+1] # Step over empty Installable Options tab when moving backwards. if next_page_nr == self.PAGE_INSTALLABLE_OPTIONS and \ @@ -1428,11 +1556,12 @@ def nextNPTab(self, step=1): else: name = 'printer' - name = self.makeNameUnique (name) - self.entNPName.set_text (name) + if self.dialog_mode != "duplicate": + name = self.makeNameUnique (name) + self.entNPName.set_text (name) - if descr: - self.entNPDescription.set_text (descr) + if descr: + self.entNPDescription.set_text (descr) # Set the read-only driver field. driver_name = _get_driver_name_from_ppd(self.ppd, self.ppds) @@ -1446,7 +1575,20 @@ def nextNPTab(self, step=1): def _getPagesOrderForDialogMode (self): order = [] - if self.dialog_mode == "class": + if self.dialog_mode == "duplicate": + if getattr(self, 'duplicate_driver_selection', False): + order = [ + self.PAGE_SELECT_INSTALL_METHOD, + self.PAGE_CHOOSE_DRIVER_FROM_DB, + self.PAGE_INSTALLABLE_OPTIONS, + self.PAGE_DESCRIBE_PRINTER + ] + else: + order = [ + self.PAGE_DESCRIBE_PRINTER, + self.PAGE_INSTALLABLE_OPTIONS + ] + elif self.dialog_mode == "class": order = [ self.PAGE_DESCRIBE_PRINTER, self.PAGE_CHOOSE_CLASS_MEMBERS, @@ -2364,7 +2506,15 @@ def setNPButtons(self): if nr == self.PAGE_DESCRIBE_PRINTER: self.btnNPBack.show() if self.dialog_mode == "printer" or \ - self.dialog_mode == "printer_with_uri": + self.dialog_mode == "printer_with_uri" or \ + (self.dialog_mode == "duplicate" and + getattr(self, 'duplicate_driver_selection', False)): + self.btnNPForward.hide() + self.btnNPApply.show() + self.btnNPApply.set_sensitive( + checkNPName(self.printers, self.entNPName.get_text())) + elif (self.dialog_mode == "duplicate" and + not getattr(self, 'installable_options', False)): self.btnNPForward.hide() self.btnNPApply.show() self.btnNPApply.set_sensitive( @@ -2411,6 +2561,12 @@ def setNPButtons(self): if self.dialog_mode == "printer_with_uri" and \ self.exactdrivermatch: self.btnNPBack.hide () + elif (self.dialog_mode == "duplicate" and + not getattr(self, 'duplicate_driver_selection', False)): + self.btnNPForward.hide () + self.btnNPApply.show () + self.btnNPApply.set_sensitive( + checkNPName(self.printers, self.entNPName.get_text())) if nr == self.PAGE_DOWNLOAD_DRIVER: accepted = self._is_driver_license_accepted() self.btnNPForward.set_sensitive(accepted) @@ -4909,7 +5065,7 @@ def on_btnNPApply_clicked(self, widget): self.printer_finder = None self.dec_spinner_task () - if self.dialog_mode in ("class", "printer", "printer_with_uri"): + if self.dialog_mode in ("class", "printer", "printer_with_uri", "duplicate"): name = self.entNPName.get_text() location = self.entNPLocation.get_text() info = self.entNPDescription.get_text() @@ -4934,7 +5090,8 @@ def on_btnNPApply_clicked(self, widget): self.show_IPP_Error(e, msg) return elif self.dialog_mode == "printer" or \ - self.dialog_mode == "printer_with_uri": + self.dialog_mode == "printer_with_uri" or \ + self.dialog_mode == "duplicate": uri = None if self.device.uri: uri = self.device.uri @@ -4980,7 +5137,7 @@ def on_btnNPApply_clicked(self, widget): fatalException (1) self.cups._end_operation() ready (self.NewPrinterWindow) - if self.dialog_mode in ("class", "printer", "printer_with_uri"): + if self.dialog_mode in ("class", "printer", "printer_with_uri", "duplicate"): self.cups._begin_operation (_("modifying printer %s") % name) try: cupshelpers.activateNewPrinter (self.cups, name) @@ -5072,7 +5229,7 @@ def on_btnNPApply_clicked(self, widget): self.nextNPTab(0); self.NewPrinterWindow.hide() - if self.dialog_mode in ["printer", "printer_with_uri", "class"]: + if self.dialog_mode in ["printer", "printer_with_uri", "duplicate", "class"]: self.emit ('printer-added', name) elif self.dialog_mode == "download_driver": self.emit ('driver-download-checked', self.installed_driver_files) diff --git a/system-config-printer.py b/system-config-printer.py index 087f11043..ed42dcea2 100755 --- a/system-config-printer.py +++ b/system-config-printer.py @@ -1665,36 +1665,90 @@ def duplicate_printer (self, new_name): parent=self.PrintersWindow) return ret + def _get_discovered_device_for_uri(self, uri): + """Recover the normal Add Printer Device identity for a queue URI.""" + try: + devices = cupshelpers.getDevices(self.cups) + except Exception: + return None + + normalized_uri = newprinter._normalize_uri(uri).rstrip('/') + for device in devices.values(): + if newprinter._normalize_uri(device.uri).rstrip('/') == normalized_uri: + return device + return None + def on_duplicate_activate(self, *UNUSED): iconview = self.dests_iconview paths = iconview.get_selected_items () model = self.dests_iconview.get_model () iter = model.get_iter (paths[0]) name = model.get_value (iter, 2) - self.entDuplicateName.set_text(name) - self.NewPrinterName.set_transient_for (self.PrintersWindow) - result = self.NewPrinterName.run() - self.NewPrinterName.hide() - if result == Gtk.ResponseType.CANCEL: + printer = self.printers.get(name) + if not printer: return - try: - self.propertiesDlg.load (name, - host=self.connect_server, - encryption=self.connect_encrypt, - parent=self.PrintersWindow) - except RuntimeError: - # Perhaps cupsGetPPD2 failed for a browsed printer - pass - except cups.IPPError as e: - (e, m) = e.args - show_IPP_Error (e, m, self.PrintersWindow) - self.populateList () + if printer.is_class: + self.entDuplicateName.set_text(name) + self.NewPrinterName.set_transient_for (self.PrintersWindow) + result = self.NewPrinterName.run() + self.NewPrinterName.hide() + + if result == Gtk.ResponseType.CANCEL: + return + + try: + self.propertiesDlg.load (name, + host=self.connect_server, + encryption=self.connect_encrypt, + parent=self.PrintersWindow) + except RuntimeError: + # Perhaps cupsGetPPD2 failed for a browsed printer + pass + except cups.IPPError as e: + (e, m) = e.args + show_IPP_Error (e, m, self.PrintersWindow) + self.populateList () + return + + self.duplicate_printer (self.entDuplicateName.get_text ()) + self.monitor.update () return - self.duplicate_printer (self.entDuplicateName.get_text ()) - self.monitor.update () + # For normal printers, use the New Printer GUI in duplicate mode + ppd_filename = None + try: + ppd_filename = self.cups.getPPD(name) + ppd_obj = cups.PPD(ppd_filename) + except (cups.IPPError, RuntimeError): + ppd_obj = None + finally: + if ppd_filename: + try: + os.unlink(ppd_filename) + except OSError: + pass + + discovered_device = self._get_discovered_device_for_uri( + printer.device_uri) + + busy (self.PrintersWindow) + self.desensitise_new_printer_widgets () + if not self.newPrinterGUI.init("duplicate", + device_uri=printer.device_uri, + name=name, + ppd=ppd_obj, + host=self.connect_server, + encryption=self.connect_encrypt, + parent=self.PrintersWindow, + description=printer.info, + location=printer.location, + is_shared=printer.is_shared, + discovered_device=discovered_device): + self.sensitise_new_printer_widgets () + self.monitor.update () + ready (self.PrintersWindow) def on_entDuplicateName_changed(self, widget): # restrict diff --git a/test_newprinter.py b/test_newprinter.py index fd1d8ce30..bbd5a35fc 100644 --- a/test_newprinter.py +++ b/test_newprinter.py @@ -880,6 +880,7 @@ def test_choose_driver_button_visible_for_non_driverless(): def test_clicking_choose_driver_button_enters_manual_workflow(): np = get_dummy_gui() + np.dialog_mode = "duplicate" np.device.driverless = True np.exactdrivermatch = True np.ppds = MagicMock() @@ -888,12 +889,27 @@ def test_clicking_choose_driver_button_enters_manual_workflow(): np.on_rbtnNPFoomatic_toggled = MagicMock() np.nextNPTab = MagicMock() np._loadPPDsForDevice = MagicMock() + np._ensure_driver_selection_spinner = MagicMock(return_value=True) + np._driver_selection_stack = MagicMock() + np._driver_selection_spinner = MagicMock() + np.ntbkPPDSource = MagicMock() newprinter.NewPrinterGUI.on_btnNPChooseDriver_clicked(np, None) - assert np.device.driverless is False + assert np.device.driverless is True + assert np.duplicate_driver_selection is True + assert np._getPagesOrderForDialogMode() == [ + np.PAGE_SELECT_INSTALL_METHOD, + np.PAGE_CHOOSE_DRIVER_FROM_DB, + np.PAGE_INSTALLABLE_OPTIONS, + np.PAGE_DESCRIBE_PRINTER, + ] assert np.exactdrivermatch is False assert np.searchedfordriverpackages is True + np._driver_selection_stack.set_visible_child_name.assert_any_call("loading") + np._driver_selection_stack.set_visible_child_name.assert_any_call("content") + np._driver_selection_spinner.start.assert_called_once() + np._driver_selection_spinner.stop.assert_called_once() np._loadPPDsForDevice.assert_not_called() np.ntbkNewPrinter.set_current_page.assert_called_with(newprinter.NewPrinterGUI.PAGE_SELECT_INSTALL_METHOD) np.rbtnNPFoomatic.set_active.assert_called_with(True) @@ -2599,3 +2615,164 @@ def test_ppdsloader_packagekit_private_bus_disables_exit_on_disconnect_and_falls idle_add_mock.assert_called_once_with(loader._on_packagekit_done, False, mock_bus) mock_bus.close.assert_called_once() loader._query_cups.assert_called_once() + +def test_duplicate_mode_page_order(): + np = get_dummy_gui() + np.dialog_mode = "duplicate" + order = np._getPagesOrderForDialogMode() + assert order == [ + np.PAGE_DESCRIBE_PRINTER, + np.PAGE_INSTALLABLE_OPTIONS, + ] + + +def test_duplicate_initialisation_preserves_metadata_and_original_ppd(monkeypatch): + monkeypatch.setattr(cups, "PPD", MockCupsPPD) + np = get_dummy_gui() + np._name = "Office Printer" + np._device_uri = "ipp://printer.example/ipp/print" + np._description = "Original description" + np._location = "Reception" + np._is_shared = True + np.orig_ppd = MockCupsPPD({"NickName": "Xerox B235 MFP"}) + np.makeNameUnique = MagicMock(return_value="Office-Printer_copy") + np.entNPName = MagicMock() + np.entNPLocation = MagicMock() + np.entNPDescription = MagicMock() + np.entNPDriver = MagicMock() + np.entSMBURI = MagicMock() + np.entSMBUsername = MagicMock() + np.entSMBPassword = MagicMock() + np.isSharedCbx = MagicMock() + np.rbtnChangePPDKeepSettings = MagicMock() + np.NewPrinterWindow = MagicMock() + np.ntbkNewPrinter = MagicMock() + + np._initialiseDuplicateMode() + + assert np.ppd is np.orig_ppd + assert np.device.id == "" + assert np.device.make_and_model == "" + np.entNPName.set_text.assert_called_with("Office-Printer_copy") + np.entNPDescription.set_text.assert_called_with("Original description") + np.entNPLocation.set_text.assert_called_with("Reception") + np.isSharedCbx.set_active.assert_called_with(True) + np.entNPDriver.set_text.assert_called_with("Xerox B235 MFP") + np.ntbkNewPrinter.set_current_page.assert_called_with( + np.PAGE_DESCRIBE_PRINTER) + + +def test_duplicate_initialisation_reuses_discovered_device_identity(monkeypatch): + monkeypatch.setattr(cups, "PPD", MockCupsPPD) + np = get_dummy_gui() + np._name = "Office Printer" + np._device_uri = "ipps://Xerox%20B235._ipps._tcp.local" + np._description = "Original description" + np._location = "Reception" + np._is_shared = True + np.orig_ppd = MockCupsPPD({"NickName": "Xerox B235 MFP"}) + np.discovered_device = cupshelpers.Device( + np._device_uri, + **{"device-id": "MFG:Xerox;MDL:Xerox(R) B235 MFP;" + "CMD:PCLM,PS,PWGRaster;", + "device-info": "Xerox printer (driverless)", + "device-make-and-model": "Xerox Xerox(R) B235 MFP"}) + np.makeNameUnique = MagicMock(return_value="Office-Printer_copy") + np.entNPName = MagicMock() + np.entNPLocation = MagicMock() + np.entNPDescription = MagicMock() + np.entNPDriver = MagicMock() + np.entSMBURI = MagicMock() + np.entSMBUsername = MagicMock() + np.entSMBPassword = MagicMock() + np.isSharedCbx = MagicMock() + np.rbtnChangePPDKeepSettings = MagicMock() + np.NewPrinterWindow = MagicMock() + np.ntbkNewPrinter = MagicMock() + + np._initialiseDuplicateMode() + + assert np.device is np.discovered_device + assert np.device.driverless is True + assert np.device.id_dict["MFG"] == "Xerox" + assert np.device.id_dict["MDL"] == "Xerox(R) B235 MFP" + assert np.device.id_dict["CMD"] == ["PCLM", "PS", "PWGRaster"] + + +def test_duplicate_initialisation_falls_back_without_fake_device_identity(monkeypatch): + monkeypatch.setattr(cups, "PPD", MockCupsPPD) + np = get_dummy_gui() + np._name = "Unavailable Printer" + np._device_uri = "ipps://unavailable.example/ipp/print" + np._description = "Original description" + np._location = "Reception" + np._is_shared = False + np.orig_ppd = MockCupsPPD({"NickName": "Original Driver"}) + np.device_id = "" + np.device_make_and_model = "" + np.makeNameUnique = MagicMock(return_value="Unavailable-Printer_copy") + np.entNPName = MagicMock() + np.entNPLocation = MagicMock() + np.entNPDescription = MagicMock() + np.entNPDriver = MagicMock() + np.entSMBURI = MagicMock() + np.entSMBUsername = MagicMock() + np.entSMBPassword = MagicMock() + np.isSharedCbx = MagicMock() + np.rbtnChangePPDKeepSettings = MagicMock() + np.NewPrinterWindow = MagicMock() + np.ntbkNewPrinter = MagicMock() + + np._initialiseDuplicateMode() + + assert np.ppd is np.orig_ppd + assert np.device.id == "" + assert np.device.make_and_model == "" + assert np.device.id_dict["MFG"] == "" + assert np.device.id_dict["MDL"] == "" +def test_duplicate_forward_uses_original_ppd_without_loading_or_matching(): + np = get_dummy_gui() + np.dialog_mode = "duplicate" + np.duplicate_driver_selection = False + np.orig_ppd = MockCupsPPD({"NickName": "Xerox B235 MFP"}) + np.ppd = np.orig_ppd + np.ntbkNewPrinter = MagicMock() + np.ntbkNewPrinter.get_current_page.return_value = np.PAGE_DESCRIBE_PRINTER + np.btnNPForward = MagicMock() + np.btnNPApply = MagicMock() + np.btnNPBack = MagicMock() + np.entNPName = MagicMock() + np.entNPDriver = MagicMock() + np.entNPName.get_text.return_value = "Xerox_copy" + np.printers = {} + np.installable_options = False + np.fillNPInstallableOptions = MagicMock( + side_effect=lambda: setattr(np, "installable_options", True)) + np._handlePrinterInstallationMode = MagicMock() + np.getNPPPD = MagicMock() + np.setNPButtons = MagicMock() + + newprinter.NewPrinterGUI.nextNPTab(np) + + np._handlePrinterInstallationMode.assert_not_called() + np.getNPPPD.assert_not_called() + np._loadPPDsForDevice.assert_not_called() + np.ntbkNewPrinter.set_current_page.assert_called_with( + np.PAGE_INSTALLABLE_OPTIONS) + assert np.ppd is np.orig_ppd + + +def test_duplicate_state_two_uses_existing_ppd_loader(): + np = get_dummy_gui() + np.dialog_mode = "duplicate" + np.duplicate_driver_selection = True + np.ppds = None + np._selectDeviceForInstallation = MagicMock() + np._installHPScannerFilesIfNeeded = MagicMock() + np._loadPPDsForDevice = MagicMock() + + result = newprinter.NewPrinterGUI._handlePrinterInstallationStage( + np, newprinter.NewPrinterGUI.PAGE_SELECT_INSTALL_METHOD, 0) + + np._loadPPDsForDevice.assert_called_once() + assert result == newprinter.NewPrinterGUI.INSTALL_RESULT_OPS_PENDING From 9f737a10c3b87dfdefacc88f3652a1782a389cca Mon Sep 17 00:00:00 2001 From: SinghCod3r Date: Sun, 4 Oct 2026 14:39:15 +0530 Subject: [PATCH 2/4] Fixed Duplicate Printer Naming Suffix --- newprinter.py | 51 +++++++++++++++++++--------------- test_newprinter.py | 69 ++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 93 insertions(+), 27 deletions(-) diff --git a/newprinter.py b/newprinter.py index 9ba00789d..7b01e2b03 100644 --- a/newprinter.py +++ b/newprinter.py @@ -889,6 +889,9 @@ def _initialiseDuplicateMode (self): _get_driver_name_from_ppd (self.ppd, self.ppds)) self.entNPDriver.get_style_context().add_class ("readonly") + base_name = self._getSuggestedPrinterName () or self._name + self.entNPName.set_text (self.makeNameUnique (base_name)) + self.NewPrinterWindow.set_title (_("Duplicate Printer")) self.rbtnChangePPDKeepSettings.set_active (True) self._initialiseAutoVariables () @@ -943,7 +946,7 @@ def _initialisePPDMode (self): def _initialiseWidgetsForMode (self, mode_name): if mode_name == "duplicate": - self.entNPName.set_text (self.makeNameUnique (self._name + "_copy")) + self.entNPName.set_text ('') self.isSharedCbx.set_active (self._is_shared) self.entNPLocation.set_text (self._location) self.entNPDescription.set_text (self._description) @@ -971,6 +974,30 @@ def _initialiseAutoVariables (self): self.auto_model = "" self.auto_driver = None + def _getSuggestedPrinterName (self): + name = None + try: + if (self.device.id and + not self.device.type in ("socket", "lpd", "ipp", + "http", "https", "bluetooth")): + name = "%s %s" % (self.device.id_dict["MFG"], + self.device.id_dict["MDL"]) + except: + nonfatalException () + + try: + if name is None and isinstance (self.ppd, cups.PPD): + mname = self.ppd.findAttr ("modelName").value + make, model = cupshelpers.ppds.ppdMakeModelSplit (mname) + if make and model: + name = "%s %s" % (make, model) + elif make or model: + name = "%s%s" % (make, model) + except: + nonfatalException () + + return name + def change_ppd_got_devs (self, conn, result): self.fetchDevices_conn._end_operation () self.fetchDevices_conn.destroy () @@ -1528,28 +1555,8 @@ def nextNPTab(self, step=1): if step >= 0 and next_page_nr == self.PAGE_DESCRIBE_PRINTER: # About to choose a name. # Suggest an appropriate name. - name = None descr = None - - try: - if (self.device.id and - not self.device.type in ("socket", "lpd", "ipp", - "http", "https", "bluetooth")): - name = "%s %s" % (self.device.id_dict["MFG"], - self.device.id_dict["MDL"]) - except: - nonfatalException () - - try: - if name is None and isinstance (self.ppd, cups.PPD): - mname = self.ppd.findAttr ("modelName").value - make, model = cupshelpers.ppds.ppdMakeModelSplit (mname) - if make and model: - name = "%s %s" % (make, model) - elif make or model: - name = "%s%s" % (make, model) - except: - nonfatalException () + name = self._getSuggestedPrinterName () if name: descr = name diff --git a/test_newprinter.py b/test_newprinter.py index bbd5a35fc..c1d68050a 100644 --- a/test_newprinter.py +++ b/test_newprinter.py @@ -2626,6 +2626,31 @@ def test_duplicate_mode_page_order(): ] +def test_make_name_unique_uses_incrementing_collision_suffixes(): + np = get_dummy_gui() + np.printers = {} + assert np.makeNameUnique("printer") == "printer" + + existing = type("ExistingPrinter", (), { + "discovered": False, + "name": "printer", + })() + np.printers = {"printer": existing} + assert np.makeNameUnique("printer") == "printer-2" + + np.printers["printer-1"] = type("ExistingPrinter", (), { + "discovered": False, + "name": "printer-1", + })() + assert np.makeNameUnique("printer") == "printer-2" + + np.printers["printer-2"] = type("ExistingPrinter", (), { + "discovered": False, + "name": "printer-2", + })() + assert np.makeNameUnique("printer") == "printer-3" + + def test_duplicate_initialisation_preserves_metadata_and_original_ppd(monkeypatch): monkeypatch.setattr(cups, "PPD", MockCupsPPD) np = get_dummy_gui() @@ -2635,7 +2660,7 @@ def test_duplicate_initialisation_preserves_metadata_and_original_ppd(monkeypatc np._location = "Reception" np._is_shared = True np.orig_ppd = MockCupsPPD({"NickName": "Xerox B235 MFP"}) - np.makeNameUnique = MagicMock(return_value="Office-Printer_copy") + np.makeNameUnique = MagicMock(return_value="Office-Printer") np.entNPName = MagicMock() np.entNPLocation = MagicMock() np.entNPDescription = MagicMock() @@ -2653,11 +2678,12 @@ def test_duplicate_initialisation_preserves_metadata_and_original_ppd(monkeypatc assert np.ppd is np.orig_ppd assert np.device.id == "" assert np.device.make_and_model == "" - np.entNPName.set_text.assert_called_with("Office-Printer_copy") + np.entNPName.set_text.assert_called_with("Office-Printer") np.entNPDescription.set_text.assert_called_with("Original description") np.entNPLocation.set_text.assert_called_with("Reception") np.isSharedCbx.set_active.assert_called_with(True) np.entNPDriver.set_text.assert_called_with("Xerox B235 MFP") + np.makeNameUnique.assert_called_with("Office Printer") np.ntbkNewPrinter.set_current_page.assert_called_with( np.PAGE_DESCRIBE_PRINTER) @@ -2677,7 +2703,7 @@ def test_duplicate_initialisation_reuses_discovered_device_identity(monkeypatch) "CMD:PCLM,PS,PWGRaster;", "device-info": "Xerox printer (driverless)", "device-make-and-model": "Xerox Xerox(R) B235 MFP"}) - np.makeNameUnique = MagicMock(return_value="Office-Printer_copy") + np.makeNameUnique = MagicMock(return_value="Office-Printer") np.entNPName = MagicMock() np.entNPLocation = MagicMock() np.entNPDescription = MagicMock() @@ -2699,6 +2725,39 @@ def test_duplicate_initialisation_reuses_discovered_device_identity(monkeypatch) assert np.device.id_dict["CMD"] == ["PCLM", "PS", "PWGRaster"] +def test_duplicate_uses_stable_device_name_for_suffixed_source(monkeypatch): + monkeypatch.setattr(cups, "PPD", MockCupsPPD) + np = get_dummy_gui() + np._name = "Xerox-Xerox(R)-B235-MFP-2" + np._device_uri = "ipps://Xerox%20B235._ipps._tcp.local" + np._description = "Original description" + np._location = "Reception" + np._is_shared = True + np.orig_ppd = MockCupsPPD({"NickName": "Xerox B235 MFP"}) + np.discovered_device = cupshelpers.Device( + np._device_uri, + **{"device-id": "MFG:Xerox;MDL:Xerox(R) B235 MFP;", + "device-make-and-model": "Xerox Xerox(R) B235 MFP"}) + np.makeNameUnique = MagicMock(return_value="Xerox-Xerox(R)-B235-MFP-4") + np.entNPName = MagicMock() + np.entNPLocation = MagicMock() + np.entNPDescription = MagicMock() + np.entNPDriver = MagicMock() + np.entSMBURI = MagicMock() + np.entSMBUsername = MagicMock() + np.entSMBPassword = MagicMock() + np.isSharedCbx = MagicMock() + np.rbtnChangePPDKeepSettings = MagicMock() + np.NewPrinterWindow = MagicMock() + np.ntbkNewPrinter = MagicMock() + + np._initialiseDuplicateMode() + + np.makeNameUnique.assert_called_with("Xerox Xerox(R) B235 MFP") + np.entNPName.set_text.assert_called_with( + "Xerox-Xerox(R)-B235-MFP-4") + + def test_duplicate_initialisation_falls_back_without_fake_device_identity(monkeypatch): monkeypatch.setattr(cups, "PPD", MockCupsPPD) np = get_dummy_gui() @@ -2710,7 +2769,7 @@ def test_duplicate_initialisation_falls_back_without_fake_device_identity(monkey np.orig_ppd = MockCupsPPD({"NickName": "Original Driver"}) np.device_id = "" np.device_make_and_model = "" - np.makeNameUnique = MagicMock(return_value="Unavailable-Printer_copy") + np.makeNameUnique = MagicMock(return_value="Unavailable-Printer") np.entNPName = MagicMock() np.entNPLocation = MagicMock() np.entNPDescription = MagicMock() @@ -2743,7 +2802,7 @@ def test_duplicate_forward_uses_original_ppd_without_loading_or_matching(): np.btnNPBack = MagicMock() np.entNPName = MagicMock() np.entNPDriver = MagicMock() - np.entNPName.get_text.return_value = "Xerox_copy" + np.entNPName.get_text.return_value = "Xerox" np.printers = {} np.installable_options = False np.fillNPInstallableOptions = MagicMock( From 7d561ee2253112571c752b02f4d00c284974f01d Mon Sep 17 00:00:00 2001 From: SinghCod3r Date: Sun, 4 Oct 2026 15:15:26 +0530 Subject: [PATCH 3/4] Fixed Duplicate the Bumaga printer, the suggested printer name --- newprinter.py | 22 ++++++++++++++++++++-- test_newprinter.py | 41 ++++++++++++++++++++++++++++++++++------- 2 files changed, 54 insertions(+), 9 deletions(-) diff --git a/newprinter.py b/newprinter.py index 7b01e2b03..6b6b35236 100644 --- a/newprinter.py +++ b/newprinter.py @@ -721,6 +721,25 @@ def makeNameUnique(self, name): name += "-" + str (suffix) return name + def _makeDuplicateNameUnique(self, name): + """Make a duplicate queue name from the existing queue name.""" + name = name.replace (" ", "-") + name = name.replace ("/", "-") + name = name.replace ("#", "-") + match = re.search (r"-(\d+)$", name) + if match: + suffix = int (match.group (1)) + 1 + name = name[:match.start ()] + else: + suffix = 1 + + while not checkNPName (self.printers, name + "-" + str (suffix)): + suffix += 1 + if suffix == 100: + break + + return name + "-" + str (suffix) + def destroy (self): self.emit ('destroy') @@ -889,8 +908,7 @@ def _initialiseDuplicateMode (self): _get_driver_name_from_ppd (self.ppd, self.ppds)) self.entNPDriver.get_style_context().add_class ("readonly") - base_name = self._getSuggestedPrinterName () or self._name - self.entNPName.set_text (self.makeNameUnique (base_name)) + self.entNPName.set_text (self._makeDuplicateNameUnique (self._name)) self.NewPrinterWindow.set_title (_("Duplicate Printer")) self.rbtnChangePPDKeepSettings.set_active (True) diff --git a/test_newprinter.py b/test_newprinter.py index c1d68050a..4a3f99e1f 100644 --- a/test_newprinter.py +++ b/test_newprinter.py @@ -33,6 +33,11 @@ class MockPrinter: def __init__(self, uri): self.device_uri = uri +class MockExistingPrinter: + def __init__(self, name): + self.discovered = False + self.name = name + class MockDevice: def __init__(self, uri): self.uri = uri @@ -2651,6 +2656,26 @@ def test_make_name_unique_uses_incrementing_collision_suffixes(): assert np.makeNameUnique("printer") == "printer-3" +@pytest.mark.parametrize("source_name,existing_names,expected_name", [ + ("Boomaga", ["Boomaga"], "Boomaga-1"), + ("Boomaga", ["Boomaga", "Boomaga-1"], "Boomaga-2"), + ("Boomaga", ["Boomaga", "Boomaga-1", "Boomaga-2"], "Boomaga-3"), + ("Boomaga-2", ["Boomaga-2"], "Boomaga-3"), + ("Boomaga-2", ["Boomaga-2", "Boomaga-3"], "Boomaga-4"), + ("Xerox-Xerox(R)-B235-MFP", ["Xerox-Xerox(R)-B235-MFP"], + "Xerox-Xerox(R)-B235-MFP-1"), +]) +def test_duplicate_name_uses_existing_queue_name(source_name, existing_names, + expected_name): + np = get_dummy_gui() + np.printers = { + name: MockExistingPrinter(name) + for name in existing_names + } + + assert np._makeDuplicateNameUnique(source_name) == expected_name + + def test_duplicate_initialisation_preserves_metadata_and_original_ppd(monkeypatch): monkeypatch.setattr(cups, "PPD", MockCupsPPD) np = get_dummy_gui() @@ -2660,7 +2685,7 @@ def test_duplicate_initialisation_preserves_metadata_and_original_ppd(monkeypatc np._location = "Reception" np._is_shared = True np.orig_ppd = MockCupsPPD({"NickName": "Xerox B235 MFP"}) - np.makeNameUnique = MagicMock(return_value="Office-Printer") + np._makeDuplicateNameUnique = MagicMock(return_value="Office-Printer") np.entNPName = MagicMock() np.entNPLocation = MagicMock() np.entNPDescription = MagicMock() @@ -2683,7 +2708,7 @@ def test_duplicate_initialisation_preserves_metadata_and_original_ppd(monkeypatc np.entNPLocation.set_text.assert_called_with("Reception") np.isSharedCbx.set_active.assert_called_with(True) np.entNPDriver.set_text.assert_called_with("Xerox B235 MFP") - np.makeNameUnique.assert_called_with("Office Printer") + np._makeDuplicateNameUnique.assert_called_with("Office Printer") np.ntbkNewPrinter.set_current_page.assert_called_with( np.PAGE_DESCRIBE_PRINTER) @@ -2703,7 +2728,7 @@ def test_duplicate_initialisation_reuses_discovered_device_identity(monkeypatch) "CMD:PCLM,PS,PWGRaster;", "device-info": "Xerox printer (driverless)", "device-make-and-model": "Xerox Xerox(R) B235 MFP"}) - np.makeNameUnique = MagicMock(return_value="Office-Printer") + np._makeDuplicateNameUnique = MagicMock(return_value="Office-Printer") np.entNPName = MagicMock() np.entNPLocation = MagicMock() np.entNPDescription = MagicMock() @@ -2723,9 +2748,10 @@ def test_duplicate_initialisation_reuses_discovered_device_identity(monkeypatch) assert np.device.id_dict["MFG"] == "Xerox" assert np.device.id_dict["MDL"] == "Xerox(R) B235 MFP" assert np.device.id_dict["CMD"] == ["PCLM", "PS", "PWGRaster"] + np._makeDuplicateNameUnique.assert_called_with("Office Printer") -def test_duplicate_uses_stable_device_name_for_suffixed_source(monkeypatch): +def test_duplicate_uses_queue_name_for_suffixed_source(monkeypatch): monkeypatch.setattr(cups, "PPD", MockCupsPPD) np = get_dummy_gui() np._name = "Xerox-Xerox(R)-B235-MFP-2" @@ -2738,7 +2764,8 @@ def test_duplicate_uses_stable_device_name_for_suffixed_source(monkeypatch): np._device_uri, **{"device-id": "MFG:Xerox;MDL:Xerox(R) B235 MFP;", "device-make-and-model": "Xerox Xerox(R) B235 MFP"}) - np.makeNameUnique = MagicMock(return_value="Xerox-Xerox(R)-B235-MFP-4") + np._makeDuplicateNameUnique = MagicMock( + return_value="Xerox-Xerox(R)-B235-MFP-4") np.entNPName = MagicMock() np.entNPLocation = MagicMock() np.entNPDescription = MagicMock() @@ -2753,7 +2780,7 @@ def test_duplicate_uses_stable_device_name_for_suffixed_source(monkeypatch): np._initialiseDuplicateMode() - np.makeNameUnique.assert_called_with("Xerox Xerox(R) B235 MFP") + np._makeDuplicateNameUnique.assert_called_with("Xerox-Xerox(R)-B235-MFP-2") np.entNPName.set_text.assert_called_with( "Xerox-Xerox(R)-B235-MFP-4") @@ -2769,7 +2796,7 @@ def test_duplicate_initialisation_falls_back_without_fake_device_identity(monkey np.orig_ppd = MockCupsPPD({"NickName": "Original Driver"}) np.device_id = "" np.device_make_and_model = "" - np.makeNameUnique = MagicMock(return_value="Unavailable-Printer") + np._makeDuplicateNameUnique = MagicMock(return_value="Unavailable-Printer") np.entNPName = MagicMock() np.entNPLocation = MagicMock() np.entNPDescription = MagicMock() From 45ad52ec090bae101ed70799d179d4b75ec1af8a Mon Sep 17 00:00:00 2001 From: SinghCod3r Date: Sun, 4 Oct 2026 15:22:04 +0530 Subject: [PATCH 4/4] Generic Naming Test Case Added --- test_newprinter.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/test_newprinter.py b/test_newprinter.py index 4a3f99e1f..b249dab12 100644 --- a/test_newprinter.py +++ b/test_newprinter.py @@ -2657,6 +2657,13 @@ def test_make_name_unique_uses_incrementing_collision_suffixes(): @pytest.mark.parametrize("source_name,existing_names,expected_name", [ + ("Printer", ["Printer"], "Printer-1"), + ("Printer", ["Printer", "Printer-1", "Printer-2"], "Printer-3"), + ("Printer-2", ["Printer-2"], "Printer-3"), + ("Printer-2", ["Printer-2", "Printer-3"], "Printer-4"), + ("Printer2", ["Printer2"], "Printer2-1"), + ("Printer123", ["Printer123"], "Printer123-1"), + ("Printer-2024-A", ["Printer-2024-A"], "Printer-2024-A-1"), ("Boomaga", ["Boomaga"], "Boomaga-1"), ("Boomaga", ["Boomaga", "Boomaga-1"], "Boomaga-2"), ("Boomaga", ["Boomaga", "Boomaga-1", "Boomaga-2"], "Boomaga-3"),