1 # -*- coding: utf-8 -*-
3 # (c) Copyright 2003-2009 Hewlett-Packard Development Company, L.P.
5 # This program is free software; you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation; either version 2 of the License, or
8 # (at your option) any later version.
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
15 # You should have received a copy of the GNU General Public License
16 # along with this program; if not, write to the Free Software
17 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
34 from base import utils, models
36 INVALID_PRINTER_NAME_CHARS = """~`!@#$%^&*()=+[]{}()\\/,.<>?'\";:| """
38 # Handle case where cups.py (via device.py) is loaded
39 # and cupsext doesn't exist yet. This happens in the
40 # installer and in a fresh sandbox if the Python extensions
41 # aren't installed yet.
43 current_language = os.getenv("LANG")
46 # this is a workaround due CUPS rejecting all encoding except ASCII
48 # if the locale contains the encoding, switch to the same locale,
49 # but with utf-8 encoding. Otherwise use C locale.
50 if current_language is not None and current_language.count('.'):
51 newlang, encoding = current_language.split('.')
54 os.environ['LANG'] = newlang
56 # the same works for LC_CTYPE, in case it's not set
57 current_ctype = os.getenv("LC_CTYPE")
60 if current_ctype is not None and current_ctype.count('.'):
61 newctype, encoding = current_ctype.split('.')
64 os.environ['LC_CTYPE'] = newctype
68 # restore the old env values
69 if current_ctype is not None:
70 os.environ['LC_CTYPE'] = current_ctype
72 if current_language is not None:
73 os.environ['LANG'] = current_language
76 if not os.getenv("HPLIP_BUILD"):
77 log.warn("CUPSEXT could not be loaded. Please check HPLIP installation.")
81 IPP_PRINTER_STATE_IDLE = 3
82 IPP_PRINTER_STATE_PROCESSING = 4
83 IPP_PRINTER_STATE_STOPPED = 5
85 # Std CUPS option types
86 PPD_UI_BOOLEAN = 0 # True or False option
87 PPD_UI_PICKONE = 1 # Pick one from a list
88 PPD_UI_PICKMANY = 2 # Pick zero or more from a list
91 UI_SPINNER = 100 # Simple spinner with opt. suffix (ie, %)
92 UI_UNITS_SPINNER = 101 # Spinner control w/pts, cm, in, etc. units (not impl.)
93 UI_BANNER_JOB_SHEETS = 102 # dual combos for banner job-sheets
94 UI_PAGE_RANGE = 103 # Radio + page range entry field
96 # Non-std: Job storage
97 UI_JOB_STORAGE_MODE = 104 # Combo w/linkage
98 UI_JOB_STORAGE_PIN = 105 # Radios w/PIN entry
99 UI_JOB_STORAGE_USERNAME = 106 # Radios w/text entry
100 UI_JOB_STORAGE_ID = 107 # Radios w/text entry
101 UI_JOB_STORAGE_ID_EXISTS = 108 # Combo
103 UI_INFO = 109 # Information field, required Information name and Value
106 IPP_PAUSE_PRINTER = 0x0010
107 IPP_RESUME_PRINTER = 0x011
108 IPP_PURGE_JOBS = 0x012
109 CUPS_GET_DEFAULT = 0x4001
110 CUPS_GET_PRINTERS = 0x4002
111 CUPS_ADD_MODIFY_PRINTER = 0x4003
112 CUPS_DELETE_PRINTER = 0x4004
113 CUPS_GET_CLASSES = 0x4005
114 CUPS_ADD_MODIFY_CLASS = 0x4006
115 CUPS_DELETE_CLASS = 0x4007
116 CUPS_ACCEPT_JOBS = 0x4008
117 CUPS_REJECT_JOBS = 0x4009
118 CUPS_SET_DEFAULT = 0x400a
119 CUPS_GET_DEVICES = 0x400b
120 CUPS_GET_PPDS = 0x400c
121 CUPS_MOVE_JOB = 0x400d
122 CUPS_AUTHENTICATE_JOB = 0x400e
125 IPP_JOB_PENDING = 3 # Job is waiting to be printed
126 IPP_JOB_HELD = 4 # Job is held for printing
127 IPP_JOB_PROCESSING = 5 # Job is currently printing
128 IPP_JOB_STOPPED = 6 # Job has been stopped
129 IPP_JOB_CANCELLED = 7 # Job has been cancelled
130 IPP_JOB_ABORTED = 8 # Job has aborted due to error
131 IPP_JOB_COMPLETED = 8 # Job has completed successfully
134 IPP_OK = 0x0000 # successful-ok
135 IPP_OK_SUBST = 0x001 # successful-ok-ignored-or-substituted-attributes
136 IPP_OK_CONFLICT = 0x002 # successful-ok-conflicting-attributes
137 IPP_OK_IGNORED_SUBSCRIPTIONS = 0x003 # successful-ok-ignored-subscriptions
138 IPP_OK_IGNORED_NOTIFICATIONS = 0x004 # successful-ok-ignored-notifications
139 IPP_OK_TOO_MANY_EVENTS = 0x005 # successful-ok-too-many-events
140 IPP_OK_BUT_CANCEL_SUBSCRIPTION = 0x006 # successful-ok-but-cancel-subscription
141 IPP_OK_EVENTS_COMPLETE = 0x007 # successful-ok-events-complete
142 IPP_REDIRECTION_OTHER_SITE = 0x300
143 IPP_BAD_REQUEST = 0x0400 # client-error-bad-request
144 IPP_FORBIDDEN = 0x0401 # client-error-forbidden
145 IPP_NOT_AUTHENTICATED = 0x0402 # client-error-not-authenticated
146 IPP_NOT_AUTHORIZED = 0x0403 # client-error-not-authorized
147 IPP_NOT_POSSIBLE = 0x0404 # client-error-not-possible
148 IPP_TIMEOUT = 0x0405 # client-error-timeout
149 IPP_NOT_FOUND = 0x0406 # client-error-not-found
150 IPP_GONE = 0x0407 # client-error-gone
151 IPP_REQUEST_ENTITY = 0x0408 # client-error-request-entity-too-large
152 IPP_REQUEST_VALUE = 0x0409 # client-error-request-value-too-long
153 IPP_DOCUMENT_FORMAT = 0x040a # client-error-document-format-not-supported
154 IPP_ATTRIBUTES = 0x040b # client-error-attributes-or-values-not-supported
155 IPP_URI_SCHEME = 0x040c # client-error-uri-scheme-not-supported
156 IPP_CHARSET = 0x040d # client-error-charset-not-supported
157 IPP_CONFLICT = 0x040e # client-error-conflicting-attributes
158 IPP_COMPRESSION_NOT_SUPPORTED = 0x040f # client-error-compression-not-supported
159 IPP_COMPRESSION_ERROR = 0x0410 # client-error-compression-error
160 IPP_DOCUMENT_FORMAT_ERROR = 0x0411 # client-error-document-format-error
161 IPP_DOCUMENT_ACCESS_ERROR = 0x0412 # client-error-document-access-error
162 IPP_ATTRIBUTES_NOT_SETTABLE = 0x0413 # client-error-attributes-not-settable
163 IPP_IGNORED_ALL_SUBSCRIPTIONS = 0x0414 # client-error-ignored-all-subscriptions
164 IPP_TOO_MANY_SUBSCRIPTIONS = 0x0415 # client-error-too-many-subscriptions
165 IPP_IGNORED_ALL_NOTIFICATIONS = 0x0416 # client-error-ignored-all-notifications
166 IPP_PRINT_SUPPORT_FILE_NOT_FOUND = 0x0417 # client-error-print-support-file-not-found
167 IPP_INTERNAL_ERROR = 0x0500 # server-error-internal-error
168 IPP_OPERATION_NOT_SUPPORTED = 0x0501 # server-error-operation-not-supported
169 IPP_SERVICE_UNAVAILABLE = 0x0502 # server-error-service-unavailable
170 IPP_VERSION_NOT_SUPPORTED = 0x0503 # server-error-version-not-supported
171 IPP_DEVICE_ERROR = 0x0504 # server-error-device-error
172 IPP_TEMPORARY_ERROR = 0x0505 # server-error-temporary-error
173 IPP_NOT_ACCEPTING = 0x0506 # server-error-not-accepting-jobs
174 IPP_PRINTER_BUSY = 0x0507 # server-error-busy
175 IPP_ERROR_JOB_CANCELLED = 0x0508 # server-error-job-canceled
176 IPP_MULTIPLE_JOBS_NOT_SUPPORTED = 0x0509 # server-error-multiple-document-jobs-not-supported
177 IPP_PRINTER_IS_DEACTIVATED = 0x050a # server-error-printer-is-deactivated
179 CUPS_ERROR_BAD_NAME = 0x0f00
180 CUPS_ERROR_BAD_PARAMETERS = 0x0f01
182 nickname_pat = re.compile(r'''\*NickName:\s*\"(.*)"''', re.MULTILINE)
183 pat_cups_error_log = re.compile("""^loglevel\s?(debug|debug2|warn|info|error|none)""", re.I)
184 ppd_pat = re.compile(r'''.*hp-(.*?)(-.*)*\.ppd.*''', re.I)
188 def getPPDPath(addtional_paths=None):
190 Returns the CUPS ppd path (not the foomatic one under /usr/share/ppd).
191 Usually this is /usr/share/cups/model.
193 if addtional_paths is None:
196 search_paths = prop.ppd_search_path.split(';') + addtional_paths
198 for path in search_paths:
199 ppd_path = os.path.join(path, 'cups/model')
200 if os.path.exists(ppd_path):
204 def getAllowableMIMETypes():
206 Scan all /etc/cups/*.convs and /usr/share/cups/mime
207 files for allowable file formats.
210 allowable_mime_types = []
212 if os.path.exists("/etc/cups"):
213 paths.append("/etc/cups/*.convs")
214 if os.path.exists("/usr/share/cups/mime"):
215 paths.append("/usr/share/cups/mime/*.convs")
217 files.extend(glob.glob(path))
219 #log.debug( "Capturing allowable MIME types from: %s" % f )
220 conv_file = file(f, 'r')
222 for line in conv_file:
223 if not line.startswith("#") and len(line) > 1:
225 source, dest, cost, prog = line.split()
229 if source not in ('application/octet-stream', 'application/vnd.cups-postscript'):
230 allowable_mime_types.append(source)
232 # Add some well-known MIME types that may not appear in the .convs files
233 allowable_mime_types.append("image/x-bmp")
234 allowable_mime_types.append("text/cpp")
235 allowable_mime_types.append("application/x-python")
236 allowable_mime_types.append("application/hplip-fax")
238 return allowable_mime_types
241 def getPPDDescription(f):
242 if f.endswith('.gz'):
243 nickname = gzip.GzipFile(f, 'r').read(4096)
245 nickname = file(f, 'r').read(4096)
248 desc = nickname_pat.search(nickname).group(1)
249 except AttributeError:
256 major, minor, patch = getVersionTuple()
257 ppds = {} # {'ppd name' : 'desc', ...}
259 if major == 1 and minor < 2:
260 ppd_dir = sys_conf.get('dirs', 'ppd')
261 log.debug("(CUPS 1.1.x) Searching for PPDs in: %s" % ppd_dir)
263 for f in utils.walkFiles(ppd_dir, pattern="HP*ppd*;hp*ppd*", abs_paths=True):
264 desc = getPPDDescription(f)
266 if not ('foo2' in desc or
267 'gutenprint' in desc.lower() or
271 log.debug("%s: %s" % (f, desc))
274 log.debug("(CUPS 1.2.x) Getting list of PPDs using CUPS_GET_PPDS...")
275 ppd_dict = cupsext.getPPDList()
276 cups_ppd_path = getPPDPath() # usually /usr/share/cups/model
277 foomatic_ppd_path = sys_conf.get('dirs', 'ppdbase', '/usr/share/ppd')
279 if not foomatic_ppd_path or not os.path.exists(foomatic_ppd_path):
280 foomatic_ppd_path = '/usr/share/ppd'
282 log.debug("CUPS PPD base path = %s" % cups_ppd_path)
283 log.debug("Foomatic PPD base path = %s" % foomatic_ppd_path)
289 if 'hp-' in ppd.lower() or 'hp_' in ppd.lower() and \
290 ppd_dict[ppd]['ppd-make'] == 'HP':
292 desc = ppd_dict[ppd]['ppd-make-and-model']
294 if not ('foo2' in desc.lower() or
295 'gutenprint' in desc.lower() or
296 'gutenprint' in ppd):
298 # PPD files returned by CUPS_GET_PPDS (and by lpinfo -m)
299 # can be relative to /usr/share/ppd/ or to
300 # /usr/share/cups/model/. Not sure why this is.
301 # Here we will try both and see which one it is...
303 if os.path.exists(ppd):
307 path = os.path.join(foomatic_ppd_path, ppd)
308 except AttributeError: # happens on some boxes with provider: style ppds (foomatic: etc)
311 if not os.path.exists(path):
313 path = os.path.join(cups_ppd_path, ppd)
314 except AttributeError:
317 if not os.path.exists(path):
318 path = ppd # foomatic: or some other driver
321 #log.debug("%s: %s" % (path, desc))
326 ## TODO: Move this to CUPSEXT for better performance
327 def levenshtein_distance(a,b):
329 Calculates the Levenshtein distance between a and b.
330 Written by Magnus Lie Hetland.
332 n, m = len(a), len(b)
338 for i in range(1,m+1):
339 previous, current = current, [i]+[0]*m
341 for j in range(1,n+1):
342 add, delete = previous[j]+1, current[j-1]+1
343 change = previous[j-1]
348 current[j] = min(add, delete, change)
353 number_pat = re.compile(r""".*?(\d+)""", re.IGNORECASE)
355 STRIP_STRINGS2 = ['foomatic:', 'hp-', 'hp_', 'hp ', '.gz', '.ppd',
356 'drv:', '-pcl', '-pcl3', '-jetready',
357 '-zxs', '-zjs', '-ps', '-postscript',
358 '-jr', '-lidl', '-lidil', '-ldl', '-hpijs']
361 for p in models.TECH_CLASS_PDLS.values():
363 if pp not in STRIP_STRINGS2:
364 STRIP_STRINGS2.append(pp)
367 STRIP_STRINGS = STRIP_STRINGS2[:]
368 STRIP_STRINGS.extend(['-series', ' series', '_series'])
371 def stripModel2(model): # For new 2.8.10+ PPD find algorithm
372 model = model.lower()
374 for x in STRIP_STRINGS2:
375 model = model.replace(x, '')
380 def stripModel(model): # for old PPD find algorithm (removes "series" as well)
381 model = model.lower()
383 for x in STRIP_STRINGS:
384 model = model.replace(x, '')
389 def getPPDFile(stripped_model, ppds): # Old PPD find
391 Match up a model name to a PPD from a list of system PPD files.
393 log.debug("1st stage edit distance match")
396 min_edit_distance = sys.maxint
398 log.debug("Determining edit distance from %s (only showing edit distances < 4)..." % stripped_model)
400 t = stripModel(os.path.basename(f))
401 eds[f] = levenshtein_distance(stripped_model, t)
403 log.debug("dist('%s') = %d" % (t, eds[f]))
404 min_edit_distance = min(min_edit_distance, eds[f])
406 log.debug("Min. dist = %d" % min_edit_distance)
409 if eds[f] == min_edit_distance:
411 if os.path.basename(m) == os.path.basename(f):
412 break # File already in list possibly with different path (Ubuntu, etc)
418 if len(mins) > 1: # try pattern matching the model number
419 log.debug("2nd stage matching with model number")
422 model_number = number_pat.match(stripped_model).group(1)
423 model_number = int(model_number)
424 except AttributeError:
429 log.debug("model_number=%d" % model_number)
431 for x in range(3): # 1, 10, 100
433 log.debug("Factor = %d" % factor)
434 adj_model_number = int(model_number/factor)*factor
435 number_matching, match = 0, ''
439 mins_model_number = number_pat.match(os.path.basename(m)).group(1)
440 mins_model_number = int(mins_model_number)
441 log.debug("mins_model_number= %d" % mins_model_number)
442 except AttributeError:
447 mins_adj_model_number = int(mins_model_number/factor)*factor
448 log.debug("mins_adj_model_number=%d" % mins_adj_model_number)
449 log.debug("adj_model_number=%d" % adj_model_number)
451 if mins_adj_model_number == adj_model_number:
466 def getPPDFile2(stripped_model, ppds): # New PPD find
467 # This routine is for the new PPD naming scheme begun in 2.8.10
468 # and beginning with implementation in 2.8.12 (Qt4 hp-setup)
469 # hp-<model name from models.dat w/o beginning hp_>[-<pdl>][-<pdl>][...].ppd[.gz]
470 # 3.9.6: Added handling for hpijs vs. hpcups PPDs/DRVs
471 log.debug("Matching PPD list to model %s..." % stripped_model)
474 match = ppd_pat.match(f)
475 if match is not None:
476 if match.group(1) == stripped_model:
477 log.debug("Found match: %s" % f)
479 pdls = match.group(2).split('-')
480 except AttributeError:
483 if (prop.hpcups_build and 'hpijs' not in f) or \
484 ((prop.hpijs_build and 'hpijs' in pdls) or (prop.hpcups_build and 'hpijs' not in pdls)) or \
486 matches.append((f, [p for p in pdls if p and p != 'hpijs']))
489 num_matches = len(matches)
492 log.warn("No PPD found for model %s using new algorithm. Trying old algorithm..." % stripped_model)
493 matches2 = getPPDFile(stripModel(stripped_model), ppds).items()
495 num_matches2 = len(matches2)
497 for f, d in matches2:
498 match = ppd_pat.match(f)
499 if match is not None:
500 log.debug("Found match: %s" % f)
502 pdls = match.group(2).split('-')
503 except AttributeError:
506 if (prop.hpcups_build and 'hpijs' not in f) or \
507 ((prop.hpijs_build and 'hpijs' in pdls) or (prop.hpcups_build and 'hpijs' not in pdls)) or \
509 matches.append((f, [p for p in pdls if p and p != 'hpijs']))
512 num_matches = len(matches)
515 log.error("No PPD found for model %s using old algorithm." % stripModel(stripped_model))
518 elif num_matches == 1:
519 log.debug("One match found.")
520 return (matches[0][0], '')
523 log.debug("%d matches found. Selecting based on PDL: Host > PS > PCL/Other" % num_matches)
524 for p in [models.PDL_TYPE_HOST, models.PDL_TYPE_PS, models.PDL_TYPE_PCL]:
525 for f, pdl_list in matches:
527 # default to HOST-based PDLs, as newly supported PDLs will most likely be of this type
528 if models.PDL_TYPES.get(x, models.PDL_TYPE_HOST) == p:
529 log.debug("Selecting '-%s' PPD: %s" % (x, f))
532 # No specific PDL found, so just return 1st found PPD file
533 log.debug("No specific PDL located. Defaulting to first found PPD file.")
534 return (matches[0][0], '')
538 def getErrorLogLevel():
539 cups_conf = '/etc/cups/cupsd.conf'
541 f = file(cups_conf, 'r')
543 log.error("%s not found." % cups_conf)
545 log.error("%s: I/O error." % cups_conf)
548 m = pat_cups_error_log.match(l)
550 level = m.group(1).lower()
551 log.debug("CUPS error_log LogLevel: %s" % level)
554 log.debug("CUPS error_log LogLevel: unknown")
558 def getPrintJobErrorLog(job_id, max_lines=1000, cont_interval=5):
560 s = '[Job %d]' % job_id
561 #level = getErrorLogLevel()
562 cups_conf = '/var/log/cups/error_log'
564 #if level in ('debug', 'debug2'):
567 f = file(cups_conf, 'r')
568 except (IOError, OSError):
569 log.error("Could not open the CUPS error_log file: %s" % cups_conf)
573 if s in file(cups_conf, 'r').read():
574 queue = utils.Queue()
584 ret.append(queue.get())
586 ret.append(line.strip())
588 if len(ret) > max_lines:
593 queue.put(line.strip())
595 if len(queue) > cont_interval:
598 return '\n'.join(ret)
605 def getDefaultPrinter():
606 r = cupsext.getDefaultPrinter()
608 log.debug("The CUPS default printer is not set.")
611 def setDefaultPrinter(printer_name):
612 setPasswordPrompt("You do not have permission to set the default printer.")
613 return cupsext.setDefaultPrinter(printer_name)
615 def accept(printer_name):
616 setPasswordPrompt("You do not have permission to accept jobs on a printer queue.")
617 return controlPrinter(printer_name, CUPS_ACCEPT_JOBS)
619 def reject(printer_name):
620 setPasswordPrompt("You do not have permission to reject jobs on a printer queue.")
621 return controlPrinter(printer_name, CUPS_REJECT_JOBS)
623 def start(printer_name):
624 setPasswordPrompt("You do not have permission to start a printer queue.")
625 return controlPrinter(printer_name, IPP_RESUME_PRINTER)
627 def stop(printer_name):
628 setPasswordPrompt("You do not have permission to stop a printer queue.")
629 return controlPrinter(printer_name, IPP_PAUSE_PRINTER)
631 def purge(printer_name):
632 setPasswordPrompt("You do not have permission to purge jobs.")
633 return controlPrinter(printer_name, IPP_PURGE_JOBS)
635 def controlPrinter(printer_name, cups_op):
636 if cups_op in (CUPS_ACCEPT_JOBS, CUPS_REJECT_JOBS, IPP_PAUSE_PRINTER, IPP_RESUME_PRINTER, IPP_PURGE_JOBS):
637 return cupsext.controlPrinter(printer_name, cups_op)
641 def openPPD(printer):
645 return cupsext.openPPD(printer)
648 return cupsext.closePPD()
654 return cupsext.getPPD(printer)
656 def getPPDOption(option):
657 return cupsext.getPPDOption(option)
659 def getPPDPageSize():
660 return cupsext.getPPDPageSize()
664 ## p = cupsext.getPrinters()
668 ## pn = pp.name.decode('utf-8')
669 ## except UnicodeError:
675 return cupsext.getPrinters()
677 def getJobs(my_job=0, completed=0):
678 return cupsext.getJobs(my_job, completed)
680 def getAllJobs(my_job=0):
681 return cupsext.getJobs(my_job, 0) + cupsext.getJobs(my_job, 1)
684 return cupsext.getVersion()
686 def getVersionTuple():
687 return cupsext.getVersionTuple()
690 return cupsext.getServer()
692 def cancelJob(jobid, dest=None):
693 setPasswordPrompt("You do not have permission to cancel a job.")
695 return cupsext.cancelJob(dest, jobid)
697 jobs = cupsext.getJobs(0, 0)
700 return cupsext.cancelJob(j.dest, jobid)
705 return cupsext.resetOptions()
707 def addOption(option):
708 return cupsext.addOption(option)
711 return cupsext.getOptions()
713 def printFile(printer, filename, title):
714 if os.path.exists(filename):
715 printer = printer.encode('utf-8')
716 filename = filename.encode('utf-8')
717 title = title.encode('utf-8')
718 return cupsext.printFileWithOptions(printer, filename, title)
723 def addPrinter(printer_name, device_uri, location, ppd_file, model, info):
724 log.debug("addPrinter('%s', '%s', '%s', '%s', '%s', '%s')" %
725 ( printer_name, device_uri, location, ppd_file, model, info))
727 if ppd_file and not os.path.exists(ppd_file):
728 log.error("PPD file '%s' not found." % ppd_file)
729 return (-1, "PPD file not found")
731 return cupsext.addPrinter(printer_name, device_uri, location, ppd_file, model, info)
733 def delPrinter(printer_name):
734 setPasswordPrompt("You do not have permission to delete a printer.")
735 return cupsext.delPrinter(printer_name)
737 def enablePrinter(printer_name):
738 setPasswordPrompt("You do not have permission to enable a printer.")
739 cmd= "cupsenable %s"%printer_name
740 return os.system(cmd)
743 return cupsext.getGroupList()
746 return cupsext.getGroup(group)
748 def getOptionList(group):
749 return cupsext.getOptionList(group)
751 def getOption(group, option):
752 return cupsext.getOption(group, option)
754 def getChoiceList(group, option):
755 return cupsext.getChoiceList(group, option)
757 def getChoice(group, option, choice):
758 return cupsext.getChoice(group, option, choice)
761 return cupsext.setOptions()
763 def removeOption(option):
764 return cupsext.removeOption(option)
766 def setPasswordCallback(func):
767 return cupsext.setPasswordCallback(func)
769 def setPasswordPrompt(prompt):
770 return cupsext.setPasswordPrompt(prompt)
772 def findPPDAttribute(name, spec):
773 return cupsext.findPPDAttribute(name, spec)