2 # -*- coding: utf-8 -*-
21 import urllib.request as compat_urllib_request
22 except ImportError: # Python 2
23 import urllib2 as compat_urllib_request
26 import urllib.error as compat_urllib_error
27 except ImportError: # Python 2
28 import urllib2 as compat_urllib_error
31 import urllib.parse as compat_urllib_parse
32 except ImportError: # Python 2
33 import urllib as compat_urllib_parse
36 from urllib.parse import urlparse as compat_urllib_parse_urlparse
37 except ImportError: # Python 2
38 from urlparse import urlparse as compat_urllib_parse_urlparse
41 import urllib.parse as compat_urlparse
42 except ImportError: # Python 2
43 import urlparse as compat_urlparse
46 import http.cookiejar as compat_cookiejar
47 except ImportError: # Python 2
48 import cookielib as compat_cookiejar
51 import html.entities as compat_html_entities
52 except ImportError: # Python 2
53 import htmlentitydefs as compat_html_entities
56 import html.parser as compat_html_parser
57 except ImportError: # Python 2
58 import HTMLParser as compat_html_parser
61 import http.client as compat_http_client
62 except ImportError: # Python 2
63 import httplib as compat_http_client
66 from urllib.error import HTTPError as compat_HTTPError
67 except ImportError: # Python 2
68 from urllib2 import HTTPError as compat_HTTPError
71 from urllib.request import urlretrieve as compat_urlretrieve
72 except ImportError: # Python 2
73 from urllib import urlretrieve as compat_urlretrieve
77 from subprocess import DEVNULL
78 compat_subprocess_get_DEVNULL = lambda: DEVNULL
80 compat_subprocess_get_DEVNULL = lambda: open(os.path.devnull, 'w')
83 from urllib.parse import parse_qs as compat_parse_qs
84 except ImportError: # Python 2
85 # HACK: The following is the correct parse_qs implementation from cpython 3's stdlib.
86 # Python 2's version is apparently totally broken
87 def _unquote(string, encoding='utf-8', errors='replace'):
90 res = string.split('%')
97 # pct_sequence: contiguous sequence of percent-encoded bytes, decoded
104 pct_sequence += item[:2].decode('hex')
107 # This segment was just a single percent-encoded character.
108 # May be part of a sequence of code units, so delay decoding.
109 # (Stored in pct_sequence).
113 # Encountered non-percent-encoded characters. Flush the current
115 string += pct_sequence.decode(encoding, errors) + rest
118 # Flush the final pct_sequence
119 string += pct_sequence.decode(encoding, errors)
122 def _parse_qsl(qs, keep_blank_values=False, strict_parsing=False,
123 encoding='utf-8', errors='replace'):
124 qs, _coerce_result = qs, unicode
125 pairs = [s2 for s1 in qs.split('&') for s2 in s1.split(';')]
127 for name_value in pairs:
128 if not name_value and not strict_parsing:
130 nv = name_value.split('=', 1)
133 raise ValueError("bad query field: %r" % (name_value,))
134 # Handle case of a control-name with no equal sign
135 if keep_blank_values:
139 if len(nv[1]) or keep_blank_values:
140 name = nv[0].replace('+', ' ')
141 name = _unquote(name, encoding=encoding, errors=errors)
142 name = _coerce_result(name)
143 value = nv[1].replace('+', ' ')
144 value = _unquote(value, encoding=encoding, errors=errors)
145 value = _coerce_result(value)
146 r.append((name, value))
149 def compat_parse_qs(qs, keep_blank_values=False, strict_parsing=False,
150 encoding='utf-8', errors='replace'):
152 pairs = _parse_qsl(qs, keep_blank_values, strict_parsing,
153 encoding=encoding, errors=errors)
154 for name, value in pairs:
155 if name in parsed_result:
156 parsed_result[name].append(value)
158 parsed_result[name] = [value]
162 compat_str = unicode # Python 2
167 compat_chr = unichr # Python 2
172 if type(c) is int: return c
175 # This is not clearly defined otherwise
176 compiled_regex_type = type(re.compile(''))
179 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:10.0) Gecko/20100101 Firefox/10.0 (Chrome)',
180 'Accept-Charset': 'ISO-8859-1,utf-8;q=0.7,*;q=0.7',
181 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
182 'Accept-Encoding': 'gzip, deflate',
183 'Accept-Language': 'en-us,en;q=0.5',
186 def preferredencoding():
187 """Get preferred encoding.
189 Returns the best encoding scheme for the system, based on
190 locale.getpreferredencoding() and some further tweaks.
193 pref = locale.getpreferredencoding()
200 if sys.version_info < (3,0):
202 print(s.encode(preferredencoding(), 'xmlcharrefreplace'))
205 assert type(s) == type(u'')
208 # In Python 2.x, json.dump expects a bytestream.
209 # In Python 3.x, it writes to a character stream
210 if sys.version_info < (3,0):
211 def write_json_file(obj, fn):
212 with open(fn, 'wb') as f:
215 def write_json_file(obj, fn):
216 with open(fn, 'w', encoding='utf-8') as f:
219 if sys.version_info >= (2,7):
220 def find_xpath_attr(node, xpath, key, val):
221 """ Find the xpath xpath[@key=val] """
222 assert re.match(r'^[a-zA-Z]+$', key)
223 assert re.match(r'^[a-zA-Z0-9@\s]*$', val)
224 expr = xpath + u"[@%s='%s']" % (key, val)
225 return node.find(expr)
227 def find_xpath_attr(node, xpath, key, val):
228 for f in node.findall(xpath):
229 if f.attrib.get(key) == val:
233 def htmlentity_transform(matchobj):
234 """Transforms an HTML entity to a character.
236 This function receives a match object and is intended to be used with
237 the re.sub() function.
239 entity = matchobj.group(1)
241 # Known non-numeric HTML entity
242 if entity in compat_html_entities.name2codepoint:
243 return compat_chr(compat_html_entities.name2codepoint[entity])
245 mobj = re.match(u'(?u)#(x?\\d+)', entity)
247 numstr = mobj.group(1)
248 if numstr.startswith(u'x'):
250 numstr = u'0%s' % numstr
253 return compat_chr(int(numstr, base))
255 # Unknown entity in name, return its literal representation
256 return (u'&%s;' % entity)
258 compat_html_parser.locatestarttagend = re.compile(r"""<[a-zA-Z][-.a-zA-Z0-9:_]*(?:\s+(?:(?<=['"\s])[^\s/>][^\s/=>]*(?:\s*=+\s*(?:'[^']*'|"[^"]*"|(?!['"])[^>\s]*))?\s*)*)?\s*""", re.VERBOSE) # backport bugfix
259 class BaseHTMLParser(compat_html_parser.HTMLParser):
261 compat_html_parser.HTMLParser.__init__(self)
264 def loads(self, html):
269 class AttrParser(BaseHTMLParser):
270 """Modified HTMLParser that isolates a tag with the specified attribute"""
271 def __init__(self, attribute, value):
272 self.attribute = attribute
277 self.watch_startpos = False
279 BaseHTMLParser.__init__(self)
281 def error(self, message):
282 if self.error_count > 10 or self.started:
283 raise compat_html_parser.HTMLParseError(message, self.getpos())
284 self.rawdata = '\n'.join(self.html.split('\n')[self.getpos()[0]:]) # skip one line
285 self.error_count += 1
288 def handle_starttag(self, tag, attrs):
291 self.find_startpos(None)
292 if self.attribute in attrs and attrs[self.attribute] == self.value:
295 self.watch_startpos = True
297 if not tag in self.depth: self.depth[tag] = 0
300 def handle_endtag(self, tag):
302 if tag in self.depth: self.depth[tag] -= 1
303 if self.depth[self.result[0]] == 0:
305 self.result.append(self.getpos())
307 def find_startpos(self, x):
308 """Needed to put the start position of the result (self.result[1])
309 after the opening tag with the requested id"""
310 if self.watch_startpos:
311 self.watch_startpos = False
312 self.result.append(self.getpos())
313 handle_entityref = handle_charref = handle_data = handle_comment = \
314 handle_decl = handle_pi = unknown_decl = find_startpos
316 def get_result(self):
317 if self.result is None:
319 if len(self.result) != 3:
321 lines = self.html.split('\n')
322 lines = lines[self.result[1][0]-1:self.result[2][0]]
323 lines[0] = lines[0][self.result[1][1]:]
325 lines[-1] = lines[-1][:self.result[2][1]-self.result[1][1]]
326 lines[-1] = lines[-1][:self.result[2][1]]
327 return '\n'.join(lines).strip()
328 # Hack for https://github.com/rg3/youtube-dl/issues/662
329 if sys.version_info < (2, 7, 3):
330 AttrParser.parse_endtag = (lambda self, i:
331 i + len("</scr'+'ipt>")
332 if self.rawdata[i:].startswith("</scr'+'ipt>")
333 else compat_html_parser.HTMLParser.parse_endtag(self, i))
335 def get_element_by_id(id, html):
336 """Return the content of the tag with the specified ID in the passed HTML document"""
337 return get_element_by_attribute("id", id, html)
339 def get_element_by_attribute(attribute, value, html):
340 """Return the content of the tag with the specified attribute in the passed HTML document"""
341 parser = AttrParser(attribute, value)
344 except compat_html_parser.HTMLParseError:
346 return parser.get_result()
348 class MetaParser(BaseHTMLParser):
350 Modified HTMLParser that isolates a meta tag with the specified name
353 def __init__(self, name):
354 BaseHTMLParser.__init__(self)
359 def handle_starttag(self, tag, attrs):
363 if attrs.get('name') == self.name:
364 self.result = attrs.get('content')
366 def get_result(self):
369 def get_meta_content(name, html):
371 Return the content attribute from the meta tag with the given name attribute.
373 parser = MetaParser(name)
376 except compat_html_parser.HTMLParseError:
378 return parser.get_result()
381 def clean_html(html):
382 """Clean an HTML snippet into a readable string"""
384 html = html.replace('\n', ' ')
385 html = re.sub(r'\s*<\s*br\s*/?\s*>\s*', '\n', html)
386 html = re.sub(r'<\s*/\s*p\s*>\s*<\s*p[^>]*>', '\n', html)
388 html = re.sub('<.*?>', '', html)
389 # Replace html entities
390 html = unescapeHTML(html)
394 def sanitize_open(filename, open_mode):
395 """Try to open the given filename, and slightly tweak it if this fails.
397 Attempts to open the given filename. If this fails, it tries to change
398 the filename slightly, step by step, until it's either able to open it
399 or it fails and raises a final exception, like the standard open()
402 It returns the tuple (stream, definitive_file_name).
406 if sys.platform == 'win32':
408 msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
409 return (sys.stdout.buffer if hasattr(sys.stdout, 'buffer') else sys.stdout, filename)
410 stream = open(encodeFilename(filename), open_mode)
411 return (stream, filename)
412 except (IOError, OSError) as err:
413 if err.errno in (errno.EACCES,):
416 # In case of error, try to remove win32 forbidden chars
417 alt_filename = os.path.join(
418 re.sub(u'[/<>:"\\|\\\\?\\*]', u'#', path_part)
419 for path_part in os.path.split(filename)
421 if alt_filename == filename:
424 # An exception here should be caught in the caller
425 stream = open(encodeFilename(filename), open_mode)
426 return (stream, alt_filename)
429 def timeconvert(timestr):
430 """Convert RFC 2822 defined time string into system timestamp"""
432 timetuple = email.utils.parsedate_tz(timestr)
433 if timetuple is not None:
434 timestamp = email.utils.mktime_tz(timetuple)
437 def sanitize_filename(s, restricted=False, is_id=False):
438 """Sanitizes a string so it could be used as part of a filename.
439 If restricted is set, use a stricter subset of allowed characters.
440 Set is_id if this is not an arbitrary string, but an ID that should be kept if possible
442 def replace_insane(char):
443 if char == '?' or ord(char) < 32 or ord(char) == 127:
446 return '' if restricted else '\''
448 return '_-' if restricted else ' -'
449 elif char in '\\/|*<>':
451 if restricted and (char in '!&\'()[]{}$;`^,#' or char.isspace()):
453 if restricted and ord(char) > 127:
457 result = u''.join(map(replace_insane, s))
459 while '__' in result:
460 result = result.replace('__', '_')
461 result = result.strip('_')
462 # Common case of "Foreign band name - English song title"
463 if restricted and result.startswith('-_'):
469 def orderedSet(iterable):
470 """ Remove all duplicates from the input iterable """
481 assert type(s) == type(u'')
483 result = re.sub(u'(?u)&(.+?);', htmlentity_transform, s)
486 def encodeFilename(s):
488 @param s The name of the file
491 assert type(s) == type(u'')
493 # Python 3 has a Unicode API
494 if sys.version_info >= (3, 0):
497 if sys.platform == 'win32' and sys.getwindowsversion()[0] >= 5:
498 # Pass u'' directly to use Unicode APIs on Windows 2000 and up
499 # (Detecting Windows NT 4 is tricky because 'major >= 4' would
500 # match Windows 9x series as well. Besides, NT 4 is obsolete.)
503 encoding = sys.getfilesystemencoding()
506 return s.encode(encoding, 'ignore')
508 def decodeOption(optval):
511 if isinstance(optval, bytes):
512 optval = optval.decode(preferredencoding())
514 assert isinstance(optval, compat_str)
517 def formatSeconds(secs):
519 return '%d:%02d:%02d' % (secs // 3600, (secs % 3600) // 60, secs % 60)
521 return '%d:%02d' % (secs // 60, secs % 60)
525 def make_HTTPS_handler(opts):
526 if sys.version_info < (3,2):
527 # Python's 2.x handler is very simplistic
528 return compat_urllib_request.HTTPSHandler()
531 context = ssl.SSLContext(ssl.PROTOCOL_SSLv23)
532 context.set_default_verify_paths()
534 context.verify_mode = (ssl.CERT_NONE
535 if opts.no_check_certificate
536 else ssl.CERT_REQUIRED)
537 return compat_urllib_request.HTTPSHandler(context=context)
539 class ExtractorError(Exception):
540 """Error during info extraction."""
541 def __init__(self, msg, tb=None, expected=False, cause=None):
542 """ tb, if given, is the original traceback (so that it can be printed out).
543 If expected is set, this is a normal error message and most likely not a bug in youtube-dl.
546 if sys.exc_info()[0] in (compat_urllib_error.URLError, socket.timeout, UnavailableVideoError):
549 msg = msg + u'; please report this issue on https://yt-dl.org/bug . Be sure to call youtube-dl with the --verbose flag and include its complete output. Make sure you are using the latest version; type youtube-dl -U to update.'
550 super(ExtractorError, self).__init__(msg)
553 self.exc_info = sys.exc_info() # preserve original exception
556 def format_traceback(self):
557 if self.traceback is None:
559 return u''.join(traceback.format_tb(self.traceback))
562 class DownloadError(Exception):
563 """Download Error exception.
565 This exception may be thrown by FileDownloader objects if they are not
566 configured to continue on errors. They will contain the appropriate
569 def __init__(self, msg, exc_info=None):
570 """ exc_info, if given, is the original exception that caused the trouble (as returned by sys.exc_info()). """
571 super(DownloadError, self).__init__(msg)
572 self.exc_info = exc_info
575 class SameFileError(Exception):
576 """Same File exception.
578 This exception will be thrown by FileDownloader objects if they detect
579 multiple files would have to be downloaded to the same file on disk.
584 class PostProcessingError(Exception):
585 """Post Processing exception.
587 This exception may be raised by PostProcessor's .run() method to
588 indicate an error in the postprocessing task.
590 def __init__(self, msg):
593 class MaxDownloadsReached(Exception):
594 """ --max-downloads limit has been reached. """
598 class UnavailableVideoError(Exception):
599 """Unavailable Format exception.
601 This exception will be thrown when a video is requested
602 in a format that is not available for that video.
607 class ContentTooShortError(Exception):
608 """Content Too Short exception.
610 This exception may be raised by FileDownloader objects when a file they
611 download is too small for what the server announced first, indicating
612 the connection was probably interrupted.
618 def __init__(self, downloaded, expected):
619 self.downloaded = downloaded
620 self.expected = expected
622 class YoutubeDLHandler(compat_urllib_request.HTTPHandler):
623 """Handler for HTTP requests and responses.
625 This class, when installed with an OpenerDirector, automatically adds
626 the standard headers to every HTTP request and handles gzipped and
627 deflated responses from web servers. If compression is to be avoided in
628 a particular request, the original request in the program code only has
629 to include the HTTP header "Youtubedl-No-Compression", which will be
630 removed before making the real request.
632 Part of this code was copied from:
634 http://techknack.net/python-urllib2-handlers/
636 Andrew Rowls, the author of that code, agreed to release it to the
643 return zlib.decompress(data, -zlib.MAX_WBITS)
645 return zlib.decompress(data)
648 def addinfourl_wrapper(stream, headers, url, code):
649 if hasattr(compat_urllib_request.addinfourl, 'getcode'):
650 return compat_urllib_request.addinfourl(stream, headers, url, code)
651 ret = compat_urllib_request.addinfourl(stream, headers, url)
655 def http_request(self, req):
656 for h,v in std_headers.items():
660 if 'Youtubedl-no-compression' in req.headers:
661 if 'Accept-encoding' in req.headers:
662 del req.headers['Accept-encoding']
663 del req.headers['Youtubedl-no-compression']
664 if 'Youtubedl-user-agent' in req.headers:
665 if 'User-agent' in req.headers:
666 del req.headers['User-agent']
667 req.headers['User-agent'] = req.headers['Youtubedl-user-agent']
668 del req.headers['Youtubedl-user-agent']
671 def http_response(self, req, resp):
674 if resp.headers.get('Content-encoding', '') == 'gzip':
675 content = resp.read()
676 gz = gzip.GzipFile(fileobj=io.BytesIO(content), mode='rb')
678 uncompressed = io.BytesIO(gz.read())
679 except IOError as original_ioerror:
680 # There may be junk add the end of the file
681 # See http://stackoverflow.com/q/4928560/35070 for details
682 for i in range(1, 1024):
684 gz = gzip.GzipFile(fileobj=io.BytesIO(content[:-i]), mode='rb')
685 uncompressed = io.BytesIO(gz.read())
690 raise original_ioerror
691 resp = self.addinfourl_wrapper(uncompressed, old_resp.headers, old_resp.url, old_resp.code)
692 resp.msg = old_resp.msg
694 if resp.headers.get('Content-encoding', '') == 'deflate':
695 gz = io.BytesIO(self.deflate(resp.read()))
696 resp = self.addinfourl_wrapper(gz, old_resp.headers, old_resp.url, old_resp.code)
697 resp.msg = old_resp.msg
700 https_request = http_request
701 https_response = http_response
703 def unified_strdate(date_str):
704 """Return a string with the date in the format YYYYMMDD"""
707 date_str = date_str.replace(',',' ')
708 # %z (UTC offset) is only supported in python>=3.2
709 date_str = re.sub(r' (\+|-)[\d]*$', '', date_str)
710 format_expressions = [
718 '%Y-%m-%dT%H:%M:%SZ',
721 for expression in format_expressions:
723 upload_date = datetime.datetime.strptime(date_str, expression).strftime('%Y%m%d')
728 def determine_ext(url, default_ext=u'unknown_video'):
729 guess = url.partition(u'?')[0].rpartition(u'.')[2]
730 if re.match(r'^[A-Za-z0-9]+$', guess):
735 def subtitles_filename(filename, sub_lang, sub_format):
736 return filename.rsplit('.', 1)[0] + u'.' + sub_lang + u'.' + sub_format
738 def date_from_str(date_str):
740 Return a datetime object from a string in the format YYYYMMDD or
741 (now|today)[+-][0-9](day|week|month|year)(s)?"""
742 today = datetime.date.today()
743 if date_str == 'now'or date_str == 'today':
745 match = re.match('(now|today)(?P<sign>[+-])(?P<time>\d+)(?P<unit>day|week|month|year)(s)?', date_str)
746 if match is not None:
747 sign = match.group('sign')
748 time = int(match.group('time'))
751 unit = match.group('unit')
760 delta = datetime.timedelta(**{unit: time})
762 return datetime.datetime.strptime(date_str, "%Y%m%d").date()
764 class DateRange(object):
765 """Represents a time interval between two dates"""
766 def __init__(self, start=None, end=None):
767 """start and end must be strings in the format accepted by date"""
768 if start is not None:
769 self.start = date_from_str(start)
771 self.start = datetime.datetime.min.date()
773 self.end = date_from_str(end)
775 self.end = datetime.datetime.max.date()
776 if self.start > self.end:
777 raise ValueError('Date range: "%s" , the start date must be before the end date' % self)
780 """Returns a range that only contains the given day"""
782 def __contains__(self, date):
783 """Check if the date is in the range"""
784 if not isinstance(date, datetime.date):
785 date = date_from_str(date)
786 return self.start <= date <= self.end
788 return '%s - %s' % ( self.start.isoformat(), self.end.isoformat())
792 """ Returns the platform name as a compat_str """
793 res = platform.platform()
794 if isinstance(res, bytes):
795 res = res.decode(preferredencoding())
797 assert isinstance(res, compat_str)
801 def write_string(s, out=None):
804 assert type(s) == type(u'')
806 if ('b' in getattr(out, 'mode', '') or
807 sys.version_info[0] < 3): # Python 2 lies about mode of sys.stderr
808 s = s.encode(preferredencoding(), 'ignore')
813 def bytes_to_intlist(bs):
816 if isinstance(bs[0], int): # Python 3
819 return [ord(c) for c in bs]
822 def intlist_to_bytes(xs):
825 if isinstance(chr(0), bytes): # Python 2
826 return ''.join([chr(x) for x in xs])
831 def get_cachedir(params={}):
832 cache_root = os.environ.get('XDG_CACHE_HOME',
833 os.path.expanduser('~/.cache'))
834 return params.get('cachedir', os.path.join(cache_root, 'youtube-dl'))
837 # Cross-platform file locking
838 if sys.platform == 'win32':
839 import ctypes.wintypes
842 class OVERLAPPED(ctypes.Structure):
844 ('Internal', ctypes.wintypes.LPVOID),
845 ('InternalHigh', ctypes.wintypes.LPVOID),
846 ('Offset', ctypes.wintypes.DWORD),
847 ('OffsetHigh', ctypes.wintypes.DWORD),
848 ('hEvent', ctypes.wintypes.HANDLE),
851 kernel32 = ctypes.windll.kernel32
852 LockFileEx = kernel32.LockFileEx
853 LockFileEx.argtypes = [
854 ctypes.wintypes.HANDLE, # hFile
855 ctypes.wintypes.DWORD, # dwFlags
856 ctypes.wintypes.DWORD, # dwReserved
857 ctypes.wintypes.DWORD, # nNumberOfBytesToLockLow
858 ctypes.wintypes.DWORD, # nNumberOfBytesToLockHigh
859 ctypes.POINTER(OVERLAPPED) # Overlapped
861 LockFileEx.restype = ctypes.wintypes.BOOL
862 UnlockFileEx = kernel32.UnlockFileEx
863 UnlockFileEx.argtypes = [
864 ctypes.wintypes.HANDLE, # hFile
865 ctypes.wintypes.DWORD, # dwReserved
866 ctypes.wintypes.DWORD, # nNumberOfBytesToLockLow
867 ctypes.wintypes.DWORD, # nNumberOfBytesToLockHigh
868 ctypes.POINTER(OVERLAPPED) # Overlapped
870 UnlockFileEx.restype = ctypes.wintypes.BOOL
871 whole_low = 0xffffffff
872 whole_high = 0x7fffffff
874 def _lock_file(f, exclusive):
875 overlapped = OVERLAPPED()
876 overlapped.Offset = 0
877 overlapped.OffsetHigh = 0
878 overlapped.hEvent = 0
879 f._lock_file_overlapped_p = ctypes.pointer(overlapped)
880 handle = msvcrt.get_osfhandle(f.fileno())
881 if not LockFileEx(handle, 0x2 if exclusive else 0x0, 0,
882 whole_low, whole_high, f._lock_file_overlapped_p):
883 raise OSError('Locking file failed: %r' % ctypes.FormatError())
886 assert f._lock_file_overlapped_p
887 handle = msvcrt.get_osfhandle(f.fileno())
888 if not UnlockFileEx(handle, 0,
889 whole_low, whole_high, f._lock_file_overlapped_p):
890 raise OSError('Unlocking file failed: %r' % ctypes.FormatError())
895 def _lock_file(f, exclusive):
896 fcntl.lockf(f, fcntl.LOCK_EX if exclusive else fcntl.LOCK_SH)
899 fcntl.lockf(f, fcntl.LOCK_UN)
902 class locked_file(object):
903 def __init__(self, filename, mode, encoding=None):
904 assert mode in ['r', 'a', 'w']
905 self.f = io.open(filename, mode, encoding=encoding)
909 exclusive = self.mode != 'r'
911 _lock_file(self.f, exclusive)
917 def __exit__(self, etype, value, traceback):
926 def write(self, *args):
927 return self.f.write(*args)
929 def read(self, *args):
930 return self.f.read(*args)
933 def shell_quote(args):
934 return ' '.join(map(pipes.quote, args))